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>
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>
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>
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>
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>
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>
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>