/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/services/Documents.service.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; const BUCKET = 'documents'; // ── Helpers ────────────────────────────────────────────────── async function getOwnerId() { const { data, error } = await supabase.auth.getUser(); if (error) throw error; const uid = data?.user?.id; if (!uid) throw new Error('Sessão inválida.'); return uid; } async function getActiveTenantId(uid) { const { data, error } = await supabase .from('tenant_members') .select('tenant_id') .eq('user_id', uid) .eq('status', 'active') .order('created_at', { ascending: false }) .limit(1) .single(); if (error) throw error; if (!data?.tenant_id) throw new Error('Tenant não encontrado.'); return data.tenant_id; } function buildStoragePath(tenantId, patientId, fileName) { const timestamp = Date.now(); const safe = String(fileName || 'arquivo').replace(/[^a-zA-Z0-9._-]/g, '_'); return `${tenantId}/${patientId}/${timestamp}-${safe}`; } // ── Upload ────────────────────────────────────────────────── /** * Faz upload de arquivo ao Storage e registra na tabela documents. * * @param {File} file - Objeto File do input * @param {string} patientId - UUID do paciente * @param {object} meta - { tipo_documento, categoria, descricao, tags[], agenda_evento_id, visibilidade } * @returns {object} - Registro criado em documents */ export async function uploadDocument(file, patientId, meta = {}) { if (!file) throw new Error('Nenhum arquivo selecionado.'); if (!patientId) throw new Error('Paciente não informado.'); const ownerId = await getOwnerId(); const tenantId = await getActiveTenantId(ownerId); // Upload ao Storage const path = buildStoragePath(tenantId, patientId, file.name); const { error: upErr } = await supabase.storage .from(BUCKET) .upload(path, file, { contentType: file.type }); if (upErr) throw upErr; // Insert na tabela const row = { owner_id: ownerId, tenant_id: tenantId, patient_id: patientId, bucket_path: path, storage_bucket: BUCKET, nome_original: file.name, mime_type: file.type || null, tamanho_bytes: file.size || null, tipo_documento: meta.tipo_documento || 'outro', categoria: meta.categoria || null, descricao: meta.descricao || null, tags: meta.tags || [], agenda_evento_id: meta.agenda_evento_id || null, visibilidade: meta.visibilidade || 'privado', compartilhado_portal: meta.compartilhado_portal || false, compartilhado_supervisor: meta.compartilhado_supervisor || false, enviado_pelo_paciente: meta.enviado_pelo_paciente || false, status_revisao: meta.enviado_pelo_paciente ? 'pendente' : 'aprovado', uploaded_by: ownerId }; const { data, error } = await supabase .from('documents') .insert(row) .select('*') .single(); if (error) { // Tenta limpar o arquivo do Storage em caso de erro no insert await supabase.storage.from(BUCKET).remove([path]).catch(() => {}); throw error; } return data; } // ── List ──────────────────────────────────────────────────── /** * Lista documentos de um paciente (excluindo soft-deleted). * * @param {string} patientId * @param {object} filters - { tipo_documento, categoria, tag, search } */ export async function listDocuments(patientId, filters = {}) { const ownerId = await getOwnerId(); let query = supabase .from('documents') .select('*') .eq('owner_id', ownerId) .eq('patient_id', patientId) .is('deleted_at', null) .order('uploaded_at', { ascending: false }); if (filters.tipo_documento) { query = query.eq('tipo_documento', filters.tipo_documento); } if (filters.categoria) { query = query.eq('categoria', filters.categoria); } if (filters.tag) { query = query.contains('tags', [filters.tag]); } if (filters.search) { query = query.ilike('nome_original', `%${filters.search}%`); } const { data, error } = await query; if (error) throw error; return data || []; } /** * Lista todos os documentos do owner (todos os pacientes). */ export async function listAllDocuments(filters = {}) { const ownerId = await getOwnerId(); let query = supabase .from('documents') .select('*, patients!inner(nome_completo)') .eq('owner_id', ownerId) .is('deleted_at', null) .order('uploaded_at', { ascending: false }); if (filters.tipo_documento) { query = query.eq('tipo_documento', filters.tipo_documento); } if (filters.search) { query = query.ilike('nome_original', `%${filters.search}%`); } const { data, error } = await query; if (error) throw error; return data || []; } // ── Get one ───────────────────────────────────────────────── export async function getDocument(id) { const ownerId = await getOwnerId(); const { data, error } = await supabase .from('documents') .select('*') .eq('id', id) .eq('owner_id', ownerId) .single(); if (error) throw error; return data; } // ── Update ────────────────────────────────────────────────── export async function updateDocument(id, payload) { const ownerId = await getOwnerId(); if (!id) throw new Error('ID inválido.'); const row = {}; if (payload.tipo_documento !== undefined) row.tipo_documento = payload.tipo_documento; if (payload.categoria !== undefined) row.categoria = payload.categoria; if (payload.descricao !== undefined) row.descricao = payload.descricao; if (payload.tags !== undefined) row.tags = payload.tags; if (payload.visibilidade !== undefined) row.visibilidade = payload.visibilidade; if (payload.compartilhado_portal !== undefined) row.compartilhado_portal = payload.compartilhado_portal; if (payload.compartilhado_supervisor !== undefined) row.compartilhado_supervisor = payload.compartilhado_supervisor; if (payload.status_revisao !== undefined) { row.status_revisao = payload.status_revisao; row.revisado_por = ownerId; row.revisado_em = new Date().toISOString(); } row.updated_at = new Date().toISOString(); const { data, error } = await supabase .from('documents') .update(row) .eq('id', id) .eq('owner_id', ownerId) .select('*') .single(); if (error) throw error; return data; } // ── Soft Delete ───────────────────────────────────────────── /** * Soft delete com retencao. O arquivo permanece no Storage. * retencaoAnos: numero de anos de retencao (padrao 5 — CFP). */ export async function softDeleteDocument(id, retencaoAnos = 5) { const ownerId = await getOwnerId(); if (!id) throw new Error('ID inválido.'); const retencaoAte = new Date(); retencaoAte.setFullYear(retencaoAte.getFullYear() + retencaoAnos); const { error } = await supabase .from('documents') .update({ deleted_at: new Date().toISOString(), deleted_by: ownerId, retencao_ate: retencaoAte.toISOString(), updated_at: new Date().toISOString() }) .eq('id', id) .eq('owner_id', ownerId); if (error) throw error; return true; } /** * Restaurar documento soft-deleted. */ export async function restoreDocument(id) { const ownerId = await getOwnerId(); if (!id) throw new Error('ID inválido.'); const { error } = await supabase .from('documents') .update({ deleted_at: null, deleted_by: null, retencao_ate: null, updated_at: new Date().toISOString() }) .eq('id', id) .eq('owner_id', ownerId); if (error) throw error; return true; } // ── Download URL ──────────────────────────────────────────── /** * Gera URL assinada para download (valida por 60s por padrao). */ export async function getDownloadUrl(bucketPath, expiresIn = 60, bucket = BUCKET) { const { data, error } = await supabase.storage .from(bucket) .createSignedUrl(bucketPath, expiresIn); if (error) throw error; return data?.signedUrl; } // ── Tags (autocomplete) ──────────────────────────────────── /** * Retorna tags unicas ja usadas pelo owner (para autocomplete). */ export async function getUsedTags() { const ownerId = await getOwnerId(); const { data, error } = await supabase .from('documents') .select('tags') .eq('owner_id', ownerId) .is('deleted_at', null); if (error) throw error; const set = new Set(); for (const row of data || []) { for (const tag of row.tags || []) { if (tag) set.add(tag); } } return [...set].sort((a, b) => a.localeCompare(b, 'pt-BR')); }