'use client'; import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle, Button, cn } from '@horux/shared-ui'; import { PeriodSelector } from '@horux/shared-ui'; import { useContribuyenteStore } from '@/stores/contribuyente-store'; import { useContribuyentes } from '@/lib/hooks/use-contribuyentes'; import { useAuthStore } from '@/stores/auth-store'; import { apiClient } from '@/lib/api/client'; import { Header } from '@/components/layouts/header'; import { ClipboardList, CheckCircle2, Circle, Clock, AlertTriangle, Building2, } from 'lucide-react'; interface DeclaracionLink { id: number; año: number; mes: number; tipo: 'normal' | 'complementaria'; pdfFilename: string | null; } interface ObligacionPeriodo { id: string; nombre: string; frecuencia: string | null; fechaLimite: string | null; categoria: string | null; activa: boolean; esRecomendada: boolean; completada: boolean; completadaAt: string | null; completadaPor: string | null; periodoCompletado: string | null; periodStatus: 'pendiente' | 'completada' | 'atrasada'; periodoAplica: string; /** Cuando la obligación fue completada al subir una declaración, apunta a ella. */ declaracion: DeclaracionLink | null; } interface ContribuyenteResumen { id: string; rfc: string; nombre: string; total: number; completadas: number; atrasadas: number; pendientes: number; obligaciones: ObligacionPeriodo[]; } export default function PendientesPage() { const { selectedContribuyenteId, setSelectedContribuyente } = useContribuyenteStore(); const { data: contribuyentes } = useContribuyentes(); const user = useAuthStore((s) => s.user); const now = new Date(); const [periodo, setPeriodo] = useState(() => { const y = now.getFullYear(); const m = now.getMonth() + 1; return `${y}-${String(m).padStart(2, '0')}`; }); // Derive fechaInicio/fechaFin for PeriodSelector const fechaInicio = `${periodo}-01`; const lastDay = new Date(parseInt(periodo.split('-')[0]), parseInt(periodo.split('-')[1]), 0).getDate(); const fechaFin = `${periodo}-${String(lastDay).padStart(2, '0')}`; const [resumenes, setResumenes] = useState([]); const [loading, setLoading] = useState(true); const [singleObligaciones, setSingleObligaciones] = useState([]); const [filter, setFilter] = useState<'todos' | 'mis'>('todos'); const [toggling, setToggling] = useState(null); // Single contribuyente view — fetch period-aware data useEffect(() => { if (!selectedContribuyenteId) return; setLoading(true); apiClient.get(`/contribuyentes/${selectedContribuyenteId}/obligaciones/periodo?periodo=${periodo}&atrasados=true`) .then(({ data }) => setSingleObligaciones(data.data || [])) .catch(() => setSingleObligaciones([])) .finally(() => setLoading(false)); }, [selectedContribuyenteId, periodo]); // Portfolio view — fetch period-aware data for all contribuyentes useEffect(() => { if (selectedContribuyenteId) return; if (!contribuyentes || contribuyentes.length === 0) { setLoading(false); return; } setLoading(true); Promise.all( contribuyentes.map(async (c) => { try { const { data } = await apiClient.get(`/contribuyentes/${c.id}/obligaciones/periodo?periodo=${periodo}&atrasados=true`); const items: ObligacionPeriodo[] = data.data || []; return { id: c.id, rfc: c.rfc, nombre: c.nombre, total: items.length, completadas: items.filter((o) => o.periodStatus === 'completada').length, atrasadas: items.filter((o) => o.periodStatus === 'atrasada').length, pendientes: items.filter((o) => o.periodStatus === 'pendiente').length, obligaciones: items, }; } catch { return { id: c.id, rfc: c.rfc, nombre: c.nombre, total: 0, completadas: 0, atrasadas: 0, pendientes: 0, obligaciones: [] }; } }) ) .then(setResumenes) .finally(() => setLoading(false)); }, [selectedContribuyenteId, contribuyentes, periodo]); // Filter portfolio: "Mis asignados" shows only the contribuyentes visible to the current user. // For supervisors: their cartera contribuyentes (already filtered by useContribuyentes). // For owners: all contribuyentes (no filter needed). // Since useContribuyentes already filters by role, "Mis asignados" for non-owner // is effectively the same as "Todos" (they only see their assigned ones). const filteredResumenes = filter === 'mis' && user && contribuyentes ? resumenes.filter((r) => contribuyentes.some((c) => c.id === r.id)) : resumenes; // Derived counts for single view const completadasCount = singleObligaciones.filter((o) => o.periodStatus === 'completada').length; const atrasadasCount = singleObligaciones.filter((o) => o.periodStatus === 'atrasada').length; const pendientesCount = singleObligaciones.filter((o) => o.periodStatus === 'pendiente').length; const categorias = [...new Set(singleObligaciones.map((o) => o.categoria || 'Sin categoría'))]; const toggleComplete = async (obligacionId: string, currentStatus: string, periodoAplica: string) => { if (!selectedContribuyenteId) return; const key = `${obligacionId}:${periodoAplica}`; setToggling(key); try { if (currentStatus === 'completada') { await apiClient.post( `/contribuyentes/${selectedContribuyenteId}/obligaciones/${obligacionId}/uncomplete-periodo`, { periodo: periodoAplica } ); } else { await apiClient.post( `/contribuyentes/${selectedContribuyenteId}/obligaciones/${obligacionId}/complete-periodo`, { periodo: periodoAplica } ); } // Refetch const { data } = await apiClient.get(`/contribuyentes/${selectedContribuyenteId}/obligaciones/periodo?periodo=${periodo}&atrasados=true`); setSingleObligaciones(data.data || []); } catch { // silent — state stays as-is } finally { setToggling(null); } }; // Status badge const statusBadge = (status: string) => { if (status === 'completada') return Completada; if (status === 'atrasada') return Atrasada; return Pendiente; }; // Frecuencia badge const frecBadge = (f: string | null) => { const colors: Record = { mensual: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300', bimestral: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300', trimestral: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300', anual: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300', }; return f ? ( {f} ) : null; }; // Progress bar for a resumen row const ProgressBar = ({ r }: { r: ContribuyenteResumen }) => { const pct = r.total > 0 ? Math.round((r.completadas / r.total) * 100) : 0; if (r.total === 0) return null; return (
{r.completadas}/{r.total} {pct}%
50 ? 'bg-blue-500' : 'bg-amber-500' )} style={{ width: `${pct}%` }} />
); }; return ( <>
{loading ? (
Cargando...
) : !contribuyentes || contribuyentes.length === 0 ? (

Sin contribuyentes

Agrega contribuyentes para ver sus pendientes.

) : selectedContribuyenteId ? ( /* =============== SINGLE CONTRIBUYENTE VIEW =============== */ <> {/* Period selector */}
Periodo: setPeriodo(fi.substring(0, 7))} />
{/* Summary cards */}

{singleObligaciones.length}

Total periodo

{atrasadasCount}

Atrasadas

{pendientesCount}

Pendientes

{completadasCount}

Completadas

{/* Obligations by category */} {singleObligaciones.length === 0 ? (

Sin obligaciones para este periodo

Ve a Configuración → Obligaciones Fiscales para generar recomendaciones.

) : ( categorias.map((cat) => ( {cat} {singleObligaciones.filter((o) => (o.categoria || 'Sin categoría') === cat).map((ob) => { const toggleKey = `${ob.id}:${ob.periodoAplica}`; return (

{ob.nombre}

{frecBadge(ob.frecuencia)} {statusBadge(ob.periodStatus)}
); })}
)) )} ) : ( /* =============== ALL CONTRIBUYENTES VIEW =============== */ <> {/* Period selector + filter bar */}
Periodo: setPeriodo(fi.substring(0, 7))} />
{filteredResumenes.length} contribuyente{filteredResumenes.length !== 1 ? 's' : ''}

Resumen de obligaciones por contribuyente para el periodo seleccionado. Selecciona uno para ver el detalle.

{filteredResumenes.length === 0 ? (

{filter === 'mis' ? 'No tienes contribuyentes asignados' : 'Sin contribuyentes'}

) : (
{filteredResumenes.map((r) => ( setSelectedContribuyente(r.id, r.rfc, r.nombre)} >
{r.nombre.substring(0, 2).toUpperCase()}

{r.nombre}

{r.rfc}

{r.total > 0 ? ( <>
{r.atrasadas > 0 && (

{r.atrasadas}

atrasadas

)}

{r.pendientes}

pendientes

) : (
Sin configurar
)}
))}
)} )}
); }