From 76e01bfd799428a5c5188ff2447424698dae2b96 Mon Sep 17 00:00:00 2001 From: Sergio Nicolas Lavaise Date: Thu, 23 Apr 2026 01:09:29 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20agregar=20sistema=20de=20autenticaci?= =?UTF-8?q?=C3=B3n=20con=20usuarios=20y=20roles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agregar Login con autenticación por DNI y contraseña - Crear tabla de usuarios con roles (admin, médico, enfermero) - Implementar gestión de usuarios (CRUD) para administradores - Agregar control de permisos basado en roles en el menú - Persistir estado de sesión en backend - Inicializar usuario admin por defecto (DNI: 12345678, pass: admin123) - Corregir inicialización de arrays en el store para evitar errores al cargar estado --- package-lock.json | 12 +- package.json | 3 +- server/db.js | 98 +++++++++++ server/index.js | 201 +++++++++++++++++++++- server/package.json | 6 +- src/App.tsx | 12 +- src/hooks/useHospitalStore.ts | 118 ++++++++++++- src/sections/GestionUsuarios.tsx | 275 +++++++++++++++++++++++++++++++ src/sections/Layout.tsx | 69 ++++++-- src/sections/Login.tsx | 88 ++++++++++ src/types/index.ts | 18 +- 11 files changed, 869 insertions(+), 31 deletions(-) create mode 100644 src/sections/GestionUsuarios.tsx create mode 100644 src/sections/Login.tsx diff --git a/package-lock.json b/package-lock.json index 42887a0..b56b234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -78,7 +79,7 @@ "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.3.2" } }, "node_modules/@alloc/quick-lru": { @@ -5500,6 +5501,14 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -10559,7 +10568,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index fc26472..01b7b57 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -80,6 +81,6 @@ "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.3.2" } } diff --git a/server/db.js b/server/db.js index 048cc19..e9abd65 100644 --- a/server/db.js +++ b/server/db.js @@ -2,6 +2,7 @@ import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; +import bcrypt from 'bcryptjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const DB_PATH = __dirname + '/data/hospital.db'; @@ -19,6 +20,22 @@ export async function openDb() { ); `); + await db.exec(` + CREATE TABLE IF NOT EXISTS usuarios ( + id TEXT PRIMARY KEY, + apellido TEXT NOT NULL, + nombre TEXT NOT NULL, + dni TEXT UNIQUE NOT NULL, + fechaNacimiento TEXT NOT NULL, + email TEXT NOT NULL, + rol TEXT NOT NULL CHECK(rol IN ('admin', 'medico', 'enfermero')), + matriculaProfesional TEXT, + passwordHash TEXT NOT NULL, + areaId TEXT, + fechaCreacion TEXT NOT NULL + ); + `); + return db; } @@ -34,3 +51,84 @@ export async function setValue(key, value) { await db.run('INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value', key, value); await db.close(); } + +export async function getUsuarioByDni(dni) { + const db = await openDb(); + const row = await db.get('SELECT * FROM usuarios WHERE dni = ?', dni); + await db.close(); + return row || null; +} + +export async function getUsuarioById(id) { + const db = await openDb(); + const row = await db.get('SELECT * FROM usuarios WHERE id = ?', id); + await db.close(); + return row || null; +} + +export async function getAllUsuarios() { + const db = await openDb(); + const rows = await db.all('SELECT * FROM usuarios ORDER BY apellido, nombre'); + await db.close(); + return rows; +} + +export async function createUsuario(usuario) { + const db = await openDb(); + await db.run( + `INSERT INTO usuarios (id, apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, passwordHash, areaId, fechaCreacion) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + usuario.id, + usuario.apellido, + usuario.nombre, + usuario.dni, + usuario.fechaNacimiento, + usuario.email, + usuario.rol, + usuario.matriculaProfesional || null, + usuario.passwordHash, + usuario.areaId || null, + usuario.fechaCreacion + ); + await db.close(); +} + +export async function updateUsuario(id, datos) { + const db = await openDb(); + const fields = []; + const values = []; + + if (datos.apellido !== undefined) { fields.push('apellido = ?'); values.push(datos.apellido); } + if (datos.nombre !== undefined) { fields.push('nombre = ?'); values.push(datos.nombre); } + if (datos.fechaNacimiento !== undefined) { fields.push('fechaNacimiento = ?'); values.push(datos.fechaNacimiento); } + if (datos.email !== undefined) { fields.push('email = ?'); values.push(datos.email); } + if (datos.rol !== undefined) { fields.push('rol = ?'); values.push(datos.rol); } + if (datos.matriculaProfesional !== undefined) { fields.push('matriculaProfesional = ?'); values.push(datos.matriculaProfesional); } + if (datos.passwordHash !== undefined) { fields.push('passwordHash = ?'); values.push(datos.passwordHash); } + if (datos.areaId !== undefined) { fields.push('areaId = ?'); values.push(datos.areaId); } + + if (fields.length > 0) { + values.push(id); + await db.run(`UPDATE usuarios SET ${fields.join(', ')} WHERE id = ?`, values); + } + await db.close(); +} + +export async function deleteUsuario(id) { + const db = await openDb(); + await db.run('DELETE FROM usuarios WHERE id = ?', id); + await db.close(); +} + +export async function verifyPassword(dni, password) { + const db = await openDb(); + const row = await db.get('SELECT passwordHash FROM usuarios WHERE dni = ?', dni); + await db.close(); + + if (!row) return false; + return bcrypt.compareSync(password, row.passwordHash); +} + +export async function hashPassword(password) { + return bcrypt.hashSync(password, 10); +} diff --git a/server/index.js b/server/index.js index 9dd5d78..8dd0b67 100644 --- a/server/index.js +++ b/server/index.js @@ -1,6 +1,6 @@ import express from 'express'; import cors from 'cors'; -import { getValue, setValue } from './db.js'; +import { getValue, setValue, getUsuarioByDni, getUsuarioById, getAllUsuarios, createUsuario, updateUsuario, deleteUsuario, verifyPassword, hashPassword } from './db.js'; const app = express(); app.use(cors()); @@ -34,3 +34,202 @@ app.put('/api/state', async (req, res) => { app.listen(PORT, () => { console.log(`API server listening on http://localhost:${PORT}`); }); + +// Crear usuario admin inicial si no existe +async function initAdminUser() { + try { + const adminExists = await getUsuarioByDni('12345678'); + if (!adminExists) { + const passwordHash = await hashPassword('admin123'); + const adminUser = { + id: generateUUID(), + apellido: 'Admin', + nombre: 'Sistema', + dni: '12345678', + fechaNacimiento: '1970-01-01', + email: 'admin@hospital.gob.ar', + rol: 'admin', + matriculaProfesional: null, + passwordHash, + areaId: null, + fechaCreacion: new Date().toISOString().split('T')[0] + }; + await createUsuario(adminUser); + console.log('Usuario admin creado: DNI 12345678, Password admin123'); + } + } catch (err) { + console.error('Error creating admin user:', err); + } +} + +initAdminUser(); + +// === AUTENTICACIÓN === + +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +app.post('/api/auth/login', async (req, res) => { + try { + const { dni, password } = req.body; + if (!dni || !password) { + return res.status(400).json({ error: 'DNI y contraseña requeridos' }); + } + + const usuario = await getUsuarioByDni(dni); + if (!usuario) { + return res.status(401).json({ error: 'Usuario no encontrado' }); + } + + const validPassword = await verifyPassword(dni, password); + if (!validPassword) { + return res.status(401).json({ error: 'Contraseña incorrecta' }); + } + + const { passwordHash, ...userWithoutPassword } = usuario; + res.json(userWithoutPassword); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Error en autenticación' }); + } +}); + +app.post('/api/auth/change-password', async (req, res) => { + try { + const { dni, oldPassword, newPassword } = req.body; + if (!dni || !oldPassword || !newPassword) { + return res.status(400).json({ error: 'Todos los campos son requeridos' }); + } + + const validPassword = await verifyPassword(dni, oldPassword); + if (!validPassword) { + return res.status(401).json({ error: 'Contraseña actual incorrecta' }); + } + + const newHash = await hashPassword(newPassword); + await updateUsuarioByDni(dni, { passwordHash: newHash }); + res.json({ ok: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Error al cambiar contraseña' }); + } +}); + +app.put('/api/auth/update-email', async (req, res) => { + try { + const { dni, newEmail } = req.body; + if (!dni || !newEmail) { + return res.status(400).json({ error: 'DNI y email son requeridos' }); + } + + await updateUsuarioByDni(dni, { email: newEmail }); + res.json({ ok: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Error al actualizar email' }); + } +}); + +// === GESTIÓN DE USUARIOS (ADMIN) === + +app.get('/api/usuarios', async (req, res) => { + try { + const usuarios = await getAllUsuarios(); + const usuariosSinPassword = usuarios.map(({ passwordHash, ...u }) => u); + res.json(usuariosSinPassword); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Error al obtener usuarios' }); + } +}); + +app.post('/api/usuarios', async (req, res) => { + try { + const { apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, password, areaId } = req.body; + + if (!apellido || !nombre || !dni || !fechaNacimiento || !email || !rol || !password) { + return res.status(400).json({ error: 'Todos los campos obligatorios deben estar presentes' }); + } + + const existente = await getUsuarioByDni(dni); + if (existente) { + return res.status(400).json({ error: 'Ya existe un usuario con ese DNI' }); + } + + const passwordHash = await hashPassword(password); + const usuario = { + id: generateUUID(), + apellido, + nombre, + dni, + fechaNacimiento, + email, + rol, + matriculaProfesional: matriculaProfesional || null, + passwordHash, + areaId: areaId || null, + fechaCreacion: new Date().toISOString().split('T')[0] + }; + + await createUsuario(usuario); + res.json({ ...usuario, passwordHash: undefined }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Error al crear usuario' }); + } +}); + +app.put('/api/usuarios/:id', async (req, res) => { + try { + const { id } = req.params; + const { apellido, nombre, fechaNacimiento, email, rol, matriculaProfesional, areaId, password } = req.body; + + const usuario = await getUsuarioById(id); + if (!usuario) { + return res.status(404).json({ error: 'Usuario no encontrado' }); + } + + const datos = {}; + if (apellido !== undefined) datos.apellido = apellido; + if (nombre !== undefined) datos.nombre = nombre; + if (fechaNacimiento !== undefined) datos.fechaNacimiento = fechaNacimiento; + if (email !== undefined) datos.email = email; + if (rol !== undefined) datos.rol = rol; + if (matriculaProfesional !== undefined) datos.matriculaProfesional = matriculaProfesional; + if (areaId !== undefined) datos.areaId = areaId; + if (password) { + datos.passwordHash = await hashPassword(password); + } + + await updateUsuario(id, datos); + res.json({ ok: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Error al actualizar usuario' }); + } +}); + +app.delete('/api/usuarios/:id', async (req, res) => { + try { + const { id } = req.params; + await deleteUsuario(id); + res.json({ ok: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Error al eliminar usuario' }); + } +}); + +// Helper para actualizar por DNI +async function updateUsuarioByDni(dni, datos) { + const { getUsuarioByDni, updateUsuario } = await import('./db.js'); + const usuario = await getUsuarioByDni(dni); + if (usuario) { + await updateUsuario(usuario.id, datos); + } +} diff --git a/server/package.json b/server/package.json index f1d0103..87b577d 100644 --- a/server/package.json +++ b/server/package.json @@ -6,9 +6,11 @@ "start": "node index.js" }, "dependencies": { - "better-sqlite3": "^11.7.0", + "bcryptjs": "^2.4.3", "cors": "^2.8.5", "express": "^4.21.2", - "http-proxy-middleware": "^3.0.5" + "http-proxy-middleware": "^3.0.5", + "sqlite": "^5.1.7", + "sqlite3": "^5.1.7" } } diff --git a/src/App.tsx b/src/App.tsx index aa94969..1d52565 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,8 @@ import { Cultivos } from '@/sections/Cultivos'; import { HistoriaClinica } from '@/sections/HistoriaClinica'; import { NuevoIngreso } from '@/sections/NuevoIngreso'; import { EditIngreso } from '@/sections/EditIngreso'; +import { Login } from '@/sections/Login'; +import { GestionUsuarios } from '@/sections/GestionUsuarios'; import { Spinner } from '@/components/ui/spinner'; import { Button } from '@/components/ui/button'; import { ThemeProvider } from "@/components/theme-provider"; @@ -26,6 +28,10 @@ function AppContent() { ); } + if (!store.isAuthenticated) { + return ; + } + const renderVista = () => { switch (store.vistaActual) { case 'dashboard': @@ -217,13 +223,17 @@ function AppContent() { /> ); } + case 'usuarios': + return ( + + ); default: return null; } }; return ( - + {renderVista()} ); diff --git a/src/hooks/useHospitalStore.ts b/src/hooks/useHospitalStore.ts index 781beb1..a4d58c0 100644 --- a/src/hooks/useHospitalStore.ts +++ b/src/hooks/useHospitalStore.ts @@ -12,7 +12,9 @@ import type { Interconsulta, ATB, Indicacion, - Vista + Vista, + Usuario, + RolUsuario } from '@/types'; // Fallback UUID generator for non-secure contexts (HTTP without localhost) @@ -44,10 +46,13 @@ interface HospitalState { movimientosIndicaciones: any[]; vistaActual: Vista; currentInternacionId?: string | null; + usuarios: Usuario[]; + currentUser: Usuario | null; + isAuthenticated: boolean; } -const API_BASE = import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`; +const API_BASE = 'http://localhost:4001/api'; const defaultState = (): HospitalState => ({ pacientes: [], @@ -63,7 +68,10 @@ const defaultState = (): HospitalState => ({ vistaActual: 'dashboard', indicaciones: [], movimientosIndicaciones: [], - camas: [] + camas: [], + usuarios: [], + currentUser: null, + isAuthenticated: false, }); export function useHospitalStore() { @@ -79,8 +87,15 @@ export function useHospitalStore() { if (!res.ok) throw new Error('no state'); const body = await res.json(); if (mounted && body) { - const normalized = body as HospitalState; - normalized.movimientosIndicaciones = normalized.movimientosIndicaciones || []; + const defaults = defaultState(); + const normalized = { + ...defaults, + ...body, + estudiosComplementarios: body.estudiosComplementarios || [], + interconsultas: body.interconsultas || [], + atb: body.atb || [], + movimientosIndicaciones: body.movimientosIndicaciones || [], + }; setState(normalized); } } catch (err) { @@ -542,6 +557,93 @@ const getEstadisticas = useCallback(() => { setState(prev => ({ ...prev, currentInternacionId: id ?? null })); }, []); + // Auth functions + const login = useCallback(async (dni: string, password: string) => { + try { + const res = await fetch(`${API_BASE}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dni, password }), + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Error en autenticación'); + } + const user = await res.json(); + setState(prev => ({ ...prev, currentUser: user, isAuthenticated: true })); + return user; + } catch (err) { + throw err; + } + }, []); + + const logout = useCallback(() => { + setState(prev => { + const newState = { ...prev, currentUser: null, isAuthenticated: false }; + fetch(`${API_BASE}/state`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...newState }), + }).catch(() => {}); + return newState; + }); + }, []); + + const changePassword = useCallback(async (dni: string, oldPassword: string, newPassword: string) => { + const res = await fetch(`${API_BASE}/auth/change-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dni, oldPassword, newPassword }), + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Error al cambiar contraseña'); + } + return true; + }, []); + + const updateEmail = useCallback(async (dni: string, newEmail: string) => { + const res = await fetch(`${API_BASE}/auth/update-email`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dni, newEmail }), + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Error al actualizar email'); + } + return true; + }, []); + + const hasPermission = useCallback((permission: 'read' | 'write', section: string): boolean => { + const user = state.currentUser; + if (!user) return false; + if (user.rol === 'admin') return true; + if (user.rol === 'enfermero') { + if (section === 'glucemias' || section === 'signosvitales') return permission === 'read' || permission === 'write'; + return permission === 'read'; + } + if (user.rol === 'medico') { + if (section === 'miArea') return permission === 'read' || permission === 'write'; + return permission === 'read'; + } + return false; + }, [state.currentUser]); + + const canAccessInternacion = useCallback((internacionId: string): boolean => { + const user = state.currentUser; + if (!user) return false; + const rol = user.rol as string; + if (rol === 'admin') return true; + if (rol === 'medico' && user.areaId) { + const internacion = state.internaciones.find(i => i.id === internacionId); + if (!internacion) return false; + const cama = state.camas.find(c => c.id === internacion.camaId); + return cama?.areaId === user.areaId; + } + return rol === 'admin'; + }, [state.currentUser, state.internaciones, state.camas]); + return { ...state, isLoaded, @@ -594,5 +696,11 @@ const getEstadisticas = useCallback(() => { actualizarLaboratorio, setCurrentInternacion, getEstadisticas, + login, + logout, + changePassword, + updateEmail, + hasPermission, + canAccessInternacion, }; } diff --git a/src/sections/GestionUsuarios.tsx b/src/sections/GestionUsuarios.tsx new file mode 100644 index 0000000..b6d0501 --- /dev/null +++ b/src/sections/GestionUsuarios.tsx @@ -0,0 +1,275 @@ +import { useState, useEffect } from 'react'; +import { useHospitalStore } from '@/hooks/useHospitalStore'; +import type { Usuario, RolUsuario } from '@/types'; +import { Card, CardContent, CardDescription, 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Plus, Pencil, Trash2, UserCog, Mail, Shield } from 'lucide-react'; + +const API_BASE = 'http://localhost:4001/api'; + +export function GestionUsuarios() { + const { logout, areas } = useHospitalStore(); + const [usuarios, setUsuarios] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editUsuario, setEditUsuario] = useState(null); + + const [form, setForm] = useState({ + apellido: '', + nombre: '', + dni: '', + fechaNacimiento: '', + email: '', + rol: 'medico' as RolUsuario, + matriculaProfesional: '', + password: '', + areaId: '', + }); + + useEffect(() => { + fetchUsuarios(); + }, []); + + const fetchUsuarios = async () => { + try { + const res = await fetch(`${API_BASE}/usuarios`); + const data = await res.json(); + setUsuarios(data); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + const resetForm = () => { + setForm({ + apellido: '', + nombre: '', + dni: '', + fechaNacimiento: '', + email: '', + rol: 'medico', + matriculaProfesional: '', + password: '', + areaId: '', + }); + setEditUsuario(null); + }; + + const openEdit = (usu: Usuario) => { + setEditUsuario(usu); + setForm({ + apellido: usu.apellido, + nombre: usu.nombre, + dni: usu.dni, + fechaNacimiento: usu.fechaNacimiento, + email: usu.email, + rol: usu.rol, + matriculaProfesional: usu.matriculaProfesional || '', + password: '', + areaId: usu.areaId || '', + }); + setDialogOpen(true); + }; + + const handleSubmit = async () => { + try { + if (editUsuario) { + await fetch(`${API_BASE}/usuarios/${editUsuario.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + } else { + await fetch(`${API_BASE}/usuarios`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + } + setDialogOpen(false); + resetForm(); + fetchUsuarios(); + } catch (err) { + console.error(err); + alert('Error al guardar usuario'); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('¿Está seguro de eliminar este usuario?')) return; + try { + await fetch(`${API_BASE}/usuarios/${id}`, { method: 'DELETE' }); + fetchUsuarios(); + } catch (err) { + console.error(err); + } + }; + + const getRolLabel = (rol: RolUsuario) => { + switch (rol) { + case 'admin': return 'Administrador'; + case 'medico': return 'Médico'; + case 'enfermero': return 'Enfermero'; + } + }; + + const getAreaName = (areaId?: string) => { + if (!areaId) return '-'; + return areas.find(a => a.id === areaId)?.nombre || '-'; + }; + + if (loading) { + return
Cargando...
; + } + + return ( +
+
+
+

Gestión de Usuarios

+

Administrar usuarios del sistema

+
+ +
+ + + + + + + Apellido, Nombre + DNI + Rol + Área + Email + Matrícula + Acciones + + + + {usuarios.map((usu) => ( + + {usu.apellido}, {usu.nombre} + {usu.dni} + + + {getRolLabel(usu.rol)} + + + {getAreaName(usu.areaId)} + {usu.email} + {usu.matriculaProfesional || '-'} + +
+ + +
+
+
+ ))} +
+
+
+
+ + + + + {editUsuario ? 'Editar Usuario' : 'Nuevo Usuario'} + +
+
+
+ + setForm({ ...form, apellido: e.target.value })} /> +
+
+ + setForm({ ...form, nombre: e.target.value })} /> +
+
+ +
+
+ + setForm({ ...form, dni: e.target.value })} disabled={!!editUsuario} /> +
+
+ + setForm({ ...form, fechaNacimiento: e.target.value })} /> +
+
+ +
+ + setForm({ ...form, email: e.target.value })} /> +
+ +
+
+ + +
+
+ + setForm({ ...form, matriculaProfesional: e.target.value })} /> +
+
+ + {form.rol === 'medico' && ( +
+ + +
+ )} + +
+ + setForm({ ...form, password: e.target.value })} /> +
+
+ +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/sections/Layout.tsx b/src/sections/Layout.tsx index 4ef38ec..63ac650 100644 --- a/src/sections/Layout.tsx +++ b/src/sections/Layout.tsx @@ -4,17 +4,18 @@ import { Users, ClipboardList, FileText, - Activity, Microscope, Menu, Sun, - Moon + Moon, + LogOut, + UserCog } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; -import type { Vista } from '@/types'; +import type { Vista, Usuario } from '@/types'; import { DropdownMenu, @@ -28,27 +29,39 @@ interface LayoutProps { children: React.ReactNode; vistaActual: Vista; onCambiarVista: (vista: Vista) => void; + currentUser: Usuario | null; + onLogout: () => void; } -const menuItems: { vista: Vista; label: string; icon: React.ElementType }[] = [ - { vista: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, - { vista: 'camas', label: 'Camas', icon: Bed }, - { vista: 'pacientes', label: 'Pacientes', icon: Users }, - { vista: 'internaciones', label: 'Internaciones', icon: ClipboardList }, - { vista: 'evoluciones', label: 'Evoluciones', icon: FileText }, - //{ vista: 'laboratorios', label: 'Laboratorios', icon: FlaskConical }, - //{ vista: 'acidobase', label: 'Ácido-Base', icon: Activity }, - { vista: 'cultivos', label: 'Cultivos', icon: Microscope }, -]; - -export function Layout({ children, vistaActual, onCambiarVista }: LayoutProps) { +export function Layout({ children, vistaActual, onCambiarVista, currentUser, onLogout }: LayoutProps) { const { theme, setTheme } = useTheme() const toggleDarkMode = () => setTheme(theme === "dark" ? "light" : "dark") + const menuItems: { vista: Vista; label: string; icon: React.ElementType; rolRequerido?: string }[] = [ + { vista: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, + { vista: 'camas', label: 'Camas', icon: Bed, rolRequerido: 'medico' }, + { vista: 'pacientes', label: 'Pacientes', icon: Users }, + { vista: 'internaciones', label: 'Internaciones', icon: ClipboardList }, + { vista: 'evoluciones', label: 'Evoluciones', icon: FileText }, + { vista: 'cultivos', label: 'Cultivos', icon: Microscope }, + ]; + + const filteredMenuItems = menuItems.filter(item => { + const rol = currentUser?.rol as string | undefined; + if (!item.rolRequerido) return true; + if (rol === 'admin') return true; + if (item.rolRequerido === 'medico' && (rol === 'medico' || rol === 'admin')) return true; + return false; + }); + + if ((currentUser?.rol as string) === 'admin') { + filteredMenuItems.push({ vista: 'usuarios', label: 'Usuarios', icon: UserCog }); + } + const NavContent = () => (