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:
2025-05-20 12:38:55 -03:00
parent daf84d2708
commit b6ba52f074
228 changed files with 10745 additions and 178 deletions

View 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;

View File

@@ -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;

View 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;

View 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;

View 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;

View 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;