- Nueva pagina /tareas para ver y marcar tareas operativas - Endpoint GET /tareas/mis-tareas con periodo actual - Quitado boton de marcar completada de obligaciones fiscales en /pendientes
224 lines
8.9 KiB
TypeScript
224 lines
8.9 KiB
TypeScript
'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<string, string> = {
|
|
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 (
|
|
<DashboardShell title="Mis Tareas">
|
|
<p className="text-muted-foreground">Cargando tareas...</p>
|
|
</DashboardShell>
|
|
);
|
|
}
|
|
|
|
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<string, TareaItem[]>, t: TareaItem) => {
|
|
const key = t.contribuyenteId;
|
|
if (!acc[key]) acc[key] = [];
|
|
acc[key].push(t);
|
|
return acc;
|
|
}, {});
|
|
|
|
const contribuyenteMap = all.reduce((acc: Record<string, { rfc: string; razonSocial: string }>, 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 (
|
|
<DashboardShell title="Mis Tareas">
|
|
<div className="space-y-4">
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-amber-100 dark:bg-amber-900 rounded-full p-2">
|
|
<Clock className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold">{pendingCount}</p>
|
|
<p className="text-xs text-muted-foreground">Pendientes</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-green-100 dark:bg-green-900 rounded-full p-2">
|
|
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold">{completedCount}</p>
|
|
<p className="text-xs text-muted-foreground">Completadas</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex gap-2">
|
|
{(['todas', 'pendientes', 'completadas'] as const).map((f) => (
|
|
<Button
|
|
key={f}
|
|
variant={filter === f ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setFilter(f)}
|
|
>
|
|
{f === 'todas' ? 'Todas' : f === 'pendientes' ? 'Pendientes' : 'Completadas'}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tareas por contribuyente */}
|
|
{Object.keys(grouped).length === 0 ? (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-muted-foreground">
|
|
No hay tareas {filter === 'pendientes' ? 'pendientes' : filter === 'completadas' ? 'completadas' : ''}.
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
Object.keys(grouped).map((contribuyenteId) => {
|
|
const info = contribuyenteMap[contribuyenteId];
|
|
const items = grouped[contribuyenteId];
|
|
return (
|
|
<Card key={contribuyenteId}>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm flex items-center gap-2">
|
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
<span>{info.razonSocial}</span>
|
|
<span className="text-muted-foreground font-normal">({info.rfc})</span>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{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 (
|
|
<div
|
|
key={t.id}
|
|
className={cn(
|
|
'flex items-center gap-3 py-2 border-b last:border-0',
|
|
p?.completada && 'opacity-60'
|
|
)}
|
|
>
|
|
<button
|
|
onClick={() => {
|
|
if (!p) return;
|
|
if (p.completada) {
|
|
descompletarMut.mutate(p.id);
|
|
} else {
|
|
completarMut.mutate(p.id);
|
|
}
|
|
}}
|
|
disabled={!p || completarMut.isPending || descompletarMut.isPending}
|
|
title={p?.completada ? 'Marcar pendiente' : 'Marcar completada'}
|
|
className="flex-shrink-0 focus:outline-none"
|
|
>
|
|
{p?.completada ? (
|
|
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
|
) : atrasada ? (
|
|
<AlertTriangle className="h-5 w-5 text-red-400" />
|
|
) : (
|
|
<Circle className="h-5 w-5 text-muted-foreground" />
|
|
)}
|
|
</button>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className={cn('text-sm font-medium', p?.completada && 'line-through text-muted-foreground')}>
|
|
{t.nombre}
|
|
</span>
|
|
{t.soloSupervisorCompleta && (
|
|
<span className="text-[10px] uppercase bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200 rounded px-1.5 py-0.5">
|
|
Supervisor
|
|
</span>
|
|
)}
|
|
{atrasada && (
|
|
<span className="text-[10px] uppercase bg-red-100 text-red-700 rounded px-1.5 py-0.5">
|
|
Atrasada
|
|
</span>
|
|
)}
|
|
</div>
|
|
{t.descripcion && (
|
|
<p className="text-xs text-muted-foreground truncate">{t.descripcion}</p>
|
|
)}
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
{recLabel} · {cuando}
|
|
{fl && ` · vence ${fl.toLocaleDateString('es-MX', { day: 'numeric', month: 'short' })}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</DashboardShell>
|
|
);
|
|
}
|