'use client'; import { Fragment, useState } from 'react'; import { useOpiniones, useConsultarOpinion, useDescargarPdf } from '@/lib/hooks/use-documentos'; import { useDeclaraciones, useCreateDeclaracion, useUploadComprobantePago, useDeleteDeclaracion, useDownloadDeclaracionPdf, } from '@/lib/hooks/use-declaraciones'; import { fileToBase64, type Declaracion, type Impuesto, type Periodicidad } from '@/lib/api/declaraciones'; import { useConstancias, useConsultarConstancia, useDescargarConstanciaPdf } from '@/lib/hooks/use-constancias'; import type { Constancia } from '@/lib/api/constancias'; import { useAuthStore } from '@/stores/auth-store'; import { useContribuyenteStore } from '@/stores/contribuyente-store'; import { Header } from '@/components/layouts/header'; import { Card, CardContent, Button, Tabs, TabsList, TabsTrigger, TabsContent, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Input, Label } from '@horux/shared-ui'; import { FileCheck, Download, RefreshCw, Loader2, AlertTriangle, CheckCircle2, XCircle, Clock, IdCard, FileText, Upload, Plus, Trash2, Receipt, FolderOpen, Tag, Briefcase, } from 'lucide-react'; import { PapeleriaTab } from '@/components/documentos/papeleria-tab'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import * as docsApi from '@/lib/api/documentos'; const MESES = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; const IMPUESTOS: Impuesto[] = ['IVA', 'ISR', 'IEPS', 'ISN', 'DIOT', 'OTRO', 'ISH']; const PERIODICIDADES: { value: Periodicidad; label: string }[] = [ { value: 'mensual', label: 'Mensual' }, { value: 'bimestral', label: 'Bimestral' }, { value: 'trimestral', label: 'Trimestral' }, { value: 'semestral', label: 'Semestral' }, { value: 'anual', label: 'Anual' }, ]; const PERIODICIDAD_LABELS: Record = { mensual: 'Mensual', bimestral: 'Bimestral', trimestral: 'Trimestral', semestral: 'Semestral', anual: 'Anual', }; /** Returns period options based on periodicidad. Each option has a value (mes start) and a label. */ function getPeriodOptions(periodicidad: Periodicidad): { value: number; label: string }[] { switch (periodicidad) { case 'mensual': return MESES.map((m, i) => ({ value: i + 1, label: m })); case 'bimestral': return [ { value: 1, label: 'Enero – Febrero' }, { value: 3, label: 'Marzo – Abril' }, { value: 5, label: 'Mayo – Junio' }, { value: 7, label: 'Julio – Agosto' }, { value: 9, label: 'Septiembre – Octubre' }, { value: 11, label: 'Noviembre – Diciembre' }, ]; case 'trimestral': return [ { value: 1, label: 'Enero – Marzo' }, { value: 4, label: 'Abril – Junio' }, { value: 7, label: 'Julio – Septiembre' }, { value: 10, label: 'Octubre – Diciembre' }, ]; case 'semestral': return [ { value: 1, label: 'Enero – Junio' }, { value: 7, label: 'Julio – Diciembre' }, ]; case 'anual': return [{ value: 1, label: 'Anual' }]; default: return MESES.map((m, i) => ({ value: i + 1, label: m })); } } /** Returns a display label for a period (mes) given its periodicidad */ function getPeriodLabel(periodicidad: string, mes: number): string { const options = getPeriodOptions(periodicidad as Periodicidad); return options.find(o => o.value === mes)?.label || MESES[mes - 1] || String(mes); } const ROLES_UPLOAD = ['owner', 'cfo', 'contador', 'auxiliar', 'supervisor']; function EstatusBadge({ estatus }: { estatus: string }) { if (estatus === 'Positiva') return {estatus}; if (estatus === 'Negativa') return {estatus}; return {estatus}; } export default function DocumentosPage() { const user = useAuthStore((s) => s.user); const canConsultarOpinion = user?.role === 'owner' || user?.role === 'cfo'; const canSeePapeleria = user?.role !== 'cliente'; return ( <>
Opinión de Cumplimiento Constancia de Situación Fiscal Declaraciones Extras {canSeePapeleria && ( Papelería de Trabajo )} {canSeePapeleria && ( )}
); } // ============================================================================ // Opinión de Cumplimiento // ============================================================================ function OpinionTab({ canConsultar }: { canConsultar: boolean }) { const { data: opiniones, isLoading, error } = useOpiniones(); const consultar = useConsultarOpinion(); const descargar = useDescargarPdf(); return (

Opinión de Cumplimiento

{canConsultar && ( )}
{consultar.isError &&
Error: {(consultar.error as Error).message}
} {isLoading &&
} {error &&
Error al cargar opiniones: {(error as Error).message}
} {!isLoading && !error && opiniones?.length === 0 && (

No hay opiniones registradas.

La consulta automática se ejecuta cada semana.

)} {!isLoading && opiniones && opiniones.length > 0 && (
{opiniones.map((op) => ( ))}
Fecha de consulta Estatus Folio RFC PDF
{new Date(op.fechaConsulta).toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
{op.folio} {op.rfc}
)}
); } // ============================================================================ // Constancia de Situación Fiscal // ============================================================================ function ConstanciaTab() { const user = useAuthStore((s) => s.user); const canConsultar = user?.role === 'owner' || user?.role === 'cfo'; const { data: constancias, isLoading, error } = useConstancias(); const consultar = useConsultarConstancia(); const descargar = useDescargarConstanciaPdf(); const [expandedId, setExpandedId] = useState(null); const latest = constancias?.[0]; return (

Constancia de Situación Fiscal

Descarga mensual automática del SAT el día 1. También se actualizan el domicilio fiscal y los regímenes activos del tenant.

{canConsultar && ( )}
{consultar.isError &&
Error: {(consultar.error as Error).message}
} {isLoading &&
} {error &&
Error: {(error as Error).message}
} {!isLoading && !error && constancias?.length === 0 && (

No hay constancias registradas.

La consulta automática se ejecuta el 1° de cada mes.

)} {latest && ( )}
{constancias && constancias.length > 0 && (

Historial (últimas {constancias.length})

{constancias.map((c) => { const activos = c.datos.regimenes.filter(r => !r.fechaFin).length; const expanded = expandedId === c.id; return ( {expanded && ( )} ); })}
Fecha de consulta Estatus RFC Régimenes activos Acciones
{new Date(c.fechaConsulta).toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' })}
{c.estatusPadron || '—'} {c.rfc} {activos}
)}
); } function ConstanciaDetalle({ datos }: { datos: Constancia['datos'] }) { const d = datos.domicilio; const domicilio = [ [d.tipoVialidad, d.nombreVialidad].filter(Boolean).join(' '), d.numeroExterior && `Ext. ${d.numeroExterior}`, d.numeroInterior && d.numeroInterior.toUpperCase() !== 'SIN NUMERO' && `Int. ${d.numeroInterior}`, d.colonia, [d.codigoPostal, d.localidad].filter(Boolean).join(' '), [d.municipio, d.entidadFederativa].filter(Boolean).join(', '), ].filter(Boolean).join(' · '); const regimenesActivos = datos.regimenes.filter(r => !r.fechaFin); return (

Identificación

RFC
{datos.rfc}
{datos.curp &&
CURP
{datos.curp}
} {datos.razonSocial &&
Razón social
{datos.razonSocial}
} {!datos.razonSocial && (datos.nombre || datos.primerApellido) && (
Nombre
{[datos.nombre, datos.primerApellido, datos.segundoApellido].filter(Boolean).join(' ')}
)}
Estatus
{datos.estatusPadron}
Inicio de operaciones
{datos.fechaInicioOperaciones}

Domicilio fiscal

{domicilio || '—'}

Regímenes activos ({regimenesActivos.length})

{regimenesActivos.length === 0 ? (

Sin regímenes activos

) : (
    {regimenesActivos.map((r, i) => (
  • {r.nombre} desde {r.fechaInicio}
  • ))}
)}
{datos.obligaciones.length > 0 && (

Obligaciones ({datos.obligaciones.filter(o => !o.fechaFin).length} activas)

    {datos.obligaciones.filter(o => !o.fechaFin).map((o, i) => (
  • {o.descripcion} — {o.descripcionVencimiento}
  • ))}
)}
); } // ============================================================================ // Declaraciones // ============================================================================ function DeclaracionesTab() { const user = useAuthStore((s) => s.user); const canUpload = !!user?.role && ROLES_UPLOAD.includes(user.role); const { selectedContribuyenteId } = useContribuyenteStore(); // Default: current month range const now = new Date(); const y = now.getFullYear(); const m = now.getMonth() + 1; const lastDay = new Date(y, m, 0).getDate(); const [fechaDesde, setFechaDesde] = useState(`${y}-${String(m).padStart(2, '0')}-01`); const [fechaHasta, setFechaHasta] = useState(`${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`); const [uploadOpen, setUploadOpen] = useState(false); const [pagoDeclaracion, setPagoDeclaracion] = useState(null); const { data: declaraciones, isLoading, error } = useDeclaraciones(fechaDesde, fechaHasta, selectedContribuyenteId); const deleteDecl = useDeleteDeclaracion(); const downloadPdf = useDownloadDeclaracionPdf(); return (

Declaraciones

Sube el PDF de cada declaración y su comprobante de pago. Al subirla, se desactivan los recordatorios correspondientes.

{canUpload && }
setFechaDesde(e.target.value)} className="h-8 w-[150px] text-sm" />
setFechaHasta(e.target.value)} className="h-8 w-[150px] text-sm" />
{isLoading &&
} {error &&
Error: {(error as Error).message}
} {!isLoading && !error && declaraciones && declaraciones.length === 0 && (

No hay declaraciones en el rango seleccionado.

{canUpload &&

Usa el botón "Subir declaración" para cargar la primera.

}
)} {!isLoading && declaraciones && declaraciones.length > 0 && (
{declaraciones.map((d) => { const isPaidByAmount = d.montoPago === 0; const isPaid = d.tienePagoPdf || isPaidByAmount; return ( ); })}
Periodo Tipo Impuestos Monto Declaración Pago Fecha subida Acciones
{getPeriodLabel(d.periodicidad, d.mes)} {d.año} {d.tipo === 'normal' ? 'Normal' : 'Complementaria'}
{d.impuestos.map(i => {i})}
{d.montoPago != null ? `$${d.montoPago.toLocaleString('es-MX', { minimumFractionDigits: 2 })}` : '—'}
{d.pdfFilename && ( )} {d.tieneLigaPago && ( )}
{d.tienePagoPdf ? ( ) : isPaidByAmount ? ( $0 — Sin pago ) : ( Sin comprobante )} {new Date(d.createdAt).toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' })}
{canUpload && !isPaid && ( )} {canUpload && ( )}
)}
{uploadOpen && setUploadOpen(false)} />} {pagoDeclaracion && setPagoDeclaracion(null)} />}
); } // ============================================================================ // Upload Dialog // ============================================================================ function UploadDialog({ onClose }: { onClose: () => void }) { const create = useCreateDeclaracion(); const { selectedContribuyenteId } = useContribuyenteStore(); const currentYear = new Date().getFullYear(); const [año, setAño] = useState(currentYear); const [mes, setMes] = useState(new Date().getMonth() + 1); const [tipo, setTipo] = useState<'normal' | 'complementaria'>('normal'); const [periodicidad, setPeriodicidad] = useState('mensual'); const yearsOptions = Array.from({ length: 6 }, (_, i) => currentYear - i); const [impuestos, setImpuestos] = useState([]); const [montoPago, setMontoPago] = useState(''); const [file, setFile] = useState(null); const [ligaFile, setLigaFile] = useState(null); const [notas, setNotas] = useState(''); const [err, setErr] = useState(null); const periodOptions = getPeriodOptions(periodicidad); const handlePeriodicidadChange = (p: Periodicidad) => { setPeriodicidad(p); // Reset mes to first valid option for the new periodicidad const opts = getPeriodOptions(p); if (!opts.find(o => o.value === mes)) { setMes(opts[0].value); } }; const toggleImpuesto = (i: Impuesto) => { setImpuestos(prev => prev.includes(i) ? prev.filter(x => x !== i) : [...prev, i]); }; const submit = async (e: React.FormEvent) => { e.preventDefault(); setErr(null); if (!file) return setErr('Selecciona el PDF de la declaración'); if (impuestos.length === 0) return setErr('Selecciona al menos un impuesto'); try { const pdfBase64 = await fileToBase64(file); const ligaPagoBase64 = ligaFile ? await fileToBase64(ligaFile) : undefined; const montoNum = montoPago.trim() !== '' ? parseFloat(montoPago) : undefined; await create.mutateAsync({ año, mes, tipo, periodicidad, impuestos, montoPago: montoNum, pdfBase64, pdfFilename: file.name, ligaPagoBase64, ligaPagoFilename: ligaFile?.name, notas: notas.trim() || undefined, contribuyenteId: selectedContribuyenteId || undefined, }); onClose(); } catch (e: any) { setErr(e?.response?.data?.message || e?.message || 'Error al subir'); } }; return ( { if (!o) onClose(); }}> Subir declaración Al subir se marcarán como resueltos los recordatorios de declaración correspondientes. Si es complementaria, también los de pago.
{periodicidad !== 'anual' && (
)}
setMontoPago(e.target.value)} placeholder="0.00" className="mt-1" />

Si es $0.00, se marca como pagado automáticamente.

{IMPUESTOS.map(i => ( ))}

Selecciona todos los impuestos que incluye esta declaración — definen qué recordatorios se desactivan.

setFile(e.target.files?.[0] || null)} className="mt-1" />
setLigaFile(e.target.files?.[0] || null)} className="mt-1" />

Documento con la línea de captura/referencia para pagar la declaración.