import { useCallback, useEffect, useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { Alert, Badge, Button, Card, PageHeader, Select } from "@eden/ui"; import { useAuth } from "../../lib/auth/AuthContext.js"; import { hasPermission } from "../../lib/auth/permissions.js"; import { ApiError } from "../../lib/api/http.js"; import { cancelEnvelope, deleteEnvelope, downloadFinalPdf, finalizeEnvelope, getEnvelope, listAuditEvents, openDocumentPdf, sealCheckpoint, sendInvite, verifyAudit, } from "../../lib/api/signatures.js"; import type { EnvelopeWithDetails, SendInviteTargets, SignatureAuditEvent, VerifyAuditResult, } from "../../lib/api/signatures.js"; import { getCompany } from "../../lib/api/companies.js"; import type { Company } from "../../lib/api/companies.js"; import { formatDateTimeBR } from "../../lib/format.js"; import { Breadcrumb } from "../../components/Breadcrumb.js"; import { ENVELOPE_STATUS_LABEL, ENVELOPE_STATUS_VARIANT, SIGNER_STATUS_LABEL, SIGNER_STATUS_VARIANT, actorTypeLabel, auditEventLabel, partyRoleLabel, } from "./signatureStatus.js"; const DELETE_BLOCKED_STATUSES = new Set(["SIGNED", "FINALIZING", "COMPLETED"]); const SEND_INVITE_BLOCKED_STATUSES = new Set(["COMPLETED", "CANCELLED", "DECLINED", "EXPIRED", "SUPERSEDED", "ERROR"]); /** Mirrors `ENVELOPE_TRANSITIONS` in `packages/database/src/repositories/signatures.ts` — `CANCELLED` is only a valid target from these pre-signing states; anything past `SIGNING` (inclusive) 409s server-side. UX-only gating, the backend is still the one enforcing it. */ const CANCEL_BLOCKED_STATUSES = new Set([ "SIGNING", "SIGNED", "FINALIZING", "COMPLETED", "DECLINED", "EXPIRED", "CANCELLED", "SUPERSEDED", "ERROR", ]); export function EnvelopeDetailPage() { const { id } = useParams<{ id: string }>(); const { accessToken, permissions } = useAuth(); const [envelope, setEnvelope] = useState(null); const [company, setCompany] = useState(null); const [auditEvents, setAuditEvents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const canEdit = hasPermission(permissions, "signatures", "edit"); const canDelete = hasPermission(permissions, "signatures", "delete"); const load = useCallback(() => { if (!id || !accessToken) return; setLoading(true); setError(null); getEnvelope(id, accessToken) .then(async (result) => { setEnvelope(result); const [companyResult, events] = await Promise.all([ getCompany(result.company_id, accessToken).catch(() => null), listAuditEvents(id, accessToken).catch(() => []), ]); setCompany(companyResult); setAuditEvents(events); }) .catch((err) => setError(err instanceof ApiError ? err.message : "Não foi possível carregar o envelope.")) .finally(() => setLoading(false)); }, [id, accessToken]); useEffect(() => { load(); }, [load]); if (loading) { return (

Carregando envelope…

); } if (error || !envelope) { return (
{error ?? "Envelope não encontrado."}
); } const verificationUrl = envelope.verification_id ? `${window.location.origin}/verificar/${envelope.verification_id}` : null; return (
{ENVELOPE_STATUS_LABEL[envelope.status]}} />
{envelope.status === "COMPLETED" ? (

Qualquer pessoa com este link pode conferir a integridade do documento (sem autenticação):

{verificationUrl ? ( {verificationUrl} ) : null}
) : null}
{envelope.documents.length === 0 ? (

Nenhum documento neste envelope.

) : (
{envelope.documents .slice() .sort((a, b) => a.sort_order - b.sort_order) .map((doc) => ( ))}
Documento Versão Páginas
)}
{envelope.signers.length === 0 ? (

Nenhum signatário cadastrado.

) : (
{envelope.signers .slice() .sort((a, b) => a.sort_order - b.sort_order) .map((signer) => ( ))}
Papel Nome Contato Cargo Status
{partyRoleLabel(signer.party)} {signer.full_name} {signer.cpf ? {signer.cpf} : null} {signer.email} {signer.phone ? {signer.phone} : null} {signer.role_title ?? "—"} {SIGNER_STATUS_LABEL[signer.status]}
)}
); } function Row({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function DocumentRow({ envelopeId, document, accessToken, }: { envelopeId: string; document: EnvelopeWithDetails["documents"][number]; accessToken: string | null; }) { const [opening, setOpening] = useState(false); const [error, setError] = useState(null); async function handleOpen() { if (!accessToken) return; setError(null); setOpening(true); try { await openDocumentPdf(envelopeId, document.id, accessToken); } catch { setError("Não foi possível abrir o PDF."); } finally { setOpening(false); } } return ( {document.title} v{document.document_version} {document.page_count ?? "—"} {error ?

{error}

: null} ); } function EnvelopeActionsCard({ envelope, accessToken, canEdit, canDelete, onChanged, }: { envelope: EnvelopeWithDetails; accessToken: string | null; canEdit: boolean; canDelete: boolean; onChanged: () => void; }) { const [targets, setTargets] = useState("both"); const [sending, setSending] = useState(false); const [sealing, setSealing] = useState(false); const [finalizing, setFinalizing] = useState(false); const [downloadingFinal, setDownloadingFinal] = useState(false); const [verifying, setVerifying] = useState(false); const [verifyResult, setVerifyResult] = useState(null); const [confirmCancel, setConfirmCancel] = useState(false); const [cancelReason, setCancelReason] = useState(""); const [cancelling, setCancelling] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const [deleting, setDeleting] = useState(false); const [actionError, setActionError] = useState(null); const canSendInvite = canEdit && !SEND_INVITE_BLOCKED_STATUSES.has(envelope.status); const canCancel = canEdit && !CANCEL_BLOCKED_STATUSES.has(envelope.status); const canDeleteEnvelope = canDelete && !DELETE_BLOCKED_STATUSES.has(envelope.status); const canSeal = canEdit && envelope.status === "SIGNED"; const canFinalize = canEdit && envelope.status === "FINALIZING"; const canDownloadFinal = envelope.status === "COMPLETED"; async function handleSend() { if (!accessToken) return; setActionError(null); setSending(true); try { await sendInvite(envelope.id, targets, accessToken); onChanged(); } catch (err) { setActionError(err instanceof ApiError ? err.message : "Não foi possível enviar o convite."); } finally { setSending(false); } } async function handleCancelConfirm() { if (!accessToken) return; setActionError(null); setCancelling(true); try { await cancelEnvelope(envelope.id, cancelReason || undefined, accessToken); setConfirmCancel(false); setCancelReason(""); onChanged(); } catch (err) { setActionError(err instanceof ApiError ? err.message : "Não foi possível cancelar o envelope."); } finally { setCancelling(false); } } async function handleDeleteConfirm() { if (!accessToken) return; setActionError(null); setDeleting(true); try { await deleteEnvelope(envelope.id, accessToken); window.location.assign("/signatures"); } catch (err) { setActionError(err instanceof ApiError ? err.message : "Não foi possível excluir o envelope."); setDeleting(false); setConfirmDelete(false); } } async function handleSeal() { if (!accessToken) return; setActionError(null); setSealing(true); try { await sealCheckpoint(envelope.id, accessToken); onChanged(); } catch (err) { setActionError(err instanceof ApiError ? err.message : "Não foi possível selar o checkpoint."); } finally { setSealing(false); } } async function handleFinalize() { if (!accessToken) return; setActionError(null); setFinalizing(true); try { await finalizeEnvelope(envelope.id, accessToken); onChanged(); } catch (err) { setActionError(err instanceof ApiError ? err.message : "Não foi possível finalizar o envelope."); } finally { setFinalizing(false); } } async function handleDownloadFinal() { if (!accessToken) return; setActionError(null); setDownloadingFinal(true); try { await downloadFinalPdf(envelope.id, envelope.envelope_number, accessToken); } catch { setActionError("Não foi possível baixar o PDF final."); } finally { setDownloadingFinal(false); } } async function handleVerify() { if (!accessToken) return; setActionError(null); setVerifying(true); setVerifyResult(null); try { const result = await verifyAudit(envelope.id, accessToken); setVerifyResult(result); } catch (err) { setActionError(err instanceof ApiError ? err.message : "Não foi possível verificar a integridade."); } finally { setVerifying(false); } } const hasAnyAction = canSendInvite || canCancel || canDeleteEnvelope || canSeal || canFinalize || canDownloadFinal; return ( {actionError ? ( {actionError} ) : null} {!hasAnyAction ?

Nenhuma ação disponível para este status.

: null}
{canSendInvite ? (

Enviar convite

setCancelReason(e.target.value)} className="w-full rounded-md border border-border-color px-3 py-2 text-sm" />
)}
) : null} {canDeleteEnvelope ? (
{!confirmDelete ? ( ) : (

Excluir permanentemente este envelope? Esta ação não pode ser desfeita.

)}
) : null}
); } function AuditTrailCard({ events }: { events: SignatureAuditEvent[] }) { const sorted = useMemo(() => events.slice().sort((a, b) => Number(a.seq) - Number(b.seq)), [events]); return ( {sorted.length === 0 ? (

Nenhum evento registrado ainda.

) : (
    {sorted.map((event) => (
  1. ))}
)}
); }