From b14c5de1b4cfb8c9102a12a02dc96f3a700638fa Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 31 Jul 2025 10:24:26 -0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Implementaci=C3=B3n=20de=20m=C3=B3dulos?= =?UTF-8?q?=20ABM=20de=20suscripciones=20por=20cliente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Suscripciones/SuscripcionesController.cs | 97 +++++++++ .../Suscripciones/UpdateSuscripcionDto.cs | 24 +++ Backend/GestionIntegral.Api/Program.cs | 1 + .../Suscripciones/ISuscripcionService.cs | 12 ++ .../Suscripciones/SuscripcionService.cs | 143 ++++++++++++ .../Suscripciones/SuscripcionFormModal.tsx | 202 +++++++++++++++++ .../Suscripciones/SuscriptorFormModal.tsx | 179 +++++++++++++++ Frontend/src/layouts/MainLayout.tsx | 1 + .../Suscripciones/CreateSuscripcionDto.ts | 9 + .../dtos/Suscripciones/SuscripcionDto.ts | 11 + .../Suscripciones/UpdateSuscripcionDto.ts | 7 + .../GestionarSuscripcionesSuscriptorPage.tsx | 172 +++++++++++++++ .../GestionarSuscriptoresPage.tsx | 203 ++++++++++++++++++ .../Suscripciones/SuscripcionesIndexPage.tsx | 82 +++++++ Frontend/src/routes/AppRoutes.tsx | 28 +++ .../Suscripciones/suscripcionService.ts | 33 +++ 16 files changed, 1204 insertions(+) create mode 100644 Backend/GestionIntegral.Api/Controllers/Suscripciones/SuscripcionesController.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Suscripciones/UpdateSuscripcionDto.cs create mode 100644 Backend/GestionIntegral.Api/Services/Suscripciones/ISuscripcionService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Suscripciones/SuscripcionService.cs create mode 100644 Frontend/src/components/Modals/Suscripciones/SuscripcionFormModal.tsx create mode 100644 Frontend/src/components/Modals/Suscripciones/SuscriptorFormModal.tsx create mode 100644 Frontend/src/models/dtos/Suscripciones/CreateSuscripcionDto.ts create mode 100644 Frontend/src/models/dtos/Suscripciones/SuscripcionDto.ts create mode 100644 Frontend/src/models/dtos/Suscripciones/UpdateSuscripcionDto.ts create mode 100644 Frontend/src/pages/Suscripciones/GestionarSuscripcionesSuscriptorPage.tsx create mode 100644 Frontend/src/pages/Suscripciones/GestionarSuscriptoresPage.tsx create mode 100644 Frontend/src/pages/Suscripciones/SuscripcionesIndexPage.tsx create mode 100644 Frontend/src/services/Suscripciones/suscripcionService.ts diff --git a/Backend/GestionIntegral.Api/Controllers/Suscripciones/SuscripcionesController.cs b/Backend/GestionIntegral.Api/Controllers/Suscripciones/SuscripcionesController.cs new file mode 100644 index 0000000..24beb70 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Suscripciones/SuscripcionesController.cs @@ -0,0 +1,97 @@ +// Archivo: GestionIntegral.Api/Controllers/Suscripciones/SuscripcionesController.cs + +using GestionIntegral.Api.Dtos.Suscripciones; +using GestionIntegral.Api.Services.Suscripciones; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace GestionIntegral.Api.Controllers.Suscripciones +{ + [Route("api/suscripciones")] // Ruta base para acciones sobre una suscripción específica + [ApiController] + [Authorize] + public class SuscripcionesController : ControllerBase + { + private readonly ISuscripcionService _suscripcionService; + private readonly ILogger _logger; + + // Permisos (nuevos, a crear en la BD) + private const string PermisoGestionarSuscripciones = "SU005"; + + public SuscripcionesController(ISuscripcionService suscripcionService, ILogger logger) + { + _suscripcionService = suscripcionService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en SuscripcionesController."); + return null; + } + + // Endpoint anidado para obtener las suscripciones de un suscriptor + // GET: api/suscriptores/{idSuscriptor}/suscripciones + [HttpGet("~/api/suscriptores/{idSuscriptor:int}/suscripciones")] + public async Task GetBySuscriptor(int idSuscriptor) + { + // Se podría usar el permiso de ver suscriptores (SU001) o el de gestionar suscripciones (SU005) + if (!TienePermiso("SU001")) return Forbid(); + + var suscripciones = await _suscripcionService.ObtenerPorSuscriptorId(idSuscriptor); + return Ok(suscripciones); + } + + // GET: api/suscripciones/{id} + [HttpGet("{id:int}", Name = "GetSuscripcionById")] + public async Task GetById(int id) + { + if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); + var suscripcion = await _suscripcionService.ObtenerPorId(id); + if (suscripcion == null) return NotFound(); + return Ok(suscripcion); + } + + // POST: api/suscripciones + [HttpPost] + public async Task Create([FromBody] CreateSuscripcionDto createDto) + { + if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _suscripcionService.Crear(createDto, userId.Value); + + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear la suscripción."); + + return CreatedAtRoute("GetSuscripcionById", new { id = dto.IdSuscripcion }, dto); + } + + // PUT: api/suscripciones/{id} + [HttpPut("{id:int}")] + public async Task Update(int id, [FromBody] UpdateSuscripcionDto updateDto) + { + if (!TienePermiso(PermisoGestionarSuscripciones)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _suscripcionService.Actualizar(id, updateDto, userId.Value); + + if (!exito) + { + if (error != null && error.Contains("no encontrada")) return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Suscripciones/UpdateSuscripcionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Suscripciones/UpdateSuscripcionDto.cs new file mode 100644 index 0000000..6ac3609 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Suscripciones/UpdateSuscripcionDto.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Suscripciones +{ + public class UpdateSuscripcionDto + { + // No permitimos cambiar el suscriptor o la publicación una vez creada. + // Se debe cancelar y crear una nueva. + + [Required] + public DateTime FechaInicio { get; set; } + + public DateTime? FechaFin { get; set; } + + [Required] + public string Estado { get; set; } = string.Empty; + + [Required(ErrorMessage = "Debe especificar los días de entrega.")] + public List DiasEntrega { get; set; } = new List(); + + [StringLength(250)] + public string? Observaciones { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 8bd04e7..da98088 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -114,6 +114,7 @@ builder.Services.AddScoped(); // Servicios builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // --- SERVICIO DE HEALTH CHECKS --- // Añadimos una comprobación específica para SQL Server. diff --git a/Backend/GestionIntegral.Api/Services/Suscripciones/ISuscripcionService.cs b/Backend/GestionIntegral.Api/Services/Suscripciones/ISuscripcionService.cs new file mode 100644 index 0000000..925666e --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Suscripciones/ISuscripcionService.cs @@ -0,0 +1,12 @@ +using GestionIntegral.Api.Dtos.Suscripciones; + +namespace GestionIntegral.Api.Services.Suscripciones +{ + public interface ISuscripcionService + { + Task> ObtenerPorSuscriptorId(int idSuscriptor); + Task ObtenerPorId(int idSuscripcion); + Task<(SuscripcionDto? Suscripcion, string? Error)> Crear(CreateSuscripcionDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> Actualizar(int idSuscripcion, UpdateSuscripcionDto updateDto, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Suscripciones/SuscripcionService.cs b/Backend/GestionIntegral.Api/Services/Suscripciones/SuscripcionService.cs new file mode 100644 index 0000000..158e6a5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Suscripciones/SuscripcionService.cs @@ -0,0 +1,143 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Data.Repositories.Suscripciones; +using GestionIntegral.Api.Dtos.Suscripciones; +using GestionIntegral.Api.Models.Suscripciones; +using System.Data; + +namespace GestionIntegral.Api.Services.Suscripciones +{ + public class SuscripcionService : ISuscripcionService + { + private readonly ISuscripcionRepository _suscripcionRepository; + private readonly ISuscriptorRepository _suscriptorRepository; + private readonly IPublicacionRepository _publicacionRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public SuscripcionService( + ISuscripcionRepository suscripcionRepository, + ISuscriptorRepository suscriptorRepository, + IPublicacionRepository publicacionRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _suscripcionRepository = suscripcionRepository; + _suscriptorRepository = suscriptorRepository; + _publicacionRepository = publicacionRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task MapToDto(Suscripcion suscripcion) + { + if (suscripcion == null) return null; + var publicacion = await _publicacionRepository.GetByIdSimpleAsync(suscripcion.IdPublicacion); + return new SuscripcionDto + { + IdSuscripcion = suscripcion.IdSuscripcion, + IdSuscriptor = suscripcion.IdSuscriptor, + IdPublicacion = suscripcion.IdPublicacion, + NombrePublicacion = publicacion?.Nombre ?? "Desconocida", + FechaInicio = suscripcion.FechaInicio.ToString("yyyy-MM-dd"), + FechaFin = suscripcion.FechaFin?.ToString("yyyy-MM-dd"), + Estado = suscripcion.Estado, + DiasEntrega = suscripcion.DiasEntrega, + Observaciones = suscripcion.Observaciones + }; + } + + public async Task ObtenerPorId(int idSuscripcion) + { + var suscripcion = await _suscripcionRepository.GetByIdAsync(idSuscripcion); + if (suscripcion == null) + return null; + return await MapToDto(suscripcion); + } + + public async Task> ObtenerPorSuscriptorId(int idSuscriptor) + { + var suscripciones = await _suscripcionRepository.GetBySuscriptorIdAsync(idSuscriptor); + var dtosTasks = suscripciones.Select(s => MapToDto(s)); + var dtos = await Task.WhenAll(dtosTasks); + return dtos.Where(dto => dto != null)!; + } + + public async Task<(SuscripcionDto? Suscripcion, string? Error)> Crear(CreateSuscripcionDto createDto, int idUsuario) + { + if (await _suscriptorRepository.GetByIdAsync(createDto.IdSuscriptor) == null) + return (null, "El suscriptor no existe."); + if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) + return (null, "La publicación no existe."); + if (createDto.FechaFin.HasValue && createDto.FechaFin.Value < createDto.FechaInicio) + return (null, "La fecha de fin no puede ser anterior a la fecha de inicio."); + + var nuevaSuscripcion = new Suscripcion + { + IdSuscriptor = createDto.IdSuscriptor, + IdPublicacion = createDto.IdPublicacion, + FechaInicio = createDto.FechaInicio, + FechaFin = createDto.FechaFin, + Estado = createDto.Estado, + DiasEntrega = string.Join(",", createDto.DiasEntrega), + Observaciones = createDto.Observaciones, + IdUsuarioAlta = idUsuario + }; + + using var connection = _connectionFactory.CreateConnection(); + await (connection as System.Data.Common.DbConnection)!.OpenAsync(); + using var transaction = connection.BeginTransaction(); + try + { + var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction); + if (creada == null) throw new DataException("Error al crear la suscripción."); + + transaction.Commit(); + _logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario); + return (await MapToDto(creada), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error al crear suscripción para suscriptor ID {IdSuscriptor}", createDto.IdSuscriptor); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> Actualizar(int idSuscripcion, UpdateSuscripcionDto updateDto, int idUsuario) + { + var existente = await _suscripcionRepository.GetByIdAsync(idSuscripcion); + if (existente == null) return (false, "Suscripción no encontrada."); + + if (updateDto.FechaFin.HasValue && updateDto.FechaFin.Value < updateDto.FechaInicio) + return (false, "La fecha de fin no puede ser anterior a la fecha de inicio."); + + existente.FechaInicio = updateDto.FechaInicio; + existente.FechaFin = updateDto.FechaFin; + existente.Estado = updateDto.Estado; + existente.DiasEntrega = string.Join(",", updateDto.DiasEntrega); + existente.Observaciones = updateDto.Observaciones; + existente.IdUsuarioMod = idUsuario; + existente.FechaMod = DateTime.Now; + + using var connection = _connectionFactory.CreateConnection(); + await (connection as System.Data.Common.DbConnection)!.OpenAsync(); + using var transaction = connection.BeginTransaction(); + try + { + var actualizado = await _suscripcionRepository.UpdateAsync(existente, transaction); + if (!actualizado) throw new DataException("Error al actualizar la suscripción."); + + transaction.Commit(); + _logger.LogInformation("Suscripción ID {Id} actualizada por Usuario ID {UserId}.", idSuscripcion, idUsuario); + return (true, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error al actualizar suscripción ID: {IdSuscripcion}", idSuscripcion); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Frontend/src/components/Modals/Suscripciones/SuscripcionFormModal.tsx b/Frontend/src/components/Modals/Suscripciones/SuscripcionFormModal.tsx new file mode 100644 index 0000000..fcdb7cd --- /dev/null +++ b/Frontend/src/components/Modals/Suscripciones/SuscripcionFormModal.tsx @@ -0,0 +1,202 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, FormGroup, FormControlLabel, + Checkbox, type SelectChangeEvent, Paper +} from '@mui/material'; +import type { SuscripcionDto } from '../../../models/dtos/Suscripciones/SuscripcionDto'; +import type { CreateSuscripcionDto } from '../../../models/dtos/Suscripciones/CreateSuscripcionDto'; +import type { UpdateSuscripcionDto } from '../../../models/dtos/Suscripciones/UpdateSuscripcionDto'; +import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto'; +import publicacionService from '../../../services/Distribucion/publicacionService'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '95%', sm: '80%', md: '600px' }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +const dias = [ + { label: 'Lunes', value: 'L' }, { label: 'Martes', value: 'M' }, + { label: 'Miércoles', value: 'X' }, { label: 'Jueves', value: 'J' }, + { label: 'Viernes', value: 'V' }, { label: 'Sábado', value: 'S' }, + { label: 'Domingo', value: 'D' } +]; + +interface SuscripcionFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateSuscripcionDto | UpdateSuscripcionDto, id?: number) => Promise; + idSuscriptor: number; + initialData?: SuscripcionDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +// Usamos una interfaz local que contenga todos los campos posibles del formulario +interface FormState { + idPublicacion?: number | ''; + fechaInicio?: string; + fechaFin?: string | null; + estado?: 'Activa' | 'Pausada' | 'Cancelada'; + observaciones?: string; +} + +const SuscripcionFormModal: React.FC = ({ open, onClose, onSubmit, idSuscriptor, initialData, errorMessage, clearErrorMessage }) => { + const [formData, setFormData] = useState({}); + const [selectedDays, setSelectedDays] = useState>(new Set()); + const [publicaciones, setPublicaciones] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingPubs, setLoadingPubs] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchPublicaciones = async () => { + setLoadingPubs(true); + try { + const data = await publicacionService.getPublicacionesForDropdown(true); + setPublicaciones(data); + } catch (error) { + setLocalErrors(prev => ({ ...prev, publicaciones: 'Error al cargar publicaciones.' })); + } finally { + setLoadingPubs(false); + } + }; + + if (open) { + fetchPublicaciones(); + const diasEntrega = initialData?.diasEntrega ? new Set(initialData.diasEntrega.split(',')) : new Set(); + setSelectedDays(diasEntrega); + setFormData({ + idPublicacion: initialData?.idPublicacion || '', + fechaInicio: initialData?.fechaInicio || '', + fechaFin: initialData?.fechaFin || '', + estado: initialData?.estado || 'Activa', + observaciones: initialData?.observaciones || '' + }); + setLocalErrors({}); + } + }, [open, initialData]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!formData.idPublicacion) errors.idPublicacion = "Debe seleccionar una publicación."; + if (!formData.fechaInicio?.trim()) errors.fechaInicio = 'La Fecha de Inicio es obligatoria.'; + if (formData.fechaFin && formData.fechaInicio && new Date(formData.fechaFin) < new Date(formData.fechaInicio)) { + errors.fechaFin = 'La Fecha de Fin no puede ser anterior a la de inicio.'; + } + if (selectedDays.size === 0) errors.diasEntrega = "Debe seleccionar al menos un día de entrega."; + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleDayChange = (dayValue: string) => { + const newSelection = new Set(selectedDays); + if (newSelection.has(dayValue)) newSelection.delete(dayValue); + else newSelection.add(dayValue); + setSelectedDays(newSelection); + if (localErrors.diasEntrega) setLocalErrors(prev => ({...prev, diasEntrega: null})); + if (errorMessage) clearErrorMessage(); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null })); + if (errorMessage) clearErrorMessage(); + }; + + const handleSelectChange = (e: SelectChangeEvent) => { + 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) => { + e.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + let success = false; + try { + const dataToSubmit = { + ...formData, + fechaFin: formData.fechaFin || null, + diasEntrega: Array.from(selectedDays), + }; + + if (isEditing && initialData) { + await onSubmit(dataToSubmit as UpdateSuscripcionDto, initialData.idSuscripcion); + } else { + await onSubmit({ ...dataToSubmit, idSuscriptor, idPublicacion: Number(formData.idPublicacion) } as CreateSuscripcionDto); + } + success = true; + } catch (error) { + success = false; + } finally { + setLoading(false); + if (success) onClose(); + } + }; + + return ( + + + {isEditing ? 'Editar Suscripción' : 'Nueva Suscripción'} + + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + Días de Entrega * + + + {dias.map(d => handleDayChange(d.value)} disabled={loading}/>} label={d.label} />)} + + + {localErrors.diasEntrega && {localErrors.diasEntrega}} + + + + + + + Estado + + + + + {errorMessage && {errorMessage}} + + + + + + + + + ); +}; + +export default SuscripcionFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Suscripciones/SuscriptorFormModal.tsx b/Frontend/src/components/Modals/Suscripciones/SuscriptorFormModal.tsx new file mode 100644 index 0000000..2dc4d99 --- /dev/null +++ b/Frontend/src/components/Modals/Suscripciones/SuscriptorFormModal.tsx @@ -0,0 +1,179 @@ +// Archivo: Frontend/src/components/Modals/Suscripciones/SuscriptorFormModal.tsx + +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent } from '@mui/material'; // 1. Importar SelectChangeEvent +import type { SuscriptorDto } from '../../../models/dtos/Suscripciones/SuscriptorDto'; +import type { CreateSuscriptorDto } from '../../../models/dtos/Suscripciones/CreateSuscriptorDto'; +import type { UpdateSuscriptorDto } from '../../../models/dtos/Suscripciones/UpdateSuscriptorDto'; +import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto'; +import formaPagoService from '../../../services/Suscripciones/formaPagoService'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '95%', sm: '80%', md: '600px' }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface SuscriptorFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateSuscriptorDto | UpdateSuscriptorDto, id?: number) => Promise; + initialData?: SuscriptorDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const SuscriptorFormModal: React.FC = ({ + open, onClose, onSubmit, initialData, errorMessage, clearErrorMessage +}) => { + const [formData, setFormData] = useState>({}); + const [formasDePago, setFormasDePago] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingFormasPago, setLoadingFormasPago] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + const CBURequerido = formasDePago.find(fp => fp.idFormaPago === formData.idFormaPagoPreferida)?.requiereCBU ?? false; + + useEffect(() => { + const fetchFormasDePago = async () => { + setLoadingFormasPago(true); + try { + const data = await formaPagoService.getAllFormasDePago(); + setFormasDePago(data); + } catch (error) { + console.error("Error al cargar formas de pago", error); + setLocalErrors(prev => ({ ...prev, formasDePago: 'Error al cargar formas de pago.' })); + } finally { + setLoadingFormasPago(false); + } + }; + + if (open) { + fetchFormasDePago(); + setFormData(initialData || { + nombreCompleto: '', tipoDocumento: 'DNI', nroDocumento: '', cbu: '' + }); + setLocalErrors({}); + } + }, [open, initialData]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!formData.nombreCompleto?.trim()) errors.nombreCompleto = 'El nombre es obligatorio.'; + if (!formData.direccion?.trim()) errors.direccion = 'La dirección es obligatoria.'; + if (!formData.tipoDocumento) errors.tipoDocumento = 'El tipo de documento es obligatorio.'; + if (!formData.nroDocumento?.trim()) errors.nroDocumento = 'El número de documento es obligatorio.'; + if (!formData.idFormaPagoPreferida) errors.idFormaPagoPreferida = 'La forma de pago es obligatoria.'; + if (CBURequerido && (!formData.cbu || formData.cbu.trim().length !== 22)) { + errors.cbu = 'El CBU es obligatorio y debe tener 22 dígitos.'; + } + if (formData.email && !/^\S+@\S+\.\S+$/.test(formData.email)) { + errors.email = 'El formato del email no es válido.'; + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + if (localErrors[name]) { + setLocalErrors(prev => ({ ...prev, [name]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + // 2. Crear un handler específico para los Select + const handleSelectChange = (e: SelectChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + if (localErrors[name]) { + setLocalErrors(prev => ({ ...prev, [name]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + let success = false; + try { + const dataToSubmit = formData as CreateSuscriptorDto | UpdateSuscriptorDto; + await onSubmit(dataToSubmit, initialData?.idSuscriptor); + success = true; + } catch (error) { + success = false; + } finally { + setLoading(false); + if (success) { + onClose(); + } + } + }; + + return ( + + + + {isEditing ? 'Editar Suscriptor' : 'Nuevo Suscriptor'} + + + + + + + + + + + Tipo + {/* 3. Aplicar el nuevo handler a los Selects */} + + + + + + Forma de Pago + {/* 3. Aplicar el nuevo handler a los Selects */} + + {localErrors.idFormaPagoPreferida && {localErrors.idFormaPagoPreferida}} + + {CBURequerido && ( + + )} + + + {errorMessage && {errorMessage}} + + + + + + + + + ); +}; + +export default SuscriptorFormModal; \ No newline at end of file diff --git a/Frontend/src/layouts/MainLayout.tsx b/Frontend/src/layouts/MainLayout.tsx index a182c08..887aaf1 100644 --- a/Frontend/src/layouts/MainLayout.tsx +++ b/Frontend/src/layouts/MainLayout.tsx @@ -31,6 +31,7 @@ const getTipoAlertaLabel = (tipoAlerta: string): string => { const allAppModules = [ { label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible { label: 'Distribución', path: '/distribucion', requiredPermission: 'SS001' }, + { label: 'Suscripciones', path: '/suscripciones', requiredPermission: 'SS007' }, { label: 'Contables', path: '/contables', requiredPermission: 'SS002' }, { label: 'Impresión', path: '/impresion', requiredPermission: 'SS003' }, { label: 'Reportes', path: '/reportes', requiredPermission: 'SS004' }, diff --git a/Frontend/src/models/dtos/Suscripciones/CreateSuscripcionDto.ts b/Frontend/src/models/dtos/Suscripciones/CreateSuscripcionDto.ts new file mode 100644 index 0000000..e29ac2e --- /dev/null +++ b/Frontend/src/models/dtos/Suscripciones/CreateSuscripcionDto.ts @@ -0,0 +1,9 @@ +export interface CreateSuscripcionDto { + idSuscriptor: number; + idPublicacion: number; + fechaInicio: string; // "yyyy-MM-dd" + fechaFin?: string | null; + estado: 'Activa' | 'Pausada' | 'Cancelada'; + diasEntrega: string[]; // ["L", "M", "X"] + observaciones?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Suscripciones/SuscripcionDto.ts b/Frontend/src/models/dtos/Suscripciones/SuscripcionDto.ts new file mode 100644 index 0000000..3f7270b --- /dev/null +++ b/Frontend/src/models/dtos/Suscripciones/SuscripcionDto.ts @@ -0,0 +1,11 @@ +export interface SuscripcionDto { + idSuscripcion: number; + idSuscriptor: number; + idPublicacion: number; + nombrePublicacion: string; + fechaInicio: string; // "yyyy-MM-dd" + fechaFin?: string | null; + estado: 'Activa' | 'Pausada' | 'Cancelada'; + diasEntrega: string; // "L,M,X,J,V,S,D" + observaciones?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Suscripciones/UpdateSuscripcionDto.ts b/Frontend/src/models/dtos/Suscripciones/UpdateSuscripcionDto.ts new file mode 100644 index 0000000..b79f2ef --- /dev/null +++ b/Frontend/src/models/dtos/Suscripciones/UpdateSuscripcionDto.ts @@ -0,0 +1,7 @@ +export interface UpdateSuscripcionDto { + fechaInicio: string; // "yyyy-MM-dd" + fechaFin?: string | null; + estado: 'Activa' | 'Pausada' | 'Cancelada'; + diasEntrega: string[]; // ["L", "M", "X"] + observaciones?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/pages/Suscripciones/GestionarSuscripcionesSuscriptorPage.tsx b/Frontend/src/pages/Suscripciones/GestionarSuscripcionesSuscriptorPage.tsx new file mode 100644 index 0000000..261c60f --- /dev/null +++ b/Frontend/src/pages/Suscripciones/GestionarSuscripcionesSuscriptorPage.tsx @@ -0,0 +1,172 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Box, Typography, Button, Paper, IconButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, CircularProgress, Alert, Chip, Tooltip } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import EditIcon from '@mui/icons-material/Edit'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import suscripcionService from '../../services/Suscripciones/suscripcionService'; +import suscriptorService from '../../services/Suscripciones/suscriptorService'; +import type { SuscripcionDto } from '../../models/dtos/Suscripciones/SuscripcionDto'; +import type { SuscriptorDto } from '../../models/dtos/Suscripciones/SuscriptorDto'; +import SuscripcionFormModal from '../../components/Modals/Suscripciones/SuscripcionFormModal'; +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'; + +const GestionarSuscripcionesSuscriptorPage: React.FC = () => { + const { idSuscriptor: idSuscriptorStr } = useParams<{ idSuscriptor: string }>(); + const navigate = useNavigate(); + const idSuscriptor = Number(idSuscriptorStr); + + const [suscriptor, setSuscriptor] = useState(null); + const [suscripciones, setSuscripciones] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [modalOpen, setModalOpen] = useState(false); + const [editingSuscripcion, setEditingSuscripcion] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVer = isSuperAdmin || tienePermiso("SU001"); + const puedeGestionar = isSuperAdmin || tienePermiso("SU005"); + + const cargarDatos = useCallback(async () => { + if (isNaN(idSuscriptor)) { + setError("ID de Suscriptor inválido."); setLoading(false); return; + } + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const [suscriptorData, suscripcionesData] = await Promise.all([ + suscriptorService.getSuscriptorById(idSuscriptor), + suscripcionService.getSuscripcionesPorSuscriptor(idSuscriptor) + ]); + setSuscriptor(suscriptorData); + setSuscripciones(suscripcionesData); + } catch (err) { + setError('Error al cargar los datos del suscriptor y sus suscripciones.'); + } finally { + setLoading(false); + } + }, [idSuscriptor, puedeVer]); + + useEffect(() => { cargarDatos(); }, [cargarDatos]); + + const handleOpenModal = (suscripcion?: SuscripcionDto) => { + setEditingSuscripcion(suscripcion || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingSuscripcion(null); + }; + + const handleSubmitModal = async (data: CreateSuscripcionDto | UpdateSuscripcionDto, id?: number) => { + setApiErrorMessage(null); + try { + if(id && editingSuscripcion) { + await suscripcionService.updateSuscripcion(id, data as UpdateSuscripcionDto); + } else { + await suscripcionService.createSuscripcion(data as CreateSuscripcionDto); + } + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la suscripción.'; + setApiErrorMessage(message); + throw err; + } + }; + + const formatDate = (dateString?: string | null) => { + if (!dateString) return 'Indefinido'; + // Asume que la fecha viene como "yyyy-MM-dd" + const parts = dateString.split('-'); + return `${parts[2]}/${parts[1]}/${parts[0]}`; + }; + + if (loading) return ; + if (error) return {error}; + if (!puedeVer) return Acceso Denegado. + + return ( + + + Suscripciones de: {suscriptor?.nombreCompleto || 'Cargando...'} + + Documento: {suscriptor?.tipoDocumento} {suscriptor?.nroDocumento} | Dirección: {suscriptor?.direccion} + + + {puedeGestionar && } + + {apiErrorMessage && {apiErrorMessage}} + + + + + + Publicación + Estado + Días Entrega + Inicio + Fin + Observaciones + Acciones + + + + {suscripciones.length === 0 ? ( + Este cliente no tiene suscripciones. + ) : ( + suscripciones.map((s) => ( + + {s.nombrePublicacion} + + + + {s.diasEntrega.split(',').join(', ')} + {formatDate(s.fechaInicio)} + {formatDate(s.fechaFin)} + {s.observaciones || '-'} + + + + handleOpenModal(s)} disabled={!puedeGestionar}> + + + + + + + )) + )} + +
+
+ + {idSuscriptor && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarSuscripcionesSuscriptorPage; \ No newline at end of file diff --git a/Frontend/src/pages/Suscripciones/GestionarSuscriptoresPage.tsx b/Frontend/src/pages/Suscripciones/GestionarSuscriptoresPage.tsx new file mode 100644 index 0000000..4dff0b2 --- /dev/null +++ b/Frontend/src/pages/Suscripciones/GestionarSuscriptoresPage.tsx @@ -0,0 +1,203 @@ +// Archivo: Frontend/src/pages/Suscripciones/GestionarSuscriptoresPage.tsx + +import React, { useState, useEffect, useCallback } from 'react'; +import { Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, CircularProgress, Alert, Chip, ListItemIcon, ListItemText, FormControlLabel } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import ToggleOnIcon from '@mui/icons-material/ToggleOn'; +import ToggleOffIcon from '@mui/icons-material/ToggleOff'; +import suscriptorService from '../../services/Suscripciones/suscriptorService'; +import type { SuscriptorDto } from '../../models/dtos/Suscripciones/SuscriptorDto'; +import type { CreateSuscriptorDto } from '../../models/dtos/Suscripciones/CreateSuscriptorDto'; +import type { UpdateSuscriptorDto } from '../../models/dtos/Suscripciones/UpdateSuscriptorDto'; +import SuscriptorFormModal from '../../components/Modals/Suscripciones/SuscriptorFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; +import ArticleIcon from '@mui/icons-material/Article'; + +const GestionarSuscriptoresPage: React.FC = () => { + const [suscriptores, setSuscriptores] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNombre, setFiltroNombre] = useState(''); + const [filtroNroDoc, setFiltroNroDoc] = useState(''); + const [filtroSoloActivos, setFiltroSoloActivos] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [editingSuscriptor, setEditingSuscriptor] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(15); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + const puedeVer = isSuperAdmin || tienePermiso("SU001"); + const puedeCrear = isSuperAdmin || tienePermiso("SU002"); + const puedeModificar = isSuperAdmin || tienePermiso("SU003"); + const puedeActivarDesactivar = isSuperAdmin || tienePermiso("SU004"); + + const navigate = useNavigate(); + + const cargarSuscriptores = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); + setError(null); + setApiErrorMessage(null); + try { + const data = await suscriptorService.getAllSuscriptores(filtroNombre, filtroNroDoc, filtroSoloActivos); + setSuscriptores(data); + } catch (err) { + console.error(err); + setError('Error al cargar los suscriptores.'); + } finally { + setLoading(false); + } + }, [filtroNombre, filtroNroDoc, filtroSoloActivos, puedeVer]); + + useEffect(() => { + cargarSuscriptores(); + }, [cargarSuscriptores]); + + const handleOpenModal = (suscriptor?: SuscriptorDto) => { + setEditingSuscriptor(suscriptor || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingSuscriptor(null); + }; + + const handleSubmitModal = async (data: CreateSuscriptorDto | UpdateSuscriptorDto, id?: number) => { + setApiErrorMessage(null); + try { + if (id && editingSuscriptor) { + await suscriptorService.updateSuscriptor(id, data as UpdateSuscriptorDto); + } else { + await suscriptorService.createSuscriptor(data as CreateSuscriptorDto); + } + cargarSuscriptores(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Error al guardar el suscriptor.'; + setApiErrorMessage(message); + throw err; // Re-lanzar para que el modal sepa que falló + } + }; + + const handleToggleActivo = async (suscriptor: SuscriptorDto) => { + const action = suscriptor.activo ? 'desactivar' : 'activar'; + if (window.confirm(`¿Está seguro de que desea ${action} a ${suscriptor.nombreCompleto}?`)) { + setApiErrorMessage(null); + try { + if (suscriptor.activo) { + await suscriptorService.deactivateSuscriptor(suscriptor.idSuscriptor); + } else { + await suscriptorService.activateSuscriptor(suscriptor.idSuscriptor); + } + cargarSuscriptores(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${action} el suscriptor.`; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, suscriptor: SuscriptorDto) => { + setAnchorEl(event.currentTarget); + setSelectedRow(suscriptor); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedRow(null); + }; + + const handleNavigateToSuscripciones = (idSuscriptor: number) => { + navigate(`/suscripciones/suscriptor/${idSuscriptor}/suscripciones`); + handleMenuClose(); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const displayData = suscriptores.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!puedeVer) { + return {error || "No tiene permiso para ver esta sección."}; + } + + return ( + + Gestionar Suscriptores + + + setFiltroNombre(e.target.value)} sx={{ flexGrow: 1 }} /> + setFiltroNroDoc(e.target.value)} sx={{ flexGrow: 1 }} /> + setFiltroSoloActivos(e.target.checked)} />} label="Solo Activos" /> + + {puedeCrear && } + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && ( + <> + + + + NombreDocumentoDirección + Forma de PagoEstadoAcciones + + + {displayData.map((s) => ( + + {s.nombreCompleto} + {s.tipoDocumento} {s.nroDocumento} + {s.direccion} + {s.nombreFormaPagoPreferida} + + + handleMenuOpen(e, s)}> + + + ))} + +
+
+ + + )} + + + {selectedRow && puedeModificar && { handleOpenModal(selectedRow); handleMenuClose(); }}>Editar} + {selectedRow && handleNavigateToSuscripciones(selectedRow.idSuscriptor)}>Ver Suscripciones} + {selectedRow && puedeActivarDesactivar && ( + handleToggleActivo(selectedRow)}> + {selectedRow.activo ? : } + {selectedRow.activo ? 'Desactivar' : 'Activar'} + + )} + + + setApiErrorMessage(null)} /> +
+ ); +}; + +export default GestionarSuscriptoresPage; \ No newline at end of file diff --git a/Frontend/src/pages/Suscripciones/SuscripcionesIndexPage.tsx b/Frontend/src/pages/Suscripciones/SuscripcionesIndexPage.tsx new file mode 100644 index 0000000..1ff9279 --- /dev/null +++ b/Frontend/src/pages/Suscripciones/SuscripcionesIndexPage.tsx @@ -0,0 +1,82 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; +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' }, +]; + +const SuscripcionesIndexPage: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { tienePermiso, isSuperAdmin } = usePermissions(); + const [selectedSubTab, setSelectedSubTab] = useState(false); + + // Filtra los sub-módulos a los que el usuario tiene acceso + const accessibleSubModules = suscripcionesSubModules.filter( + (subModule) => isSuperAdmin || tienePermiso(subModule.requiredPermission) + ); + + useEffect(() => { + if (accessibleSubModules.length === 0) { + // Si no tiene acceso a ningún submódulo, no hacemos nada. + // El enrutador principal debería manejar esto. + return; + } + + const currentBasePath = '/suscripciones'; + const subPath = location.pathname.startsWith(`${currentBasePath}/`) + ? location.pathname.substring(currentBasePath.length + 1) + : (location.pathname === currentBasePath ? accessibleSubModules[0]?.path : undefined); + + const activeTabIndex = accessibleSubModules.findIndex( + (subModule) => subModule.path === subPath + ); + + if (activeTabIndex !== -1) { + setSelectedSubTab(activeTabIndex); + } else if (location.pathname === currentBasePath) { + navigate(accessibleSubModules[0].path, { replace: true }); + } else { + setSelectedSubTab(false); + } + }, [location.pathname, navigate, accessibleSubModules]); + + const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { + navigate(accessibleSubModules[newValue].path); + }; + + if (accessibleSubModules.length === 0) { + return No tiene permisos para acceder a este módulo.; + } + + return ( + + Módulo de Suscripciones + + + {accessibleSubModules.map((subModule) => ( + + ))} + + + + + + + ); +}; + +export default SuscripcionesIndexPage; \ No newline at end of file diff --git a/Frontend/src/routes/AppRoutes.tsx b/Frontend/src/routes/AppRoutes.tsx index 403f1bf..e8f4239 100644 --- a/Frontend/src/routes/AppRoutes.tsx +++ b/Frontend/src/routes/AppRoutes.tsx @@ -76,6 +76,11 @@ import GestionarNovedadesCanillaPage from '../pages/Distribucion/GestionarNoveda import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage'; import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage'; +// Suscripciones +import SuscripcionesIndexPage from '../pages/Suscripciones/SuscripcionesIndexPage'; +import GestionarSuscriptoresPage from '../pages/Suscripciones/GestionarSuscriptoresPage'; +import GestionarSuscripcionesSuscriptorPage from '../pages/Suscripciones/GestionarSuscripcionesSuscriptorPage'; + // Anonalías import AlertasPage from '../pages/Anomalia/AlertasPage'; @@ -178,6 +183,29 @@ const AppRoutes = () => { + {/* --- Módulo de Suscripciones --- */} + + + + } + > + } /> + + + + } /> + + + + } /> + {/* Aquí irán las otras sub-rutas como 'facturacion', etc. */} + + {/* Módulo Contable (anidado) */} => { + const response = await apiClient.get(`${API_URL_BY_SUSCRIPTOR}/${idSuscriptor}/suscripciones`); + return response.data; +}; + +const getSuscripcionById = async (id: number): Promise => { + const response = await apiClient.get(`${API_URL_BASE}/${id}`); + return response.data; +}; + +const createSuscripcion = async (data: CreateSuscripcionDto): Promise => { + const response = await apiClient.post(API_URL_BASE, data); + return response.data; +}; + +const updateSuscripcion = async (id: number, data: UpdateSuscripcionDto): Promise => { + await apiClient.put(`${API_URL_BASE}/${id}`, data); +}; + +export default { + getSuscripcionesPorSuscriptor, + getSuscripcionById, + createSuscripcion, + updateSuscripcion, +}; \ No newline at end of file