documents/generate: suporte a edicao in-place + linkage documento_id
document_generated.documento_id (FK pra documents) estava sempre NULL no INSERT — sem isso nao da pra rastrear qual generated belongs to qual documents row, impossibilitando re-edicao. DocumentGenerate.service saveGeneratedDocument: - Modo create (default): INSERT em documents PRIMEIRO pra capturar doc.id, depois INSERT em document_generated com documento_id setado. - Modo edit (editingDocId param novo): UPDATE in-place — substitui PDF no Storage (novo path), atualiza bucket_path/tamanho/nome em documents (preserva id+audit), atualiza dados_preenchidos+pdf_path em document_generated. Se nao houver registro generated (doc legado), INSERT vinculando ao documents.id. Cleanup best-effort do PDF antigo. - Nova fn loadGeneratedFromDocId(documentoId): busca template_id + dados_preenchidos pra pre-popular o dialog de edicao. useDocumentGenerate.generateAndSave: ganha 2o param editingDocId que passa pro service. Backfill SQL pra docs antigos: match dg.pdf_path = d.bucket_path + tenant/patient guard. 3 docs linkados no DB local, 5 ficaram orfaos (paths que nao existem mais em documents — cleanup antigo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||||
@@ -99,9 +99,12 @@ export function useDocumentGenerate() {
|
|||||||
// ── Gerar PDF (client-side) ────────────────────────────
|
// ── 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.');
|
if (!selectedTemplate.value) throw new Error('Nenhum template selecionado.');
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -119,7 +122,8 @@ export function useDocumentGenerate() {
|
|||||||
dadosPreenchidos: { ...variables.value },
|
dadosPreenchidos: { ...variables.value },
|
||||||
pdfBlob: blob,
|
pdfBlob: blob,
|
||||||
templateNome,
|
templateNome,
|
||||||
templateTipo: selectedTemplate.value.tipo
|
templateTipo: selectedTemplate.value.tipo,
|
||||||
|
editingDocId
|
||||||
});
|
});
|
||||||
generatedDocs.value.unshift(result);
|
generatedDocs.value.unshift(result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -404,9 +404,10 @@ export async function printDocument(template, variables = {}) {
|
|||||||
* @param {string} params.patientId
|
* @param {string} params.patientId
|
||||||
* @param {object} params.dadosPreenchidos - snapshot dos dados usados
|
* @param {object} params.dadosPreenchidos - snapshot dos dados usados
|
||||||
* @param {Blob} params.pdfBlob - PDF gerado (opcional — pode ser null se so print)
|
* @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 ownerId = await getOwnerId();
|
||||||
const tenantId = await getActiveTenantId(ownerId);
|
const tenantId = await getActiveTenantId(ownerId);
|
||||||
|
|
||||||
@@ -428,27 +429,90 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
|
|||||||
if (upErr) throw upErr;
|
if (upErr) throw upErr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registra na tabela document_generated
|
// ─── MODO EDIT (UPDATE in-place) ─────────────────────────
|
||||||
const { data, error } = await supabase
|
// Re-edicao: preserva documents.id (e o audit trail), substitui o PDF
|
||||||
.from('document_generated')
|
// no Storage, atualiza metadados. Best-effort cleanup do PDF antigo.
|
||||||
.insert({
|
if (editingDocId) {
|
||||||
template_id: templateId,
|
const { data: oldDoc } = await supabase
|
||||||
patient_id: patientId,
|
.from('documents')
|
||||||
tenant_id: tenantId,
|
.select('bucket_path, storage_bucket')
|
||||||
dados_preenchidos: dadosPreenchidos || {},
|
.eq('id', editingDocId)
|
||||||
pdf_path: pdfPath,
|
.single();
|
||||||
storage_bucket: BUCKET,
|
|
||||||
gerado_por: ownerId
|
|
||||||
})
|
|
||||||
.select('*')
|
|
||||||
.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
|
// Atualiza document_generated. Pode nao existir (docs legados sem
|
||||||
// Reutiliza o mesmo arquivo do bucket generated-docs (sem upload duplicado)
|
// 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) {
|
if (pdfPath) {
|
||||||
await supabase
|
const { data: newDoc, error: insDocErr } = await supabase
|
||||||
.from('documents')
|
.from('documents')
|
||||||
.insert({
|
.insert({
|
||||||
owner_id: ownerId,
|
owner_id: ownerId,
|
||||||
@@ -465,9 +529,52 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
|
|||||||
visibilidade: 'privado',
|
visibilidade: 'privado',
|
||||||
status_revisao: 'aprovado',
|
status_revisao: 'aprovado',
|
||||||
uploaded_by: ownerId
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user