diff --git a/Core/Interfaces/IProfessionalDom.cs b/Core/Interfaces/IProfessionalDom.cs new file mode 100644 index 0000000..5bdbde8 --- /dev/null +++ b/Core/Interfaces/IProfessionalDom.cs @@ -0,0 +1,17 @@ +using Domain.Entities; +using Domain.Generics; + +namespace Core.Interfaces +{ + public interface IProfessionalDom + { + Task CreateAsync(EProfessional entity); + Task DeleteAsync(int id); + Task ExportFilteredProfessionalsToExcelAsync(ProfessionalSearchParams searchParams); + Task> GetAllAsync(int page = 1, int pageSize = 50); + Task GetByIdAsync(int id); + Task> SearchAsync(string? fullname, string? document, string? type, + int page = 1, int pageSize = 50); + Task UpdateAsync(EProfessional entity); + } +} diff --git a/Core/Interfaces/IProfessionalSpecialtyDom.cs b/Core/Interfaces/IProfessionalSpecialtyDom.cs new file mode 100644 index 0000000..bb2127f --- /dev/null +++ b/Core/Interfaces/IProfessionalSpecialtyDom.cs @@ -0,0 +1,10 @@ +using Domain.Entities; + +namespace Core.Interfaces +{ + public interface IProfessionalSpecialtyDom + { + Task> GetAllAsync(); + Task GetByNameAsync(string name); + } +} diff --git a/Core/Services/ProfessionalService.cs b/Core/Services/ProfessionalService.cs new file mode 100644 index 0000000..929bf6c --- /dev/null +++ b/Core/Services/ProfessionalService.cs @@ -0,0 +1,145 @@ +using Core.Interfaces; +using Domain.Entities; +using Domain.Generics; +using Models.Interfaces; +using System.Reflection; +using Transversal.Services; + +namespace Core.Services +{ + public class ProfessionalService : IProfessionalDom + { + #region Declaraciones y Constructor + private readonly IPhSProfessionalRepository _repository; + + public ProfessionalService(IPhSProfessionalRepository professionalRepository) + { + _repository = professionalRepository ?? throw new ArgumentNullException(nameof(professionalRepository)); + } + #endregion + #region Métodos + public async Task> GetAllAsync(int page = 1, int pageSize = 50) + { + try + { + return await _repository.GetAllAsync(page, pageSize); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + public async Task GetByIdAsync(int id) + { + try + { + return await _repository.GetByIdAsync(id); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + public async Task CreateAsync(EProfessional entity) + { + if (entity is null) + throw new ArgumentNullException(nameof(entity), "El profesional no puede ser nulo."); + + if (string.IsNullOrWhiteSpace(entity.Fullname)) + throw new ArgumentException("Debe ingresar el nombre completo del profesional.", nameof(entity.Fullname)); + + if (string.IsNullOrWhiteSpace(entity.DocumentNumber)) + throw new ArgumentException("Debe ingresar un número de documento.", nameof(entity.DocumentNumber)); + + return await _repository.CreateAsync(entity); + } + public async Task UpdateAsync(EProfessional entity) + { + if (entity is null) + throw new ArgumentNullException(nameof(entity), "El profesional no puede ser nulo."); + + if (string.IsNullOrWhiteSpace(entity.Fullname)) + throw new ArgumentException("Debe ingresar el nombre completo del profesional.", nameof(entity.Fullname)); + + if (string.IsNullOrWhiteSpace(entity.DocumentNumber)) + throw new ArgumentException("Debe ingresar un número de documento o matricula.", nameof(entity.DocumentNumber)); + + return await _repository.UpdateAsync(entity); + } + public async Task> SearchAsync( + string? fullname, string? document, string? type, + int page = 1, int pageSize = 50) + { + try + { + return await _repository.SearchAsync(fullname, document, type, page, pageSize); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + public async Task ExportFilteredProfessionalsToExcelAsync(ProfessionalSearchParams searchParams) + { + try + { + // Realiza la búsqueda de clientes con los parámetros proporcionados + var searchResult = await SearchAsync( + searchParams.Fullname, + searchParams.Document, + searchParams.Type, + searchParams.Page, + searchParams.PageSize + ); + + if (searchResult?.Items is null || !searchResult.Items.Any()) + throw new Exception("No se encontraron profesionales para exportar."); + + var stream = new XLSXExportBase(); + var professionalsData = searchResult.Items.Select(p => new + { + p.Id, + p.Fullname, + p.DocumenttypeName, + p.DocumentNumber, + Tipo = p.Type, + Especialidad = p.Specialty?.Name, + p.Email, + Teléfono1 = p.Phone1, + Teléfono2 = p.Phone2, + Dirección = p.Address, + Ciudad = p.City, + Provincia = p.Province, + CódigoPostal = p.Postalcode, + Matrícula = p.License, + p.Active, + FechaAlta = p.Createdat.ToString("dd/MM/yyyy") + }).ToList(); + + + return stream.ExportExcel(professionalsData); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + public async Task DeleteAsync(int id) + { + try + { + return await _repository.DeleteAsync(id); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + #endregion + } +} diff --git a/Core/Services/ProfessionalSpecialtyService.cs b/Core/Services/ProfessionalSpecialtyService.cs new file mode 100644 index 0000000..ddffe97 --- /dev/null +++ b/Core/Services/ProfessionalSpecialtyService.cs @@ -0,0 +1,44 @@ +using Core.Interfaces; +using Domain.Entities; +using Models.Interfaces; +using System.Reflection; + +namespace Core.Services +{ + public class ProfessionalSpecialtyService : IProfessionalSpecialtyDom + { + #region Declaraciones y Constructor + private readonly IPhSProfessionalSpecialtyRepository _repository; + public ProfessionalSpecialtyService(IPhSProfessionalSpecialtyRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + #endregion + #region Metodos de clase + public async Task> GetAllAsync() + { + try + { + return await _repository.GetAllAsync(); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{methodName} Message: {ex.Message}", ex); + } + } + public async Task GetByNameAsync(string name) + { + try + { + return await _repository.GetByNameAsync(name); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{methodName} Message: {ex.Message}", ex); + } + } + #endregion + } +} \ No newline at end of file diff --git a/Domain/Entities/EProfessional.cs b/Domain/Entities/EProfessional.cs new file mode 100644 index 0000000..486dae4 --- /dev/null +++ b/Domain/Entities/EProfessional.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace Domain.Entities +{ + public class EProfessional + { + public int Id { get; set; } + [Required(ErrorMessage = "Debe ingresar el nombre del profesional.")] + public string Fullname { get; set; } = null!; + public string? Address { get; set; } + public string? City { get; set; } + [Required(ErrorMessage = "Debe seleccionar una provincia.")] + public string? Province { get; set; } + public string? Postalcode { get; set; } + public string? Phone1 { get; set; } + public string? Phone2 { get; set; } + public string? License { get; set; } + public string? Email { get; set; } + public string? DocumenttypeName { get; set; } + public string? DocumentNumber { get; set; } + [Required(ErrorMessage = "Debe seleccionar un tipo de documento.")] + public string? Type { get; set; } + [Required(ErrorMessage = "Debe seleccionar la especialidad del profesional.")] + public int? SpecialtyId { get; set; } + public bool Active { get; set; } = true; + public DateTime Createdat { get; set; } + public virtual EProfessionalSpecialty? Specialty { get; set; } + } + +} diff --git a/Domain/Entities/EProfessionalSpecialty.cs b/Domain/Entities/EProfessionalSpecialty.cs new file mode 100644 index 0000000..64929e2 --- /dev/null +++ b/Domain/Entities/EProfessionalSpecialty.cs @@ -0,0 +1,9 @@ +namespace Domain.Entities +{ + public class EProfessionalSpecialty + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public bool Active { get; set; } + } +} diff --git a/Domain/Generics/ProfessionalSearchParams.cs b/Domain/Generics/ProfessionalSearchParams.cs new file mode 100644 index 0000000..527fbdf --- /dev/null +++ b/Domain/Generics/ProfessionalSearchParams.cs @@ -0,0 +1,9 @@ +namespace Domain.Generics +{ + public class ProfessionalSearchParams : PagedRequest + { + public string? Fullname { get; set; } + public string? Document{ get; set; } + public string? Type { get; set; } + } +} diff --git a/Domain/obj/Domain.csproj.nuget.g.props b/Domain/obj/Domain.csproj.nuget.g.props index 8b4eb7e..3efed38 100644 --- a/Domain/obj/Domain.csproj.nuget.g.props +++ b/Domain/obj/Domain.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.13.2 + 6.13.1 diff --git a/Models/Interfaces/IPhSProfessionalRepository.cs b/Models/Interfaces/IPhSProfessionalRepository.cs new file mode 100644 index 0000000..f60b0be --- /dev/null +++ b/Models/Interfaces/IPhSProfessionalRepository.cs @@ -0,0 +1,17 @@ + +using Domain.Entities; +using Domain.Generics; + +namespace Models.Interfaces +{ + public interface IPhSProfessionalRepository + { + Task CreateAsync(EProfessional entity); + Task DeleteAsync(int id); + Task> GetAllAsync(int page = 1, int pageSize = 50); + Task GetByIdAsync(int id); + Task> SearchAsync(string? fullname, string? document, string? type, + int page = 1, int pageSize = 50); + Task UpdateAsync(EProfessional entity); + } +} diff --git a/Models/Interfaces/IPhSProfessionalSpecialtyRepository.cs b/Models/Interfaces/IPhSProfessionalSpecialtyRepository.cs new file mode 100644 index 0000000..1a8c327 --- /dev/null +++ b/Models/Interfaces/IPhSProfessionalSpecialtyRepository.cs @@ -0,0 +1,10 @@ +using Domain.Entities; + +namespace Models.Interfaces +{ + public interface IPhSProfessionalSpecialtyRepository + { + Task> GetAllAsync(); + Task GetByNameAsync(string name); + } +} diff --git a/Models/Models/PhSProfessional.cs b/Models/Models/PhSProfessional.cs new file mode 100644 index 0000000..3055781 --- /dev/null +++ b/Models/Models/PhSProfessional.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhSProfessional +{ + public int Id { get; set; } + + public string Fullname { get; set; } = null!; + + public string? Address { get; set; } + + public string? City { get; set; } + + public string? Province { get; set; } + + public string? Postalcode { get; set; } + + public string? Phone1 { get; set; } + + public string? Phone2 { get; set; } + + public string? License { get; set; } + + public string? Email { get; set; } + + public string? DocumenttypeName { get; set; } + + public string? DocumentNumber { get; set; } + + public string? Type { get; set; } + + public int? SpecialtyId { get; set; } + + public bool Active { get; set; } + + public DateTime Createdat { get; set; } + + public virtual PhSProfessionalSpecialty? Specialty { get; set; } +} diff --git a/Models/Models/PhSProfessionalSpecialty.cs b/Models/Models/PhSProfessionalSpecialty.cs new file mode 100644 index 0000000..2f1b320 --- /dev/null +++ b/Models/Models/PhSProfessionalSpecialty.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhSProfessionalSpecialty +{ + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public bool Active { get; set; } + + public virtual ICollection PhSProfessionals { get; set; } = new List(); +} diff --git a/Models/Models/PhronCareOperationsHubContext.cs b/Models/Models/PhronCareOperationsHubContext.cs index 2612b26..130f8bc 100644 --- a/Models/Models/PhronCareOperationsHubContext.cs +++ b/Models/Models/PhronCareOperationsHubContext.cs @@ -39,6 +39,10 @@ public partial class PhronCareOperationsHubContext : DbContext public virtual DbSet PhSProductCategories { get; set; } + public virtual DbSet PhSProfessionals { get; set; } + + public virtual DbSet PhSProfessionalSpecialties { get; set; } + public virtual DbSet PhSQuoteDetails { get; set; } public virtual DbSet PhSQuoteHeaders { get; set; } @@ -52,7 +56,7 @@ public partial class PhronCareOperationsHubContext : DbContext } } #endregion - //=> optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True"); + //optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True"); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -410,6 +414,78 @@ public partial class PhronCareOperationsHubContext : DbContext .HasColumnName("name"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhS_Prof__3213E83F86B052AD"); + + entity.ToTable("PhS_Professionals"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Active) + .HasDefaultValue(true) + .HasColumnName("active"); + entity.Property(e => e.Address) + .HasMaxLength(250) + .HasColumnName("address"); + entity.Property(e => e.City) + .HasMaxLength(100) + .HasColumnName("city"); + entity.Property(e => e.Createdat) + .HasDefaultValueSql("(getdate())") + .HasColumnType("datetime") + .HasColumnName("createdat"); + entity.Property(e => e.DocumentNumber) + .HasMaxLength(50) + .HasColumnName("document_number"); + entity.Property(e => e.DocumenttypeName) + .HasMaxLength(100) + .HasColumnName("documenttype_name"); + entity.Property(e => e.Email) + .HasMaxLength(150) + .HasColumnName("email"); + entity.Property(e => e.Fullname) + .HasMaxLength(200) + .HasColumnName("fullname"); + entity.Property(e => e.License) + .HasMaxLength(50) + .HasColumnName("license"); + entity.Property(e => e.Phone1) + .HasMaxLength(50) + .HasColumnName("phone1"); + entity.Property(e => e.Phone2) + .HasMaxLength(50) + .HasColumnName("phone2"); + entity.Property(e => e.Postalcode) + .HasMaxLength(20) + .HasColumnName("postalcode"); + entity.Property(e => e.Province) + .HasMaxLength(100) + .HasColumnName("province"); + entity.Property(e => e.SpecialtyId).HasColumnName("specialty_id"); + entity.Property(e => e.Type) + .HasMaxLength(25) + .HasColumnName("type"); + + entity.HasOne(d => d.Specialty).WithMany(p => p.PhSProfessionals) + .HasForeignKey(d => d.SpecialtyId) + .HasConstraintName("FK_Professionals_Specialty"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhS_Prof__3213E83F14C8B007"); + + entity.ToTable("PhS_ProfessionalSpecialties"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Active) + .HasDefaultValue(true) + .HasColumnName("active"); + entity.Property(e => e.Name) + .HasMaxLength(100) + .HasColumnName("name"); + }); + modelBuilder.Entity(entity => { entity.ToTable("PhS_QuoteDetails"); diff --git a/Models/Repositories/PhSInstitutionReposotory.cs b/Models/Repositories/PhSInstitutionRepository.cs similarity index 100% rename from Models/Repositories/PhSInstitutionReposotory.cs rename to Models/Repositories/PhSInstitutionRepository.cs diff --git a/Models/Repositories/PhSProfessionalRepository.cs b/Models/Repositories/PhSProfessionalRepository.cs new file mode 100644 index 0000000..9c94bcf --- /dev/null +++ b/Models/Repositories/PhSProfessionalRepository.cs @@ -0,0 +1,124 @@ +using Domain.Entities; +using Domain.Generics; +using Microsoft.EntityFrameworkCore; +using Models.Helpers; +using Models.Interfaces; +using Models.Models; +using System.Reflection.Metadata; + +namespace Models.Repositories +{ + public class PhSProfessionalRepository(PhronCareOperationsHubContext context) : IPhSProfessionalRepository + { + #region Declaraciones y Constructor + private readonly PhronCareOperationsHubContext _context = context; + #endregion + #region Métodos de clase + public async Task> GetAllAsync(int page = 1, int pageSize = 50) + { + var query = _context.PhSProfessionals + .Include(p => p.Specialty) + .AsQueryable(); + + var pagedEntities = await query.ToPagedResultAsync(page, pageSize); + + return new PagedResult + { + Items = pagedEntities.Items.Select(EntityMapper.MapEntity), + TotalItems = pagedEntities.TotalItems, + Page = pagedEntities.Page, + PageSize = pagedEntities.PageSize + }; + } + public async Task GetByIdAsync(int id) + { + var professional = await _context.PhSProfessionals + .Include(p => p.Specialty) + .FirstOrDefaultAsync(p => p.Id == id); + + return professional != null ? EntityMapper.MapEntity(professional) : null; + } + public async Task> SearchAsync(string? fullname, string? document, string? type, + int page = 1, int pageSize = 50) + { + var query = _context.PhSProfessionals + .Include(p => p.Specialty) + .AsQueryable(); + if (!string.IsNullOrWhiteSpace(fullname)) query = query.Where(c => c.Fullname.ToLower().Contains(fullname.ToLower())); + if (!string.IsNullOrWhiteSpace(document) && document != "?") + { + query = query.Where(p => + EF.Functions.Like(p.DocumentNumber ?? "", $"%{document}%") || + EF.Functions.Like(p.License ?? "", $"%{document}%")); + } + if (!string.IsNullOrWhiteSpace(type)) query = query.Where(c => c.Type.ToLower().Contains(type.ToLower())); + + var pagedEntities = await query.ToPagedResultAsync(page, pageSize); + + return new PagedResult + { + Items = pagedEntities.Items.Select(EntityMapper.MapEntity), + TotalItems = pagedEntities.TotalItems, + Page = pagedEntities.Page, + PageSize = pagedEntities.PageSize + }; + } + public async Task CreateAsync(EProfessional entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity), "El profesional no puede ser nulo."); + + try + { + var dbEntity = EntityMapper.MapEntity(entity); + await _context.PhSProfessionals.AddAsync(dbEntity); + await _context.SaveChangesAsync(); + + return EntityMapper.MapEntity(dbEntity); + } + catch (DbUpdateException dbEx) + { + throw new Exception("Error al guardar el profesional en la base de datos.", dbEx); + } + catch (Exception ex) + { + throw new Exception("Error inesperado al crear el profesional: " + ex.Message, ex); + } + } + public async Task UpdateAsync(EProfessional entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + try + { + var existing = await _context.PhSProfessionals + .FirstOrDefaultAsync(p => p.Id == entity.Id); + + if (existing == null) + return false; + + EntityMapper.MapEntityToExisting(entity, existing); + await _context.SaveChangesAsync(); + + return true; + } + catch (Exception) + { + return false; + } + } + public async Task DeleteAsync(int id) + { + var entity = await _context.PhSProfessionals.FindAsync(id); + if (entity == null) + return false; + + _context.PhSProfessionals.Remove(entity); + await _context.SaveChangesAsync(); + + return true; + } + #endregion + } +} \ No newline at end of file diff --git a/Models/Repositories/PhSProfessionalSpecialtyRepository.cs b/Models/Repositories/PhSProfessionalSpecialtyRepository.cs new file mode 100644 index 0000000..5206599 --- /dev/null +++ b/Models/Repositories/PhSProfessionalSpecialtyRepository.cs @@ -0,0 +1,28 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Models.Helpers; +using Models.Interfaces; +using Models.Models; + +namespace Models.Repositories +{ + public class PhSProfessionalSpecialtyRepository(PhronCareOperationsHubContext context) : IPhSProfessionalSpecialtyRepository + { + #region Declaraciones y Constructor + private readonly PhronCareOperationsHubContext _context = context; + #endregion + #region Metodos de clase + public async Task> GetAllAsync() + { + var specialties = await _context.PhSProfessionalSpecialties.ToListAsync(); + return specialties.Select(EntityMapper.MapEntity); + } + + public async Task GetByNameAsync(string name) + { + var professionalSpecialty = await _context.PhSProfessionalSpecialties.FirstOrDefaultAsync(a => a.Name.Contains(name)); + return professionalSpecialty != null ? EntityMapper.MapEntity(professionalSpecialty) : null; + } + #endregion + } +} diff --git a/Models/obj/Models.csproj.nuget.g.props b/Models/obj/Models.csproj.nuget.g.props index 57bd42c..d21a6b0 100644 --- a/Models/obj/Models.csproj.nuget.g.props +++ b/Models/obj/Models.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.13.2 + 6.13.1 diff --git a/phronCare.API/Controllers/Sales/ProfessionalController.cs b/phronCare.API/Controllers/Sales/ProfessionalController.cs new file mode 100644 index 0000000..f02d9b9 --- /dev/null +++ b/phronCare.API/Controllers/Sales/ProfessionalController.cs @@ -0,0 +1,137 @@ +using Core.Interfaces; +using Domain.Entities; +using Domain.Generics; +using Microsoft.AspNetCore.Mvc; +using System.Reflection; + +namespace phronCare.API.Controllers.Sales +{ + [Route("api/[controller]")] + [ApiController] + public class ProfessionalController : ControllerBase + { + private readonly IProfessionalDom _professionalService; + + public ProfessionalController(IProfessionalDom professionalService) + { + _professionalService = professionalService ?? throw new ArgumentNullException(nameof(professionalService)); + } + + [HttpGet("all")] + public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + try + { + var result = await _professionalService.GetAllAsync(page, pageSize); + return Ok(result); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + return StatusCode(500, $"{methodName} Message: {ex.Message}"); + } + } + + [HttpGet("search")] + public async Task Search( + [FromQuery] string? fullname, + [FromQuery] string? document, + [FromQuery] string? type, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 50) + { + try + { + var result = await _professionalService.SearchAsync(fullname, document, type, page, pageSize); + return Ok(result); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + return StatusCode(500, $"{methodName} Message: {ex.Message}"); + } + } + + + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + try + { + var professional = await _professionalService.GetByIdAsync(id); + if (professional == null) + return NotFound(); + + return Ok(professional); + } + catch (Exception ex) + { + return StatusCode(500, $"Error: {ex.Message}"); + } + } + + [HttpPost("create")] + public async Task Create([FromBody] EProfessional professional) + { + try + { + if (professional == null) + return BadRequest("El profesional no puede ser nulo."); + + var result = await _professionalService.CreateAsync(professional); + return Ok(result); + } + catch (ArgumentNullException ex) + { + return BadRequest($"Validación fallida: {ex.Message}"); + } + catch (InvalidOperationException ex) + { + return BadRequest($"Error de negocio: {ex.Message}"); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + return StatusCode(500, $"{methodName} Message: {ex.Message}"); + } + } + + [HttpPut("update")] + public async Task Update([FromBody] EProfessional professional) + { + try + { + if (professional == null || professional.Id <= 0) + return BadRequest("El profesional es inválido o no tiene un ID válido."); + + var success = await _professionalService.UpdateAsync(professional); + + if (!success) + return NotFound($"No se encontró un profesional con ID {professional.Id}."); + + return Ok("Profesional actualizado correctamente."); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + return StatusCode(500, $"{methodName} Message: {ex.Message}"); + } + } + + [HttpPost("exportfiltered")] + public async Task ExportFiltered([FromBody] ProfessionalSearchParams searchParams) + { + try + { + var file = await _professionalService.ExportFilteredProfessionalsToExcelAsync(searchParams); + return File(file, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Profesionales.xlsx"); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} diff --git a/phronCare.API/Controllers/Sales/ProfessionalSpecialtyController.cs b/phronCare.API/Controllers/Sales/ProfessionalSpecialtyController.cs new file mode 100644 index 0000000..8b90301 --- /dev/null +++ b/phronCare.API/Controllers/Sales/ProfessionalSpecialtyController.cs @@ -0,0 +1,39 @@ +using Core.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace phronCare.API.Controllers.Sales +{ + [Route("api/[controller]")] + [ApiController] + public class ProfessionalSpecialtyController : ControllerBase + { + private readonly IProfessionalSpecialtyDom _professionalSpecialtyService; + public ProfessionalSpecialtyController(IProfessionalSpecialtyDom professionalSpecialtyService) + { + _professionalSpecialtyService = professionalSpecialtyService ?? throw new ArgumentNullException(nameof(professionalSpecialtyService)); + } + [HttpGet("GetAll")] + public async Task GetAll() + { + try + { + var result = await _professionalSpecialtyService.GetAllAsync(); + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + [HttpGet("GetByName/{name}")] + public async Task GetByName(string name) + { + var result = await _professionalSpecialtyService.GetByNameAsync(name); + if (result == null) + return NotFound($"No se encontró un tipo de cuenta con el nombre '{name}'."); + + return Ok(result); + } + + } +} diff --git a/phronCare.API/Program.cs b/phronCare.API/Program.cs index 5178a35..216a01e 100644 --- a/phronCare.API/Program.cs +++ b/phronCare.API/Program.cs @@ -62,6 +62,12 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion diff --git a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json index 2f94efb..7d38dfd 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -1049,6 +1049,163 @@ ], "ReturnTypes": [] }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalController", + "Method": "GetById", + "RelativePath": "api/Professional/{id}", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "id", + "Type": "System.Int32", + "IsRequired": true + } + ], + "ReturnTypes": [ + { + "Type": "Domain.Entities.EProfessional", + "MediaTypes": [ + "text/plain", + "application/json", + "text/json" + ], + "StatusCode": 200 + } + ] + }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalController", + "Method": "GetAll", + "RelativePath": "api/Professional/all", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "page", + "Type": "System.Int32", + "IsRequired": false + }, + { + "Name": "pageSize", + "Type": "System.Int32", + "IsRequired": false + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalController", + "Method": "Create", + "RelativePath": "api/Professional/create", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "professional", + "Type": "Domain.Entities.EProfessional", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalController", + "Method": "ExportFiltered", + "RelativePath": "api/Professional/exportfiltered", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "searchParams", + "Type": "Domain.Generics.ProfessionalSearchParams", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalController", + "Method": "Search", + "RelativePath": "api/Professional/search", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "fullname", + "Type": "System.String", + "IsRequired": false + }, + { + "Name": "document", + "Type": "System.String", + "IsRequired": false + }, + { + "Name": "type", + "Type": "System.String", + "IsRequired": false + }, + { + "Name": "page", + "Type": "System.Int32", + "IsRequired": false + }, + { + "Name": "pageSize", + "Type": "System.Int32", + "IsRequired": false + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalController", + "Method": "Update", + "RelativePath": "api/Professional/update", + "HttpMethod": "PUT", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "professional", + "Type": "Domain.Entities.EProfessional", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalSpecialtyController", + "Method": "GetAll", + "RelativePath": "api/ProfessionalSpecialty/GetAll", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalSpecialtyController", + "Method": "GetByName", + "RelativePath": "api/ProfessionalSpecialty/GetByName/{name}", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "name", + "Type": "System.String", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, { "ContainingType": "phronCare.API.Controllers.Sales.TaxConditionController", "Method": "GetAll", diff --git a/phronCare.UIBlazor/Pages/Sales/ProfessionalForm.razor b/phronCare.UIBlazor/Pages/Sales/ProfessionalForm.razor new file mode 100644 index 0000000..cb4f2af --- /dev/null +++ b/phronCare.UIBlazor/Pages/Sales/ProfessionalForm.razor @@ -0,0 +1,210 @@ +@page "/sales/professionalform/" +@page "/sales/professionalform/{Id:int?}" +@using phronCare.UIBlazor.Services.Sales +@inject ProfessionalService ProfessionalService +@inject DocumentTypeService documentTypeService +@inject ProfessionalSpecialtyService specialtyService + + +@inject NavigationManager Navigation +@inject IToastService toastService + + + + + +
+
+

@((model.Id == 0) ? "Nuevo Profesional" : "Editar Profesional")

+
+ +
+
+
+ + + +
+
+ +
+
+ + + + @foreach (var type in documentTypes) + { + + } + + +
+ +
+ + + +
+ +
+ + +
+
+ +
+
+ + + +
+ +
+ + + +
+
+ +
+
+ + + +
+ +
+ + + @foreach (var option in professionalTypes) + { + + } + + +
+
+ + + + @foreach (var s in specialties) + { + + } + + +
+
+
+ + +
+
+
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + + @foreach (var province in provinces) + { + + } + + +
+
+
+ + +
+
+ +@code { + [Parameter] + public int? Id { get; set; } + + private EProfessional model = new(); + private List documentTypes = new(); + private List specialties = new(); + + private List provinces = new() + { + "Buenos Aires", "CABA", "Catamarca", "Chaco", "Chubut", "Córdoba", "Corrientes", + "Entre Ríos", "Formosa", "Jujuy", "La Pampa", "La Rioja", "Mendoza", "Misiones", + "Neuquén", "Río Negro", "Salta", "San Juan", "San Luis", "Santa Cruz", + "Santa Fe", "Santiago del Estero", "Tierra del Fuego", "Tucumán" + }; + private List<(string Value, string Text)> professionalTypes = new() + { + ("","--- Seleccionar ---"), + ("Medico", "Médico"), + ("Instrumentador", "Instrumentador quirúrgico"), + ("Enfermero", "Enfermero/a"), + ("Tecnico", "Técnico quirúrgico") + }; + protected override async Task OnInitializedAsync() + { + await LoadDocumentTypes(); + await LoadSpecialties(); + + + if (Id.HasValue && Id > 0) + { + var result = await ProfessionalService.GetById(Id.Value); + if (result != null) + model = result; + else + toastService.ShowError("No se pudo cargar el profesional."); + } + } + private async Task LoadDocumentTypes() + { + documentTypes = await documentTypeService.GetAllAsync(); + } + private async Task LoadSpecialties() + { + specialties = await specialtyService.GetAllAsync(); + } + private async Task HandleValidSubmit() + { + var result = model.Id == 0 + ? await ProfessionalService.CreateAsync(model) + : await ProfessionalService.UpdateAsync(model); + + if (result.IsSuccessStatusCode) + { + toastService.ShowSuccess("Profesional guardado correctamente."); + Navigation.NavigateTo("/sales/professionals"); + } + else + { + var error = await result.Content.ReadAsStringAsync(); + toastService.ShowError($"Error: {error}"); + } + } + + private void Cancel() + { + Navigation.NavigateTo("/sales/professionals"); + } +} \ No newline at end of file diff --git a/phronCare.UIBlazor/Pages/Sales/Professionals.razor b/phronCare.UIBlazor/Pages/Sales/Professionals.razor new file mode 100644 index 0000000..7917478 --- /dev/null +++ b/phronCare.UIBlazor/Pages/Sales/Professionals.razor @@ -0,0 +1,210 @@ +@page "/sales/professionals" +@using phronCare.UIBlazor.Services.Sales +@using phronCare.UIBlazor.Data +@using Domain.Entities +@using Domain.Generics +@inject IToastService toastService +@inject NavigationManager Navigation +@inject ProfessionalService professionalService + +
+
+

Búsqueda de profesionales

+
+
+
+ + + + + + + +
+
+
+ @if (TablaProfesionales != null && TablaProfesionales.Any()) + { + + } + else + { +

No hay resultados.

+ } +
+
+ +
+ +@code { + private ProfessionalSearchParams SearchParams = new(); + private PagedResult? PagedResult; + private List> TablaProfesionales = new(); + private List TableColumns = new() + { + "Id", "Nombre", "Tipo Documento", "N° Documento", "Tipo", "Especialidad", + "Email", "Teléfono 1", "Teléfono 2", "Matrícula", "Activo" + }; + private int PaginaDeseada = 1; + private List botones = new(); + + protected override void OnInitialized() + { + botones = new List + { + new PhTable.ButtonOptions + { + Caption = "Editar", + ElementClass = "btn btn-primary btn-sm", + UrlAction = "/sales/professionals/edit/", + OnClickAction = async (id) => + { + if (int.TryParse(id, out var profId)) + { + Navigation.NavigateTo($"/sales/professionalform/{profId}"); + } + } + } + }; + } + + private async Task BuscarProfesionales() + { + await CargarProfesionales(); + } + + private async Task CargarProfesionales() + { + PagedResult = await professionalService.SearchAsync(SearchParams); + if (PagedResult?.Items is not null) + { + TablaProfesionales = PagedResult.Items.Select(p => new Dictionary +{ + { "Id", p.Id }, + { "Nombre", p.Fullname ?? string.Empty }, + { "Tipo Documento", p.DocumenttypeName ?? string.Empty }, + { "N° Documento", p.DocumentNumber ?? string.Empty }, + { "Tipo", p.Type ?? string.Empty }, + { "Especialidad", p.Specialty?.Name ?? string.Empty }, + { "Email", p.Email ?? string.Empty }, + { "Teléfono 1", p.Phone1 ?? string.Empty }, + { "Teléfono 2", p.Phone2 ?? string.Empty }, + { "Matrícula", p.License ?? string.Empty }, + { "Activo", p.Active ? "Sí" : "No" }, +}).ToList(); + } + } + + private async Task ExportarExcel() + { + var searchParams = new ProfessionalSearchParams + { + Fullname = SearchParams.Fullname, + Document = SearchParams.Document, + Type = SearchParams.Type, + Page = 1, + PageSize = int.MaxValue + }; + try + { + await professionalService.ExportFilteredAsync(searchParams); + toastService.ShowSuccess("Exportación completada exitosamente."); + } + catch (Exception ex) + { + toastService.ShowError($"{ex.Message}"); + } + } + + private async Task PrimeraPagina() + { + SearchParams.Page = 1; + await BuscarProfesionales(); + } + + private async Task UltimaPagina() + { + SearchParams.Page = TotalPaginas; + await BuscarProfesionales(); + } + + private async Task IrAPagina() + { + if (PaginaDeseada >= 1 && PaginaDeseada <= TotalPaginas) + { + SearchParams.Page = PaginaDeseada; + await BuscarProfesionales(); + } + else + { + toastService.ShowWarning("Número de página fuera de rango."); + } + } + + private async Task SiguientePagina() => await CambiarPagina(1); + private async Task AnteriorPagina() => await CambiarPagina(-1); + private async Task CambiarPagina(int delta) + { + var nuevaPagina = SearchParams.Page + delta; + if (nuevaPagina >= 1 && nuevaPagina <= TotalPaginas) + { + SearchParams.Page = nuevaPagina; + await BuscarProfesionales(); + } + } + + private int TotalPaginas => PagedResult is null ? 1 : + (int)Math.Ceiling((double)(PagedResult.TotalItems) / SearchParams.PageSize); + + private bool PuedeRetroceder => PagedResult != null && SearchParams.Page > 1; + private bool PuedeAvanzar => PagedResult != null && SearchParams.Page < TotalPaginas; + + private void NuevoProfesional() + { + Navigation.NavigateTo("/sales/professionalform/"); + } + + public void Cancel() + { + Navigation.NavigateTo("/DashboardPanel"); + } +} diff --git a/phronCare.UIBlazor/Program.cs b/phronCare.UIBlazor/Program.cs index 33a9238..73fa9a8 100644 --- a/phronCare.UIBlazor/Program.cs +++ b/phronCare.UIBlazor/Program.cs @@ -47,6 +47,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion #region UI diff --git a/phronCare.UIBlazor/Services/Sales/ProfessionalService.cs b/phronCare.UIBlazor/Services/Sales/ProfessionalService.cs new file mode 100644 index 0000000..93f8bb2 --- /dev/null +++ b/phronCare.UIBlazor/Services/Sales/ProfessionalService.cs @@ -0,0 +1,72 @@ +using Domain.Entities; +using Domain.Generics; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text; +using Microsoft.JSInterop; +using System.Reflection; +using System.Net.Http; +using phronCare.UIBlazor.Pages.Sales; + +namespace phronCare.UIBlazor.Services.Sales +{ + public class ProfessionalService + { + private readonly HttpClient _http; + private readonly IJSRuntime _js; + public ProfessionalService(HttpClient http, IJSRuntime js) + { + _http = http; + _js = js; + } + public async Task?> SearchAsync(ProfessionalSearchParams searchParams) + { + var url = $"api/Professional/search?" + + $"fullname={searchParams.Fullname}&" + + $"document={searchParams.Document}&" + + $"type={searchParams.Type}&" + + $"page={searchParams.Page}&" + + $"pageSize={searchParams.PageSize}"; + return await _http.GetFromJsonAsync>(url); + } + + public async Task GetById(int id) + { + return await _http.GetFromJsonAsync($"/api/Professional/{id}") ; + } + public async Task CreateAsync(EProfessional professional) + { + return await _http.PostAsJsonAsync("/api/Professional/Create", professional); + } + public async Task UpdateAsync(EProfessional professional) + { + return await _http.PutAsJsonAsync("/api/Professional/Update", professional); + } + public async Task ExportFilteredAsync(ProfessionalSearchParams searchParams) + { + try + { + var content = new StringContent(JsonSerializer.Serialize(searchParams), Encoding.UTF8, "application/json"); + var response = await _http.PostAsync("api/Professional/exportfiltered", content); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new Exception(errorContent); + } + + var bytes = await response.Content.ReadAsByteArrayAsync(); + var base64 = Convert.ToBase64String(bytes); + var timestamp = DateTime.Now.ToString("yyyyMMddHHmm"); + var fileName = $"{timestamp}_profesionales.xlsx"; + await _js.InvokeVoidAsync("saveAsFile", fileName, base64); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + var message = ex.Message ?? "No message provided"; + throw new Exception($"{message}", ex); + } + } + } +} diff --git a/phronCare.UIBlazor/Services/Sales/ProfessionalSpecialtyService.cs b/phronCare.UIBlazor/Services/Sales/ProfessionalSpecialtyService.cs new file mode 100644 index 0000000..043c73a --- /dev/null +++ b/phronCare.UIBlazor/Services/Sales/ProfessionalSpecialtyService.cs @@ -0,0 +1,22 @@ +using Domain.Entities; +using System.Net.Http.Json; + +namespace phronCare.UIBlazor.Services.Sales +{ + public class ProfessionalSpecialtyService + { + private readonly HttpClient _http; + public ProfessionalSpecialtyService(HttpClient http) + { + _http = http; + } + public async Task?> GetAllAsync() + { + return await _http.GetFromJsonAsync>("api/ProfessionalSpecialty/GetAll"); + } + public async Task GetByNameAsync(string name) + { + return await _http.GetFromJsonAsync($"api/ProfessionalSpecialty/GetByName/{Uri.EscapeDataString(name)}"); + } + } +} \ No newline at end of file diff --git a/phronCare.UIBlazor/Shared/NavMenu.razor b/phronCare.UIBlazor/Shared/NavMenu.razor index 7f12850..4e723a6 100644 --- a/phronCare.UIBlazor/Shared/NavMenu.razor +++ b/phronCare.UIBlazor/Shared/NavMenu.razor @@ -81,6 +81,11 @@ Productos +