Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
// src/pages/Distribucion/GestionarNovedadesCanillaPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||
CircularProgress, Alert, TextField, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
import novedadCanillaService from '../../services/Distribucion/novedadCanillaService';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { NovedadCanillaDto } from '../../models/dtos/Distribucion/NovedadCanillaDto';
|
||||
import type { CreateNovedadCanillaDto } from '../../models/dtos/Distribucion/CreateNovedadCanillaDto';
|
||||
import type { UpdateNovedadCanillaDto } from '../../models/dtos/Distribucion/UpdateNovedadCanillaDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import NovedadCanillaFormModal from '../../components/Modals/Distribucion/NovedadCanillaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarNovedadesCanillaPage: React.FC = () => {
|
||||
const { idCanilla: idCanillaStr } = useParams<{ idCanilla: string }>();
|
||||
const navigate = useNavigate();
|
||||
const idCanilla = Number(idCanillaStr);
|
||||
|
||||
const [canillita, setCanillita] = useState<CanillaDto | null>(null);
|
||||
const [novedades, setNovedades] = useState<NovedadCanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [errorPage, setErrorPage] = useState<string | null>(null); // Error general de la página
|
||||
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingNovedad, setEditingNovedad] = useState<NovedadCanillaDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para modal/delete
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedNovedadRow, setSelectedNovedadRow] = useState<NovedadCanillaDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006");
|
||||
const puedeVerCanillitas = isSuperAdmin || tienePermiso("CG001");
|
||||
|
||||
// Cargar datos del canillita (solo una vez o si idCanilla cambia)
|
||||
useEffect(() => {
|
||||
if (isNaN(idCanilla)) {
|
||||
setErrorPage("ID de Canillita inválido.");
|
||||
setLoading(false); // Detener carga principal
|
||||
return;
|
||||
}
|
||||
if (!puedeVerCanillitas && !puedeGestionarNovedades) {
|
||||
setErrorPage("No tiene permiso para acceder a esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); // Iniciar carga para datos del canillita
|
||||
const fetchCanillita = async () => {
|
||||
try {
|
||||
if (puedeVerCanillitas) {
|
||||
const canData = await canillaService.getCanillaById(idCanilla);
|
||||
setCanillita(canData);
|
||||
} else {
|
||||
// Si no puede ver detalles del canillita pero sí novedades, al menos mostrar ID
|
||||
setCanillita({ idCanilla, nomApe: `ID ${idCanilla}` } as CanillaDto);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error cargando datos del canillita:", err);
|
||||
setErrorPage(`Error al cargar datos del canillita (ID: ${idCanilla}).`);
|
||||
}
|
||||
// No ponemos setLoading(false) aquí, porque la carga de novedades sigue.
|
||||
};
|
||||
fetchCanillita();
|
||||
}, [idCanilla, puedeVerCanillitas, puedeGestionarNovedades]);
|
||||
|
||||
|
||||
// Cargar/filtrar novedades
|
||||
const cargarNovedades = useCallback(async () => {
|
||||
if (isNaN(idCanilla) || (!puedeGestionarNovedades && !puedeVerCanillitas)) {
|
||||
// Los permisos ya se validaron en el useEffect anterior, pero es bueno tenerlo
|
||||
return;
|
||||
}
|
||||
// Si ya está cargando los datos del canillita, no iniciar otra carga paralela
|
||||
// Se usará el mismo 'loading' para ambas operaciones iniciales.
|
||||
// if (!loading) setLoading(true); // No es necesario si el useEffect anterior ya lo hizo
|
||||
|
||||
setApiErrorMessage(null); // Limpiar errores de API de acciones previas
|
||||
// setErrorPage(null); // No limpiar error de página aquí, podría ser por el canillita
|
||||
|
||||
try {
|
||||
const params = {
|
||||
fechaDesde: filtroFechaDesde || null,
|
||||
fechaHasta: filtroFechaHasta || null,
|
||||
};
|
||||
const dataNovedades = await novedadCanillaService.getNovedadesPorCanilla(idCanilla, params);
|
||||
setNovedades(dataNovedades);
|
||||
// Si no hay datos con filtros, no es un error de API, simplemente no hay datos.
|
||||
// El mensaje de "no hay novedades" se maneja en la tabla.
|
||||
} catch (err: any) {
|
||||
console.error("Error al cargar/filtrar novedades:", err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al cargar las novedades.';
|
||||
setErrorPage(message); // Usar el error de página para problemas de carga de novedades
|
||||
setNovedades([]); // Limpiar en caso de error
|
||||
} finally {
|
||||
// Solo poner setLoading(false) después de que AMBAS cargas (canillita y novedades) se intenten.
|
||||
// Como se llaman en secuencia implícita por los useEffect, el último setLoading(false) es el de novedades.
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idCanilla, puedeGestionarNovedades, puedeVerCanillitas, filtroFechaDesde, filtroFechaHasta]);
|
||||
|
||||
// useEffect para cargar novedades cuando los filtros o el canillita (o permisos) cambian
|
||||
useEffect(() => {
|
||||
// Solo cargar si tenemos un idCanilla válido y permisos
|
||||
if (!isNaN(idCanilla) && (puedeGestionarNovedades || puedeVerCanillitas)) {
|
||||
cargarNovedades();
|
||||
} else if (isNaN(idCanilla)){
|
||||
setErrorPage("ID de Canillita inválido.");
|
||||
setLoading(false);
|
||||
} else if (!puedeGestionarNovedades && !puedeVerCanillitas) {
|
||||
setErrorPage("No tiene permiso para acceder a esta sección.");
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idCanilla, cargarNovedades, puedeGestionarNovedades, puedeVerCanillitas]); // `cargarNovedades` ya tiene sus dependencias
|
||||
|
||||
|
||||
const handleOpenModal = (item?: NovedadCanillaDto) => {
|
||||
if (!puedeGestionarNovedades) {
|
||||
setApiErrorMessage("No tiene permiso para agregar o editar novedades.");
|
||||
return;
|
||||
}
|
||||
setEditingNovedad(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingNovedad(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => {
|
||||
if (!puedeGestionarNovedades) return;
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingNovedad && idNovedad) {
|
||||
await novedadCanillaService.updateNovedad(idNovedad, data as UpdateNovedadCanillaDto);
|
||||
} else {
|
||||
await novedadCanillaService.createNovedad(data as CreateNovedadCanillaDto);
|
||||
}
|
||||
cargarNovedades(); // Recargar lista de novedades
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la novedad.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idNovedadDelRow: number) => {
|
||||
if (!puedeGestionarNovedades) return;
|
||||
if (window.confirm(`¿Seguro de eliminar esta novedad (ID: ${idNovedadDelRow})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await novedadCanillaService.deleteNovedad(idNovedadDelRow);
|
||||
cargarNovedades(); // Recargar lista de novedades
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la novedad.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: NovedadCanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedNovedadRow(item);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedNovedadRow(null);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-';
|
||||
|
||||
|
||||
if (loading && !canillita) { // Muestra cargando solo si aún no tenemos los datos del canillita
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (errorPage && !canillita) { // Si hay un error al cargar el canillita, no mostrar nada más
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{errorPage}</Alert>;
|
||||
}
|
||||
|
||||
// Si no tiene permiso para la sección en general
|
||||
if (!puedeGestionarNovedades && !puedeVerCanillitas) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/canillas`)} sx={{ mb: 2 }}>
|
||||
Volver a Canillitas
|
||||
</Button>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Novedades de: {canillita?.nomApe || `Canillita ID ${idCanilla}`}
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2}}>
|
||||
{puedeGestionarNovedades && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: {xs: 2, sm:0} }}>
|
||||
Agregar Novedad
|
||||
</Button>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<FilterListIcon sx={{color: 'action.active', alignSelf:'center'}} />
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde}
|
||||
onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }}
|
||||
disabled={loading} // Deshabilitar durante cualquier carga
|
||||
/>
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta}
|
||||
onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }}
|
||||
disabled={loading} // Deshabilitar durante cualquier carga
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Mostrar error de API (de submit/delete) o error de carga de novedades */}
|
||||
{(apiErrorMessage || (errorPage && novedades.length === 0 && !loading)) && (
|
||||
<Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage || errorPage}</Alert>
|
||||
)}
|
||||
|
||||
{loading && <Box sx={{display:'flex', justifyContent:'center', my:2}}><CircularProgress size={30} /></Box>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Fecha</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold', width: '70%' }}>Detalle de Novedad</TableCell>
|
||||
{puedeGestionarNovedades && <TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{novedades.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={puedeGestionarNovedades ? 3 : 2} align="center">
|
||||
No hay novedades registradas { (filtroFechaDesde || filtroFechaHasta) && "con los filtros aplicados"}.
|
||||
</TableCell></TableRow>
|
||||
) : (
|
||||
novedades.map((nov) => (
|
||||
<TableRow key={nov.idNovedad} hover>
|
||||
<TableCell>{formatDate(nov.fecha)}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={nov.detalle || ''} arrow>
|
||||
<Typography variant="body2" sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '500px'
|
||||
}}>
|
||||
{nov.detalle || '-'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{puedeGestionarNovedades && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, nov)} disabled={!puedeGestionarNovedades}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeGestionarNovedades && selectedNovedadRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedNovedadRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar</MenuItem>)}
|
||||
{puedeGestionarNovedades && selectedNovedadRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedNovedadRow.idNovedad)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
{idCanilla &&
|
||||
<NovedadCanillaFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
idCanilla={idCanilla}
|
||||
nombreCanilla={canillita?.nomApe}
|
||||
initialData={editingNovedad}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarNovedadesCanillaPage;
|
||||
Reference in New Issue
Block a user