diff --git a/apps/web/app/(dashboard)/carteras/page.tsx b/apps/web/app/(dashboard)/carteras/page.tsx
index 2f24dda..a6f68db 100644
--- a/apps/web/app/(dashboard)/carteras/page.tsx
+++ b/apps/web/app/(dashboard)/carteras/page.tsx
@@ -5,12 +5,13 @@ import {
Button, Card, CardContent, CardHeader, CardTitle, Input, Label,
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
+ Tabs, TabsList, TabsTrigger, TabsContent,
cn,
} from '@horux/shared-ui';
import { useQueryClient } from '@tanstack/react-query';
import {
FolderOpen, Plus, Trash2, ChevronDown, ChevronUp, X,
- Users, Building2, FolderPlus, UserCog,
+ Users, Building2, FolderPlus, UserCog, ClipboardList,
} from 'lucide-react';
import {
useCarteras, useCreateCartera, useDeleteCartera,
@@ -25,14 +26,16 @@ import { useUsuarios } from '@/lib/hooks/use-usuarios';
import { useAuthStore } from '@/stores/auth-store';
import { DashboardShell } from '@/components/layouts/dashboard-shell';
import type { Cartera } from '@/lib/api/carteras';
+import SeguimientoAuxiliares from './seguimiento-auxiliares';
/* ------------------------------------------------------------------ */
/* SubcarteraCard */
/* ------------------------------------------------------------------ */
-function SubcarteraCard({ sub, usuarios, contribuyentes, onDelete }: {
+function SubcarteraCard({ sub, usuarios, contribuyentes, parentEntidadIds, onDelete }: {
sub: Cartera;
usuarios: any[];
contribuyentes: any[];
+ parentEntidadIds: string[];
onDelete: () => void;
}) {
const [expanded, setExpanded] = useState(false);
@@ -47,7 +50,7 @@ function SubcarteraCard({ sub, usuarios, contribuyentes, onDelete }: {
);
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);
@@ -319,6 +322,7 @@ function CarteraDetail({ cartera, canEdit = true, canManageSubcarteras = true }:
sub={sub}
usuarios={usuarios ?? []}
contribuyentes={contribuyentes ?? []}
+ parentEntidadIds={entidadIds ?? []}
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 canManageSubcarteras = userRole === 'owner' || userRole === 'supervisor'; // Create subcarteras
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: supervisores } = useSupervisores();
const { data: usuarios } = useUsuarios();
@@ -440,9 +448,43 @@ export default function CarterasPage() {
}
};
+ const CarterasList = () => (
+ <>
+ {isLoading ? (
+
Cargando...
+ ) : !carteras || carteras.length === 0 ? (
+
+
+
+ Sin carteras
+
+ Crea la primera cartera para organizar tus contribuyentes.
+
+ setShowCreate(true)}>Crear primera cartera
+
+
+ ) : (
+
+ {carteras.map(cartera => (
+ setExpandedId(expandedId === cartera.id ? null : cartera.id)}
+ onDelete={() => handleDelete(cartera)}
+ usuarios={usuarios ?? []}
+ canEdit={canEditCartera}
+ canManageSubcarteras={canManageSubcarteras}
+ />
+ ))}
+
+ )}
+ >
+ );
+
return (
-
+
{/* Header */}
@@ -450,42 +492,34 @@ export default function CarterasPage() {
{isAuxiliar ? 'Carteras asignadas a ti' : 'Organiza contribuyentes en carteras y asigna subcarteras a cada auxiliar'}
- {canCreate && (
+ {canCreate && activeTab === 'carteras' && (
setShowCreate(true)} className="flex items-center gap-2">
Nueva cartera
)}
- {/* List */}
- {isLoading ? (
-
Cargando...
- ) : !carteras || carteras.length === 0 ? (
-
-
-
- Sin carteras
-
- Crea la primera cartera para organizar tus contribuyentes.
-
- setShowCreate(true)}>Crear primera cartera
-
-
+ {puedeVerSeguimiento ? (
+
+
+
+ Carteras
+
+
+ Seguimiento de Auxiliares
+
+
+
+
+
+
+
+
+
+
+
) : (
-
- {carteras.map(cartera => (
- setExpandedId(expandedId === cartera.id ? null : cartera.id)}
- onDelete={() => handleDelete(cartera)}
- usuarios={usuarios ?? []}
- canEdit={canEditCartera}
- canManageSubcarteras={canManageSubcarteras}
- />
- ))}
-
+
)}
{/* Create dialog */}
diff --git a/apps/web/app/(dashboard)/carteras/seguimiento-auxiliares.tsx b/apps/web/app/(dashboard)/carteras/seguimiento-auxiliares.tsx
new file mode 100644
index 0000000..b484f1a
--- /dev/null
+++ b/apps/web/app/(dashboard)/carteras/seguimiento-auxiliares.tsx
@@ -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
(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 Cargando asignaciones...
;
+ }
+
+ const obligacionesAsignadas = asignaciones?.obligaciones ?? [];
+ const tareasAsignadas = asignaciones?.tareas ?? [];
+ const obligacionesSinAsignar = sinAsignar?.obligaciones ?? [];
+ const tareasSinAsignar = sinAsignar?.tareas ?? [];
+
+ return (
+
+
+
+ Asignadas
+ Sin asignar
+
+
+
+
+
+ Obligaciones ({obligacionesAsignadas.length})
+ Tareas ({tareasAsignadas.length})
+
+
+
+ openAssignModal('obligacion', item)}
+ onUnassign={(item) => handleUnassign('obligacion', item)}
+ />
+
+
+
+ openAssignModal('tarea', item)}
+ onUnassign={(item) => handleUnassign('tarea', item)}
+ />
+
+
+
+
+
+
+
+ Obligaciones ({obligacionesSinAsignar.length})
+ Tareas ({tareasSinAsignar.length})
+
+
+
+ openAssignModal('obligacion', item)}
+ />
+
+
+
+ openAssignModal('tarea', item)}
+ />
+
+
+
+
+
+ {/* Modal de asignación */}
+
+
+
+ {modalItem?.auxiliarUserId ? 'Reasignar' : 'Asignar'} {modalType === 'obligacion' ? 'obligación' : 'tarea'}
+
+
+
+ {modalType === 'obligacion' ? modalItem?.obligacionNombre : modalItem?.tareaNombre}
+
+
+ Contribuyente: {modalItem?.contribuyenteRazonSocial} ({modalItem?.contribuyenteRfc})
+
+
+
+
+
+
+ {auxiliares.map((a: any) => (
+ {a.nombre} ({a.email})
+ ))}
+
+
+
+
+ setModalOpen(false)}>Cancelar
+
+ {modalItem?.auxiliarUserId ? 'Reasignar' : 'Asignar'}
+
+
+
+
+
+ );
+}
+
+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 (
+
+
+ No hay {tipo === 'obligacion' ? 'obligaciones' : 'tareas'} asignadas.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ Auxiliar
+ Contribuyente
+ {tipo === 'obligacion' ? 'Obligación' : 'Tarea'}
+ Asignado
+ Acciones
+
+
+
+ {items.map((item) => (
+
+
+ {item.auxiliarNombre ? (
+
+ {item.auxiliarNombre}
+
+ ) : (
+
+ Sin asignar
+
+ )}
+
+
+ {item.contribuyenteRazonSocial}
+ {item.contribuyenteRfc}
+
+ {tipo === 'obligacion' ? item.obligacionNombre : item.tareaNombre}
+
+ {item.asignadoAt ? new Date(item.asignadoAt).toLocaleDateString('es-MX') : '-'}
+
+
+
+ onAssign(item)}>
+
+
+ {item.auxiliarUserId && (
+ onUnassign(item)}>
+
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
+
+function SinAsignarTable({
+ items,
+ tipo,
+ auxiliares,
+ onAssign,
+}: {
+ items: any[];
+ tipo: 'obligacion' | 'tarea';
+ auxiliares: any[];
+ onAssign: (item: any) => void;
+}) {
+ if (items.length === 0) {
+ return (
+
+
+ No hay {tipo === 'obligacion' ? 'obligaciones' : 'tareas'} sin asignar.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ Contribuyente
+ {tipo === 'obligacion' ? 'Obligación' : 'Tarea'}
+ Acción
+
+
+
+ {items.map((item, idx) => (
+
+
+ {item.contribuyenteRazonSocial}
+ {item.contribuyenteRfc}
+
+ {tipo === 'obligacion' ? item.obligacionNombre : item.tareaNombre}
+
+ onAssign(item)}>
+ Asignar
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/lib/api/asignaciones.ts b/apps/web/lib/api/asignaciones.ts
new file mode 100644
index 0000000..716cc8f
--- /dev/null
+++ b/apps/web/lib/api/asignaciones.ts
@@ -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('/carteras/asignaciones').then(r => r.data);
+
+export const getAsignacionesPorAuxiliar = () =>
+ apiClient.get('/carteras/asignaciones/mias').then(r => r.data);
+
+export interface SinAsignarResponse {
+ obligaciones: Omit[];
+ tareas: Omit[];
+}
+
+export const getSinAsignar = () =>
+ apiClient.get('/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);
diff --git a/apps/web/lib/hooks/use-asignaciones.ts b/apps/web/lib/hooks/use-asignaciones.ts
new file mode 100644
index 0000000..85646a4
--- /dev/null
+++ b/apps/web/lib/hooks/use-asignaciones.ts
@@ -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'] });
+ },
+ });
+}