244 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			244 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | 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; |