Finalización de Reportes y arreglos varios de controles y comportamientos...
This commit is contained in:
@@ -8,6 +8,7 @@ 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 canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
|
||||
@@ -121,8 +122,8 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Canillitas</Typography>
|
||||
<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
|
||||
@@ -156,9 +157,7 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
@@ -203,7 +202,7 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{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}}/>}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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, FormControl, InputLabel, Select, Tooltip
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
@@ -11,7 +11,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
import controlDevolucionesService from '../../services/Distribucion/controlDevolucionesService';
|
||||
import empresaService from '../../services/Distribucion/empresaService'; // Para el filtro de empresa
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto';
|
||||
import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
|
||||
@@ -28,9 +28,8 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
@@ -45,22 +44,35 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
const [selectedRow, setSelectedRow] = useState<ControlDevolucionesDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
// Permisos CD001 (Ver), CD002 (Crear), CD003 (Modificar)
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CD001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); // Asumiendo que modificar incluye eliminar
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("CD003");
|
||||
|
||||
// CORREGIDO: Función para formatear la fecha
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
// Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..."
|
||||
const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
// parts[0] = YYYY, parts[1] = MM, parts[2] = DD
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY
|
||||
}
|
||||
return datePart; // Fallback si el formato no es el esperado
|
||||
};
|
||||
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const empresasData = await empresaService.getAllEmpresas();
|
||||
setEmpresas(empresasData);
|
||||
const empresasData = await empresaService.getAllEmpresas();
|
||||
setEmpresas(empresasData);
|
||||
} catch (err) {
|
||||
console.error("Error cargando empresas para filtro:", err);
|
||||
setError("Error al cargar opciones de filtro.");
|
||||
console.error("Error cargando empresas para filtro:", err);
|
||||
setError("Error al cargar opciones de filtro.");
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false);
|
||||
setLoadingFiltersDropdown(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -110,13 +122,13 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
|
||||
const handleDelete = async (idControl: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await controlDevolucionesService.deleteControlDevoluciones(idControl);
|
||||
cargarControles();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setApiErrorMessage(message);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
@@ -131,81 +143,81 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
setRowsPerPage(parseInt(event.target.value, 25)); setPage(0);
|
||||
};
|
||||
// displayData ahora usará la 'controles' directamente, el formato se aplica en el renderizado
|
||||
const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Control de Devoluciones a Empresa</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</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}}/>
|
||||
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Empresa</InputLabel>
|
||||
<Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)}
|
||||
<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 }} />
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Empresa</InputLabel>
|
||||
<Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</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>}
|
||||
{error && !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>Fecha</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell align="right">Entrada (Total Dev.)</TableCell>
|
||||
<TableCell align="right">Sobrantes</TableCell>
|
||||
<TableCell align="right">Sin Cargo</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idControl} hover>
|
||||
<TableCell>{formatDate(c.fecha)}</TableCell>
|
||||
<TableCell>{c.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right">{c.entrada}</TableCell>
|
||||
<TableCell align="right">{c.sobrantes}</TableCell>
|
||||
<TableCell align="right">{c.sinCargo}</TableCell>
|
||||
<TableCell><Tooltip title={c.detalle || ''}><Box sx={{maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{c.detalle || '-'}</Box></Tooltip></TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50]} component="div" count={controles.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> {/* Ajusta maxHeight según sea necesario */}
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell align="right">Entrada (Por Remito)</TableCell>
|
||||
<TableCell align="right">Sobrantes</TableCell>
|
||||
<TableCell align="right">Ejemplares Sin Cargo</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idControl} hover>
|
||||
<TableCell>{formatDate(c.fecha)}</TableCell>
|
||||
<TableCell>{c.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right">{c.entrada}</TableCell>
|
||||
<TableCell align="right">{c.sobrantes}</TableCell>
|
||||
<TableCell align="right">{c.sinCargo}</TableCell>
|
||||
<TableCell><Tooltip title={c.detalle || ''}><Box sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.detalle || '-'}</Box></Tooltip></TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50]} component="div" count={controles.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && selectedRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
{puedeEliminar && selectedRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
<ControlDevolucionesFormModal
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import TrashIcon from '@mui/icons-material/Delete';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
@@ -110,8 +112,8 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Distribuidores</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Distribuidores</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
@@ -133,9 +135,7 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
{/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
@@ -179,8 +179,8 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}>Eliminar</MenuItem>)}
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} />Modificar</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}><TrashIcon fontSize="small" sx={{ mr: 1 }} />Eliminar</MenuItem>)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones
|
||||
import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar
|
||||
import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar
|
||||
import empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto';
|
||||
@@ -42,9 +46,9 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
|
||||
const cargarEmpresas = useCallback(async () => {
|
||||
if (!puedeVer) { // Si no tiene permiso de ver, no cargar nada
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -90,8 +94,8 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
} catch (err: any) {
|
||||
console.error("Error en submit modal (padre):", err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al guardar la empresa.';
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al guardar la empresa.';
|
||||
setApiErrorMessage(message);
|
||||
throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre
|
||||
}
|
||||
@@ -101,16 +105,16 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
const handleDelete = async (id: number) => {
|
||||
// Opcional: mostrar un mensaje de confirmación más detallado
|
||||
if (window.confirm(`¿Está seguro de que desea eliminar esta empresa (ID: ${id})? Esta acción también eliminará los saldos asociados.`)) {
|
||||
setApiErrorMessage(null); // Limpiar errores previos
|
||||
try {
|
||||
setApiErrorMessage(null); // Limpiar errores previos
|
||||
try {
|
||||
await empresaService.deleteEmpresa(id);
|
||||
cargarEmpresas(); // Recargar la lista para reflejar la eliminación
|
||||
} catch (err: any) {
|
||||
console.error("Error al eliminar empresa:", err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al eliminar la empresa.';
|
||||
setApiErrorMessage(message); // Mostrar error de API
|
||||
console.error("Error al eliminar empresa:", err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al eliminar la empresa.';
|
||||
setApiErrorMessage(message); // Mostrar error de API
|
||||
}
|
||||
}
|
||||
handleMenuClose(); // Cerrar el menú de acciones
|
||||
@@ -127,115 +131,113 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
setSelectedEmpresaRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
// Datos a mostrar en la tabla actual según paginación
|
||||
const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
// Datos a mostrar en la tabla actual según paginación
|
||||
const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
// 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>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Empresas</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Gestionar Empresas
|
||||
</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)}
|
||||
/>
|
||||
</Box>
|
||||
{/* Mostrar botón de agregar solo si tiene permiso */}
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
>
|
||||
Agregar Nueva Empresa
|
||||
</Button>
|
||||
</Box>
|
||||
<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)}
|
||||
/>
|
||||
</Box>
|
||||
{/* Mostrar botón de agregar solo si tiene permiso */}
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
>
|
||||
Agregar Nueva Empresa
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Indicador de carga */}
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{/* Mensaje de error al cargar datos */}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{/* Mensaje de error de la API (modal/delete) */}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{/* Tabla de datos (solo si no está cargando y no hubo error de carga inicial) */}
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
{/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */}
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron empresas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((emp) => (
|
||||
<TableRow key={emp.idEmpresa}>
|
||||
<TableCell>{emp.nombre}</TableCell>
|
||||
<TableCell>{emp.detalle || '-'}</TableCell>
|
||||
{/* Mostrar botón de acciones solo si tiene permiso */}
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, emp)}
|
||||
// Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro)
|
||||
disabled={!puedeModificar && !puedeEliminar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/* Paginación */}
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={empresas.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
{/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */}
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron empresas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((emp) => (
|
||||
<TableRow key={emp.idEmpresa}>
|
||||
<TableCell>{emp.nombre}</TableCell>
|
||||
<TableCell>{emp.detalle || '-'}</TableCell>
|
||||
{/* Mostrar botón de acciones solo si tiene permiso */}
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, emp)}
|
||||
// Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro)
|
||||
disabled={!puedeModificar && !puedeEliminar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/* Paginación */}
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={empresas.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{/* Menú contextual para acciones de fila */}
|
||||
<Menu
|
||||
@@ -245,15 +247,17 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
>
|
||||
{/* Mostrar opción Modificar solo si tiene permiso */}
|
||||
{puedeModificar && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}>
|
||||
Modificar
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Mostrar opción Eliminar solo si tiene permiso */}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedEmpresaRow!.idEmpresa)}>
|
||||
Eliminar
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleDelete(selectedEmpresaRow!.idEmpresa)}>
|
||||
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Mensaje si no hay acciones disponibles (por si acaso) */}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
@@ -7,27 +6,27 @@ import {
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import PrintIcon from '@mui/icons-material/Print';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; // Para Liquidar
|
||||
import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck';
|
||||
|
||||
import entradaSalidaCanillaService from '../../services/Distribucion/entradaSalidaCanillaService';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
|
||||
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
||||
import type { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto';
|
||||
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
|
||||
|
||||
|
||||
import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
|
||||
const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]);
|
||||
@@ -35,13 +34,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = 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 [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
@@ -58,9 +56,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false);
|
||||
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
// MC001 (Ver), MC002 (Crear), MC003 (Modificar), MC004 (Eliminar), MC005 (Liquidar)
|
||||
const puedeVer = isSuperAdmin || tienePermiso("MC001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("MC002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("MC003");
|
||||
@@ -68,6 +64,17 @@ 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]}`;
|
||||
}
|
||||
return datePart;
|
||||
};
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
@@ -120,22 +127,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); };
|
||||
|
||||
const handleSubmitModal = async (data: CreateEntradaSalidaCanillaDto | UpdateEntradaSalidaCanillaDto, idParte?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (idParte && editingMovimiento) {
|
||||
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data as UpdateEntradaSalidaCanillaDto);
|
||||
} else {
|
||||
await entradaSalidaCanillaService.createEntradaSalidaCanilla(data as CreateEntradaSalidaCanillaDto);
|
||||
}
|
||||
cargarMovimientos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idParte: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
|
||||
@@ -147,7 +138,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedRow(item);
|
||||
// Almacenar el idParte en el propio elemento del menú para referencia
|
||||
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedRow(item);
|
||||
};
|
||||
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
|
||||
|
||||
@@ -177,40 +171,150 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
|
||||
const handleConfirmLiquidar = async () => {
|
||||
setApiErrorMessage(null); setLoading(true);
|
||||
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
|
||||
|
||||
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()
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando 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
|
||||
|
||||
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
|
||||
idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
|
||||
fechaLiquidacion: fechaLiquidacionDialog
|
||||
fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
|
||||
};
|
||||
|
||||
try {
|
||||
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
|
||||
cargarMovimientos(); // Recargar para ver los cambios
|
||||
setOpenLiquidarDialog(false);
|
||||
setOpenLiquidarDialog(false);
|
||||
|
||||
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
|
||||
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
|
||||
|
||||
await cargarMovimientos();
|
||||
|
||||
if (movimientoParaTicket) {
|
||||
console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla);
|
||||
await handleImprimirTicketLiquidacion(
|
||||
movimientoParaTicket.idCanilla,
|
||||
fechaLiquidacionDialog,
|
||||
movimientoParaTicket.canillaEsAccionista
|
||||
);
|
||||
} else {
|
||||
console.warn("No se pudo encontrar información del movimiento para generar el 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) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar los cambios.';
|
||||
setApiErrorMessage(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
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
|
||||
) => {
|
||||
setLoadingTicketPdf(true);
|
||||
setApiErrorMessage(null);
|
||||
|
||||
try {
|
||||
const params = {
|
||||
fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD
|
||||
idCanilla: idCanilla,
|
||||
esAccionista: 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.";
|
||||
setApiErrorMessage(msg);
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, '_blank');
|
||||
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.';
|
||||
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
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) 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: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Entradas/Salidas Canillitas</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas</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 }}>
|
||||
@@ -250,36 +354,62 @@ const GestionarEntradasSalidasCanillaPage: 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 && !apiErrorMessage && <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 }}>
|
||||
<CircularProgress size={20} sx={{ mr: 1 }} />
|
||||
<Typography variant="body2">Cargando ticket...</Typography>
|
||||
</Box>
|
||||
}
|
||||
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' &&
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage}
|
||||
checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage}
|
||||
onChange={handleSelectAllForLiquidar}
|
||||
disabled={numNotLiquidatedOnPage === 0}
|
||||
/>
|
||||
</TableCell>
|
||||
}
|
||||
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell><TableCell>Canillita</TableCell>
|
||||
<TableCell align="right">Salida</TableCell><TableCell align="right">Entrada</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell><TableCell align="right">A Rendir</TableCell>
|
||||
<TableCell>Liquidado</TableCell><TableCell>F. Liq.</TableCell><TableCell>Obs.</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0}
|
||||
checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage}
|
||||
onChange={handleSelectAllForLiquidar}
|
||||
disabled={numNotLiquidatedOnPage === 0}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>Fecha</TableCell>
|
||||
<TableCell>Publicación</TableCell>
|
||||
<TableCell>Canillita</TableCell>
|
||||
<TableCell align="right">Salida</TableCell>
|
||||
<TableCell align="right">Entrada</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
<TableCell align="right">A Rendir</TableCell>
|
||||
<TableCell>Liquidado</TableCell>
|
||||
<TableCell>F. Liq.</TableCell>
|
||||
<TableCell>Obs.</TableCell>
|
||||
{(puedeModificar || puedeEliminar || puedeLiquidar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 12 : 11} align="center">No se encontraron movimientos.</TableCell></TableRow>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={
|
||||
(puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) +
|
||||
9 +
|
||||
((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
|
||||
}
|
||||
align="center"
|
||||
>
|
||||
No se encontraron movimientos.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayData.map((m) => (
|
||||
<TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' &&
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={selectedIdsParaLiquidar.has(m.idParte)}
|
||||
@@ -287,29 +417,34 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
disabled={m.liquidado}
|
||||
/>
|
||||
</TableCell>
|
||||
}
|
||||
)}
|
||||
<TableCell>{formatDate(m.fecha)}</TableCell>
|
||||
<TableCell>{m.nombrePublicacion}</TableCell>
|
||||
<TableCell>{m.nomApeCanilla}</TableCell>
|
||||
<TableCell align="right">{m.cantSalida}</TableCell>
|
||||
<TableCell align="right">{m.cantEntrada}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>{m.vendidos}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>${m.montoARendir.toFixed(2)}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||
{m.montoARendir.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</TableCell>
|
||||
<TableCell align="center">{m.liquidado ? <Chip label="Sí" color="success" size="small" /> : <Chip label="No" size="small" />}</TableCell>
|
||||
<TableCell>{m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'}</TableCell>
|
||||
<TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell>
|
||||
<Tooltip title={m.observacion || ''}>
|
||||
<Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{m.observacion || '-'}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{(puedeModificar || puedeEliminar || puedeLiquidar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, m)}
|
||||
disabled={
|
||||
// Deshabilitar si no tiene ningún permiso de eliminación O
|
||||
// si está liquidado y no tiene permiso para eliminar liquidados
|
||||
!((!m.liquidado && puedeEliminar) || (m.liquidado && puedeEliminarLiquidados))
|
||||
}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<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
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
@@ -327,18 +462,45 @@ 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>)}
|
||||
{selectedRow && (
|
||||
(!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)
|
||||
|
||||
{/* Opción de Imprimir Ticket Liq. */}
|
||||
{selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir)
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior
|
||||
handleImprimirTicketLiquidacion(
|
||||
selectedRow.idCanilla,
|
||||
selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento
|
||||
selectedRow.canillaEsAccionista
|
||||
);
|
||||
}
|
||||
// handleMenuClose() es llamado por handleImprimirTicketLiquidacion
|
||||
}}
|
||||
disabled={loadingTicketPdf}
|
||||
>
|
||||
<PrintIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{loadingTicketPdf && <CircularProgress size={16} sx={{ mr: 1 }} />}
|
||||
Reimprimir Ticket Liq.
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{selectedRow && ( // Opción de Eliminar
|
||||
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
|
||||
) && (
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}>
|
||||
<MenuItem onClick={() => {
|
||||
if (selectedRow) handleDelete(selectedRow.idParte);
|
||||
}}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
<EntradaSalidaCanillaFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingMovimiento} errorMessage={apiErrorMessage}
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleModalEditSubmit}
|
||||
initialData={editingMovimiento}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
@@ -31,14 +30,12 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||
const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>('');
|
||||
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
@@ -55,6 +52,19 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
const puedeVer = isSuperAdmin || tienePermiso("MD001");
|
||||
const puedeGestionar = isSuperAdmin || tienePermiso("MD002");
|
||||
|
||||
// CORREGIDO: Función para formatear la fecha
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
// Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..."
|
||||
const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
// parts[0] = YYYY, parts[1] = MM, parts[2] = DD
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY
|
||||
}
|
||||
return datePart; // Fallback si el formato no es el esperado
|
||||
};
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
@@ -126,16 +136,16 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
setRowsPerPage(parseInt(event.target.value, 25)); setPage(0);
|
||||
};
|
||||
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
|
||||
// La función formatDate ya está definida arriba.
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Entradas/Salidas a Distribuidores</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas a Distribuidores</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 }}>
|
||||
@@ -187,6 +197,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
) : (
|
||||
displayData.map((m) => (
|
||||
<TableRow key={m.idParte} hover>
|
||||
{/* Usar la función formatDate aquí */}
|
||||
<TableCell>{formatDate(m.fecha)}</TableCell>
|
||||
<TableCell>{m.nombrePublicacion} <Chip label={m.nombreEmpresaPublicacion} size="small" variant="outlined" sx={{ ml: 0.5 }} /></TableCell>
|
||||
<TableCell>{m.nombreDistribuidor}</TableCell>
|
||||
@@ -196,7 +207,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
<TableCell align="right">{m.cantidad}</TableCell>
|
||||
<TableCell>{m.remito}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', color: m.tipoMovimiento === 'Salida' ? 'success.main' : (m.montoCalculado === 0 ? 'inherit' : 'error.main') }}>
|
||||
{m.tipoMovimiento === 'Salida' ? '$'+m.montoCalculado.toFixed(2) : '$-'+m.montoCalculado.toFixed(2) }
|
||||
{(m.tipoMovimiento === 'Salida' ? '$' : '$-') + m.montoCalculado.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</TableCell>
|
||||
<TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell>
|
||||
{puedeGestionar && (
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones
|
||||
import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar
|
||||
import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar
|
||||
import otroDestinoService from '../../services/Distribucion/otroDestinoService';
|
||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
||||
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
|
||||
@@ -40,9 +44,9 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
|
||||
const cargarOtrosDestinos = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -84,8 +88,8 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
cargarOtrosDestinos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al guardar el destino.';
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al guardar el destino.';
|
||||
setApiErrorMessage(message);
|
||||
throw err;
|
||||
}
|
||||
@@ -93,15 +97,15 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await otroDestinoService.deleteOtroDestino(id);
|
||||
cargarOtrosDestinos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al eliminar el destino.';
|
||||
setApiErrorMessage(message);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al eliminar el destino.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
@@ -117,98 +121,102 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
setSelectedDestinoRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Otros Destinos</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)}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Destino
|
||||
</Button>
|
||||
</Box>
|
||||
<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)}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Destino
|
||||
</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>}
|
||||
{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>Observación</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((destino) => (
|
||||
<TableRow key={destino.idDestino}>
|
||||
<TableCell>{destino.nombre}</TableCell>
|
||||
<TableCell>{destino.obs || '-'}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={otrosDestinos.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((destino) => (
|
||||
<TableRow key={destino.idDestino}>
|
||||
<TableCell>{destino.nombre}</TableCell>
|
||||
<TableCell>{destino.obs || '-'}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={otrosDestinos.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(selectedDestinoRow!); handleMenuClose(); }}>Modificar</MenuItem>
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>Eliminar</MenuItem>
|
||||
<MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>
|
||||
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
@@ -3,15 +3,26 @@ import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip,
|
||||
FormControlLabel
|
||||
FormControlLabel,
|
||||
ListItemText,
|
||||
ListItemIcon
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para el menú de acciones
|
||||
import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar
|
||||
import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar
|
||||
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; // Icono para días de semana
|
||||
import LocalOfferIcon from '@mui/icons-material/LocalOffer'; // Para Precios
|
||||
import AddCardIcon from '@mui/icons-material/AddCard'; // Para Recargos
|
||||
import PercentIcon from '@mui/icons-material/Percent'; // Para Porcentajes
|
||||
import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; // Para Porc./Monto Canillita
|
||||
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt'; // Para Secciones
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto';
|
||||
import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto';
|
||||
import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal';
|
||||
import PublicacionDiasSemanaModal from '../../components/Modals/Distribucion/PublicacionDiasSemanaModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -40,9 +51,11 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedPublicacionRow, setSelectedPublicacionRow] = useState<PublicacionDto | null>(null);
|
||||
|
||||
const [diasSemanaModalOpen, setDiasSemanaModalOpen] = useState(false);
|
||||
const [selectedPublicacionParaDias, setSelectedPublicacionParaDias] = useState<PublicacionDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("DP001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("DP002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("DP003");
|
||||
@@ -179,6 +192,23 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleOpenDiasSemanaModal = (publicacion: PublicacionDto) => {
|
||||
setSelectedPublicacionParaDias(publicacion);
|
||||
setDiasSemanaModalOpen(true);
|
||||
handleMenuClose(); // Cerrar el menú de acciones si estaba abierto
|
||||
};
|
||||
|
||||
const handleCloseDiasSemanaModal = () => {
|
||||
setDiasSemanaModalOpen(false);
|
||||
setSelectedPublicacionParaDias(null);
|
||||
};
|
||||
|
||||
const handleConfigDiasSaved = () => {
|
||||
// Opcional: Recargar publicaciones o simplemente mostrar un mensaje de éxito.
|
||||
// Por ahora, solo cerramos el modal.
|
||||
console.log("Configuración de días guardada.");
|
||||
};
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -191,8 +221,8 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Publicaciones</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Publicaciones</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
||||
@@ -261,17 +291,75 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeGestionarPrecios && (<MenuItem onClick={() => handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios</MenuItem>)}
|
||||
{puedeGestionarRecargos && (<MenuItem onClick={() => handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos</MenuItem>)}
|
||||
{puedeGestionarPorcDist && (<MenuItem onClick={() => handleNavigateToPorcentajesPagoDist(selectedPublicacionRow!.idPublicacion)}>Porcentajes Pago (Dist.)</MenuItem>)}
|
||||
{puedeGestionarPorcCan && (<MenuItem onClick={() => handleNavigateToPorcMonCanilla(selectedPublicacionRow!.idPublicacion)}>Porc./Monto Canillita</MenuItem>)}
|
||||
{puedeGestionarSecciones && (<MenuItem onClick={() => handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar</MenuItem>)}
|
||||
{/* Si no hay permisos para ninguna acción */}
|
||||
{(!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones) &&
|
||||
<MenuItem disabled>Sin acciones</MenuItem>}
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
PaperProps={{
|
||||
style: {
|
||||
minWidth: 250, // Un ancho mínimo para que los textos no se corten tanto
|
||||
},
|
||||
}}
|
||||
>
|
||||
{puedeModificar && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeGestionarPrecios && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleNavigateToPrecios(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
|
||||
<ListItemIcon><LocalOfferIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Gestionar Precios</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeGestionarRecargos && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleNavigateToRecargos(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
|
||||
<ListItemIcon><AddCardIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Gestionar Recargos</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeGestionarPorcDist && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleNavigateToPorcentajesPagoDist(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
|
||||
<ListItemIcon><PercentIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Porcentajes Pago (Dist.)</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeGestionarPorcCan && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleNavigateToPorcMonCanilla(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
|
||||
<ListItemIcon><RequestQuoteIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Porc./Monto Canillita</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeGestionarSecciones && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleNavigateToSecciones(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
|
||||
<ListItemIcon><ViewQuiltIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Gestionar Secciones</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeModificar && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleOpenDiasSemanaModal(selectedPublicacionRow as PublicacionDto); handleMenuClose(); }}>
|
||||
<ListItemIcon><CalendarMonthIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Días de Salida</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && selectedPublicacionRow && (
|
||||
<MenuItem onClick={() => { handleDelete(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
|
||||
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Si no hay permisos para ninguna acción y hay una fila seleccionada */}
|
||||
{selectedPublicacionRow &&
|
||||
!puedeModificar && !puedeEliminar &&
|
||||
!puedeGestionarPrecios && !puedeGestionarRecargos &&
|
||||
!puedeGestionarPorcDist && !puedeGestionarPorcCan &&
|
||||
!puedeGestionarSecciones && (
|
||||
<MenuItem disabled>
|
||||
<ListItemText>Sin acciones disponibles</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
<PublicacionFormModal
|
||||
@@ -279,6 +367,12 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
initialData={editingPublicacion} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
<PublicacionDiasSemanaModal
|
||||
open={diasSemanaModalOpen}
|
||||
onClose={handleCloseDiasSemanaModal}
|
||||
publicacion={selectedPublicacionParaDias}
|
||||
onConfigSaved={handleConfigDiasSaved}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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, FormControl, InputLabel, Select
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
@@ -29,9 +29,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
|
||||
|
||||
@@ -48,25 +47,37 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
||||
const [selectedRow, setSelectedRow] = useState<SalidaOtroDestinoDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
// SO001, SO002 (crear/modificar), SO003 (eliminar)
|
||||
const puedeVer = isSuperAdmin || tienePermiso("SO001");
|
||||
const puedeCrearModificar = isSuperAdmin || tienePermiso("SO002");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("SO003");
|
||||
|
||||
// CORREGIDO: Función para formatear la fecha
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
// Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..."
|
||||
const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
// parts[0] = YYYY, parts[1] = MM, parts[2] = DD
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY
|
||||
}
|
||||
return datePart; // Fallback si el formato no es el esperado
|
||||
};
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [pubsData, destinosData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
otroDestinoService.getAllOtrosDestinos()
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setOtrosDestinos(destinosData);
|
||||
const [pubsData, destinosData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
otroDestinoService.getAllOtrosDestinos()
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setOtrosDestinos(destinosData);
|
||||
} catch (err) {
|
||||
console.error("Error cargando datos para filtros:", err);
|
||||
setError("Error al cargar opciones de filtro.");
|
||||
console.error("Error cargando datos para filtros:", err);
|
||||
setError("Error al cargar opciones de filtro.");
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false);
|
||||
setLoadingFiltersDropdown(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -117,13 +128,13 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
||||
|
||||
const handleDelete = async (idParte: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este registro de salida (ID: ${idParte})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await salidaOtroDestinoService.deleteSalidaOtroDestino(idParte);
|
||||
cargarSalidas();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setApiErrorMessage(message);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
@@ -140,81 +151,91 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
|
||||
const displayData = salidas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
|
||||
// La función formatDate ya está definida arriba.
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Salidas a Otros Destinos</Typography>
|
||||
<Box sx={{ p: 1}}>
|
||||
<Typography variant="h5" gutterBottom>Salidas a Otros Destinos</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros</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}}/>
|
||||
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Publicación</InputLabel>
|
||||
<Select value={filtroIdPublicacion} label="Publicación" 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>Destino</InputLabel>
|
||||
<Select value={filtroIdDestino} label="Destino" onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{otrosDestinos.map(d => <MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeCrearModificar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Salida</Button>)}
|
||||
<Typography variant="h6" gutterBottom>Filtros</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 }} />
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Publicación</InputLabel>
|
||||
<Select value={filtroIdPublicacion} label="Publicación" 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>Destino</InputLabel>
|
||||
<Select value={filtroIdDestino} label="Destino" onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{otrosDestinos.map(d => <MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeCrearModificar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Salida</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>}
|
||||
{error && !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>Fecha</TableCell><TableCell>Publicación</TableCell>
|
||||
<TableCell>Destino</TableCell><TableCell align="right">Cantidad</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
{(puedeCrearModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={6} align="center">No se encontraron salidas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((s) => (
|
||||
<TableRow key={s.idParte} hover>
|
||||
<TableCell>{formatDate(s.fecha)}</TableCell><TableCell>{s.nombrePublicacion}</TableCell>
|
||||
<TableCell>{s.nombreDestino}</TableCell><TableCell align="right">{s.cantidad}</TableCell>
|
||||
<TableCell>{s.observacion || '-'}</TableCell>
|
||||
{(puedeCrearModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={salidas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell>
|
||||
<TableCell>Destino</TableCell><TableCell align="right">Cantidad</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
{(puedeCrearModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeCrearModificar || puedeEliminar ? 6 : 5} align="center">No se encontraron salidas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((s) => (
|
||||
<TableRow key={s.idParte} hover>
|
||||
{/* Usar la función formatDate aquí */}
|
||||
<TableCell>{formatDate(s.fecha)}</TableCell>
|
||||
<TableCell>{s.nombrePublicacion}</TableCell>
|
||||
<TableCell>{s.nombreDestino}</TableCell>
|
||||
<TableCell align="right">{s.cantidad}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={s.observacion || ''}>
|
||||
<Box sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{s.observacion || '-'}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{(puedeCrearModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={salidas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeCrearModificar && selectedRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
{puedeEliminar && selectedRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
<SalidaOtroDestinoFormModal
|
||||
|
||||
@@ -2,10 +2,14 @@ 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
|
||||
CircularProgress, Alert,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones
|
||||
import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar
|
||||
import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar
|
||||
import zonaService from '../../services/Distribucion/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
|
||||
@@ -132,8 +136,8 @@ const GestionarZonasPage: React.FC = () => {
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Gestionar Zonas
|
||||
</Typography>
|
||||
|
||||
@@ -149,7 +153,6 @@ const GestionarZonasPage: React.FC = () => {
|
||||
{/* <TextField label="Filtrar por Descripción" ... /> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
@@ -158,7 +161,6 @@ const GestionarZonasPage: React.FC = () => {
|
||||
>
|
||||
Agregar Nueva Zona
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
@@ -218,12 +220,14 @@ const GestionarZonasPage: React.FC = () => {
|
||||
>
|
||||
{puedeModificar && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedZonaRow!); handleMenuClose(); }}>
|
||||
Modificar
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedZonaRow!.idZona)}>
|
||||
Eliminar
|
||||
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
|
||||
Reference in New Issue
Block a user