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; |