feat: recordatorios periódicos; supervisor invita auxiliares; owner edita usuarios; precios planes y MSI
This commit is contained in:
@@ -9,7 +9,7 @@ import { addClienteAcceso } from '@/lib/api/contribuyentes';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { Users, UserPlus, Trash2, Shield, Eye, Calculator, UserCheck, UserCog, Building2, FolderOpen, KeyRound } from 'lucide-react';
|
||||
import { Users, UserPlus, Trash2, Shield, Eye, Calculator, UserCheck, UserCog, Building2, FolderOpen, KeyRound, Pencil } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@horux/shared-ui';
|
||||
import Link from 'next/link';
|
||||
import { cn } from '@horux/shared-ui';
|
||||
@@ -79,7 +79,7 @@ export default function UsuariosPage() {
|
||||
const isDespacho = isDespachoTenant(currentUser?.tenantRfc);
|
||||
const inviteRoles = isDespacho
|
||||
? (currentUser?.role === 'supervisor'
|
||||
? despachoInviteRoles.filter(r => r.value === 'cliente')
|
||||
? despachoInviteRoles.filter(r => r.value === 'cliente' || r.value === 'auxiliar')
|
||||
: despachoInviteRoles)
|
||||
: legacyInviteRoles;
|
||||
const defaultInviteRole = isDespacho ? 'auxiliar' : 'visor';
|
||||
@@ -106,6 +106,13 @@ export default function UsuariosPage() {
|
||||
|
||||
const [currentSupervisorNombre, setCurrentSupervisorNombre] = useState<string>('');
|
||||
|
||||
// Edit user modal (owner only)
|
||||
const [editingUser, setEditingUser] = useState<{ id: string; nombre: string; role: Role; email: string } | null>(null);
|
||||
const [editForm, setEditForm] = useState<{ nombre: string; role: Role }>({ nombre: '', role: 'auxiliar' });
|
||||
const [savingUser, setSavingUser] = useState(false);
|
||||
|
||||
const isOwner = currentUser?.role === 'owner';
|
||||
|
||||
const openEditSupervisor = async (userId: string, nombre: string) => {
|
||||
try {
|
||||
const res = await apiClient.get<{ supervisorUserId: string | null; supervisorNombre: string | null }>(`/usuarios/${userId}/supervisor`);
|
||||
@@ -132,6 +139,24 @@ export default function UsuariosPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openEditUser = (usuario: { id: string; nombre: string; role: Role; email: string }) => {
|
||||
setEditingUser(usuario);
|
||||
setEditForm({ nombre: usuario.nombre, role: usuario.role });
|
||||
};
|
||||
|
||||
const handleSaveUser = async () => {
|
||||
if (!editingUser) return;
|
||||
setSavingUser(true);
|
||||
try {
|
||||
await updateUsuario.mutateAsync({ id: editingUser.id, data: editForm });
|
||||
setEditingUser(null);
|
||||
} catch (error: any) {
|
||||
alert(error.response?.data?.message || 'Error al guardar usuario');
|
||||
} finally {
|
||||
setSavingUser(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openEditAccesos = async (userId: string, nombre: string) => {
|
||||
try {
|
||||
const res = await apiClient.get<{ data: string[] }>(`/usuarios/${userId}/accesos`);
|
||||
@@ -270,7 +295,16 @@ export default function UsuariosPage() {
|
||||
<Label htmlFor="role">Rol</Label>
|
||||
<Select
|
||||
value={inviteForm.role}
|
||||
onValueChange={(v) => { setInviteForm({ ...inviteForm, role: v as UserInvite['role'], supervisorUserId: undefined }); if (v !== 'cliente') setSelectedRfcIds([]); }}
|
||||
onValueChange={(v) => {
|
||||
const isAuxiliar = v === 'auxiliar';
|
||||
const isSupervisor = currentUser?.role === 'supervisor';
|
||||
setInviteForm({
|
||||
...inviteForm,
|
||||
role: v as UserInvite['role'],
|
||||
supervisorUserId: isAuxiliar && isSupervisor ? currentUser?.id : undefined,
|
||||
});
|
||||
if (v !== 'cliente') setSelectedRfcIds([]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -393,6 +427,18 @@ export default function UsuariosPage() {
|
||||
<RoleIcon className="h-4 w-4" />
|
||||
<span className="text-sm">{roleInfo.label}</span>
|
||||
</div>
|
||||
{isOwner && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => openEditUser(usuario)}
|
||||
title="Editar nombre y rol"
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-1" /> Editar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isAdmin && !isCurrentUser && (
|
||||
<div className="flex gap-1">
|
||||
{isDespacho && usuario.role === 'cliente' && (
|
||||
@@ -526,6 +572,67 @@ export default function UsuariosPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* Edit User Modal */}
|
||||
{editingUser && (
|
||||
<Dialog open onOpenChange={(open) => { if (!open) setEditingUser(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar usuario</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-email">Email</Label>
|
||||
<Input id="edit-email" value={editingUser.email} disabled />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-nombre">Nombre</Label>
|
||||
<Input
|
||||
id="edit-nombre"
|
||||
value={editForm.nombre}
|
||||
onChange={e => setEditForm({ ...editForm, nombre: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-role">Rol</Label>
|
||||
{editingUser.id === currentUser?.id ? (
|
||||
<div className="text-sm border rounded-md px-3 py-2 bg-muted text-muted-foreground">
|
||||
{getRoleInfo(editForm.role, isDespacho).label}
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={editForm.role}
|
||||
onValueChange={(v) => setEditForm({ ...editForm, role: v as Role })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(isDespacho
|
||||
? (['owner', 'supervisor', 'auxiliar', 'cliente'] as Role[])
|
||||
: (['owner', 'cfo', 'contador', 'visor', 'auxiliar'] as Role[])
|
||||
).map((r) => (
|
||||
<SelectItem key={r} value={r}>
|
||||
{getRoleInfo(r, isDespacho).label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{editingUser.id === currentUser?.id && (
|
||||
<p className="text-xs text-muted-foreground">No puedes cambiar tu propio rol.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditingUser(null)}>Cancelar</Button>
|
||||
<Button onClick={handleSaveUser} disabled={savingUser}>
|
||||
{savingUser ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</DashboardShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user