Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// src/pages/distribucion/DistribucionIndexPage.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel, ListItemIcon, ListItemText // << AÑADIR ListItemIcon, ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNote'; // << AÑADIR IMPORTACIÓN DEL ICONO
|
||||
import { useNavigate } from 'react-router-dom'; // << AÑADIR IMPORTACIÓN DE useNavigate
|
||||
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
|
||||
@@ -31,17 +34,24 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25); // << CAMBIADO DE 5 a 25 (valor más común)
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedCanillitaRow, setSelectedCanillitaRow] = useState<CanillaDto | null>(null);
|
||||
|
||||
const navigate = useNavigate(); // << INICIALIZAR useNavigate
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CG001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CG002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CG003");
|
||||
// CG004 para Porcentajes/Montos, se gestionará por separado.
|
||||
const puedeDarBaja = isSuperAdmin || tienePermiso("CG005");
|
||||
// Permisos para Novedades
|
||||
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006"); // << DEFINIR PERMISO
|
||||
// Para la opción "Ver Novedades", podemos usar el permiso de ver canillitas (CG001)
|
||||
// O si solo se quiere mostrar si puede gestionarlas, usar puedeGestionarNovedades
|
||||
const puedeVerNovedadesCanilla = puedeVer || puedeGestionarNovedades; // << LÓGICA PARA MOSTRAR LA OPCIÓN
|
||||
|
||||
|
||||
const cargarCanillitas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
@@ -51,12 +61,12 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 25) : undefined;
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 10) : undefined; // << CORREGIDO: parseInt con base 10
|
||||
if (filtroLegajo && isNaN(legajoNum!)) {
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]); // Limpiar resultados si el filtro es inválido
|
||||
setLoading(false);
|
||||
return;
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = await canillaService.getAllCanillas(filtroNomApe, legajoNum, filtroSoloActivos);
|
||||
setCanillitas(data);
|
||||
@@ -83,6 +93,7 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
await canillaService.createCanilla(data as CreateCanillaDto);
|
||||
}
|
||||
cargarCanillitas();
|
||||
// No es necesario llamar a handleCloseModal aquí si el modal se cierra solo en éxito
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el canillita.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
@@ -93,17 +104,22 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
setApiErrorMessage(null);
|
||||
const accion = canillita.baja ? "reactivar" : "dar de baja";
|
||||
if (window.confirm(`¿Está seguro de que desea ${accion} a ${canillita.nomApe}?`)) {
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err:any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleOpenNovedades = (idCan: number) => {
|
||||
navigate(`/distribucion/canillas/${idCan}/novedades`);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, canillita: CanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedCanillitaRow(canillita);
|
||||
};
|
||||
@@ -118,98 +134,120 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert></Box>; // Mensaje más genérico
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Canillitas</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number" // Mantener como number para el input, la conversión se hace al usarlo
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos} // Default a true
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }} // Dar más espacio al nombre
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Ver Activos"
|
||||
sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
/>
|
||||
}
|
||||
label="Ver Activos" // Cambiado el label para más claridad
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button>
|
||||
)}
|
||||
)}
|
||||
</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>}
|
||||
{/* Mostrar error general si no hay error de API específico */}
|
||||
{error && !apiErrorMessage && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined"/> : <Chip label="No" color="default" size="small" variant="outlined"/>}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}>
|
||||
<MoreVertIcon />
|
||||
{!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true también aquí
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
{/* Mostrar acciones solo si tiene algún permiso para el menú */}
|
||||
{(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) ? 7 : 6} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined" /> : <Chip label="No" color="default" size="small" variant="outlined" />}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
{(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)}
|
||||
// Deshabilitar si NO tiene NINGUNO de los permisos para las acciones del menú
|
||||
disabled={!puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={canillitas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={canillitas.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(selectedCanillitaRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
{selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>}
|
||||
{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}
|
||||
</MenuItem>
|
||||
{/* Mostrar opción de Novedades si tiene permiso de ver canillitas o gestionar novedades */}
|
||||
{puedeVerNovedadesCanilla && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleOpenNovedades(selectedCanillitaRow.idCanilla)}>
|
||||
<ListItemIcon><EventNoteIcon /></ListItemIcon>
|
||||
<ListItemText>Novedades</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeModificar && selectedCanillitaRow && ( // Asegurar que selectedCanillitaRow existe
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
<ListItemIcon>{selectedCanillitaRow.baja ? <ToggleOnIcon /> : <ToggleOffIcon />}</ListItemIcon>
|
||||
<ListItemText>{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Mostrar "Sin acciones" si no hay ninguna acción permitida para la fila seleccionada */}
|
||||
{selectedCanillitaRow && !puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla && (
|
||||
<MenuItem disabled>Sin acciones</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeDarBaja) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<CanillaFormModal
|
||||
|
||||
@@ -146,8 +146,8 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
// 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>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Empresas</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, Checkbox, Tooltip,
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle,
|
||||
ToggleButtonGroup, ToggleButton
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import PrintIcon from '@mui/icons-material/Print';
|
||||
@@ -19,7 +20,7 @@ import canillaService from '../../services/Distribucion/canillaService';
|
||||
|
||||
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
||||
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
|
||||
|
||||
@@ -28,25 +29,32 @@ import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
|
||||
type TipoDestinatarioFiltro = 'canillitas' | 'accionistas';
|
||||
|
||||
const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true); // Para carga principal de movimientos
|
||||
const [error, setError] = useState<string | null>(null); // Error general o de carga
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para errores de modal/API
|
||||
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFecha, setFiltroFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>('');
|
||||
const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados');
|
||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||
const [filtroIdCanillitaSeleccionado, setFiltroIdCanillitaSeleccionado] = useState<number | string>('');
|
||||
const [filtroTipoDestinatario, setFiltroTipoDestinatario] = useState<TipoDestinatarioFiltro>('canillitas');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
|
||||
const [prefillModalData, setPrefillModalData] = useState<{
|
||||
fecha?: string;
|
||||
idCanilla?: number | string;
|
||||
nombreCanilla?: string; // << AÑADIDO PARA PASAR AL MODAL
|
||||
idPublicacion?: number | string;
|
||||
} | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
@@ -64,70 +72,123 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const puedeLiquidar = isSuperAdmin || tienePermiso("MC005");
|
||||
const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006");
|
||||
|
||||
// Función para formatear fechas YYYY-MM-DD a DD/MM/YYYY
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
const datePart = dateString.split('T')[0];
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
if (parts.length === 3) { return `${parts[2]}/${parts[1]}/${parts[0]}`; }
|
||||
return datePart;
|
||||
};
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [pubsData, canData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
canillaService.getAllCanillas(undefined, undefined, true)
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setCanillitas(canData);
|
||||
} catch (err) {
|
||||
console.error(err); setError("Error al cargar opciones de filtro.");
|
||||
} finally { setLoadingFiltersDropdown(false); }
|
||||
useEffect(() => {
|
||||
const fetchPublicaciones = async () => {
|
||||
setLoadingFiltersDropdown(true); // Mover al inicio de la carga de pubs
|
||||
try {
|
||||
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicaciones(pubsData);
|
||||
} catch (err) {
|
||||
console.error("Error cargando publicaciones para filtro:",err);
|
||||
setError("Error al cargar publicaciones."); // Usar error general
|
||||
} finally {
|
||||
// No poner setLoadingFiltersDropdown(false) aquí, esperar a que ambas cargas terminen
|
||||
}
|
||||
};
|
||||
fetchPublicaciones();
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
|
||||
const fetchDestinatariosParaDropdown = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true); // Poner al inicio de esta carga también
|
||||
setFiltroIdCanillitaSeleccionado('');
|
||||
setDestinatariosDropdown([]);
|
||||
setError(null); // Limpiar errores de carga de dropdowns previos
|
||||
try {
|
||||
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
|
||||
const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter);
|
||||
setDestinatariosDropdown(data);
|
||||
} catch (err) {
|
||||
console.error("Error cargando destinatarios para filtro:", err);
|
||||
setError("Error al cargar canillitas/accionistas."); // Usar error general
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false); // Poner al final de AMBAS cargas de dropdown
|
||||
}
|
||||
}, [filtroTipoDestinatario]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDestinatariosParaDropdown();
|
||||
}, [fetchDestinatariosParaDropdown]);
|
||||
|
||||
|
||||
const cargarMovimientos = useCallback(async () => {
|
||||
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
|
||||
if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; }
|
||||
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
let liquidadosFilter: boolean | null = null;
|
||||
let incluirNoLiquidadosFilter: boolean | null = true; // Por defecto mostrar no liquidados
|
||||
|
||||
if (filtroEstadoLiquidacion === 'liquidados') {
|
||||
liquidadosFilter = true;
|
||||
incluirNoLiquidadosFilter = false;
|
||||
} else if (filtroEstadoLiquidacion === 'noLiquidados') {
|
||||
liquidadosFilter = false;
|
||||
incluirNoLiquidadosFilter = true;
|
||||
} // Si es 'todos', ambos son null o true y false respectivamente (backend debe manejarlo)
|
||||
|
||||
|
||||
const params = {
|
||||
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
|
||||
fechaDesde: filtroFecha,
|
||||
fechaHasta: filtroFecha,
|
||||
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
|
||||
idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null,
|
||||
liquidados: liquidadosFilter,
|
||||
incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter,
|
||||
idCanilla: Number(filtroIdCanillitaSeleccionado),
|
||||
liquidados: null,
|
||||
incluirNoLiquidados: null,
|
||||
};
|
||||
const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params);
|
||||
setMovimientos(data);
|
||||
setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar
|
||||
setSelectedIdsParaLiquidar(new Set());
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar movimientos.');
|
||||
} finally { setLoading(false); }
|
||||
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]);
|
||||
console.error("Error al cargar movimientos:", err);
|
||||
setError('Error al cargar movimientos.');
|
||||
setMovimientos([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
||||
cargarMovimientos();
|
||||
} else {
|
||||
setMovimientos([]);
|
||||
if (loading) setLoading(false); // Asegurar que no se quede en loading si los filtros se limpian
|
||||
}
|
||||
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); // `cargarMovimientos` ya tiene sus dependencias
|
||||
|
||||
useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
|
||||
|
||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
if (!puedeCrear && !item) {
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
return;
|
||||
}
|
||||
if (item && !puedeModificar) {
|
||||
setApiErrorMessage("No tiene permiso para modificar movimientos.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
setEditingMovimiento(item);
|
||||
setPrefillModalData(null);
|
||||
} else {
|
||||
// --- CAMBIO: Obtener nombre del canillita seleccionado para prefill ---
|
||||
const canillitaSeleccionado = destinatariosDropdown.find(
|
||||
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
|
||||
);
|
||||
setEditingMovimiento(null);
|
||||
setPrefillModalData({
|
||||
fecha: filtroFecha,
|
||||
idCanilla: filtroIdCanillitaSeleccionado,
|
||||
nombreCanilla: canillitaSeleccionado?.nomApe, // << AÑADIR NOMBRE
|
||||
idPublicacion: filtroIdPublicacion
|
||||
});
|
||||
}
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// ... handleDelete, handleMenuOpen, handleMenuClose, handleSelectRowForLiquidar, handleSelectAllForLiquidar, handleOpenLiquidarDialog, handleCloseLiquidarDialog sin cambios ...
|
||||
const handleDelete = async (idParte: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
@@ -138,7 +199,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
|
||||
// Almacenar el idParte en el propio elemento del menú para referencia
|
||||
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedRow(item);
|
||||
@@ -154,7 +214,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
if (event.target.checked) {
|
||||
const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
|
||||
setSelectedIdsParaLiquidar(newSelectedIds);
|
||||
} else {
|
||||
@@ -170,74 +230,65 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
setOpenLiquidarDialog(true);
|
||||
};
|
||||
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
|
||||
|
||||
|
||||
const handleConfirmLiquidar = async () => {
|
||||
if (selectedIdsParaLiquidar.size === 0) {
|
||||
setApiErrorMessage("No hay movimientos seleccionados para liquidar.");
|
||||
return;
|
||||
}
|
||||
if (!fechaLiquidacionDialog) {
|
||||
setApiErrorMessage("Debe seleccionar una fecha de liquidación.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- VALIDACIÓN DE FECHA ---
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); // Usar Z para consistencia con formatDate si es necesario, o T00:00:00 para local
|
||||
|
||||
if (selectedIdsParaLiquidar.size === 0) { /* ... */ return; }
|
||||
if (!fechaLiquidacionDialog) { /* ... */ return; }
|
||||
// ... (validación de fecha sin cambios)
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
|
||||
let fechaMovimientoMasReciente: Date | null = null;
|
||||
|
||||
selectedIdsParaLiquidar.forEach(idParte => {
|
||||
const movimiento = movimientos.find(m => m.idParte === idParte);
|
||||
if (movimiento && movimiento.fecha) { // Asegurarse que movimiento.fecha existe
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); // Consistencia con Z
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
|
||||
if (movimiento && movimiento.fecha) {
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
setApiErrorMessage(null);
|
||||
setLoading(true); // Usar el loading general para la operación de liquidar
|
||||
|
||||
setLoading(true);
|
||||
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
|
||||
idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
|
||||
fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
|
||||
fechaLiquidacion: fechaLiquidacionDialog
|
||||
};
|
||||
|
||||
try {
|
||||
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
|
||||
setOpenLiquidarDialog(false);
|
||||
|
||||
setOpenLiquidarDialog(false);
|
||||
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
|
||||
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
|
||||
|
||||
await cargarMovimientos();
|
||||
await cargarMovimientos();
|
||||
|
||||
if (movimientoParaTicket) {
|
||||
console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla);
|
||||
// --- CAMBIO: NO IMPRIMIR TICKET SI ES ACCIONISTA ---
|
||||
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
||||
await handleImprimirTicketLiquidacion(
|
||||
movimientoParaTicket.idCanilla,
|
||||
fechaLiquidacionDialog,
|
||||
movimientoParaTicket.canillaEsAccionista
|
||||
fechaLiquidacionDialog,
|
||||
false // esAccionista = false
|
||||
);
|
||||
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
|
||||
} else {
|
||||
console.warn("No se pudo encontrar información del movimiento para generar el ticket post-liquidación.");
|
||||
console.warn("No se pudo encontrar información del movimiento para ticket post-liquidación.");
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.';
|
||||
setApiErrorMessage(msg);
|
||||
setApiErrorMessage(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Esta función se pasa al modal para que la invoque al hacer submit en MODO EDICIÓN
|
||||
const handleModalEditSubmit = async (data: UpdateEntradaSalidaCanillaDto, idParte: number) => {
|
||||
// ... (sin cambios)
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
|
||||
@@ -251,32 +302,21 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingMovimiento(null);
|
||||
// Recargar siempre que se cierre el modal y no haya un error pendiente a nivel de página
|
||||
// Opcionalmente, podrías tener una bandera ' cambiosGuardados' que el modal active
|
||||
// para ser más selectivo con la recarga.
|
||||
setPrefillModalData(null);
|
||||
if (!apiErrorMessage) {
|
||||
cargarMovimientos();
|
||||
}
|
||||
};
|
||||
|
||||
const handleImprimirTicketLiquidacion = useCallback(async (
|
||||
// Parámetros necesarios para el ticket
|
||||
idCanilla: number,
|
||||
fecha: string, // Fecha para la que se genera el ticket (probablemente fechaLiquidacionDialog)
|
||||
esAccionista: boolean
|
||||
idCanilla: number, fecha: string, esAccionista: boolean
|
||||
) => {
|
||||
// ... (sin cambios)
|
||||
setLoadingTicketPdf(true);
|
||||
setApiErrorMessage(null);
|
||||
|
||||
try {
|
||||
const params = {
|
||||
fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD
|
||||
idCanilla: idCanilla,
|
||||
esAccionista: esAccionista,
|
||||
};
|
||||
|
||||
const params = { fecha: fecha.split('T')[0], idCanilla, esAccionista };
|
||||
const blob = await reportesService.getTicketLiquidacionCanillaPdf(params);
|
||||
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar el ticket PDF.";
|
||||
@@ -287,16 +327,11 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
if (!w) alert("Permita popups para ver el PDF del ticket.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error al generar ticket de liquidación:", error);
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message
|
||||
? error.response.data.message
|
||||
: 'Ocurrió un error al generar el ticket.';
|
||||
console.error("Error al generar ticket:", error);
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message ? error.response.data.message : 'Error al generar ticket.';
|
||||
setApiErrorMessage(message);
|
||||
} finally {
|
||||
setLoadingTicketPdf(false);
|
||||
// No cerramos el menú aquí si se llama desde handleConfirmLiquidar
|
||||
}
|
||||
}, []); // Dependencias vacías si no usa nada del scope exterior que cambie, o añadir si es necesario
|
||||
} finally { setLoadingTicketPdf(false); }
|
||||
}, []);
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
@@ -305,47 +340,77 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { // Modificado para solo mostrar si no hay filtros y no puede ver
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
}
|
||||
|
||||
|
||||
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
|
||||
// Corregido: numNotLiquidatedOnPage debe calcularse sobre 'movimientos' filtrados, no solo 'displayData'
|
||||
// O, si la selección es solo por página, displayData está bien. Asumamos selección por página por ahora.
|
||||
const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas</Typography>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas & Accionistas</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
onChange={(e) => setFiltroFecha(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
|
||||
required
|
||||
error={!filtroFecha} // Se marca error si está vacío
|
||||
helperText={!filtroFecha ? "Fecha es obligatoria" : ""}
|
||||
/>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={filtroTipoDestinatario}
|
||||
exclusive
|
||||
onChange={(_, newValue: TipoDestinatarioFiltro | null) => {
|
||||
if (newValue !== null) {
|
||||
setFiltroTipoDestinatario(newValue);
|
||||
}
|
||||
}}
|
||||
aria-label="Tipo de Destinatario"
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="canillitas">Canillitas</ToggleButton>
|
||||
<ToggleButton value="accionistas">Accionistas</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingFiltersDropdown} required error={!filtroIdCanillitaSeleccionado}>
|
||||
<InputLabel>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</InputLabel>
|
||||
<Select
|
||||
value={filtroIdCanillitaSeleccionado}
|
||||
label={filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}
|
||||
onChange={(e) => setFiltroIdCanillitaSeleccionado(e.target.value as number | string)}
|
||||
>
|
||||
<MenuItem value=""><em>Seleccione uno</em></MenuItem>
|
||||
{destinatariosDropdown.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe} {c.legajo ? `(Leg: ${c.legajo})`: ''}</MenuItem>)}
|
||||
</Select>
|
||||
{!filtroIdCanillitaSeleccionado && <Typography component="p" color="error" variant="caption" sx={{ml:1.5, fontSize:'0.65rem'}}>Selección obligatoria</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Publicación</InputLabel>
|
||||
<Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
|
||||
<InputLabel>Publicación (Opcional)</InputLabel>
|
||||
<Select value={filtroIdPublicacion} label="Publicación (Opcional)" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Canillita</InputLabel>
|
||||
<Select value={filtroIdCanilla} label="Canillita" onChange={(e) => setFiltroIdCanilla(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{canillitas.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
|
||||
<InputLabel>Estado Liquidación</InputLabel>
|
||||
<Select value={filtroEstadoLiquidacion} label="Estado Liquidación" onChange={(e) => setFiltroEstadoLiquidacion(e.target.value as 'todos' | 'liquidados' | 'noLiquidados')}>
|
||||
<MenuItem value="noLiquidados">No Liquidados</MenuItem>
|
||||
<MenuItem value="liquidados">Liquidados</MenuItem>
|
||||
<MenuItem value="todos">Todos</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</Button>)}
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && numSelectedToLiquidate > 0 && (
|
||||
{/* --- CAMBIO: DESHABILITAR BOTÓN SI FILTROS OBLIGATORIOS NO ESTÁN --- */}
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado} // <<-- AÑADIDO
|
||||
>
|
||||
Registrar Movimiento
|
||||
</Button>
|
||||
)}
|
||||
{puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && (
|
||||
<Button variant="contained" color="success" startIcon={<PlaylistAddCheckIcon />} onClick={handleOpenLiquidarDialog}>
|
||||
Liquidar Seleccionados ({numSelectedToLiquidate})
|
||||
</Button>
|
||||
@@ -353,8 +418,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{!filtroFecha && <Alert severity="info" sx={{my:1}}>Por favor, seleccione una fecha.</Alert>}
|
||||
{filtroFecha && !filtroIdCanillitaSeleccionado && <Alert severity="info" sx={{my:1}}>Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.</Alert>}
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{/* Mostrar error general si no hay error de API específico y no está cargando filtros */}
|
||||
{error && !loading && !apiErrorMessage && !loadingFiltersDropdown && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{loadingTicketPdf &&
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}>
|
||||
@@ -364,12 +433,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
|
||||
// ... (Tabla y Paginación sin cambios)
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
{puedeLiquidar && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0}
|
||||
@@ -381,7 +451,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
)}
|
||||
<TableCell>Fecha</TableCell>
|
||||
<TableCell>Publicación</TableCell>
|
||||
<TableCell>Canillita</TableCell>
|
||||
<TableCell>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</TableCell>
|
||||
<TableCell align="right">Salida</TableCell>
|
||||
<TableCell align="right">Entrada</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
@@ -397,19 +467,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={
|
||||
(puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) +
|
||||
9 +
|
||||
((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
|
||||
(puedeLiquidar ? 1 : 0) + 9 + ((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
|
||||
}
|
||||
align="center"
|
||||
>
|
||||
No se encontraron movimientos.
|
||||
No se encontraron movimientos con los filtros aplicados.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayData.map((m) => (
|
||||
<TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
{puedeLiquidar && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={selectedIdsParaLiquidar.has(m.idParte)}
|
||||
@@ -440,8 +508,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, m)}
|
||||
data-rowid={m.idParte.toString()} // Guardar el id de la fila aquí
|
||||
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} // Lógica simplificada, refinar si es necesario
|
||||
data-rowid={m.idParte.toString()}
|
||||
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
@@ -462,19 +530,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && selectedRow && !selectedRow.liquidado && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
|
||||
{/* Opción de Imprimir Ticket Liq. */}
|
||||
{selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir)
|
||||
{/* --- CAMBIO: MOSTRAR REIMPRIMIR TICKET SIEMPRE SI ESTÁ LIQUIDADO --- */}
|
||||
{selectedRow && selectedRow.liquidado && puedeLiquidar && ( // Usar puedeLiquidar para consistencia
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior
|
||||
if (selectedRow) {
|
||||
handleImprimirTicketLiquidacion(
|
||||
selectedRow.idCanilla,
|
||||
selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento
|
||||
selectedRow.canillaEsAccionista
|
||||
selectedRow.fechaLiquidado || selectedRow.fecha,
|
||||
selectedRow.canillaEsAccionista // Pasar si es accionista
|
||||
);
|
||||
}
|
||||
// handleMenuClose() es llamado por handleImprimirTicketLiquidacion
|
||||
}}
|
||||
disabled={loadingTicketPdf}
|
||||
>
|
||||
@@ -483,13 +549,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
Reimprimir Ticket Liq.
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{selectedRow && ( // Opción de Eliminar
|
||||
{selectedRow && (
|
||||
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
|
||||
) && (
|
||||
<MenuItem onClick={() => {
|
||||
if (selectedRow) handleDelete(selectedRow.idParte);
|
||||
}}>
|
||||
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -498,13 +561,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<EntradaSalidaCanillaFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleModalEditSubmit}
|
||||
onSubmit={handleModalEditSubmit} // Este onSubmit es solo para edición
|
||||
initialData={editingMovimiento}
|
||||
prefillData={prefillModalData}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
{/* ... (Dialog de Liquidación sin cambios) ... */}
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
<DialogTitle>Confirmar Liquidación</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
@@ -523,7 +588,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -37,15 +37,16 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("OD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("OD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("OD004");
|
||||
|
||||
const cargarOtrosDestinos = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setError("No tiene permiso para ver los 'Otros Destinos'.");
|
||||
setLoading(false);
|
||||
setOtrosDestinos([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -131,8 +132,8 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Otros Destinos</Typography> {/* Cambiado h4 a h5 */}
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -241,7 +241,7 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={filtroSoloHabilitadas === undefined ? true : filtroSoloHabilitadas} onChange={(e) => setFiltroSoloHabilitadas(e.target.checked)} size="small" />}
|
||||
label="Solo Habilitadas"
|
||||
label="Ver Habilitadas"
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Publicación</Button>)}
|
||||
|
||||
@@ -37,13 +37,19 @@ const GestionarZonasPage: React.FC = () => {
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Ajustar códigos de permiso para Zonas
|
||||
const puedeVer = isSuperAdmin || tienePermiso("ZD001"); // Permiso para ver Zonas
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("ZD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("ZD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("ZD004");
|
||||
|
||||
|
||||
const cargarZonas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver las zonas.");
|
||||
setLoading(false);
|
||||
setZonas([]); // Asegurar que no se muestren datos previos
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -134,6 +140,17 @@ const GestionarZonasPage: React.FC = () => {
|
||||
// Adaptar para paginación
|
||||
const displayData = zonas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Gestionar Zonas
|
||||
</Typography>
|
||||
{/* El error de "sin permiso" ya fue seteado en cargarZonas */}
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
@@ -150,7 +167,6 @@ const GestionarZonasPage: React.FC = () => {
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
/>
|
||||
{/* <TextField label="Filtrar por Descripción" ... /> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
@@ -165,11 +181,11 @@ const GestionarZonasPage: React.FC = () => {
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{error && !loading && puedeVer && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
|
||||
{!loading && !error && (
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
||||
Reference in New Issue
Block a user