feat: add global user administration for admin users
Backend: - Add getAllUsuarios() to get users from all tenants - Add updateUsuarioGlobal() to edit users and change their tenant - Add deleteUsuarioGlobal() for global user deletion - Add global admin check based on tenant RFC - Add new API routes: /usuarios/global/* Frontend: - Add UserListItem.tenantId and tenantName fields - Add /admin/usuarios page with full user management - Support filtering by tenant and search - Inline editing for name, role, and tenant assignment - Group users by company for better organization - Add "Admin Usuarios" menu item for admin navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,21 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import * as usuariosService from '../services/usuarios.service.js';
|
import * as usuariosService from '../services/usuarios.service.js';
|
||||||
import { AppError } from '../utils/errors.js';
|
import { AppError } from '../utils/errors.js';
|
||||||
|
import { prisma } from '../config/database.js';
|
||||||
|
|
||||||
|
// RFC del tenant administrador global
|
||||||
|
const ADMIN_TENANT_RFC = 'AASI940812GM6';
|
||||||
|
|
||||||
|
async function isGlobalAdmin(req: Request): Promise<boolean> {
|
||||||
|
if (req.user!.role !== 'admin') return false;
|
||||||
|
|
||||||
|
const tenant = await prisma.tenant.findUnique({
|
||||||
|
where: { id: req.user!.tenantId },
|
||||||
|
select: { rfc: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return tenant?.rfc === ADMIN_TENANT_RFC;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUsuarios(req: Request, res: Response, next: NextFunction) {
|
export async function getUsuarios(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
@@ -11,6 +26,21 @@ export async function getUsuarios(req: Request, res: Response, next: NextFunctio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene todos los usuarios de todas las empresas (solo admin global)
|
||||||
|
*/
|
||||||
|
export async function getAllUsuarios(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
if (!(await isGlobalAdmin(req))) {
|
||||||
|
throw new AppError(403, 'Solo el administrador global puede ver todos los usuarios');
|
||||||
|
}
|
||||||
|
const usuarios = await usuariosService.getAllUsuarios();
|
||||||
|
res.json(usuarios);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function inviteUsuario(req: Request, res: Response, next: NextFunction) {
|
export async function inviteUsuario(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
if (req.user!.role !== 'admin') {
|
if (req.user!.role !== 'admin') {
|
||||||
@@ -28,7 +58,8 @@ export async function updateUsuario(req: Request, res: Response, next: NextFunct
|
|||||||
if (req.user!.role !== 'admin') {
|
if (req.user!.role !== 'admin') {
|
||||||
throw new AppError(403, 'Solo administradores pueden modificar usuarios');
|
throw new AppError(403, 'Solo administradores pueden modificar usuarios');
|
||||||
}
|
}
|
||||||
const usuario = await usuariosService.updateUsuario(req.user!.tenantId, req.params.id, req.body);
|
const userId = req.params.id as string;
|
||||||
|
const usuario = await usuariosService.updateUsuario(req.user!.tenantId, userId, req.body);
|
||||||
res.json(usuario);
|
res.json(usuario);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -40,10 +71,49 @@ export async function deleteUsuario(req: Request, res: Response, next: NextFunct
|
|||||||
if (req.user!.role !== 'admin') {
|
if (req.user!.role !== 'admin') {
|
||||||
throw new AppError(403, 'Solo administradores pueden eliminar usuarios');
|
throw new AppError(403, 'Solo administradores pueden eliminar usuarios');
|
||||||
}
|
}
|
||||||
if (req.params.id === req.user!.id) {
|
const userId = req.params.id as string;
|
||||||
|
if (userId === req.user!.userId) {
|
||||||
throw new AppError(400, 'No puedes eliminar tu propia cuenta');
|
throw new AppError(400, 'No puedes eliminar tu propia cuenta');
|
||||||
}
|
}
|
||||||
await usuariosService.deleteUsuario(req.user!.tenantId, req.params.id);
|
await usuariosService.deleteUsuario(req.user!.tenantId, userId);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualiza un usuario globalmente (puede cambiar de empresa)
|
||||||
|
*/
|
||||||
|
export async function updateUsuarioGlobal(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
if (!(await isGlobalAdmin(req))) {
|
||||||
|
throw new AppError(403, 'Solo el administrador global puede modificar usuarios globalmente');
|
||||||
|
}
|
||||||
|
const userId = req.params.id as string;
|
||||||
|
if (userId === req.user!.userId && req.body.tenantId) {
|
||||||
|
throw new AppError(400, 'No puedes cambiar tu propia empresa');
|
||||||
|
}
|
||||||
|
const usuario = await usuariosService.updateUsuarioGlobal(userId, req.body);
|
||||||
|
res.json(usuario);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina un usuario globalmente
|
||||||
|
*/
|
||||||
|
export async function deleteUsuarioGlobal(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
if (!(await isGlobalAdmin(req))) {
|
||||||
|
throw new AppError(403, 'Solo el administrador global puede eliminar usuarios globalmente');
|
||||||
|
}
|
||||||
|
const userId = req.params.id as string;
|
||||||
|
if (userId === req.user!.userId) {
|
||||||
|
throw new AppError(400, 'No puedes eliminar tu propia cuenta');
|
||||||
|
}
|
||||||
|
await usuariosService.deleteUsuarioGlobal(userId);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -6,9 +6,15 @@ const router = Router();
|
|||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
|
|
||||||
|
// Rutas por tenant
|
||||||
router.get('/', usuariosController.getUsuarios);
|
router.get('/', usuariosController.getUsuarios);
|
||||||
router.post('/invite', usuariosController.inviteUsuario);
|
router.post('/invite', usuariosController.inviteUsuario);
|
||||||
router.patch('/:id', usuariosController.updateUsuario);
|
router.patch('/:id', usuariosController.updateUsuario);
|
||||||
router.delete('/:id', usuariosController.deleteUsuario);
|
router.delete('/:id', usuariosController.deleteUsuario);
|
||||||
|
|
||||||
|
// Rutas globales (solo admin global)
|
||||||
|
router.get('/global/all', usuariosController.getAllUsuarios);
|
||||||
|
router.patch('/global/:id', usuariosController.updateUsuarioGlobal);
|
||||||
|
router.delete('/global/:id', usuariosController.deleteUsuarioGlobal);
|
||||||
|
|
||||||
export { router as usuariosRoutes };
|
export { router as usuariosRoutes };
|
||||||
|
|||||||
@@ -105,3 +105,93 @@ export async function deleteUsuario(tenantId: string, userId: string): Promise<v
|
|||||||
where: { id: userId, tenantId },
|
where: { id: userId, tenantId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene todos los usuarios de todas las empresas (solo admin global)
|
||||||
|
*/
|
||||||
|
export async function getAllUsuarios(): Promise<UserListItem[]> {
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
nombre: true,
|
||||||
|
role: true,
|
||||||
|
active: true,
|
||||||
|
lastLogin: true,
|
||||||
|
createdAt: true,
|
||||||
|
tenantId: true,
|
||||||
|
tenant: {
|
||||||
|
select: {
|
||||||
|
nombre: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ tenant: { nombre: 'asc' } }, { createdAt: 'desc' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return users.map(u => ({
|
||||||
|
id: u.id,
|
||||||
|
email: u.email,
|
||||||
|
nombre: u.nombre,
|
||||||
|
role: u.role,
|
||||||
|
active: u.active,
|
||||||
|
lastLogin: u.lastLogin?.toISOString() || null,
|
||||||
|
createdAt: u.createdAt.toISOString(),
|
||||||
|
tenantId: u.tenantId,
|
||||||
|
tenantName: u.tenant.nombre,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualiza un usuario globalmente (puede cambiar de tenant)
|
||||||
|
*/
|
||||||
|
export async function updateUsuarioGlobal(
|
||||||
|
userId: string,
|
||||||
|
data: UserUpdate & { tenantId?: string }
|
||||||
|
): Promise<UserListItem> {
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
...(data.nombre && { nombre: data.nombre }),
|
||||||
|
...(data.role && { role: data.role }),
|
||||||
|
...(data.active !== undefined && { active: data.active }),
|
||||||
|
...(data.tenantId && { tenantId: data.tenantId }),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
nombre: true,
|
||||||
|
role: true,
|
||||||
|
active: true,
|
||||||
|
lastLogin: true,
|
||||||
|
createdAt: true,
|
||||||
|
tenantId: true,
|
||||||
|
tenant: {
|
||||||
|
select: {
|
||||||
|
nombre: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
nombre: user.nombre,
|
||||||
|
role: user.role,
|
||||||
|
active: user.active,
|
||||||
|
lastLogin: user.lastLogin?.toISOString() || null,
|
||||||
|
createdAt: user.createdAt.toISOString(),
|
||||||
|
tenantId: user.tenantId,
|
||||||
|
tenantName: user.tenant.nombre,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina un usuario globalmente
|
||||||
|
*/
|
||||||
|
export async function deleteUsuarioGlobal(userId: string): Promise<void> {
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
305
apps/web/app/(dashboard)/admin/usuarios/page.tsx
Normal file
305
apps/web/app/(dashboard)/admin/usuarios/page.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { DashboardShell } from '@/components/layouts/dashboard-shell';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { useAllUsuarios, useUpdateUsuarioGlobal, useDeleteUsuarioGlobal } from '@/lib/hooks/use-usuarios';
|
||||||
|
import { getTenants, type Tenant } from '@/lib/api/tenants';
|
||||||
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
|
import { Users, Pencil, Trash2, Shield, Eye, Calculator, Building2, X, Check } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const roleLabels = {
|
||||||
|
admin: { label: 'Administrador', icon: Shield, color: 'text-primary' },
|
||||||
|
contador: { label: 'Contador', icon: Calculator, color: 'text-green-600' },
|
||||||
|
visor: { label: 'Visor', icon: Eye, color: 'text-muted-foreground' },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EditingUser {
|
||||||
|
id: string;
|
||||||
|
nombre: string;
|
||||||
|
role: 'admin' | 'contador' | 'visor';
|
||||||
|
tenantId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminUsuariosPage() {
|
||||||
|
const { user: currentUser } = useAuthStore();
|
||||||
|
const { data: usuarios, isLoading, error } = useAllUsuarios();
|
||||||
|
const updateUsuario = useUpdateUsuarioGlobal();
|
||||||
|
const deleteUsuario = useDeleteUsuarioGlobal();
|
||||||
|
|
||||||
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
|
const [editingUser, setEditingUser] = useState<EditingUser | null>(null);
|
||||||
|
const [filterTenant, setFilterTenant] = useState<string>('all');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getTenants().then(setTenants).catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEdit = (usuario: any) => {
|
||||||
|
setEditingUser({
|
||||||
|
id: usuario.id,
|
||||||
|
nombre: usuario.nombre,
|
||||||
|
role: usuario.role,
|
||||||
|
tenantId: usuario.tenantId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!editingUser) return;
|
||||||
|
try {
|
||||||
|
await updateUsuario.mutateAsync({
|
||||||
|
id: editingUser.id,
|
||||||
|
data: {
|
||||||
|
nombre: editingUser.nombre,
|
||||||
|
role: editingUser.role,
|
||||||
|
tenantId: editingUser.tenantId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setEditingUser(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.response?.data?.error || 'Error al actualizar usuario');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (!confirm('Estas seguro de eliminar este usuario?')) return;
|
||||||
|
try {
|
||||||
|
await deleteUsuario.mutateAsync(id);
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.response?.data?.error || 'Error al eliminar usuario');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredUsuarios = usuarios?.filter(u => {
|
||||||
|
const matchesTenant = filterTenant === 'all' || u.tenantId === filterTenant;
|
||||||
|
const matchesSearch = !searchTerm ||
|
||||||
|
u.nombre.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
u.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
return matchesTenant && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agrupar por empresa
|
||||||
|
const groupedByTenant = filteredUsuarios?.reduce((acc, u) => {
|
||||||
|
const key = u.tenantId || 'sin-empresa';
|
||||||
|
if (!acc[key]) {
|
||||||
|
acc[key] = {
|
||||||
|
tenantName: u.tenantName || 'Sin empresa',
|
||||||
|
users: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
acc[key].users.push(u);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, { tenantName: string; users: typeof filteredUsuarios }>);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<DashboardShell title="Administracion de Usuarios">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-8 text-center">
|
||||||
|
<p className="text-destructive">
|
||||||
|
No tienes permisos para ver esta pagina o ocurrio un error.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</DashboardShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardShell title="Administracion de Usuarios">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Filtros */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-4">
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar por nombre o email..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-[250px]">
|
||||||
|
<Select value={filterTenant} onValueChange={setFilterTenant}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Filtrar por empresa" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todas las empresas</SelectItem>
|
||||||
|
{tenants.map(t => (
|
||||||
|
<SelectItem key={t.id} value={t.id}>{t.nombre}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="h-5 w-5" />
|
||||||
|
<span className="font-medium">{filteredUsuarios?.length || 0} usuarios</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{Object.keys(groupedByTenant || {}).length} empresas</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Users by tenant */}
|
||||||
|
{isLoading ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-8 text-center text-muted-foreground">
|
||||||
|
Cargando usuarios...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
Object.entries(groupedByTenant || {}).map(([tenantId, { tenantName, users }]) => (
|
||||||
|
<Card key={tenantId}>
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
{tenantName}
|
||||||
|
<span className="text-muted-foreground font-normal text-sm">
|
||||||
|
({users?.length} usuarios)
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="divide-y">
|
||||||
|
{users?.map(usuario => {
|
||||||
|
const roleInfo = roleLabels[usuario.role];
|
||||||
|
const RoleIcon = roleInfo.icon;
|
||||||
|
const isCurrentUser = usuario.id === currentUser?.id;
|
||||||
|
const isEditing = editingUser?.id === usuario.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={usuario.id} className="p-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4 flex-1">
|
||||||
|
<div className={cn(
|
||||||
|
'w-10 h-10 rounded-full flex items-center justify-center',
|
||||||
|
'bg-primary/10 text-primary font-medium'
|
||||||
|
)}>
|
||||||
|
{usuario.nombre.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
value={editingUser.nombre}
|
||||||
|
onChange={(e) => setEditingUser({ ...editingUser, nombre: e.target.value })}
|
||||||
|
className="h-8"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
value={editingUser.role}
|
||||||
|
onValueChange={(v) => setEditingUser({ ...editingUser, role: v as any })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-[140px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">Administrador</SelectItem>
|
||||||
|
<SelectItem value="contador">Contador</SelectItem>
|
||||||
|
<SelectItem value="visor">Visor</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={editingUser.tenantId}
|
||||||
|
onValueChange={(v) => setEditingUser({ ...editingUser, tenantId: v })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 flex-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{tenants.map(t => (
|
||||||
|
<SelectItem key={t.id} value={t.id}>{t.nombre}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">{usuario.nombre}</span>
|
||||||
|
{isCurrentUser && (
|
||||||
|
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded">Tu</span>
|
||||||
|
)}
|
||||||
|
{!usuario.active && (
|
||||||
|
<span className="text-xs bg-destructive/10 text-destructive px-2 py-0.5 rounded">Inactivo</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{usuario.email}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{!isEditing && (
|
||||||
|
<div className={cn('flex items-center gap-1', roleInfo.color)}>
|
||||||
|
<RoleIcon className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{roleInfo.label}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCurrentUser && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={updateUsuario.isPending}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setEditingUser(null)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleEdit(usuario)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDelete(usuario.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DashboardShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Bell,
|
Bell,
|
||||||
Users,
|
Users,
|
||||||
Building2,
|
Building2,
|
||||||
|
UserCog,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { logout } from '@/lib/api/auth';
|
import { logout } from '@/lib/api/auth';
|
||||||
@@ -33,6 +34,7 @@ const navigation = [
|
|||||||
|
|
||||||
const adminNavigation = [
|
const adminNavigation = [
|
||||||
{ name: 'Clientes', href: '/clientes', icon: Building2 },
|
{ name: 'Clientes', href: '/clientes', icon: Building2 },
|
||||||
|
{ name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
|
|||||||
@@ -19,3 +19,18 @@ export async function updateUsuario(id: string, data: UserUpdate): Promise<UserL
|
|||||||
export async function deleteUsuario(id: string): Promise<void> {
|
export async function deleteUsuario(id: string): Promise<void> {
|
||||||
await apiClient.delete(`/usuarios/${id}`);
|
await apiClient.delete(`/usuarios/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Funciones globales (admin global)
|
||||||
|
export async function getAllUsuarios(): Promise<UserListItem[]> {
|
||||||
|
const response = await apiClient.get<UserListItem[]>('/usuarios/global/all');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUsuarioGlobal(id: string, data: UserUpdate): Promise<UserListItem> {
|
||||||
|
const response = await apiClient.patch<UserListItem>(`/usuarios/global/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUsuarioGlobal(id: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/usuarios/global/${id}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,3 +38,31 @@ export function useDeleteUsuario() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hooks globales (admin global)
|
||||||
|
export function useAllUsuarios() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['usuarios', 'global'],
|
||||||
|
queryFn: usuariosApi.getAllUsuarios,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateUsuarioGlobal() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: string; data: UserUpdate }) => usuariosApi.updateUsuarioGlobal(id, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['usuarios'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteUsuarioGlobal() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => usuariosApi.deleteUsuarioGlobal(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['usuarios'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,12 +38,15 @@ export interface UserListItem {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
lastLogin: string | null;
|
lastLogin: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
tenantId?: string;
|
||||||
|
tenantName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserUpdate {
|
export interface UserUpdate {
|
||||||
nombre?: string;
|
nombre?: string;
|
||||||
role?: 'admin' | 'contador' | 'visor';
|
role?: 'admin' | 'contador' | 'visor';
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
tenantId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuditLog {
|
export interface AuditLog {
|
||||||
|
|||||||
Reference in New Issue
Block a user