Update project including dist

This commit is contained in:
Isaac Aracena
2026-08-20 14:19:35 -04:00
commit 7262b8de30
48 changed files with 9759 additions and 0 deletions
+399
View File
@@ -0,0 +1,399 @@
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<string>(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<T>(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<GlmForm>;
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<GlmForm[]> {
const rows = await callAuthenticatedRpc<RpcFormRow[]>("cruces_glm_get_forms");
return rows.map(fromRpcRow).filter((form): form is GlmForm => form !== null);
}
async function rpcGetForm(formId: string): Promise<GlmForm | null> {
const rows = await callAuthenticatedRpc<RpcFormRow[]>("cruces_glm_get_form", {
p_form_id: formId,
});
return fromRpcRow(rows[0] ?? {});
}
async function rpcSaveForm(form: GlmForm): Promise<GlmForm> {
const normalizado = normalizarFormulario(form);
const rows = await callAuthenticatedRpc<RpcFormRow[]>("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<void> {
await callAuthenticatedRpc<boolean>("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<GlmForm[]>(
DEMO_AUTH_ENABLED ? FORMULARIOS_SEED : [],
);
const [favoritos, setFavoritos] = useState<string[]>([]);
const [recientes, setRecientes] = useState<string[]>([]);
const [listo, setListo] = useState(false);
const [errorCarga, setErrorCarga] = useState<string | null>(null);
const cargar = useCallback(async () => {
setErrorCarga(null);
try {
if (DEMO_AUTH_ENABLED) {
const cargados = normalizarFormularios(
read<GlmForm[]>(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<string[]>(FAVS_KEY, []));
setRecientes(read<string[]>(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<GlmForm | null> {
if (DEMO_AUTH_ENABLED) {
const forms = normalizarFormularios(read<GlmForm[]>(LEGACY_FORMS_KEY, FORMULARIOS_SEED));
return forms.find((form) => form.id === formId) ?? null;
}
return rpcGetForm(formId);
}