panels-origin/web-panel/middleware/auth.global.ts
Alberto Martinez 90bcd48f95 Facturas, nómina, dispersión e IMSS (#28)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Resumen

Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían.

## Qué queda disponible

- Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC.
- Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada.
- Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios.
- Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos.
- Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar.
- Dispersión: corte semanal, corrección antes de definitiva y comprobante.
- Historial de alta, baja y reingreso en la ficha, más movimientos IMSS.
- Reporte por periodo en Excel y PDF.
- Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel.

## Verificación

`deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-5e23dc3b-921e-4091-9650-7091d3a52bbf&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
2026-09-25 03:26:30 +00:00

68 lines
2 KiB
TypeScript

const ROUTE_PERMISSIONS: Record<string, string> = {
"/": "reports.view",
"/proyectos": "projects.view",
"/obras": "projects.view",
"/presupuesto": "budget.view",
"/programa-obra": "work_program.view",
"/control-presupuesto": "cost_control.view",
"/facturas": "invoices.view",
"/gastos": "expenses.view",
"/almacen": "warehouse.view",
"/kanban": "kanban.view",
"/padron": "workers.view",
"/gafetes": "documents.view",
"/nomina": "payroll.view",
"/configuracion": "settings.view",
"/usuarios": "users.view",
};
export default defineNuxtRouteMiddleware(async (to) => {
if (to.path === "/login") return;
const { user, fetchMe } = useAuth();
const { api } = useApi();
try {
if (!user.value) {
await Promise.race([
fetchMe(),
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 4000)),
]);
}
} catch {
user.value = null;
}
if (!user.value) return navigateTo("/login");
if (user.value.role_code === "platform_admin" && user.value.realm === "platform") {
user.value = null;
try {
await api("/v1/auth/logout", { method: "POST" });
} catch {
/* ignore */
}
if (import.meta.client) {
window.location.href = "http://localhost:3001/login";
return abortNavigation();
}
return navigateTo("/login");
}
if (user.value.must_change_password && to.path !== "/cambiar-password") {
return navigateTo("/cambiar-password");
}
if (!user.value.must_change_password && to.path === "/cambiar-password") {
return navigateTo("/");
}
const perms = usePermissions();
const userScope = useScope();
if (!perms.loaded.value) await perms.load();
if (!userScope.loaded.value) await userScope.load();
const required = Object.entries(ROUTE_PERMISSIONS).find(([path]) =>
path === "/" ? to.path === "/" : to.path === path || to.path.startsWith(`${path}/`),
)?.[1];
if (required && !perms.can(required)) {
return navigateTo("/");
}
});