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;