diff --git a/apps/web/app/(dashboard)/pendientes/page.tsx b/apps/web/app/(dashboard)/pendientes/page.tsx index 9ac5a1d..0199363 100644 --- a/apps/web/app/(dashboard)/pendientes/page.tsx +++ b/apps/web/app/(dashboard)/pendientes/page.tsx @@ -11,7 +11,7 @@ import { Header } from '@/components/layouts/header'; import { ClipboardList, CheckCircle2, - Circle, + Clock, AlertTriangle, Building2, @@ -75,7 +75,7 @@ export default function PendientesPage() { 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(() => { @@ -132,31 +132,7 @@ export default function PendientesPage() { 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) => { @@ -311,18 +287,15 @@ export default function PendientesPage() { )} >
- +

{ob.nombre}

diff --git a/apps/web/app/(dashboard)/tareas/page.tsx b/apps/web/app/(dashboard)/tareas/page.tsx new file mode 100644 index 0000000..25f1349 --- /dev/null +++ b/apps/web/app/(dashboard)/tareas/page.tsx @@ -0,0 +1,223 @@ +'use client'; + +import { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, Button, cn } from '@horux/shared-ui'; +import { DashboardShell } from '@/components/layouts/dashboard-shell'; +import { useMisTareas, useCompletarTareaPeriodo, useDescompletarTareaPeriodo } from '@/lib/hooks/use-tareas-mis'; +import { CheckCircle2, Circle, AlertTriangle, Clock, Building2 } from 'lucide-react'; + +const RECURRENCIAS: Record = { + semanal: 'Semanal', + quincenal: 'Quincenal', + mensual: 'Mensual', + bimestral: 'Bimestral', + trimestral: 'Trimestral', + semestral: 'Semestral', + anual: 'Anual', +}; + +const DIAS_SEMANA = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado']; + +interface TareaItem { + id: string; + contribuyenteId: string; + contribuyenteRfc: string; + contribuyenteRazonSocial: string; + nombre: string; + descripcion: string | null; + recurrencia: string; + diaSemana: number | null; + diaMes: number | null; + soloSupervisorCompleta: boolean; + periodoActual: { + id: string; + fechaLimite: string; + completada: boolean; + completadaAt: string | null; + completadaPor: string | null; + notas: string | null; + } | null; +} + +export default function TareasPage() { + const { data: tareas, isLoading } = useMisTareas(); + const completarMut = useCompletarTareaPeriodo(); + const descompletarMut = useDescompletarTareaPeriodo(); + const [filter, setFilter] = useState<'todas' | 'pendientes' | 'completadas'>('todas'); + + if (isLoading) { + return ( + +

Cargando tareas...

+
+ ); + } + + const all = tareas ?? []; + const filtered = all.filter((t: TareaItem) => { + if (filter === 'pendientes') return !t.periodoActual?.completada; + if (filter === 'completadas') return t.periodoActual?.completada; + return true; + }); + + // Agrupar por contribuyente + const grouped = filtered.reduce((acc: Record, t: TareaItem) => { + const key = t.contribuyenteId; + if (!acc[key]) acc[key] = []; + acc[key].push(t); + return acc; + }, {}); + + const contribuyenteMap = all.reduce((acc: Record, t: TareaItem) => { + if (!acc[t.contribuyenteId]) { + acc[t.contribuyenteId] = { rfc: t.contribuyenteRfc, razonSocial: t.contribuyenteRazonSocial }; + } + return acc; + }, {}); + + const pendingCount = all.filter((t: TareaItem) => !t.periodoActual?.completada).length; + const completedCount = all.filter((t: TareaItem) => t.periodoActual?.completada).length; + + return ( + +
+ {/* Stats */} +
+ + +
+
+ +
+
+

{pendingCount}

+

Pendientes

+
+
+
+
+ + +
+
+ +
+
+

{completedCount}

+

Completadas

+
+
+
+
+
+ + {/* Filters */} +
+ {(['todas', 'pendientes', 'completadas'] as const).map((f) => ( + + ))} +
+ + {/* Tareas por contribuyente */} + {Object.keys(grouped).length === 0 ? ( + + + No hay tareas {filter === 'pendientes' ? 'pendientes' : filter === 'completadas' ? 'completadas' : ''}. + + + ) : ( + Object.keys(grouped).map((contribuyenteId) => { + const info = contribuyenteMap[contribuyenteId]; + const items = grouped[contribuyenteId]; + return ( + + + + + {info.razonSocial} + ({info.rfc}) + + + + {items.map((t: TareaItem) => { + const p = t.periodoActual; + const fl = p ? new Date(p.fechaLimite) : null; + const today = new Date(); today.setHours(0, 0, 0, 0); + const atrasada = !!fl && !p?.completada && fl < today; + const recLabel = RECURRENCIAS[t.recurrencia] || t.recurrencia; + const cuando = (t.recurrencia === 'semanal' || t.recurrencia === 'quincenal') + ? DIAS_SEMANA[t.diaSemana ?? 1] + : `día ${t.diaMes}`; + + return ( +
+ +
+
+ + {t.nombre} + + {t.soloSupervisorCompleta && ( + + Supervisor + + )} + {atrasada && ( + + Atrasada + + )} +
+ {t.descripcion && ( +

{t.descripcion}

+ )} +

+ {recLabel} · {cuando} + {fl && ` · vence ${fl.toLocaleDateString('es-MX', { day: 'numeric', month: 'short' })}`} +

+
+
+ ); + })} +
+
+ ); + }) + )} +
+
+ ); +} diff --git a/apps/web/lib/api/tareas-mis.ts b/apps/web/lib/api/tareas-mis.ts new file mode 100644 index 0000000..4c54886 --- /dev/null +++ b/apps/web/lib/api/tareas-mis.ts @@ -0,0 +1,31 @@ +import { apiClient } from './client'; + +export interface TareaConContribuyente { + id: string; + contribuyenteId: string; + contribuyenteRfc: string; + contribuyenteRazonSocial: string; + nombre: string; + descripcion: string | null; + recurrencia: string; + diaSemana: number | null; + diaMes: number | null; + soloSupervisorCompleta: boolean; + esDefault: boolean; + active: boolean; + orden: number; + createdAt: string; + auxiliarAsignadoId?: string | null; + periodoActual: { + id: string; + periodo: string; + fechaLimite: string; + completada: boolean; + completadaAt: string | null; + completadaPor: string | null; + notas: string | null; + } | null; +} + +export const getMisTareas = () => + apiClient.get('/tareas/mis-tareas').then(r => r.data); diff --git a/apps/web/lib/hooks/use-tareas-mis.ts b/apps/web/lib/hooks/use-tareas-mis.ts new file mode 100644 index 0000000..2225618 --- /dev/null +++ b/apps/web/lib/hooks/use-tareas-mis.ts @@ -0,0 +1,37 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getMisTareas } from '../api/tareas-mis'; +import { apiClient } from '../api/client'; + +export function useMisTareas() { + return useQuery({ + queryKey: ['tareas-mis-tareas'], + queryFn: getMisTareas, + }); +} + +export function useCompletarTareaPeriodo() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (periodoId: string) => + apiClient.post(`/tareas/periodo/${periodoId}/completar`).then(r => r.data), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['tareas-mis-tareas'] }); + qc.invalidateQueries({ queryKey: ['tareas'] }); + }, + onError: (err: any) => { + alert(err.response?.data?.message || 'No se pudo marcar como completada'); + }, + }); +} + +export function useDescompletarTareaPeriodo() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (periodoId: string) => + apiClient.delete(`/tareas/periodo/${periodoId}/completar`).then(r => r.data), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['tareas-mis-tareas'] }); + qc.invalidateQueries({ queryKey: ['tareas'] }); + }, + }); +}