panels-origin/db/core/changesets/040-fn-worker-document-delete.sql
Alberto Martinez 34c0da30d8 fix(padron): vista previa de documentos, eliminar y reemplazar por tipo (#26)
<!-- 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>&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-16 21:17:39 +00:00

63 lines
2.2 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · eliminar documento de expediente (trabajador)
--changeset panel:core-040a-fn-worker-document-delete splitStatements:false
CREATE OR REPLACE FUNCTION core.fn_worker_document_delete(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
v_doc_id bigint := NULLIF(payload->>'doc_id', '')::bigint;
v_doc documents%ROWTYPE;
v_next_id bigint;
BEGIN
IF v_worker_id IS NULL OR v_doc_id IS NULL THEN
RETURN core.rpc_err(
'VALIDATION',
'fn_worker_document_delete: worker_id y doc_id son obligatorios',
jsonb_build_object('fn', 'fn_worker_document_delete', 'worker_id', v_worker_id, 'doc_id', v_doc_id)
);
END IF;
SELECT * INTO v_doc FROM documents WHERE id = v_doc_id AND worker_id = v_worker_id;
IF NOT FOUND THEN
RETURN core.rpc_err(
'NOT_FOUND',
format('fn_worker_document_delete: documento id=%s no encontrado para trabajador %s', v_doc_id, v_worker_id),
jsonb_build_object('fn', 'fn_worker_document_delete', 'doc_id', v_doc_id, 'worker_id', v_worker_id)
);
END IF;
DELETE FROM documents WHERE id = v_doc_id AND worker_id = v_worker_id;
IF v_doc.is_current THEN
SELECT id INTO v_next_id
FROM documents
WHERE worker_id = v_worker_id
AND type_code = v_doc.type_code
ORDER BY uploaded_at DESC, id DESC
LIMIT 1;
IF v_next_id IS NOT NULL THEN
UPDATE documents SET is_current = true WHERE id = v_next_id;
END IF;
END IF;
RETURN core.rpc_ok(
jsonb_build_object(
'deleted_id', v_doc_id,
'worker_id', v_worker_id,
'type_code', v_doc.type_code,
'storage_name', v_doc.storage_name
),
format('Documento id=%s eliminado del expediente del trabajador %s', v_doc_id, v_worker_id),
jsonb_build_object('fn', 'fn_worker_document_delete', 'doc_id', v_doc_id, 'worker_id', v_worker_id)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_worker_document_delete', SQLSTATE, SQLERRM);
END;
$$;
--changeset panel:core-040b-fn-worker-document-delete-grant endDelimiter:; splitStatements:true
GRANT EXECUTE ON FUNCTION core.fn_worker_document_delete(jsonb) TO panels_core_app;