feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
Backend API:
- Canillitas (`dist_dtCanillas`):
  - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
  - Auditoría en `dist_dtCanillas_H`.
  - Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
  - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Auditoría en `dist_dtDistribuidores_H`.
  - Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
  - Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
  - Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
  - Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
  - Lógica de negocio para reabrir período de precio anterior al eliminar el último.
  - Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
  - Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
  - Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
  - Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
  - Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.
Frontend React:
- Canillitas:
  - `canillaService.ts`.
  - `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
  - `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
  - `distribuidorService.ts`.
  - `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
  - `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
  - `precioService.ts`.
  - `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
  - `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
  - Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
  - Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
			
			
This commit is contained in:
		
							
								
								
									
										159
									
								
								Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| import React, { useState, useEffect, useCallback } from 'react'; | ||||
| import { useParams, useNavigate } from 'react-router-dom'; | ||||
| import { | ||||
|     Box, Typography, Button, Paper, CircularProgress, Alert, | ||||
|     Checkbox, FormControlLabel, FormGroup // Para el caso sin componente checklist | ||||
| } from '@mui/material'; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import SaveIcon from '@mui/icons-material/Save'; | ||||
| import perfilService from '../../services/Usuarios/perfilService'; | ||||
| import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto'; | ||||
| import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; // Para verificar si el usuario actual puede estar aquí | ||||
| import axios from 'axios'; | ||||
| import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist'; // Importar el componente | ||||
|  | ||||
| const AsignarPermisosAPerfilPage: React.FC = () => { | ||||
|   const { idPerfil } = useParams<{ idPerfil: string }>(); | ||||
|   const navigate = useNavigate(); | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|  | ||||
|   const puedeAsignar = isSuperAdmin || tienePermiso("PU004"); | ||||
|  | ||||
|   const [perfil, setPerfil] = useState<PerfilDto | null>(null); | ||||
|   const [permisosDisponibles, setPermisosDisponibles] = useState<PermisoAsignadoDto[]>([]); | ||||
|   // Usamos un Set para los IDs de los permisos seleccionados para eficiencia | ||||
|   const [permisosSeleccionados, setPermisosSeleccionados] = useState<Set<number>>(new Set()); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [saving, setSaving] = useState(false); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [successMessage, setSuccessMessage] = useState<string | null>(null); | ||||
|  | ||||
|   const idPerfilNum = Number(idPerfil); | ||||
|  | ||||
|   const cargarDatos = useCallback(async () => { | ||||
|     if (!puedeAsignar) { | ||||
|         setError("Acceso denegado. No tiene permiso para asignar permisos."); | ||||
|         setLoading(false); | ||||
|         return; | ||||
|     } | ||||
|     if (isNaN(idPerfilNum)) { | ||||
|         setError("ID de Perfil inválido."); | ||||
|         setLoading(false); | ||||
|         return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setSuccessMessage(null); | ||||
|     try { | ||||
|       const [perfilData, permisosData] = await Promise.all([ | ||||
|         perfilService.getPerfilById(idPerfilNum), | ||||
|         perfilService.getPermisosPorPerfil(idPerfilNum) | ||||
|       ]); | ||||
|       setPerfil(perfilData); | ||||
|       setPermisosDisponibles(permisosData); | ||||
|       // Inicializar los permisos seleccionados basados en los que vienen 'asignado: true' | ||||
|       setPermisosSeleccionados(new Set(permisosData.filter(p => p.asignado).map(p => p.id))); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       setError('Error al cargar datos del perfil o permisos.'); | ||||
|        if (axios.isAxiosError(err) && err.response?.status === 404) { | ||||
|         setError(`Perfil con ID ${idPerfilNum} no encontrado.`); | ||||
|       } | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }, [idPerfilNum, puedeAsignar]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     cargarDatos(); | ||||
|   }, [cargarDatos]); | ||||
|  | ||||
|   const handlePermisoChange = (permisoId: number, asignado: boolean) => { | ||||
|     setPermisosSeleccionados(prev => { | ||||
|       const next = new Set(prev); | ||||
|       if (asignado) { | ||||
|         next.add(permisoId); | ||||
|       } else { | ||||
|         next.delete(permisoId); | ||||
|       } | ||||
|       return next; | ||||
|     }); | ||||
|     // Limpiar mensajes al cambiar selección | ||||
|     if (successMessage) setSuccessMessage(null); | ||||
|     if (error) setError(null); | ||||
|   }; | ||||
|  | ||||
|   const handleGuardarCambios = async () => { | ||||
|     if (!puedeAsignar || !perfil) return; | ||||
|     setSaving(true); setError(null); setSuccessMessage(null); | ||||
|     try { | ||||
|       await perfilService.updatePermisosPorPerfil(perfil.id, { | ||||
|         permisosIds: Array.from(permisosSeleccionados) | ||||
|       }); | ||||
|       setSuccessMessage('Permisos actualizados correctamente.'); | ||||
|       // Opcional: recargar datos, aunque el estado local ya está actualizado | ||||
|       // cargarDatos(); | ||||
|     } catch (err: any) { | ||||
|       console.error(err); | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message | ||||
|                         ? err.response.data.message | ||||
|                         : 'Error al guardar los permisos.'; | ||||
|       setError(message); | ||||
|     } finally { | ||||
|       setSaving(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (loading) { | ||||
|     return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>; | ||||
|   } | ||||
|  | ||||
|   if (error && !perfil) { // Si hay un error crítico al cargar el perfil | ||||
|     return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>; | ||||
|   } | ||||
|   if (!puedeAsignar) { | ||||
|      return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>; | ||||
|   } | ||||
|   if (!perfil) { // Si no hay error, pero el perfil es null después de cargar (no debería pasar si no hay error) | ||||
|       return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado.</Alert>; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}> | ||||
|         Volver a Perfiles | ||||
|       </Button> | ||||
|       <Typography variant="h4" gutterBottom> | ||||
|         Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'} | ||||
|       </Typography> | ||||
|       <Typography variant="body2" color="textSecondary" gutterBottom> | ||||
|         ID Perfil: {perfil?.id} | ||||
|       </Typography> | ||||
|  | ||||
|       {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|       {successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>} | ||||
|  | ||||
|       <Paper sx={{ p: 2, mt: 2 }}> | ||||
|         <PermisosChecklist | ||||
|           permisosDisponibles={permisosDisponibles} | ||||
|           permisosSeleccionados={permisosSeleccionados} | ||||
|           onPermisoChange={handlePermisoChange} | ||||
|           disabled={saving} | ||||
|         /> | ||||
|         <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}> | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             color="primary" | ||||
|             startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />} | ||||
|             onClick={handleGuardarCambios} | ||||
|             disabled={saving || !puedeAsignar} | ||||
|           > | ||||
|             Guardar Cambios | ||||
|           </Button> | ||||
|         </Box> | ||||
|       </Paper> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default AsignarPermisosAPerfilPage; | ||||
| @@ -0,0 +1,22 @@ | ||||
| import React from 'react'; | ||||
| import { Typography, Container, Button } from '@mui/material'; | ||||
| import { useAuth } from '../../contexts/AuthContext'; | ||||
|  | ||||
| const ChangePasswordPagePlaceholder: React.FC = () => { | ||||
|     const { setShowForcedPasswordChangeModal } = useAuth(); | ||||
|   return ( | ||||
|     <Container> | ||||
|       <Typography variant="h4" component="h1" gutterBottom> | ||||
|         Cambiar Contraseña (Página) | ||||
|       </Typography> | ||||
|       <Typography> | ||||
|         La funcionalidad de cambio de contraseña ahora se maneja principalmente a través de un modal. | ||||
|       </Typography> | ||||
|       <Button onClick={() => setShowForcedPasswordChangeModal(true)}> | ||||
|         Abrir Modal de Cambio de Contraseña | ||||
|       </Button> | ||||
|     </Container> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ChangePasswordPagePlaceholder; | ||||
							
								
								
									
										237
									
								
								Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | ||||
| import React, { useState, useEffect, useCallback } from 'react'; | ||||
| import { | ||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||
|     CircularProgress, Alert, Tooltip // Añadir Tooltip | ||||
| } from '@mui/material'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | ||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||
| import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; // Para asignar permisos | ||||
| import perfilService from '../../services/Usuarios/perfilService'; | ||||
| import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; | ||||
| import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto'; | ||||
| import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto'; | ||||
| import PerfilFormModal from '../../components/Modals/Usuarios/PerfilFormModal'; | ||||
| // import PermisosPorPerfilModal from '../../components/Modals/PermisosPorPerfilModal'; // Lo crearemos después | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
| import { useNavigate } from 'react-router-dom'; // Para navegar | ||||
|  | ||||
| const GestionarPerfilesPage: React.FC = () => { | ||||
|   const [perfiles, setPerfiles] = useState<PerfilDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [filtroNombre, setFiltroNombre] = useState(''); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingPerfil, setEditingPerfil] = useState<PerfilDto | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   // const [permisosModalOpen, setPermisosModalOpen] = useState(false); // Para modal de permisos | ||||
|   // const [selectedPerfilForPermisos, setSelectedPerfilForPermisos] = useState<PerfilDto | null>(null); | ||||
|  | ||||
|   const [page, setPage] = useState(0); | ||||
|   const [rowsPerPage, setRowsPerPage] = useState(5); | ||||
|  | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedPerfilRow, setSelectedPerfilRow] = useState<PerfilDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|   const navigate = useNavigate(); // Hook para navegación | ||||
|  | ||||
|   // Permisos para Perfiles (PU001 a PU004) | ||||
|   const puedeVer = isSuperAdmin || tienePermiso("PU001"); | ||||
|   const puedeCrear = isSuperAdmin || tienePermiso("PU002"); | ||||
|   const puedeModificar = isSuperAdmin || tienePermiso("PU003"); // Modificar nombre/desc | ||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("PU003"); // Excel dice PU003 para eliminar | ||||
|   const puedeAsignarPermisos = isSuperAdmin || tienePermiso("PU004"); | ||||
|  | ||||
|   const cargarPerfiles = useCallback(async () => { | ||||
|     if (!puedeVer) { | ||||
|         setError("No tiene permiso para ver esta sección."); | ||||
|         setLoading(false); | ||||
|         return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const data = await perfilService.getAllPerfiles(filtroNombre); | ||||
|       setPerfiles(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); setError('Error al cargar los perfiles.'); | ||||
|     } finally { setLoading(false); } | ||||
|   }, [filtroNombre, puedeVer]); | ||||
|  | ||||
|   useEffect(() => { cargarPerfiles(); }, [cargarPerfiles]); | ||||
|  | ||||
|   const handleOpenModal = (perfil?: PerfilDto) => { | ||||
|     setEditingPerfil(perfil || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingPerfil(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreatePerfilDto | (UpdatePerfilDto & { id: number })) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingPerfil && 'id' in data) { | ||||
|         await perfilService.updatePerfil(editingPerfil.id, data); | ||||
|       } else { | ||||
|         await perfilService.createPerfil(data as CreatePerfilDto); | ||||
|       } | ||||
|       cargarPerfiles(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el perfil.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (id: number) => { | ||||
|     if (window.confirm(`¿Está seguro? ID: ${id}`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await perfilService.deletePerfil(id); | ||||
|         cargarPerfiles(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el perfil.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, perfil: PerfilDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedPerfilRow(perfil); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedPerfilRow(null); | ||||
|   }; | ||||
|  | ||||
|   const handleOpenPermisosModal = (perfil: PerfilDto) => { | ||||
|     // setSelectedPerfilForPermisos(perfil); | ||||
|     // setPermisosModalOpen(true); | ||||
|     handleMenuClose(); | ||||
|     // Navegar a la página de asignación de permisos | ||||
|     navigate(`/usuarios/perfiles/${perfil.id}/permisos`); | ||||
|   }; | ||||
|   // const handleClosePermisosModal = () => { | ||||
|   //   setPermisosModalOpen(false); setSelectedPerfilForPermisos(null); | ||||
|   // }; | ||||
|   // const handleSubmitPermisos = async (idPerfil: number, permisosIds: number[]) => { | ||||
|   //   try { | ||||
|   //     // await perfilService.updatePermisosPorPerfil(idPerfil, permisosIds); | ||||
|   //     // console.log("Permisos actualizados para perfil:", idPerfil); | ||||
|   //     // Quizás un snackbar de éxito | ||||
|   //   } catch (error) { | ||||
|   //     console.error("Error al actualizar permisos:", error); | ||||
|   //     setApiErrorMessage("Error al actualizar permisos."); | ||||
|   //   } | ||||
|   //   handleClosePermisosModal(); | ||||
|   // }; | ||||
|  | ||||
|  | ||||
|    const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||
|    const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||
|      setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||
|    }; | ||||
|    const displayData = perfiles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|  | ||||
|   if (!loading && !puedeVer) { | ||||
|       return ( | ||||
|            <Box sx={{ p: 2 }}> | ||||
|               <Typography variant="h4" gutterBottom>Gestionar Perfiles</Typography> | ||||
|               <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> | ||||
|            </Box> | ||||
|       ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Typography variant="h4" gutterBottom>Gestionar Perfiles</Typography> | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|          <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> | ||||
|             <TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} /> | ||||
|          </Box> | ||||
|          {puedeCrear && ( | ||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> | ||||
|              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||
|                 Agregar Nuevo Perfil | ||||
|              </Button> | ||||
|           </Box> | ||||
|         )} | ||||
|       </Paper> | ||||
|  | ||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} | ||||
|  | ||||
|       {!loading && !error && ( | ||||
|          <TableContainer component={Paper}> | ||||
|            <Table> | ||||
|              <TableHead> | ||||
|                <TableRow> | ||||
|                  <TableCell>Nombre del Perfil</TableCell> | ||||
|                  <TableCell>Descripción</TableCell> | ||||
|                  {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && <TableCell align="right">Acciones</TableCell>} | ||||
|                </TableRow> | ||||
|              </TableHead> | ||||
|              <TableBody> | ||||
|                {displayData.length === 0 && !loading ? ( | ||||
|                   <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar || puedeAsignarPermisos) ? 3 : 2} align="center">No se encontraron perfiles.</TableCell></TableRow> | ||||
|                ) : ( | ||||
|                  displayData.map((perfil) => ( | ||||
|                      <TableRow key={perfil.id}> | ||||
|                      <TableCell>{perfil.nombrePerfil}</TableCell> | ||||
|                      <TableCell>{perfil.descripcion || '-'}</TableCell> | ||||
|                      {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && ( | ||||
|                         <TableCell align="right"> | ||||
|                             <IconButton onClick={(e) => handleMenuOpen(e, perfil)} disabled={!puedeModificar && !puedeEliminar && !puedeAsignarPermisos}> | ||||
|                                 <MoreVertIcon /> | ||||
|                             </IconButton> | ||||
|                         </TableCell> | ||||
|                      )} | ||||
|                      </TableRow> | ||||
|                  )) | ||||
|                )} | ||||
|              </TableBody> | ||||
|            </Table> | ||||
|            <TablePagination | ||||
|              rowsPerPageOptions={[5, 10, 25]} component="div" count={perfiles.length} | ||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||
|            /> | ||||
|          </TableContainer> | ||||
|        )} | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {puedeModificar && ( | ||||
|             <MenuItem onClick={() => { handleOpenModal(selectedPerfilRow!); handleMenuClose(); }}>Modificar</MenuItem> | ||||
|         )} | ||||
|         {puedeEliminar && ( | ||||
|             <MenuItem onClick={() => handleDelete(selectedPerfilRow!.id)}>Eliminar</MenuItem> | ||||
|         )} | ||||
|         {puedeAsignarPermisos && ( | ||||
|             <MenuItem onClick={() => handleOpenPermisosModal(selectedPerfilRow!)}>Asignar Permisos</MenuItem> | ||||
|         )} | ||||
|         {(!puedeModificar && !puedeEliminar && !puedeAsignarPermisos) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||
|       </Menu> | ||||
|  | ||||
|       <PerfilFormModal | ||||
|         open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|         initialData={editingPerfil} errorMessage={apiErrorMessage} | ||||
|         clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|       /> | ||||
|       {/* {selectedPerfilForPermisos && ( | ||||
|           <PermisosPorPerfilModal | ||||
|               open={permisosModalOpen} | ||||
|               onClose={handleClosePermisosModal} | ||||
|               perfil={selectedPerfilForPermisos} | ||||
|               onSubmit={handleSubmitPermisos} | ||||
|               // Asume que tienes un servicio para obtener todos los permisos disponibles | ||||
|               // getAllPermisosDisponibles={async () => []} // Implementar esto | ||||
|           /> | ||||
|       )} */} | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarPerfilesPage; | ||||
							
								
								
									
										200
									
								
								Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | ||||
| import React, { useState, useEffect, useCallback } from 'react'; | ||||
| import { | ||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||
|     CircularProgress, Alert | ||||
| } from '@mui/material'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | ||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||
| import permisoService from '../../services/Usuarios/permisoService'; | ||||
| import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto'; | ||||
| import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto'; | ||||
| import type { UpdatePermisoDto } from '../../models/dtos/Usuarios/UpdatePermisoDto'; | ||||
| import PermisoFormModal from '../../components/Modals/Usuarios/PermisoFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarPermisosPage: React.FC = () => { | ||||
|   const [permisos, setPermisos] = useState<PermisoDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [filtroModulo, setFiltroModulo] = useState(''); | ||||
|   const [filtroCodAcc, setFiltroCodAcc] = useState(''); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingPermiso, setEditingPermiso] = useState<PermisoDto | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   const [page, setPage] = useState(0); | ||||
|   const [rowsPerPage, setRowsPerPage] = useState(10); // Un poco más para esta tabla | ||||
|  | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedPermisoRow, setSelectedPermisoRow] = useState<PermisoDto | null>(null); | ||||
|  | ||||
|   const { isSuperAdmin } = usePermissions(); // Solo SuperAdmin puede acceder | ||||
|  | ||||
|   const cargarPermisos = useCallback(async () => { | ||||
|     if (!isSuperAdmin) { | ||||
|         setError("Acceso denegado. Solo SuperAdmin puede gestionar permisos."); | ||||
|         setLoading(false); | ||||
|         return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const data = await permisoService.getAllPermisos(filtroModulo, filtroCodAcc); | ||||
|       setPermisos(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); setError('Error al cargar los permisos.'); | ||||
|     } finally { setLoading(false); } | ||||
|   }, [filtroModulo, filtroCodAcc, isSuperAdmin]); | ||||
|  | ||||
|   useEffect(() => { cargarPermisos(); }, [cargarPermisos]); | ||||
|  | ||||
|   const handleOpenModal = (permiso?: PermisoDto) => { | ||||
|     setEditingPermiso(permiso || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingPermiso(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreatePermisoDto | (UpdatePermisoDto & { id: number })) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingPermiso && 'id' in data) { | ||||
|         await permisoService.updatePermiso(editingPermiso.id, data); | ||||
|       } else { | ||||
|         await permisoService.createPermiso(data as CreatePermisoDto); | ||||
|       } | ||||
|       cargarPermisos(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el permiso.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (id: number) => { | ||||
|     if (window.confirm(`¿Está seguro de eliminar este permiso (ID: ${id})?`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await permisoService.deletePermiso(id); | ||||
|         cargarPermisos(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el permiso.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, permiso: PermisoDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedPermisoRow(permiso); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedPermisoRow(null); | ||||
|   }; | ||||
|  | ||||
|    const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||
|    const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||
|      setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||
|    }; | ||||
|    const displayData = permisos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|  | ||||
|   if (!loading && !isSuperAdmin) { | ||||
|       return ( | ||||
|            <Box sx={{ p: 2 }}> | ||||
|               <Typography variant="h4" gutterBottom>Definición de Permisos</Typography> | ||||
|               <Alert severity="error">{error || "Acceso denegado."}</Alert> | ||||
|            </Box> | ||||
|       ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Typography variant="h4" gutterBottom>Definición de Permisos (SuperAdmin)</Typography> | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|         <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> | ||||
|             <TextField | ||||
|                 label="Filtrar por Módulo" | ||||
|                 variant="outlined" | ||||
|                 size="small" | ||||
|                 value={filtroModulo} | ||||
|                 onChange={(e) => setFiltroModulo(e.target.value)} | ||||
|                 sx={{ flexGrow: 1, minWidth: '200px' }} // Para que se adapte mejor | ||||
|             /> | ||||
|             <TextField | ||||
|                 label="Filtrar por CodAcc" | ||||
|                 variant="outlined" | ||||
|                 size="small" | ||||
|                 value={filtroCodAcc} | ||||
|                 onChange={(e) => setFiltroCodAcc(e.target.value)} | ||||
|                 sx={{ flexGrow: 1, minWidth: '200px' }} | ||||
|             /> | ||||
|             {/* El botón de búsqueda es opcional si el filtro es en tiempo real */} | ||||
|             {/* <Button variant="contained" onClick={cargarPermisos}>Buscar</Button> */} | ||||
|         </Box> | ||||
|          {isSuperAdmin && ( | ||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> | ||||
|              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||
|                 Agregar Nuevo Permiso | ||||
|              </Button> | ||||
|           </Box> | ||||
|         )} | ||||
|       </Paper> | ||||
|  | ||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} | ||||
|  | ||||
|       {!loading && !error && isSuperAdmin && ( | ||||
|          <TableContainer component={Paper}> | ||||
|            <Table size="small"> | ||||
|              <TableHead> | ||||
|                <TableRow> | ||||
|                  <TableCell>Módulo</TableCell> | ||||
|                  <TableCell>Descripción</TableCell> | ||||
|                  <TableCell>CodAcc</TableCell> | ||||
|                  <TableCell align="right">Acciones</TableCell> | ||||
|                </TableRow> | ||||
|              </TableHead> | ||||
|              <TableBody> | ||||
|                {displayData.length === 0 && !loading ? ( | ||||
|                   <TableRow><TableCell colSpan={4} align="center">No se encontraron permisos.</TableCell></TableRow> | ||||
|                ) : ( | ||||
|                  displayData.map((permiso) => ( | ||||
|                      <TableRow key={permiso.id}> | ||||
|                      <TableCell>{permiso.modulo}</TableCell> | ||||
|                      <TableCell>{permiso.descPermiso}</TableCell> | ||||
|                      <TableCell>{permiso.codAcc}</TableCell> | ||||
|                      <TableCell align="right"> | ||||
|                         <IconButton onClick={(e) => handleMenuOpen(e, permiso)}> | ||||
|                             <MoreVertIcon /> | ||||
|                         </IconButton> | ||||
|                      </TableCell> | ||||
|                      </TableRow> | ||||
|                  )) | ||||
|                )} | ||||
|              </TableBody> | ||||
|            </Table> | ||||
|            <TablePagination | ||||
|              rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={permisos.length} | ||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||
|            /> | ||||
|          </TableContainer> | ||||
|        )} | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         <MenuItem onClick={() => { handleOpenModal(selectedPermisoRow!); handleMenuClose(); }}>Modificar</MenuItem> | ||||
|         <MenuItem onClick={() => handleDelete(selectedPermisoRow!.id)}>Eliminar</MenuItem> | ||||
|       </Menu> | ||||
|  | ||||
|       <PermisoFormModal | ||||
|         open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|         initialData={editingPermiso} errorMessage={apiErrorMessage} | ||||
|         clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|       /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarPermisosPage; | ||||
							
								
								
									
										264
									
								
								Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | ||||
| import React, { useState, useEffect, useCallback } from 'react'; | ||||
| import { | ||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, | ||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||
|     CircularProgress, Alert, Tooltip | ||||
| } from '@mui/material'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | ||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||
| import VpnKeyIcon from '@mui/icons-material/VpnKey'; // Para resetear clave | ||||
| import usuarioService from '../../services/Usuarios/usuarioService'; | ||||
| import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto'; | ||||
| import type { CreateUsuarioRequestDto } from '../../models/dtos/Usuarios/CreateUsuarioRequestDto'; | ||||
| import type { UpdateUsuarioRequestDto } from '../../models/dtos/Usuarios/UpdateUsuarioRequestDto'; | ||||
| import type { SetPasswordRequestDto } from '../../models/dtos/Usuarios/SetPasswordRequestDto'; | ||||
| import UsuarioFormModal from '../../components/Modals/Usuarios/UsuarioFormModal'; | ||||
| import SetPasswordModal from '../../components/Modals/Usuarios/SetPasswordModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarUsuariosPage: React.FC = () => { | ||||
|   const [usuarios, setUsuarios] = useState<UsuarioDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [filtroUser, setFiltroUser] = useState(''); | ||||
|   const [filtroNombre, setFiltroNombre] = useState(''); | ||||
|  | ||||
|   const [usuarioModalOpen, setUsuarioModalOpen] = useState(false); | ||||
|   const [editingUsuario, setEditingUsuario] = useState<UsuarioDto | null>(null); | ||||
|  | ||||
|   const [setPasswordModalOpen, setSetPasswordModalOpen] = useState(false); | ||||
|   const [selectedUsuarioForPassword, setSelectedUsuarioForPassword] = useState<UsuarioDto | null>(null); | ||||
|  | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   const [page, setPage] = useState(0); | ||||
|   const [rowsPerPage, setRowsPerPage] = useState(5); | ||||
|  | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedUsuarioRow, setSelectedUsuarioRow] = useState<UsuarioDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin, currentUser } = usePermissions(); | ||||
|  | ||||
|   const puedeVer = isSuperAdmin || tienePermiso("CU001"); | ||||
|   const puedeCrear = isSuperAdmin || tienePermiso("CU002"); | ||||
|   const puedeModificar = isSuperAdmin || tienePermiso("CU003"); // Modificar datos básicos | ||||
|   const puedeAsignarPerfil = isSuperAdmin || tienePermiso("CU004"); // Modificar perfil | ||||
|   // Resetear clave es típicamente SuperAdmin | ||||
|   const puedeResetearClave = isSuperAdmin; | ||||
|  | ||||
|   const cargarUsuarios = useCallback(async () => { | ||||
|     if (!puedeVer) { | ||||
|       setError("No tiene permiso para ver esta sección."); | ||||
|       setLoading(false); | ||||
|       return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const data = await usuarioService.getAllUsuarios(filtroUser, filtroNombre); | ||||
|       setUsuarios(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); setError('Error al cargar los usuarios.'); | ||||
|     } finally { setLoading(false); } | ||||
|   }, [filtroUser, filtroNombre, puedeVer]); | ||||
|  | ||||
|   useEffect(() => { cargarUsuarios(); }, [cargarUsuarios]); | ||||
|  | ||||
|   const handleOpenUsuarioModal = (usuario?: UsuarioDto) => { | ||||
|     setEditingUsuario(usuario || null); setApiErrorMessage(null); setUsuarioModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseUsuarioModal = () => { | ||||
|     setUsuarioModalOpen(false); setEditingUsuario(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitUsuarioModal = async (data: CreateUsuarioRequestDto | UpdateUsuarioRequestDto, id?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (id && editingUsuario) { // Es Update | ||||
|         await usuarioService.updateUsuario(id, data as UpdateUsuarioRequestDto); | ||||
|       } else { // Es Create | ||||
|         await usuarioService.createUsuario(data as CreateUsuarioRequestDto); | ||||
|       } | ||||
|       cargarUsuarios(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el usuario.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleOpenSetPasswordModal = (usuario: UsuarioDto) => { | ||||
|     setSelectedUsuarioForPassword(usuario); | ||||
|     setApiErrorMessage(null); | ||||
|     setSetPasswordModalOpen(true); | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|   const handleCloseSetPasswordModal = () => { | ||||
|     setSetPasswordModalOpen(false); setSelectedUsuarioForPassword(null); | ||||
|   }; | ||||
|   const handleSubmitSetPassword = async (userId: number, data: SetPasswordRequestDto) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|         await usuarioService.setPassword(userId, data); | ||||
|         cargarUsuarios(); // Para reflejar el cambio en 'DebeCambiarClave' | ||||
|     } catch (err:any) { | ||||
|         const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al establecer la contraseña.'; | ||||
|         setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleToggleHabilitado = async (usuario: UsuarioDto) => { | ||||
|     setApiErrorMessage(null); | ||||
|     // Un usuario no puede deshabilitarse a sí mismo | ||||
|     if (currentUser?.userId === usuario.id) { | ||||
|         setApiErrorMessage("No puede cambiar el estado de habilitación de su propio usuario."); | ||||
|         return; | ||||
|     } | ||||
|     try { | ||||
|         await usuarioService.toggleHabilitado(usuario.id, !usuario.habilitada); | ||||
|         cargarUsuarios(); | ||||
|     } catch (err: any) { | ||||
|         const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar el estado del usuario.'; | ||||
|         setApiErrorMessage(message); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, usuario: UsuarioDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedUsuarioRow(usuario); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedUsuarioRow(null); | ||||
|   }; | ||||
|  | ||||
|    const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||
|    const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||
|      setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||
|    }; | ||||
|    const displayData = usuarios.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|  | ||||
|   if (!loading && !puedeVer) { | ||||
|       return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert></Box>; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Typography variant="h4" gutterBottom>Gestionar Usuarios</Typography> | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|          {/* SECCIÓN DE FILTROS CORREGIDA */} | ||||
|          <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> | ||||
|             <TextField | ||||
|                 label="Filtrar por Usuario" | ||||
|                 variant="outlined" | ||||
|                 size="small" | ||||
|                 value={filtroUser} | ||||
|                 onChange={(e) => setFiltroUser(e.target.value)} | ||||
|                 sx={{ flexGrow: 1, minWidth: '200px' }} | ||||
|             /> | ||||
|             <TextField | ||||
|                 label="Filtrar por Nombre/Apellido" | ||||
|                 variant="outlined" | ||||
|                 size="small" | ||||
|                 value={filtroNombre} | ||||
|                 onChange={(e) => setFiltroNombre(e.target.value)} | ||||
|                 sx={{ flexGrow: 1, minWidth: '200px' }} | ||||
|             /> | ||||
|          </Box> | ||||
|          {puedeCrear && ( | ||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> | ||||
|              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenUsuarioModal()} sx={{ mb: 2 }}> | ||||
|                 Agregar Nuevo Usuario | ||||
|              </Button> | ||||
|           </Box> | ||||
|         )} | ||||
|       </Paper> | ||||
|  | ||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} | ||||
|  | ||||
|       {!loading && !error && puedeVer && ( | ||||
|          <TableContainer component={Paper}> | ||||
|            <Table size="small"> | ||||
|              <TableHead> | ||||
|                <TableRow> | ||||
|                  <TableCell>Usuario</TableCell> | ||||
|                  <TableCell>Nombre Completo</TableCell> | ||||
|                  <TableCell>Perfil</TableCell> | ||||
|                  <TableCell>Habilitado</TableCell> | ||||
|                  <TableCell>Cambiar Clave</TableCell> | ||||
|                  <TableCell>SuperAdmin</TableCell> | ||||
|                  <TableCell align="right">Acciones</TableCell> | ||||
|                </TableRow> | ||||
|              </TableHead> | ||||
|              <TableBody> | ||||
|                {displayData.length === 0 && !loading ? ( | ||||
|                   <TableRow><TableCell colSpan={7} align="center">No se encontraron usuarios.</TableCell></TableRow> | ||||
|                ) : ( | ||||
|                  displayData.map((usr) => ( | ||||
|                      <TableRow key={usr.id}> | ||||
|                      <TableCell>{usr.user}</TableCell> | ||||
|                      <TableCell>{`${usr.nombre} ${usr.apellido}`}</TableCell> | ||||
|                      <TableCell>{usr.nombrePerfil}</TableCell> | ||||
|                      <TableCell> | ||||
|                         <Tooltip title={usr.habilitada ? "Deshabilitar" : "Habilitar"}> | ||||
|                             <Switch | ||||
|                                 checked={usr.habilitada} | ||||
|                                 onChange={() => handleToggleHabilitado(usr)} | ||||
|                                 disabled={!puedeModificar || currentUser?.userId === usr.id} | ||||
|                                 size="small" | ||||
|                             /> | ||||
|                         </Tooltip> | ||||
|                      </TableCell> | ||||
|                      <TableCell>{usr.debeCambiarClave ? 'Sí' : 'No'}</TableCell> | ||||
|                      <TableCell>{usr.supAdmin ? 'Sí' : 'No'}</TableCell> | ||||
|                      <TableCell align="right"> | ||||
|                         <IconButton onClick={(e) => handleMenuOpen(e, usr)} disabled={!puedeModificar && !puedeAsignarPerfil && !puedeResetearClave}> | ||||
|                             <MoreVertIcon /> | ||||
|                         </IconButton> | ||||
|                      </TableCell> | ||||
|                      </TableRow> | ||||
|                  )) | ||||
|                )} | ||||
|              </TableBody> | ||||
|            </Table> | ||||
|            <TablePagination | ||||
|              rowsPerPageOptions={[5, 10, 25]} component="div" count={usuarios.length} | ||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||
|            /> | ||||
|          </TableContainer> | ||||
|        )} | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {(puedeModificar || puedeAsignarPerfil) && ( | ||||
|             <MenuItem onClick={() => { handleOpenUsuarioModal(selectedUsuarioRow!); handleMenuClose(); }}>Modificar</MenuItem> | ||||
|         )} | ||||
|         {puedeResetearClave && selectedUsuarioRow && currentUser?.userId !== selectedUsuarioRow.id && ( | ||||
|             <MenuItem onClick={() => handleOpenSetPasswordModal(selectedUsuarioRow!)}> | ||||
|                 <VpnKeyIcon fontSize="small" sx={{ mr: 1 }} /> Resetear Contraseña | ||||
|             </MenuItem> | ||||
|         )} | ||||
|          {/* No hay "Eliminar" directo, se usa el switch de Habilitado */} | ||||
|         {(!puedeModificar && !puedeAsignarPerfil && !puedeResetearClave) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||
|       </Menu> | ||||
|  | ||||
|       <UsuarioFormModal | ||||
|         open={usuarioModalOpen} onClose={handleCloseUsuarioModal} onSubmit={handleSubmitUsuarioModal} | ||||
|         initialData={editingUsuario} errorMessage={apiErrorMessage} | ||||
|         clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|       /> | ||||
|       {selectedUsuarioForPassword && ( | ||||
|         <SetPasswordModal | ||||
|             open={setPasswordModalOpen} | ||||
|             onClose={handleCloseSetPasswordModal} | ||||
|             onSubmit={handleSubmitSetPassword} | ||||
|             usuario={selectedUsuarioForPassword} | ||||
|             errorMessage={apiErrorMessage} | ||||
|             clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|         /> | ||||
|       )} | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarUsuariosPage; | ||||
							
								
								
									
										68
									
								
								Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; | ||||
| import { Outlet, useNavigate, useLocation } from 'react-router-dom'; | ||||
|  | ||||
| const usuariosSubModules = [ | ||||
|   { label: 'Perfiles', path: 'perfiles' }, | ||||
|   { label: 'Permisos (Definición)', path: 'permisos' }, | ||||
|   { label: 'Usuarios', path: 'gestion-usuarios' }, | ||||
| ]; | ||||
|  | ||||
| const UsuariosIndexPage: React.FC = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const location = useLocation(); | ||||
|   const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const currentBasePath = '/usuarios'; | ||||
|     const subPath = location.pathname.startsWith(currentBasePath + '/') | ||||
|                       ? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Tomar solo la primera parte de la subruta | ||||
|                       : (location.pathname === currentBasePath ? usuariosSubModules[0]?.path : undefined); | ||||
|  | ||||
|     const activeTabIndex = usuariosSubModules.findIndex( | ||||
|       (subModule) => subModule.path === subPath | ||||
|     ); | ||||
|  | ||||
|     if (activeTabIndex !== -1) { | ||||
|       setSelectedSubTab(activeTabIndex); | ||||
|     } else { | ||||
|        if (location.pathname === currentBasePath && usuariosSubModules.length > 0) { | ||||
|          navigate(usuariosSubModules[0].path, { replace: true }); | ||||
|          setSelectedSubTab(0); | ||||
|        } else { | ||||
|           setSelectedSubTab(false); | ||||
|        } | ||||
|     } | ||||
|   }, [location.pathname, navigate]); | ||||
|  | ||||
|   const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { | ||||
|     setSelectedSubTab(newValue); | ||||
|     navigate(usuariosSubModules[newValue].path); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box> | ||||
|       <Typography variant="h5" gutterBottom>Módulo de Usuarios y Seguridad</Typography> | ||||
|       <Paper square elevation={1}> | ||||
|         <Tabs | ||||
|           value={selectedSubTab} | ||||
|           onChange={handleSubTabChange} | ||||
|           indicatorColor="primary" | ||||
|           textColor="primary" | ||||
|           variant="scrollable" | ||||
|           scrollButtons="auto" | ||||
|           aria-label="sub-módulos de usuarios" | ||||
|         > | ||||
|           {usuariosSubModules.map((subModule) => ( | ||||
|             <Tab key={subModule.path} label={subModule.label} /> | ||||
|           ))} | ||||
|         </Tabs> | ||||
|       </Paper> | ||||
|       <Box sx={{ pt: 2 }}> | ||||
|         <Outlet /> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default UsuariosIndexPage; | ||||
		Reference in New Issue
	
	Block a user