Refactor: Mejora la lógica de facturación y la UI
Este commit introduce una refactorización significativa en el módulo de
suscripciones para alinear el sistema con reglas de negocio clave:
facturación consolidada por empresa, cobro a mes adelantado con
imputación de ajustes diferida, y una interfaz de usuario más clara.
Backend:
- **Facturación por Empresa:** Se modifica `FacturacionService` para
agrupar las suscripciones por cliente y empresa, generando una
factura consolidada para cada combinación. Esto asegura la correcta
separación fiscal.
- **Imputación de Ajustes:** Se ajusta la lógica para que la facturación
de un período (ej. Septiembre) aplique únicamente los ajustes
pendientes cuya fecha corresponde al período anterior (Agosto).
- **Cierre Secuencial:** Se implementa una validación en
`GenerarFacturacionMensual` que impide generar la facturación de un
período si el anterior no ha sido cerrado, garantizando el orden
cronológico.
- **Emails Consolidados:** El proceso de notificación automática al
generar el cierre ahora envía un único email consolidado por
suscriptor, detallando los cargos de todas sus facturas/empresas.
- **Envío de PDF Individual:** Se refactoriza el endpoint de envío manual
para que opere sobre una `idFactura` individual y adjunte el PDF
correspondiente si existe.
- **Repositorios Mejorados:** Se optimizan y añaden métodos en
`FacturaRepository` y `AjusteRepository` para soportar los nuevos
requisitos de filtrado y consulta de datos consolidados.
Frontend:
- **Separación de Vistas:** La página de "Facturación" se divide en dos:
- `ProcesosPage`: Para acciones masivas (generar cierre, archivo de
débito, procesar respuesta).
- `ConsultaFacturasPage`: Una nueva página dedicada a buscar,
filtrar y gestionar facturas individuales con una interfaz de doble
acordeón (Suscriptor -> Empresa).
- **Filtros Avanzados:** La página `ConsultaFacturasPage` ahora incluye
filtros por nombre de suscriptor, estado de pago y estado de
facturación.
- **Filtros de Fecha por Defecto:** La página de "Cuenta Corriente"
ahora filtra por el mes actual por defecto para mejorar el rendimiento
y la usabilidad.
- **Validación de Fechas:** Se añade lógica en los filtros de fecha para
impedir la selección de rangos inválidos.
- **Validación de Monto de Pago:** El modal de pago manual ahora impide
registrar un monto superior al saldo pendiente de la factura.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
// Archivo: Frontend/src/components/Modals/Suscripciones/AjusteFormModal.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 { CreateAjusteDto } from '../../../models/dtos/Suscripciones/CreateAjusteDto';
|
||||
import type { UpdateAjusteDto } from '../../../models/dtos/Suscripciones/UpdateAjusteDto';
|
||||
import type { AjusteDto } from '../../../models/dtos/Suscripciones/AjusteDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
@@ -12,34 +16,47 @@ const modalStyle = {
|
||||
boxShadow: 24, p: 4,
|
||||
};
|
||||
|
||||
// --- TIPO UNIFICADO PARA EL ESTADO DEL FORMULARIO ---
|
||||
type AjusteFormData = Partial<CreateAjusteDto & UpdateAjusteDto>;
|
||||
|
||||
interface AjusteFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateAjusteDto) => Promise<void>;
|
||||
onSubmit: (data: CreateAjusteDto | UpdateAjusteDto, id?: number) => Promise<void>;
|
||||
initialData?: AjusteDto | null;
|
||||
idSuscriptor: number;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const AjusteFormModal: React.FC<AjusteFormModalProps> = ({ open, onClose, onSubmit, idSuscriptor, errorMessage, clearErrorMessage }) => {
|
||||
const [formData, setFormData] = useState<Partial<CreateAjusteDto>>({});
|
||||
const AjusteFormModal: React.FC<AjusteFormModalProps> = ({ open, onClose, onSubmit, idSuscriptor, errorMessage, clearErrorMessage, initialData }) => {
|
||||
const [formData, setFormData] = useState<AjusteFormData>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// Formatear fecha correctamente: el DTO de Ajuste tiene FechaAlta con hora, pero el input necesita "yyyy-MM-dd"
|
||||
const fechaParaFormulario = initialData?.fechaAjuste
|
||||
? initialData.fechaAjuste.split(' ')[0] // Tomar solo la parte de la fecha
|
||||
: new Date().toISOString().split('T')[0];
|
||||
|
||||
setFormData({
|
||||
idSuscriptor: idSuscriptor,
|
||||
tipoAjuste: 'Credito', // Por defecto es un crédito (descuento)
|
||||
monto: 0,
|
||||
motivo: ''
|
||||
idSuscriptor: initialData?.idSuscriptor || idSuscriptor,
|
||||
fechaAjuste: fechaParaFormulario,
|
||||
tipoAjuste: initialData?.tipoAjuste || 'Credito',
|
||||
monto: initialData?.monto || undefined, // undefined para que el placeholder se muestre
|
||||
motivo: initialData?.motivo || ''
|
||||
});
|
||||
setLocalErrors({});
|
||||
}
|
||||
}, [open, idSuscriptor]);
|
||||
}, [open, initialData, idSuscriptor]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!formData.fechaAjuste) errors.fechaAjuste = "La fecha es obligatoria.";
|
||||
if (!formData.tipoAjuste) errors.tipoAjuste = "Seleccione un tipo.";
|
||||
if (!formData.monto || formData.monto <= 0) errors.monto = "El monto debe ser mayor a cero.";
|
||||
if (!formData.motivo?.trim()) errors.motivo = "El motivo es obligatorio.";
|
||||
@@ -47,16 +64,20 @@ const AjusteFormModal: React.FC<AjusteFormModalProps> = ({ open, onClose, onSubm
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
// --- HANDLERS CON TIPADO EXPLÍCITO ---
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: name === 'monto' ? parseFloat(value) : value }));
|
||||
setFormData((prev: AjusteFormData) => ({
|
||||
...prev,
|
||||
[name]: name === 'monto' && value !== '' ? parseFloat(value) : value
|
||||
}));
|
||||
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSelectChange = (e: SelectChangeEvent<any>) => {
|
||||
|
||||
const handleSelectChange = (e: SelectChangeEvent<string>) => { // Tipado como string
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
setFormData((prev: AjusteFormData) => ({ ...prev, [name]: value }));
|
||||
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
@@ -68,7 +89,11 @@ const AjusteFormModal: React.FC<AjusteFormModalProps> = ({ open, onClose, onSubm
|
||||
setLoading(true);
|
||||
let success = false;
|
||||
try {
|
||||
await onSubmit(formData as CreateAjusteDto);
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit(formData as UpdateAjusteDto, initialData.idAjuste);
|
||||
} else {
|
||||
await onSubmit(formData as CreateAjusteDto);
|
||||
}
|
||||
success = true;
|
||||
} catch (error) {
|
||||
success = false;
|
||||
@@ -81,20 +106,22 @@ const AjusteFormModal: React.FC<AjusteFormModalProps> = ({ open, onClose, onSubm
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6">Registrar Ajuste Manual</Typography>
|
||||
<Typography variant="h6">{isEditing ? 'Editar Ajuste Manual' : 'Registrar Ajuste Manual'}</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField name="fechaAjuste" label="Fecha del Ajuste" type="date" value={formData.fechaAjuste || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaAjuste} helperText={localErrors.fechaAjuste} />
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.tipoAjuste}>
|
||||
<InputLabel id="tipo-ajuste-label" required>Tipo de Ajuste</InputLabel>
|
||||
<Select name="tipoAjuste" labelId="tipo-ajuste-label" value={formData.tipoAjuste || ''} onChange={handleSelectChange} label="Tipo de Ajuste">
|
||||
<MenuItem value="Credito">Crédito (Descuento a favor del cliente)</MenuItem>
|
||||
<MenuItem value="Debito">Débito (Cargo extra al cliente)</MenuItem>
|
||||
</Select>
|
||||
<InputLabel id="tipo-ajuste-label" required>Tipo de Ajuste</InputLabel>
|
||||
<Select name="tipoAjuste" labelId="tipo-ajuste-label" value={formData.tipoAjuste || ''} onChange={handleSelectChange} label="Tipo de Ajuste">
|
||||
<MenuItem value="Credito">Crédito (Descuento a favor del cliente)</MenuItem>
|
||||
<MenuItem value="Debito">Débito (Cargo extra al cliente)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField name="monto" label="Monto" 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="motivo" label="Motivo" value={formData.motivo || ''} onChange={handleInputChange} required fullWidth margin="dense" multiline rows={3} error={!!localErrors.motivo} helperText={localErrors.motivo} />
|
||||
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
Nota: Este ajuste se aplicará en la facturación del período correspondiente a la "Fecha del Ajuste".
|
||||
</Alert>
|
||||
{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}>
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
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 { Modal, Box, Typography, Button, CircularProgress, Alert, Select, MenuItem, FormControl, InputLabel, List, ListItem, ListItemText, IconButton, Divider, type SelectChangeEvent, TextField } 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 type { PromocionAsignadaDto } from '../../../models/dtos/Suscripciones/PromocionAsignadaDto';
|
||||
import type { AsignarPromocionDto } from '../../../models/dtos/Suscripciones/AsignarPromocionDto';
|
||||
import suscripcionService from '../../../services/Suscripciones/suscripcionService';
|
||||
|
||||
const modalStyle = { /* ... */ };
|
||||
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 GestionarPromocionesSuscripcionModalProps {
|
||||
open: boolean;
|
||||
@@ -15,12 +29,15 @@ interface GestionarPromocionesSuscripcionModalProps {
|
||||
}
|
||||
|
||||
const GestionarPromocionesSuscripcionModal: React.FC<GestionarPromocionesSuscripcionModalProps> = ({ open, onClose, suscripcion }) => {
|
||||
const [asignadas, setAsignadas] = useState<PromocionDto[]>([]);
|
||||
const [asignadas, setAsignadas] = useState<PromocionAsignadaDto[]>([]);
|
||||
const [disponibles, setDisponibles] = useState<PromocionDto[]>([]);
|
||||
const [selectedPromo, setSelectedPromo] = useState<number | string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [selectedPromo, setSelectedPromo] = useState<number | string>('');
|
||||
const [vigenciaDesde, setVigenciaDesde] = useState('');
|
||||
const [vigenciaHasta, setVigenciaHasta] = useState('');
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (!suscripcion) return;
|
||||
setLoading(true);
|
||||
@@ -40,16 +57,30 @@ const GestionarPromocionesSuscripcionModal: React.FC<GestionarPromocionesSuscrip
|
||||
}, [suscripcion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (open && suscripcion) {
|
||||
cargarDatos();
|
||||
setSelectedPromo('');
|
||||
setVigenciaDesde(suscripcion.fechaInicio);
|
||||
setVigenciaHasta('');
|
||||
}
|
||||
}, [open, cargarDatos]);
|
||||
}, [open, suscripcion]);
|
||||
|
||||
const handleAsignar = async () => {
|
||||
if (!suscripcion || !selectedPromo) return;
|
||||
if (!suscripcion || !selectedPromo || !vigenciaDesde) {
|
||||
setError("Debe seleccionar una promoción y una fecha de inicio.");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
await suscripcionService.asignarPromocion(suscripcion.idSuscripcion, Number(selectedPromo));
|
||||
const dto: AsignarPromocionDto = {
|
||||
idPromocion: Number(selectedPromo),
|
||||
vigenciaDesde: vigenciaDesde,
|
||||
vigenciaHasta: vigenciaHasta || null
|
||||
};
|
||||
await suscripcionService.asignarPromocion(suscripcion.idSuscripcion, dto);
|
||||
setSelectedPromo('');
|
||||
setVigenciaDesde(suscripcion.fechaInicio);
|
||||
setVigenciaHasta('');
|
||||
cargarDatos();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Error al asignar la promoción.");
|
||||
@@ -58,14 +89,34 @@ const GestionarPromocionesSuscripcionModal: React.FC<GestionarPromocionesSuscrip
|
||||
|
||||
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.");
|
||||
setError(null);
|
||||
if (window.confirm("¿Está seguro de que desea quitar esta promoción de la suscripción?")) {
|
||||
try {
|
||||
await suscripcionService.quitarPromocion(suscripcion.idSuscripcion, idPromocion);
|
||||
cargarDatos();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Error al quitar la promoción.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return 'Indefinida';
|
||||
const parts = dateString.split('-');
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
};
|
||||
|
||||
const formatSecondaryText = (promo: PromocionAsignadaDto): string => {
|
||||
let text = '';
|
||||
switch (promo.tipoEfecto) {
|
||||
case 'DescuentoPorcentajeTotal': text = `Descuento Total: ${promo.valorEfecto}%`; break;
|
||||
case 'DescuentoMontoFijoTotal': text = `Descuento Total: $${promo.valorEfecto.toFixed(2)}`; break;
|
||||
case 'BonificarEntregaDia': text = 'Bonificación de Día'; break;
|
||||
default: text = 'Tipo desconocido';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
if (!suscripcion) return null;
|
||||
|
||||
return (
|
||||
@@ -73,30 +124,39 @@ const GestionarPromocionesSuscripcionModal: React.FC<GestionarPromocionesSuscrip
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6">Gestionar Promociones</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Suscripción a: {suscripcion.nombrePublicacion}
|
||||
Suscripción a: <strong>{suscripcion.nombrePublicacion}</strong>
|
||||
</Typography>
|
||||
{error && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{loading ? <CircularProgress /> : (
|
||||
{loading ? <CircularProgress sx={{ display: 'block', margin: '20px auto' }} /> : (
|
||||
<>
|
||||
<Typography sx={{ mt: 2 }}>Promociones Asignadas</Typography>
|
||||
<Typography sx={{ mt: 2, fontWeight: 'medium' }}>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}`} />
|
||||
<ListItemText
|
||||
primary={p.descripcion}
|
||||
secondary={`Vigente del ${formatDate(p.vigenciaDesdeAsignacion)} al ${formatDate(p.vigenciaHastaAsignacion)} - ${formatSecondaryText(p)}`}
|
||||
/>
|
||||
</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">
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<FormControl fullWidth size="small" sx={{ mb: 2 }}>
|
||||
<InputLabel>Promociones Disponibles</InputLabel>
|
||||
<Select value={selectedPromo} label="Promociones Disponibles" onChange={(e) => setSelectedPromo(e.target.value)}>
|
||||
<Select value={selectedPromo} label="Promociones Disponibles" onChange={(e: SelectChangeEvent<number | string>) => 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 sx={{ display: 'flex', gap: 2 }}>
|
||||
<TextField label="Vigencia Desde" type="date" value={vigenciaDesde} onChange={(e) => setVigenciaDesde(e.target.value)} required fullWidth size="small" InputLabelProps={{ shrink: true }} />
|
||||
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaHasta} onChange={(e) => setVigenciaHasta(e.target.value)} fullWidth size="small" InputLabelProps={{ shrink: true }} />
|
||||
</Box>
|
||||
<Button variant="contained" onClick={handleAsignar} disabled={!selectedPromo} sx={{ mt: 2 }} startIcon={<AddCircleOutlineIcon />}>
|
||||
Asignar
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// 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';
|
||||
@@ -54,7 +52,7 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
||||
fetchFormasDePago();
|
||||
setFormData({
|
||||
idFactura: factura.idFactura,
|
||||
monto: factura.importeFinal,
|
||||
monto: factura.saldoPendiente, // Usar el saldo pendiente como valor por defecto
|
||||
fechaPago: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
setLocalErrors({});
|
||||
@@ -64,8 +62,18 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
||||
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.";
|
||||
|
||||
const monto = formData.monto ?? 0;
|
||||
const saldo = factura?.saldoPendiente ?? 0;
|
||||
|
||||
if (monto <= 0) {
|
||||
errors.monto = "El monto debe ser mayor a cero.";
|
||||
} else if (monto > saldo) {
|
||||
// Usamos toFixed(2) para mostrar el formato de moneda correcto en el mensaje
|
||||
errors.monto = `El monto no puede superar el saldo pendiente de $${saldo.toFixed(2)}.`;
|
||||
}
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
@@ -109,8 +117,8 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
||||
<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 variant="subtitle1" gutterBottom sx={{fontWeight: 'bold'}}>
|
||||
Saldo Pendiente: ${factura.saldoPendiente.toFixed(2)}
|
||||
</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} />
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
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 { 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%',
|
||||
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'
|
||||
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
|
||||
const tiposEfecto = [
|
||||
{ value: 'DescuentoPorcentajeTotal', label: 'Descuento en Porcentaje (%) sobre el total' },
|
||||
{ value: 'DescuentoMontoFijoTotal', label: 'Descuento en Monto Fijo ($) sobre el total' },
|
||||
{ value: 'BonificarEntregaDia', label: 'Bonificar / Día Gratis (Precio del día = $0)' },
|
||||
];
|
||||
const tiposCondicion = [
|
||||
{ value: 'Siempre', label: 'Siempre (en todos los días de entrega)' },
|
||||
{ value: 'DiaDeSemana', label: 'Un día de la semana específico' },
|
||||
{ value: 'PrimerDiaSemanaDelMes', label: 'El primer día de la semana del mes' },
|
||||
];
|
||||
const diasSemana = [
|
||||
{ value: 1, label: 'Lunes' }, { value: 2, label: 'Martes' }, { value: 3, label: 'Miércoles' },
|
||||
{ value: 4, label: 'Jueves' }, { value: 5, label: 'Viernes' }, { value: 6, label: 'Sábado' },
|
||||
{ value: 7, label: 'Domingo' }
|
||||
];
|
||||
|
||||
interface PromocionFormModalProps {
|
||||
@@ -38,18 +43,22 @@ const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose,
|
||||
const [formData, setFormData] = useState<Partial<CreatePromocionDto>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
const necesitaValorCondicion = formData.tipoCondicion === 'DiaDeSemana' || formData.tipoCondicion === 'PrimerDiaSemanaDelMes';
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFormData(initialData || {
|
||||
const defaults = {
|
||||
descripcion: '',
|
||||
tipoPromocion: 'Porcentaje',
|
||||
valor: 0,
|
||||
tipoEfecto: 'DescuentoPorcentajeTotal' as const,
|
||||
valorEfecto: 0,
|
||||
tipoCondicion: 'Siempre' as const,
|
||||
valorCondicion: null,
|
||||
fechaInicio: new Date().toISOString().split('T')[0],
|
||||
activa: true
|
||||
});
|
||||
};
|
||||
setFormData(initialData ? { ...initialData } : defaults);
|
||||
setLocalErrors({});
|
||||
}
|
||||
}, [open, initialData]);
|
||||
@@ -57,10 +66,16 @@ const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose,
|
||||
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.tipoEfecto) errors.tipoEfecto = 'El tipo de efecto es obligatorio.';
|
||||
if (formData.tipoEfecto !== 'BonificarEntregaDia' && (!formData.valorEfecto || formData.valorEfecto <= 0)) {
|
||||
errors.valorEfecto = 'El valor debe ser mayor a cero.';
|
||||
}
|
||||
if (formData.tipoEfecto === 'DescuentoPorcentajeTotal' && formData.valorEfecto && formData.valorEfecto > 100) {
|
||||
errors.valorEfecto = 'El valor para porcentaje no puede ser mayor a 100.';
|
||||
}
|
||||
if (!formData.tipoCondicion) errors.tipoCondicion = 'La condición es obligatoria.';
|
||||
if (necesitaValorCondicion && !formData.valorCondicion) {
|
||||
errors.valorCondicion = "Debe seleccionar un día para esta condición.";
|
||||
}
|
||||
if (!formData.fechaInicio) errors.fechaInicio = 'La fecha de inicio es obligatoria.';
|
||||
if (formData.fechaFin && formData.fechaInicio && new Date(formData.fechaFin) < new Date(formData.fechaInicio)) {
|
||||
@@ -72,7 +87,7 @@ const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose,
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
const finalValue = type === 'checkbox' ? checked : (type === 'number' ? parseFloat(value) : value);
|
||||
const finalValue = type === 'checkbox' ? checked : (name === 'valorEfecto' && value !== '' ? parseFloat(value) : value);
|
||||
setFormData(prev => ({ ...prev, [name]: finalValue }));
|
||||
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
@@ -80,7 +95,16 @@ const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose,
|
||||
|
||||
const handleSelectChange = (e: SelectChangeEvent<any>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
const newFormData = { ...formData, [name]: value };
|
||||
|
||||
if (name === 'tipoCondicion' && value === 'Siempre') {
|
||||
newFormData.valorCondicion = null;
|
||||
}
|
||||
if (name === 'tipoEfecto' && value === 'BonificarEntregaDia') {
|
||||
newFormData.valorEfecto = 0; // Bonificar no necesita valor
|
||||
}
|
||||
|
||||
setFormData(newFormData);
|
||||
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
@@ -93,11 +117,7 @@ const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose,
|
||||
setLoading(true);
|
||||
let success = false;
|
||||
try {
|
||||
const dataToSubmit = {
|
||||
...formData,
|
||||
fechaFin: formData.fechaFin || null
|
||||
} as CreatePromocionDto | UpdatePromocionDto;
|
||||
|
||||
const dataToSubmit = { ...formData, fechaFin: formData.fechaFin || null } as CreatePromocionDto | UpdatePromocionDto;
|
||||
await onSubmit(dataToSubmit, initialData?.idPromocion);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
@@ -111,32 +131,43 @@ const PromocionFormModal: React.FC<PromocionFormModalProps> = ({ open, onClose,
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2">{isEditing ? 'Editar Promoción' : 'Nueva Promoción'}</Typography>
|
||||
<Typography variant="h6">{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> }}
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.tipoEfecto}>
|
||||
<InputLabel>Efecto de la Promoción</InputLabel>
|
||||
<Select name="tipoEfecto" value={formData.tipoEfecto || ''} onChange={handleSelectChange} label="Efecto de la Promoción" disabled={loading}>
|
||||
{tiposEfecto.map(t => <MenuItem key={t.value} value={t.value}>{t.label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{formData.tipoEfecto !== 'BonificarEntregaDia' && (
|
||||
<TextField name="valorEfecto" label="Valor" type="number" value={formData.valorEfecto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.valorEfecto} helperText={localErrors.valorEfecto} disabled={loading}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">{formData.tipoEfecto === 'DescuentoPorcentajeTotal' ? '%' : '$'}</InputAdornment> }}
|
||||
inputProps={{ step: "0.01" }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 1 }}>
|
||||
)}
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.tipoCondicion}>
|
||||
<InputLabel>Condición de Aplicación</InputLabel>
|
||||
<Select name="tipoCondicion" value={formData.tipoCondicion || ''} onChange={handleSelectChange} label="Condición de Aplicación" disabled={loading}>
|
||||
{tiposCondicion.map(t => <MenuItem key={t.value} value={t.value}>{t.label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{necesitaValorCondicion && (
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.valorCondicion}>
|
||||
<InputLabel>Día de la Semana</InputLabel>
|
||||
<Select name="valorCondicion" value={formData.valorCondicion || ''} onChange={handleSelectChange} label="Día de la Semana" disabled={loading}>
|
||||
{diasSemana.map(d => <MenuItem key={d.value} value={d.value}>{d.label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
<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}>
|
||||
|
||||
@@ -25,10 +25,10 @@ const modalStyle = {
|
||||
};
|
||||
|
||||
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' }
|
||||
{ label: 'Lunes', value: 'Lun' }, { label: 'Martes', value: 'Mar' },
|
||||
{ label: 'Miércoles', value: 'Mie' }, { label: 'Jueves', value: 'Jue' },
|
||||
{ label: 'Viernes', value: 'Vie' }, { label: 'Sábado', value: 'Sab' },
|
||||
{ label: 'Domingo', value: 'Dom' }
|
||||
];
|
||||
|
||||
interface SuscripcionFormModalProps {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// 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 { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent } from '@mui/material';
|
||||
import type { SuscriptorDto } from '../../../models/dtos/Suscripciones/SuscriptorDto';
|
||||
import type { CreateSuscriptorDto } from '../../../models/dtos/Suscripciones/CreateSuscriptorDto';
|
||||
import type { UpdateSuscriptorDto } from '../../../models/dtos/Suscripciones/UpdateSuscriptorDto';
|
||||
@@ -31,9 +29,7 @@ interface SuscriptorFormModalProps {
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
|
||||
open, onClose, onSubmit, initialData, errorMessage, clearErrorMessage
|
||||
}) => {
|
||||
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);
|
||||
@@ -59,9 +55,18 @@ const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
|
||||
|
||||
if (open) {
|
||||
fetchFormasDePago();
|
||||
setFormData(initialData || {
|
||||
nombreCompleto: '', tipoDocumento: 'DNI', nroDocumento: '', cbu: ''
|
||||
});
|
||||
const dataParaFormulario: Partial<CreateSuscriptorDto> = {
|
||||
nombreCompleto: initialData?.nombreCompleto || '',
|
||||
email: initialData?.email || '',
|
||||
telefono: initialData?.telefono || '',
|
||||
direccion: initialData?.direccion || '',
|
||||
tipoDocumento: initialData?.tipoDocumento || 'DNI',
|
||||
nroDocumento: initialData?.nroDocumento || '',
|
||||
cbu: initialData?.cbu || '',
|
||||
idFormaPagoPreferida: initialData?.idFormaPagoPreferida,
|
||||
observaciones: initialData?.observaciones || ''
|
||||
};
|
||||
setFormData(dataParaFormulario);
|
||||
setLocalErrors({});
|
||||
}
|
||||
}, [open, initialData]);
|
||||
@@ -73,9 +78,15 @@ const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
|
||||
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 (CBURequerido) {
|
||||
if (!formData.cbu || formData.cbu.trim().length !== 22) {
|
||||
errors.cbu = 'El CBU es obligatorio y debe tener 22 dígitos.';
|
||||
}
|
||||
} else if (formData.cbu && formData.cbu.trim().length > 0 && formData.cbu.trim().length !== 22) {
|
||||
errors.cbu = 'El CBU debe tener 22 dígitos o estar vacío.';
|
||||
}
|
||||
|
||||
if (formData.email && !/^\S+@\S+\.\S+$/.test(formData.email)) {
|
||||
errors.email = 'El formato del email no es válido.';
|
||||
}
|
||||
@@ -86,23 +97,25 @@ const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
|
||||
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 (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 }));
|
||||
const newFormData = { ...formData, [name]: value };
|
||||
|
||||
if (name === 'idFormaPagoPreferida') {
|
||||
const formaDePagoSeleccionada = formasDePago.find(fp => fp.idFormaPago === value);
|
||||
if (formaDePagoSeleccionada && !formaDePagoSeleccionada.requiereCBU) {
|
||||
newFormData.cbu = '';
|
||||
}
|
||||
}
|
||||
setFormData(newFormData);
|
||||
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
@@ -111,7 +124,12 @@ const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
|
||||
setLoading(true);
|
||||
let success = false;
|
||||
try {
|
||||
const dataToSubmit = formData as CreateSuscriptorDto | UpdateSuscriptorDto;
|
||||
const dataToSubmit = {
|
||||
...formData,
|
||||
idFormaPagoPreferida: Number(formData.idFormaPagoPreferida),
|
||||
cbu: formData.cbu?.trim() || null
|
||||
} as CreateSuscriptorDto | UpdateSuscriptorDto;
|
||||
|
||||
await onSubmit(dataToSubmit, initialData?.idSuscriptor);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
@@ -140,7 +158,6 @@ const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
|
||||
<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>
|
||||
@@ -151,15 +168,37 @@ const SuscriptorFormModal: React.FC<SuscriptorFormModalProps> = ({
|
||||
</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}>
|
||||
<Select
|
||||
labelId="forma-pago-label"
|
||||
name="idFormaPagoPreferida"
|
||||
value={loadingFormasPago ? '' : formData.idFormaPagoPreferida || ''}
|
||||
onChange={handleSelectChange}
|
||||
label="Forma de Pago"
|
||||
disabled={loading || loadingFormasPago}
|
||||
>
|
||||
{loadingFormasPago && <MenuItem value=""><em>Cargando...</em></MenuItem>}
|
||||
|
||||
{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="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>}
|
||||
|
||||
Reference in New Issue
Block a user