diff --git a/database-novo/tmp/backfill-document-generated-link.sql b/database-novo/tmp/backfill-document-generated-link.sql new file mode 100644 index 0000000..75e63d4 --- /dev/null +++ b/database-novo/tmp/backfill-document-generated-link.sql @@ -0,0 +1,39 @@ +-- Backfill: linkar document_generated.documento_id em registros antigos +-- pra suportar re-edicao in-place de documentos gerados. +-- +-- O codigo novo (DocumentGenerate.service.js saveGeneratedDocument) ja +-- preenche o documento_id no INSERT pra criacoes novas. Este script eh +-- one-off pra docs gerados ANTES desse fix. +-- +-- Match: dg.pdf_path = d.bucket_path + match de tenant/patient pra evitar +-- linkar a doc errado em caso colidente. Registros sem match (paths que +-- nao existem mais em documents — docs deletados/cleanup) ficam orfaos +-- com documento_id=NULL: nao quebra nada, so nao tem caminho de re-edit. + +BEGIN; + +UPDATE public.document_generated dg +SET documento_id = d.id +FROM public.documents d +WHERE dg.documento_id IS NULL + AND dg.pdf_path = d.bucket_path + AND dg.patient_id = d.patient_id + AND dg.tenant_id = d.tenant_id + AND d.deleted_at IS NULL; + +-- Relatorio pos-backfill +DO $REPORT$ +DECLARE + v_linked int; + v_orphans int; +BEGIN + SELECT count(*) FILTER (WHERE documento_id IS NOT NULL), + count(*) FILTER (WHERE documento_id IS NULL) + INTO v_linked, v_orphans + FROM public.document_generated; + RAISE NOTICE 'document_generated: % linked, % orphans (sem documents correspondente)', + v_linked, v_orphans; +END; +$REPORT$; + +COMMIT; diff --git a/src/features/documents/composables/useDocumentGenerate.js b/src/features/documents/composables/useDocumentGenerate.js index 2534bab..9911008 100644 --- a/src/features/documents/composables/useDocumentGenerate.js +++ b/src/features/documents/composables/useDocumentGenerate.js @@ -99,9 +99,12 @@ export function useDocumentGenerate() { // ── Gerar PDF (client-side) ──────────────────────────── /** - * Gera PDF blob, faz download, salva no Storage + banco. + * Gera PDF blob, salva no Storage + banco. + * @param {string} patientId + * @param {string|null} editingDocId - se setado, UPDATE no doc existente + * (in-place replace de PDF + metadados, preserva documents.id e audit). */ - async function generateAndSave(patientId) { + async function generateAndSave(patientId, editingDocId = null) { if (!selectedTemplate.value) throw new Error('Nenhum template selecionado.'); loading.value = true; @@ -119,7 +122,8 @@ export function useDocumentGenerate() { dadosPreenchidos: { ...variables.value }, pdfBlob: blob, templateNome, - templateTipo: selectedTemplate.value.tipo + templateTipo: selectedTemplate.value.tipo, + editingDocId }); generatedDocs.value.unshift(result); return result; diff --git a/src/services/DocumentGenerate.service.js b/src/services/DocumentGenerate.service.js index 4fd1e4d..de40188 100644 --- a/src/services/DocumentGenerate.service.js +++ b/src/services/DocumentGenerate.service.js @@ -404,9 +404,10 @@ export async function printDocument(template, variables = {}) { * @param {string} params.patientId * @param {object} params.dadosPreenchidos - snapshot dos dados usados * @param {Blob} params.pdfBlob - PDF gerado (opcional — pode ser null se so print) - * @returns {object} registro criado + * @param {string} [params.editingDocId] - se setado, re-edita doc existente (UPDATE) + * @returns {object} registro criado/atualizado em document_generated */ -export async function saveGeneratedDocument({ templateId, patientId, dadosPreenchidos, pdfBlob, templateNome, templateTipo }) { +export async function saveGeneratedDocument({ templateId, patientId, dadosPreenchidos, pdfBlob, templateNome, templateTipo, editingDocId = null }) { const ownerId = await getOwnerId(); const tenantId = await getActiveTenantId(ownerId); @@ -428,27 +429,90 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc if (upErr) throw upErr; } - // Registra na tabela document_generated - const { data, error } = await supabase - .from('document_generated') - .insert({ - template_id: templateId, - patient_id: patientId, - tenant_id: tenantId, - dados_preenchidos: dadosPreenchidos || {}, - pdf_path: pdfPath, - storage_bucket: BUCKET, - gerado_por: ownerId - }) - .select('*') - .single(); + // ─── MODO EDIT (UPDATE in-place) ───────────────────────── + // Re-edicao: preserva documents.id (e o audit trail), substitui o PDF + // no Storage, atualiza metadados. Best-effort cleanup do PDF antigo. + if (editingDocId) { + const { data: oldDoc } = await supabase + .from('documents') + .select('bucket_path, storage_bucket') + .eq('id', editingDocId) + .single(); - if (error) throw error; + const docPatch = { + tipo_documento: mapTipoDocumento(templateTipo), + descricao: `Gerado a partir do template: ${templateNome || 'documento'}` + }; + if (pdfPath) { + docPatch.bucket_path = pdfPath; + docPatch.storage_bucket = BUCKET; + docPatch.tamanho_bytes = pdfBlob?.size || null; + docPatch.nome_original = filename.replace(/_/g, ' ').replace('.pdf', '') + '.pdf'; + } + const { error: upDocErr } = await supabase + .from('documents') + .update(docPatch) + .eq('id', editingDocId); + if (upDocErr) throw upDocErr; - // Registra na tabela documents para aparecer na lista do paciente - // Reutiliza o mesmo arquivo do bucket generated-docs (sem upload duplicado) + // Atualiza document_generated. Pode nao existir (docs legados sem + // linkage) — INSERT nesse caso, com documento_id apontando pro doc. + const { data: existingGen } = await supabase + .from('document_generated') + .select('id') + .eq('documento_id', editingDocId) + .maybeSingle(); + + let data; + if (existingGen) { + const genPatch = { + template_id: templateId, + dados_preenchidos: dadosPreenchidos || {} + }; + if (pdfPath) genPatch.pdf_path = pdfPath; + const { data: updated, error: upGenErr } = await supabase + .from('document_generated') + .update(genPatch) + .eq('id', existingGen.id) + .select('*') + .single(); + if (upGenErr) throw upGenErr; + data = updated; + } else { + const { data: inserted, error: insGenErr } = await supabase + .from('document_generated') + .insert({ + template_id: templateId, + patient_id: patientId, + tenant_id: tenantId, + dados_preenchidos: dadosPreenchidos || {}, + pdf_path: pdfPath, + storage_bucket: BUCKET, + gerado_por: ownerId, + documento_id: editingDocId + }) + .select('*') + .single(); + if (insGenErr) throw insGenErr; + data = inserted; + } + + // Cleanup do PDF antigo no Storage. Falha silenciosa — arquivo orfao + // nao quebra nada, so ocupa espaco minimo. + if (oldDoc?.bucket_path && oldDoc.bucket_path !== pdfPath && oldDoc.storage_bucket) { + supabase.storage.from(oldDoc.storage_bucket).remove([oldDoc.bucket_path]) + .catch((e) => console.warn('[saveGeneratedDocument] cleanup antigo falhou:', e)); + } + + return data; + } + + // ─── MODO CREATE (insert) ──────────────────────────────── + // Insere documents primeiro pra capturar o id e linkar em + // document_generated via documento_id (FK). + let documentoId = null; if (pdfPath) { - await supabase + const { data: newDoc, error: insDocErr } = await supabase .from('documents') .insert({ owner_id: ownerId, @@ -465,9 +529,52 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc visibilidade: 'privado', status_revisao: 'aprovado', uploaded_by: ownerId - }); + }) + .select('id') + .single(); + if (insDocErr) throw insDocErr; + documentoId = newDoc?.id || null; } + // Registra em document_generated com o linkage documento_id preenchido + const { data, error } = await supabase + .from('document_generated') + .insert({ + template_id: templateId, + patient_id: patientId, + tenant_id: tenantId, + dados_preenchidos: dadosPreenchidos || {}, + pdf_path: pdfPath, + storage_bucket: BUCKET, + gerado_por: ownerId, + documento_id: documentoId + }) + .select('*') + .single(); + + if (error) throw error; + + return data; +} + +// ── Buscar generated existente pra modo edit ───────────────── + +/** + * Busca o registro document_generated vinculado a um documents.id. + * Retorna template_id + dados_preenchidos pra pre-popular o dialog. + * Null se nao houver linkage (docs uploaded direto, sem template). + */ +export async function loadGeneratedFromDocId(documentoId) { + if (!documentoId) return null; + const { data, error } = await supabase + .from('document_generated') + .select('id, template_id, dados_preenchidos, pdf_path, gerado_em') + .eq('documento_id', documentoId) + .maybeSingle(); + if (error) { + console.error('[loadGeneratedFromDocId]', error); + return null; + } return data; }