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.
209 lines
9.2 KiB
TypeScript
209 lines
9.2 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
|
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox, InputAdornment
|
|
} from '@mui/material';
|
|
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 { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Para el dropdown
|
|
import canillaService from '../../../services/Distribucion/canillaService'; // Para cargar canillitas
|
|
|
|
const modalStyle = { /* ... (mismo estilo) ... */
|
|
position: 'absolute' as 'absolute',
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: { xs: '90%', sm: 550 },
|
|
bgcolor: 'background.paper',
|
|
border: '2px solid #000',
|
|
boxShadow: 24,
|
|
p: 4,
|
|
maxHeight: '90vh',
|
|
overflowY: 'auto'
|
|
};
|
|
|
|
interface PorcMonCanillaFormModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (data: CreatePorcMonCanillaDto | UpdatePorcMonCanillaDto, idPorcMon?: number) => Promise<void>;
|
|
idPublicacion: number;
|
|
initialData?: PorcMonCanillaDto | null;
|
|
errorMessage?: string | null;
|
|
clearErrorMessage: () => void;
|
|
}
|
|
|
|
const PorcMonCanillaFormModal: React.FC<PorcMonCanillaFormModalProps> = ({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
idPublicacion,
|
|
initialData,
|
|
errorMessage,
|
|
clearErrorMessage
|
|
}) => {
|
|
const [idCanilla, setIdCanilla] = useState<number | string>('');
|
|
const [vigenciaD, setVigenciaD] = useState('');
|
|
const [vigenciaH, setVigenciaH] = useState('');
|
|
const [porcMon, setPorcMon] = useState<string>('');
|
|
const [esPorcentaje, setEsPorcentaje] = useState(true); // Default a porcentaje
|
|
|
|
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [loadingCanillitas, setLoadingCanillitas] = useState(false);
|
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
|
|
|
const isEditing = Boolean(initialData);
|
|
|
|
useEffect(() => {
|
|
const fetchCanillitas = async () => {
|
|
setLoadingCanillitas(true);
|
|
try {
|
|
// Aquí podríamos querer filtrar solo canillitas accionistas si la regla de negocio lo impone
|
|
// o todos los activos. Por ahora, todos los activos.
|
|
const data = await canillaService.getAllCanillas(undefined, undefined, true);
|
|
setCanillitas(data);
|
|
} catch (error) {
|
|
console.error("Error al cargar canillitas", error);
|
|
setLocalErrors(prev => ({...prev, canillitas: 'Error al cargar canillitas.'}));
|
|
} finally {
|
|
setLoadingCanillitas(false);
|
|
}
|
|
};
|
|
|
|
if (open) {
|
|
fetchCanillitas();
|
|
setIdCanilla(initialData?.idCanilla || '');
|
|
setVigenciaD(initialData?.vigenciaD || '');
|
|
setVigenciaH(initialData?.vigenciaH || '');
|
|
setPorcMon(initialData?.porcMon?.toString() || '');
|
|
setEsPorcentaje(initialData ? initialData.esPorcentaje : true);
|
|
setLocalErrors({});
|
|
clearErrorMessage();
|
|
}
|
|
}, [open, initialData, clearErrorMessage]);
|
|
|
|
const validate = (): boolean => {
|
|
const errors: { [key: string]: string | null } = {};
|
|
if (!idCanilla) errors.idCanilla = 'Debe seleccionar un canillita.';
|
|
if (!isEditing && !vigenciaD.trim()) {
|
|
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
|
|
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
|
|
errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).';
|
|
}
|
|
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
|
|
errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).';
|
|
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
|
|
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
|
|
}
|
|
if (!porcMon.trim()) errors.porcMon = 'El valor es obligatorio.';
|
|
else {
|
|
const numVal = parseFloat(porcMon);
|
|
if (isNaN(numVal) || numVal < 0) {
|
|
errors.porcMon = 'El valor debe ser un número positivo.';
|
|
} else if (esPorcentaje && numVal > 100) {
|
|
errors.porcMon = 'El porcentaje no puede ser mayor a 100.';
|
|
}
|
|
}
|
|
setLocalErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const handleInputChange = (fieldName: string) => {
|
|
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
|
if (errorMessage) clearErrorMessage();
|
|
};
|
|
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
clearErrorMessage();
|
|
if (!validate()) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const valorNum = parseFloat(porcMon);
|
|
|
|
if (isEditing && initialData) {
|
|
const dataToSubmit: UpdatePorcMonCanillaDto = {
|
|
porcMon: valorNum,
|
|
esPorcentaje,
|
|
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
|
|
};
|
|
await onSubmit(dataToSubmit, initialData.idPorcMon);
|
|
} else {
|
|
const dataToSubmit: CreatePorcMonCanillaDto = {
|
|
idPublicacion,
|
|
idCanilla: Number(idCanilla),
|
|
vigenciaD,
|
|
porcMon: valorNum,
|
|
esPorcentaje,
|
|
};
|
|
await onSubmit(dataToSubmit);
|
|
}
|
|
onClose();
|
|
} catch (error: any) {
|
|
console.error("Error en submit de PorcMonCanillaFormModal:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal open={open} onClose={onClose}>
|
|
<Box sx={modalStyle}>
|
|
<Typography variant="h6" component="h2" gutterBottom>
|
|
{isEditing ? 'Editar Porcentaje/Monto Canillita' : 'Agregar Nuevo Porcentaje/Monto Canillita'}
|
|
</Typography>
|
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
|
<FormControl fullWidth margin="dense" error={!!localErrors.idCanilla}>
|
|
<InputLabel id="canilla-pmc-select-label" required>Canillita</InputLabel>
|
|
<Select labelId="canilla-pmc-select-label" label="Canillita" value={idCanilla}
|
|
onChange={(e) => {setIdCanilla(e.target.value as number); handleInputChange('idCanilla');}}
|
|
disabled={loading || loadingCanillitas || isEditing}
|
|
>
|
|
<MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem>
|
|
{canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))}
|
|
</Select>
|
|
{localErrors.idCanilla && <Typography color="error" variant="caption">{localErrors.idCanilla}</Typography>}
|
|
</FormControl>
|
|
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
|
|
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
|
|
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
|
|
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
|
/>
|
|
{isEditing && (
|
|
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
|
|
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
|
|
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
|
|
disabled={loading} InputLabelProps={{ shrink: true }}
|
|
/>
|
|
)}
|
|
<TextField label="Valor" type="number" value={porcMon} required
|
|
onChange={(e) => {setPorcMon(e.target.value); handleInputChange('porcMon');}}
|
|
margin="dense" fullWidth error={!!localErrors.porcMon} helperText={localErrors.porcMon || ''}
|
|
disabled={loading}
|
|
InputProps={{ startAdornment: esPorcentaje ? undefined : <InputAdornment position="start">$</InputAdornment>,
|
|
endAdornment: esPorcentaje ? <InputAdornment position="end">%</InputAdornment> : undefined }}
|
|
inputProps={{ step: "0.01", lang:"es-AR" }}
|
|
/>
|
|
<FormControlLabel
|
|
control={<Checkbox checked={esPorcentaje} onChange={(e) => setEsPorcentaje(e.target.checked)} disabled={loading}/>}
|
|
label="Es Porcentaje (si no, es Monto Fijo)" sx={{mt:1}}
|
|
/>
|
|
|
|
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
|
{localErrors.canillitas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.canillitas}</Alert>}
|
|
|
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
|
<Button type="submit" variant="contained" disabled={loading || loadingCanillitas}>
|
|
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar')}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default PorcMonCanillaFormModal; |