'use client'; import { isApiError, type DeclarationDto } from '@impuestos/contracts'; import { formatDateLong, formatGs, formatGsAmount, type Locale } from '@impuestos/i18n'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useRef, useState } from 'react'; import { FormPreview } from '@/components/form-preview'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { useT } from '@/i18n/t'; import { api } from '@/lib/api'; import { FilingChecklist } from './filing-checklist'; /** * Flow D2 and D3. The human summary first, the official layout underneath, and one * primary action. Nothing here files anything: the checklist that follows approval is how * the user presents it themselves. */ export function DeclarationDetail({ id, locale }: { id: string; locale: string }) { const t = useT(); const queryClient = useQueryClient(); const confirmDialog = useRef(null); const [staleWarning, setStaleWarning] = useState(null); const declaration = useQuery({ queryKey: ['declarations', id], queryFn: ({ signal }) => api.getDeclaration(id, signal), retry: (count, error) => !isApiError(error) && count < 1, }); const approve = useMutation({ mutationFn: () => api.approveDeclaration(id), onSuccess: async () => { setStaleWarning(null); confirmDialog.current?.close(); await queryClient.invalidateQueries({ queryKey: ['declarations'] }); await queryClient.invalidateQueries({ queryKey: ['dashboard'] }); }, onError: (error) => { confirmDialog.current?.close(); // A 409 means the documents moved: the numbers on screen are stale, not wrong. setStaleWarning(isApiError(error) ? error.message : t('common.error.generic')); void queryClient.invalidateQueries({ queryKey: ['declarations', id] }); }, }); if (declaration.isPending) { return ( ); } if (declaration.isError || !declaration.data) { const missing = isApiError(declaration.error) && declaration.error.code === 'not_found'; return (

{missing ? t('error.not_found') : t('common.error.generic')}

{!missing ? ( ) : null}
); } const data = declaration.data; const approved = data.status === 'approved'; // Spanish in every locale, like the form it names (COPY.md multi-language policy). const officialTitle = `Formulario ${data.formCode}`; return (

{officialTitle}

{data.period}

{staleWarning ? (

{staleWarning}

) : null}

{t('decl.summary.title')}

{humanSummary(data, t)}

{approved ? ( ) : ( <> {/* Sticky footer: the primary action stays reachable however long the form is. */}
{t('decl.downloadPdf')}

{t('decl.approve.confirmTitle')}

{t('decl.approve.confirmBody', { form: data.formCode, period: data.period, amount: formatGsAmount(payable(data)), })}

)} {approved && data.approvedAt ? (

{t('decl.approvedAt', { date: formatDateLong(locale as Locale, data.approvedAt.slice(0, 10)) })}

) : null}
); } type Translate = ReturnType; /** FLOWS.md D2: the sentence comes before the form, always. */ function humanSummary(declaration: DeclarationDto, t: Translate): string { const summary = declaration.summary; if (declaration.formCode === '120') { const aPagar = Number(summary['aPagar'] ?? 0); const result = aPagar > 0 ? t('decl.f120.toPay', { amount: formatGsAmount(aPagar) }) : t('decl.f120.inFavor', { amount: formatGsAmount(Number(summary['saldoAFavor'] ?? 0)) }); return t('decl.f120.summary', { sales: formatGsAmount(Number(summary['sales'] ?? 0)), purchases: formatGsAmount(Number(summary['purchases'] ?? 0)), result, }); } return t('decl.f515.summary', { income: formatGsAmount(Number(summary['grossIncome'] ?? 0)), deductions: formatGsAmount(Number(summary['totalDeductions'] ?? 0)), tax: formatGsAmount(Number(summary['tax'] ?? 0)), }); } function Breakdown({ declaration }: { declaration: DeclarationDto }) { const t = useT(); const summary = declaration.summary; const rows: [string, number][] = declaration.formCode === '120' ? [ [t('decl.breakdown.debito'), Number(summary['debito'] ?? 0)], [t('decl.breakdown.credito'), Number(summary['credito'] ?? 0)], [t('decl.breakdown.saldoAnterior'), Number(summary['saldoAnterior'] ?? 0)], ] : [ [t('profile.income'), Number(summary['grossIncome'] ?? 0)], [t('docs.detail.deductible'), Number(summary['totalDeductions'] ?? 0)], [t('decl.f515.netIncome'), Number(summary['netIncome'] ?? 0)], ]; const capExcess = Number(summary['capExcess'] ?? 0); return (
{rows.map(([label, amount]) => (
{label}
{formatGs(amount)}
))} {capExcess > 0 ? (

{t('decl.f515.capNote', { amount: formatGsAmount(capExcess) })}

) : null}
); } function payable(declaration: DeclarationDto): number { const summary = declaration.summary; return declaration.formCode === '120' ? Number(summary['aPagar'] ?? 0) || Number(summary['saldoAFavor'] ?? 0) : Number(summary['tax'] ?? 0); }