Contrasta las observaciones de arctec-ar con gastos, proveedores y nómina actuales, y deja el orden de implementación reutilizando el catálogo de proveedores.
Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema
En el expediente del padrón, al ver un documento el navegador lo trataba como descarga (cabeceras `application/octet-stream` + `attachment` y blob sin MIME). No había forma de eliminar archivos cargados y al subir otro del mismo tipo algunos entornos fallaban o no refrescaban bien la lista vigente.
## Cambios
- **API:** los GET de documentos (trabajador, proyecto, empresa) envían el `Content-Type` real del archivo y `Content-Disposition: inline` por defecto (`?disposition=attachment` para forzar descarga).
- **UI:** `DocumentPreviewDialog` pide `disposition=inline` y reconstruye el blob con MIME correcto para que PDF/imagen se vean en el diálogo.
- **Eliminar:** RPC `core.fn_worker_document_delete`, `DELETE /v1/workers/:id/documents/:docId`, permiso `documents.delete` (incluido en `manage_documents`), botón de papelera en vigentes e histórico.
- **Reemplazar:** `fn_worker_document_store` marca como no vigentes **todas** las filas del mismo tipo antes del INSERT; se limpia el `FileUpload` tras subir; filtro `is_current` tolera booleano o `1`.
## Migraciones
- Core: `040-fn-worker-document-delete.sql`, `041-fn-worker-document-store-replace.sql`
- IAM: `006-documents-delete-permission.sql`
## Verificación
Tras aplicar Liquibase, en un trabajador del padrón: abrir ojo en PDF/JPG (debe verse en modal), eliminar un doc de prueba, subir dos veces el mismo tipo (debe quedar una vigente y la anterior en histórico).
<!-- 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> <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> </div>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
<!-- 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> <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> </div>
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>
- 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>
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>
- 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>
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>
<!-- 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> <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> </div>