'use client'; import { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Card, CardContent, Button, Input, Label, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, cn, } from '@horux/shared-ui'; import { apiClient } from '@/lib/api/client'; import { useAuthStore } from '@/stores/auth-store'; import { useContribuyenteStore } from '@/stores/contribuyente-store'; import { Upload, Download, Trash2, CheckCircle2, XCircle, Clock, AlertTriangle, MessageSquare, UserCheck } from 'lucide-react'; const MESES = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre']; const ALLOWED_MIMES = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ]; const ALLOWED_EXT = '.pdf,.doc,.docx,.xls,.xlsx'; const MAX_SIZE = 5 * 1024 * 1024; const ROLES_APROBADOR = new Set(['owner', 'cfo', 'supervisor']); interface Papeleria { id: number; contribuyenteId: string; nombre: string; descripcion: string | null; archivoFilename: string; archivoMime: string; archivoSize: number; anio: number; mes: number; requiereAprobacion: boolean; estado: 'pendiente' | 'aprobado' | 'rechazado' | null; comentarioRechazo: string | null; requiereAprobacionCliente: boolean; estadoCliente: 'pendiente' | 'aprobado' | 'rechazado' | null; comentarioRechazoCliente: string | null; subidoPor: string; createdAt: string; } function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => { const result = String(r.result); const i = result.indexOf(','); resolve(i >= 0 ? result.substring(i + 1) : result); }; r.onerror = reject; r.readAsDataURL(file); }); } function estadoGlobal(item: Papeleria): 'sin_aprobacion' | 'pendiente' | 'aprobado' | 'rechazado' { const reqOwner = item.requiereAprobacion; const reqCliente = item.requiereAprobacionCliente; const estOwner = item.estado; const estCliente = item.estadoCliente; if (!reqOwner && !reqCliente) return 'sin_aprobacion'; if (estOwner === 'rechazado' || estCliente === 'rechazado') return 'rechazado'; if (reqOwner && reqCliente) { if (estOwner === 'aprobado' && estCliente === 'aprobado') return 'aprobado'; return 'pendiente'; } if (reqOwner) return estOwner ?? 'pendiente'; return estCliente ?? 'pendiente'; } function EstadoBadge({ item }: { item: Papeleria }) { const global = estadoGlobal(item); if (global === 'sin_aprobacion') { return Sin aprobación; } if (global === 'aprobado') { return Aprobado; } if (global === 'rechazado') { return Rechazado; } // Pendiente — mostrar quién falta const faltaOwner = item.requiereAprobacion && item.estado !== 'aprobado'; const faltaCliente = item.requiereAprobacionCliente && item.estadoCliente !== 'aprobado'; let label = 'Pendiente'; if (faltaOwner && faltaCliente) label = 'Pendiente (ambos)'; else if (faltaOwner) label = 'Pendiente (owner)'; else if (faltaCliente) label = 'Pendiente (cliente)'; return {label}; } export function PapeleriaTab() { const user = useAuthStore(s => s.user); const { selectedContribuyenteId } = useContribuyenteStore(); const queryClient = useQueryClient(); const isCliente = user?.role === 'cliente'; const canApproveOwner = user?.role ? ROLES_APROBADOR.has(user.role) : false; const canUpload = !isCliente; const [showUpload, setShowUpload] = useState(false); const [rechazoFor, setRechazoFor] = useState(null); const [comentarioRechazo, setComentarioRechazo] = useState(''); const [rechazoClienteFor, setRechazoClienteFor] = useState(null); const [comentarioRechazoCliente, setComentarioRechazoCliente] = useState(''); // Filtros const currentYear = new Date().getFullYear(); const [filterAnio, setFilterAnio] = useState(''); const [filterMes, setFilterMes] = useState(''); const [filterEstado, setFilterEstado] = useState(''); const query = useQuery({ queryKey: ['papeleria', selectedContribuyenteId, filterAnio, filterMes, filterEstado], queryFn: async () => { const p = new URLSearchParams({ contribuyenteId: selectedContribuyenteId! }); if (filterAnio) p.set('anio', String(filterAnio)); if (filterMes) p.set('mes', String(filterMes)); if (filterEstado) p.set('estado', filterEstado); const { data } = await apiClient.get(`/papeleria?${p}`); return data; }, enabled: !!selectedContribuyenteId, }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['papeleria'] }); // Upload form state const [file, setFile] = useState(null); const [nombre, setNombre] = useState(''); const [descripcion, setDescripcion] = useState(''); const [anio, setAnio] = useState(currentYear); const [mes, setMes] = useState(new Date().getMonth() + 1); const [requiereAprobacion, setRequiereAprobacion] = useState(false); const [requiereAprobacionCliente, setRequiereAprobacionCliente] = useState(false); const [uploadError, setUploadError] = useState(null); const resetUpload = () => { setFile(null); setNombre(''); setDescripcion(''); setAnio(currentYear); setMes(new Date().getMonth() + 1); setRequiereAprobacion(false); setRequiereAprobacionCliente(false); setUploadError(null); }; const uploadMutation = useMutation({ mutationFn: async () => { if (!file) throw new Error('Selecciona un archivo'); if (!ALLOWED_MIMES.includes(file.type)) throw new Error('Formato no permitido. Usa PDF, Word o Excel.'); if (file.size > MAX_SIZE) throw new Error('El archivo excede 5 MB.'); const base64 = await fileToBase64(file); await apiClient.post('/papeleria', { contribuyenteId: selectedContribuyenteId, nombre: nombre || file.name, descripcion: descripcion || null, anio, mes, requiereAprobacion, requiereAprobacionCliente, archivoBase64: base64, archivoFilename: file.name, archivoMime: file.type, }); }, onError: (err: any) => { setUploadError(err?.response?.data?.message || err.message || 'Error al subir'); }, onSuccess: () => { setShowUpload(false); resetUpload(); invalidate(); }, }); const downloadMutation = useMutation({ mutationFn: async (item: Papeleria) => { const res = await apiClient.get(`/papeleria/${item.id}/download`, { responseType: 'blob' }); const url = URL.createObjectURL(res.data); const a = document.createElement('a'); a.href = url; a.download = item.archivoFilename; a.click(); URL.revokeObjectURL(url); }, }); const aprobarMutation = useMutation({ mutationFn: async (id: number) => apiClient.post(`/papeleria/${id}/aprobar`), onSuccess: invalidate, }); const rechazarMutation = useMutation({ mutationFn: async ({ id, comentario }: { id: number; comentario: string | null }) => apiClient.post(`/papeleria/${id}/rechazar`, { comentario }), onSuccess: () => { setRechazoFor(null); setComentarioRechazo(''); invalidate(); }, }); const aprobarClienteMutation = useMutation({ mutationFn: async (id: number) => apiClient.post(`/papeleria/${id}/aprobar-cliente`), onSuccess: invalidate, }); const rechazarClienteMutation = useMutation({ mutationFn: async ({ id, comentario }: { id: number; comentario: string | null }) => apiClient.post(`/papeleria/${id}/rechazar-cliente`, { comentario }), onSuccess: () => { setRechazoClienteFor(null); setComentarioRechazoCliente(''); invalidate(); }, }); const eliminarMutation = useMutation({ mutationFn: async (id: number) => apiClient.delete(`/papeleria/${id}`), onSuccess: invalidate, }); if (!selectedContribuyenteId) { return ( Selecciona un contribuyente para ver su papelería. ); } const items = query.data ?? []; const años = useMemo(() => { const set = new Set([currentYear]); items.forEach(i => set.add(i.anio)); return [...set].sort((a, b) => b - a); }, [items, currentYear]); return (
{/* Filtros + upload */}
{canUpload && ( )}
{/* Listado */} {query.isLoading ? (

Cargando...

) : items.length === 0 ? ( No hay documentos en papelería con los filtros seleccionados. ) : (
{items.map(it => (
{it.nombre} {MESES[it.mes - 1]} {it.anio}
{it.descripcion && (

{it.descripcion}

)}

{it.archivoFilename} · {(it.archivoSize / 1024).toFixed(0)} KB · subido {new Date(it.createdAt).toLocaleDateString('es-MX')}

{/* Mostrar estado detallado para no-clientes */} {!isCliente && (it.requiereAprobacion || it.requiereAprobacionCliente) && (
{it.requiereAprobacion && ( Owner: {it.estado ?? '—'} )} {it.requiereAprobacionCliente && ( Cliente: {it.estadoCliente ?? '—'} )}
)} {it.estado === 'rechazado' && it.comentarioRechazo && (

Owner: {it.comentarioRechazo}

)} {it.estadoCliente === 'rechazado' && it.comentarioRechazoCliente && (

Cliente: {it.comentarioRechazoCliente}

)}
{/* Botones owner/supervisor */} {canApproveOwner && it.requiereAprobacion && it.estado === 'pendiente' && ( <> )} {/* Botones cliente */} {isCliente && it.requiereAprobacionCliente && it.estadoCliente === 'pendiente' && ( <> )} {canUpload && ( )}
))}
)} {/* Modal Upload */} { setShowUpload(o); if (!o) resetUpload(); }}> Subir documento de papelería
{ const f = e.target.files?.[0] ?? null; setFile(f); if (f && !nombre) setNombre(f.name.replace(/\.[^.]+$/, '')); setUploadError(null); }} className="block w-full text-sm border rounded-md px-3 py-2" /> {file && (

{file.name} · {(file.size / 1024).toFixed(0)} KB

)}
setNombre(e.target.value)} placeholder="Ej. Reporte de cuentas" />
setDescripcion(e.target.value)} />
setAnio(parseInt(e.target.value, 10) || currentYear)} />
{uploadError && (

{uploadError}

)}
{/* Modal Rechazo Owner */} { if (!o) { setRechazoFor(null); setComentarioRechazo(''); } }}> Rechazar documento

Vas a rechazar {rechazoFor?.nombre}. El comentario es opcional.

setComentarioRechazo(e.target.value)} placeholder="Motivo del rechazo..." />
{/* Modal Rechazo Cliente */} { if (!o) { setRechazoClienteFor(null); setComentarioRechazoCliente(''); } }}> Rechazar documento

Vas a rechazar {rechazoClienteFor?.nombre}. El comentario es opcional.

setComentarioRechazoCliente(e.target.value)} placeholder="Motivo del rechazo..." />
); }