Feat: Implementa flujo completo de facturación y promociones
Este commit introduce la funcionalidad completa para la facturación mensual, la gestión de promociones y la comunicación con el cliente en el módulo de suscripciones. Backend: - Se añade el servicio de Facturación que calcula automáticamente los importes mensuales basándose en las suscripciones activas, días de entrega y precios. - Se implementa el servicio DebitoAutomaticoService, capaz de generar el archivo de texto plano para "Pago Directo Galicia" y de procesar el archivo de respuesta para la conciliación de pagos. - Se desarrolla el ABM completo para Promociones (Servicio, Repositorio, Controlador y DTOs), permitiendo la creación de descuentos por porcentaje o monto fijo. - Se implementa la lógica para asignar y desasignar promociones a suscripciones específicas. - Se añade un servicio de envío de email (EmailService) integrado con MailKit y un endpoint para notificar facturas a los clientes. - Se crea la lógica para registrar pagos manuales (efectivo, tarjeta, etc.) y actualizar el estado de las facturas. - Se añaden todos los permisos necesarios a la base de datos para segmentar el acceso a las nuevas funcionalidades. Frontend: - Se crea la página de Facturación, que permite al usuario seleccionar un período, generar la facturación, listar los resultados y generar el archivo de débito para el banco. - Se implementa la funcionalidad para subir y procesar el archivo de respuesta del banco, actualizando la UI en consecuencia. - Se añade la página completa para el ABM de Promociones. - Se integra un modal en la gestión de suscripciones para asignar y desasignar promociones a un cliente. - Se añade la opción "Enviar Email" en el menú de acciones de las facturas, conectada al nuevo endpoint del backend. - Se completan y corrigen los componentes `PagoManualModal` y `FacturacionPage` para incluir la lógica de registro de pagos y solucionar errores de TypeScript.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Box, Typography, Button, CircularProgress, Alert, Select, MenuItem, FormControl, InputLabel, List, ListItem, ListItemText, IconButton, Divider } from '@mui/material';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import type { SuscripcionDto } from '../../../models/dtos/Suscripciones/SuscripcionDto';
|
||||
import type { PromocionDto } from '../../../models/dtos/Suscripciones/PromocionDto';
|
||||
import suscripcionService from '../../../services/Suscripciones/suscripcionService';
|
||||
|
||||
const modalStyle = { /* ... */ };
|
||||
|
||||
interface GestionarPromocionesSuscripcionModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
suscripcion: SuscripcionDto | null;
|
||||
}
|
||||
|
||||
const GestionarPromocionesSuscripcionModal: React.FC<GestionarPromocionesSuscripcionModalProps> = ({ open, onClose, suscripcion }) => {
|
||||
const [asignadas, setAsignadas] = useState<PromocionDto[]>([]);
|
||||
const [disponibles, setDisponibles] = useState<PromocionDto[]>([]);
|
||||
const [selectedPromo, setSelectedPromo] = useState<number | string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (!suscripcion) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [asignadasData, disponiblesData] = await Promise.all([
|
||||
suscripcionService.getPromocionesAsignadas(suscripcion.idSuscripcion),
|
||||
suscripcionService.getPromocionesDisponibles(suscripcion.idSuscripcion)
|
||||
]);
|
||||
setAsignadas(asignadasData);
|
||||
setDisponibles(disponiblesData);
|
||||
} catch (err) {
|
||||
setError("Error al cargar las promociones.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [suscripcion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
cargarDatos();
|
||||
}
|
||||
}, [open, cargarDatos]);
|
||||
|
||||
const handleAsignar = async () => {
|
||||
if (!suscripcion || !selectedPromo) return;
|
||||
try {
|
||||
await suscripcionService.asignarPromocion(suscripcion.idSuscripcion, Number(selectedPromo));
|
||||
setSelectedPromo('');
|
||||
cargarDatos();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Error al asignar la promoción.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuitar = async (idPromocion: number) => {
|
||||
if (!suscripcion) return;
|
||||
try {
|
||||
await suscripcionService.quitarPromocion(suscripcion.idSuscripcion, idPromocion);
|
||||
cargarDatos();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Error al quitar la promoción.");
|
||||
}
|
||||
};
|
||||
|
||||
if (!suscripcion) return null;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6">Gestionar Promociones</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Suscripción a: {suscripcion.nombrePublicacion}
|
||||
</Typography>
|
||||
{error && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{loading ? <CircularProgress /> : (
|
||||
<>
|
||||
<Typography sx={{ mt: 2 }}>Promociones Asignadas</Typography>
|
||||
<List dense>
|
||||
{asignadas.length === 0 && <ListItem><ListItemText primary="No hay promociones asignadas." /></ListItem>}
|
||||
{asignadas.map(p => (
|
||||
<ListItem key={p.idPromocion} secondaryAction={<IconButton edge="end" onClick={() => handleQuitar(p.idPromocion)}><DeleteIcon /></IconButton>}>
|
||||
<ListItemText primary={p.descripcion} secondary={`Tipo: ${p.tipoPromocion}, Valor: ${p.valor}`} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography>Asignar Nueva Promoción</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 1 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Promociones Disponibles</InputLabel>
|
||||
<Select value={selectedPromo} label="Promociones Disponibles" onChange={(e) => setSelectedPromo(e.target.value)}>
|
||||
{disponibles.map(p => <MenuItem key={p.idPromocion} value={p.idPromocion}>{p.descripcion}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button variant="contained" onClick={handleAsignar} disabled={!selectedPromo}><AddCircleOutlineIcon /></Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<Box sx={{ mt: 3, textAlign: 'right' }}>
|
||||
<Button onClick={onClose}>Cerrar</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPromocionesSuscripcionModal;
|
||||
141
Frontend/src/components/Modals/Suscripciones/PagoManualModal.tsx
Normal file
141
Frontend/src/components/Modals/Suscripciones/PagoManualModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
// Archivo: Frontend/src/components/Modals/Suscripciones/PagoManualModal.tsx
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||
import type { FacturaDto } from '../../../models/dtos/Suscripciones/FacturaDto';
|
||||
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
||||
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: '500px' },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface PagoManualModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
||||
factura: FacturaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, errorMessage, clearErrorMessage }) => {
|
||||
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
||||
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFormasDePago = async () => {
|
||||
setLoadingFormasPago(true);
|
||||
try {
|
||||
const data = await formaPagoService.getAllFormasDePago();
|
||||
setFormasDePago(data.filter(fp => !fp.requiereCBU));
|
||||
} catch (error) {
|
||||
setLocalErrors(prev => ({ ...prev, formasDePago: 'Error al cargar formas de pago.' }));
|
||||
} finally {
|
||||
setLoadingFormasPago(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open && factura) {
|
||||
fetchFormasDePago();
|
||||
setFormData({
|
||||
idFactura: factura.idFactura,
|
||||
monto: factura.importeFinal,
|
||||
fechaPago: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
setLocalErrors({});
|
||||
}
|
||||
}, [open, factura]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!formData.idFormaPago) errors.idFormaPago = "Seleccione una forma de pago.";
|
||||
if (!formData.monto || formData.monto <= 0) errors.monto = "El monto debe ser mayor a cero.";
|
||||
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const finalValue = name === 'monto' && value !== '' ? parseFloat(value) : value;
|
||||
setFormData(prev => ({ ...prev, [name]: finalValue }));
|
||||
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 {
|
||||
await onSubmit(formData as CreatePagoDto);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
success = false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (success) onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!factura) return null;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6">Registrar Pago Manual</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Factura #{factura.idFactura} para {factura.nombreSuscriptor}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
||||
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
||||
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
||||
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
||||
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
||||
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
||||
|
||||
{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 || loadingFormasPago}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PagoManualModal;
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox,
|
||||
type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||
import type { PromocionDto } from '../../../models/dtos/Suscripciones/PromocionDto';
|
||||
import type { CreatePromocionDto, UpdatePromocionDto } from '../../../models/dtos/Suscripciones/CreatePromocionDto';
|
||||
|
||||
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 tiposPromocion = [
|
||||
{ value: 'Porcentaje', label: 'Descuento Porcentual (%)' },
|
||||
{ value: 'MontoFijo', label: 'Descuento de Monto Fijo ($)' },
|
||||
// { value: 'BonificacionDias', label: 'Bonificación de Días' }, // Descomentar para futuras implementaciones
|
||||
];
|
||||
|
||||
interface PromocionFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePromocionDto | UpdatePromocionDto, id?: number) => Promise<void>;
|
||||
initialData?: PromocionDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose, onSubmit, initialData, errorMessage, clearErrorMessage }) => {
|
||||
const [formData, setFormData] = useState<Partial<CreatePromocionDto>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFormData(initialData || {
|
||||
descripcion: '',
|
||||
tipoPromocion: 'Porcentaje',
|
||||
valor: 0,
|
||||
fechaInicio: new Date().toISOString().split('T')[0],
|
||||
activa: true
|
||||
});
|
||||
setLocalErrors({});
|
||||
}
|
||||
}, [open, initialData]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!formData.descripcion?.trim()) errors.descripcion = 'La descripción es obligatoria.';
|
||||
if (!formData.tipoPromocion) errors.tipoPromocion = 'El tipo de promoción es obligatorio.';
|
||||
if (!formData.valor || formData.valor <= 0) errors.valor = 'El valor debe ser mayor a cero.';
|
||||
if (formData.tipoPromocion === 'Porcentaje' && (formData.valor ?? 0) > 100) {
|
||||
errors.valor = 'El valor para porcentaje no puede ser mayor a 100.';
|
||||
}
|
||||
if (!formData.fechaInicio) 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.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const finalValue = type === 'checkbox' ? checked : (type === 'number' ? parseFloat(value) : value);
|
||||
setFormData(prev => ({ ...prev, [name]: finalValue }));
|
||||
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
|
||||
} as CreatePromocionDto | UpdatePromocionDto;
|
||||
|
||||
await onSubmit(dataToSubmit, initialData?.idPromocion);
|
||||
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 Promoción' : 'Nueva Promoción'}</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField name="descripcion" label="Descripción" value={formData.descripcion || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.descripcion} helperText={localErrors.descripcion} disabled={loading} autoFocus />
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<FormControl fullWidth margin="dense" sx={{flex: 2}} error={!!localErrors.tipoPromocion}>
|
||||
<InputLabel id="tipo-promo-label" required>Tipo</InputLabel>
|
||||
<Select name="tipoPromocion" labelId="tipo-promo-label" value={formData.tipoPromocion || ''} onChange={handleSelectChange} label="Tipo" disabled={loading}>
|
||||
{tiposPromocion.map(t => <MenuItem key={t.value} value={t.value}>{t.label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField name="valor" label="Valor" type="number" value={formData.valor || ''} onChange={handleInputChange} required fullWidth margin="dense" sx={{flex: 1}} error={!!localErrors.valor} helperText={localErrors.valor} disabled={loading}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">{formData.tipoPromocion === 'Porcentaje' ? '%' : '$'}</InputAdornment> }}
|
||||
inputProps={{ step: "0.01" }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<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>
|
||||
|
||||
<FormControlLabel control={<Checkbox name="activa" checked={formData.activa ?? true} onChange={handleInputChange} disabled={loading}/>} label="Promoción Activa" sx={{mt: 1}} />
|
||||
|
||||
{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}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Guardar'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromocionFormModal;
|
||||
Reference in New Issue
Block a user