feat: seguimiento auxiliares UI con tabs Asignadas/Sin asignar
- Componente seguimiento-auxiliares.tsx con tabs Asignadas/Sin asignar - Tabs internos Obligaciones/Tareas en cada vista - API client y hooks para asignaciones - Fix: invalidar query sin-asignar al asignar/desasignar
This commit is contained in:
@@ -5,12 +5,13 @@ import {
|
|||||||
Button, Card, CardContent, CardHeader, CardTitle, Input, Label,
|
Button, Card, CardContent, CardHeader, CardTitle, Input, Label,
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
|
Tabs, TabsList, TabsTrigger, TabsContent,
|
||||||
cn,
|
cn,
|
||||||
} from '@horux/shared-ui';
|
} from '@horux/shared-ui';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
FolderOpen, Plus, Trash2, ChevronDown, ChevronUp, X,
|
FolderOpen, Plus, Trash2, ChevronDown, ChevronUp, X,
|
||||||
Users, Building2, FolderPlus, UserCog,
|
Users, Building2, FolderPlus, UserCog, ClipboardList,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
useCarteras, useCreateCartera, useDeleteCartera,
|
useCarteras, useCreateCartera, useDeleteCartera,
|
||||||
@@ -25,14 +26,16 @@ import { useUsuarios } from '@/lib/hooks/use-usuarios';
|
|||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { DashboardShell } from '@/components/layouts/dashboard-shell';
|
import { DashboardShell } from '@/components/layouts/dashboard-shell';
|
||||||
import type { Cartera } from '@/lib/api/carteras';
|
import type { Cartera } from '@/lib/api/carteras';
|
||||||
|
import SeguimientoAuxiliares from './seguimiento-auxiliares';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* SubcarteraCard */
|
/* SubcarteraCard */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
function SubcarteraCard({ sub, usuarios, contribuyentes, onDelete }: {
|
function SubcarteraCard({ sub, usuarios, contribuyentes, parentEntidadIds, onDelete }: {
|
||||||
sub: Cartera;
|
sub: Cartera;
|
||||||
usuarios: any[];
|
usuarios: any[];
|
||||||
contribuyentes: any[];
|
contribuyentes: any[];
|
||||||
|
parentEntidadIds: string[];
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
@@ -47,7 +50,7 @@ function SubcarteraCard({ sub, usuarios, contribuyentes, onDelete }: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const available = (contribuyentes ?? []).filter(
|
const available = (contribuyentes ?? []).filter(
|
||||||
(c: any) => !(entidadIds ?? []).includes(c.id)
|
(c: any) => (parentEntidadIds ?? []).includes(c.id) && !(entidadIds ?? []).includes(c.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const auxiliarUser = usuarios?.find((u: any) => u.id === sub.auxiliarUserId);
|
const auxiliarUser = usuarios?.find((u: any) => u.id === sub.auxiliarUserId);
|
||||||
@@ -319,6 +322,7 @@ function CarteraDetail({ cartera, canEdit = true, canManageSubcarteras = true }:
|
|||||||
sub={sub}
|
sub={sub}
|
||||||
usuarios={usuarios ?? []}
|
usuarios={usuarios ?? []}
|
||||||
contribuyentes={contribuyentes ?? []}
|
contribuyentes={contribuyentes ?? []}
|
||||||
|
parentEntidadIds={entidadIds ?? []}
|
||||||
onDelete={() => handleDeleteSubcartera(sub.id)}
|
onDelete={() => handleDeleteSubcartera(sub.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -396,6 +400,10 @@ export default function CarterasPage() {
|
|||||||
const canEditCartera = userRole === 'owner'; // Edit/delete top-level carteras + add/remove RFCs
|
const canEditCartera = userRole === 'owner'; // Edit/delete top-level carteras + add/remove RFCs
|
||||||
const canManageSubcarteras = userRole === 'owner' || userRole === 'supervisor'; // Create subcarteras
|
const canManageSubcarteras = userRole === 'owner' || userRole === 'supervisor'; // Create subcarteras
|
||||||
const isAuxiliar = userRole === 'auxiliar';
|
const isAuxiliar = userRole === 'auxiliar';
|
||||||
|
const isSupervisor = userRole === 'supervisor';
|
||||||
|
const isOwner = userRole === 'owner';
|
||||||
|
const puedeVerSeguimiento = isOwner || isSupervisor;
|
||||||
|
const [activeTab, setActiveTab] = useState('carteras');
|
||||||
const { data: carteras, isLoading } = useCarteras();
|
const { data: carteras, isLoading } = useCarteras();
|
||||||
const { data: supervisores } = useSupervisores();
|
const { data: supervisores } = useSupervisores();
|
||||||
const { data: usuarios } = useUsuarios();
|
const { data: usuarios } = useUsuarios();
|
||||||
@@ -440,9 +448,43 @@ export default function CarterasPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CarterasList = () => (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-muted-foreground">Cargando...</p>
|
||||||
|
) : !carteras || carteras.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<FolderOpen className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold">Sin carteras</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1 mb-4">
|
||||||
|
Crea la primera cartera para organizar tus contribuyentes.
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => setShowCreate(true)}>Crear primera cartera</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{carteras.map(cartera => (
|
||||||
|
<CarteraCard
|
||||||
|
key={cartera.id}
|
||||||
|
cartera={cartera}
|
||||||
|
expanded={expandedId === cartera.id}
|
||||||
|
onToggle={() => setExpandedId(expandedId === cartera.id ? null : cartera.id)}
|
||||||
|
onDelete={() => handleDelete(cartera)}
|
||||||
|
usuarios={usuarios ?? []}
|
||||||
|
canEdit={canEditCartera}
|
||||||
|
canManageSubcarteras={canManageSubcarteras}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardShell title="Carteras">
|
<DashboardShell title="Carteras">
|
||||||
<div className="max-w-3xl mx-auto space-y-6">
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -450,42 +492,34 @@ export default function CarterasPage() {
|
|||||||
{isAuxiliar ? 'Carteras asignadas a ti' : 'Organiza contribuyentes en carteras y asigna subcarteras a cada auxiliar'}
|
{isAuxiliar ? 'Carteras asignadas a ti' : 'Organiza contribuyentes en carteras y asigna subcarteras a cada auxiliar'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{canCreate && (
|
{canCreate && activeTab === 'carteras' && (
|
||||||
<Button onClick={() => setShowCreate(true)} className="flex items-center gap-2">
|
<Button onClick={() => setShowCreate(true)} className="flex items-center gap-2">
|
||||||
<Plus className="h-4 w-4" /> Nueva cartera
|
<Plus className="h-4 w-4" /> Nueva cartera
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List */}
|
{puedeVerSeguimiento ? (
|
||||||
{isLoading ? (
|
<Tabs value={activeTab} onValueChange={setActiveTab} defaultValue="carteras" className="space-y-4">
|
||||||
<p className="text-muted-foreground">Cargando...</p>
|
<TabsList>
|
||||||
) : !carteras || carteras.length === 0 ? (
|
<TabsTrigger value="carteras">
|
||||||
<Card>
|
<FolderOpen className="h-4 w-4 mr-1.5" /> Carteras
|
||||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
</TabsTrigger>
|
||||||
<FolderOpen className="h-12 w-12 text-muted-foreground mb-4" />
|
<TabsTrigger value="seguimiento">
|
||||||
<h3 className="text-lg font-semibold">Sin carteras</h3>
|
<ClipboardList className="h-4 w-4 mr-1.5" /> Seguimiento de Auxiliares
|
||||||
<p className="text-sm text-muted-foreground mt-1 mb-4">
|
</TabsTrigger>
|
||||||
Crea la primera cartera para organizar tus contribuyentes.
|
</TabsList>
|
||||||
</p>
|
|
||||||
<Button onClick={() => setShowCreate(true)}>Crear primera cartera</Button>
|
<TabsContent value="carteras">
|
||||||
</CardContent>
|
<CarterasList />
|
||||||
</Card>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="seguimiento">
|
||||||
|
<SeguimientoAuxiliares />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<CarterasList />
|
||||||
{carteras.map(cartera => (
|
|
||||||
<CarteraCard
|
|
||||||
key={cartera.id}
|
|
||||||
cartera={cartera}
|
|
||||||
expanded={expandedId === cartera.id}
|
|
||||||
onToggle={() => setExpandedId(expandedId === cartera.id ? null : cartera.id)}
|
|
||||||
onDelete={() => handleDelete(cartera)}
|
|
||||||
usuarios={usuarios ?? []}
|
|
||||||
canEdit={canEditCartera}
|
|
||||||
canManageSubcarteras={canManageSubcarteras}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create dialog */}
|
{/* Create dialog */}
|
||||||
|
|||||||
332
apps/web/app/(dashboard)/carteras/seguimiento-auxiliares.tsx
Normal file
332
apps/web/app/(dashboard)/carteras/seguimiento-auxiliares.tsx
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
'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,
|
||||||
|
} 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 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>
|
||||||
|
<Select value={selectedAuxiliar} onValueChange={setSelectedAuxiliar}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Selecciona un auxiliar" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{auxiliares.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}>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
apps/web/lib/api/asignaciones.ts
Normal file
58
apps/web/lib/api/asignaciones.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { apiClient } from './client';
|
||||||
|
|
||||||
|
export interface AsignacionObligacion {
|
||||||
|
id: string;
|
||||||
|
obligacionId: string;
|
||||||
|
obligacionNombre: string;
|
||||||
|
contribuyenteId: string;
|
||||||
|
contribuyenteRfc: string;
|
||||||
|
contribuyenteRazonSocial: string;
|
||||||
|
auxiliarUserId: string;
|
||||||
|
auxiliarNombre: string | null;
|
||||||
|
asignadoPor: string;
|
||||||
|
asignadoAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsignacionTarea {
|
||||||
|
id: string;
|
||||||
|
tareaId: string;
|
||||||
|
tareaNombre: string;
|
||||||
|
contribuyenteId: string;
|
||||||
|
contribuyenteRfc: string;
|
||||||
|
contribuyenteRazonSocial: string;
|
||||||
|
auxiliarUserId: string;
|
||||||
|
auxiliarNombre: string | null;
|
||||||
|
asignadoPor: string;
|
||||||
|
asignadoAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsignacionesResponse {
|
||||||
|
obligaciones: AsignacionObligacion[];
|
||||||
|
tareas: AsignacionTarea[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAsignacionesPorSupervisor = () =>
|
||||||
|
apiClient.get<AsignacionesResponse>('/carteras/asignaciones').then(r => r.data);
|
||||||
|
|
||||||
|
export const getAsignacionesPorAuxiliar = () =>
|
||||||
|
apiClient.get<AsignacionesResponse>('/carteras/asignaciones/mias').then(r => r.data);
|
||||||
|
|
||||||
|
export interface SinAsignarResponse {
|
||||||
|
obligaciones: Omit<AsignacionObligacion, 'id' | 'auxiliarUserId' | 'auxiliarNombre' | 'asignadoPor' | 'asignadoAt'>[];
|
||||||
|
tareas: Omit<AsignacionTarea, 'id' | 'auxiliarUserId' | 'auxiliarNombre' | 'asignadoPor' | 'asignadoAt'>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSinAsignar = () =>
|
||||||
|
apiClient.get<SinAsignarResponse>('/carteras/asignaciones/sin-asignar').then(r => r.data);
|
||||||
|
|
||||||
|
export const asignarObligacion = (contribuyenteId: string, obligacionId: string, auxiliarUserId: string) =>
|
||||||
|
apiClient.post(`/contribuyentes/${contribuyenteId}/obligaciones/${obligacionId}/asignar`, { auxiliarUserId }).then(r => r.data);
|
||||||
|
|
||||||
|
export const desasignarObligacion = (contribuyenteId: string, obligacionId: string) =>
|
||||||
|
apiClient.delete(`/contribuyentes/${contribuyenteId}/obligaciones/${obligacionId}/asignar`).then(r => r.data);
|
||||||
|
|
||||||
|
export const asignarTarea = (tareaId: string, auxiliarUserId: string) =>
|
||||||
|
apiClient.post(`/tareas/${tareaId}/asignar`, { auxiliarUserId }).then(r => r.data);
|
||||||
|
|
||||||
|
export const desasignarTarea = (tareaId: string) =>
|
||||||
|
apiClient.delete(`/tareas/${tareaId}/asignar`).then(r => r.data);
|
||||||
89
apps/web/lib/hooks/use-asignaciones.ts
Normal file
89
apps/web/lib/hooks/use-asignaciones.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
getAsignacionesPorSupervisor,
|
||||||
|
getAsignacionesPorAuxiliar,
|
||||||
|
getSinAsignar,
|
||||||
|
asignarObligacion,
|
||||||
|
desasignarObligacion,
|
||||||
|
asignarTarea,
|
||||||
|
desasignarTarea,
|
||||||
|
} from '../api/asignaciones';
|
||||||
|
|
||||||
|
export function useAsignacionesSupervisor() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['asignaciones-supervisor'],
|
||||||
|
queryFn: getAsignacionesPorSupervisor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAsignacionesAuxiliar() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['asignaciones-auxiliar'],
|
||||||
|
queryFn: getAsignacionesPorAuxiliar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSinAsignar() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['asignaciones-sin-asignar'],
|
||||||
|
queryFn: getSinAsignar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAsignarObligacion() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ contribuyenteId, obligacionId, auxiliarUserId }: {
|
||||||
|
contribuyenteId: string;
|
||||||
|
obligacionId: string;
|
||||||
|
auxiliarUserId: string;
|
||||||
|
}) => asignarObligacion(contribuyenteId, obligacionId, auxiliarUserId),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-supervisor'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-sin-asignar'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['obligaciones'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesasignarObligacion() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ contribuyenteId, obligacionId }: {
|
||||||
|
contribuyenteId: string;
|
||||||
|
obligacionId: string;
|
||||||
|
}) => desasignarObligacion(contribuyenteId, obligacionId),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-supervisor'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-sin-asignar'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['obligaciones'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAsignarTarea() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ tareaId, auxiliarUserId }: {
|
||||||
|
tareaId: string;
|
||||||
|
auxiliarUserId: string;
|
||||||
|
}) => asignarTarea(tareaId, auxiliarUserId),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-supervisor'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-sin-asignar'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['tareas'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesasignarTarea() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ tareaId }: { tareaId: string }) => desasignarTarea(tareaId),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-supervisor'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['asignaciones-sin-asignar'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['tareas'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user