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:
		
							
								
								
									
										251
									
								
								Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,251 @@ | ||||
| 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 tipoBobinaService from '../../services/tipoBobinaService'; // Servicio específico | ||||
| import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; | ||||
| import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; | ||||
| import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto'; | ||||
| import TipoBobinaFormModal from '../../components/Modals/TipoBobinaFormModal'; // Modal específico | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarTiposBobinaPage: React.FC = () => { | ||||
|   const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [filtroDenominacion, setFiltroDenominacion] = useState(''); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingTipoBobina, setEditingTipoBobina] = useState<TipoBobinaDto | 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 [selectedTipoBobinaRow, setSelectedTipoBobinaRow] = useState<TipoBobinaDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|  | ||||
|   // Permisos específicos para Tipos de Bobina (IB006 a IB009) | ||||
|   const puedeVer = isSuperAdmin || tienePermiso("IB006"); | ||||
|   const puedeCrear = isSuperAdmin || tienePermiso("IB007"); | ||||
|   const puedeModificar = isSuperAdmin || tienePermiso("IB008"); | ||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("IB009"); | ||||
|  | ||||
|   const cargarTiposBobina = 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 tipoBobinaService.getAllTiposBobina(filtroDenominacion); | ||||
|       setTiposBobina(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       setError('Error al cargar los tipos de bobina.'); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }, [filtroDenominacion, puedeVer]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     cargarTiposBobina(); | ||||
|   }, [cargarTiposBobina]); | ||||
|  | ||||
|   const handleOpenModal = (tipoBobina?: TipoBobinaDto) => { | ||||
|     setEditingTipoBobina(tipoBobina || null); | ||||
|     setApiErrorMessage(null); | ||||
|     setModalOpen(true); | ||||
|   }; | ||||
|  | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); | ||||
|     setEditingTipoBobina(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreateTipoBobinaDto | (UpdateTipoBobinaDto & { idTipoBobina: number })) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingTipoBobina && 'idTipoBobina' in data) { | ||||
|         await tipoBobinaService.updateTipoBobina(editingTipoBobina.idTipoBobina, data); | ||||
|       } else { | ||||
|         await tipoBobinaService.createTipoBobina(data as CreateTipoBobinaDto); | ||||
|       } | ||||
|       cargarTiposBobina(); | ||||
|     } catch (err: any) { | ||||
|       console.error("Error en submit modal (padre - TiposBobina):", err); | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message | ||||
|         ? err.response.data.message | ||||
|         : 'Ocurrió un error inesperado al guardar el tipo de bobina.'; | ||||
|       setApiErrorMessage(message); | ||||
|       throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (id: number) => { | ||||
|     if (window.confirm(`¿Está seguro de que desea eliminar este tipo de bobina (ID: ${id})?`)) { | ||||
|       setApiErrorMessage(null); | ||||
|       try { | ||||
|         await tipoBobinaService.deleteTipoBobina(id); | ||||
|         cargarTiposBobina(); | ||||
|       } catch (err: any) { | ||||
|         console.error("Error al eliminar tipo de bobina:", err); | ||||
|         const message = axios.isAxiosError(err) && err.response?.data?.message | ||||
|           ? err.response.data.message | ||||
|           : 'Ocurrió un error inesperado al eliminar el tipo de bobina.'; | ||||
|         setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, tipoBobina: TipoBobinaDto) => { | ||||
|     setAnchorEl(event.currentTarget); | ||||
|     setSelectedTipoBobinaRow(tipoBobina); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); | ||||
|     setSelectedTipoBobinaRow(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 = tiposBobina.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|  | ||||
|   if (!loading && !puedeVer) { | ||||
|     return ( | ||||
|       <Box sx={{ p: 2 }}> | ||||
|         <Typography variant="h4" gutterBottom>Gestionar Tipos de Bobina</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 Tipos de Bobina | ||||
|       </Typography> | ||||
|  | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|         <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> | ||||
|           <TextField | ||||
|             label="Filtrar por Denominación" | ||||
|             variant="outlined" | ||||
|             size="small" | ||||
|             value={filtroDenominacion} | ||||
|             onChange={(e) => setFiltroDenominacion(e.target.value)} | ||||
|           /> | ||||
|           {/* <Button variant="contained" onClick={cargarTiposBobina}>Buscar</Button> */} | ||||
|         </Box> | ||||
|         {puedeCrear && ( | ||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               startIcon={<AddIcon />} | ||||
|               onClick={() => handleOpenModal()} | ||||
|               sx={{ mb: 2 }} | ||||
|             > | ||||
|               Agregar Nuevo Tipo | ||||
|             </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>Denominación</TableCell> | ||||
|                 {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} | ||||
|               </TableRow> | ||||
|             </TableHead> | ||||
|             <TableBody> | ||||
|               {displayData.length === 0 && !loading ? ( | ||||
|                 <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 2 : 1} align="center">No se encontraron tipos de bobina.</TableCell></TableRow> | ||||
|               ) : ( | ||||
|                 displayData.map((tipo) => ( | ||||
|                   <TableRow key={tipo.idTipoBobina}> | ||||
|                     <TableCell>{tipo.denominacion}</TableCell> | ||||
|                     {(puedeModificar || puedeEliminar) && ( | ||||
|                       <TableCell align="right"> | ||||
|                         <IconButton | ||||
|                           onClick={(e) => handleMenuOpen(e, tipo)} | ||||
|                           disabled={!puedeModificar && !puedeEliminar} | ||||
|                         > | ||||
|                           <MoreVertIcon /> | ||||
|                         </IconButton> | ||||
|                       </TableCell> | ||||
|                     )} | ||||
|                   </TableRow> | ||||
|                 )) | ||||
|               )} | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|           <TablePagination | ||||
|             rowsPerPageOptions={[5, 10, 25]} | ||||
|             component="div" | ||||
|             count={tiposBobina.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(selectedTipoBobinaRow!); handleMenuClose(); }}> | ||||
|             Modificar | ||||
|           </MenuItem> | ||||
|         )} | ||||
|         {puedeEliminar && ( | ||||
|           <MenuItem onClick={() => handleDelete(selectedTipoBobinaRow!.idTipoBobina)}> | ||||
|             Eliminar | ||||
|           </MenuItem> | ||||
|         )} | ||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||
|       </Menu> | ||||
|  | ||||
|       <TipoBobinaFormModal | ||||
|         open={modalOpen} | ||||
|         onClose={handleCloseModal} | ||||
|         onSubmit={handleSubmitModal} | ||||
|         initialData={editingTipoBobina} | ||||
|         errorMessage={apiErrorMessage} | ||||
|         clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|       /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarTiposBobinaPage; | ||||
		Reference in New Issue
	
	Block a user