feat: Implementación de Secciones, Recargos, Porc. Pago Dist. y backend E/S Dist.

Backend API:
- Recargos por Zona (`dist_RecargoZona`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/recargos`.
  - Lógica de negocio para vigencias (cierre/reapertura de períodos).
  - Auditoría en `dist_RecargoZona_H`.
- Porcentajes de Pago Distribuidores (`dist_PorcPago`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajespago`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcPago_H`.
- Porcentajes/Montos Pago Canillitas (`dist_PorcMonPagoCanilla`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajesmoncanilla`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcMonPagoCanilla_H`.
- Secciones de Publicación (`dist_dtPubliSecciones`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/secciones`.
  - Auditoría en `dist_dtPubliSecciones_H`.
- Entradas/Salidas Distribuidores (`dist_EntradasSalidas`):
  - Implementado backend (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para determinar precios/recargos/porcentajes aplicables.
  - Cálculo de monto y afectación de saldos de distribuidores en `cue_Saldos`.
  - Auditoría en `dist_EntradasSalidas_H`.
- Correcciones de Mapeo Dapper:
  - Aplicados alias explícitos en repositorios de RecargoZona, PorcPago, PorcMonCanilla, PubliSeccion,
    Canilla, Distribuidor y Precio para asegurar mapeo correcto de IDs y columnas.

Frontend React:
- Recargos por Zona:
  - `recargoZonaService.ts`.
  - `RecargoZonaFormModal.tsx` para crear/editar períodos de recargos.
  - `GestionarRecargosPublicacionPage.tsx` para listar y gestionar recargos por publicación.
- Porcentajes de Pago Distribuidores:
  - `porcPagoService.ts`.
  - `PorcPagoFormModal.tsx`.
  - `GestionarPorcentajesPagoPage.tsx`.
- Porcentajes/Montos Pago Canillitas:
  - `porcMonCanillaService.ts`.
  - `PorcMonCanillaFormModal.tsx`.
  - `GestionarPorcMonCanillaPage.tsx`.
- Secciones de Publicación:
  - `publiSeccionService.ts`.
  - `PubliSeccionFormModal.tsx`.
  - `GestionarSeccionesPublicacionPage.tsx`.
- Navegación:
  - Actualizadas rutas y menús para acceder a la gestión de recargos, porcentajes (dist. y canillita) y secciones desde la vista de una publicación.
- Layout:
  - Uso consistente de `Box` con Flexbox en lugar de `Grid` en nuevos modales y páginas para evitar errores de tipo.
This commit is contained in:
2025-05-21 14:58:52 -03:00
parent b6ba52f074
commit e7e185a9cb
140 changed files with 10465 additions and 394 deletions

View File

@@ -68,11 +68,6 @@ const DistribucionIndexPage: React.FC = () => {
aria-label="sub-módulos de distribución"
>
{distribucionSubModules.map((subModule) => (
// Usar RouterLink para que el tab se comporte como un enlace y actualice la URL
// La navegación real la manejamos con navigate en handleSubTabChange
// para poder actualizar el estado del tab seleccionado.
// Podríamos usar `component={RouterLink} to={subModule.path}` también,
// pero manejarlo con navigate da más control sobre el estado.
<Tab key={subModule.path} label={subModule.label} />
))}
</Tabs>

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const ESDistribuidoresPage: React.FC = () => {
return <Typography variant="h6">Página de Gestión de E/S de Distribuidores</Typography>;
};
export default ESDistribuidoresPage;

View File

@@ -0,0 +1,250 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
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 entradaSalidaDistService from '../../services/Distribucion/entradaSalidaDistService';
import publicacionService from '../../services/Distribucion/publicacionService';
import distribuidorService from '../../services/Distribucion/distribuidorService';
import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto';
import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarEntradasSalidasDistPage: React.FC = () => {
const [movimientos, setMovimientos] = useState<EntradaSalidaDistDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
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);
const [modalOpen, setModalOpen] = useState(false);
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaDistDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<EntradaSalidaDistDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("MD001");
const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); // Para Crear, Editar, Eliminar
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [pubsData, distData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
distribuidorService.getAllDistribuidores()
]);
setPublicaciones(pubsData);
setDistribuidores(distData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
}
}, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const cargarMovimientos = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
fechaDesde: filtroFechaDesde || null,
fechaHasta: filtroFechaHasta || null,
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null,
tipoMovimiento: filtroTipoMov || null,
};
const data = await entradaSalidaDistService.getAllEntradasSalidasDist(params);
setMovimientos(data);
} catch (err) {
console.error(err); setError('Error al cargar los movimientos.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDistribuidor, filtroTipoMov]);
useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
const handleOpenModal = (item?: EntradaSalidaDistDto) => {
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingMovimiento(null);
};
const handleSubmitModal = async (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => {
setApiErrorMessage(null);
try {
if (idParte && editingMovimiento) {
await entradaSalidaDistService.updateEntradaSalidaDist(idParte, data as UpdateEntradaSalidaDistDto);
} else {
await entradaSalidaDistService.createEntradaSalidaDist(data as CreateEntradaSalidaDistDto);
}
cargarMovimientos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el movimiento.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})? Esta acción revertirá el impacto en el saldo del distribuidor.`)) {
setApiErrorMessage(null);
try {
await entradaSalidaDistService.deleteEntradaSalidaDist(idParte);
cargarMovimientos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaDistDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = 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>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Entradas/Salidas 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}}>
<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>Distribuidor</InputLabel>
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 150, flexGrow: 1}}>
<InputLabel>Tipo</InputLabel>
<Select value={filtroTipoMov} label="Tipo" onChange={(e) => setFiltroTipoMov(e.target.value as 'Salida' | 'Entrada' | '')}>
<MenuItem value=""><em>Todos</em></MenuItem>
<MenuItem value="Salida">Salida</MenuItem>
<MenuItem value="Entrada">Entrada</MenuItem>
</Select>
</FormControl>
</Box>
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</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>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Publicación (Empresa)</TableCell>
<TableCell>Distribuidor</TableCell><TableCell>Tipo</TableCell>
<TableCell align="right">Cantidad</TableCell><TableCell>Remito</TableCell>
<TableCell align="right">Monto Afectado</TableCell><TableCell>Obs.</TableCell>
{puedeGestionar && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeGestionar ? 9 : 8} align="center">No se encontraron movimientos.</TableCell></TableRow>
) : (
displayData.map((m) => (
<TableRow key={m.idParte} hover>
<TableCell>{formatDate(m.fecha)}</TableCell>
<TableCell>{m.nombrePublicacion} ({m.nombreEmpresaPublicacion})</TableCell>
<TableCell>{m.nombreDistribuidor}</TableCell>
<TableCell>
<Chip label={m.tipoMovimiento} color={m.tipoMovimiento === 'Salida' ? 'primary' : 'secondary'} size="small"/>
</TableCell>
<TableCell align="right">{m.cantidad}</TableCell>
<TableCell>{m.remito}</TableCell>
<TableCell align="right" sx={{color: m.montoCalculado < 0 ? 'green' : (m.montoCalculado > 0 ? 'red' : 'inherit')}}>
${m.montoCalculado.toFixed(2)}
</TableCell>
<TableCell>{m.observacion || '-'}</TableCell>
{puedeGestionar && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, m)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={movimientos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
{puedeGestionar && selectedRow && ( // O un permiso más específico si "eliminar" es diferente de "modificar"
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
<EntradaSalidaDistFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingMovimiento} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarEntradasSalidasDistPage;

View File

@@ -0,0 +1,189 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import porcMonCanillaService from '../../services/Distribucion/porcMonCanillaService';
import publicacionService from '../../services/Distribucion/publicacionService';
import type { PorcMonCanillaDto } from '../../models/dtos/Distribucion/PorcMonCanillaDto';
import type { CreatePorcMonCanillaDto } from '../../models/dtos/Distribucion/CreatePorcMonCanillaDto';
import type { UpdatePorcMonCanillaDto } from '../../models/dtos/Distribucion/UpdatePorcMonCanillaDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import PorcMonCanillaFormModal from '../../components/Modals/Distribucion/PorcMonCanillaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarPorcMonCanillaPage: React.FC = () => {
const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>();
const navigate = useNavigate();
const idPublicacion = Number(idPublicacionStr);
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
const [items, setItems] = useState<PorcMonCanillaDto[]>([]); // Renombrado de 'porcentajes' a 'items'
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState<PorcMonCanillaDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<PorcMonCanillaDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Permiso CG004 para porcentajes/montos de pago de canillitas
const puedeGestionar = isSuperAdmin || tienePermiso("CG004");
const cargarDatos = useCallback(async () => {
if (isNaN(idPublicacion)) {
setError("ID de Publicación inválido."); setLoading(false); return;
}
if (!puedeGestionar) {
setError("No tiene permiso para gestionar esta configuración."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const [pubData, data] = await Promise.all([
publicacionService.getPublicacionById(idPublicacion),
porcMonCanillaService.getPorcMonCanillaPorPublicacion(idPublicacion)
]);
setPublicacion(pubData);
setItems(data);
} catch (err: any) {
console.error(err);
if (axios.isAxiosError(err) && err.response?.status === 404) {
setError(`Publicación ID ${idPublicacion} no encontrada.`);
} else {
setError('Error al cargar los datos.');
}
} finally { setLoading(false); }
}, [idPublicacion, puedeGestionar]);
useEffect(() => { cargarDatos(); }, [cargarDatos]);
const handleOpenModal = (item?: PorcMonCanillaDto) => {
setEditingItem(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingItem(null);
};
const handleSubmitModal = async (data: CreatePorcMonCanillaDto | UpdatePorcMonCanillaDto, idPorcMon?: number) => {
setApiErrorMessage(null);
try {
if (editingItem && idPorcMon) {
await porcMonCanillaService.updatePorcMonCanilla(idPublicacion, idPorcMon, data as UpdatePorcMonCanillaDto);
} else {
await porcMonCanillaService.createPorcMonCanilla(idPublicacion, data as CreatePorcMonCanillaDto);
}
cargarDatos();
} 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 (idPorcMonDelRow: number) => {
if (window.confirm(`¿Seguro de eliminar este registro (ID: ${idPorcMonDelRow})?`)) {
setApiErrorMessage(null);
try {
await porcMonCanillaService.deletePorcMonCanilla(idPublicacion, idPorcMonDelRow);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: PorcMonCanillaDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedRow(null);
};
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00').toLocaleDateString('es-AR') : '-';
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
if (error) return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
if (!puedeGestionar) return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/publicaciones`)} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Typography variant="h4" gutterBottom>Porcentajes/Montos Pago Canillita: {publicacion?.nombre || 'Cargando...'}</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{puedeGestionar && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Configuración
</Button>
)}
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell sx={{fontWeight: 'bold'}}>Canillita</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vig. Desde</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vig. Hasta</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Valor</TableCell>
<TableCell align="center" sx={{fontWeight: 'bold'}}>Tipo</TableCell>
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{items.length === 0 ? (
<TableRow><TableCell colSpan={7} align="center">No hay configuraciones definidas.</TableCell></TableRow>
) : (
items.sort((a,b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nomApeCanilla.localeCompare(b.nomApeCanilla))
.map((item) => (
<TableRow key={item.idPorcMon} hover>
<TableCell>{item.nomApeCanilla}</TableCell><TableCell>{formatDate(item.vigenciaD)}</TableCell>
<TableCell>{formatDate(item.vigenciaH)}</TableCell>
<TableCell align="right">{item.esPorcentaje ? `${item.porcMon.toFixed(2)}%` : `$${item.porcMon.toFixed(2)}`}</TableCell>
<TableCell align="center">{item.esPorcentaje ? <Chip label="%" color="primary" size="small" variant="outlined"/> : <Chip label="Monto" color="secondary" size="small" variant="outlined"/>}</TableCell>
<TableCell align="center">{!item.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, item)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
</TableContainer>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar/Cerrar</MenuItem>)}
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idPorcMon)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
{idPublicacion &&
<PorcMonCanillaFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
idPublicacion={idPublicacion} initialData={editingItem}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarPorcMonCanillaPage;

View File

@@ -0,0 +1,187 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import porcPagoService from '../../services/Distribucion/porcPagoService';
import publicacionService from '../../services/Distribucion/publicacionService';
import type { PorcPagoDto } from '../../models/dtos/Distribucion/PorcPagoDto';
import type { CreatePorcPagoDto } from '../../models/dtos/Distribucion/CreatePorcPagoDto';
import type { UpdatePorcPagoDto } from '../../models/dtos/Distribucion/UpdatePorcPagoDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import PorcPagoFormModal from '../../components/Modals/Distribucion/PorcPagoFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarPorcentajesPagoPage: React.FC = () => {
const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>();
const navigate = useNavigate();
const idPublicacion = Number(idPublicacionStr);
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
const [porcentajes, setPorcentajes] = useState<PorcPagoDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [editingPorcentaje, setEditingPorcentaje] = useState<PorcPagoDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedPorcentajeRow, setSelectedPorcentajeRow] = useState<PorcPagoDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Permiso DG004 para porcentajes de pago de distribuidores
const puedeGestionar = isSuperAdmin || tienePermiso("DG004");
const cargarDatos = useCallback(async () => {
if (isNaN(idPublicacion)) {
setError("ID de Publicación inválido."); setLoading(false); return;
}
if (!puedeGestionar) { // Permiso para ver precios de una publicacion (DP004) o el específico de porcentajes (DG004)
setError("No tiene permiso para gestionar porcentajes de pago."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const [pubData, data] = await Promise.all([
publicacionService.getPublicacionById(idPublicacion),
porcPagoService.getPorcentajesPorPublicacion(idPublicacion)
]);
setPublicacion(pubData);
setPorcentajes(data);
} catch (err: any) {
console.error(err);
if (axios.isAxiosError(err) && err.response?.status === 404) {
setError(`Publicación ID ${idPublicacion} no encontrada.`);
} else {
setError('Error al cargar los datos.');
}
} finally { setLoading(false); }
}, [idPublicacion, puedeGestionar]);
useEffect(() => { cargarDatos(); }, [cargarDatos]);
const handleOpenModal = (item?: PorcPagoDto) => {
setEditingPorcentaje(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingPorcentaje(null);
};
const handleSubmitModal = async (data: CreatePorcPagoDto | UpdatePorcPagoDto, idPorcentaje?: number) => {
setApiErrorMessage(null);
try {
if (editingPorcentaje && idPorcentaje) {
await porcPagoService.updatePorcPago(idPublicacion, idPorcentaje, data as UpdatePorcPagoDto);
} else {
await porcPagoService.createPorcPago(idPublicacion, data as CreatePorcPagoDto);
}
cargarDatos();
} 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 (idPorcentajeDelRow: number) => {
if (window.confirm(`¿Seguro de eliminar este porcentaje de pago (ID: ${idPorcentajeDelRow})?`)) {
setApiErrorMessage(null);
try {
await porcPagoService.deletePorcPago(idPublicacion, idPorcentajeDelRow);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: PorcPagoDto) => {
setAnchorEl(event.currentTarget); setSelectedPorcentajeRow(item);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedPorcentajeRow(null);
};
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00').toLocaleDateString('es-AR') : '-';
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
if (error) return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
if (!puedeGestionar) return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/publicaciones`)} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Typography variant="h4" gutterBottom>Porcentajes Pago Distribuidor: {publicacion?.nombre || 'Cargando...'}</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{puedeGestionar && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Porcentaje
</Button>
)}
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell sx={{fontWeight: 'bold'}}>Distribuidor</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Desde</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Hasta</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Porcentaje (%)</TableCell>
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{porcentajes.length === 0 ? (
<TableRow><TableCell colSpan={6} align="center">No hay porcentajes definidos.</TableCell></TableRow>
) : (
porcentajes.sort((a,b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nombreDistribuidor.localeCompare(b.nombreDistribuidor))
.map((p) => (
<TableRow key={p.idPorcentaje} hover>
<TableCell>{p.nombreDistribuidor}</TableCell><TableCell>{formatDate(p.vigenciaD)}</TableCell>
<TableCell>{formatDate(p.vigenciaH)}</TableCell>
<TableCell align="right">{p.porcentaje.toFixed(2)}%</TableCell>
<TableCell align="center">{!p.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
</TableContainer>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionar && selectedPorcentajeRow && (
<MenuItem onClick={() => { handleOpenModal(selectedPorcentajeRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar/Cerrar</MenuItem>)}
{puedeGestionar && selectedPorcentajeRow && (
<MenuItem onClick={() => handleDelete(selectedPorcentajeRow.idPorcentaje)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
{idPublicacion &&
<PorcPagoFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
idPublicacion={idPublicacion} initialData={editingPorcentaje}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarPorcentajesPagoPage;

View File

@@ -48,8 +48,13 @@ const GestionarPublicacionesPage: React.FC = () => {
const puedeModificar = isSuperAdmin || tienePermiso("DP003");
const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004");
const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005");
const puedeGestionarPorcDist = isSuperAdmin || tienePermiso("DG004");
const puedeEliminar = isSuperAdmin || tienePermiso("DP006");
// Permiso DP007 para secciones
const puedeGestionarSecciones = isSuperAdmin || tienePermiso("DP007");
// Permiso CG004 para porcentajes/montos de pago de canillitas
const puedeGestionarPorcCan = isSuperAdmin || tienePermiso("CG004");
const fetchEmpresas = useCallback(async () => {
setLoadingEmpresas(true);
@@ -149,11 +154,30 @@ const GestionarPublicacionesPage: React.FC = () => {
// TODO: Implementar navegación a páginas de gestión de Precios, Recargos, Secciones
const handleNavigateToPrecios = (idPub: number) => {
console.log("Navegando a precios para ID:", idPub);
console.log("Fila seleccionada:", selectedPublicacionRow);
navigate(`/distribucion/publicaciones/${idPub}/precios`); // Ruta anidada
handleMenuClose();
};
const handleNavigateToRecargos = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/recargos`); handleMenuClose(); };
const handleNavigateToSecciones = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/secciones`); handleMenuClose(); };
const handleNavigateToRecargos = (idPub: number) => {
navigate(`/distribucion/publicaciones/${idPub}/recargos`);
handleMenuClose();
};
const handleNavigateToPorcentajesPagoDist = (idPub: number) => {
navigate(`/distribucion/publicaciones/${idPub}/porcentajes-pago-dist`);
handleMenuClose();
};
const handleNavigateToPorcMonCanilla = (idPub: number) => {
navigate(`/distribucion/publicaciones/${idPub}/porcentajes-mon-canilla`);
handleMenuClose();
};
const handleNavigateToSecciones = (idPub: number) => {
navigate(`/distribucion/publicaciones/${idPub}/secciones`);
handleMenuClose();
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
@@ -241,6 +265,8 @@ const GestionarPublicacionesPage: React.FC = () => {
{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 */}

View File

@@ -0,0 +1,197 @@
// src/pages/Distribucion/Publicaciones/GestionarRecargosPublicacionPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import recargoZonaService from '../../services/Distribucion/recargoZonaService';
import publicacionService from '../../services/Distribucion/publicacionService';
import type { RecargoZonaDto } from '../../models/dtos/Distribucion/RecargoZonaDto';
import type { CreateRecargoZonaDto } from '../../models/dtos/Distribucion/CreateRecargoZonaDto';
import type { UpdateRecargoZonaDto } from '../../models/dtos/Distribucion/UpdateRecargoZonaDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import RecargoZonaFormModal from '../../components/Modals/Distribucion/RecargoZonaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarRecargosPublicacionPage: React.FC = () => {
const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>();
const navigate = useNavigate();
const idPublicacion = Number(idPublicacionStr);
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
const [recargos, setRecargos] = useState<RecargoZonaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [editingRecargo, setEditingRecargo] = useState<RecargoZonaDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRecargoRow, setSelectedRecargoRow] = useState<RecargoZonaDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005");
const cargarDatos = useCallback(async () => {
if (isNaN(idPublicacion)) {
setError("ID de Publicación inválido."); setLoading(false); return;
}
if (!puedeGestionarRecargos) {
setError("No tiene permiso para gestionar recargos."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const [pubData, recargosData] = await Promise.all([
publicacionService.getPublicacionById(idPublicacion),
recargoZonaService.getRecargosPorPublicacion(idPublicacion)
]);
setPublicacion(pubData);
setRecargos(recargosData);
} catch (err: any) {
console.error(err);
if (axios.isAxiosError(err) && err.response?.status === 404) {
setError(`Publicación ID ${idPublicacion} no encontrada o sin acceso a sus recargos.`);
} else {
setError('Error al cargar los datos de recargos.');
}
} finally { setLoading(false); }
}, [idPublicacion, puedeGestionarRecargos]);
useEffect(() => { cargarDatos(); }, [cargarDatos]);
const handleOpenModal = (recargo?: RecargoZonaDto) => {
setEditingRecargo(recargo || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingRecargo(null);
};
const handleSubmitModal = async (data: CreateRecargoZonaDto | UpdateRecargoZonaDto, idRecargo?: number) => {
setApiErrorMessage(null);
try {
if (editingRecargo && idRecargo) {
await recargoZonaService.updateRecargoZona(idPublicacion, idRecargo, data as UpdateRecargoZonaDto);
} else {
await recargoZonaService.createRecargoZona(idPublicacion, data as CreateRecargoZonaDto);
}
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el recargo.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idRecargoDelRow: number) => {
if (window.confirm(`¿Seguro de eliminar este recargo (ID: ${idRecargoDelRow})? Puede afectar vigencias.`)) {
setApiErrorMessage(null);
try {
await recargoZonaService.deleteRecargoZona(idPublicacion, idRecargoDelRow);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el recargo.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, recargo: RecargoZonaDto) => {
setAnchorEl(event.currentTarget); setSelectedRecargoRow(recargo);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedRecargoRow(null);
};
const formatDate = (dateString?: string | null) => {
if (!dateString) return '-';
// Asume que dateString es "yyyy-MM-dd" del backend, ya formateado por el DTO.
// Si viniera como DateTime completo, necesitarías parsearlo y formatearlo.
const parts = dateString.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`; // dd/MM/yyyy
}
return dateString; // Devolver como está si no es el formato esperado
};
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
if (error) return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
if (!puedeGestionarRecargos) return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/publicaciones`)} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Typography variant="h4" gutterBottom>Recargos por Zona para: {publicacion?.nombre || 'Cargando...'}</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{puedeGestionarRecargos && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Recargo
</Button>
)}
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell sx={{fontWeight: 'bold'}}>Zona</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Desde</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Hasta</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Valor</TableCell>
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{recargos.length === 0 ? (
<TableRow><TableCell colSpan={6} align="center">No hay recargos definidos para esta publicación.</TableCell></TableRow>
) : (
recargos.sort((a, b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nombreZona.localeCompare(b.nombreZona)) // Ordenar por fecha desc, luego zona asc
.map((r) => (
<TableRow key={r.idRecargo} hover>
<TableCell>{r.nombreZona}</TableCell><TableCell>{formatDate(r.vigenciaD)}</TableCell>
<TableCell>{formatDate(r.vigenciaH)}</TableCell>
<TableCell align="right">${r.valor.toFixed(2)}</TableCell>
<TableCell align="center">{!r.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, r)} disabled={!puedeGestionarRecargos}><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
</TableContainer>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionarRecargos && selectedRecargoRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRecargoRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar/Cerrar</MenuItem>)}
{puedeGestionarRecargos && selectedRecargoRow && (
<MenuItem onClick={() => handleDelete(selectedRecargoRow.idRecargo)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
{idPublicacion &&
<RecargoZonaFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
idPublicacion={idPublicacion} initialData={editingRecargo}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarRecargosPublicacionPage;

View File

@@ -0,0 +1,229 @@
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
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import salidaOtroDestinoService from '../../services/Distribucion/salidaOtroDestinoService';
import publicacionService from '../../services/Distribucion/publicacionService';
import otroDestinoService from '../../services/Distribucion/otroDestinoService';
import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto';
import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto';
import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarSalidasOtrosDestinosPage: React.FC = () => {
const [salidas, setSalidas] = useState<SalidaOtroDestinoDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingSalida, setEditingSalida] = useState<SalidaOtroDestinoDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
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");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
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.");
} finally {
setLoadingFiltersDropdown(false);
}
}, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const cargarSalidas = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
fechaDesde: filtroFechaDesde || null,
fechaHasta: filtroFechaHasta || null,
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
idDestino: filtroIdDestino ? Number(filtroIdDestino) : null,
};
const data = await salidaOtroDestinoService.getAllSalidasOtrosDestinos(params);
setSalidas(data);
} catch (err) {
console.error(err); setError('Error al cargar las salidas.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDestino]);
useEffect(() => { cargarSalidas(); }, [cargarSalidas]);
const handleOpenModal = (item?: SalidaOtroDestinoDto) => {
setEditingSalida(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingSalida(null);
};
const handleSubmitModal = async (data: CreateSalidaOtroDestinoDto | UpdateSalidaOtroDestinoDto, idParte?: number) => {
setApiErrorMessage(null);
try {
if (idParte && editingSalida) {
await salidaOtroDestinoService.updateSalidaOtroDestino(idParte, data as UpdateSalidaOtroDestinoDto);
} else {
await salidaOtroDestinoService.createSalidaOtroDestino(data as CreateSalidaOtroDestinoDto);
}
cargarSalidas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la salida.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este registro de salida (ID: ${idParte})?`)) {
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);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: SalidaOtroDestinoDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = salidas.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>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>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{!loading && !error && 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>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeCrearModificar && selectedRow && (
<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>)}
</Menu>
<SalidaOtroDestinoFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingSalida} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarSalidasOtrosDestinosPage;

View File

@@ -0,0 +1,186 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem, Switch,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip, FormControlLabel
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import publiSeccionService from '../../services/Distribucion/publiSeccionService';
import publicacionService from '../../services/Distribucion/publicacionService';
import type { PubliSeccionDto } from '../../models/dtos/Distribucion/PubliSeccionDto';
import type { CreatePubliSeccionDto } from '../../models/dtos/Distribucion/CreatePubliSeccionDto';
import type { UpdatePubliSeccionDto } from '../../models/dtos/Distribucion/UpdatePubliSeccionDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import PubliSeccionFormModal from '../../components/Modals/Distribucion/PubliSeccionFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarSeccionesPublicacionPage: React.FC = () => {
const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>();
const navigate = useNavigate();
const idPublicacion = Number(idPublicacionStr);
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
const [secciones, setSecciones] = useState<PubliSeccionDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filtroSoloActivas, setFiltroSoloActivas] = useState<boolean | undefined>(undefined); // undefined para mostrar todas
const [modalOpen, setModalOpen] = useState(false);
const [editingSeccion, setEditingSeccion] = useState<PubliSeccionDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedSeccionRow, setSelectedSeccionRow] = useState<PubliSeccionDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Permiso DP007 para gestionar secciones
const puedeGestionar = isSuperAdmin || tienePermiso("DP007");
const cargarDatos = useCallback(async () => {
if (isNaN(idPublicacion)) {
setError("ID de Publicación inválido."); setLoading(false); return;
}
if (!puedeGestionar) { // O también DP001 si solo quiere ver
setError("No tiene permiso para gestionar secciones."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const [pubData, data] = await Promise.all([
publicacionService.getPublicacionById(idPublicacion),
publiSeccionService.getSeccionesPorPublicacion(idPublicacion, filtroSoloActivas)
]);
setPublicacion(pubData);
setSecciones(data);
} catch (err: any) {
console.error(err);
if (axios.isAxiosError(err) && err.response?.status === 404) {
setError(`Publicación ID ${idPublicacion} no encontrada.`);
} else {
setError('Error al cargar los datos.');
}
} finally { setLoading(false); }
}, [idPublicacion, puedeGestionar, filtroSoloActivas]);
useEffect(() => { cargarDatos(); }, [cargarDatos]);
const handleOpenModal = (item?: PubliSeccionDto) => {
setEditingSeccion(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingSeccion(null);
};
const handleSubmitModal = async (data: CreatePubliSeccionDto | UpdatePubliSeccionDto, idSeccion?: number) => {
setApiErrorMessage(null);
try {
if (editingSeccion && idSeccion) {
await publiSeccionService.updatePubliSeccion(idPublicacion, idSeccion, data as UpdatePubliSeccionDto);
} else {
await publiSeccionService.createPubliSeccion(idPublicacion, data as CreatePubliSeccionDto);
}
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la sección.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idSeccionDelRow: number) => {
if (window.confirm(`¿Seguro de eliminar esta sección (ID: ${idSeccionDelRow})?`)) {
setApiErrorMessage(null);
try {
await publiSeccionService.deletePubliSeccion(idPublicacion, idSeccionDelRow);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la sección.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: PubliSeccionDto) => {
setAnchorEl(event.currentTarget); setSelectedSeccionRow(item);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedSeccionRow(null);
};
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
if (error) return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
if (!puedeGestionar) return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/publicaciones`)} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Typography variant="h4" gutterBottom>Secciones de: {publicacion?.nombre || 'Cargando...'}</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap'}}>
{puedeGestionar && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: {xs: 2, sm:0} }}>
Agregar Sección
</Button>
)}
<FormControlLabel
control={<Switch checked={filtroSoloActivas === undefined ? false : filtroSoloActivas} onChange={(e) => setFiltroSoloActivas(e.target.checked ? true : undefined)} />}
label="Mostrar solo activas"
/>
</Box>
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell sx={{fontWeight: 'bold'}}>Nombre Sección</TableCell>
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{secciones.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center">No hay secciones definidas.</TableCell></TableRow>
) : (
secciones.map((s) => (
<TableRow key={s.idSeccion} hover>
<TableCell>{s.nombre}</TableCell>
<TableCell align="center">{s.estado ? <Chip label="Activa" color="success" size="small" /> : <Chip label="Inactiva" size="small" />}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
</TableContainer>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionar && selectedSeccionRow && (
<MenuItem onClick={() => { handleOpenModal(selectedSeccionRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar</MenuItem>)}
{puedeGestionar && selectedSeccionRow && (
<MenuItem onClick={() => handleDelete(selectedSeccionRow.idSeccion)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
{idPublicacion &&
<PubliSeccionFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
idPublicacion={idPublicacion} initialData={editingSeccion}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarSeccionesPublicacionPage;

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const SalidastrosDestinosPage: React.FC = () => {
return <Typography variant="h6">Página de Gestión de Salidas a Otros Destinos</Typography>;
};
export default SalidastrosDestinosPage;

View File

@@ -0,0 +1,302 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; // Para cambiar estado
import stockBobinaService from '../../services/Impresion/stockBobinaService';
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
import plantaService from '../../services/Impresion/plantaService';
import estadoBobinaService from '../../services/Impresion/estadoBobinaService';
import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto';
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarStockBobinasPage: React.FC = () => {
const [stock, setStock] = useState<StockBobinaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Estados para filtros
const [filtroTipoBobina, setFiltroTipoBobina] = useState<number | string>('');
const [filtroNroBobina, setFiltroNroBobina] = useState('');
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
const [filtroRemito, setFiltroRemito] = useState('');
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
// Datos para dropdowns de filtros
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("IB001");
const puedeIngresar = isSuperAdmin || tienePermiso("IB002");
const puedeCambiarEstado = isSuperAdmin || tienePermiso("IB003");
const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004");
const puedeEliminar = isSuperAdmin || tienePermiso("IB005");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas(),
estadoBobinaService.getAllEstadosBobina()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
setEstadosBobina(estadosData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
}
}, []);
useEffect(() => {
fetchFiltersDropdownData();
}, [fetchFiltersDropdownData]);
const cargarStock = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
idTipoBobina: filtroTipoBobina ? Number(filtroTipoBobina) : null,
nroBobinaFilter: filtroNroBobina || null,
idPlanta: filtroPlanta ? Number(filtroPlanta) : null,
idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null,
remitoFilter: filtroRemito || null,
fechaDesde: filtroFechaDesde || null,
fechaHasta: filtroFechaHasta || null,
};
const data = await stockBobinaService.getAllStockBobinas(params);
setStock(data);
} catch (err) {
console.error(err); setError('Error al cargar el stock de bobinas.');
} finally { setLoading(false); }
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaDesde, filtroFechaHasta]);
useEffect(() => {
cargarStock();
}, [cargarStock]);
// Handlers para modales
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.ingresarBobina(data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenEditModal = (bobina: StockBobinaDto) => {
setSelectedBobina(bobina); setApiErrorMessage(null); setEditModalOpen(true); handleMenuClose();
};
const handleCloseEditModal = () => { setEditModalOpen(false); setSelectedBobina(null); };
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenCambioEstadoModal = (bobina: StockBobinaDto) => {
setSelectedBobina(bobina); setApiErrorMessage(null); setCambioEstadoModalOpen(true); handleMenuClose();
};
const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobina(null); };
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado.'; setApiErrorMessage(msg); throw err; }
};
const handleDeleteBobina = async (idBobina: number) => {
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${idBobina})? Solo se permite si está 'Disponible'.`)) {
setApiErrorMessage(null);
try { await stockBobinaService.deleteIngresoBobina(idBobina); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, bobina: StockBobinaDto) => {
setAnchorEl(event.currentTarget); setSelectedBobina(bobina);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedBobina(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Stock de Bobinas</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2}}>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Tipo Bobina</InputLabel>
<Select value={filtroTipoBobina} label="Tipo Bobina" onChange={(e) => setFiltroTipoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Nro. Bobina" size="small" value={filtroNroBobina} onChange={(e) => setFiltroNroBobina(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Planta</InputLabel>
<Select value={filtroPlanta} label="Planta" onChange={(e) => setFiltroPlanta(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todas</em></MenuItem>
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Estado</InputLabel>
<Select value={filtroEstadoBobina} label="Estado" onChange={(e) => setFiltroEstadoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{estadosBobina.map(e => <MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/>
</Box>
{/* <Button variant="outlined" onClick={cargarStock} sx={{ mr: 1 }}>Aplicar Filtros</Button>
<Button variant="outlined" color="secondary" onClick={() => { // Resetear filtros
setFiltroTipoBobina(''); setFiltroNroBobina(''); setFiltroPlanta('');
setFiltroEstadoBobina(''); setFiltroRemito(''); setFiltroFechaDesde(''); setFiltroFechaHasta('');
// cargarStock(); // Opcional: recargar inmediatamente
}}>Limpiar Filtros</Button> */}
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ mt:2 }}>Ingresar Bobina</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>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell>Peso (Kg)</TableCell>
<TableCell>Planta</TableCell><TableCell>Estado</TableCell><TableCell>Remito</TableCell>
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell>
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell>
<TableCell>Obs.</TableCell><TableCell align="right">Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={12} align="center">No se encontraron bobinas con los filtros aplicados.</TableCell></TableRow>
) : (
displayData.map((b) => (
<TableRow key={b.idBobina} hover>
<TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell>
<TableCell align="right">{b.peso}</TableCell><TableCell>{b.nombrePlanta}</TableCell>
<TableCell><Chip label={b.nombreEstadoBobina} size="small" color={
b.idEstadoBobina === 1 ? "success" : b.idEstadoBobina === 2 ? "primary" : b.idEstadoBobina === 3 ? "error" : "default"
}/></TableCell>
<TableCell>{b.remito}</TableCell><TableCell>{formatDate(b.fechaRemito)}</TableCell>
<TableCell>{formatDate(b.fechaEstado)}</TableCell>
<TableCell>{b.nombrePublicacion || '-'}</TableCell><TableCell>{b.nombreSeccion || '-'}</TableCell>
<TableCell>{b.obs || '-'}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, b)}
disabled={!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar}
><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={stock.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{selectedBobina?.idEstadoBobina === 1 && puedeModificarDatos && (
<MenuItem onClick={() => handleOpenEditModal(selectedBobina!)}><EditIcon fontSize="small" sx={{mr:1}}/> Editar Datos</MenuItem>)}
{selectedBobina?.idEstadoBobina !== 3 && puedeCambiarEstado && ( // No se puede cambiar estado si está dañada
<MenuItem onClick={() => handleOpenCambioEstadoModal(selectedBobina!)}><SwapHorizIcon fontSize="small" sx={{mr:1}}/> Cambiar Estado</MenuItem>)}
{selectedBobina?.idEstadoBobina === 1 && puedeEliminar && (
<MenuItem onClick={() => handleDeleteBobina(selectedBobina!.idBobina)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Ingreso</MenuItem>)}
{selectedBobina && selectedBobina.idEstadoBobina === 3 && (!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
{selectedBobina && selectedBobina.idEstadoBobina !== 1 && selectedBobina.idEstadoBobina !== 3 && (!puedeCambiarEstado) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>
<StockBobinaIngresoFormModal
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
{selectedBobina && editModalOpen &&
<StockBobinaEditFormModal
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
initialData={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
{selectedBobina && cambioEstadoModalOpen &&
<StockBobinaCambioEstadoModal
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarStockBobinasPage;

View File

@@ -0,0 +1,213 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Accordion, AccordionSummary, AccordionDetails, Chip,
FormControl,
InputLabel,
Select
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import FilterListIcon from '@mui/icons-material/FilterList';
import tiradaService from '../../services/Impresion/tiradaService';
import publicacionService from '../../services/Distribucion/publicacionService'; // Para filtro
import plantaService from '../../services/Impresion/plantaService'; // Para filtro
import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto';
import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarTiradasPage: React.FC = () => {
const [tiradas, setTiradas] = useState<TiradaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFecha, setFiltroFecha] = useState<string>('');
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
// No hay "editing" para tiradas por ahora, solo crear y borrar.
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("IT001");
const puedeRegistrar = isSuperAdmin || tienePermiso("IT002");
const puedeEliminar = isSuperAdmin || tienePermiso("IT003");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [pubsData, plantasData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
plantaService.getAllPlantas()
]);
setPublicaciones(pubsData);
setPlantas(plantasData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
}
}, []);
useEffect(() => {
fetchFiltersDropdownData();
}, [fetchFiltersDropdownData]);
const cargarTiradas = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
fecha: filtroFecha || null,
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
idPlanta: filtroIdPlanta ? Number(filtroIdPlanta) : null,
};
const data = await tiradaService.getTiradas(params);
setTiradas(data);
} catch (err) {
console.error(err); setError('Error al cargar las tiradas.');
} finally { setLoading(false); }
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdPlanta]);
useEffect(() => {
cargarTiradas();
}, [cargarTiradas]);
const handleOpenModal = () => { setApiErrorMessage(null); setModalOpen(true); };
const handleCloseModal = () => setModalOpen(false);
const handleSubmitModal = async (data: CreateTiradaRequestDto) => {
setApiErrorMessage(null);
try {
await tiradaService.registrarTirada(data);
cargarTiradas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al registrar la tirada.';
setApiErrorMessage(message); throw err;
}
};
const handleDeleteTirada = async (tirada: TiradaDto) => {
if (window.confirm(`¿Seguro de eliminar la tirada del ${tirada.fecha} para "${tirada.nombrePublicacion}" en planta "${tirada.nombrePlanta}"? Esta acción eliminará el total de ejemplares y todas sus secciones asociadas.`)) {
setApiErrorMessage(null);
try {
await tiradaService.deleteTiradaCompleta(tirada.fecha, tirada.idPublicacion, tirada.idPlanta);
cargarTiradas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la tirada.';
setApiErrorMessage(message);
}
}
};
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>Gestión de Tiradas</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" type="date" size="small" value={filtroFecha} onChange={(e) => setFiltroFecha(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} ({p.nombreEmpresa})</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Planta</InputLabel>
<Select value={filtroIdPlanta} label="Planta" onChange={(e) => setFiltroIdPlanta(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
{/* <Button variant="outlined" onClick={cargarTiradas} size="small">Buscar</Button> */}
</Box>
{puedeRegistrar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenModal}>Registrar Nueva Tirada</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>}
{!loading && !error && puedeVer && (
<Box>
{tiradas.length === 0 ? (
<Typography sx={{mt:2, textAlign:'center'}}>No se encontraron tiradas con los filtros aplicados.</Typography>
) : (
tiradas.map((tirada) => (
<Accordion key={tirada.idRegistroTirada} sx={{mb:1}}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{display: 'flex', justifyContent: 'space-between', width: '100%', alignItems: 'center'}}>
<Typography sx={{fontWeight:'bold'}}>{formatDate(tirada.fecha)} - {tirada.nombrePublicacion} ({tirada.nombrePlanta})</Typography>
<Box>
<Chip label={`${tirada.ejemplares} ej.`} color="primary" size="small" sx={{mr:1}}/>
<Chip label={`${tirada.totalPaginasSumadas} pág.`} size="small" />
{puedeEliminar && (
<IconButton size="small" onClick={(e) => { e.stopPropagation(); handleDeleteTirada(tirada);}} sx={{ml:1}}>
<DeleteIcon color="error"/>
</IconButton>
)}
</Box>
</Box>
</AccordionSummary>
<AccordionDetails>
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead><TableRow>
<TableCell>Sección</TableCell>
<TableCell align="right">Páginas</TableCell>
</TableRow></TableHead>
<TableBody>
{tirada.seccionesImpresas.map(sec => (
<TableRow key={sec.idRegPublicacionSeccion}>
<TableCell>{sec.nombreSeccion}</TableCell>
<TableCell align="right">{sec.cantPag}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))
)}
</Box>
)}
{/* No hay paginación para la lista de Acordeones por ahora */}
<TiradaFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarTiradasPage;

View File

@@ -7,8 +7,8 @@ const impresionSubModules = [
{ label: 'Plantas', path: 'plantas' },
{ label: 'Tipos Bobina', path: 'tipos-bobina' },
{ label: 'Estados Bobina', path: 'estados-bobina' },
// { label: 'Stock Bobinas', path: 'stock-bobinas' },
// { label: 'Tiradas', path: 'tiradas' },
{ label: 'Stock Bobinas', path: 'stock-bobinas' },
{ label: 'Tiradas', path: 'tiradas' },
];
const ImpresionIndexPage: React.FC = () => {