Files
HoruxDespachosNuevo/apps/web/app/(dashboard)/carteras/seguimiento-auxiliares.tsx
Horux Dev 2208cee87f fix(impuestos): desactivar JIT en queries con subplans correlacionados
- 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
2026-05-28 02:38:30 +00:00

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>
);
}