Files
GestionIntegralWeb/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx

276 lines
12 KiB
TypeScript
Raw Normal View History

feat: Implementación módulos Empresas, Plantas, Tipos y Estados Bobina Backend API: - Implementado CRUD completo para Empresas (DE001-DE004): - EmpresaRepository, EmpresaService, EmpresasController. - Lógica de creación/eliminación de saldos iniciales en EmpresaService. - Transacciones y registro en tablas _H. - Verificación de permisos específicos. - Implementado CRUD completo para Plantas de Impresión (IP001-IP004): - PlantaRepository, PlantaService, PlantasController. - Transacciones y registro en tablas _H. - Verificación de permisos. - Implementado CRUD completo para Tipos de Bobina (IB006-IB009): - TipoBobinaRepository, TipoBobinaService, TiposBobinaController. - Transacciones y registro en tablas _H. - Verificación de permisos. - Implementado CRUD completo para Estados de Bobina (IB010-IB013): - EstadoBobinaRepository, EstadoBobinaService, EstadosBobinaController. - Transacciones y registro en tablas _H. - Verificación de permisos. Frontend React: - Módulo Empresas: - empresaService.ts para interactuar con la API. - EmpresaFormModal.tsx para crear/editar empresas. - GestionarEmpresasPage.tsx con tabla, filtro, paginación y menú de acciones. - Integración con el hook usePermissions para control de acceso. - Módulo Plantas de Impresión: - plantaService.ts. - PlantaFormModal.tsx. - GestionarPlantasPage.tsx con tabla, filtro, paginación y acciones. - Integración con usePermissions. - Módulo Tipos de Bobina: - tipoBobinaService.ts. - TipoBobinaFormModal.tsx. - GestionarTiposBobinaPage.tsx con tabla, filtro, paginación y acciones. - Integración con usePermissions. - Módulo Estados de Bobina: - estadoBobinaService.ts. - EstadoBobinaFormModal.tsx. - GestionarEstadosBobinaPage.tsx con tabla, filtro, paginación y acciones. - Integración con usePermissions. - Navegación: - Añadidas sub-pestañas y rutas para los nuevos módulos dentro de "Distribución" (Empresas) e "Impresión" (Plantas, Tipos Bobina, Estados Bobina). - Creado ImpresionIndexPage.tsx para la navegación interna del módulo de Impresión. Correcciones: - Corregido el uso de CommitAsync/RollbackAsync a Commit/Rollback síncronos en PlantaService.cs debido a que IDbTransaction no los soporta asíncronamente.
2025-05-09 10:08:53 -03:00
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 empresaService from '../../services/empresaService'; // Importar el servicio de Empresas
import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto';
import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto';
import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto';
import EmpresaFormModal from '../../components/Modals/EmpresaFormModal'; // Importar el modal de Empresas
import { usePermissions } from '../../hooks/usePermissions'; // Importar hook de permisos
import axios from 'axios'; // Para manejo de errores de API
const GestionarEmpresasPage: React.FC = () => {
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); // Para errores al cargar datos
const [filtroNombre, setFiltroNombre] = useState('');
// const [filtroDetalle, setFiltroDetalle] = useState(''); // Descomentar si añades filtro por detalle
const [modalOpen, setModalOpen] = useState(false);
const [editingEmpresa, setEditingEmpresa] = useState<EmpresaDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para errores del modal (Create/Update/Delete)
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
// Para el menú contextual de acciones por fila
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedEmpresaRow, setSelectedEmpresaRow] = useState<EmpresaDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions(); // Usar el hook
// Determinar permisos específicos para Empresas (basado en los códigos DE001 a DE004)
const puedeVer = isSuperAdmin || tienePermiso("DE001"); // Necesario para mostrar la página
const puedeCrear = isSuperAdmin || tienePermiso("DE002");
const puedeModificar = isSuperAdmin || tienePermiso("DE003");
const puedeEliminar = isSuperAdmin || tienePermiso("DE004");
const cargarEmpresas = useCallback(async () => {
if (!puedeVer) { // Si no tiene permiso de ver, no cargar nada
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true);
setError(null);
setApiErrorMessage(null); // Limpiar errores API al recargar
try {
const data = await empresaService.getAllEmpresas(filtroNombre/*, filtroDetalle*/);
setEmpresas(data);
} catch (err) {
console.error(err);
setError('Error al cargar las empresas.');
} finally {
setLoading(false);
}
}, [filtroNombre, puedeVer /*, filtroDetalle*/]); // Añadir puedeVer como dependencia
useEffect(() => {
cargarEmpresas();
}, [cargarEmpresas]); // Ejecutar al montar y cuando cambien las dependencias de cargarEmpresas
const handleOpenModal = (empresa?: EmpresaDto) => {
setEditingEmpresa(empresa || null);
setApiErrorMessage(null); // Limpiar error API antes de abrir modal
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false);
setEditingEmpresa(null);
// No limpiamos apiErrorMessage aquí, se limpia al intentar un nuevo submit o al recargar
};
const handleSubmitModal = async (data: CreateEmpresaDto | (UpdateEmpresaDto & { idEmpresa: number })) => {
setApiErrorMessage(null); // Limpiar error previo
// No necesitamos setLoading aquí, el modal lo maneja
try {
if (editingEmpresa && 'idEmpresa' in data) { // Es Update (verificamos si initialData existe Y data tiene id)
await empresaService.updateEmpresa(editingEmpresa.idEmpresa, data);
} else { // Es Create
await empresaService.createEmpresa(data as CreateEmpresaDto);
}
cargarEmpresas(); // Recargar lista en éxito
// handleCloseModal(); // El modal se cierra solo desde su propio onSubmit exitoso
} catch (err: any) {
console.error("Error en submit modal (padre):", err);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al guardar la empresa.';
setApiErrorMessage(message);
throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre
}
// No poner finally setLoading(false) aquí, el modal lo controla
};
const handleDelete = async (id: number) => {
// Opcional: mostrar un mensaje de confirmación más detallado
if (window.confirm(`¿Está seguro de que desea eliminar esta empresa (ID: ${id})? Esta acción también eliminará los saldos asociados.`)) {
setApiErrorMessage(null); // Limpiar errores previos
try {
await empresaService.deleteEmpresa(id);
cargarEmpresas(); // Recargar la lista para reflejar la eliminación
} catch (err: any) {
console.error("Error al eliminar empresa:", err);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al eliminar la empresa.';
setApiErrorMessage(message); // Mostrar error de API
}
}
handleMenuClose(); // Cerrar el menú de acciones
};
// --- Handlers para el menú y paginación (sin cambios respecto a Zonas/TiposPago) ---
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, empresa: EmpresaDto) => {
setAnchorEl(event.currentTarget);
setSelectedEmpresaRow(empresa);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedEmpresaRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
// Datos a mostrar en la tabla actual según paginación
const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
// Si no tiene permiso para ver, mostrar mensaje y salir
if (!loading && !puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Empresas</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 Empresas
</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)}
// Puedes añadir un botón de buscar explícito o dejar que filtre al escribir
/>
</Box>
{/* Mostrar botón de agregar solo si tiene permiso */}
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenModal()}
>
Agregar Nueva Empresa
</Button>
</Box>
)}
</Paper>
{/* Indicador de carga */}
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{/* Mensaje de error al cargar datos */}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{/* Mensaje de error de la API (modal/delete) */}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{/* Tabla de datos (solo si no está cargando y no hubo error de carga inicial) */}
{!loading && !error && (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre</TableCell>
<TableCell>Detalle</TableCell>
{/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */}
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron empresas.</TableCell></TableRow>
) : (
displayData.map((emp) => (
<TableRow key={emp.idEmpresa}>
<TableCell>{emp.nombre}</TableCell>
<TableCell>{emp.detalle || '-'}</TableCell>
{/* Mostrar botón de acciones solo si tiene permiso */}
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton
onClick={(e) => handleMenuOpen(e, emp)}
// Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro)
disabled={!puedeModificar && !puedeEliminar}
>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
{/* Paginación */}
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={empresas.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
{/* Menú contextual para acciones de fila */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
{/* Mostrar opción Modificar solo si tiene permiso */}
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}>
Modificar
</MenuItem>
)}
{/* Mostrar opción Eliminar solo si tiene permiso */}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedEmpresaRow!.idEmpresa)}>
Eliminar
</MenuItem>
)}
{/* Mensaje si no hay acciones disponibles (por si acaso) */}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>
{/* Modal para Crear/Editar */}
<EmpresaFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
initialData={editingEmpresa}
errorMessage={apiErrorMessage} // Pasar el mensaje de error de la API al modal
clearErrorMessage={() => setApiErrorMessage(null)} // Pasar función para limpiar el error
/>
</Box>
);
};
export default GestionarEmpresasPage;