Feat: Implementa ABM y anulación de ajustes manuales
Este commit introduce la funcionalidad completa para la gestión de ajustes manuales (créditos/débitos) en la cuenta corriente de un suscriptor, cerrando un requerimiento clave detectado en el análisis del flujo de trabajo manual. Backend: - Se añade la tabla `susc_Ajustes` para registrar movimientos manuales. - Se crean el Modelo, DTOs, Repositorio y Servicio (`AjusteService`) para el ABM completo de los ajustes. - Se implementa la lógica para anular ajustes que se encuentren en estado "Pendiente", registrando el usuario y fecha de anulación para mantener la trazabilidad. - Se integra la lógica de aplicación de ajustes pendientes en el `FacturacionService`, afectando el `ImporteFinal` de la factura generada. - Se añaden los nuevos endpoints en `AjustesController` para crear, listar y anular ajustes. Frontend: - Se crea el componente `CuentaCorrienteSuscriptorTab` para mostrar el historial de ajustes de un cliente. - Se desarrolla el modal `AjusteFormModal` que permite a los usuarios registrar nuevos créditos o débitos. - Se integra una nueva pestaña "Cuenta Corriente / Ajustes" en la vista de gestión de un suscriptor. - Se añade la funcionalidad de "Anular" en la tabla de ajustes, permitiendo a los usuarios corregir errores antes del ciclo de facturación.
This commit is contained in:
110
Frontend/src/components/Modals/Suscripciones/AjusteFormModal.tsx
Normal file
110
Frontend/src/components/Modals/Suscripciones/AjusteFormModal.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
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';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
interface AjusteFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateAjusteDto) => Promise<void>;
|
||||
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 [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFormData({
|
||||
idSuscriptor: idSuscriptor,
|
||||
tipoAjuste: 'Credito', // Por defecto es un crédito (descuento)
|
||||
monto: 0,
|
||||
motivo: ''
|
||||
});
|
||||
setLocalErrors({});
|
||||
}
|
||||
}, [open, idSuscriptor]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
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.";
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: name === 'monto' ? parseFloat(value) : 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 {
|
||||
await onSubmit(formData as CreateAjusteDto);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
success = false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (success) onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6">Registrar Ajuste Manual</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<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>
|
||||
</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} />
|
||||
|
||||
{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 Ajuste'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AjusteFormModal;
|
||||
Reference in New Issue
Block a user