Add Professional y Specialty on API UI
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 5m31s

This commit is contained in:
Leandro Hernan Rojas 2025-04-24 20:03:42 -03:00
parent 319f7234c5
commit f4d6bd9e28
27 changed files with 1438 additions and 3 deletions

View File

@ -0,0 +1,17 @@
using Domain.Entities;
using Domain.Generics;
namespace Core.Interfaces
{
public interface IProfessionalDom
{
Task<EProfessional> CreateAsync(EProfessional entity);
Task<bool> DeleteAsync(int id);
Task<byte[]> ExportFilteredProfessionalsToExcelAsync(ProfessionalSearchParams searchParams);
Task<PagedResult<EProfessional>> GetAllAsync(int page = 1, int pageSize = 50);
Task<EProfessional?> GetByIdAsync(int id);
Task<PagedResult<EProfessional>> SearchAsync(string? fullname, string? document, string? type,
int page = 1, int pageSize = 50);
Task<bool> UpdateAsync(EProfessional entity);
}
}

View File

@ -0,0 +1,10 @@
using Domain.Entities;
namespace Core.Interfaces
{
public interface IProfessionalSpecialtyDom
{
Task<IEnumerable<EProfessionalSpecialty>> GetAllAsync();
Task<EProfessionalSpecialty?> GetByNameAsync(string name);
}
}

View File

@ -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<PagedResult<EProfessional>> 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<EProfessional?> 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<EProfessional> 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<bool> 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<PagedResult<EProfessional>> 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<byte[]> 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<bool> 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
}
}

View File

@ -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<IEnumerable<EProfessionalSpecialty>> 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<EProfessionalSpecialty?> 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
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.13.2</NuGetToolVersion>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.13.1</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -0,0 +1,17 @@

using Domain.Entities;
using Domain.Generics;
namespace Models.Interfaces
{
public interface IPhSProfessionalRepository
{
Task<EProfessional> CreateAsync(EProfessional entity);
Task<bool> DeleteAsync(int id);
Task<PagedResult<EProfessional>> GetAllAsync(int page = 1, int pageSize = 50);
Task<EProfessional?> GetByIdAsync(int id);
Task<PagedResult<EProfessional>> SearchAsync(string? fullname, string? document, string? type,
int page = 1, int pageSize = 50);
Task<bool> UpdateAsync(EProfessional entity);
}
}

View File

@ -0,0 +1,10 @@
using Domain.Entities;
namespace Models.Interfaces
{
public interface IPhSProfessionalSpecialtyRepository
{
Task<IEnumerable<EProfessionalSpecialty>> GetAllAsync();
Task<EProfessionalSpecialty?> GetByNameAsync(string name);
}
}

View File

@ -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; }
}

View File

@ -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<PhSProfessional> PhSProfessionals { get; set; } = new List<PhSProfessional>();
}

View File

@ -39,6 +39,10 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhSProductCategory> PhSProductCategories { get; set; }
public virtual DbSet<PhSProfessional> PhSProfessionals { get; set; }
public virtual DbSet<PhSProfessionalSpecialty> PhSProfessionalSpecialties { get; set; }
public virtual DbSet<PhSQuoteDetail> PhSQuoteDetails { get; set; }
public virtual DbSet<PhSQuoteHeader> 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<PhSProfessional>(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<PhSProfessionalSpecialty>(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<PhSQuoteDetail>(entity =>
{
entity.ToTable("PhS_QuoteDetails");

View File

@ -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<PagedResult<EProfessional>> 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<EProfessional>
{
Items = pagedEntities.Items.Select(EntityMapper.MapEntity<PhSProfessional, EProfessional>),
TotalItems = pagedEntities.TotalItems,
Page = pagedEntities.Page,
PageSize = pagedEntities.PageSize
};
}
public async Task<EProfessional?> GetByIdAsync(int id)
{
var professional = await _context.PhSProfessionals
.Include(p => p.Specialty)
.FirstOrDefaultAsync(p => p.Id == id);
return professional != null ? EntityMapper.MapEntity<PhSProfessional, EProfessional>(professional) : null;
}
public async Task<PagedResult<EProfessional>> 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<EProfessional>
{
Items = pagedEntities.Items.Select(EntityMapper.MapEntity<PhSProfessional, EProfessional>),
TotalItems = pagedEntities.TotalItems,
Page = pagedEntities.Page,
PageSize = pagedEntities.PageSize
};
}
public async Task<EProfessional> CreateAsync(EProfessional entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity), "El profesional no puede ser nulo.");
try
{
var dbEntity = EntityMapper.MapEntity<EProfessional, PhSProfessional>(entity);
await _context.PhSProfessionals.AddAsync(dbEntity);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSProfessional, EProfessional>(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<bool> 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<bool> 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
}
}

View File

@ -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<IEnumerable<EProfessionalSpecialty>> GetAllAsync()
{
var specialties = await _context.PhSProfessionalSpecialties.ToListAsync();
return specialties.Select(EntityMapper.MapEntity<PhSProfessionalSpecialty, EProfessionalSpecialty>);
}
public async Task<EProfessionalSpecialty?> GetByNameAsync(string name)
{
var professionalSpecialty = await _context.PhSProfessionalSpecialties.FirstOrDefaultAsync(a => a.Name.Contains(name));
return professionalSpecialty != null ? EntityMapper.MapEntity<PhSProfessionalSpecialty, EProfessionalSpecialty>(professionalSpecialty) : null;
}
#endregion
}
}

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.13.2</NuGetToolVersion>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.13.1</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -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<IActionResult> 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<IActionResult> 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<ActionResult<EProfessional>> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
}
}
}
}

View File

@ -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<IActionResult> GetAll()
{
try
{
var result = await _professionalSpecialtyService.GetAllAsync();
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet("GetByName/{name}")]
public async Task<IActionResult> 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);
}
}
}

View File

@ -62,6 +62,12 @@ builder.Services.AddScoped<IPhSBusinessUnitRepository, PhSBusinessUnitRepository
builder.Services.AddScoped<IPatientDom, PatientService>();
builder.Services.AddScoped<IPhSPatientRepository, PhSPatientRepository>();
builder.Services.AddScoped<IProfessionalSpecialtyDom, ProfessionalSpecialtyService>();
builder.Services.AddScoped<IPhSProfessionalSpecialtyRepository, PhSProfessionalSpecialtyRepository>();
builder.Services.AddScoped<IProfessionalDom, ProfessionalService>();
builder.Services.AddScoped<IPhSProfessionalRepository, PhSProfessionalRepository>();
builder.Services.AddScoped<IInstitutionDom, InstitutionService>();
builder.Services.AddScoped<IPhSInstitutionRepository, PhSInstitutionRepository>();
#endregion

View File

@ -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",

View File

@ -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
<EditForm Model="model" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="card mt-4" style="zoom:80%">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="card-title m-0">@((model.Id == 0) ? "Nuevo Profesional" : "Editar Profesional")</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-12 mb-3">
<label for="Fullname" class="form-label">Nombre Completo</label>
<InputText id="Fullname" class="form-control" @bind-Value="model.Fullname" />
<ValidationMessage For="@(() => model.Fullname)" />
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="DocumentType" class="form-label">Tipo de documento:</label>
<InputSelect id="DocumentType" @bind-Value="model.DocumenttypeName" class="form-control">
<option value="">Seleccione un tipo</option>
@foreach (var type in documentTypes)
{
<option value="@type.Code">@type.Description</option>
}
</InputSelect>
<ValidationMessage For="@(() => model.DocumenttypeName)" />
</div>
<div class="col-md-4 mb-3">
<label for="DocumentNumber" class="form-label">Número</label>
<InputText id="DocumentNumber" class="form-control" @bind-Value="model.DocumentNumber" />
<ValidationMessage For="@(() => model.DocumentNumber)" />
</div>
<div class="col-md-4 mb-3">
<label for="License" class="form-label">Matrícula</label>
<InputText id="License" class="form-control" @bind-Value="model.License" />
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="Phone1" class="form-label">Teléfono Principal</label>
<InputText id="Phone1" class="form-control" @bind-Value="model.Phone1" />
<ValidationMessage For="@(() => model.Phone1)" />
</div>
<div class="col-md-6 mb-3">
<label for="Phone2" class="form-label">Teléfono Secundario</label>
<InputText id="Phone2" class="form-control" @bind-Value="model.Phone2" />
<ValidationMessage For="@(() => model.Phone2)" />
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="Email" class="form-label">Email</label>
<InputText id="Email" type="email" class="form-control" @bind-Value="model.Email" />
<ValidationMessage For="@(() => model.Email)" />
</div>
<div class="col-md-3 mb-3">
<label for="Type" class="form-label">Tipo de Profesional</label>
<InputSelect id="Type" class="form-select" @bind-Value="model.Type">
@foreach (var option in professionalTypes)
{
<option value="@option.Value">@option.Text</option>
}
</InputSelect>
<ValidationMessage For="@(() => model.Type)" />
</div>
<div class="col-md-4 mb-3">
<label for="Specialtyid" class="form-label">Especialidad</label>
<InputSelect id="Specialtyid" class="form-select" @bind-Value="model.SpecialtyId">
<option value="">--- Seleccionar ---</option>
@foreach (var s in specialties)
{
<option value="@s.Id">@s.Name</option>
}
</InputSelect>
<ValidationMessage For="@(() => model.SpecialtyId)" />
</div>
<div class="col-md-1 mb-3 d-flex align-items-center justify-content-start mt-4">
<div class="form-check form-switch">
<InputCheckbox id="Isactive" @bind-Value="model.Active" class="form-check-input" />
<label class="form-check-label ms-2" for="Isactive">Activo</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="Address" class="form-label">Dirección</label>
<InputText id="Address" class="form-control" @bind-Value="model.Address" />
<ValidationMessage For="@(() => model.Address)" />
</div>
<div class="col-md-3 mb-3">
<label for="City" class="form-label">Ciudad</label>
<InputText id="City" class="form-control" @bind-Value="model.City" />
<ValidationMessage For="@(() => model.City)" />
</div>
<div class="col-md-3 mb-3">
<label for="Province" class="form-label">Provincia:</label>
<InputSelect id="Province" @bind-Value="model.Province" class="form-control">
<option value="">--- Seleccionar ---</option>
@foreach (var province in provinces)
{
<option value="@province">@province</option>
}
</InputSelect>
<ValidationMessage For="@(() => model.Province)" />
</div>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end align-items-center py-3">
<button type="submit" class="btn btn-primary me-2">Guardar</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">Cancelar</button>
</div>
</div>
</div>
</EditForm>
@code {
[Parameter]
public int? Id { get; set; }
private EProfessional model = new();
private List<EDocumentType> documentTypes = new();
private List<EProfessionalSpecialty> specialties = new();
private List<string> 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");
}
}

View File

@ -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
<div class="card">
<div class="card-header d-flex justify-content-center align-items-center" style="zoom:80%;">
<h3 class="card-title m-0">Búsqueda de profesionales</h3>
</div>
<div class="card-body" style="zoom:80%;">
<div class="mb-4 space-y-2">
<input @bind="SearchParams.Fullname" placeholder="Nombre y apellido" class="border rounded p-1 w-full" />
<input @bind="SearchParams.Document" placeholder="Documento o Matricula" class="border rounded p-1 w-full" />
<input @bind="SearchParams.Type" placeholder="Tipo de profesional" class="border rounded p-1 w-full" />
<button class="btn btn-primary rounded-pill" @onclick="BuscarProfesionales">
<i class="fas fa-binoculars me-1"></i> Buscar
</button>
<button class="btn btn-success rounded-pill" @onclick="NuevoProfesional">
<i class="fas fa-plus me-1"></i> Nuevo
</button>
<button class="btn btn-success rounded-pill" @onclick="ExportarExcel">
<i class="fas fa-file-excel me-1"></i> Excel
</button>
<button class="btn btn-secondary rounded-pill" @onclick="Cancel">
<i class="fas fa-arrow-left me-1"></i> Volver
</button>
</div>
<hr />
<div>
@if (TablaProfesionales != null && TablaProfesionales.Any())
{
<PhTable Columns="TableColumns"
Data="TablaProfesionales"
SelectionField="Id"
RowsPerPage=SearchParams.PageSize
RenderButtons="true" Buttons="botones"
ShowPageButtons="false"
ShowQuickSearch="false"
RenderSelect="false" />
}
else
{
<p>No hay resultados.</p>
}
</div>
</div>
<div class="card-footer d-flex justify-content-center align-items-center" style="zoom:80%;">
<div class="d-flex align-items-center gap-3">
<button class="btn btn-secondary rounded-pill" @onclick="PrimeraPagina" disabled="@(SearchParams.Page == 1)">
<i class="fas fa-angle-double-left me-1"></i> Primera
</button>
<button class="btn btn-secondary rounded-pill" @onclick="AnteriorPagina" disabled="@(!PuedeRetroceder)">
<i class="fas fa-chevron-left me-1"></i> Anterior
</button>
<span class="mx-2">
Página <strong>@SearchParams.Page</strong> de <strong>@TotalPaginas</strong>
</span>
<button class="btn btn-secondary rounded-pill" @onclick="SiguientePagina" disabled="@(!PuedeAvanzar)">
Siguiente <i class="fas fa-chevron-right ms-1"></i>
</button>
<button class="btn btn-secondary rounded-pill" @onclick="UltimaPagina" disabled="@(SearchParams.Page == TotalPaginas)">
Última <i class="fas fa-angle-double-right ms-1"></i>
</button>
<div class="d-flex align-items-center ms-3">
<input type="number" class="form-control form-control-sm rounded" style="width: 80px;" min="1" max="@TotalPaginas" @bind="PaginaDeseada" />
<button class="btn btn-outline-primary btn-sm ms-2 rounded-pill" @onclick="IrAPagina">
<i class="fas fa-arrow-right-to-bracket me-1"></i> Ir
</button>
</div>
</div>
</div>
</div>
@code {
private ProfessionalSearchParams SearchParams = new();
private PagedResult<EProfessional>? PagedResult;
private List<Dictionary<string, object>> TablaProfesionales = new();
private List<string> 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<PhTable.ButtonOptions> botones = new();
protected override void OnInitialized()
{
botones = new List<PhTable.ButtonOptions>
{
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<string, object>
{
{ "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");
}
}

View File

@ -47,6 +47,8 @@ builder.Services.AddScoped<DocumentTypeService>();
builder.Services.AddScoped<InstitutionService>();
builder.Services.AddScoped<ProductService>();
builder.Services.AddScoped<BusinessUnitService>();
builder.Services.AddScoped<ProfessionalService>();
builder.Services.AddScoped<ProfessionalSpecialtyService>();
builder.Services.AddScoped<ProductCategoryService>();
#endregion
#region UI

View File

@ -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<PagedResult<EProfessional>?> 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<PagedResult<EProfessional>>(url);
}
public async Task<EProfessional?> GetById(int id)
{
return await _http.GetFromJsonAsync<EProfessional>($"/api/Professional/{id}") ;
}
public async Task<HttpResponseMessage> CreateAsync(EProfessional professional)
{
return await _http.PostAsJsonAsync("/api/Professional/Create", professional);
}
public async Task<HttpResponseMessage> 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);
}
}
}
}

View File

@ -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<List<EProfessionalSpecialty>?> GetAllAsync()
{
return await _http.GetFromJsonAsync<List<EProfessionalSpecialty>>("api/ProfessionalSpecialty/GetAll");
}
public async Task<EProfessionalSpecialty?> GetByNameAsync(string name)
{
return await _http.GetFromJsonAsync<EProfessionalSpecialty>($"api/ProfessionalSpecialty/GetByName/{Uri.EscapeDataString(name)}");
}
}
}

View File

@ -81,6 +81,11 @@
<li aria-hidden="true"></li> Productos
</NavLink>
</div>
<div class="nav-item px-1">
<NavLink class="nav-link" href="sales/professionals/">
<li aria-hidden="true"></li> Profesionales
</NavLink>
</div>
<div class="nav-item px-1">
<NavLink class="nav-link" href="sales/institutions/">
<li aria-hidden="true"></li> Instituciones