Initial commit: Horux Despachos project
This commit is contained in:
539
apps/web/app/(dashboard)/carteras/page.tsx
Normal file
539
apps/web/app/(dashboard)/carteras/page.tsx
Normal file
@@ -0,0 +1,539 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button, Card, CardContent, CardHeader, CardTitle, Input, Label,
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
cn,
|
||||
} from '@horux/shared-ui';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
FolderOpen, Plus, Trash2, ChevronDown, ChevronUp, X,
|
||||
Users, Building2, FolderPlus, UserCog,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
useCarteras, useCreateCartera, useDeleteCartera,
|
||||
useCarteraEntidades, useSubcarteras, useCreateSubcartera,
|
||||
useSupervisores,
|
||||
} from '@/lib/hooks/use-carteras';
|
||||
import {
|
||||
addEntidadToCartera, removeEntidadFromCartera,
|
||||
} from '@/lib/api/carteras';
|
||||
import { useContribuyentes } from '@/lib/hooks/use-contribuyentes';
|
||||
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';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* SubcarteraCard */
|
||||
/* ------------------------------------------------------------------ */
|
||||
function SubcarteraCard({ sub, usuarios, contribuyentes, onDelete }: {
|
||||
sub: Cartera;
|
||||
usuarios: any[];
|
||||
contribuyentes: any[];
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const qc = useQueryClient();
|
||||
const { data: entidadIds, isLoading } = useCarteraEntidades(expanded ? sub.id : null);
|
||||
const [addingEntidad, setAddingEntidad] = useState(false);
|
||||
const [selectedEntidadId, setSelectedEntidadId] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const entidadMap = Object.fromEntries(
|
||||
(contribuyentes ?? []).map((c: any) => [c.id, { rfc: c.rfc, nombre: c.nombre }])
|
||||
);
|
||||
|
||||
const available = (contribuyentes ?? []).filter(
|
||||
(c: any) => !(entidadIds ?? []).includes(c.id)
|
||||
);
|
||||
|
||||
const auxiliarUser = usuarios?.find((u: any) => u.id === sub.auxiliarUserId);
|
||||
|
||||
const invalidate = () => {
|
||||
qc.invalidateQueries({ queryKey: ['cartera-entidades', sub.id] });
|
||||
qc.invalidateQueries({ queryKey: ['subcarteras'] });
|
||||
qc.invalidateQueries({ queryKey: ['carteras'] });
|
||||
};
|
||||
|
||||
const handleAddEntidad = async () => {
|
||||
if (!selectedEntidadId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await addEntidadToCartera(sub.id, selectedEntidadId);
|
||||
setSelectedEntidadId('');
|
||||
setAddingEntidad(false);
|
||||
invalidate();
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const handleRemoveEntidad = async (entidadId: string) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await removeEntidadFromCartera(sub.id, entidadId);
|
||||
invalidate();
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg p-3 bg-muted/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<button onClick={() => setExpanded(!expanded)} className="flex items-center gap-2 flex-1 text-left">
|
||||
<UserCog className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<span className="font-medium text-sm">{sub.nombre}</span>
|
||||
{auxiliarUser && (
|
||||
<span className="text-xs text-muted-foreground ml-2">({auxiliarUser.nombre})</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground ml-auto mr-2">{sub.entidadesCount} RFCs</span>
|
||||
{expanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||
</button>
|
||||
<button onClick={onDelete} className="text-muted-foreground hover:text-destructive p-1">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{!addingEntidad && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setAddingEntidad(true)} className="h-7 gap-1 text-xs">
|
||||
<Plus className="h-3 w-3" /> Asignar RFC
|
||||
</Button>
|
||||
)}
|
||||
{addingEntidad && (
|
||||
<div className="flex items-center gap-2">
|
||||
<select className="flex-1 rounded-md border bg-background px-2 py-1 text-sm" value={selectedEntidadId} onChange={e => setSelectedEntidadId(e.target.value)}>
|
||||
<option value="">-- Seleccionar RFC --</option>
|
||||
{available.map((c: any) => <option key={c.id} value={c.id}>{c.rfc} — {c.nombre}</option>)}
|
||||
</select>
|
||||
<Button size="sm" onClick={handleAddEntidad} disabled={!selectedEntidadId || busy}>Agregar</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { setAddingEntidad(false); setSelectedEntidadId(''); }}>Cancelar</Button>
|
||||
</div>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<p className="text-xs text-muted-foreground">Cargando...</p>
|
||||
) : !entidadIds || entidadIds.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">Sin RFCs asignados a esta subcartera.</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{entidadIds.map(id => {
|
||||
const info = entidadMap[id];
|
||||
return (
|
||||
<li key={id} className="flex items-center justify-between bg-background rounded px-2 py-1 text-sm">
|
||||
<span>{info ? <><span className="font-mono text-xs">{info.rfc}</span> <span className="text-muted-foreground">{info.nombre}</span></> : id}</span>
|
||||
<button onClick={() => handleRemoveEntidad(id)} disabled={busy} className="text-muted-foreground hover:text-destructive"><X className="h-3 w-3" /></button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* CarteraDetail */
|
||||
/* ------------------------------------------------------------------ */
|
||||
function CarteraDetail({ cartera, canEdit = true, canManageSubcarteras = true }: { cartera: Cartera; canEdit?: boolean; canManageSubcarteras?: boolean }) {
|
||||
const qc = useQueryClient();
|
||||
const { data: contribuyentes } = useContribuyentes();
|
||||
const { data: usuarios } = useUsuarios();
|
||||
const { data: entidadIds, isLoading: loadingEntidades } = useCarteraEntidades(cartera.id);
|
||||
const { data: subcarteras, isLoading: loadingSubs } = useSubcarteras(cartera.id);
|
||||
const createSub = useCreateSubcartera();
|
||||
|
||||
const [addingEntidad, setAddingEntidad] = useState(false);
|
||||
const [selectedEntidadId, setSelectedEntidadId] = useState('');
|
||||
const [showCreateSub, setShowCreateSub] = useState(false);
|
||||
const [subForm, setSubForm] = useState({ nombre: '', auxiliarUserId: '' });
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const entidadMap = Object.fromEntries(
|
||||
(contribuyentes ?? []).map((c) => [c.id, { rfc: c.rfc, nombre: c.nombre }])
|
||||
);
|
||||
|
||||
const available = (contribuyentes ?? []).filter(
|
||||
(c) => !(entidadIds ?? []).includes(c.id)
|
||||
);
|
||||
|
||||
// Auxiliares available for subcarteras (those assigned to this supervisor)
|
||||
const auxiliares = (usuarios ?? []).filter((u: any) => u.role === 'auxiliar');
|
||||
|
||||
const supervisorUser = usuarios?.find((u: any) => u.id === cartera.supervisorUserId);
|
||||
|
||||
const invalidate = () => {
|
||||
qc.invalidateQueries({ queryKey: ['cartera-entidades', cartera.id] });
|
||||
qc.invalidateQueries({ queryKey: ['subcarteras', cartera.id] });
|
||||
qc.invalidateQueries({ queryKey: ['carteras'] });
|
||||
};
|
||||
|
||||
const handleAddEntidad = async () => {
|
||||
if (!selectedEntidadId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await addEntidadToCartera(cartera.id, selectedEntidadId);
|
||||
setSelectedEntidadId('');
|
||||
setAddingEntidad(false);
|
||||
invalidate();
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const handleRemoveEntidad = async (entidadId: string) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await removeEntidadFromCartera(cartera.id, entidadId);
|
||||
invalidate();
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const handleCreateSubcartera = async () => {
|
||||
if (!subForm.nombre.trim() || !subForm.auxiliarUserId) return;
|
||||
try {
|
||||
await createSub.mutateAsync({
|
||||
carteraId: cartera.id,
|
||||
nombre: subForm.nombre.trim(),
|
||||
auxiliarUserId: subForm.auxiliarUserId,
|
||||
});
|
||||
setSubForm({ nombre: '', auxiliarUserId: '' });
|
||||
setShowCreateSub(false);
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.message || 'Error al crear subcartera');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSubcartera = async (subId: string) => {
|
||||
if (!confirm('¿Eliminar esta subcartera?')) return;
|
||||
try {
|
||||
const { deleteCartera } = await import('@/lib/api/carteras');
|
||||
await deleteCartera(subId);
|
||||
invalidate();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.message || 'Error al eliminar');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-t mt-4 pt-4 space-y-6">
|
||||
{/* Supervisor info */}
|
||||
{supervisorUser && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<UserCog className="h-3.5 w-3.5" />
|
||||
Supervisor: <span className="font-medium text-foreground">{supervisorUser.nombre}</span> ({supervisorUser.email})
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---- Contribuyentes ---- */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-1.5">
|
||||
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||
Contribuyentes ({entidadIds?.length || 0})
|
||||
</h3>
|
||||
{canEdit && !addingEntidad && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setAddingEntidad(true)} className="h-7 gap-1 text-xs">
|
||||
<Plus className="h-3 w-3" /> Agregar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canEdit && addingEntidad && (
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<select className="flex-1 rounded-md border bg-background px-3 py-1.5 text-sm" value={selectedEntidadId} onChange={e => setSelectedEntidadId(e.target.value)}>
|
||||
<option value="">-- Seleccionar RFC --</option>
|
||||
{available.map(c => <option key={c.id} value={c.id}>{c.rfc} — {c.nombre}</option>)}
|
||||
</select>
|
||||
<Button size="sm" onClick={handleAddEntidad} disabled={!selectedEntidadId || busy}>Agregar</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { setAddingEntidad(false); setSelectedEntidadId(''); }}>Cancelar</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingEntidades ? (
|
||||
<p className="text-xs text-muted-foreground">Cargando...</p>
|
||||
) : !entidadIds || entidadIds.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">Sin contribuyentes asignados.</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{entidadIds.map(id => {
|
||||
const info = entidadMap[id];
|
||||
return (
|
||||
<li key={id} className="flex items-center justify-between rounded-md bg-muted/40 px-3 py-1.5 text-sm">
|
||||
<span>{info ? <><span className="font-mono text-xs">{info.rfc}</span> <span className="text-muted-foreground ml-2">{info.nombre}</span></> : <span className="font-mono text-xs">{id}</span>}</span>
|
||||
{canEdit && <button onClick={() => handleRemoveEntidad(id)} disabled={busy} className="text-muted-foreground hover:text-destructive ml-2"><X className="h-3.5 w-3.5" /></button>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ---- Subcarteras ---- */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-1.5">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
Subcarteras ({subcarteras?.length || 0})
|
||||
</h3>
|
||||
{canManageSubcarteras && !showCreateSub && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowCreateSub(true)} className="h-7 gap-1 text-xs">
|
||||
<FolderPlus className="h-3 w-3" /> Nueva subcartera
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canManageSubcarteras && showCreateSub && (
|
||||
<div className="border rounded-lg p-3 mb-3 space-y-3 bg-muted/20">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">Nombre</Label>
|
||||
<Input value={subForm.nombre} onChange={e => setSubForm(p => ({ ...p, nombre: e.target.value }))} placeholder="Ej. Cartera de María" className="h-8 text-sm mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">Auxiliar</Label>
|
||||
<select className="w-full h-8 rounded-md border bg-background px-2 text-sm mt-1" value={subForm.auxiliarUserId} onChange={e => setSubForm(p => ({ ...p, auxiliarUserId: e.target.value }))}>
|
||||
<option value="">-- Seleccionar auxiliar --</option>
|
||||
{auxiliares.map((u: any) => <option key={u.id} value={u.id}>{u.nombre} ({u.email})</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleCreateSubcartera} disabled={!subForm.nombre.trim() || !subForm.auxiliarUserId || createSub.isPending}>Crear</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { setShowCreateSub(false); setSubForm({ nombre: '', auxiliarUserId: '' }); }}>Cancelar</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingSubs ? (
|
||||
<p className="text-xs text-muted-foreground">Cargando...</p>
|
||||
) : !subcarteras || subcarteras.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">Sin subcarteras. Crea una para asignar RFCs a un auxiliar.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{subcarteras.map(sub => (
|
||||
<SubcarteraCard
|
||||
key={sub.id}
|
||||
sub={sub}
|
||||
usuarios={usuarios ?? []}
|
||||
contribuyentes={contribuyentes ?? []}
|
||||
onDelete={() => handleDeleteSubcartera(sub.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* CarteraCard */
|
||||
/* ------------------------------------------------------------------ */
|
||||
function CarteraCard({ cartera, expanded, onToggle, onDelete, usuarios, canEdit, canManageSubcarteras }: {
|
||||
cartera: Cartera;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
onDelete: () => void;
|
||||
usuarios: any[];
|
||||
canEdit: boolean;
|
||||
canManageSubcarteras: boolean;
|
||||
}) {
|
||||
const supervisorUser = usuarios?.find((u: any) => u.id === cartera.supervisorUserId);
|
||||
return (
|
||||
<Card className={cn('transition-shadow', expanded && 'ring-1 ring-primary/30 shadow-md')}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<button onClick={onToggle} className="flex-1 text-left flex items-center gap-2 group">
|
||||
<FolderOpen className={cn('h-5 w-5 flex-shrink-0 transition-colors', expanded ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground')} />
|
||||
<div>
|
||||
<CardTitle className="text-base">{cartera.nombre}</CardTitle>
|
||||
{cartera.descripcion && <p className="text-xs text-muted-foreground mt-0.5">{cartera.descripcion}</p>}
|
||||
</div>
|
||||
{expanded ? <ChevronUp className="h-4 w-4 text-muted-foreground ml-auto" /> : <ChevronDown className="h-4 w-4 text-muted-foreground ml-auto" />}
|
||||
</button>
|
||||
{canEdit && (
|
||||
<Button variant="ghost" size="sm" onClick={onDelete} className="text-destructive hover:text-destructive flex-shrink-0 h-8 w-8 p-0">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-4 mt-1 pl-7">
|
||||
{supervisorUser && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<UserCog className="inline h-3 w-3 mr-1" />
|
||||
{supervisorUser.nombre}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Building2 className="inline h-3 w-3 mr-1" />
|
||||
{cartera.entidadesCount} RFCs
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Users className="inline h-3 w-3 mr-1" />
|
||||
{cartera.subcarterasCount} subcarteras
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{expanded && (
|
||||
<CardContent className="pt-0">
|
||||
<CarteraDetail cartera={cartera} canEdit={canEdit} canManageSubcarteras={canManageSubcarteras} />
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Page */
|
||||
/* ------------------------------------------------------------------ */
|
||||
export default function CarterasPage() {
|
||||
const { user } = useAuthStore();
|
||||
const userRole = user?.role || 'visor';
|
||||
const canCreate = userRole === 'owner'; // Create top-level carteras
|
||||
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 { data: carteras, isLoading } = useCarteras();
|
||||
const { data: supervisores } = useSupervisores();
|
||||
const { data: usuarios } = useUsuarios();
|
||||
const createMut = useCreateCartera();
|
||||
const deleteMut = useDeleteCartera();
|
||||
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [form, setForm] = useState({ nombre: '', descripcion: '', supervisorUserId: '' });
|
||||
|
||||
const hasSupervisores = supervisores && supervisores.length > 0;
|
||||
|
||||
const resetForm = () => {
|
||||
setForm({ nombre: '', descripcion: '', supervisorUserId: '' });
|
||||
setShowCreate(false);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!form.nombre.trim()) return;
|
||||
try {
|
||||
const supervisorUserId = form.supervisorUserId && form.supervisorUserId !== '__self__'
|
||||
? form.supervisorUserId : undefined;
|
||||
const cartera = await createMut.mutateAsync({
|
||||
nombre: form.nombre.trim(),
|
||||
descripcion: form.descripcion.trim() || undefined,
|
||||
supervisorUserId,
|
||||
});
|
||||
resetForm();
|
||||
setExpandedId(cartera.id);
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.message || 'Error al crear cartera');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (cartera: Cartera) => {
|
||||
if (!confirm(`¿Eliminar la cartera "${cartera.nombre}"? Se eliminarán también sus subcarteras.`)) return;
|
||||
try {
|
||||
await deleteMut.mutateAsync(cartera.id);
|
||||
if (expandedId === cartera.id) setExpandedId(null);
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.message || 'Error al eliminar cartera');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardShell title="Carteras">
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isAuxiliar ? 'Carteras asignadas a ti' : 'Organiza contribuyentes en carteras y asigna subcarteras a cada auxiliar'}
|
||||
</p>
|
||||
</div>
|
||||
{canCreate && (
|
||||
<Button onClick={() => setShowCreate(true)} className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" /> Nueva cartera
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Create dialog */}
|
||||
<Dialog open={showCreate} onOpenChange={open => { if (!open) resetForm(); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nueva cartera</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label>Nombre *</Label>
|
||||
<Input value={form.nombre} onChange={e => setForm(p => ({ ...p, nombre: e.target.value }))} placeholder="Ej. Clientes CDMX" autoFocus />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Descripcion (opcional)</Label>
|
||||
<Input value={form.descripcion} onChange={e => setForm(p => ({ ...p, descripcion: e.target.value }))} placeholder="Descripcion breve" />
|
||||
</div>
|
||||
{hasSupervisores ? (
|
||||
<div>
|
||||
<Label>Asignar a supervisor</Label>
|
||||
<Select value={form.supervisorUserId} onValueChange={v => setForm(p => ({ ...p, supervisorUserId: v }))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Yo mismo (Owner)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__self__">Yo mismo (Owner)</SelectItem>
|
||||
{supervisores!.map(s => (
|
||||
<SelectItem key={s.userId} value={s.userId}>{s.nombre} ({s.email})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground mt-1">Si no seleccionas, la cartera se asigna a ti.</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground border rounded-md p-3 bg-muted/30">
|
||||
No hay supervisores registrados. La cartera se asignará a ti como owner.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={resetForm}>Cancelar</Button>
|
||||
<Button onClick={handleCreate} disabled={!form.nombre.trim() || createMut.isPending}>
|
||||
{createMut.isPending ? 'Creando...' : 'Crear'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</DashboardShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user