diff --git a/database-novo/migrations/20260521000007_list_my_signatures_rpc.sql b/database-novo/migrations/20260521000007_list_my_signatures_rpc.sql
new file mode 100644
index 0000000..bcc05e7
--- /dev/null
+++ b/database-novo/migrations/20260521000007_list_my_signatures_rpc.sql
@@ -0,0 +1,102 @@
+-- ============================================================================
+-- Compliance CFP #7 — RPC list_my_signatures (portal do paciente)
+-- ----------------------------------------------------------------------------
+-- Retorna as solicitações de assinatura do paciente logado (auth.uid()
+-- associado a patients.user_id). SECURITY DEFINER pra bypassar a RLS de
+-- document_signatures (que hoje só libera pra tenant_members).
+--
+-- Cada item já vem com o share_link.token associado, pra que o portal
+-- aponte direto pra /shared/document/:token onde o usuário vai assinar.
+-- O link público é gerado quando o terapeuta solicita a assinatura.
+-- ============================================================================
+
+BEGIN;
+
+CREATE OR REPLACE FUNCTION public.list_my_signatures(
+ p_status text[] DEFAULT NULL
+) RETURNS TABLE (
+ signature_id uuid,
+ documento_id uuid,
+ tenant_id uuid,
+ signatario_tipo text,
+ status text,
+ ordem smallint,
+ assinado_em timestamptz,
+ criado_em timestamptz,
+ -- Documento
+ nome_original text,
+ tipo_documento text,
+ mime_type text,
+ -- Share link (primeiro válido encontrado pro doc)
+ share_token text,
+ share_expira_em timestamptz
+)
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public
+AS $$
+DECLARE
+ v_uid uuid;
+BEGIN
+ v_uid := auth.uid();
+ IF v_uid IS NULL THEN
+ RAISE EXCEPTION 'Sessão inválida' USING ERRCODE = '28000';
+ END IF;
+
+ RETURN QUERY
+ SELECT
+ s.id AS signature_id,
+ s.documento_id AS documento_id,
+ s.tenant_id AS tenant_id,
+ s.signatario_tipo AS signatario_tipo,
+ s.status AS status,
+ s.ordem AS ordem,
+ s.assinado_em AS assinado_em,
+ s.criado_em AS criado_em,
+ d.nome_original AS nome_original,
+ d.tipo_documento AS tipo_documento,
+ d.mime_type AS mime_type,
+ sl.token AS share_token,
+ sl.expira_em AS share_expira_em
+ FROM public.document_signatures s
+ JOIN public.documents d ON d.id = s.documento_id AND d.deleted_at IS NULL
+ LEFT JOIN LATERAL (
+ SELECT token, expira_em
+ FROM public.document_share_links
+ WHERE documento_id = d.id
+ AND ativo = true
+ AND expira_em > now()
+ AND usos < usos_max
+ ORDER BY criado_em DESC
+ LIMIT 1
+ ) sl ON true
+ WHERE (
+ -- signatario_id direto (quando registrado)
+ s.signatario_id = v_uid
+ OR
+ -- Fallback: paciente pelo email (quando signatario_id veio NULL)
+ s.signatario_email = (SELECT email FROM auth.users WHERE id = v_uid)
+ OR
+ -- Fallback: paciente pelo patient_id (documents.patient_id -> patients.user_id)
+ d.patient_id IN (SELECT p.id FROM public.patients p WHERE p.user_id = v_uid)
+ )
+ AND (p_status IS NULL OR s.status = ANY (p_status))
+ ORDER BY
+ CASE s.status
+ WHEN 'pendente' THEN 0
+ WHEN 'enviado' THEN 1
+ WHEN 'assinado' THEN 2
+ WHEN 'recusado' THEN 3
+ WHEN 'expirado' THEN 4
+ ELSE 99
+ END,
+ s.criado_em DESC;
+END;
+$$;
+
+COMMENT ON FUNCTION public.list_my_signatures(text[]) IS
+ 'Lista signatures do paciente logado (auth.uid()) cruzando por signatario_id, email ou patient.user_id. Inclui share_token pra link de assinatura.';
+
+GRANT EXECUTE ON FUNCTION public.list_my_signatures(text[]) TO authenticated;
+
+COMMIT;
diff --git a/src/features/documents/composables/useDocumentSignatures.js b/src/features/documents/composables/useDocumentSignatures.js
index 21a2c0f..0e6a96a 100644
--- a/src/features/documents/composables/useDocumentSignatures.js
+++ b/src/features/documents/composables/useDocumentSignatures.js
@@ -15,6 +15,7 @@ import {
signByPortal,
signByToken,
getSignableDocumentByToken,
+ listMySignatures,
hashDocument
} from '@/services/DocumentSignatures.service';
@@ -110,6 +111,22 @@ export function useDocumentSignatures() {
}
}
+ async function loadMine(statusFilter = null) {
+ loading.value = true;
+ error.value = '';
+ try {
+ const rows = await listMySignatures(statusFilter);
+ signatures.value = Array.isArray(rows) ? rows : [];
+ return signatures.value;
+ } catch (e) {
+ error.value = e?.message || 'Falha ao carregar minhas assinaturas.';
+ signatures.value = [];
+ throw e;
+ } finally {
+ loading.value = false;
+ }
+ }
+
async function loadByToken(token) {
loading.value = true;
error.value = '';
@@ -144,6 +161,7 @@ export function useDocumentSignatures() {
refuse,
signWithToken,
loadByToken,
+ loadMine,
hashDocument
};
}
diff --git a/src/navigation/menus/portal.menu.js b/src/navigation/menus/portal.menu.js
index 6b80e2f..3f014f6 100644
--- a/src/navigation/menus/portal.menu.js
+++ b/src/navigation/menus/portal.menu.js
@@ -26,6 +26,11 @@ export default [
items: [{ label: 'Sessões', icon: 'pi pi-fw pi-calendar', to: '/portal/sessoes' }]
},
+ {
+ label: 'Documentos',
+ items: [{ label: 'Para assinar', icon: 'pi pi-fw pi-file-edit', to: '/portal/documentos' }]
+ },
+
{
label: 'Conta',
items: [
diff --git a/src/router/routes.portal.js b/src/router/routes.portal.js
index a35862b..442ce0c 100644
--- a/src/router/routes.portal.js
+++ b/src/router/routes.portal.js
@@ -27,6 +27,12 @@ export default {
name: 'portal-sessoes',
component: () => import('@/views/pages/portal/MinhasSessoes.vue')
},
+ {
+ path: 'documentos',
+ name: 'portal-documentos',
+ component: () => import('@/views/pages/portal/PortalDocumentos.vue'),
+ meta: { area: 'portal', requiresAuth: true }
+ },
// ======================================================
// 💳 MEU PLANO (assinatura pessoal do paciente)
diff --git a/src/services/DocumentSignatures.service.js b/src/services/DocumentSignatures.service.js
index 7b1fce4..37107ed 100644
--- a/src/services/DocumentSignatures.service.js
+++ b/src/services/DocumentSignatures.service.js
@@ -208,6 +208,21 @@ export async function signByToken(token, signatureId = null, hashDocumento = nul
return data;
}
+// ── Listar minhas assinaturas (portal do paciente) ──────────
+//
+// Wrapper sobre RPC list_my_signatures. Resolve auth.uid() server-side
+// e cruza por signatario_id / email / patient.user_id pra encontrar
+// todas as assinaturas que pertencem ao usuário logado. Inclui
+// share_token p/ apontar direto pra /shared/document/:token.
+//
+export async function listMySignatures(statusFilter = null) {
+ const { data, error } = await supabase.rpc('list_my_signatures', {
+ p_status: Array.isArray(statusFilter) ? statusFilter : null
+ });
+ if (error) throw error;
+ return data || [];
+}
+
// ── Pré-visualizar documento por token (sem assinar) ────────
//
// Usado pela página pública pra carregar info do documento +
diff --git a/src/views/pages/portal/PortalDocumentos.vue b/src/views/pages/portal/PortalDocumentos.vue
new file mode 100644
index 0000000..742563d
--- /dev/null
+++ b/src/views/pages/portal/PortalDocumentos.vue
@@ -0,0 +1,197 @@
+
+
+
+
+ Carregando seus documentos…