// ROCA GOLF — shared data adapted to real SQL schema (oc_compras) const { useState, useEffect, useMemo, useRef, useCallback } = React; const ROCA_USER = { name: "Mariana Vega", role: "Contralor", initials: "MV" }; const DEFAULT_THRESHOLDS = { warn: 80, alert: 90, over: 100 }; // ───────────────────────── PROYECTOS (proyectos) ───────────────────────── const PROYECTOS = [ { id: 1, nombre: "Mantenimiento de Greens" }, { id: 2, nombre: "Casa Club & F&B" }, { id: 3, nombre: "Pro Shop" }, { id: 4, nombre: "Ampliación Hoyo 14" }, { id: 5, nombre: "Operación general" } ]; // ───────────────────────── FAMILIAS (catalogo_insumos.codigofamilia) ───────────────────────── const FAMILIAS = [ { codigo: "01", nombre: "Fertilizantes y agroquímicos", budget: 480000, spent: 502400, trend: [40,55,62,70,82,95,104] }, { codigo: "02", nombre: "Mantenimiento de carros", budget: 220000, spent: 211200, trend: [20,28,40,55,68,82,96] }, { codigo: "03", nombre: "Riego y agua", budget: 360000, spent: 332000, trend: [35,48,55,62,70,80,92] }, { codigo: "04", nombre: "Alimentos y bebidas", budget: 540000, spent: 458600, trend: [25,38,45,55,64,72,85] }, { codigo: "05", nombre: "Pro shop y vestuario", budget: 320000, spent: 198400, trend: [12,18,25,32,42,52,62] }, { codigo: "06", nombre: "Oficina y administración", budget: 180000, spent: 84000, trend: [8,15,22,28,34,42,47] }, { codigo: "07", nombre: "Limpieza e higiene", budget: 140000, spent: 112000, trend: [18,28,38,48,58,70,80] }, { codigo: "08", nombre: "Eventos y torneos", budget: 260000, spent: 156000, trend: [15,22,30,38,45,54,60] } ]; // Backwards-compatible alias used by older components const CATEGORIES_BASE = FAMILIAS.map(f => ({ id: f.codigo, name: f.nombre, meta: "—", budget: f.budget, spent: f.spent, trend: f.trend })); // ───────────────────────── PROVEEDORES (proveedores) ───────────────────────── const PROVEEDORES = [ { id: 1, nombre: "AgroVerde Suministros", rfc: "AVS990412L23", banco: "BBVA", moneda: "MXN", contacto: "Luis Pacheco", email: "ventas@agroverde.mx", activo: 1, ocs: 28, monto: 482300 }, { id: 2, nombre: "QuímicaPro MX", rfc: "QPM050818H44", banco: "Banamex", moneda: "MXN", contacto: "Daniela Ríos", email: "contacto@quimicapro.mx", activo: 1, ocs: 19, monto: 412000 }, { id: 3, nombre: "Hidráulica del Bajío", rfc: "HDB070220A11", banco: "Santander", moneda: "MXN", contacto: "Roberto López", email: "ventas@hidrobajio.mx", activo: 1, ocs: 14, monto: 198400 }, { id: 4, nombre: "Distribuidora La Reserva", rfc: "DLR110630T55", banco: "Banorte", moneda: "MXN", contacto: "Carmen Vidal", email: "compras@lareserva.mx", activo: 1, ocs: 22, monto: 356800 }, { id: 5, nombre: "Talleres Norte", rfc: "TLN960115F77", banco: "BBVA", moneda: "MXN", contacto: "Jorge Méndez", email: "jorge@talleresnorte.mx", activo: 1, ocs: 11, monto: 142600 }, { id: 6, nombre: "Pro Equipment USA", rfc: "FOREIGN", banco: "Wells Fargo",moneda: "USD", contacto: "Karen Smith", email: "sales@proequip.com", activo: 1, ocs: 6, monto: 18400 }, { id: 7, nombre: "Imprenta Continental", rfc: "ICO020410B22", banco: "HSBC", moneda: "MXN", contacto: "Ana Beltrán", email: "ana@continental.mx", activo: 1, ocs: 8, monto: 64200 }, { id: 8, nombre: "Servicios Generales SG", rfc: "SGS080912R33", banco: "Banorte", moneda: "MXN", contacto: "Pedro Aguilar", email: "pedro@sgservicios.mx", activo: 0, ocs: 4, monto: 22800 } ]; // ───────────────────────── ÓRDENES DE COMPRA (ordenes_compra + oc_partidas) ───────────────────────── // statuspago: PAGADO | PAGO_PARCIAL | PENDIENTE // statusflujo: BORRADOR | CON_PRIORIDAD | AUTORIZADA // prioridad: NORMAL | URGENTE | CRITICO const ORDENES = [ { id: 1, folio: 2604021, fecha: "2026-04-26", proyectoid: 1, proveedorid: 1, moneda: "MXN", subtotal: 72690, iva: 11630, total: 84320, statuspago: "PAGADO", montopagado: 84320, montopendiente: 0, prioridad: "NORMAL", statusflujo: "AUTORIZADA", familia: "01" }, { id: 2, folio: 2604020, fecha: "2026-04-26", proyectoid: 1, proveedorid: 3, moneda: "MXN", subtotal: 18793, iva: 3007, total: 21800, statuspago: "PENDIENTE", montopagado: 0, montopendiente: 21800, prioridad: "URGENTE", statusflujo: "CON_PRIORIDAD", familia: "03" }, { id: 3, folio: 2604019, fecha: "2026-04-25", proyectoid: 2, proveedorid: 4, moneda: "MXN", subtotal: 48620, iva: 7780, total: 56400, statuspago: "PAGO_PARCIAL", montopagado: 28200, montopendiente: 28200, prioridad: "NORMAL", statusflujo: "AUTORIZADA", familia: "04" }, { id: 4, folio: 2604018, fecha: "2026-04-25", proyectoid: 1, proveedorid: 2, moneda: "MXN", subtotal: 87930, iva: 14070, total: 102000,statuspago: "PAGADO", montopagado: 102000, montopendiente: 0, prioridad: "CRITICO", statusflujo: "AUTORIZADA", familia: "01" }, { id: 5, folio: 2604017, fecha: "2026-04-24", proyectoid: 5, proveedorid: 5, moneda: "MXN", subtotal: 16078, iva: 2572, total: 18650, statuspago: "PENDIENTE", montopagado: 0, montopendiente: 18650, prioridad: "NORMAL", statusflujo: "BORRADOR", familia: "02" }, { id: 6, folio: 2604016, fecha: "2026-04-23", proyectoid: 3, proveedorid: 6, moneda: "USD", subtotal: 4200, iva: 0, total: 4200, statuspago: "PAGADO", montopagado: 4200, montopendiente: 0, prioridad: "NORMAL", statusflujo: "AUTORIZADA", familia: "05" }, { id: 7, folio: 2604015, fecha: "2026-04-22", proyectoid: 2, proveedorid: 4, moneda: "MXN", subtotal: 32500, iva: 5200, total: 37700, statuspago: "PAGADO", montopagado: 37700, montopendiente: 0, prioridad: "NORMAL", statusflujo: "AUTORIZADA", familia: "04" }, { id: 8, folio: 2604014, fecha: "2026-04-21", proyectoid: 4, proveedorid: 2, moneda: "MXN", subtotal: 64655, iva: 10345, total: 75000, statuspago: "PAGO_PARCIAL", montopagado: 30000, montopendiente: 45000, prioridad: "URGENTE", statusflujo: "AUTORIZADA", familia: "01" }, { id: 9, folio: 2604013, fecha: "2026-04-20", proyectoid: 5, proveedorid: 7, moneda: "MXN", subtotal: 12931, iva: 2069, total: 15000, statuspago: "PAGADO", montopagado: 15000, montopendiente: 0, prioridad: "NORMAL", statusflujo: "AUTORIZADA", familia: "06" }, { id:10, folio: 2604012, fecha: "2026-04-19", proyectoid: 1, proveedorid: 1, moneda: "MXN", subtotal: 41379, iva: 6621, total: 48000, statuspago: "PENDIENTE", montopagado: 0, montopendiente: 48000, prioridad: "CRITICO", statusflujo: "CON_PRIORIDAD", familia: "01" }, { id:11, folio: 2604011, fecha: "2026-04-18", proyectoid: 2, proveedorid: 4, moneda: "MXN", subtotal: 28017, iva: 4483, total: 32500, statuspago: "PAGADO", montopagado: 32500, montopendiente: 0, prioridad: "NORMAL", statusflujo: "AUTORIZADA", familia: "07" } ]; // ───────────────────────── CATÁLOGO DE INSUMOS (catalogo_insumos) ───────────────────────── const INSUMOS = [ { id: 1, clave: "01-0101", descripcion: "Fertilizante NPK 18-46-00 50kg", codigofamilia: "01", nombrefamilia: "Fertilizantes y agroquímicos", activo: 1, usos: 24 }, { id: 2, clave: "01-0205", descripcion: "Herbicida selectivo 20L", codigofamilia: "01", nombrefamilia: "Fertilizantes y agroquímicos", activo: 1, usos: 12 }, { id: 3, clave: "01-0312", descripcion: "Fungicida sistémico 5L", codigofamilia: "01", nombrefamilia: "Fertilizantes y agroquímicos", activo: 1, usos: 8 }, { id: 4, clave: "02-0801", descripcion: "Aceite hidráulico cart 19L", codigofamilia: "02", nombrefamilia: "Mantenimiento de carros", activo: 1, usos: 18 }, { id: 5, clave: "02-0902", descripcion: "Llanta carrito 18x8.5-8", codigofamilia: "02", nombrefamilia: "Mantenimiento de carros", activo: 1, usos: 6 }, { id: 6, clave: "03-1101", descripcion: "Aspersor rotor 360°", codigofamilia: "03", nombrefamilia: "Riego y agua", activo: 1, usos: 32 }, { id: 7, clave: "03-1204", descripcion: "Tubería PVC 4\" 6m", codigofamilia: "03", nombrefamilia: "Riego y agua", activo: 1, usos: 14 }, { id: 8, clave: "04-2001", descripcion: "Botella vino tinto Reserva 750ml", codigofamilia: "04", nombrefamilia: "Alimentos y bebidas", activo: 1, usos: 42 }, { id: 9, clave: "04-2150", descripcion: "Filete res Premium kg", codigofamilia: "04", nombrefamilia: "Alimentos y bebidas", activo: 1, usos: 38 }, { id:10, clave: "05-3010", descripcion: "Polo manga corta logo bordado", codigofamilia: "05", nombrefamilia: "Pro shop y vestuario", activo: 1, usos: 22 }, { id:11, clave: "06-4002", descripcion: "Tóner impresora HP CF410X", codigofamilia: "06", nombrefamilia: "Oficina y administración", activo: 1, usos: 9 }, { id:12, clave: "07-5005", descripcion: "Cloro líquido 20L", codigofamilia: "07", nombrefamilia: "Limpieza e higiene", activo: 1, usos: 16 }, { id:13, clave: "08-6101", descripcion: "Trofeo cristal 30cm", codigofamilia: "08", nombrefamilia: "Eventos y torneos", activo: 1, usos: 4 }, { id:14, clave: "01-0420", descripcion: "Bioestimulante foliar 10L", codigofamilia: "01", nombrefamilia: "Fertilizantes y agroquímicos", activo: 0, usos: 0 } ]; // ───────────────────────── HISTORIAL (historial) ───────────────────────── const HISTORIAL = [ { id: 1, fecha: "2026-04-26 14:32", usuario: "Mariana Vega", accion: "CREAR_OC", desc: "OC #2604021 creada — AgroVerde Suministros", tabla: "ordenes_compra" }, { id: 2, fecha: "2026-04-26 14:08", usuario: "Carlos Ortega", accion: "PAGO_REGISTRADO",desc: "Pago $84,320 OC #2604021", tabla: "oc_pagos" }, { id: 3, fecha: "2026-04-26 11:22", usuario: "Mariana Vega", accion: "EDITAR_INSUMO", desc: "Insumo 01-0101 actualizado", tabla: "catalogo_insumos" }, { id: 4, fecha: "2026-04-26 10:14", usuario: "Daniel Sosa", accion: "CAMBIO_PRIORIDAD",desc: "OC #2604018 marcada como CRÍTICA", tabla: "ordenes_compra" }, { id: 5, fecha: "2026-04-26 09:40", usuario: "Mariana Vega", accion: "LOGIN", desc: "Inicio de sesión", tabla: "usuarios" }, { id: 6, fecha: "2026-04-25 18:12", usuario: "Carlos Ortega", accion: "CREAR_OC", desc: "OC #2604019 creada — Distribuidora La Reserva", tabla: "ordenes_compra" }, { id: 7, fecha: "2026-04-25 16:55", usuario: "Sofía Cabrera", accion: "EDITAR_PROVEEDOR",desc: "Proveedor #4 actualizado — datos bancarios", tabla: "proveedores" }, { id: 8, fecha: "2026-04-25 14:30", usuario: "Mariana Vega", accion: "ELIMINAR_INSUMO",desc: "Insumo 01-0420 desactivado", tabla: "catalogo_insumos" }, { id: 9, fecha: "2026-04-25 12:18", usuario: "Daniel Sosa", accion: "PAGO_REGISTRADO",desc: "Pago parcial $30,000 OC #2604014", tabla: "oc_pagos" }, { id:10, fecha: "2026-04-25 10:02", usuario: "Carlos Ortega", accion: "CREAR_OC", desc: "OC #2604018 creada — QuímicaPro MX", tabla: "ordenes_compra" }, { id:11, fecha: "2026-04-24 17:45", usuario: "Mariana Vega", accion: "CREAR_INSUMO", desc: "Insumo 03-1204 creado", tabla: "catalogo_insumos" }, { id:12, fecha: "2026-04-24 11:30", usuario: "Sofía Cabrera", accion: "LOGIN", desc: "Inicio de sesión", tabla: "usuarios" } ]; // ───────────────────────── ALERTAS DERIVADAS ───────────────────────── const ALERTS_DATA = [ { id: 1, level: "over", cat: "01", title: "Fertilizantes excedió el presupuesto", desc: "Categoría 01 (Mant. Greens) cerró en 104.7%. OC #2604018 disparó el umbral.", time: "Hace 12 min · Auto" }, { id: 2, level: "alert", cat: "02", title: "Mantenimiento de carros al 96%", desc: "Cruzó el umbral del 90%. Revisar OCs en revisión antes de generar nuevas.", time: "Hace 1 h · Auto" }, { id: 3, level: "alert", cat: "03", title: "Riego y agua al 92%", desc: "Proyección de cierre 108% si se mantiene el ritmo actual.", time: "Hoy 09:14 · Auto" }, { id: 4, level: "warn", cat: "07", title: "Limpieza e higiene al 80%", desc: "Llegó al primer umbral. Hay 6 partidas pendientes de recibir.", time: "Hoy 08:02 · Auto" }, { id: 5, level: "warn", cat: "04", title: "Alimentos y bebidas al 85%", desc: "Crecimiento +14% vs mes anterior. Evento de socios programado para el 28.", time: "Ayer 18:40 · Auto" }, { id: 6, level: "ok", cat: "06", title: "Oficina y administración estable", desc: "Cerró en 47%. Sin acciones requeridas.", time: "Ayer 14:20 · Auto" } ]; // Recent OCs alias for old dashboard const RECENT_OCS = ORDENES.slice(0, 5).map(o => { const prov = PROVEEDORES.find(p => p.id === o.proveedorid); return { folio: "OC-" + o.folio, vendor: prov.nombre, cat: o.familia, amount: o.total, status: o.statuspago === "PAGADO" ? "approved" : "pending" }; }); // ───────────────────────── HELPERS ───────────────────────── function flagLevel(pct, t = DEFAULT_THRESHOLDS) { if (pct >= t.over) return "over"; if (pct >= t.alert) return "alert"; if (pct >= t.warn) return "warn"; return "ok"; } function fmtMoney(n, opts = {}) { const { compact, sign } = opts; if (compact && Math.abs(n) >= 1000) { const k = n / 1000; return (k >= 1000 ? (k/1000).toFixed(1)+"M" : k.toFixed(0)+"K"); } const s = Math.round(n).toLocaleString("es-MX"); return sign && n > 0 ? "+" + s : s; } function flagColor(level) { return ({ ok: "var(--rg-flag-ok)", warn: "var(--rg-flag-warn)", alert: "var(--rg-flag-alert)", over: "var(--rg-flag-over)" })[level]; } function levelLabel(level) { return { ok: "OK", warn: "AVISO 80%", alert: "ALERTA 90%", over: "EXCEDIDO" }[level]; } function fmtFolio(f) { const s = String(f); // Folio largo (ej. 2604021) → OC-2604-021 if (s.length > 4) return "OC-" + s.slice(0, 4) + "-" + s.slice(4); // Folio corto (ej. 0, 1, 13) → OC-0001 return "OC-" + s.padStart(4, "0"); } function fmtFecha(d) { const [y,m,dd] = d.split("-"); return `${dd}/${m}/${y.slice(2)}`; } function nombreProyecto(id) { return (PROYECTOS.find(p => p.id === id) || {}).nombre || "—"; } function nombreProveedor(id) { return (PROVEEDORES.find(p => p.id === id) || {}).nombre || "—"; } function nombreFamilia(cod) { return (FAMILIAS.find(f => f.codigo === cod) || {}).nombre || "—"; } // ───────────────────────── ICONS ───────────────────────── const Icon = { dashboard: (s = 16) => (), budget: (s = 16) => (), oc: (s = 16) => (), vendors: (s = 16) => (), catalog: (s = 16) => (), history: (s = 16) => (), bell: (s = 16) => (), settings: (s = 16) => (), reports: (s = 16) => (), search: (s = 14) => (), filter: (s = 14) => (), download: (s = 14) => (), more: (s = 14) => (), arrowRight: (s = 12) => (), arrowUp: (s = 12) => (), arrowDown: (s = 12) => (), check: (s = 12) => (), x: (s = 12) => (), plus: (s = 14) => (), edit: (s = 14) => (), eye: (s = 14) => (), excel: (s = 14) => () }; // ───────────────────────── GOLF FLAG ───────────────────────── function GolfFlag({ level = "ok", size = 26, animate = true }) { const color = flagColor(level); return (
{animate && (level === "alert" || level === "over") && ( )}
); } function Sparkline({ data, color = "var(--rg-green-500)", width = 80, height = 24 }) { const max = Math.max(...data), min = Math.min(...data), range = max - min || 1; const step = width / (data.length - 1); const pts = data.map((v, i) => `${i * step},${height - ((v - min) / range) * height}`).join(" "); const areaPts = `0,${height} ${pts} ${width},${height}`; return ( ); } // Status / priority badges function StatusPagoBadge({ status }) { const map = { PAGADO: { color: "var(--rg-flag-ok)", bg: "rgba(45,106,68,0.15)", label: "Pagado" }, PAGO_PARCIAL: { color: "var(--rg-flag-warn)", bg: "rgba(212,160,23,0.15)", label: "Parcial" }, PENDIENTE: { color: "var(--rg-flag-over)", bg: "rgba(178,34,34,0.12)", label: "Pendiente" } }[status] || { color: "var(--rg-muted)", bg: "var(--rg-cream)", label: status }; return {map.label} ; } function PrioridadBadge({ p }) { const map = { NORMAL: { c: "var(--rg-muted)", bg: "var(--rg-cream)" }, URGENTE: { c: "var(--rg-flag-alert)", bg: "rgba(200,101,27,0.12)" }, CRITICO: { c: "var(--rg-flag-over)", bg: "rgba(178,34,34,0.12)" } }[p] || { c: "var(--rg-muted)", bg: "var(--rg-cream)" }; return {p}; } function FlujoBadge({ f }) { const map = { NUEVO: { c: "var(--rg-muted)", bg: "var(--rg-cream)", l: "Nuevo" }, BORRADOR: { c: "var(--rg-muted)", bg: "var(--rg-cream)", l: "Borrador" }, CON_PRIORIDAD: { c: "var(--rg-flag-warn)", bg: "rgba(212,160,23,0.12)", l: "Con prioridad" }, AUTORIZADA: { c: "var(--rg-flag-ok)", bg: "rgba(45,106,68,0.12)", l: "Autorizada" } }[f] || { c: "var(--rg-muted)", bg: "var(--rg-cream)", l: f }; return {map.l}; } // ───────────────────────────────────────────────────────────────────────────── // SISTEMA DE TOASTS (notificaciones de error / éxito) // ───────────────────────────────────────────────────────────────────────────── const ToastContext = React.createContext({ addToast: () => {} }); function ToastProvider({ children }) { const [toasts, setToasts] = useState([]); const addToast = React.useCallback((msg, type = "error") => { const id = Date.now() + Math.random(); setToasts(t => [...t, { id, msg, type }]); setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 6000); }, []); const dismiss = (id) => setToasts(t => t.filter(x => x.id !== id)); const iconFor = (type) => { if (type === "error") return "✕"; if (type === "success") return "✓"; return "!"; }; const bgFor = (type) => { if (type === "error") return { bg: "#b22222", border: "#8b0000" }; if (type === "success") return { bg: "#2d6a44", border: "#1a4a2e" }; return { bg: "#c8651b", border: "#a0480e" }; }; return ( {children}
{toasts.map(t => { const { bg, border } = bgFor(t.type); return (
{iconFor(t.type)} {t.msg}
); })}
); } function useToast() { return React.useContext(ToastContext); } // ───────────────────────────────────────────────────────────────────────────── // CONTEXTO DE DATOS (API → estado global) // ───────────────────────────────────────────────────────────────────────────── const AppDataContext = React.createContext(null); // Construye alertas automáticas a partir de las familias con presupuesto function buildAlertsFromFamilias(familias, thresholds = DEFAULT_THRESHOLDS) { return familias .map(f => { const pct = f.budget > 0 ? (f.spent / f.budget) * 100 : 0; const level = flagLevel(pct, thresholds); if (level === "ok") return null; return { id: f.codigo, level, cat: f.codigo, title: `${f.nombre} al ${Math.round(pct)}%`, desc: level === "over" ? `La familia ${f.codigo} ha excedido el presupuesto (${Math.round(pct)}%).` : `Utilización del ${Math.round(pct)}% — revisa OCs pendientes.`, time: "Auto · Tiempo real" }; }) .filter(Boolean); } // Convierte datos de la API al shape que usan los componentes function normalizeFamilias(apiFamilias) { return apiFamilias.map(f => ({ codigo: f.codigo, nombre: f.nombre, budget: f.budget || 0, spent: f.spent || 0, trend: f.trend || [0], ...f })); } function normalizeOrdenes(apiOrdenes) { return apiOrdenes.map(o => ({ ...o, familia: o.familia || "", })); } function AppDataProvider({ children }) { const { addToast } = React.useContext(ToastContext); const apiBase = window.RG_API_BASE || ""; const [state, setState] = useState({ proyectos: PROYECTOS, familias: FAMILIAS, ordenes: ORDENES, insumos: INSUMOS, proveedores: PROVEEDORES, historial: HISTORIAL, alerts: ALERTS_DATA, reportes: null, loading: Boolean(apiBase), apiReady: false, }); // Expone un helper para recargar datos individuales const refresh = React.useCallback(async (endpoint) => { if (!apiBase) return; try { const res = await fetch(`${apiBase}/${endpoint}`); const json = await res.json(); if (!json.ok) throw new Error(json.error || "Error desconocido"); return json.data; } catch (err) { addToast(`Error al recargar ${endpoint}: ${err.message}`); return null; } }, [apiBase, addToast]); useEffect(() => { if (!apiBase) return; const controller = new AbortController(); const signal = controller.signal; async function fetchAll() { const call = (ep) => fetch(`${apiBase}/${ep}`, { signal }) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .catch(err => ({ ok: false, error: err.message, _ep: ep })); const [dashR, ordR, insR, provR, histR] = await Promise.all([ call("dashboard.php"), call("ordenes.php?limit=200"), call("insumos.php?limit=500"), call("proveedores.php"), call("historial.php?limit=100"), ]); if (signal.aborted) return; const updates = { loading: false, apiReady: true }; if (dashR.ok) { const fams = normalizeFamilias(dashR.data.familias || []); updates.familias = fams.length ? fams : FAMILIAS; updates.proyectos = dashR.data.proyectos || PROYECTOS; updates.alerts = buildAlertsFromFamilias(fams, DEFAULT_THRESHOLDS); } else { addToast(`Dashboard: ${dashR.error || "Error al conectar"}`); } if (ordR.ok) { const rows = normalizeOrdenes(ordR.data.rows || []); updates.ordenes = rows.length ? rows : ORDENES; } else { addToast(`Órdenes: ${ordR.error || "Error al conectar"}`); } if (insR.ok) { const rows = insR.data.rows || []; updates.insumos = rows.length ? rows : INSUMOS; } else { addToast(`Insumos: ${insR.error || "Error al conectar"}`); } if (provR.ok) { const rows = provR.data || []; updates.proveedores = rows.length ? rows : PROVEEDORES; } else { addToast(`Proveedores: ${provR.error || "Error al conectar"}`); } if (histR.ok) { const rows = histR.data.rows || []; updates.historial = rows.length ? rows : HISTORIAL; } else { addToast(`Historial: ${histR.error || "Error al conectar"}`); } setState(s => ({ ...s, ...updates })); } fetchAll().catch(err => { if (err.name === "AbortError") return; addToast(`Error de conexión: ${err.message}`); setState(s => ({ ...s, loading: false })); }); return () => controller.abort(); }, [apiBase]); // Carga reportes por separado (se pide solo cuando el usuario va a Reports) const loadReportes = React.useCallback(async () => { if (!apiBase || state.reportes) return; const data = await refresh("reports.php"); if (data) setState(s => ({ ...s, reportes: data })); }, [apiBase, refresh, state.reportes]); const ctx = { ...state, refresh, loadReportes, addToast }; return ( {children} ); } function useAppData() { const ctx = React.useContext(AppDataContext); if (!ctx) { // Fallback sin provider (modo estático) return { proyectos: PROYECTOS, familias: FAMILIAS, ordenes: ORDENES, insumos: INSUMOS, proveedores: PROVEEDORES, historial: HISTORIAL, alerts: ALERTS_DATA, reportes: null, loading: false, apiReady: false, refresh: () => {}, loadReportes: () => {}, addToast: () => {} }; } return ctx; } // Helper: nombre de proveedor desde lista dinámica function nombreProveedorDyn(id, proveedores) { return (proveedores.find(p => p.id === id) || {}).nombre || "—"; } function nombreProyectoDyn(id, proyectos) { return (proyectos.find(p => p.id === id) || {}).nombre || "—"; } function nombreFamiliaDyn(cod, familias) { return (familias.find(f => f.codigo === cod) || {}).nombre || "—"; } // Spinner de carga function LoadingOverlay() { return (

Cargando datos…

); } Object.assign(window, { ROCA_USER, DEFAULT_THRESHOLDS, FAMILIAS, CATEGORIES_BASE, PROYECTOS, PROVEEDORES, ORDENES, INSUMOS, HISTORIAL, ALERTS_DATA, RECENT_OCS, flagLevel, fmtMoney, flagColor, levelLabel, fmtFolio, fmtFecha, nombreProyecto, nombreProveedor, nombreFamilia, nombreProveedorDyn, nombreProyectoDyn, nombreFamiliaDyn, buildAlertsFromFamilias, Icon, GolfFlag, Sparkline, StatusPagoBadge, PrioridadBadge, FlujoBadge, ToastProvider, useToast, AppDataProvider, useAppData, LoadingOverlay, useState, useEffect, useMemo, useRef, React });