mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:43:18 +00:00
fix(padron): vista previa inline, eliminar y reemplazar documentos
- Sirve documentos con Content-Type real e inline por defecto (query disposition) - DocumentPreviewDialog reconstruye blob con MIME correcto para PDF/imagen - RPC y DELETE para eliminar documentos del expediente; botón en UI - Permiso IAM documents.delete; store archiva todas las versiones previas del tipo - Limpia FileUpload tras subir; is_current robusto en listas vigente/histórico Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
parent
cb04a9a04c
commit
17d9393b9e
11 changed files with 283 additions and 16 deletions
24
api/document_serve.ts
Normal file
24
api/document_serve.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/** Cabeceras HTTP para bytes de documento cifrado ya descifrados. */
|
||||
export function documentServeHeaders(
|
||||
doc: { original_name: string; mime?: string },
|
||||
opts: { inline?: boolean } = {},
|
||||
): Record<string, string> {
|
||||
const inline = opts.inline !== false;
|
||||
const mime = (doc.mime || "").trim() || "application/octet-stream";
|
||||
const name = doc.original_name || "documento";
|
||||
const disposition = inline ? "inline" : "attachment";
|
||||
return {
|
||||
"Content-Type": mime,
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Content-Disposition": `${disposition}; filename="${encodeURIComponent(name)}"`,
|
||||
"Cache-Control": "private, max-age=60",
|
||||
};
|
||||
}
|
||||
|
||||
export function wantsInlineDocumentDisposition(url: URL | string): boolean {
|
||||
const u = typeof url === "string" ? new URL(url, "http://local") : url;
|
||||
const d = (u.searchParams.get("disposition") || u.searchParams.get("inline") || "").toLowerCase();
|
||||
if (d === "attachment" || d === "download" || d === "0" || d === "false") return false;
|
||||
if (d === "inline" || d === "1" || d === "true") return true;
|
||||
return true;
|
||||
}
|
||||
13
api/document_serve_test.ts
Normal file
13
api/document_serve_test.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { documentServeHeaders, wantsInlineDocumentDisposition } from "./document_serve.ts";
|
||||
|
||||
Deno.test("documentServeHeaders inline uses mime", () => {
|
||||
const h = documentServeHeaders({ original_name: "a.pdf", mime: "application/pdf" }, { inline: true });
|
||||
if (h["Content-Type"] !== "application/pdf") throw new Error(h["Content-Type"]);
|
||||
if (!h["Content-Disposition"].startsWith("inline;")) throw new Error(h["Content-Disposition"]);
|
||||
});
|
||||
|
||||
Deno.test("wantsInlineDocumentDisposition", () => {
|
||||
if (!wantsInlineDocumentDisposition("http://x/doc")) throw new Error("default inline");
|
||||
if (wantsInlineDocumentDisposition("http://x/doc?disposition=attachment")) throw new Error("attachment");
|
||||
if (!wantsInlineDocumentDisposition("http://x/doc?disposition=inline")) throw new Error("inline");
|
||||
});
|
||||
15
api/excel.ts
15
api/excel.ts
|
|
@ -247,6 +247,21 @@ export async function storeDocument(
|
|||
await refreshPipeline(db, workerId);
|
||||
}
|
||||
|
||||
export async function deleteWorkerDocument(
|
||||
db: Db,
|
||||
workerId: number,
|
||||
docId: number,
|
||||
): Promise<{ storage_name: string }> {
|
||||
const env = await callCoreFn<{ storage_name?: string }>(db, "core.fn_worker_document_delete", {
|
||||
worker_id: workerId,
|
||||
doc_id: docId,
|
||||
});
|
||||
if (!env.ok) throw new RpcCallError(env);
|
||||
const storage = String(env.data?.storage_name ?? "");
|
||||
await refreshPipeline(db, workerId);
|
||||
return { storage_name: storage };
|
||||
}
|
||||
|
||||
export async function storeProjectDocument(
|
||||
db: Db,
|
||||
projectId: number,
|
||||
|
|
|
|||
56
api/main.ts
56
api/main.ts
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from "./db.ts";
|
||||
import { callCoreFn } from "./rpc.ts";
|
||||
import { normalizeDocumentGetPayload } from "./document_rpc.ts";
|
||||
import { documentServeHeaders, wantsInlineDocumentDisposition } from "./document_serve.ts";
|
||||
import {
|
||||
respondRpc,
|
||||
respondApiError,
|
||||
|
|
@ -50,7 +51,7 @@ import { createTenant, getTenantDetail, issueTenantAdminAccess, listTenants, upd
|
|||
import { smtpConfigured, testSmtp } from "./mail.ts";
|
||||
import { saveSmtpSettings, smtpPublicView } from "./smtp.ts";
|
||||
import { decryptBytes } from "./docs_crypto.ts";
|
||||
import { importExcel, storeDocument, storeProjectDocument, storeCompanyDocument, buildImportTemplate } from "./excel.ts";
|
||||
import { importExcel, storeDocument, storeProjectDocument, storeCompanyDocument, buildImportTemplate, deleteWorkerDocument } from "./excel.ts";
|
||||
import {
|
||||
normalizeWorker,
|
||||
validateCurp,
|
||||
|
|
@ -81,7 +82,7 @@ import {
|
|||
importBudgetExcel,
|
||||
listBudget,
|
||||
} from "./budget.ts";
|
||||
import { badgeJobPdfKey, companyDocKey, getObject, pingStorage, projectDocKey, workerDocKey } from "./storage.ts";
|
||||
import { badgeJobPdfKey, companyDocKey, deleteObject, getObject, pingStorage, projectDocKey, workerDocKey } from "./storage.ts";
|
||||
import { cacheCore, cacheKeyCore } from "./cache.ts";
|
||||
import { pingRedis } from "./redis.ts";
|
||||
|
||||
|
|
@ -128,6 +129,17 @@ async function rpcDocumentForDownload(
|
|||
return { doc, envelope };
|
||||
}
|
||||
|
||||
function applyDocumentResponseHeaders(
|
||||
c: { header: (name: string, value: string) => void; req: { url: string } },
|
||||
doc: StorageDoc,
|
||||
inline?: boolean,
|
||||
) {
|
||||
const useInline = inline ?? wantsInlineDocumentDisposition(c.req.url);
|
||||
for (const [name, value] of Object.entries(documentServeHeaders(doc, { inline: useInline }))) {
|
||||
c.header(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
async function readExcelUpload(
|
||||
c: { req: { formData: () => Promise<FormData> } },
|
||||
): Promise<{ file: File; bytes: Uint8Array } | { error: string }> {
|
||||
|
|
@ -473,9 +485,7 @@ app.get("/v1/companies/:id/documents/:docId", ...requireCoreAuth, requirePermiss
|
|||
if (!doc) return respondRpc(c, envelope);
|
||||
const enc = await getObject(companyDocKey(companyId, doc.storage_name));
|
||||
const plain = await decryptBytes(doc.iv, enc);
|
||||
c.header("Content-Type", "application/octet-stream");
|
||||
c.header("X-Content-Type-Options", "nosniff");
|
||||
c.header("Content-Disposition", `attachment; filename="${encodeURIComponent(doc.original_name)}"`);
|
||||
applyDocumentResponseHeaders(c, doc);
|
||||
return c.body(plain.buffer as ArrayBuffer);
|
||||
});
|
||||
|
||||
|
|
@ -717,9 +727,7 @@ app.get("/v1/projects/:id/documents/:docId", ...requireCoreAuth, requirePermissi
|
|||
if (!await denyUnlessProjectDocView(c, db, typeCode)) return;
|
||||
const enc = await getObject(projectDocKey(projectId, doc.storage_name));
|
||||
const plain = await decryptBytes(doc.iv, enc);
|
||||
c.header("Content-Type", "application/octet-stream");
|
||||
c.header("X-Content-Type-Options", "nosniff");
|
||||
c.header("Content-Disposition", `attachment; filename="${encodeURIComponent(doc.original_name)}"`);
|
||||
applyDocumentResponseHeaders(c, doc);
|
||||
return c.body(plain.buffer as ArrayBuffer);
|
||||
});
|
||||
|
||||
|
|
@ -1271,12 +1279,38 @@ app.get("/v1/workers/:id/documents/:docId", ...requireCoreAuth, requirePermissio
|
|||
if (!doc) return respondRpc(c, envelope);
|
||||
const enc = await getObject(workerDocKey(workerId, doc.storage_name));
|
||||
const plain = await decryptBytes(doc.iv, enc);
|
||||
c.header("Content-Type", "application/octet-stream");
|
||||
c.header("X-Content-Type-Options", "nosniff");
|
||||
c.header("Content-Disposition", `attachment; filename="${encodeURIComponent(doc.original_name)}"`);
|
||||
applyDocumentResponseHeaders(c, doc);
|
||||
return c.body(plain.buffer as ArrayBuffer);
|
||||
});
|
||||
|
||||
app.delete("/v1/workers/:id/documents/:docId", ...requireCoreAuth, requirePermission("documents.delete"), async (c) => {
|
||||
const workerId = Number(c.req.param("id"));
|
||||
const docId = Number(c.req.param("docId"));
|
||||
const db = c.get("db");
|
||||
const tid = tenantScope(c.get("user"));
|
||||
const route = routeLabel(c);
|
||||
const workerEnv = await callCoreFn(db, "core.fn_worker_get", { id: workerId, tenant_id: tid }, { route });
|
||||
if (!workerEnv.ok) return respondRpc(c, workerEnv);
|
||||
try {
|
||||
const { storage_name: storageName } = await deleteWorkerDocument(db, workerId, docId);
|
||||
if (storageName) {
|
||||
try {
|
||||
await deleteObject(workerDocKey(workerId, storageName));
|
||||
} catch {
|
||||
/* metadatos ya eliminados; objeto huérfano es aceptable */
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "No se pudo eliminar el documento";
|
||||
return respondApiError(c, "VALIDATION", message, { route, worker_id: workerId, doc_id: docId });
|
||||
}
|
||||
return c.json({
|
||||
ok: true,
|
||||
checklist: await checklistFor(db, workerId, tid),
|
||||
...await imssFlagsFor(db, workerId, tid),
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/v1/workers/:id/photo", ...requireCoreAuth, requirePermission("workers.view"), async (c) => {
|
||||
const id = Number(c.req.param("id"));
|
||||
const db = c.get("db");
|
||||
|
|
|
|||
|
|
@ -158,6 +158,22 @@ export async function getObject(key: string): Promise<Uint8Array> {
|
|||
return await readFile(path);
|
||||
}
|
||||
|
||||
export async function deleteObject(key: string): Promise<void> {
|
||||
if (s3Configured()) {
|
||||
await getClient().send(
|
||||
new DeleteObjectCommand({ Bucket: config.s3Bucket, Key: key }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const path = join(DATA_DIR, "local-objects", key);
|
||||
try {
|
||||
const { unlink } = await import("node:fs/promises");
|
||||
await unlink(path);
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Convenciones de key por dominio (equivalentes a los antiguos
|
||||
// workerDir/projectDir/companyDir + Deno.readFile/writeFile) ---
|
||||
export function workerDocKey(workerId: number, storageName: string): string {
|
||||
|
|
|
|||
|
|
@ -49,5 +49,6 @@
|
|||
<include file="changesets/037-fn-work-program-get-curve-vrow.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/038-fn-work-program-vs-cost.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/039-fn-next-project-code-global.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/040-fn-worker-document-delete.sql" relativeToChangelogFile="true"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
63
db/core/changesets/040-fn-worker-document-delete.sql
Normal file
63
db/core/changesets/040-fn-worker-document-delete.sql
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
--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;
|
||||
|
|
@ -11,5 +11,6 @@
|
|||
<include file="changesets/003-expenses-warehouse-permissions.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/004-rpc-users-permissions.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/005-iam-v2-roles-crud.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/006-documents-delete-permission.sql" relativeToChangelogFile="true"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
22
db/iam/changesets/006-documents-delete-permission.sql
Normal file
22
db/iam/changesets/006-documents-delete-permission.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
--liquibase formatted sql
|
||||
-- PANELS · iam · permiso eliminar documentos de expediente
|
||||
|
||||
--changeset panel:iam-006a-documents-delete endDelimiter:; splitStatements:true
|
||||
INSERT INTO iam.permissions (code, label, module, verb, perm_group) VALUES
|
||||
('documents.delete', 'Eliminar documentos de expediente', 'documents', 'delete', NULL)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
label = EXCLUDED.label,
|
||||
module = EXCLUDED.module,
|
||||
verb = EXCLUDED.verb,
|
||||
perm_group = EXCLUDED.perm_group;
|
||||
|
||||
INSERT INTO iam._legacy_perm_map (legacy_code, v2_code) VALUES
|
||||
('manage_documents', 'documents.delete')
|
||||
ON CONFLICT (legacy_code, v2_code) DO NOTHING;
|
||||
|
||||
INSERT INTO iam.role_permissions (role_id, permission_code)
|
||||
SELECT r.id, 'documents.delete'
|
||||
FROM iam.roles r
|
||||
WHERE r.is_system = true
|
||||
AND r.code IN ('tenant_admin')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
|
@ -60,6 +60,25 @@ const kind = computed<"image" | "pdf" | "other">(() => {
|
|||
return "other";
|
||||
});
|
||||
|
||||
function previewFetchPath(path: string) {
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
return `${path}${sep}disposition=inline`;
|
||||
}
|
||||
|
||||
function resolvedPreviewMime(raw: Blob): string {
|
||||
const fromProp = (props.mime || "").trim();
|
||||
if (fromProp) return fromProp;
|
||||
const fromBlob = (raw.type || "").trim();
|
||||
if (fromBlob && fromBlob !== "application/octet-stream") return fromBlob;
|
||||
const name = props.name.toLowerCase();
|
||||
if (name.endsWith(".pdf")) return "application/pdf";
|
||||
if (/\.(jpe?g)$/.test(name)) return "image/jpeg";
|
||||
if (name.endsWith(".png")) return "image/png";
|
||||
if (name.endsWith(".webp")) return "image/webp";
|
||||
if (name.endsWith(".gif")) return "image/gif";
|
||||
return fromBlob || "application/octet-stream";
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
revoke();
|
||||
error.value = "";
|
||||
|
|
@ -67,7 +86,9 @@ async function loadPreview() {
|
|||
if (!props.path) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const blob = await api<Blob>(props.path);
|
||||
const raw = await api<Blob>(previewFetchPath(props.path));
|
||||
const mime = resolvedPreviewMime(raw);
|
||||
const blob = mime === raw.type ? raw : new Blob([raw], { type: mime });
|
||||
blobRef.value = blob;
|
||||
url.value = URL.createObjectURL(blob);
|
||||
} catch (err: unknown) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
/>
|
||||
</FormField>
|
||||
<FileUpload
|
||||
ref="fileUploadRef"
|
||||
mode="basic"
|
||||
:auto="true"
|
||||
choose-label="Seleccionar archivo"
|
||||
|
|
@ -95,7 +96,19 @@
|
|||
</Column>
|
||||
<Column header="">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-eye" text rounded size="small" aria-label="Ver documento" @click="openDocument(data)" />
|
||||
<div class="flex gap-1">
|
||||
<Button icon="pi pi-eye" text rounded size="small" aria-label="Ver documento" @click="openDocument(data)" />
|
||||
<Button
|
||||
v-if="!readonly && canDeleteDoc"
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
aria-label="Eliminar documento"
|
||||
@click="confirmDelete(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
|
@ -111,7 +124,19 @@
|
|||
</Column>
|
||||
<Column header="">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-eye" text rounded size="small" aria-label="Ver documento" @click="openDocument(data)" />
|
||||
<div class="flex gap-1">
|
||||
<Button icon="pi pi-eye" text rounded size="small" aria-label="Ver documento" @click="openDocument(data)" />
|
||||
<Button
|
||||
v-if="!readonly && canDeleteDoc"
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
aria-label="Eliminar documento"
|
||||
@click="confirmDelete(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
|
@ -158,7 +183,11 @@ const props = defineProps<{
|
|||
}>();
|
||||
const emit = defineEmits<{ changed: []; registerAlta: [] }>();
|
||||
const { api } = useApi();
|
||||
const { can } = usePermissions();
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const fileUploadRef = ref<{ clear?: () => void } | null>(null);
|
||||
const canDeleteDoc = computed(() => can("documents.delete") || can("documents.create"));
|
||||
|
||||
const documentType = ref(
|
||||
props.initialType && props.initialType !== "alta_imss" && props.initialType !== "baja_imss"
|
||||
|
|
@ -194,11 +223,14 @@ const requiredDone = computed(() => requiredItems.value.filter((item) => item.pr
|
|||
const requiredProgress = computed(() =>
|
||||
requiredItems.value.length ? Math.round((requiredDone.value / requiredItems.value.length) * 100) : 0,
|
||||
);
|
||||
function isDocCurrent(doc: DocumentRow) {
|
||||
return doc.is_current === true || doc.is_current === 1;
|
||||
}
|
||||
const currentDocuments = computed(() =>
|
||||
props.documents.filter((doc) => doc.is_current && doc.type_code !== "alta_imss" && doc.type_code !== "baja_imss"),
|
||||
props.documents.filter((doc) => isDocCurrent(doc) && doc.type_code !== "alta_imss" && doc.type_code !== "baja_imss"),
|
||||
);
|
||||
const historyDocuments = computed(() =>
|
||||
props.documents.filter((doc) => !doc.is_current && doc.type_code !== "alta_imss" && doc.type_code !== "baja_imss"),
|
||||
props.documents.filter((doc) => !isDocCurrent(doc) && doc.type_code !== "alta_imss" && doc.type_code !== "baja_imss"),
|
||||
);
|
||||
const freshnessRequired = computed(() => !!props.freshnessRequired);
|
||||
const imssReady = computed(() => !!props.imssReady);
|
||||
|
|
@ -263,6 +295,7 @@ async function uploadDocument(event: { files: File[] }) {
|
|||
body.append("type", documentType.value);
|
||||
body.append("file", file);
|
||||
await api(`/v1/workers/${props.workerId}/documents`, { method: "POST", body });
|
||||
fileUploadRef.value?.clear?.();
|
||||
toast.add({ severity: "success", summary: "Documento cifrado y guardado", life: 2200 });
|
||||
docsTab.value = "current";
|
||||
emit("changed");
|
||||
|
|
@ -282,6 +315,30 @@ function openDocument(row: DocumentRow) {
|
|||
previewMime.value = row.mime || null;
|
||||
previewOpen.value = true;
|
||||
}
|
||||
function confirmDelete(row: DocumentRow) {
|
||||
confirm.require({
|
||||
message: `¿Eliminar ${typeLabel(row.type_code)} (${row.original_name})? Esta acción no se puede deshacer.`,
|
||||
header: "Eliminar documento",
|
||||
icon: "pi pi-exclamation-triangle",
|
||||
acceptLabel: "Eliminar",
|
||||
rejectLabel: "Cancelar",
|
||||
acceptClass: "p-button-danger",
|
||||
accept: () => deleteDocument(row),
|
||||
});
|
||||
}
|
||||
async function deleteDocument(row: DocumentRow) {
|
||||
try {
|
||||
await api(`/v1/workers/${props.workerId}/documents/${row.id}`, { method: "DELETE" });
|
||||
toast.add({ severity: "success", summary: "Documento eliminado", life: 2200 });
|
||||
emit("changed");
|
||||
} catch (error: unknown) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: error instanceof Error ? error.message : "No se pudo eliminar",
|
||||
life: 3500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.initialType, (value) => {
|
||||
if (value && value !== "alta_imss" && value !== "baja_imss") documentType.value = value;
|
||||
|
|
|
|||
Loading…
Reference in a new issue