Commit graph

64 commits

Author SHA1 Message Date
Cursor Agent
8617bdfecf
fix: curva del programa de obra sin agregados SQL anidados
Tras importar Excel el GET /work-program fallaba con 42803 porque
fn_work_program_get hacía jsonb_agg(... SUM(...)). El SUM queda en
una subquery y jsonb_agg afuera.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:29:58 +00:00
Alberto Martinez
4750dab95f Unir fixes de las pruebas (PDF, presupuesto, programa, gastos, almacén, documentos, gafete, nómina)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Unifica los PRs de las pruebas de hoy en una sola rama contra `main`, con el changelog de Liquibase ya reconciliado.

**Incluye**
- #11 ver PDF de proyecto (`doc_id`)
- #12 toast al subir presupuesto que no es Excel
- #13 preview del programa de obra
- #14 ribbon de Control presupuesto (ya venía dentro de #15)
- #15 captura de gastos, proveedores y unidades
- #16 almacén global / abrir-cerrar / catálogo
- #17 documentos sin fecha de emisión ni vencimiento
- #18 gafete: `fn_badge_job_get` + empresa del tenant
- #19 asistencia y faltas L–S en Nómina

**No mergear los PRs sueltos** (#11–#19) si mergeas este: chocan en `changelog-master.xml` porque varios añadieron un changeset `032`. Aquí están todos incluidos con IDs distintos.

**Cómo probar (rápido)**
- Proyecto: abrir un PDF del expediente
- Presupuesto: subir un archivo que no sea Excel y ver el toast
- Programa de obra: importar Excel y ver preview
- Gastos: alta con PrimeVue y reclasificar sin concepto
- Almacén: global + abrir/cerrar obra + catálogo
- Padrón: subir documento sin fechas
- Gafetes: consultar/descargar job y ver el nombre de la empresa
- Nómina → Jornal: columnas L–S para marcar asistencia/falta

Mergea **este** PR. Después se pueden cerrar #11–#19.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?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-d8a5d34a-ead9-4912-8296-322cff799783&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-08 21:23:23 +00:00
Cursor Agent
ad152d46f4
merge: #18 gafete job y empresa del tenant
Añade 032-fn-badge-job-get al changelog junto a los demás 032.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:13:45 +00:00
Cursor Agent
5e4d9a6710
merge: #17 documentos sin fechas de vigencia
Añade 032-document-validity-from-upload junto a los demás 032.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:13:33 +00:00
Cursor Agent
15ffb3ca29
merge: #15+#16 gastos, proveedores y almacén
Conserva los changesets 032 ya unidos y añade 033–035.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:13:02 +00:00
Cursor Agent
9c198a3be7
merge: #19 asistencia y faltas en nómina
Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:12:49 +00:00
Cursor Agent
2f2d23004e
merge: #13 preview programa de obra
Incluye ambos changesets 032 (document-get y work-program) en el changelog.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:12:47 +00:00
Cursor Agent
202eca337b
merge: #12 toast presupuesto no Excel
Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:12:37 +00:00
Cursor Agent
016bc0bcee
merge: #11 ver PDF de proyecto
Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:12:37 +00:00
Cursor Agent
14985ae829
fix: filtrar asistencia del paquete de nómina por tenant
attendance no tiene RLS propio; el JOIN a projects ahora también
restringe por tenant_id para no mezclar marcas de otro cliente.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:59:27 +00:00
Cursor Agent
9c8b6ea19c
fix: devolver días L–S y celdas de asistencia en el paquete de nómina
La UI de Jornal ya tenía checkboxes por día, pero GET /v1/payroll/weeks
no enviaba days ni cells, así que las columnas L–S no se renderizaban
y solo se veía el total Días en cero.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:58:41 +00:00
Cursor Agent
31a7c1cbf5
fix: gafete usa la empresa del tenant y corrige fn_badge_job_get
El ORDER BY del listado de personas refería la tabla w fuera del
subquery (42P01). El frente del gafete ya no imprime Construcciones
Arctec/Zendala: usa el nombre comercial de la empresa del proyecto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:52:35 +00:00
Cursor Agent
e89b51bf43
fix: subir documentos sin fecha de emisión ni vencimiento
La vigencia de 3 meses para alta o reingreso cuenta desde la fecha de
carga. Quien recibe el expediente revisa si el documento es válido.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:47:16 +00:00
Cursor Agent
af5bbd7566
fix: consolidar stock global en cliente si el API aún no agrupa
Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:42:13 +00:00
Cursor Agent
e528d217d3
feat: almacén global, abrir/cerrar por obra y catálogo de materiales
El árbol de Almacén deja de listar todas las obras. El almacén global
siempre está disponible y consolida el stock de los almacenes abiertos.
Las obras abren almacén a demanda y al cerrarlo pueden devolver el
remanente al global. Entradas y traspasos usan el catálogo de materiales
con unidades de medida y uno o varios proveedores.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:41:18 +00:00
Cursor Agent
61e36696b9
feat: reclasificar gastos sin concepto o cambiar de partida
Si un gasto se guardó sin clasificar, ahora se edita desde la lista (clic o Reclasificar) y se le asigna o cambia el concepto del presupuesto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:19:13 +00:00
Cursor Agent
4b5d9572ee
fix: concepto de presupuesto con Select filtrable de PrimeVue
El buscador pasa a Select con filtro, el mismo patrón que proveedor y unidad, para que overlay y tema coincidan con el resto del formulario.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:13:06 +00:00
Cursor Agent
7a31453dfd
fix: overlay del buscador de concepto acotado al campo
El AutoComplete se estiraba con las descripciones largas y tapaba el diálogo. La lista queda al ancho del input, con clave + texto envuelto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:11:56 +00:00
Cursor Agent
1e5bc22936
fix: catálogo de proveedores y unidades en la captura de gastos
Proveedor y unidad pasan a Select de catálogo (no texto libre). Subtotal, IVA y total se calculan al vuelo. El comprobante usa FileUpload de PrimeVue.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:06:45 +00:00
Cursor Agent
6fd673f581
fix: ribbon de Control presupuesto y captura de gastos por concepto
La pantalla de control era solo lectura, pero heredaba los botones de Programa de obra (no-op). Ahora tiene acciones propias, explica que el ejecutado viene de Gastos/almacén, y abre la captura ligada al concepto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:55:37 +00:00
Cursor Agent
b7bd167e08
fix: importar programa de obra sin jsonb_object_length
Postgres no tiene jsonb_object_length; fn_work_program_replace
tronaba al confirmar el Excel. El conteo de semanas ahora usa
un contador en el loop de periodos.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:49:26 +00:00
Cursor Agent
b0a1a3147c
fix: mostrar preview del programa de obra al importar Excel
El Excel Neodata por partida sí se leía (A001–A009, 9 semanas), pero
la API devolvía { ok, data } sin envelope y el panel no desembalaba
data: el diálogo mostraba 0 y «Sin match». Ahora se aplana la
respuesta y el preview lista las partidas encontradas.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:44:35 +00:00
Cursor Agent
fa3656d142
fix: mensajes claros al subir presupuesto que no es Excel
El toast de error salía vacío porque el preview 400 no traía message.
Ahora solo se aceptan .xlsx/.xls, y si el Excel no tiene el formato
de presupuesto se explica qué columnas o plantilla usar.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:34:57 +00:00
Cursor Agent
de07975d4a
fix: abrir documentos de proyecto enviando doc_id a la RPC
La API pedía el PDF con { id } y fn_project_document_get leía
payload->>'doc_id', así que el id llegaba vacío aunque el archivo
ya estuviera en el bucket. Mismo desajuste en empresa y trabajador.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:27:24 +00:00
Alberto Martinez
9ea4706954 IAM UX: dialog fixes, auto role code, permission matrix
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Changes

### IAM dialog UX
- Checkbox / scope fixes in user edit dialog
- Auto-generated role code from label (no manual Código field)

### Permission matrix (detail panel)
Replaces the flat list of 50+ checkboxes with a structured matrix:

- **Grouped by module** (Presupuesto, Gastos, Nómina, Documentos de obra, etc.)
- **Short description** per module explaining what it controls
- **CRUD columns**: Ver / Crear / Editar / Eliminar
- **Tooltips** on each cell describing the specific permission
- **Bulk actions**: Solo ver, Limpiar, Todo / Solo ver per module
- **Search** by module or permission name
- **Plantilla rápida** dropdown (Operativo, Supervisor, Contador, …)

## Test

1. Usuarios IAM → Roles → select a tenant role (not system).
2. Detail panel shows grouped matrix instead of flat list.
3. Use module checkbox or **Todo** / **Solo ver** to toggle a whole area.
4. Hover a cell for tooltip; apply **Plantilla rápida** → **Guardar permisos**.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-0da3751a-38ff-40ab-bba2-5df2322c8cda?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-0da3751a-38ff-40ab-bba2-5df2322c8cda&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-08 19:15:52 +00:00
Cursor Agent
1a50823368
IAM: grouped CRUD permission matrix with hints and bulk actions
Replace the flat checkbox list with modules grouped in a Ver/Crear/Editar/
Eliminar grid, module descriptions, search, view-only shortcuts, and quick
template apply.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:11:14 +00:00
Cursor Agent
2f7f121121
IAM: auto-generate role code from label when creating roles
Remove the manual Código field from the new-role dialog. Prefill label from
the selected template and derive a unique slug code on save.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:08:46 +00:00
Cursor Agent
d9aeee1073
IAM dialog: checkbox text no longer toggles the control
Replace label[for] captions with plain spans so warehouse and scope
options only change when clicking the checkbox itself.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:07:02 +00:00
Cursor Agent
4767676976
Fix scope MultiSelect toggling 'Todas las obras' checkbox
FormField wrapped slot content in a <label>, so clicks on the project
MultiSelect bubbled to the first control inside (scope_all_projects).
Use a div wrapper for multi-control field groups.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:05:19 +00:00
Alberto Martinez
c5f9ac0563 Fix IAM v2 Liquibase migrate failure on deploy
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problem

Coolify deploy built all images successfully but failed when the `migrate` job exited with code 1. The IAM changeset `005-iam-v2-roles-crud.sql` had already applied through `005g` on production; it failed at `005h-swap-roles`.

## Root cause

Changeset `005h` used `splitStatements:true` with `endDelimiter:;` while containing a `DO $$ ... END $$` block. Liquibase splits on every semicolon, which breaks PL/pgSQL blocks and causes the migrate step to fail.

Secondary risks on production data:
- Users without `role_id` after backfill (missing per-tenant `operativo` role)
- Multiple `tenant_admin` users in the same tenant (unique partial index would fail)

## Fix

1. **Move the unique-index `DO` block** to a new changeset `005h2-unique-tenant-admin` with `splitStatements:false`.
2. **Add changeset `005g2-ensure-role-backfill`** (new, safe for DBs that already ran `005g`):
   - Create `operativo` roles for every tenant that has users
   - Re-backfill any remaining `role_id`
   - Demote duplicate tenant admins to `operativo` (keep lowest `id` per tenant)
   - Remove orphan users without `tenant_id`

## Deploy

Merge to `main` and redeploy in Coolify. No manual SQL should be required if the DB stopped at `005h` as expected.

If migrate still fails, capture logs with:

```bash
docker logs migrate-pdyrt8ccp804tfmutefau0k6-<latest> 2>&1 | tail -100
```
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-0da3751a-38ff-40ab-bba2-5df2322c8cda?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-0da3751a-38ff-40ab-bba2-5df2322c8cda&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-08 18:58:33 +00:00
Cursor Agent
a9fbca232d
Fix IAM v2 migrate: split DO block and harden role backfill
The migrate job failed at changeset 005h during Coolify deploy. Liquibase
was splitting the DO $$ block on inner semicolons (splitStatements:true),
which breaks PL/pgSQL. Move the unique-index DO block to 005h2 with
splitStatements:false.

Add 005g2 to create missing operativo roles per tenant, backfill any
remaining role_id values, demote duplicate tenant_admin users to operativo,
and remove orphan users without tenant_id before SET NOT NULL.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 18:57:19 +00:00
Alberto Martinez
0d660c4549 IAM clientes: roles por tenant, matriz CRUD y alcance
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Implements full client-side IAM: per-tenant custom roles, CRUD permission matrix, user/project/warehouse scope, and enforcement across API and web-panel.

## Key changes

- Auth with `role_id`, `is_owner`, status checks; API key without tenant_admin bypass
- IAM REST: users/roles CRUD, reset access, `/v1/me/permissions`, `/v1/me/scope`
- CRUD route gates + project/warehouse scope + project doc category filtering
- Rewritten `usuarios.vue` with ribbon, tree, matrix; dynamic menu via `usePermissions`
- Tests: `iam_test.ts` (9 passing); `deno check` and web-panel build green

Apply migrations `005-iam-v2-roles-crud.sql` and `031-project-doc-types-technical.sql` before deploy.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-0da3751a-38ff-40ab-bba2-5df2322c8cda?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-0da3751a-38ff-40ab-bba2-5df2322c8cda&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-08 18:49:53 +00:00
Cursor Agent
aed2f4531b
Implement client IAM v2: roles per tenant, CRUD matrix, scope, and panel UI
- Add IAM v2 auth with role_id, is_owner, status checks, and API key without tenant_admin bypass
- Enforce CRUD permissions and project/warehouse scope across API routes
- Rewrite usuarios.vue with ribbon, tree, user/role management, and permission matrix
- Add dynamic menu, route guards, usePermissions/useScope composables
- Apply role templates on create; filter project docs by category; hide cost tab without permission
- Add iam_test unit tests; update bootstrap and SaaS tenant admin to use role_id

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 18:16:59 +00:00
Cursor Agent
25ceb6a6bd
Add IAM v2 roles/permissions migration and project doc types
Introduce Liquibase changeset 005 for IAM schema v2: CRUD permission codes,
tenant-scoped roles with system tenant_admin, role_permissions by role_id,
user scope fields and user_projects, RLS policies, and full RPC suite for
roles and users. Add core changeset 031 for technical project document types.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 18:10:45 +00:00
Alberto Martinez
7bcef22d59 Menú agrupado y ribbon tipo mockup
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
El menú principal deja de ser una lista plana que se parte en dos filas. Queda en una sola barra con iconos a la izquierda y grupos:

- Inicio
- Obra (Proyectos, Presupuesto, Programa de obra, Control costos)
- Operación (Gastos, Almacén, Kanban)
- Personal (Padrón, Gafetes, Nómina)
- Administración (Usuarios)

Configuración y empresas siguen en el menú del avatar. Ayuda duplicada se quitó del menú (queda el botón de la derecha).

El ribbon (Nuevo/Editar) se compacta como tarjeta: icono arriba, grupos, hover suave y Alta en azul.

Validación:
- `npm run build` en `web-panel` completó sin errores.
- Capturas del escritorio en `/proyectos`: menú en una fila, dropdown de Obra y ribbon de Acciones.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-3755a070-b0cd-40ee-b616-7493e3911e5c?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-3755a070-b0cd-40ee-b616-7493e3911e5c&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-08 16:57:19 +00:00
Alberto Martinez
496273b41c feat: Programa de obra por proyecto
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Implementa el módulo **Programa de obra** por proyecto, vinculado al presupuesto ya existente.

### Flujo
1. Cargar presupuesto (requisito)
2. Definir fechas de inicio/término en el proyecto
3. Crear programa: **importar Excel Neodata/Opus**, **generar automático** o **crear vacío**
4. Editar en cuadrícula (conceptos con `Barra=` y partidas con erogaciones semanales)
5. Visualizar Gantt resumido y comparativo **programado vs ejecutado**

### Backend
- Tablas: `work_programs`, `work_program_periods`, `work_program_concept_slots`, `work_program_partida_amounts`, `work_program_progress`
- RPC: list/get, replace (import), generate (wizard), update celdas, recalculate partidas, vs-cost
- Parser Excel para formatos **por concepto** (`Barra=`) y **por partida** (montos semanales)
- Tests con fixtures reales del usuario

### Frontend
- Nueva página `/programa-obra` con pestañas: Conceptos, Partidas, Gantt, Programado vs costos
- Diálogos de importación y generación automática
- Menú principal, enlace en ficha de proyecto, banner en presupuesto

### Permisos
Reutiliza `manage_budget` en MVP.

## Test plan
- [x] `deno test api/work_program_excel_test.ts` (5 tests)
- [x] `deno check` API modules
- [x] `npm run build` web-panel
- [ ] Migrar DB (`029`, `030`) en entorno con Postgres
- [ ] Importar Excel por concepto y por partida en obra con presupuesto
- [ ] Generar automático con estrategia keyword
- [ ] Recalcular partidas desde conceptos
- [ ] Verificar tab Programado vs costos con gastos registrados
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-3985fe74-d26b-4506-bd4d-f9ad57374141?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-3985fe74-d26b-4506-bd4d-f9ad57374141&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-08 04:51:09 +00:00
Alberto Martinez
f4667dd325 fix(web-panel): corregir crash post-login y mejorar pantalla de error
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema
Tras iniciar sesión en producción (`panels.mrdev.mx`), el panel mostraba "No se pudo cargar el panel" con:
`TypeError: Cannot read properties of undefined (reading 'filter')`

## Causa
La API devuelve respuestas con envelope RPC (`{ ok, code, data, message, layer }`), pero pantallas legacy como el dashboard esperaban shape plano (`workers`, `projects`, etc.). Al asignar `workers.value = w.workers`, el valor quedaba `undefined` y los `.filter()` en computed fallaban.

## Solución
- Normalizar respuestas en `useApi` con `unwrapApiPayload()` para compatibilidad con pantallas existentes
- Endurecer `index.vue` con arrays por defecto y manejo de errores en carga
- Rediseñar la pantalla de error global: mensaje "¡Oops! Algo falló", código de error, resumen legible y detalle técnico colapsable

## Deploy
Reconstruir y desplegar `web-panel`. Si las migraciones `027`/`028` no están en prod, aplicar `./db/update.sh core iam`.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?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-eeec3b5c-f789-43e8-a353-0755e4706377&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-04 06:57:33 +00:00
Cursor Agent
1f85b44e80
fix(web-panel): unwrap API envelope post-login and improve error screen
- Normalize RPC responses in useApi so legacy pages receive flat shapes
- Harden dashboard index.vue against undefined arrays and load failures
- Redesign Nuxt error boundary with Oops message, error code, and retry

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-04 06:56:25 +00:00
Alberto Martinez
41273dfb04 Gastos, almacén y control presupuestal
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Validación de endpoints y SPs

Se probaron todos los endpoints nuevos (gastos, almacén, control presupuestal, IAM) y sus RPCs asociados.

### Bugs corregidos en migraciones/SQL

1. **IAM grants en schema core** — `iam-004e` intentaba `GRANT` sobre `core` con rol `iam_owner` (sin permiso). Los grants cruzados se movieron a `core-027-iam-rpc-cross-grants.sql`.
2. **`_cost_settings` ambiguo** — columnas `budget_warn_pct` etc. colisionaban con `RETURNS TABLE` en PL/pgSQL, rompiendo `fn_expense_create` y control presupuestal. Corregido en `core-028-fix-cost-settings-ambiguous.sql`.

### Tests añadidos

- `api/cost_modules_test.ts` — CRUD RPC gastos, flujo almacén completo, control presupuestal, IAM permisos
- `scripts/crud-smoke-test.sh` — smoke HTTP de todos los endpoints nuevos

### Resultados

- `deno test cost_modules_test.ts` — 5/5 OK
- `./scripts/crud-smoke-test.sh` — todos los checks OK (gastos CRUD, IVA, cost-settings, almacén, transferencias, cost-control, IAM)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?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-eeec3b5c-f789-43e8-a353-0755e4706377&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-04 04:29:02 +00:00
Cursor Agent
130decd48c
test: validar endpoints y RPCs de gastos, almacén e IAM
- Añade cost_modules_test.ts con CRUD de gastos, almacén, control presupuestal e IAM
- Amplía crud-smoke-test.sh para cubrir todos los endpoints HTTP nuevos
- Corrige migración IAM: grants de core.rpc_* movidos a changeset core-027
- Corrige ambigüedad PL/pgSQL en _cost_settings que rompía fn_expense_create

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-04 04:17:47 +00:00
Cursor Agent
b8e97b5137
fix(api): corregir tipos en iam_http y warehouse_http para deno check
Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-04 04:12:43 +00:00
Cursor Agent
ab89727522
feat: gastos, almacén central/obra y control presupuestal
Implementa el módulo integral de costos según el plan acordado:

- Esquema Liquibase: expense_entries, almacén (central + obra), IVA,
  tenant_cost_settings, permisos IAM y RLS
- Funciones RPC core: gastos, almacén, control presupuestal, sync nómina,
  cierre de almacén al concluir proyecto
- API HTTP: expenses_http, warehouse_http, cost_control_http, iam_http
- Middleware requirePermission con matriz IAM
- UI web-panel: /gastos, /almacen, /control-presupuesto, /usuarios
- Componentes BudgetItemPicker, CostSemaphore y semáforos CSS
- Smoke tests extendidos para gastos/almacén/control

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-04 04:04:17 +00:00
Alberto Martinez
b9bf7044b2 refactor(core): migrate core CRUD to PostgreSQL RPC + unified HTTP errors
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Migrates the `core` schema business logic from inline `db.prepare()` calls in the API to PostgreSQL RPC functions (`core.fn_*`) with a unified JSON envelope for errors and HTTP status mapping.

### Database (Liquibase changesets 006–017)

- **006** — RPC infra: `rpc_ok`, `rpc_err`, `rpc_created`, `rpc_from_exception`
- **007** — Catalogs and tenant settings
- **008** — Companies CRUD
- **009** — Projects, checklists, document lists
- **010** — Workers CRUD, pipeline, checklist, assign
- **011** — Budget CRUD + `fn_budget_replace`
- **012** — Badge jobs
- **013** — Payroll (settings, attendance, loans, weeks, destajo)
- **014** — Worker import batch + document store
- **015** — Project/company/worker document metadata RPCs
- **016–017** — Fixes: `needs_badge` default on worker create; Liquibase `splitStatements:false` on function changesets

### API

- `api/rpc.ts` — `callCoreFn()`, `RpcCallError` (jsonb payload fix: pass JS object, not `JSON.stringify`)
- `api/http_errors.ts` — `mapRpcToStatus()`, `respondRpc()`, `respondApiError()`, `onAppError()`
- Refactored: `main.ts`, `companies.ts`, `db.ts`, `budget.ts`, `excel.ts`, `payroll.ts`, `payroll_http.ts`
- Front helpers: `web-panel/composables/api-response.ts`, `web-saas/composables/api-response.ts`

### Envelope contract

DB functions return `{ ok, code, layer: "db", message, context, data, errors }`. The API adds `status` (HTTP code) via `respondRpc()` / `respondApiError()`.

### Out of scope

`iam`, `platform`, `saas.ts`, auth/sessions, S3, PDF generation, Excel parsing, and bootstrap scripts still use direct SQL where appropriate.

## Test plan

- [x] `deno check main.ts` — compila sin errores de tipos
- [x] `npm run build` — web-panel y web-saas compilan
- [x] `deno test` — 25 tests unitarios (http_errors, companies, budget, mx, document_validity)
- [x] Liquibase migrations `006`–`017` aplicadas en Postgres local (`--context-filter=dev`)
- [x] API levantada localmente; `/v1/health` OK
- [x] Smoke CRUD vía `scripts/crud-smoke-test.sh`: empresas, proyectos, trabajadores, catálogos (create/get/list/patch)
- [ ] Import Excel de trabajadores (flujo multipart + S3/local storage)
- [ ] Import presupuesto desde Excel
- [ ] Flujo nómina: asistencia → cerrar semana
- [ ] CI en el remoto (sin checks reportados aún)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-06667c14-38e8-42a8-9ed9-6b1322f12ae7?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-06667c14-38e8-42a8-9ed9-6b1322f12ae7&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-04 02:34:43 +00:00
Alberto Martinez
b596bb70cd Migrar a PostgreSQL, Redis y R2
Some checks failed
CI / api (push) Has been cancelled
CI / web (web-panel) (push) Has been cancelled
CI / web (web-saas) (push) Has been cancelled
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Resumen

Sustituye SQLite por **PostgreSQL** (2 bases: `panels_platform` + `panels_product` / esquemas `iam` y `core`), **Redis** (sesiones y cache) y **Cloudflare R2** para archivos. Validado en Coolify: Liquibase en el job `migrate`, API healthy, login SaaS tras `bootstrap-admin.ts platform`.

## Qué entra

- Changelogs Liquibase Postgres, roles `_owner` / `_app`, RLS por `tenant_id`.
- API Deno con postgres.js + Redis; sin FFI SQLite ni JRE en la imagen de tráfico.
- `docker-compose.yml` de Coolify: `migrate` (one-shot, `!dev`) → `api` → `web-panel` / `web-saas`. Overlay local en `overlays/compose.local.yml` (no mezclar en Coolify).
- `migrate` y `api` unidos a la red `coolify` para resolver UUIDs internos de Postgres/Redis.
- R2: ListObjects (no HeadBucket); Deno `--allow-sys` para el SDK AWS.

## Coolify / ops (no olvidar)

- Compose **solo** `docker-compose.yml`. Hosts = URL **internal**, no CONTAINER ID.
- 6 `DATABASE_URL_*` + 2 `REDIS_URL_*` expandidas; `DOCS_KEY` 64 hex; `SESSION_SECRET`; `S3_*`; `DENO_ENV=production`.
- Primer admin SaaS: `docker exec` → `bootstrap-admin.ts platform`. Usuario `admin` / `SEED_PASSWORD`. No se crea en cada boot.

## Cómo probar

- `/v1/health` con core, platform, redis y `storage.backend: "s3"`.
- Login SaaS `admin` + `SEED_PASSWORD` tras el bootstrap.

Base: `main`.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-556d1a5c-47ed-4a0b-bd66-06f84a8f41e1?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-556d1a5c-47ed-4a0b-bd66-06f84a8f41e1&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-03 06:02:38 +00:00
Cursor Agent
42bba34469
api: --allow-sys para el SDK de AWS/R2 en Deno
El cliente S3 lee osRelease; sin --allow-sys el ping a R2 falla
con NotCapable aunque endpoint y bucket estén bien.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:49:14 +00:00
Cursor Agent
ab20701fea
api: ping R2 con ListObjects, no HeadBucket
Cloudflare R2 suele devolver 403 en HeadBucket con token acotado
al bucket, y eso tumba el fail-fast de arranque. El ping ahora
lista o escribe una sonda y registra el error S3.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:42:13 +00:00
Cursor Agent
ccf86c5b2e
api: etiquetar fallos de arranque (postgres, redis, s3)
Tras Liquibase, Coolify marca api unhealthy si el fail-fast tira
antes de Deno.serve. Los logs ahora dicen qué ping falló.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:32:06 +00:00
Cursor Agent
b68313c5ee
compose: unir migrate y api a la red coolify
El host qzegekm3sr2bevgxl4wh4th2 es el UUID interno de Postgres;
solo resuelve en esa red. Sin ella Liquibase lanza UnknownHostException.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:25:53 +00:00
Cursor Agent
05d1a8c133
migrate: rechazar CONTAINER ID y fallar si el host no resuelve
Coolify no imprime stdout de Liquibase; el job migrate seguía
saliendo 1 por DNS. Preflight deja el host JDBC en los logs del
contenedor y corta si parece un ID de 12 caracteres.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:24:51 +00:00
Cursor Agent
05ea4e942f
ops: sacar el overlay local del glob docker-compose*.yml
Coolify Reload Compose mezclaba docker-compose.local.yml y marcaba
PLATFORM_*_PASSWORD / IAM_* / CORE_* / *_REDIS_PASSWORD como Required.
Esas variables son solo para provision local; en Coolify van las URLs.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:19:10 +00:00