Update project including dist
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user