Feat: Implementación de módulos ABM de suscripciones por cliente

This commit is contained in:
2025-07-31 10:24:26 -03:00
parent d62ca7feb3
commit b14c5de1b4
16 changed files with 1204 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, FormGroup, FormControlLabel,
Checkbox, type SelectChangeEvent, Paper
} from '@mui/material';
import type { SuscripcionDto } from '../../../models/dtos/Suscripciones/SuscripcionDto';
import type { CreateSuscripcionDto } from '../../../models/dtos/Suscripciones/CreateSuscripcionDto';
import type { UpdateSuscripcionDto } from '../../../models/dtos/Suscripciones/UpdateSuscripcionDto';
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
import publicacionService from '../../../services/Distribucion/publicacionService';
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95%', sm: '80%', md: '600px' },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
const dias = [
{ label: 'Lunes', value: 'L' }, { label: 'Martes', value: 'M' },
{ label: 'Miércoles', value: 'X' }, { label: 'Jueves', value: 'J' },
{ label: 'Viernes', value: 'V' }, { label: 'Sábado', value: 'S' },
{ label: 'Domingo', value: 'D' }
];
interface SuscripcionFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateSuscripcionDto | UpdateSuscripcionDto, id?: number) => Promise<void>;
idSuscriptor: number;
initialData?: SuscripcionDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
// Usamos una interfaz local que contenga todos los campos posibles del formulario
interface FormState {
idPublicacion?: number | '';
fechaInicio?: string;
fechaFin?: string | null;
estado?: 'Activa' | 'Pausada' | 'Cancelada';
observaciones?: string;
}
const SuscripcionFormModal: React.FC<SuscripcionFormModalProps> = ({ open, onClose, onSubmit, idSuscriptor, initialData, errorMessage, clearErrorMessage }) => {
const [formData, setFormData] = useState<FormState>({});
const [selectedDays, setSelectedDays] = useState<Set<string>>(new Set());
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingPubs, setLoadingPubs] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchPublicaciones = async () => {
setLoadingPubs(true);
try {
const data = await publicacionService.getPublicacionesForDropdown(true);
setPublicaciones(data);
} catch (error) {
setLocalErrors(prev => ({ ...prev, publicaciones: 'Error al cargar publicaciones.' }));
} finally {
setLoadingPubs(false);
}
};
if (open) {
fetchPublicaciones();
const diasEntrega = initialData?.diasEntrega ? new Set<string>(initialData.diasEntrega.split(',')) : new Set<string>();
setSelectedDays(diasEntrega);
setFormData({
idPublicacion: initialData?.idPublicacion || '',
fechaInicio: initialData?.fechaInicio || '',
fechaFin: initialData?.fechaFin || '',
estado: initialData?.estado || 'Activa',
observaciones: initialData?.observaciones || ''
});
setLocalErrors({});
}
}, [open, initialData]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!formData.idPublicacion) errors.idPublicacion = "Debe seleccionar una publicación.";
if (!formData.fechaInicio?.trim()) errors.fechaInicio = 'La Fecha de Inicio es obligatoria.';
if (formData.fechaFin && formData.fechaInicio && new Date(formData.fechaFin) < new Date(formData.fechaInicio)) {
errors.fechaFin = 'La Fecha de Fin no puede ser anterior a la de inicio.';
}
if (selectedDays.size === 0) errors.diasEntrega = "Debe seleccionar al menos un día de entrega.";
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleDayChange = (dayValue: string) => {
const newSelection = new Set(selectedDays);
if (newSelection.has(dayValue)) newSelection.delete(dayValue);
else newSelection.add(dayValue);
setSelectedDays(newSelection);
if (localErrors.diasEntrega) setLocalErrors(prev => ({...prev, diasEntrega: null}));
if (errorMessage) clearErrorMessage();
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSelectChange = (e: SelectChangeEvent<any>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
let success = false;
try {
const dataToSubmit = {
...formData,
fechaFin: formData.fechaFin || null,
diasEntrega: Array.from(selectedDays),
};
if (isEditing && initialData) {
await onSubmit(dataToSubmit as UpdateSuscripcionDto, initialData.idSuscripcion);
} else {
await onSubmit({ ...dataToSubmit, idSuscriptor, idPublicacion: Number(formData.idPublicacion) } as CreateSuscripcionDto);
}
success = true;
} catch (error) {
success = false;
} finally {
setLoading(false);
if (success) onClose();
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2">{isEditing ? 'Editar Suscripción' : 'Nueva Suscripción'}</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion}>
<InputLabel id="pub-label" required>Publicación</InputLabel>
<Select name="idPublicacion" labelId="pub-label" value={formData.idPublicacion || ''} onChange={handleSelectChange} label="Publicación" disabled={loading || loadingPubs || isEditing}>
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>)}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
</FormControl>
<Typography sx={{ mt: 2, mb: 1, color: localErrors.diasEntrega ? 'error.main' : 'inherit' }}>Días de Entrega *</Typography>
<Paper variant="outlined" sx={{ p: 1, borderColor: localErrors.diasEntrega ? 'error.main' : 'rgba(0, 0, 0, 0.23)' }}>
<FormGroup row>
{dias.map(d => <FormControlLabel key={d.value} control={<Checkbox checked={selectedDays.has(d.value)} onChange={() => handleDayChange(d.value)} disabled={loading}/>} label={d.label} />)}
</FormGroup>
</Paper>
{localErrors.diasEntrega && <Typography color="error" variant="caption">{localErrors.diasEntrega}</Typography>}
<Box sx={{ display: 'flex', gap: 2, mt: 1 }}>
<TextField name="fechaInicio" label="Fecha Inicio" type="date" value={formData.fechaInicio || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaInicio} helperText={localErrors.fechaInicio} disabled={loading} />
<TextField name="fechaFin" label="Fecha Fin (Opcional)" type="date" value={formData.fechaFin || ''} onChange={handleInputChange} fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaFin} helperText={localErrors.fechaFin} disabled={loading} />
</Box>
<FormControl fullWidth margin="dense">
<InputLabel id="estado-label">Estado</InputLabel>
<Select name="estado" labelId="estado-label" value={formData.estado || 'Activa'} onChange={handleSelectChange} label="Estado" disabled={loading}>
<MenuItem value="Activa">Activa</MenuItem>
<MenuItem value="Pausada">Pausada</MenuItem>
<MenuItem value="Cancelada">Cancelada</MenuItem>
</Select>
</FormControl>
<TextField name="observaciones" label="Observaciones" value={formData.observaciones || ''} onChange={handleInputChange} multiline rows={2} fullWidth margin="dense" disabled={loading} />
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</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 || loadingPubs}>
{loading ? <CircularProgress size={24} /> : 'Guardar'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default SuscripcionFormModal;

View File

@@ -0,0 +1,179 @@
// Archivo: Frontend/src/components/Modals/Suscripciones/SuscriptorFormModal.tsx
import React, { useState, useEffect } from 'react';
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent } from '@mui/material'; // 1. Importar SelectChangeEvent
import type { SuscriptorDto } from '../../../models/dtos/Suscripciones/SuscriptorDto';
import type { CreateSuscriptorDto } from '../../../models/dtos/Suscripciones/CreateSuscriptorDto';
import type { UpdateSuscriptorDto } from '../../../models/dtos/Suscripciones/UpdateSuscriptorDto';
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95%', sm: '80%', md: '600px' },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface SuscriptorFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateSuscriptorDto | UpdateSuscriptorDto, id?: number) => Promise<void>;
initialData?: SuscriptorDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
open, onClose, onSubmit, initialData, errorMessage, clearErrorMessage
}) => {
const [formData, setFormData] = useState<Partial<CreateSuscriptorDto>>({});
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
const CBURequerido = formasDePago.find(fp => fp.idFormaPago === formData.idFormaPagoPreferida)?.requiereCBU ?? false;
useEffect(() => {
const fetchFormasDePago = async () => {
setLoadingFormasPago(true);
try {
const data = await formaPagoService.getAllFormasDePago();
setFormasDePago(data);
} catch (error) {
console.error("Error al cargar formas de pago", error);
setLocalErrors(prev => ({ ...prev, formasDePago: 'Error al cargar formas de pago.' }));
} finally {
setLoadingFormasPago(false);
}
};
if (open) {
fetchFormasDePago();
setFormData(initialData || {
nombreCompleto: '', tipoDocumento: 'DNI', nroDocumento: '', cbu: ''
});
setLocalErrors({});
}
}, [open, initialData]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!formData.nombreCompleto?.trim()) errors.nombreCompleto = 'El nombre es obligatorio.';
if (!formData.direccion?.trim()) errors.direccion = 'La dirección es obligatoria.';
if (!formData.tipoDocumento) errors.tipoDocumento = 'El tipo de documento es obligatorio.';
if (!formData.nroDocumento?.trim()) errors.nroDocumento = 'El número de documento es obligatorio.';
if (!formData.idFormaPagoPreferida) errors.idFormaPagoPreferida = 'La forma de pago es obligatoria.';
if (CBURequerido && (!formData.cbu || formData.cbu.trim().length !== 22)) {
errors.cbu = 'El CBU es obligatorio y debe tener 22 dígitos.';
}
if (formData.email && !/^\S+@\S+\.\S+$/.test(formData.email)) {
errors.email = 'El formato del email no es válido.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (localErrors[name]) {
setLocalErrors(prev => ({ ...prev, [name]: null }));
}
if (errorMessage) clearErrorMessage();
};
// 2. Crear un handler específico para los Select
const handleSelectChange = (e: SelectChangeEvent<any>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (localErrors[name]) {
setLocalErrors(prev => ({ ...prev, [name]: null }));
}
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
let success = false;
try {
const dataToSubmit = formData as CreateSuscriptorDto | UpdateSuscriptorDto;
await onSubmit(dataToSubmit, initialData?.idSuscriptor);
success = true;
} catch (error) {
success = false;
} finally {
setLoading(false);
if (success) {
onClose();
}
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Suscriptor' : 'Nuevo Suscriptor'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField name="nombreCompleto" label="Nombre Completo" value={formData.nombreCompleto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.nombreCompleto} helperText={localErrors.nombreCompleto} disabled={loading} autoFocus />
<TextField name="direccion" label="Dirección" value={formData.direccion || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.direccion} helperText={localErrors.direccion} disabled={loading} />
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<TextField name="email" label="Email" type="email" value={formData.email || ''} onChange={handleInputChange} fullWidth margin="dense" sx={{ flex: 1 }} error={!!localErrors.email} helperText={localErrors.email} disabled={loading} />
<TextField name="telefono" label="Teléfono" value={formData.telefono || ''} onChange={handleInputChange} fullWidth margin="dense" sx={{ flex: 1 }} disabled={loading} />
</Box>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<FormControl margin="dense" sx={{ minWidth: 120 }}>
<InputLabel id="tipo-doc-label">Tipo</InputLabel>
{/* 3. Aplicar el nuevo handler a los Selects */}
<Select labelId="tipo-doc-label" name="tipoDocumento" value={formData.tipoDocumento || 'DNI'} onChange={handleSelectChange} label="Tipo" disabled={loading}>
<MenuItem value="DNI">DNI</MenuItem>
<MenuItem value="CUIT">CUIT</MenuItem>
<MenuItem value="CUIL">CUIL</MenuItem>
</Select>
</FormControl>
<TextField name="nroDocumento" label="Nro Documento" value={formData.nroDocumento || ''} onChange={handleInputChange} required fullWidth margin="dense" sx={{ flex: 2 }} error={!!localErrors.nroDocumento} helperText={localErrors.nroDocumento} disabled={loading} />
</Box>
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPagoPreferida}>
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
{/* 3. Aplicar el nuevo handler a los Selects */}
<Select labelId="forma-pago-label" name="idFormaPagoPreferida" value={formData.idFormaPagoPreferida || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loading || loadingFormasPago}>
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
</Select>
{localErrors.idFormaPagoPreferida && <Typography color="error" variant="caption">{localErrors.idFormaPagoPreferida}</Typography>}
</FormControl>
{CBURequerido && (
<TextField name="cbu" label="CBU" value={formData.cbu || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.cbu} helperText={localErrors.cbu} disabled={loading} inputProps={{ maxLength: 22 }} />
)}
<TextField name="observaciones" label="Observaciones" value={formData.observaciones || ''} onChange={handleInputChange} multiline rows={2} fullWidth margin="dense" disabled={loading} />
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</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 || loadingFormasPago}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Crear Suscriptor')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default SuscriptorFormModal;