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. +

+ +
+
+ ) : ( +
+ {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' && ( )}
- {/* List */} - {isLoading ? ( -

Cargando...

- ) : !carteras || carteras.length === 0 ? ( - - - -

Sin carteras

-

- Crea la primera cartera para organizar tus contribuyentes. -

- -
-
+ {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}) +

+ +
+ + + + +
+
+
+ ); +} + +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 ( + + +
+ + + + + + + + + + + + {items.map((item) => ( + + + + + + + + ))} + +
AuxiliarContribuyente{tipo === 'obligacion' ? 'Obligación' : 'Tarea'}AsignadoAcciones
+ {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') : '-'} + +
+ + {item.auxiliarUserId && ( + + )} +
+
+
+
+
+ ); +} + +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 ( + + +
+ + + + + + + + + + {items.map((item, idx) => ( + + + + + + ))} + +
Contribuyente{tipo === 'obligacion' ? 'Obligación' : 'Tarea'}Acción
+
{item.contribuyenteRazonSocial}
+
{item.contribuyenteRfc}
+
{tipo === 'obligacion' ? item.obligacionNombre : item.tareaNombre} + +
+
+
+
+ ); +} 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'] }); + }, + }); +}