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.
189 lines
9.2 KiB
TypeScript
189 lines
9.2 KiB
TypeScript
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; |