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:
		
							
								
								
									
										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; | ||||
		Reference in New Issue
	
	Block a user