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.
This commit is contained in:
		| @@ -1,7 +1,7 @@ | ||||
| // src/pages/distribucion/DistribucionIndexPage.tsx | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; | ||||
| import { Outlet, useNavigate, useLocation, Link as RouterLink } from 'react-router-dom'; | ||||
| import { Outlet, useNavigate, useLocation } from 'react-router-dom'; | ||||
|  | ||||
| // Define las sub-pestañas del módulo Distribución | ||||
| // El path es relativo a la ruta base del módulo (ej: /distribucion) | ||||
|   | ||||
| @@ -1,7 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { Typography } from '@mui/material'; | ||||
|  | ||||
| const EmpresasPage: React.FC = () => { | ||||
|   return <Typography variant="h6">Página de Gestión de Empresas</Typography>; | ||||
| }; | ||||
| export default EmpresasPage; | ||||
							
								
								
									
										276
									
								
								Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,276 @@ | ||||
| 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; | ||||
							
								
								
									
										244
									
								
								Frontend/src/pages/Distribucion/GestionarZonasPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								Frontend/src/pages/Distribucion/GestionarZonasPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,244 @@ | ||||
| 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 zonaService from '../../services/zonaService'; // Servicio de Zonas | ||||
| import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas | ||||
| import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create | ||||
| import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto'; // DTOs Update | ||||
| import ZonaFormModal from '../../components/Modals/ZonaFormModal'; // Modal de Zonas | ||||
| import { usePermissions } from '../../hooks/usePermissions'; // Hook de permisos | ||||
| import axios from 'axios'; // Para manejo de errores | ||||
|  | ||||
| const GestionarZonasPage: React.FC = () => { | ||||
|   const [zonas, setZonas] = useState<ZonaDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [filtroNombre, setFiltroNombre] = useState(''); | ||||
|   // const [filtroDescripcion, setFiltroDescripcion] = useState(''); // Si añades filtro por descripción | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingZona, setEditingZona] = useState<ZonaDto | null>(null); // Usar ZonaDto aquí también | ||||
|   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 [selectedZonaRow, setSelectedZonaRow] = useState<ZonaDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|  | ||||
|   // Ajustar códigos de permiso para Zonas | ||||
|   const puedeCrear = isSuperAdmin || tienePermiso("ZD002"); | ||||
|   const puedeModificar = isSuperAdmin || tienePermiso("ZD003"); | ||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("ZD004"); | ||||
|  | ||||
|  | ||||
|   const cargarZonas = useCallback(async () => { | ||||
|     setLoading(true); | ||||
|     setError(null); | ||||
|     try { | ||||
|       // Usar servicio de zonas y filtros | ||||
|       const data = await zonaService.getAllZonas(filtroNombre/*, filtroDescripcion*/); | ||||
|       setZonas(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       setError('Error al cargar las zonas.'); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }, [filtroNombre/*, filtroDescripcion*/]); // Añadir dependencias de filtro | ||||
|  | ||||
|   useEffect(() => { | ||||
|     cargarZonas(); | ||||
|   }, [cargarZonas]); | ||||
|  | ||||
|   const handleOpenModal = (zona?: ZonaDto) => { | ||||
|     setEditingZona(zona || null); | ||||
|     setApiErrorMessage(null); | ||||
|     setModalOpen(true); | ||||
|   }; | ||||
|  | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); | ||||
|     setEditingZona(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreateZonaDto | UpdateZonaDto) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingZona) { // Es Update | ||||
|         await zonaService.updateZona(editingZona.idZona, data as UpdateZonaDto); | ||||
|       } else { // Es Create | ||||
|         await zonaService.createZona(data as CreateZonaDto); | ||||
|       } | ||||
|       cargarZonas(); // Recargar lista | ||||
|     } catch (err: any) { | ||||
|       console.error("Error en submit modal (padre):", err); | ||||
|       if (axios.isAxiosError(err) && err.response) { | ||||
|         setApiErrorMessage(err.response.data?.message || 'Error al guardar la zona.'); | ||||
|       } else { | ||||
|         setApiErrorMessage('Ocurrió un error inesperado al guardar la zona.'); | ||||
|       } | ||||
|       throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   const handleDelete = async (id: number) => { | ||||
|     if (window.confirm('¿Está seguro de que desea eliminar esta zona? (Se marcará como inactiva)')) { | ||||
|       setApiErrorMessage(null); | ||||
|       try { | ||||
|         await zonaService.deleteZona(id); // Llama al soft delete | ||||
|         cargarZonas(); // Recarga la lista (la zona eliminada ya no debería aparecer si el filtro es solo activas) | ||||
|       } catch (err: any) { | ||||
|         console.error("Error al eliminar zona:", err); | ||||
|         if (axios.isAxiosError(err) && err.response) { | ||||
|           setApiErrorMessage(err.response.data?.message || 'Error al eliminar.'); | ||||
|         } else { | ||||
|           setApiErrorMessage('Ocurrió un error inesperado al eliminar.'); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, zona: ZonaDto) => { | ||||
|     setAnchorEl(event.currentTarget); | ||||
|     setSelectedZonaRow(zona); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); | ||||
|     setSelectedZonaRow(null); | ||||
|   }; | ||||
|  | ||||
|   const handleChangePage = (_event: unknown, newPage: number) => { | ||||
|     setPage(newPage); | ||||
|   }; | ||||
|  | ||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     setRowsPerPage(parseInt(event.target.value, 10)); | ||||
|     setPage(0); | ||||
|   }; | ||||
|  | ||||
|   // Adaptar para paginación | ||||
|   const displayData = zonas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|  | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Typography variant="h4" gutterBottom> | ||||
|         Gestionar Zonas | ||||
|       </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)} | ||||
|           /> | ||||
|           {/* <TextField label="Filtrar por Descripción" ... /> */} | ||||
|         </Box> | ||||
|         {puedeCrear && ( | ||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               startIcon={<AddIcon />} | ||||
|               onClick={() => handleOpenModal()} | ||||
|               sx={{ mb: 2 }} | ||||
|             > | ||||
|               Agregar Nueva Zona | ||||
|             </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</TableCell> | ||||
|                 <TableCell>Descripción</TableCell> | ||||
|                 <TableCell align="right">Acciones</TableCell> | ||||
|               </TableRow> | ||||
|             </TableHead> | ||||
|             <TableBody> | ||||
|               {displayData.length === 0 && !loading ? ( | ||||
|                 <TableRow><TableCell colSpan={3} align="center">No se encontraron zonas.</TableCell></TableRow> | ||||
|               ) : ( | ||||
|                 displayData.map((zona) => ( | ||||
|                   <TableRow key={zona.idZona}> | ||||
|                     <TableCell>{zona.nombre}</TableCell> | ||||
|                     <TableCell>{zona.descripcion || '-'}</TableCell> | ||||
|                     <TableCell align="right"> | ||||
|                       <IconButton | ||||
|                         onClick={(e) => handleMenuOpen(e, zona)} | ||||
|                         disabled={!puedeModificar && !puedeEliminar} | ||||
|                       > | ||||
|                         <MoreVertIcon /> | ||||
|                       </IconButton> | ||||
|                     </TableCell> | ||||
|                   </TableRow> | ||||
|                 )) | ||||
|               )} | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|           <TablePagination | ||||
|             rowsPerPageOptions={[5, 10, 25]} | ||||
|             component="div" | ||||
|             count={zonas.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(selectedZonaRow!); handleMenuClose(); }}> | ||||
|             Modificar | ||||
|           </MenuItem> | ||||
|         )} | ||||
|         {puedeEliminar && ( | ||||
|           <MenuItem onClick={() => handleDelete(selectedZonaRow!.idZona)}> | ||||
|             Eliminar | ||||
|           </MenuItem> | ||||
|         )} | ||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||
|       </Menu> | ||||
|  | ||||
|       <ZonaFormModal | ||||
|         open={modalOpen} | ||||
|         onClose={handleCloseModal} | ||||
|         onSubmit={handleSubmitModal} | ||||
|         initialData={editingZona} | ||||
|         errorMessage={apiErrorMessage} | ||||
|         clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|       /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarZonasPage; | ||||
| @@ -1,7 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { Typography } from '@mui/material'; | ||||
|  | ||||
| const ZonasPage: React.FC = () => { | ||||
|   return <Typography variant="h6">Página de Gestión de Zonas</Typography>; | ||||
| }; | ||||
| export default ZonasPage; | ||||
		Reference in New Issue
	
	Block a user