Actualizar frontend/src/pages/Asesores.jsx
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
// src/pages/Asesores.jsx
|
// src/pages/Asesores.jsx
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback, Fragment } from "react";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { Loader, ErrorBox } from "../components/UI";
|
import { Loader, ErrorBox } from "../components/UI";
|
||||||
|
|
||||||
|
const MES_LARGO = ["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
|
||||||
|
const MES_CORTO = ["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"];
|
||||||
|
const calNavBtn = { width:26, height:26, borderRadius:7, border:"1px solid #e2e8f0", background:"#f8fafc", cursor:"pointer", fontSize:13, color:"#475569" };
|
||||||
|
|
||||||
const ESTADO_INFO = {
|
const ESTADO_INFO = {
|
||||||
online: { txt: "En Línea", color: "#10b981", bg: "#d1fae5", fg: "#065f46" },
|
online: { txt: "En Línea", color: "#10b981", bg: "#d1fae5", fg: "#065f46" },
|
||||||
busy: { txt: "Ocupado", color: "#f59e0b", bg: "#fef9c3", fg: "#854d0e" },
|
busy: { txt: "Ocupado", color: "#f59e0b", bg: "#fef9c3", fg: "#854d0e" },
|
||||||
@@ -17,11 +21,47 @@ export default function Asesores() {
|
|||||||
const [histAsesor, setHistAsesor] = useState(null); // { nombre, email }
|
const [histAsesor, setHistAsesor] = useState(null); // { nombre, email }
|
||||||
const [histFilas, setHistFilas] = useState([]);
|
const [histFilas, setHistFilas] = useState([]);
|
||||||
const [histLoading, setHistLoading] = useState(false);
|
const [histLoading, setHistLoading] = useState(false);
|
||||||
|
const [semanaBase, setSemanaBase] = useState(null); // Date del lunes de la semana mostrada
|
||||||
|
const [calAbierto, setCalAbierto] = useState(false); // popover del mini-calendario
|
||||||
|
const [calVista, setCalVista] = useState("dias"); // "dias" | "meses" | "anios"
|
||||||
|
const [calAno, setCalAno] = useState(new Date().getFullYear());
|
||||||
|
const [calMes, setCalMes] = useState(new Date().getMonth());
|
||||||
|
const [histVista, setHistVista] = useState("calendario"); // "calendario" | "lista"
|
||||||
|
const [listaDia, setListaDia] = useState(""); // filtro día en vista lista (YYYY-MM-DD)
|
||||||
|
const [listaCalAbierto, setListaCalAbierto] = useState(false);
|
||||||
|
const [listaCalVista, setListaCalVista] = useState("dias");
|
||||||
|
const [listaCalAno, setListaCalAno] = useState(new Date().getFullYear());
|
||||||
|
const [listaCalMes, setListaCalMes] = useState(new Date().getMonth());
|
||||||
|
|
||||||
|
// Lunes de la semana que contiene la fecha dada.
|
||||||
|
const lunesDe = (d) => {
|
||||||
|
const x = new Date(d);
|
||||||
|
const dia = (x.getDay() + 6) % 7; // 0=lunes ... 6=domingo
|
||||||
|
x.setHours(0, 0, 0, 0);
|
||||||
|
x.setDate(x.getDate() - dia);
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Eventos ordenados de más antiguo a más reciente.
|
||||||
|
const histOrden = histFilas.slice()
|
||||||
|
.sort((a, b) => (a.fecha_hora || "").localeCompare(b.fecha_hora || ""));
|
||||||
|
|
||||||
|
// Intervalos "estuvo conectado" [desde, hasta, enCurso] a partir de los ON/OFF.
|
||||||
|
const intervalosON = (() => {
|
||||||
|
const out = [];
|
||||||
|
let ini = null;
|
||||||
|
for (const h of histOrden) {
|
||||||
|
if (h.accion === "ON" && !ini) ini = new Date(h.fecha_hora);
|
||||||
|
else if (h.accion === "OFF" && ini) { out.push([ini, new Date(h.fecha_hora), false]); ini = null; }
|
||||||
|
}
|
||||||
|
if (ini) out.push([ini, new Date(), true]); // sigue conectado hasta ahora (en curso)
|
||||||
|
return out;
|
||||||
|
})();
|
||||||
|
|
||||||
async function verHistorial(ag) {
|
async function verHistorial(ag) {
|
||||||
const nombre = ag.name || ag.available_name || "—";
|
const nombre = ag.name || ag.available_name || "—";
|
||||||
setHistAsesor({ nombre, email: ag.email });
|
setHistAsesor({ nombre, email: ag.email });
|
||||||
setHistFilas([]); setHistLoading(true);
|
setHistFilas([]); setSemanaBase(lunesDe(new Date())); setHistLoading(true);
|
||||||
try {
|
try {
|
||||||
const r = await api.asesorHistorial(ag.email);
|
const r = await api.asesorHistorial(ag.email);
|
||||||
setHistFilas(r.historial || []);
|
setHistFilas(r.historial || []);
|
||||||
@@ -117,6 +157,21 @@ export default function Asesores() {
|
|||||||
<div style={{ fontSize:11.5, color:"#94a3b8", whiteSpace:"nowrap",
|
<div style={{ fontSize:11.5, color:"#94a3b8", whiteSpace:"nowrap",
|
||||||
overflow:"hidden", textOverflow:"ellipsis" }}>{ag.email || "—"}</div>
|
overflow:"hidden", textOverflow:"ellipsis" }}>{ag.email || "—"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Auto-offline (solo lectura) */}
|
||||||
|
{(() => {
|
||||||
|
const ao = ag.auto_offline === true || ag.auto_offline === "true";
|
||||||
|
return (
|
||||||
|
<span title="Desconexión automática por inactividad (Chatwoot)"
|
||||||
|
style={{ flexShrink:0, fontSize:10.5, fontWeight:700, borderRadius:999, padding:"3px 10px",
|
||||||
|
display:"inline-flex", alignItems:"center", gap:5,
|
||||||
|
background: ao ? "#fef3c7" : "#f1f5f9",
|
||||||
|
color: ao ? "#b45309" : "#64748b",
|
||||||
|
border: `1px solid ${ao ? "#fde68a" : "#e2e8f0"}` }}>
|
||||||
|
<span style={{ width:6, height:6, borderRadius:"50%", background: ao ? "#f59e0b" : "#94a3b8" }} />
|
||||||
|
Auto-offline: {ao ? "ON" : "OFF"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{/* Historial */}
|
{/* Historial */}
|
||||||
<button onClick={()=>verHistorial(ag)}
|
<button onClick={()=>verHistorial(ag)}
|
||||||
style={{ flexShrink:0, padding:"6px 12px", fontSize:11.5, fontWeight:600, borderRadius:8,
|
style={{ flexShrink:0, padding:"6px 12px", fontSize:11.5, fontWeight:600, borderRadius:8,
|
||||||
@@ -148,7 +203,7 @@ export default function Asesores() {
|
|||||||
style={{ position:"fixed", inset:0, background:"rgba(15,23,42,.55)", backdropFilter:"blur(2px)",
|
style={{ position:"fixed", inset:0, background:"rgba(15,23,42,.55)", backdropFilter:"blur(2px)",
|
||||||
display:"flex", alignItems:"center", justifyContent:"center", zIndex:1000, padding:16 }}>
|
display:"flex", alignItems:"center", justifyContent:"center", zIndex:1000, padding:16 }}>
|
||||||
<div onClick={(e)=>e.stopPropagation()}
|
<div onClick={(e)=>e.stopPropagation()}
|
||||||
style={{ background:"#fff", borderRadius:18, width:520, maxWidth:"94vw", maxHeight:"85vh",
|
style={{ background:"#fff", borderRadius:18, width:1000, maxWidth:"96vw", maxHeight:"92vh",
|
||||||
display:"flex", flexDirection:"column", overflow:"hidden",
|
display:"flex", flexDirection:"column", overflow:"hidden",
|
||||||
boxShadow:"0 30px 80px rgba(15,23,42,.4)", border:"1px solid #e2e8f0" }}>
|
boxShadow:"0 30px 80px rgba(15,23,42,.4)", border:"1px solid #e2e8f0" }}>
|
||||||
<div style={{ background:"linear-gradient(135deg, #1e3a5f 0%, #2d5580 100%)", color:"#fff",
|
<div style={{ background:"linear-gradient(135deg, #1e3a5f 0%, #2d5580 100%)", color:"#fff",
|
||||||
@@ -157,29 +212,348 @@ export default function Asesores() {
|
|||||||
<div style={{ fontWeight:700, fontSize:16 }}>📋 Historial de conexión</div>
|
<div style={{ fontWeight:700, fontSize:16 }}>📋 Historial de conexión</div>
|
||||||
<div style={{ fontSize:12.5, opacity:.85, marginTop:2 }}>{histAsesor.nombre}</div>
|
<div style={{ fontSize:12.5, opacity:.85, marginTop:2 }}>{histAsesor.nombre}</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={()=>setHistAsesor(null)} style={{ background:"rgba(255,255,255,.15)", border:"none", color:"#fff", width:30, height:30, borderRadius:8, fontSize:18, cursor:"pointer" }}>×</button>
|
<div style={{ display:"flex", alignItems:"center", gap:10 }}>
|
||||||
|
{/* Toggle Calendario / Lista */}
|
||||||
|
<div style={{ display:"flex", background:"rgba(255,255,255,.15)", borderRadius:9, padding:3 }}>
|
||||||
|
{[["calendario","📅 Calendario"],["lista","📋 Lista"]].map(([v,txt])=>(
|
||||||
|
<button key={v} onClick={()=>setHistVista(v)}
|
||||||
|
style={{ border:"none", cursor:"pointer", padding:"5px 12px", borderRadius:7, fontSize:12, fontWeight:700,
|
||||||
|
background: histVista===v?"#fff":"transparent",
|
||||||
|
color: histVista===v?"#1e3a5f":"rgba(255,255,255,.85)" }}>{txt}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={()=>setHistAsesor(null)} style={{ background:"rgba(255,255,255,.15)", border:"none", color:"#fff", width:30, height:30, borderRadius:8, fontSize:18, cursor:"pointer" }}>×</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding:"18px 22px", overflowY:"auto", background:"#f8fafc" }}>
|
{/* Navegación de semana (solo en vista Calendario) */}
|
||||||
|
{semanaBase && histVista==="calendario" && (
|
||||||
|
<div style={{ padding:"10px 22px", borderBottom:"1px solid #e8edf3", background:"#fff",
|
||||||
|
display:"flex", alignItems:"center", justifyContent:"center", gap:14 }}>
|
||||||
|
<button onClick={()=>setSemanaBase((d)=>{ const x=new Date(d); x.setDate(x.getDate()-7); return x; })}
|
||||||
|
style={{ width:28, height:28, borderRadius:8, border:"1px solid #cbd5e1", background:"#fff", cursor:"pointer", fontSize:14 }}>‹</button>
|
||||||
|
<span style={{ fontSize:13, fontWeight:700, color:"#1e3a5f", minWidth:210, textAlign:"center" }}>
|
||||||
|
{(() => {
|
||||||
|
const fin = new Date(semanaBase); fin.setDate(fin.getDate()+6);
|
||||||
|
const f = (d)=>d.toLocaleDateString("es-PE",{day:"2-digit",month:"short"});
|
||||||
|
return `${f(semanaBase)} – ${f(fin)}`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
<button onClick={()=>setSemanaBase((d)=>{ const x=new Date(d); x.setDate(x.getDate()+7); return x; })}
|
||||||
|
style={{ width:28, height:28, borderRadius:8, border:"1px solid #cbd5e1", background:"#fff", cursor:"pointer", fontSize:14 }}>›</button>
|
||||||
|
{/* Calendario custom (botón) */}
|
||||||
|
<div style={{ position:"relative", marginLeft:8 }}>
|
||||||
|
<button title="Ir a una fecha"
|
||||||
|
onClick={()=>{ setCalAno(semanaBase.getFullYear()); setCalMes(semanaBase.getMonth()); setCalVista("dias"); setCalAbierto((v)=>!v); }}
|
||||||
|
style={{ width:34, height:30, borderRadius:8, border:"1px solid #cbd5e1",
|
||||||
|
background: calAbierto?"#eff6ff":"#f8fafc", cursor:"pointer", fontSize:15,
|
||||||
|
display:"flex", alignItems:"center", justifyContent:"center" }}>📅</button>
|
||||||
|
{calAbierto && (
|
||||||
|
<>
|
||||||
|
<div onClick={()=>setCalAbierto(false)} style={{ position:"fixed", inset:0, zIndex:1099 }} />
|
||||||
|
<div style={{ position:"absolute", top:38, left:0, zIndex:1100, width:260,
|
||||||
|
background:"#fff", borderRadius:14, border:"1px solid #e8edf3",
|
||||||
|
boxShadow:"0 16px 44px rgba(15,23,42,.22)", padding:12 }}>
|
||||||
|
{/* Cabecera del calendario */}
|
||||||
|
<div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:10 }}>
|
||||||
|
<button onClick={()=>setCalVista(calVista==="dias"?"meses":"anios")}
|
||||||
|
style={{ border:"none", background:"transparent", cursor:"pointer", fontWeight:700,
|
||||||
|
fontSize:14, color:"#1e3a5f" }}>
|
||||||
|
{calVista==="dias" ? `${MES_LARGO[calMes]} ${calAno}` : calVista==="meses" ? calAno : "Elige año"}
|
||||||
|
</button>
|
||||||
|
{calVista==="dias" && (
|
||||||
|
<div style={{ display:"flex", gap:4 }}>
|
||||||
|
<button onClick={()=>{ const m=calMes-1; if(m<0){setCalMes(11);setCalAno(calAno-1);}else setCalMes(m); }}
|
||||||
|
style={calNavBtn}>‹</button>
|
||||||
|
<button onClick={()=>{ const m=calMes+1; if(m>11){setCalMes(0);setCalAno(calAno+1);}else setCalMes(m); }}
|
||||||
|
style={calNavBtn}>›</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vista DÍAS */}
|
||||||
|
{calVista==="dias" && (() => {
|
||||||
|
const primero = new Date(calAno, calMes, 1);
|
||||||
|
const offset = (primero.getDay()+6)%7; // lunes=0
|
||||||
|
const diasMes = new Date(calAno, calMes+1, 0).getDate();
|
||||||
|
const celdas = [];
|
||||||
|
for (let i=0;i<offset;i++) celdas.push(null);
|
||||||
|
for (let d=1;d<=diasMes;d++) celdas.push(d);
|
||||||
|
const hoy = new Date();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(7,1fr)", gap:2, marginBottom:4 }}>
|
||||||
|
{["L","M","M","J","V","S","D"].map((x,i)=>(
|
||||||
|
<div key={i} style={{ textAlign:"center", fontSize:10, fontWeight:700, color:"#94a3b8" }}>{x}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(7,1fr)", gap:2 }}>
|
||||||
|
{celdas.map((d,i)=>{
|
||||||
|
if(!d) return <div key={i} />;
|
||||||
|
const fecha = new Date(calAno, calMes, d);
|
||||||
|
const esHoy = fecha.toDateString()===hoy.toDateString();
|
||||||
|
const enSemana = fecha>=semanaBase && fecha < (()=>{const f=new Date(semanaBase);f.setDate(f.getDate()+7);return f;})();
|
||||||
|
return (
|
||||||
|
<button key={i}
|
||||||
|
onClick={()=>{ setSemanaBase(lunesDe(fecha)); setCalAbierto(false); }}
|
||||||
|
style={{ height:30, borderRadius:8, border:"none", cursor:"pointer", fontSize:12.5,
|
||||||
|
fontWeight: (esHoy||enSemana)?700:500,
|
||||||
|
background: enSemana?"#2563eb":(esHoy?"#eff6ff":"transparent"),
|
||||||
|
color: enSemana?"#fff":(esHoy?"#2563eb":"#334155") }}>{d}</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Vista MESES */}
|
||||||
|
{calVista==="meses" && (
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(4,1fr)", gap:6 }}>
|
||||||
|
{MES_CORTO.map((m,i)=>(
|
||||||
|
<button key={i} onClick={()=>{ setCalMes(i); setCalVista("dias"); }}
|
||||||
|
style={{ padding:"10px 0", borderRadius:10, border:"1px solid #eef2f7", cursor:"pointer",
|
||||||
|
fontSize:12.5, fontWeight: i===calMes?700:600,
|
||||||
|
background: i===calMes?"#2563eb":"#f8fafc",
|
||||||
|
color: i===calMes?"#fff":"#475569" }}>{m}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Vista AÑOS */}
|
||||||
|
{calVista==="anios" && (
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(4,1fr)", gap:6 }}>
|
||||||
|
{Array.from({length:12},(_,i)=>calAno-5+i).map((y)=>(
|
||||||
|
<button key={y} onClick={()=>{ setCalAno(y); setCalVista("meses"); }}
|
||||||
|
style={{ padding:"10px 0", borderRadius:10, border:"1px solid #eef2f7", cursor:"pointer",
|
||||||
|
fontSize:12.5, fontWeight: y===calAno?700:600,
|
||||||
|
background: y===calAno?"#2563eb":"#f8fafc",
|
||||||
|
color: y===calAno?"#fff":"#475569" }}>{y}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ padding:"14px", overflow:"auto", background:"#f8fafc" }}>
|
||||||
{histLoading ? <div style={{ textAlign:"center", color:"#94a3b8", padding:20 }}>Cargando...</div> :
|
{histLoading ? <div style={{ textAlign:"center", color:"#94a3b8", padding:20 }}>Cargando...</div> :
|
||||||
histFilas.length === 0 ? <div style={{ textAlign:"center", color:"#94a3b8", padding:20 }}>Sin registros.</div> :
|
histVista==="lista" ? (() => {
|
||||||
<div style={{ display:"flex", flexDirection:"column", gap:8 }}>
|
const diaLoc = (iso)=>{ if(!iso) return ""; const d=new Date(iso); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,"0"); const dd=String(d.getDate()).padStart(2,"0"); return `${y}-${m}-${dd}`; };
|
||||||
{histFilas.map((h, i) => {
|
const filas = (listaDia ? histOrden.filter((h)=>diaLoc(h.fecha_hora)===listaDia) : histOrden).slice().reverse();
|
||||||
const on = h.accion === "ON";
|
return (
|
||||||
return (
|
<div>
|
||||||
<div key={i} style={{ display:"flex", alignItems:"center", gap:12, background:"#fff",
|
{/* Filtro por día (calendario custom) */}
|
||||||
border:"1px solid #e8edf3", borderRadius:10, padding:"10px 14px" }}>
|
<div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:12, flexWrap:"wrap" }}>
|
||||||
<span style={{ width:10, height:10, borderRadius:"50%", flexShrink:0,
|
<span style={{ fontSize:12.5, fontWeight:700, color:"#475569" }}>Filtrar por día:</span>
|
||||||
background: on ? "#10b981" : "#94a3b8" }} />
|
<div style={{ position:"relative" }}>
|
||||||
<span style={{ fontWeight:700, fontSize:13, color: on ? "#047857" : "#64748b", width:110 }}>
|
<button title="Elegir día"
|
||||||
{on ? "🟢 Se conectó" : "⚪ Se desconectó"}
|
onClick={()=>{ const b=listaDia?new Date(listaDia+"T00:00:00"):new Date(); setListaCalAno(b.getFullYear()); setListaCalMes(b.getMonth()); setListaCalVista("dias"); setListaCalAbierto((v)=>!v); }}
|
||||||
</span>
|
style={{ display:"inline-flex", alignItems:"center", gap:6, padding:"6px 12px", borderRadius:8,
|
||||||
<span style={{ flex:1, fontSize:12.5, color:"#334155" }}>{fmtHora(h.fecha_hora)}</span>
|
border:"1px solid #cbd5e1", background: listaCalAbierto?"#eff6ff":"#f8fafc", cursor:"pointer", fontSize:12.5, fontWeight:600, color:"#475569" }}>
|
||||||
<span style={{ fontSize:10.5, fontWeight:700, color:"#64748b", background:"#f1f5f9",
|
📅 {listaDia ? new Date(listaDia+"T00:00:00").toLocaleDateString("es-PE",{day:"2-digit",month:"short",year:"numeric"}) : "Elegir día"}
|
||||||
borderRadius:999, padding:"2px 8px" }}>{h.origen}</span>
|
</button>
|
||||||
|
{listaCalAbierto && (
|
||||||
|
<>
|
||||||
|
<div onClick={()=>setListaCalAbierto(false)} style={{ position:"fixed", inset:0, zIndex:1099 }} />
|
||||||
|
<div style={{ position:"absolute", top:38, left:0, zIndex:1100, width:260,
|
||||||
|
background:"#fff", borderRadius:14, border:"1px solid #e8edf3",
|
||||||
|
boxShadow:"0 16px 44px rgba(15,23,42,.22)", padding:12 }}>
|
||||||
|
<div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:10 }}>
|
||||||
|
<button onClick={()=>setListaCalVista(listaCalVista==="dias"?"meses":"anios")}
|
||||||
|
style={{ border:"none", background:"transparent", cursor:"pointer", fontWeight:700, fontSize:14, color:"#1e3a5f" }}>
|
||||||
|
{listaCalVista==="dias" ? `${MES_LARGO[listaCalMes]} ${listaCalAno}` : listaCalVista==="meses" ? listaCalAno : "Elige año"}
|
||||||
|
</button>
|
||||||
|
{listaCalVista==="dias" && (
|
||||||
|
<div style={{ display:"flex", gap:4 }}>
|
||||||
|
<button onClick={()=>{ const m=listaCalMes-1; if(m<0){setListaCalMes(11);setListaCalAno(listaCalAno-1);}else setListaCalMes(m); }} style={calNavBtn}>‹</button>
|
||||||
|
<button onClick={()=>{ const m=listaCalMes+1; if(m>11){setListaCalMes(0);setListaCalAno(listaCalAno+1);}else setListaCalMes(m); }} style={calNavBtn}>›</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{listaCalVista==="dias" && (() => {
|
||||||
|
const primero = new Date(listaCalAno, listaCalMes, 1);
|
||||||
|
const offset = (primero.getDay()+6)%7;
|
||||||
|
const diasMes = new Date(listaCalAno, listaCalMes+1, 0).getDate();
|
||||||
|
const celdas = []; for(let i=0;i<offset;i++) celdas.push(null); for(let d=1;d<=diasMes;d++) celdas.push(d);
|
||||||
|
const hoy = new Date();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(7,1fr)", gap:2, marginBottom:4 }}>
|
||||||
|
{["L","M","M","J","V","S","D"].map((x,i)=><div key={i} style={{ textAlign:"center", fontSize:10, fontWeight:700, color:"#94a3b8" }}>{x}</div>)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(7,1fr)", gap:2 }}>
|
||||||
|
{celdas.map((d,i)=>{
|
||||||
|
if(!d) return <div key={i} />;
|
||||||
|
const fecha = new Date(listaCalAno, listaCalMes, d);
|
||||||
|
const val = `${listaCalAno}-${String(listaCalMes+1).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
|
||||||
|
const esHoy = fecha.toDateString()===hoy.toDateString();
|
||||||
|
const sel = listaDia===val;
|
||||||
|
return (
|
||||||
|
<button key={i} onClick={()=>{ setListaDia(val); setListaCalAbierto(false); }}
|
||||||
|
style={{ height:30, borderRadius:8, border:"none", cursor:"pointer", fontSize:12.5,
|
||||||
|
fontWeight: (esHoy||sel)?700:500,
|
||||||
|
background: sel?"#2563eb":(esHoy?"#eff6ff":"transparent"),
|
||||||
|
color: sel?"#fff":(esHoy?"#2563eb":"#334155") }}>{d}</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
{listaCalVista==="meses" && (
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(4,1fr)", gap:6 }}>
|
||||||
|
{MES_CORTO.map((m,i)=>(
|
||||||
|
<button key={i} onClick={()=>{ setListaCalMes(i); setListaCalVista("dias"); }}
|
||||||
|
style={{ padding:"10px 0", borderRadius:10, border:"1px solid #eef2f7", cursor:"pointer",
|
||||||
|
fontSize:12.5, fontWeight:i===listaCalMes?700:600,
|
||||||
|
background:i===listaCalMes?"#2563eb":"#f8fafc", color:i===listaCalMes?"#fff":"#475569" }}>{m}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{listaCalVista==="anios" && (
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"repeat(4,1fr)", gap:6 }}>
|
||||||
|
{Array.from({length:12},(_,i)=>listaCalAno-5+i).map((y)=>(
|
||||||
|
<button key={y} onClick={()=>{ setListaCalAno(y); setListaCalVista("meses"); }}
|
||||||
|
style={{ padding:"10px 0", borderRadius:10, border:"1px solid #eef2f7", cursor:"pointer",
|
||||||
|
fontSize:12.5, fontWeight:y===listaCalAno?700:600,
|
||||||
|
background:y===listaCalAno?"#2563eb":"#f8fafc", color:y===listaCalAno?"#fff":"#475569" }}>{y}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{listaDia && (
|
||||||
|
<button onClick={()=>setListaDia("")}
|
||||||
|
style={{ padding:"6px 12px", border:"1px solid #cbd5e1", borderRadius:8, fontSize:12,
|
||||||
|
background:"#fff", color:"#64748b", cursor:"pointer", fontWeight:600 }}>Ver todos</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{filas.length===0 ? <div style={{ textAlign:"center", color:"#94a3b8", padding:20 }}>Sin registros{listaDia?" para este día":""}.</div> :
|
||||||
|
<div style={{ display:"flex", flexDirection:"column", gap:8 }}>
|
||||||
|
{filas.map((h,i)=>{
|
||||||
|
const on = h.accion==="ON";
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ display:"flex", alignItems:"center", gap:12, background:"#fff",
|
||||||
|
border:"1px solid #e8edf3", borderRadius:10, padding:"10px 14px" }}>
|
||||||
|
<span style={{ width:10, height:10, borderRadius:"50%", flexShrink:0, background: on?"#10b981":"#94a3b8" }} />
|
||||||
|
<span style={{ fontWeight:700, fontSize:13, color: on?"#047857":"#64748b", width:120 }}>
|
||||||
|
{on ? "🟢 Se conectó" : "⚪ Se desconectó"}
|
||||||
|
</span>
|
||||||
|
<span style={{ flex:1, fontSize:12.5, color:"#334155" }}>{fmtHora(h.fecha_hora)}</span>
|
||||||
|
<span style={{ fontSize:10.5, fontWeight:700, color:"#64748b", background:"#f1f5f9", borderRadius:999, padding:"2px 8px" }}>{h.origen}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})() :
|
||||||
|
!semanaBase ? null :
|
||||||
|
(() => {
|
||||||
|
const H_INI = 7, H_FIN = 23; // franja mostrada 7am-23h
|
||||||
|
const horas = [];
|
||||||
|
for (let h=H_INI; h<=H_FIN; h++) horas.push(h);
|
||||||
|
const dias = [];
|
||||||
|
for (let i=0; i<7; i++){ const d=new Date(semanaBase); d.setDate(d.getDate()+i); dias.push(d); }
|
||||||
|
const NOM = ["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"];
|
||||||
|
const finSem = new Date(semanaBase); finSem.setDate(finSem.getDate()+7);
|
||||||
|
// ¿algún intervalo cae en esta semana?
|
||||||
|
const hayDatos = intervalosON.some(([a,b]) => b>=semanaBase && a<finSem);
|
||||||
|
return (
|
||||||
|
(() => {
|
||||||
|
const HORA_PX = 42; // alto de cada hora (px)
|
||||||
|
const totalH = (H_FIN - H_INI + 1) * HORA_PX;
|
||||||
|
const fmt = (x)=>x.toLocaleTimeString("es-PE",{hour:"2-digit",minute:"2-digit",hour12:true});
|
||||||
|
return (
|
||||||
|
<div style={{ background:"#fff", borderRadius:12, border:"1px solid #e8edf3", overflow:"hidden" }}>
|
||||||
|
{!hayDatos && (
|
||||||
|
<div style={{ textAlign:"center", color:"#94a3b8", fontSize:12.5, padding:"8px 0", borderBottom:"1px solid #eef2f7" }}>
|
||||||
|
Sin conexiones registradas esta semana.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Cabecera de días */}
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"52px repeat(7, 1fr)", borderBottom:"2px solid #e8edf3" }}>
|
||||||
|
<div />
|
||||||
|
{dias.map((d,i)=>{
|
||||||
|
const esHoy = d.toDateString()===new Date().toDateString();
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ textAlign:"center", padding:"10px 0",
|
||||||
|
borderLeft:"1px solid #eef2f7",
|
||||||
|
borderTop: esHoy?"3px solid #2563eb":"3px solid transparent",
|
||||||
|
background: esHoy?"#eff6ff":"#fff" }}>
|
||||||
|
<div style={{ fontSize:11, fontWeight:600, color: esHoy?"#2563eb":"#94a3b8" }}>{NOM[i]}</div>
|
||||||
|
<div style={{ fontSize:18, fontWeight:700, color: esHoy?"#2563eb":"#334155" }}>{d.getDate()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Cuerpo: horas + columnas de días con bloques posicionados */}
|
||||||
|
<div style={{ display:"grid", gridTemplateColumns:"52px repeat(7, 1fr)", position:"relative" }}>
|
||||||
|
{/* Columna de horas */}
|
||||||
|
<div>
|
||||||
|
{horas.map((h)=>(
|
||||||
|
<div key={h} style={{ height:HORA_PX, textAlign:"right", paddingRight:6,
|
||||||
|
fontSize:11, color:"#94a3b8", borderTop:"1px solid #eef2f7",
|
||||||
|
position:"relative", top:-7 }}>{h}:00</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Una columna por día */}
|
||||||
|
{dias.map((d,di)=>{
|
||||||
|
const colIni = new Date(d); colIni.setHours(H_INI,0,0,0);
|
||||||
|
// líneas de hora de fondo
|
||||||
|
return (
|
||||||
|
<div key={di} style={{ position:"relative", height:totalH, borderLeft:"1px solid #eef2f7" }}>
|
||||||
|
{horas.map((h,idx)=>(
|
||||||
|
<div key={h} style={{ position:"absolute", left:0, right:0, top:idx*HORA_PX,
|
||||||
|
borderTop:"1px solid #f1f5f9", height:1 }} />
|
||||||
|
))}
|
||||||
|
{/* Bloques ON continuos */}
|
||||||
|
{intervalosON.map(([a,b,enCurso],k)=>{
|
||||||
|
const diaFin = new Date(d); diaFin.setHours(H_FIN+1,0,0,0);
|
||||||
|
const s = Math.max(a, colIni), e = Math.min(b, diaFin);
|
||||||
|
if (e<=s) return null; // no cae en este día/franja
|
||||||
|
const top = ((s - colIni) / (60*60*1000)) * HORA_PX;
|
||||||
|
const alto = Math.max(6, ((e - s) / (60*60*1000)) * HORA_PX);
|
||||||
|
const finTxt = enCurso ? "en curso" : fmt(b);
|
||||||
|
return (
|
||||||
|
<div key={k} title={enCurso ? `Conectado desde ${fmt(a)} · en curso ahora` : `Conectado de ${fmt(a)} a ${fmt(b)}`}
|
||||||
|
style={{ position:"absolute", left:3, right:3, top, height:alto, borderRadius:8,
|
||||||
|
background: enCurso ? "linear-gradient(135deg,#a7f3d0,#6ee7b7)" : "linear-gradient(135deg,#bae6fd,#7dd3fc)",
|
||||||
|
border: enCurso ? "1px solid #10b981" : "1px solid #38bdf8",
|
||||||
|
boxShadow:"0 1px 3px rgba(14,165,233,.25)", cursor:"pointer",
|
||||||
|
overflow:"hidden", color: enCurso ? "#064e3b" : "#0c4a6e", padding:"3px 7px",
|
||||||
|
fontFamily:"system-ui, -apple-system, 'Segoe UI', sans-serif",
|
||||||
|
display:"flex", flexDirection:"column", justifyContent:"center", gap:1, lineHeight:1.2 }}>
|
||||||
|
{alto>=34 ? (
|
||||||
|
<>
|
||||||
|
<span style={{ display:"flex", alignItems:"center", gap:5, fontSize:11, fontWeight:700, letterSpacing:.2 }}>
|
||||||
|
<span style={{ width:7, height:7, borderRadius:"50%", flexShrink:0,
|
||||||
|
background: enCurso ? "#059669" : "#0284c7" }} />
|
||||||
|
{fmt(a)}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize:9.5, fontWeight:500, opacity:.9, letterSpacing:.2, paddingLeft:12 }}>
|
||||||
|
{enCurso ? "en curso" : `hasta ${fmt(b)}`}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : alto>=18 ? (
|
||||||
|
<span style={{ fontSize:10, fontWeight:700, whiteSpace:"nowrap", letterSpacing:.2 }}>{fmt(a)} – {finTxt}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})()
|
||||||
</div>}
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user