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;
|
||||
8
Frontend/src/models/dtos/Suscripciones/CreatePagoDto.ts
Normal file
8
Frontend/src/models/dtos/Suscripciones/CreatePagoDto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface CreatePagoDto {
|
||||
idFactura: number;
|
||||
fechaPago: string; // "yyyy-MM-dd"
|
||||
idFormaPago: number;
|
||||
monto: number;
|
||||
referencia?: string | null;
|
||||
observaciones?: string | null;
|
||||
}
|
||||
10
Frontend/src/models/dtos/Suscripciones/CreatePromocionDto.ts
Normal file
10
Frontend/src/models/dtos/Suscripciones/CreatePromocionDto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface CreatePromocionDto {
|
||||
descripcion: string;
|
||||
tipoPromocion: 'Porcentaje' | 'MontoFijo' | 'BonificacionDias';
|
||||
valor: number;
|
||||
fechaInicio: string; // "yyyy-MM-dd"
|
||||
fechaFin?: string | null;
|
||||
activa: boolean;
|
||||
}
|
||||
// UpdatePromocionDto es igual
|
||||
export type UpdatePromocionDto = CreatePromocionDto;
|
||||
14
Frontend/src/models/dtos/Suscripciones/FacturaDto.ts
Normal file
14
Frontend/src/models/dtos/Suscripciones/FacturaDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface FacturaDto {
|
||||
idFactura: number;
|
||||
idSuscripcion: number;
|
||||
periodo: string; // "YYYY-MM"
|
||||
fechaEmision: string; // "yyyy-MM-dd"
|
||||
fechaVencimiento: string; // "yyyy-MM-dd"
|
||||
importeFinal: number;
|
||||
estado: string;
|
||||
numeroFactura?: string | null;
|
||||
|
||||
// Datos enriquecidos para la UI
|
||||
nombreSuscriptor: string;
|
||||
nombrePublicacion: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface GenerarFacturacionResponseDto {
|
||||
message: string;
|
||||
facturasGeneradas: number;
|
||||
}
|
||||
13
Frontend/src/models/dtos/Suscripciones/PagoDto.ts
Normal file
13
Frontend/src/models/dtos/Suscripciones/PagoDto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface PagoDto {
|
||||
idPago: number;
|
||||
idFactura: number;
|
||||
fechaPago: string; // "yyyy-MM-dd"
|
||||
idFormaPago: number;
|
||||
nombreFormaPago: string;
|
||||
monto: number;
|
||||
estado: string;
|
||||
referencia?: string | null;
|
||||
observaciones?: string | null;
|
||||
idUsuarioRegistro: number;
|
||||
nombreUsuarioRegistro: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ProcesamientoLoteResponseDto {
|
||||
totalRegistrosLeidos: number;
|
||||
pagosAprobados: number;
|
||||
pagosRechazados: number;
|
||||
errores: string[];
|
||||
mensajeResumen: string;
|
||||
}
|
||||
9
Frontend/src/models/dtos/Suscripciones/PromocionDto.ts
Normal file
9
Frontend/src/models/dtos/Suscripciones/PromocionDto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface PromocionDto {
|
||||
idPromocion: number;
|
||||
descripcion: string;
|
||||
tipoPromocion: 'Porcentaje' | 'MontoFijo' | 'BonificacionDias';
|
||||
valor: number;
|
||||
fechaInicio: string; // "yyyy-MM-dd"
|
||||
fechaFin?: string | null;
|
||||
activa: boolean;
|
||||
}
|
||||
321
Frontend/src/pages/Suscripciones/FacturacionPage.tsx
Normal file
321
Frontend/src/pages/Suscripciones/FacturacionPage.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Typography, Button, Paper, CircularProgress, Alert, Select, MenuItem, FormControl, InputLabel, Table, TableContainer, TableHead, TableRow, TableCell, TableBody, Chip, IconButton, Menu, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import PaymentIcon from '@mui/icons-material/Payment';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import facturacionService from '../../services/Suscripciones/facturacionService';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import type { FacturaDto } from '../../models/dtos/Suscripciones/FacturaDto';
|
||||
import PagoManualModal from '../../components/Modals/Suscripciones/PagoManualModal';
|
||||
import type { CreatePagoDto } from '../../models/dtos/Suscripciones/CreatePagoDto';
|
||||
|
||||
const anios = Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - i);
|
||||
const meses = [
|
||||
{ value: 1, label: 'Enero' }, { value: 2, label: 'Febrero' }, { value: 3, label: 'Marzo' },
|
||||
{ value: 4, label: 'Abril' }, { value: 5, label: 'Mayo' }, { value: 6, label: 'Junio' },
|
||||
{ value: 7, label: 'Julio' }, { value: 8, label: 'Agosto' }, { value: 9, label: 'Septiembre' },
|
||||
{ value: 10, label: 'Octubre' }, { value: 11, label: 'Noviembre' }, { value: 12, label: 'Diciembre' }
|
||||
];
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)', clipPath: 'inset(50%)', height: 1, overflow: 'hidden',
|
||||
position: 'absolute', bottom: 0, left: 0, whiteSpace: 'nowrap', width: 1,
|
||||
});
|
||||
|
||||
const FacturacionPage: React.FC = () => {
|
||||
const [selectedAnio, setSelectedAnio] = useState<number>(new Date().getFullYear());
|
||||
const [selectedMes, setSelectedMes] = useState<number>(new Date().getMonth() + 1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingArchivo, setLoadingArchivo] = useState(false);
|
||||
const [loadingProceso, setLoadingProceso] = useState(false);
|
||||
const [apiMessage, setApiMessage] = useState<string | null>(null);
|
||||
const [apiError, setApiError] = useState<string | null>(null);
|
||||
const [facturas, setFacturas] = useState<FacturaDto[]>([]);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGenerarFacturacion = isSuperAdmin || tienePermiso("SU006");
|
||||
const puedeGenerarArchivo = isSuperAdmin || tienePermiso("SU007");
|
||||
const puedeRegistrarPago = isSuperAdmin || tienePermiso("SU008");
|
||||
const puedeEnviarEmail = isSuperAdmin || tienePermiso("SU009");
|
||||
const [pagoModalOpen, setPagoModalOpen] = useState(false);
|
||||
const [selectedFactura, setSelectedFactura] = useState<FacturaDto | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [archivoSeleccionado, setArchivoSeleccionado] = useState<File | null>(null);
|
||||
|
||||
const cargarFacturasDelPeriodo = useCallback(async () => {
|
||||
if (!puedeGenerarFacturacion) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await facturacionService.getFacturasPorPeriodo(selectedAnio, selectedMes);
|
||||
setFacturas(data);
|
||||
} catch (err) {
|
||||
setFacturas([]);
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedAnio, selectedMes, puedeGenerarFacturacion]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarFacturasDelPeriodo();
|
||||
}, [cargarFacturasDelPeriodo]);
|
||||
|
||||
const handleGenerarFacturacion = async () => {
|
||||
if (!window.confirm(`¿Está seguro de que desea generar la facturación para ${meses.find(m => m.value === selectedMes)?.label} de ${selectedAnio}? Este proceso creará registros de cobro para todas las suscripciones activas.`)) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setApiMessage(null);
|
||||
setApiError(null);
|
||||
try {
|
||||
const response = await facturacionService.generarFacturacionMensual(selectedAnio, selectedMes);
|
||||
setApiMessage(`${response.message}. Se generaron ${response.facturasGeneradas} facturas.`);
|
||||
await cargarFacturasDelPeriodo();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar la facturación.';
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerarArchivo = async () => {
|
||||
if (!window.confirm(`Se generará el archivo de débito para las facturas del período ${meses.find(m => m.value === selectedMes)?.label}/${selectedAnio} que estén en estado 'Pendiente de Cobro'. ¿Continuar?`)) {
|
||||
return;
|
||||
}
|
||||
setLoadingArchivo(true);
|
||||
setApiMessage(null);
|
||||
setApiError(null);
|
||||
try {
|
||||
const { fileContent, fileName } = await facturacionService.generarArchivoDebito(selectedAnio, selectedMes);
|
||||
const url = window.URL.createObjectURL(new Blob([fileContent]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
setApiMessage(`Archivo "${fileName}" generado y descargado exitosamente.`);
|
||||
cargarFacturasDelPeriodo();
|
||||
} catch (err: any) {
|
||||
let message = 'Ocurrió un error al generar el archivo.';
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
const errorText = await err.response.data.text();
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
message = errorJson.message || message;
|
||||
} catch { message = errorText || message; }
|
||||
}
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setLoadingArchivo(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, factura: FacturaDto) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedFactura(factura);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedFactura(null);
|
||||
};
|
||||
|
||||
const handleOpenPagoModal = () => {
|
||||
setPagoModalOpen(true);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleClosePagoModal = () => {
|
||||
setPagoModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmitPagoModal = async (data: CreatePagoDto) => {
|
||||
setApiError(null);
|
||||
try {
|
||||
await facturacionService.registrarPagoManual(data);
|
||||
setApiMessage(`Pago para la factura #${data.idFactura} registrado exitosamente.`);
|
||||
cargarFacturasDelPeriodo();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al registrar el pago.';
|
||||
setApiError(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendEmail = async (idFactura: number) => {
|
||||
if (!window.confirm(`¿Está seguro de enviar la notificación de la factura #${idFactura} por email?`)) return;
|
||||
setApiMessage(null);
|
||||
setApiError(null);
|
||||
try {
|
||||
await facturacionService.enviarFacturaPorEmail(idFactura);
|
||||
setApiMessage(`El email para la factura #${idFactura} ha sido enviado a la cola de procesamiento.`);
|
||||
} catch (err: any) {
|
||||
setApiError(err.response?.data?.message || 'Error al intentar enviar el email.');
|
||||
} finally {
|
||||
handleMenuClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
setArchivoSeleccionado(event.target.files[0]);
|
||||
setApiMessage(null);
|
||||
setApiError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProcesarArchivo = async () => {
|
||||
if (!archivoSeleccionado) {
|
||||
setApiError("Por favor, seleccione un archivo de respuesta para procesar.");
|
||||
return;
|
||||
}
|
||||
setLoadingProceso(true);
|
||||
setApiMessage(null);
|
||||
setApiError(null);
|
||||
try {
|
||||
const response = await facturacionService.procesarArchivoRespuesta(archivoSeleccionado);
|
||||
setApiMessage(response.mensajeResumen);
|
||||
if (response.errores?.length > 0) {
|
||||
setApiError(`Se encontraron los siguientes problemas durante el proceso:\n${response.errores.join('\n')}`);
|
||||
}
|
||||
cargarFacturasDelPeriodo(); // Recargar para ver los estados finales
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.mensajeResumen
|
||||
? err.response.data.mensajeResumen
|
||||
: 'Ocurrió un error crítico al procesar el archivo.';
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setLoadingProceso(false);
|
||||
setArchivoSeleccionado(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (!puedeGenerarFacturacion) {
|
||||
return <Alert severity="error">No tiene permiso para acceder a esta sección.</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Facturación y Débito Automático</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6">1. Generación de Facturación</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Este proceso calcula los importes a cobrar para todas las suscripciones activas en el período seleccionado.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, alignItems: 'center' }}>
|
||||
<FormControl sx={{ minWidth: 120 }} size="small">
|
||||
<InputLabel>Mes</InputLabel>
|
||||
<Select value={selectedMes} label="Mes" onChange={(e) => setSelectedMes(e.target.value as number)}>{meses.map(m => <MenuItem key={m.value} value={m.value}>{m.label}</MenuItem>)}</Select>
|
||||
</FormControl>
|
||||
<FormControl sx={{ minWidth: 120 }} size="small">
|
||||
<InputLabel>Año</InputLabel>
|
||||
<Select value={selectedAnio} label="Año" onChange={(e) => setSelectedAnio(e.target.value as number)}>{anios.map(a => <MenuItem key={a} value={a}>{a}</MenuItem>)}</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Button variant="contained" color="primary" startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <PlayCircleOutlineIcon />} onClick={handleGenerarFacturacion} disabled={loading || loadingArchivo}>Generar Facturación del Período</Button>
|
||||
</Paper>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6">2. Generación de Archivo para Banco</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Crea el archivo de texto para enviar a "Pago Directo Galicia" con todas las facturas del período que estén listas para el cobro.</Typography>
|
||||
<Button variant="contained" color="secondary" startIcon={loadingArchivo ? <CircularProgress size={20} color="inherit" /> : <DownloadIcon />} onClick={handleGenerarArchivo} disabled={loading || loadingArchivo || !puedeGenerarArchivo}>Generar Archivo de Débito</Button>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6">3. Procesar Respuesta del Banco</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Suba aquí el archivo de respuesta de Galicia para actualizar automáticamente el estado de las facturas a "Pagada" o "Rechazada".
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<Button
|
||||
component="label"
|
||||
role={undefined}
|
||||
variant="outlined"
|
||||
tabIndex={-1}
|
||||
startIcon={<UploadFileIcon />}
|
||||
disabled={loadingProceso}
|
||||
>
|
||||
Seleccionar Archivo
|
||||
<VisuallyHiddenInput type="file" onChange={handleFileChange} accept=".txt, text/plain" />
|
||||
</Button>
|
||||
{archivoSeleccionado && <Typography variant="body2">{archivoSeleccionado.name}</Typography>}
|
||||
</Box>
|
||||
{archivoSeleccionado && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={handleProcesarArchivo}
|
||||
disabled={loadingProceso}
|
||||
startIcon={loadingProceso ? <CircularProgress size={20} color="inherit" /> : <PlayCircleOutlineIcon />}
|
||||
>
|
||||
Procesar Archivo de Respuesta
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{apiError && <Alert severity="error" sx={{ my: 2 }}>{apiError}</Alert>}
|
||||
{apiMessage && <Alert severity="success" sx={{ my: 2 }}>{apiMessage}</Alert>}
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 4 }}>Facturas del Período</Typography>
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell><TableCell>Suscriptor</TableCell><TableCell>Publicación</TableCell>
|
||||
<TableCell align="right">Importe</TableCell><TableCell>Estado</TableCell><TableCell>Nro. Factura</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading ? (<TableRow><TableCell colSpan={7} align="center"><CircularProgress /></TableCell></TableRow>)
|
||||
: facturas.length === 0 ? (<TableRow><TableCell colSpan={7} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
|
||||
: (facturas.map(f => (
|
||||
<TableRow key={f.idFactura} hover>
|
||||
<TableCell>{f.idFactura}</TableCell>
|
||||
<TableCell>{f.nombreSuscriptor}</TableCell>
|
||||
<TableCell>{f.nombrePublicacion}</TableCell>
|
||||
<TableCell align="right">${f.importeFinal.toFixed(2)}</TableCell>
|
||||
<TableCell><Chip label={f.estado} size="small" color={f.estado === 'Pagada' ? 'success' : (f.estado === 'Rechazada' ? 'error' : 'default')} /></TableCell>
|
||||
<TableCell>{f.numeroFactura || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, f)} disabled={f.estado === 'Pagada' || f.estado === 'Anulada'}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{selectedFactura && puedeRegistrarPago && (
|
||||
<MenuItem onClick={handleOpenPagoModal}>
|
||||
<ListItemIcon><PaymentIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Registrar Pago Manual</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedFactura && puedeEnviarEmail && (
|
||||
<MenuItem
|
||||
onClick={() => handleSendEmail(selectedFactura.idFactura)}
|
||||
disabled={!selectedFactura.numeroFactura}
|
||||
>
|
||||
<ListItemIcon><EmailIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Enviar Email</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<PagoManualModal open={pagoModalOpen} onClose={handleClosePagoModal} onSubmit={handleSubmitPagoModal} factura={selectedFactura} errorMessage={apiError} clearErrorMessage={() => setApiError(null)} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default FacturacionPage;
|
||||
157
Frontend/src/pages/Suscripciones/GestionarPromocionesPage.tsx
Normal file
157
Frontend/src/pages/Suscripciones/GestionarPromocionesPage.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Typography, Button, Paper, CircularProgress, Alert, Table, TableContainer, TableHead, TableRow, TableCell, TableBody, Chip, IconButton, Switch, FormControlLabel, Tooltip } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import promocionService from '../../services/Suscripciones/promocionService';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import type { PromocionDto } from '../../models/dtos/Suscripciones/PromocionDto';
|
||||
import type { CreatePromocionDto, UpdatePromocionDto } from '../../models/dtos/Suscripciones/CreatePromocionDto';
|
||||
import PromocionFormModal from '../../components/Modals/Suscripciones/PromocionFormModal';
|
||||
|
||||
const GestionarPromocionesPage: React.FC = () => {
|
||||
const [promociones, setPromociones] = useState<PromocionDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroSoloActivas, setFiltroSoloActivas] = useState(true);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPromocion, setEditingPromocion] = useState<PromocionDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGestionar = isSuperAdmin || tienePermiso("SU010");
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (!puedeGestionar) {
|
||||
setError("No tiene permiso para gestionar promociones.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await promocionService.getAllPromociones(filtroSoloActivas);
|
||||
setPromociones(data);
|
||||
} catch (err) {
|
||||
setError("Error al cargar las promociones.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filtroSoloActivas, puedeGestionar]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarDatos();
|
||||
}, [cargarDatos]);
|
||||
|
||||
const handleOpenModal = (promocion?: PromocionDto) => {
|
||||
setEditingPromocion(promocion || null);
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingPromocion(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreatePromocionDto | UpdatePromocionDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingPromocion) {
|
||||
await promocionService.updatePromocion(id, data as UpdatePromocionDto);
|
||||
} else {
|
||||
await promocionService.createPromocion(data as CreatePromocionDto);
|
||||
}
|
||||
cargarDatos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al guardar la promoción.';
|
||||
setApiErrorMessage(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return 'Indefinido';
|
||||
const parts = dateString.split('-');
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
};
|
||||
|
||||
const formatTipo = (tipo: string) => {
|
||||
if (tipo === 'MontoFijo') return 'Monto Fijo';
|
||||
if (tipo === 'Porcentaje') return 'Porcentaje';
|
||||
return tipo;
|
||||
};
|
||||
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
if (error) return <Alert severity="error" sx={{m: 2}}>{error}</Alert>;
|
||||
if (!puedeGestionar) return <Alert severity="error" sx={{m: 2}}>Acceso Denegado.</Alert>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Promociones</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormControlLabel control={<Switch checked={filtroSoloActivas} onChange={(e) => setFiltroSoloActivas(e.target.checked)} />} label="Ver Solo Activas" />
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>
|
||||
Nueva Promoción
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Descripción</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Tipo</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold'}}>Valor</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Inicio</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Fin</TableCell>
|
||||
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{promociones.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron promociones.</TableCell></TableRow>
|
||||
) : (
|
||||
promociones.map(p => (
|
||||
<TableRow key={p.idPromocion} hover>
|
||||
<TableCell>{p.descripcion}</TableCell>
|
||||
<TableCell>{formatTipo(p.tipoPromocion)}</TableCell>
|
||||
<TableCell align="right">{p.tipoPromocion === 'Porcentaje' ? `${p.valor}%` : `$${p.valor.toFixed(2)}`}</TableCell>
|
||||
<TableCell>{formatDate(p.fechaInicio)}</TableCell>
|
||||
<TableCell>{formatDate(p.fechaFin)}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip label={p.activa ? 'Activa' : 'Inactiva'} color={p.activa ? 'success' : 'default'} size="small" />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Editar Promoción">
|
||||
<IconButton onClick={() => handleOpenModal(p)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<PromocionFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
initialData={editingPromocion}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPromocionesPage;
|
||||
@@ -13,6 +13,8 @@ import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import type { CreateSuscripcionDto } from '../../models/dtos/Suscripciones/CreateSuscripcionDto';
|
||||
import type { UpdateSuscripcionDto } from '../../models/dtos/Suscripciones/UpdateSuscripcionDto';
|
||||
import LoyaltyIcon from '@mui/icons-material/Loyalty';
|
||||
import GestionarPromocionesSuscripcionModal from '../../components/Modals/Suscripciones/GestionarPromocionesSuscripcionModal';
|
||||
|
||||
const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
const { idSuscriptor: idSuscriptorStr } = useParams<{ idSuscriptor: string }>();
|
||||
@@ -27,6 +29,9 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
const [editingSuscripcion, setEditingSuscripcion] = useState<SuscripcionDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [promocionesModalOpen, setPromocionesModalOpen] = useState(false);
|
||||
const [selectedSuscripcion, setSelectedSuscripcion] = useState<SuscripcionDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVer = isSuperAdmin || tienePermiso("SU001");
|
||||
const puedeGestionar = isSuperAdmin || tienePermiso("SU005");
|
||||
@@ -54,11 +59,11 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
}, [idSuscriptor, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarDatos(); }, [cargarDatos]);
|
||||
|
||||
|
||||
const handleOpenModal = (suscripcion?: SuscripcionDto) => {
|
||||
setEditingSuscripcion(suscripcion || null);
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
setEditingSuscripcion(suscripcion || null);
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
@@ -69,7 +74,7 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
const handleSubmitModal = async (data: CreateSuscripcionDto | UpdateSuscripcionDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if(id && editingSuscripcion) {
|
||||
if (id && editingSuscripcion) {
|
||||
await suscripcionService.updateSuscripcion(id, data as UpdateSuscripcionDto);
|
||||
} else {
|
||||
await suscripcionService.createSuscripcion(data as CreateSuscripcionDto);
|
||||
@@ -82,6 +87,11 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenPromocionesModal = (suscripcion: SuscripcionDto) => {
|
||||
setSelectedSuscripcion(suscripcion);
|
||||
setPromocionesModalOpen(true);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return 'Indefinido';
|
||||
// Asume que la fecha viene como "yyyy-MM-dd"
|
||||
@@ -90,8 +100,8 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
};
|
||||
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
if (error) return <Alert severity="error" sx={{m: 2}}>{error}</Alert>;
|
||||
if (!puedeVer) return <Alert severity="error" sx={{m: 2}}>Acceso Denegado.</Alert>
|
||||
if (error) return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
if (!puedeVer) return <Alert severity="error" sx={{ m: 2 }}>Acceso Denegado.</Alert>
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
@@ -102,22 +112,22 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
||||
Documento: {suscriptor?.tipoDocumento} {suscriptor?.nroDocumento} | Dirección: {suscriptor?.direccion}
|
||||
</Typography>
|
||||
|
||||
|
||||
{puedeGestionar && <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ my: 2 }}>Nueva Suscripción</Button>}
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Publicación</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Estado</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Días Entrega</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Inicio</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Fin</TableCell>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Observaciones</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Publicación</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Estado</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Días Entrega</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Inicio</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Fin</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Observaciones</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -128,10 +138,10 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
<TableRow key={s.idSuscripcion} hover>
|
||||
<TableCell>{s.nombrePublicacion}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={s.estado}
|
||||
color={s.estado === 'Activa' ? 'success' : s.estado === 'Pausada' ? 'warning' : 'default'}
|
||||
size="small"
|
||||
<Chip
|
||||
label={s.estado}
|
||||
color={s.estado === 'Activa' ? 'success' : s.estado === 'Pausada' ? 'warning' : 'default'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{s.diasEntrega.split(',').join(', ')}</TableCell>
|
||||
@@ -147,14 +157,23 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Editar Suscripción">
|
||||
<span><IconButton onClick={() => handleOpenModal(s)} disabled={!puedeGestionar}><EditIcon /></IconButton></span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Gestionar Promociones">
|
||||
<span><IconButton onClick={() => handleOpenPromocionesModal(s)} disabled={!puedeGestionar}><LoyaltyIcon /></IconButton></span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{idSuscriptor &&
|
||||
{idSuscriptor &&
|
||||
<SuscripcionFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
@@ -165,6 +184,11 @@ const GestionarSuscripcionesSuscriptorPage: React.FC = () => {
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
<GestionarPromocionesSuscripcionModal
|
||||
open={promocionesModalOpen}
|
||||
onClose={() => setPromocionesModalOpen(false)}
|
||||
suscripcion={selectedSuscripcion}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import { usePermissions } from '../../hooks/usePermissions';
|
||||
// Define las pestañas del módulo. Ajusta los permisos según sea necesario.
|
||||
const suscripcionesSubModules = [
|
||||
{ label: 'Suscriptores', path: 'suscriptores', requiredPermission: 'SU001' },
|
||||
// { label: 'Facturación', path: 'facturacion', requiredPermission: 'SU005' },
|
||||
// { label: 'Promociones', path: 'promociones', requiredPermission: 'SU006' },
|
||||
{ label: 'Facturación', path: 'facturacion', requiredPermission: 'SU006' },
|
||||
{ label: 'Promociones', path: 'promociones', requiredPermission: 'SU010' },
|
||||
];
|
||||
|
||||
const SuscripcionesIndexPage: React.FC = () => {
|
||||
|
||||
@@ -80,6 +80,8 @@ import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistM
|
||||
import SuscripcionesIndexPage from '../pages/Suscripciones/SuscripcionesIndexPage';
|
||||
import GestionarSuscriptoresPage from '../pages/Suscripciones/GestionarSuscriptoresPage';
|
||||
import GestionarSuscripcionesSuscriptorPage from '../pages/Suscripciones/GestionarSuscripcionesSuscriptorPage';
|
||||
import FacturacionPage from '../pages/Suscripciones/FacturacionPage';
|
||||
import GestionarPromocionesPage from '../pages/Suscripciones/GestionarPromocionesPage';
|
||||
|
||||
// Anonalías
|
||||
import AlertasPage from '../pages/Anomalia/AlertasPage';
|
||||
@@ -203,7 +205,16 @@ const AppRoutes = () => {
|
||||
<GestionarSuscripcionesSuscriptorPage />
|
||||
</SectionProtectedRoute>
|
||||
} />
|
||||
{/* Aquí irán las otras sub-rutas como 'facturacion', etc. */}
|
||||
<Route path="facturacion" element={
|
||||
<SectionProtectedRoute requiredPermission="SU006" sectionName="Facturación de Suscripciones">
|
||||
<FacturacionPage />
|
||||
</SectionProtectedRoute>
|
||||
} />
|
||||
<Route path="promociones" element={
|
||||
<SectionProtectedRoute requiredPermission="SU010" sectionName="Promociones">
|
||||
<GestionarPromocionesPage />
|
||||
</SectionProtectedRoute>
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo Contable (anidado) */}
|
||||
|
||||
74
Frontend/src/services/Suscripciones/facturacionService.ts
Normal file
74
Frontend/src/services/Suscripciones/facturacionService.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { FacturaDto } from '../../models/dtos/Suscripciones/FacturaDto';
|
||||
import type { GenerarFacturacionResponseDto } from '../../models/dtos/Suscripciones/GenerarFacturacionResponseDto';
|
||||
import type { PagoDto } from '../../models/dtos/Suscripciones/PagoDto';
|
||||
import type { CreatePagoDto } from '../../models/dtos/Suscripciones/CreatePagoDto';
|
||||
import type { ProcesamientoLoteResponseDto } from '../../models/dtos/Suscripciones/ProcesamientoLoteResponseDto';
|
||||
|
||||
const API_URL = '/facturacion';
|
||||
const DEBITOS_URL = '/debitos';
|
||||
const PAGOS_URL = '/pagos';
|
||||
const FACTURAS_URL = '/facturas';
|
||||
|
||||
const procesarArchivoRespuesta = async (archivo: File): Promise<ProcesamientoLoteResponseDto> => {
|
||||
const formData = new FormData();
|
||||
formData.append('archivo', archivo);
|
||||
|
||||
const response = await apiClient.post<ProcesamientoLoteResponseDto>(`${DEBITOS_URL}/procesar-respuesta`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getFacturasPorPeriodo = async (anio: number, mes: number): Promise<FacturaDto[]> => {
|
||||
const response = await apiClient.get<FacturaDto[]>(`${API_URL}/${anio}/${mes}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const generarFacturacionMensual = async (anio: number, mes: number): Promise<GenerarFacturacionResponseDto> => {
|
||||
const response = await apiClient.post<GenerarFacturacionResponseDto>(`${API_URL}/${anio}/${mes}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const generarArchivoDebito = async (anio: number, mes: number): Promise<{ fileContent: Blob, fileName: string }> => {
|
||||
const response = await apiClient.post(`${DEBITOS_URL}/${anio}/${mes}/generar-archivo`, {}, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
let fileName = `debito_${anio}_${mes}.txt`;
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||
if (fileNameMatch && fileNameMatch.length > 1) {
|
||||
fileName = fileNameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
return { fileContent: response.data, fileName: fileName };
|
||||
};
|
||||
|
||||
const getPagosPorFactura = async (idFactura: number): Promise<PagoDto[]> => {
|
||||
const response = await apiClient.get<PagoDto[]>(`${FACTURAS_URL}/${idFactura}/pagos`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const registrarPagoManual = async (data: CreatePagoDto): Promise<PagoDto> => {
|
||||
const response = await apiClient.post<PagoDto>(PAGOS_URL, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const enviarFacturaPorEmail = async (idFactura: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL}/${idFactura}/enviar-email`);
|
||||
};
|
||||
|
||||
export default {
|
||||
procesarArchivoRespuesta,
|
||||
getFacturasPorPeriodo,
|
||||
generarFacturacionMensual,
|
||||
generarArchivoDebito,
|
||||
getPagosPorFactura,
|
||||
registrarPagoManual,
|
||||
enviarFacturaPorEmail,
|
||||
};
|
||||
31
Frontend/src/services/Suscripciones/promocionService.ts
Normal file
31
Frontend/src/services/Suscripciones/promocionService.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { PromocionDto } from '../../models/dtos/Suscripciones/PromocionDto';
|
||||
import type { CreatePromocionDto, UpdatePromocionDto } from '../../models/dtos/Suscripciones/CreatePromocionDto';
|
||||
|
||||
const API_URL = '/promociones';
|
||||
|
||||
const getAllPromociones = async (soloActivas: boolean = true): Promise<PromocionDto[]> => {
|
||||
const response = await apiClient.get<PromocionDto[]>(`${API_URL}?soloActivas=${soloActivas}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPromocionById = async (id: number): Promise<PromocionDto> => {
|
||||
const response = await apiClient.get<PromocionDto>(`${API_URL}/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createPromocion = async (data: CreatePromocionDto): Promise<PromocionDto> => {
|
||||
const response = await apiClient.post<PromocionDto>(API_URL, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updatePromocion = async (id: number, data: UpdatePromocionDto): Promise<void> => {
|
||||
await apiClient.put(`${API_URL}/${id}`, data);
|
||||
};
|
||||
|
||||
export default {
|
||||
getAllPromociones,
|
||||
getPromocionById,
|
||||
createPromocion,
|
||||
updatePromocion
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
|
||||
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 { PromocionDto } from '../../models/dtos/Suscripciones/PromocionDto';
|
||||
|
||||
const API_URL_BASE = '/suscripciones';
|
||||
const API_URL_BY_SUSCRIPTOR = '/suscriptores'; // Para la ruta anidada
|
||||
@@ -25,9 +26,31 @@ const updateSuscripcion = async (id: number, data: UpdateSuscripcionDto): Promis
|
||||
await apiClient.put(`${API_URL_BASE}/${id}`, data);
|
||||
};
|
||||
|
||||
const getPromocionesAsignadas = async (idSuscripcion: number): Promise<PromocionDto[]> => {
|
||||
const response = await apiClient.get<PromocionDto[]>(`${API_URL_BASE}/${idSuscripcion}/promociones`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPromocionesDisponibles = async (idSuscripcion: number): Promise<PromocionDto[]> => {
|
||||
const response = await apiClient.get<PromocionDto[]>(`${API_URL_BASE}/${idSuscripcion}/promociones-disponibles`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const asignarPromocion = async (idSuscripcion: number, idPromocion: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL_BASE}/${idSuscripcion}/promociones/${idPromocion}`);
|
||||
};
|
||||
|
||||
const quitarPromocion = async (idSuscripcion: number, idPromocion: number): Promise<void> => {
|
||||
await apiClient.delete(`${API_URL_BASE}/${idSuscripcion}/promociones/${idPromocion}`);
|
||||
};
|
||||
|
||||
export default {
|
||||
getSuscripcionesPorSuscriptor,
|
||||
getSuscripcionById,
|
||||
createSuscripcion,
|
||||
updateSuscripcion,
|
||||
getPromocionesAsignadas,
|
||||
getPromocionesDisponibles,
|
||||
asignarPromocion,
|
||||
quitarPromocion
|
||||
};
|
||||
Reference in New Issue
Block a user