Feat: Implementación de módulos ABM de suscripciones por cliente
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user