import { useCallback, useEffect, useState } from "react"; import { DEMO_AUTH_ENABLED } from "@/lib/auth"; import { callAuthenticatedRpc } from "@/lib/supabase-api"; export type GlmForm = { id: string; nombre: string; descripcion: string; categoria: string; url: string; activo: boolean; }; export const CATEGORIAS = ["Seguridad Social", "IR", "Reportes"] as const; // Estos datos solo son el fallback del modo demostrativo. En modo real la fuente // de verdad es public.cruces_glm_forms en Supabase. export const FORMULARIOS_SEED: GlmForm[] = [ { id: "cruce-facturacion-clientes", nombre: "Cruce de facturación vs. órdenes de cliente", descripcion: "Compara las facturas emitidas contra las órdenes de compra registradas y devuelve las diferencias por cuenta.", categoria: "Reportes", url: "https://n8n.example.com/form/cruce-facturacion-clientes", activo: true, }, { id: "cruce-proveedores-pagos", nombre: "Cruce de proveedores vs. pagos ejecutados", descripcion: "Valida que cada factura de proveedor cargada tenga su pago conciliado en el período seleccionado.", categoria: "Reportes", url: "https://n8n.example.com/form/cruce-proveedores-pagos", activo: true, }, { id: "conciliacion-bancaria", nombre: "Conciliación bancaria automática", descripcion: "Sube el extracto bancario y el libro mayor para obtener partidas conciliadas y pendientes.", categoria: "Reportes", url: "https://n8n.example.com/form/conciliacion-bancaria", activo: true, }, { id: "solicitud-orden-compra", nombre: "Solicitud de orden de compra", descripcion: "Registra una nueva solicitud de compra y dispara el flujo de aprobación con el área responsable.", categoria: "Reportes", url: "https://n8n.example.com/form/solicitud-orden-compra", activo: true, }, { id: "carga-facturas-proveedor", nombre: "Carga de facturas de proveedor", descripcion: "Envía la factura en PDF y sus datos fiscales para que el flujo la extraiga y la registre.", categoria: "Reportes", url: "https://n8n.example.com/form/carga-facturas-proveedor", activo: true, }, { id: "reporte-cuentas-por-cobrar", nombre: "Reporte de cuentas por cobrar", descripcion: "Genera el aging de cartera por cliente y lo envía por correo al cierre del período elegido.", categoria: "Reportes", url: "https://n8n.example.com/form/reporte-cuentas-por-cobrar", activo: true, }, ]; const LEGACY_FORMS_KEY = "glm-hub-formularios-v1"; const MIGRATION_KEY = "cruces-glm-supabase-forms-migrated-v1"; const FAVS_KEY = "glm-hub-favoritos-v1"; const RECENT_KEY = "glm-hub-recientes-v1"; const CATEGORIA_SET = new Set(CATEGORIAS); type RpcFormRow = { id?: string; nombre?: string; descripcion?: string; categoria?: string; url?: string; activo?: boolean; }; function normalizarCategoria(categoria: string) { return CATEGORIA_SET.has(categoria) ? categoria : "Reportes"; } function normalizarFormulario(form: GlmForm): GlmForm { return { ...form, categoria: normalizarCategoria(form.categoria), }; } function normalizarFormularios(formularios: GlmForm[]) { return formularios.map(normalizarFormulario); } function fromRpcRow(row: RpcFormRow): GlmForm | null { if ( typeof row.id !== "string" || typeof row.nombre !== "string" || typeof row.descripcion !== "string" || typeof row.categoria !== "string" || typeof row.url !== "string" || typeof row.activo !== "boolean" ) { return null; } return normalizarFormulario({ id: row.id, nombre: row.nombre, descripcion: row.descripcion, categoria: row.categoria, url: row.url, activo: row.activo, }); } function read(key: string, fallback: T): T { if (typeof window === "undefined") return fallback; try { const raw = window.localStorage.getItem(key); return raw ? (JSON.parse(raw) as T) : fallback; } catch { return fallback; } } function write(key: string, value: unknown) { if (typeof window === "undefined") return; try { window.localStorage.setItem(key, JSON.stringify(value)); } catch { /* almacenamiento no disponible */ } } function hasMigrationMarker() { if (typeof window === "undefined") return true; return window.localStorage.getItem(MIGRATION_KEY) === "1"; } function markMigrationComplete() { if (typeof window === "undefined") return; window.localStorage.setItem(MIGRATION_KEY, "1"); } function readLegacyFormsIfPresent(): GlmForm[] | null { if (typeof window === "undefined") return null; const raw = window.localStorage.getItem(LEGACY_FORMS_KEY); if (!raw) return null; try { const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) return null; const forms = parsed .filter((item): item is GlmForm => { if (!item || typeof item !== "object") return false; const row = item as Partial; return ( typeof row.id === "string" && typeof row.nombre === "string" && typeof row.descripcion === "string" && typeof row.categoria === "string" && typeof row.url === "string" && typeof row.activo === "boolean" ); }) .map(normalizarFormulario); return forms; } catch { return null; } } async function rpcListForms(): Promise { const rows = await callAuthenticatedRpc("cruces_glm_get_forms"); return rows.map(fromRpcRow).filter((form): form is GlmForm => form !== null); } async function rpcGetForm(formId: string): Promise { const rows = await callAuthenticatedRpc("cruces_glm_get_form", { p_form_id: formId, }); return fromRpcRow(rows[0] ?? {}); } async function rpcSaveForm(form: GlmForm): Promise { const normalizado = normalizarFormulario(form); const rows = await callAuthenticatedRpc("cruces_glm_save_form", { p_form_id: normalizado.id, p_nombre: normalizado.nombre, p_descripcion: normalizado.descripcion, p_categoria: normalizado.categoria, p_url: normalizado.url, p_activo: normalizado.activo, }); const saved = fromRpcRow(rows[0] ?? {}); if (!saved) throw new Error("Supabase no devolvió el formulario guardado."); return saved; } async function rpcDeleteForm(formId: string): Promise { await callAuthenticatedRpc("cruces_glm_delete_form", { p_form_id: formId, }); } async function migrateLegacyFormsIfNeeded(canManageForms: boolean, remoteForms: GlmForm[]) { if (DEMO_AUTH_ENABLED || hasMigrationMarker()) return remoteForms; if (remoteForms.length > 0) { markMigrationComplete(); return remoteForms; } // Solo un administrador puede migrar la información histórica del navegador. if (!canManageForms) return remoteForms; const legacyForms = readLegacyFormsIfPresent(); if (!legacyForms || legacyForms.length === 0) { markMigrationComplete(); return remoteForms; } for (const form of legacyForms) { await rpcSaveForm(form); } markMigrationComplete(); return rpcListForms(); } export function slugify(value: string) { return ( value .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") .slice(0, 48) || `formulario-${Date.now()}` ); } export function useFormularios(canManageForms = false) { const [formularios, setFormularios] = useState( DEMO_AUTH_ENABLED ? FORMULARIOS_SEED : [], ); const [favoritos, setFavoritos] = useState([]); const [recientes, setRecientes] = useState([]); const [listo, setListo] = useState(false); const [errorCarga, setErrorCarga] = useState(null); const cargar = useCallback(async () => { setErrorCarga(null); try { if (DEMO_AUTH_ENABLED) { const cargados = normalizarFormularios( read(LEGACY_FORMS_KEY, FORMULARIOS_SEED), ); setFormularios(cargados); write(LEGACY_FORMS_KEY, cargados); } else { let cargados = await rpcListForms(); cargados = await migrateLegacyFormsIfNeeded(canManageForms, cargados); setFormularios(cargados); } } catch (error) { setFormularios([]); setErrorCarga( error instanceof Error ? error.message : "No se pudieron cargar los formularios desde Supabase.", ); } finally { setListo(true); } }, [canManageForms]); useEffect(() => { setFavoritos(read(FAVS_KEY, [])); setRecientes(read(RECENT_KEY, [])); void cargar(); }, [cargar]); useEffect(() => { if (DEMO_AUTH_ENABLED || typeof window === "undefined") return; const refreshOnFocus = () => { if (document.visibilityState === "visible") void cargar(); }; window.addEventListener("focus", refreshOnFocus); document.addEventListener("visibilitychange", refreshOnFocus); return () => { window.removeEventListener("focus", refreshOnFocus); document.removeEventListener("visibilitychange", refreshOnFocus); }; }, [cargar]); const guardar = useCallback(async (form: GlmForm) => { const normalizado = normalizarFormulario(form); if (DEMO_AUTH_ENABLED) { setFormularios((prev) => { const existe = prev.some((f) => f.id === normalizado.id); const next = existe ? prev.map((f) => (f.id === normalizado.id ? normalizado : f)) : [normalizado, ...prev]; write(LEGACY_FORMS_KEY, next); return next; }); return normalizado; } const saved = await rpcSaveForm(normalizado); setFormularios((prev) => { const existe = prev.some((f) => f.id === saved.id); return existe ? prev.map((f) => (f.id === saved.id ? saved : f)) : [saved, ...prev]; }); return saved; }, []); const eliminar = useCallback(async (id: string) => { if (DEMO_AUTH_ENABLED) { setFormularios((prev) => { const next = prev.filter((f) => f.id !== id); write(LEGACY_FORMS_KEY, next); return next; }); return; } await rpcDeleteForm(id); setFormularios((prev) => prev.filter((f) => f.id !== id)); setFavoritos((prev) => { const next = prev.filter((formId) => formId !== id); write(FAVS_KEY, next); return next; }); setRecientes((prev) => { const next = prev.filter((formId) => formId !== id); write(RECENT_KEY, next); return next; }); }, []); const alternarFavorito = useCallback((id: string) => { setFavoritos((prev) => { const next = prev.includes(id) ? prev.filter((f) => f !== id) : [...prev, id]; write(FAVS_KEY, next); return next; }); }, []); const registrarUso = useCallback((id: string) => { setRecientes((prev) => { const next = [id, ...prev.filter((r) => r !== id)].slice(0, 6); write(RECENT_KEY, next); return next; }); }, []); return { formularios, favoritos, recientes, listo, errorCarga, guardar, eliminar, alternarFavorito, registrarUso, recargar: cargar, }; } export async function obtenerFormularioPorId(formId: string): Promise { if (DEMO_AUTH_ENABLED) { const forms = normalizarFormularios(read(LEGACY_FORMS_KEY, FORMULARIOS_SEED)); return forms.find((form) => form.id === formId) ?? null; } return rpcGetForm(formId); }