--- import AppLayout from '@/layouts/AppLayout.astro'; import Chart from '@/components/admin/estadisticas/Chart.tsx'; import { todayMX } from '@/lib/date'; // El middleware ya garantiza rol admin en /admin/*. const supabase = Astro.locals.supabase; const dia = todayMX(); const countHead = { count: 'exact' as const, head: true }; const startISO = dia.startUTC.toISOString(); const endISO = dia.endUTC.toISOString(); // KPIs del día — misma lógica que /admin/inventario legacy. const [ { count: nSolicitadas }, { count: nAprobadas }, { count: nRechazadas }, { count: nDevueltas }, { count: nEnPrestamo }, { count: nVencidos }, ] = await Promise.all([ supabase.from('solicitudes').select('id', countHead).gte('fecha_solicitud', startISO).lt('fecha_solicitud', endISO), supabase.from('audit_log').select('id', countHead).eq('estado_nuevo', 'aprobado').gte('at', startISO).lt('at', endISO), supabase.from('audit_log').select('id', countHead).eq('estado_nuevo', 'rechazado').gte('at', startISO).lt('at', endISO), supabase.from('audit_log').select('id', countHead).eq('estado_nuevo', 'devuelto').gte('at', startISO).lt('at', endISO), supabase.from('solicitudes').select('id', countHead).in('estado', ['aprobado', 'activo']), supabase.from('solicitudes').select('id', countHead).in('estado', ['aprobado', 'activo']).lt('fecha_devolucion_estimada', dia.isoDate), ]); const kpis = [ { label: 'Solicitadas', value: nSolicitadas ?? 0, tone: 'primary' }, { label: 'Aprobadas', value: nAprobadas ?? 0, tone: 'primary' }, { label: 'Rechazadas', value: nRechazadas ?? 0, tone: 'neutral' }, { label: 'Devoluciones', value: nDevueltas ?? 0, tone: 'primary' }, { label: 'En préstamo', value: nEnPrestamo ?? 0, tone: 'secondary', total: true }, { label: 'Vencidos', value: nVencidos ?? 0, tone: (nVencidos ?? 0) > 0 ? 'danger' : 'neutral', total: true }, ]; const toneStyle = (tone: string) => { if (tone === 'primary') return 'color: var(--color-primary);'; if (tone === 'secondary') return 'color: var(--color-secondary-hover);'; if (tone === 'danger') return 'color: var(--color-danger-text);'; return 'color: color-mix(in oklab, var(--color-ink) 65%, transparent);'; }; // Ventana 30 días (rango inclusivo del día actual). const DIAS = 30; const desde = new Date(dia.startUTC.getTime() - (DIAS - 1) * 86400000).toISOString(); // (1) Top 10 materiales solicitados últimos 30 días. const { data: topMaterialesRaw } = await supabase .from('solicitud_items') .select('cantidad, material:materiales(nombre), solicitud:solicitudes!inner(fecha_solicitud)') .gte('solicitud.fecha_solicitud', desde); const topMapa = new Map(); for (const r of (topMaterialesRaw ?? []) as any[]) { const nom = r.material?.nombre ?? '—'; topMapa.set(nom, (topMapa.get(nom) ?? 0) + (r.cantidad ?? 0)); } const topMateriales = [...topMapa.entries()] .map(([label, valor]) => ({ label, valor })) .sort((a, b) => b.valor - a.valor) .slice(0, 10); // (2) Solicitudes por día últimos 30 días. Rellena huecos con 0. const { data: solicitudesUlt30 } = await supabase .from('solicitudes') .select('fecha_solicitud') .gte('fecha_solicitud', desde) .order('fecha_solicitud', { ascending: true }); // Genera todas las fechas de la ventana en formato YYYY-MM-DD (zona MX). function toIsoDateMX(d: Date): string { return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Tijuana', year: 'numeric', month: '2-digit', day: '2-digit', }).format(d); } const conteoPorDia = new Map(); for (const r of (solicitudesUlt30 ?? []) as { fecha_solicitud: string }[]) { const k = toIsoDateMX(new Date(r.fecha_solicitud)); conteoPorDia.set(k, (conteoPorDia.get(k) ?? 0) + 1); } const solicitudesPorDia: { fecha: string; valor: number }[] = []; for (let i = 0; i < DIAS; i++) { const d = new Date(dia.startUTC.getTime() - (DIAS - 1 - i) * 86400000); const iso = toIsoDateMX(d); solicitudesPorDia.push({ fecha: iso, valor: conteoPorDia.get(iso) ?? 0 }); } // (3) Distribución de estados de todas las solicitudes. const { data: estadosRaw } = await supabase.from('solicitudes').select('estado'); const estadoLabelMap: Record = { pendiente: 'Pendiente', aprobado: 'Aprobado', activo: 'Activo', devuelto: 'Devuelto', rechazado: 'Rechazado', vencido: 'Vencido', }; const estadoMapa = new Map(); for (const r of (estadosRaw ?? []) as { estado: string | null }[]) { if (!r.estado) continue; estadoMapa.set(r.estado, (estadoMapa.get(r.estado) ?? 0) + 1); } const distribEstados = [...estadoMapa.entries()].map(([k, v]) => ({ label: estadoLabelMap[k] ?? k, valor: v, })); ---

Estadísticas

Actividad del día ·

{kpis.map((k) => (
{k.label}{k.total ? ' (total)' : ' hoy'}
{k.value}
))}

Solicitudes por día · últimos {DIAS}

{solicitudesPorDia.some((d) => d.valor > 0) ? ( ) : (

Sin actividad en los últimos {DIAS} días.

)}

Distribución de estados

{distribEstados.length > 0 ? ( ) : (

Aún no hay solicitudes registradas.

)}

Top 10 materiales · últimos {DIAS} días

{topMateriales.length > 0 ? ( ) : (

Ningún material solicitado en los últimos {DIAS} días.

)}