- Agrega helper withJitOff en impuestos.service.ts - Ejecuta getResumenIva, getIvaMensual y readResumenIvaFromCache con SET LOCAL jit = off - Evita compilación JIT de ~17s en queries con costo estimado alto feat(contribuyentes): auto-asignar a cartera del supervisor - Al crear contribuyente con supervisorUserId, se agrega automáticamente a todas las carteras top-level del supervisor feat(permisos): restricciones de UI por rol en contribuyentes - Oculta botón Add-ons para roles distintos de owner/cfo - Oculta botón Eliminar contribuyente para no-owner - Oculta botón Agregar RFC para auxiliar/visor/cliente/contador feat(cfdi): ver CFDI desde conceptos y forma de pago en Excel - Agrega botón Ver CFDI en cada fila de la tabla de Conceptos - Agrega columna Forma de Pago en export Excel de CFDIs - Agrega columna Forma de Pago en export individual de CFDI chore(migraciones): índices GIN para relaciones de activos - 048: índices btree parciales para activos - 049: índices GIN para cfdis_relacionados y uuid_relacionado
347 lines
13 KiB
TypeScript
347 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Button, Card, CardContent, CardHeader, CardTitle,
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
Tabs, TabsList, TabsTrigger, TabsContent,
|
|
} from '@horux/shared-ui';
|
|
import {
|
|
useAsignacionesSupervisor,
|
|
useSinAsignar,
|
|
useAsignarObligacion,
|
|
useDesasignarObligacion,
|
|
useAsignarTarea,
|
|
useDesasignarTarea,
|
|
useAuxiliaresElegibles,
|
|
} from '@/lib/hooks/use-asignaciones';
|
|
import { useUsuarios } from '@/lib/hooks/use-usuarios';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { UserCheck, UserX, UserCog, Plus } from 'lucide-react';
|
|
|
|
export default function SeguimientoAuxiliares() {
|
|
const { user } = useAuthStore();
|
|
const { data: asignaciones, isLoading: loadingAsignadas } = useAsignacionesSupervisor();
|
|
const { data: sinAsignar, isLoading: loadingSinAsignar } = useSinAsignar();
|
|
const { data: usuarios } = useUsuarios();
|
|
const asignarObligacionMut = useAsignarObligacion();
|
|
const desasignarObligacionMut = useDesasignarObligacion();
|
|
const asignarTareaMut = useAsignarTarea();
|
|
const desasignarTareaMut = useDesasignarTarea();
|
|
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [modalType, setModalType] = useState<'obligacion' | 'tarea'>('obligacion');
|
|
const [modalItem, setModalItem] = useState<any>(null);
|
|
const [selectedAuxiliar, setSelectedAuxiliar] = useState('');
|
|
|
|
const auxiliares = (usuarios ?? []).filter((u: any) => u.role === 'auxiliar');
|
|
|
|
const { data: elegiblesData, isLoading: loadingElegibles } = useAuxiliaresElegibles(modalItem?.contribuyenteId);
|
|
const auxiliaresIdsElegibles = elegiblesData?.auxiliares ?? [];
|
|
const auxiliaresFiltrados = auxiliares.filter((a: any) => auxiliaresIdsElegibles.includes(a.id));
|
|
const puedeAsignar = !loadingElegibles && auxiliaresFiltrados.length > 0;
|
|
|
|
const openAssignModal = (type: 'obligacion' | 'tarea', item: any) => {
|
|
setModalType(type);
|
|
setModalItem(item);
|
|
setSelectedAuxiliar(item.auxiliarUserId || '');
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const handleAssign = async () => {
|
|
if (!selectedAuxiliar || !modalItem) return;
|
|
try {
|
|
if (modalType === 'obligacion') {
|
|
await asignarObligacionMut.mutateAsync({
|
|
contribuyenteId: modalItem.contribuyenteId,
|
|
obligacionId: modalItem.obligacionId,
|
|
auxiliarUserId: selectedAuxiliar,
|
|
});
|
|
} else {
|
|
await asignarTareaMut.mutateAsync({
|
|
tareaId: modalItem.tareaId,
|
|
auxiliarUserId: selectedAuxiliar,
|
|
});
|
|
}
|
|
setModalOpen(false);
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.message || 'Error al asignar');
|
|
}
|
|
};
|
|
|
|
const handleUnassign = async (type: 'obligacion' | 'tarea', item: any) => {
|
|
if (!confirm('¿Eliminar la asignación?')) return;
|
|
try {
|
|
if (type === 'obligacion') {
|
|
await desasignarObligacionMut.mutateAsync({
|
|
contribuyenteId: item.contribuyenteId,
|
|
obligacionId: item.obligacionId,
|
|
});
|
|
} else {
|
|
await desasignarTareaMut.mutateAsync({ tareaId: item.tareaId });
|
|
}
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.message || 'Error al desasignar');
|
|
}
|
|
};
|
|
|
|
if (loadingAsignadas || loadingSinAsignar) {
|
|
return <p className="text-muted-foreground">Cargando asignaciones...</p>;
|
|
}
|
|
|
|
const obligacionesAsignadas = asignaciones?.obligaciones ?? [];
|
|
const tareasAsignadas = asignaciones?.tareas ?? [];
|
|
const obligacionesSinAsignar = sinAsignar?.obligaciones ?? [];
|
|
const tareasSinAsignar = sinAsignar?.tareas ?? [];
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Tabs defaultValue="asignadas" className="space-y-4">
|
|
<TabsList>
|
|
<TabsTrigger value="asignadas">Asignadas</TabsTrigger>
|
|
<TabsTrigger value="sin-asignar">Sin asignar</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="asignadas">
|
|
<Tabs defaultValue="obligaciones" className="space-y-4">
|
|
<TabsList>
|
|
<TabsTrigger value="obligaciones">Obligaciones ({obligacionesAsignadas.length})</TabsTrigger>
|
|
<TabsTrigger value="tareas">Tareas ({tareasAsignadas.length})</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="obligaciones">
|
|
<AsignacionesTable
|
|
items={obligacionesAsignadas}
|
|
tipo="obligacion"
|
|
modo="asignadas"
|
|
auxiliares={auxiliares}
|
|
onAssign={(item) => openAssignModal('obligacion', item)}
|
|
onUnassign={(item) => handleUnassign('obligacion', item)}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="tareas">
|
|
<AsignacionesTable
|
|
items={tareasAsignadas}
|
|
tipo="tarea"
|
|
modo="asignadas"
|
|
auxiliares={auxiliares}
|
|
onAssign={(item) => openAssignModal('tarea', item)}
|
|
onUnassign={(item) => handleUnassign('tarea', item)}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="sin-asignar">
|
|
<Tabs defaultValue="obligaciones" className="space-y-4">
|
|
<TabsList>
|
|
<TabsTrigger value="obligaciones">Obligaciones ({obligacionesSinAsignar.length})</TabsTrigger>
|
|
<TabsTrigger value="tareas">Tareas ({tareasSinAsignar.length})</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="obligaciones">
|
|
<SinAsignarTable
|
|
items={obligacionesSinAsignar}
|
|
tipo="obligacion"
|
|
auxiliares={auxiliares}
|
|
onAssign={(item) => openAssignModal('obligacion', item)}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="tareas">
|
|
<SinAsignarTable
|
|
items={tareasSinAsignar}
|
|
tipo="tarea"
|
|
auxiliares={auxiliares}
|
|
onAssign={(item) => openAssignModal('tarea', item)}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Modal de asignación */}
|
|
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{modalItem?.auxiliarUserId ? 'Reasignar' : 'Asignar'} {modalType === 'obligacion' ? 'obligación' : 'tarea'}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="py-4">
|
|
<p className="text-sm text-muted-foreground mb-2">
|
|
{modalType === 'obligacion' ? modalItem?.obligacionNombre : modalItem?.tareaNombre}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mb-4">
|
|
Contribuyente: {modalItem?.contribuyenteRazonSocial} ({modalItem?.contribuyenteRfc})
|
|
</p>
|
|
{loadingElegibles ? (
|
|
<p className="text-sm text-muted-foreground">Verificando subcarteras...</p>
|
|
) : auxiliaresFiltrados.length === 0 ? (
|
|
<p className="text-sm text-red-600">
|
|
Ningún auxiliar tiene este contribuyente en su subcartera. No se puede asignar.
|
|
</p>
|
|
) : (
|
|
<Select value={selectedAuxiliar} onValueChange={setSelectedAuxiliar}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Selecciona un auxiliar" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{auxiliaresFiltrados.map((a: any) => (
|
|
<SelectItem key={a.id} value={a.id}>{a.nombre} ({a.email})</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setModalOpen(false)}>Cancelar</Button>
|
|
<Button onClick={handleAssign} disabled={!selectedAuxiliar || !puedeAsignar}>
|
|
{modalItem?.auxiliarUserId ? 'Reasignar' : 'Asignar'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AsignacionesTable({
|
|
items,
|
|
tipo,
|
|
modo,
|
|
auxiliares,
|
|
onAssign,
|
|
onUnassign,
|
|
}: {
|
|
items: any[];
|
|
tipo: 'obligacion' | 'tarea';
|
|
modo: 'asignadas';
|
|
auxiliares: any[];
|
|
onAssign: (item: any) => void;
|
|
onUnassign: (item: any) => void;
|
|
}) {
|
|
if (items.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-muted-foreground">
|
|
No hay {tipo === 'obligacion' ? 'obligaciones' : 'tareas'} asignadas.
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted/50">
|
|
<tr>
|
|
<th className="text-left px-4 py-3 font-medium">Auxiliar</th>
|
|
<th className="text-left px-4 py-3 font-medium">Contribuyente</th>
|
|
<th className="text-left px-4 py-3 font-medium">{tipo === 'obligacion' ? 'Obligación' : 'Tarea'}</th>
|
|
<th className="text-left px-4 py-3 font-medium">Asignado</th>
|
|
<th className="text-right px-4 py-3 font-medium">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y">
|
|
{items.map((item) => (
|
|
<tr key={item.id} className="hover:bg-muted/30">
|
|
<td className="px-4 py-3">
|
|
{item.auxiliarNombre ? (
|
|
<span className="inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 flex items-center gap-1 w-fit">
|
|
<UserCheck className="h-3 w-3" /> {item.auxiliarNombre}
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-border text-muted-foreground hover:bg-secondary/80 flex items-center gap-1 w-fit">
|
|
<UserX className="h-3 w-3" /> Sin asignar
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="font-medium">{item.contribuyenteRazonSocial}</div>
|
|
<div className="text-xs text-muted-foreground">{item.contribuyenteRfc}</div>
|
|
</td>
|
|
<td className="px-4 py-3">{tipo === 'obligacion' ? item.obligacionNombre : item.tareaNombre}</td>
|
|
<td className="px-4 py-3 text-muted-foreground">
|
|
{item.asignadoAt ? new Date(item.asignadoAt).toLocaleDateString('es-MX') : '-'}
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button variant="ghost" size="sm" onClick={() => onAssign(item)}>
|
|
<UserCog className="h-4 w-4" />
|
|
</Button>
|
|
{item.auxiliarUserId && (
|
|
<Button variant="ghost" size="sm" className="text-red-600" onClick={() => onUnassign(item)}>
|
|
<UserX className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function SinAsignarTable({
|
|
items,
|
|
tipo,
|
|
auxiliares,
|
|
onAssign,
|
|
}: {
|
|
items: any[];
|
|
tipo: 'obligacion' | 'tarea';
|
|
auxiliares: any[];
|
|
onAssign: (item: any) => void;
|
|
}) {
|
|
if (items.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-muted-foreground">
|
|
No hay {tipo === 'obligacion' ? 'obligaciones' : 'tareas'} sin asignar.
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted/50">
|
|
<tr>
|
|
<th className="text-left px-4 py-3 font-medium">Contribuyente</th>
|
|
<th className="text-left px-4 py-3 font-medium">{tipo === 'obligacion' ? 'Obligación' : 'Tarea'}</th>
|
|
<th className="text-right px-4 py-3 font-medium">Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y">
|
|
{items.map((item, idx) => (
|
|
<tr key={`${item.obligacionId || item.tareaId}-${idx}`} className="hover:bg-muted/30">
|
|
<td className="px-4 py-3">
|
|
<div className="font-medium">{item.contribuyenteRazonSocial}</div>
|
|
<div className="text-xs text-muted-foreground">{item.contribuyenteRfc}</div>
|
|
</td>
|
|
<td className="px-4 py-3">{tipo === 'obligacion' ? item.obligacionNombre : item.tareaNombre}</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<Button variant="ghost" size="sm" onClick={() => onAssign(item)}>
|
|
<Plus className="h-4 w-4 mr-1" /> Asignar
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|