Update Customers Update Create and List
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 5m4s

This commit is contained in:
Leandro Hernan Rojas 2025-04-12 18:51:25 -03:00
parent ab622e72ff
commit 0177366fc9
13 changed files with 689 additions and 268 deletions

View File

@ -10,7 +10,7 @@ namespace Core.Interfaces
{ {
public interface ICustomerDom public interface ICustomerDom
{ {
Task<ECustomer> AddAsync(ECustomer entity); Task<ECustomer> CreateAsync(ECustomer entity);
Task<bool> DeleteAsync(int id); Task<bool> DeleteAsync(int id);
Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50); Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50);
Task<ECustomer?> GetByIdAsync(int id); Task<ECustomer?> GetByIdAsync(int id);

View File

@ -1,7 +1,11 @@
using System.Reflection; using System.Drawing.Printing;
using System.Reflection;
using Azure;
using System.Reflection.Metadata;
using Core.Interfaces; using Core.Interfaces;
using Domain.Entities; using Domain.Entities;
using Domain.Generics; using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Models.Helpers; using Models.Helpers;
using Models.Interfaces; using Models.Interfaces;
using Models.Models; using Models.Models;
@ -12,22 +16,11 @@ namespace Core.Services
{ {
#region Declaraciones y Constructor #region Declaraciones y Constructor
private readonly IPhSCustomerRepository _repository; private readonly IPhSCustomerRepository _repository;
public CustomerService(IPhSCustomerRepository customerRepository) public CustomerService(IPhSCustomerRepository customerRepository)
{ {
_repository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository)); _repository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
} }
#endregion #endregion
public Task<ECustomer> AddAsync(ECustomer entity)
{
throw new NotImplementedException();
}
public Task<bool> DeleteAsync(int id)
{
throw new NotImplementedException();
}
public async Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50) public async Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50)
{ {
try try
@ -40,13 +33,34 @@ namespace Core.Services
throw new Exception($"{methodName} Message: {ex.Message}", ex); throw new Exception($"{methodName} Message: {ex.Message}", ex);
} }
} }
public async Task<ECustomer?> GetByIdAsync(int id)
public Task<ECustomer?> GetByIdAsync(int id)
{ {
throw new NotImplementedException(); try
{
return await _repository.GetByIdAsync(id);
} }
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
throw new Exception($"{methodName} Message: {ex.Message}", ex);
}
}
public async Task<ECustomer> CreateAsync(ECustomer entity)
{
if (entity is null)
throw new ArgumentNullException(nameof(entity), "El cliente no puede ser nulo.");
public async Task<PagedResult<ECustomer>> SearchAsync(string? name, string? email, string? document, int page, int pageSize) if (!entity.AccounttypesId.HasValue)
throw new ArgumentException("Debe seleccionar un tipo de cuenta.", nameof(entity.AccounttypesId));
if (entity.PhSCustomerDocuments == null || !entity.PhSCustomerDocuments.Any())
throw new ArgumentException("El cliente debe tener al menos un documento (por ejemplo, CUIT).", nameof(entity.PhSCustomerDocuments));
return await _repository.CreateAsync(entity);
}
public async Task<PagedResult<ECustomer>> SearchAsync(
string? name, string? email, string? document,
int page = 1, int pageSize = 50)
{ {
try try
{ {
@ -59,10 +73,20 @@ namespace Core.Services
} }
} }
public async Task<bool> UpdateAsync(ECustomer entity)
{
if (entity is null)
throw new ArgumentNullException(nameof(entity), "El cliente no puede ser nulo.");
if (!entity.AccounttypesId.HasValue)
throw new ArgumentException("Debe seleccionar un tipo de cuenta.", nameof(entity.AccounttypesId));
if (entity.PhSCustomerDocuments == null || !entity.PhSCustomerDocuments.Any())
throw new ArgumentException("El cliente debe tener al menos un documento (por ejemplo, CUIT).", nameof(entity.PhSCustomerDocuments));
public Task<bool> UpdateAsync(ECustomer entity) return await _repository.UpdateAsync(entity);
}
public Task<bool> DeleteAsync(int id)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,13 +10,13 @@ namespace Domain.Entities
public class ECustomer public class ECustomer
{ {
public int Id { get; set; } public int Id { get; set; }
[Required(ErrorMessage = "Debe ingresar un nombre.")]
public string? Name { get; set; } public string? Name { get; set; }
[Required(ErrorMessage = "Debe ingresar una razon social.")]
public string? BusinessName { get; set; } public string? BusinessName { get; set; }
[Required(ErrorMessage = "Debe seleccionar un tipo de cuenta.")]
public int? AccounttypesId { get; set; } public int? AccounttypesId { get; set; }
[Required(ErrorMessage = "Debe seleccionar un condicion.")]
public int? TaxConditionId { get; set; } public int? TaxConditionId { get; set; }
public bool HasCreditAccount { get; set; } public bool HasCreditAccount { get; set; }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -10,7 +11,9 @@ namespace Domain.Entities
{ {
public int Id { get; set; } public int Id { get; set; }
public int CustomersId { get; set; } public int CustomersId { get; set; }
[Required(ErrorMessage = "Debe un nombre de sucursal/ubicacion.")]
public string? BusinessName { get; set; } public string? BusinessName { get; set; }
[Required(ErrorMessage = "Debe ingresar una direccion valida.")]
public string? Streetaddress1 { get; set; } public string? Streetaddress1 { get; set; }
public string? Streetaddress2 { get; set; } public string? Streetaddress2 { get; set; }
public string? City { get; set; } public string? City { get; set; }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,11 +10,10 @@ namespace Domain.Entities
public class ECustomerDocument public class ECustomerDocument
{ {
public int Id { get; set; } public int Id { get; set; }
public int CustomersId { get; set; } public int CustomersId { get; set; }
[Required(ErrorMessage = "Debe seleccionar un tipo de documento.")]
public int DocumenttypesId { get; set; } public int DocumenttypesId { get; set; }
[Required(ErrorMessage = "Debe seleccionar un numero documento.")]
public string DocumentNumber { get; set; } = null!; public string DocumentNumber { get; set; } = null!;
public DateOnly? IssueDate { get; set; } public DateOnly? IssueDate { get; set; }

View File

@ -65,7 +65,21 @@ namespace Models.Helpers
return destination; return destination;
} }
public static void MapEntityToExisting<TSource, TTarget>(TSource source, TTarget target)
{
var sourceProps = typeof(TSource).GetProperties();
var targetProps = typeof(TTarget).GetProperties();
foreach (var sourceProp in sourceProps)
{
var targetProp = targetProps.FirstOrDefault(p => p.Name == sourceProp.Name && p.PropertyType == sourceProp.PropertyType);
if (targetProp != null && targetProp.CanWrite)
{
var value = sourceProp.GetValue(source);
targetProp.SetValue(target, value);
}
}
}
private static bool IsGenericCollection(Type type) private static bool IsGenericCollection(Type type)
{ {
if (!type.IsGenericType) return false; if (!type.IsGenericType) return false;

View File

@ -11,7 +11,7 @@ namespace Models.Interfaces
{ {
public interface IPhSCustomerRepository public interface IPhSCustomerRepository
{ {
Task<ECustomer> AddAsync(ECustomer entity); Task<ECustomer> CreateAsync(ECustomer entity);
Task<bool> DeleteAsync(int id); Task<bool> DeleteAsync(int id);
Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50); Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50);
Task<ECustomer?> GetByIdAsync(int id); Task<ECustomer?> GetByIdAsync(int id);

View File

@ -13,8 +13,6 @@ namespace Models.Repositories
{ {
#region Declaraciones y Constructor #region Declaraciones y Constructor
private readonly PhronCareOperationsHubContext _context = context; private readonly PhronCareOperationsHubContext _context = context;
private readonly ILogger<PhSCustomerRepository> _logger = logger;
#endregion #endregion
public async Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50) public async Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50)
{ {
@ -34,7 +32,6 @@ namespace Models.Repositories
PageSize = pagedEntities.PageSize PageSize = pagedEntities.PageSize
}; };
} }
public async Task<ECustomer?> GetByIdAsync(int id) public async Task<ECustomer?> GetByIdAsync(int id)
{ {
var customer = await _context.PhSCustomers var customer = await _context.PhSCustomers
@ -89,23 +86,88 @@ namespace Models.Repositories
PageSize = pagedEntities.PageSize PageSize = pagedEntities.PageSize
}; };
} }
public async Task<ECustomer> CreateAsync(ECustomer entity)
public async Task<ECustomer> AddAsync(ECustomer entity) {
if (entity == null)
{
throw new ArgumentNullException(nameof(entity), "El cliente no puede ser nulo.");
}
try
{ {
var customer = EntityMapper.MapEntity<ECustomer, PhSCustomer>(entity); var customer = EntityMapper.MapEntity<ECustomer, PhSCustomer>(entity);
// Mapeo de direcciones
foreach (var address in entity.PhSCustomerAddresses)
{
var mappedAddress = EntityMapper.MapEntity<ECustomerAddress, PhSCustomerAddress>(address);
customer.PhSCustomerAddresses.Add(mappedAddress);
}
// Mapeo de documentos
foreach (var doc in entity.PhSCustomerDocuments)
{
var mappedDoc = EntityMapper.MapEntity<ECustomerDocument, PhSCustomerDocument>(doc);
customer.PhSCustomerDocuments.Add(mappedDoc);
}
// Agregar el cliente al contexto de la base de datos
await _context.PhSCustomers.AddAsync(customer); await _context.PhSCustomers.AddAsync(customer);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
// Mapear y devolver el cliente con la estructura de dominio
return EntityMapper.MapEntity<PhSCustomer, ECustomer>(customer); return EntityMapper.MapEntity<PhSCustomer, ECustomer>(customer);
} }
catch (DbUpdateException dbEx)
{
// Error relacionado con la base de datos (como violación de integridad referencial)
throw new Exception("Error al guardar el cliente en la base de datos. Es posible que haya un problema con la integridad de los datos.", dbEx);
}
catch (Exception ex)
{
// Captura cualquier otro tipo de excepción
throw new Exception("Error inesperado al crear el cliente: " + ex.Message, ex);
}
}
public async Task<bool> UpdateAsync(ECustomer entity) public async Task<bool> UpdateAsync(ECustomer entity)
{ {
var customer = await _context.PhSCustomers.FindAsync(entity.Id); if (entity == null)
if (customer == null) return false; throw new ArgumentNullException(nameof(entity));
try
{
var existingCustomer = await _context.PhSCustomers
.Include(c => c.PhSCustomerAddresses)
.Include(c => c.PhSCustomerDocuments)
.FirstOrDefaultAsync(c => c.Id == entity.Id);
if (existingCustomer == null)
return false;
EntityMapper.MapEntityToExisting(entity, existingCustomer);
_context.PhSCustomerAddresses.RemoveRange(existingCustomer.PhSCustomerAddresses);
existingCustomer.PhSCustomerAddresses.Clear();
foreach (var address in entity.PhSCustomerAddresses)
{
var mappedAddress = EntityMapper.MapEntity<ECustomerAddress, PhSCustomerAddress>(address);
existingCustomer.PhSCustomerAddresses.Add(mappedAddress);
}
_context.PhSCustomerDocuments.RemoveRange(existingCustomer.PhSCustomerDocuments);
existingCustomer.PhSCustomerDocuments.Clear();
foreach (var doc in entity.PhSCustomerDocuments)
{
var mappedDoc = EntityMapper.MapEntity<ECustomerDocument, PhSCustomerDocument>(doc);
existingCustomer.PhSCustomerDocuments.Add(mappedDoc);
}
_context.Entry(customer).CurrentValues.SetValues(entity);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return true; return true;
} }
catch (Exception ex)
{
// Podés loguear el error si querés
return false;
}
}
public async Task<bool> DeleteAsync(int id) public async Task<bool> DeleteAsync(int id)
{ {
var customer = await _context.PhSCustomers.FindAsync(id); var customer = await _context.PhSCustomers.FindAsync(id);

View File

@ -1,4 +1,5 @@
using Core.Interfaces; using Core.Interfaces;
using Domain.Entities;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Reflection; using System.Reflection;
@ -14,6 +15,7 @@ namespace phronCare.API.Controllers.Sales
{ {
_customerService = customerService ?? throw new ArgumentNullException(nameof(customerService)); _customerService = customerService ?? throw new ArgumentNullException(nameof(customerService));
} }
[HttpGet("all")] [HttpGet("all")]
public async Task<IActionResult> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50) public async Task<IActionResult> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50)
{ {
@ -28,6 +30,7 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}"); return StatusCode(500, $"{methodName} Message: {ex.Message}");
} }
} }
[HttpGet("search")] [HttpGet("search")]
public async Task<IActionResult> Search( public async Task<IActionResult> Search(
[FromQuery] string? name, [FromQuery] string? name,
@ -47,20 +50,67 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}"); return StatusCode(500, $"{methodName} Message: {ex.Message}");
} }
} }
//[HttpGet("search")] [HttpGet("{id:int}")]
//public async Task<IActionResult> Search([FromQuery] string? name, [FromQuery] string? email, [FromQuery] string? document) public async Task<ActionResult<ECustomer>> GetById(int id)
//{ {
// try try
// { {
// var result = await _customerService.SearchAsync(name, email, document); var customer = await _customerService.GetByIdAsync(id);
// return Ok(result); if (customer == null)
// } return NotFound();
// catch (Exception ex)
// {
// var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
// return StatusCode(500, $"{methodName} Message: {ex.Message}");
// }
//}
return Ok(customer);
}
catch (Exception ex)
{
return StatusCode(500, $"Error: {ex.Message}");
}
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody] ECustomer customer)
{
try
{
if (customer == null)
return BadRequest("El cliente no puede ser nulo.");
var result = await _customerService.CreateAsync(customer);
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] ECustomer customer)
{
try
{
if (customer == null || customer.Id <= 0)
return BadRequest("El cliente es inválido o no tiene un ID válido.");
var success = await _customerService.UpdateAsync(customer);
if (!success)
return NotFound($"No se encontró un cliente con ID {customer.Id}.");
return Ok("Cliente actualizado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
} }
} }

View File

@ -141,6 +141,32 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.AccountTypeController",
"Method": "GetAll",
"RelativePath": "api/AccountType/GetAll",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.AccountTypeController",
"Method": "GetByName",
"RelativePath": "api/AccountType/GetByName/{name}",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "name",
"Type": "System.String",
"IsRequired": true
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.AuthenticationController", "ContainingType": "phronCare.API.Controllers.AuthenticationController",
"Method": "ConfirmEmail", "Method": "ConfirmEmail",
@ -278,6 +304,121 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.CustomerController",
"Method": "GetById",
"RelativePath": "api/Customer/{id}",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "id",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": [
{
"Type": "Domain.Entities.ECustomer",
"MediaTypes": [
"text/plain",
"application/json",
"text/json"
],
"StatusCode": 200
}
]
},
{
"ContainingType": "phronCare.API.Controllers.Sales.CustomerController",
"Method": "GetAll",
"RelativePath": "api/Customer/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.CustomerController",
"Method": "Create",
"RelativePath": "api/Customer/create",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "customer",
"Type": "Domain.Entities.ECustomer",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.CustomerController",
"Method": "Search",
"RelativePath": "api/Customer/search",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "name",
"Type": "System.String",
"IsRequired": false
},
{
"Name": "email",
"Type": "System.String",
"IsRequired": false
},
{
"Name": "document",
"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.CustomerController",
"Method": "Update",
"RelativePath": "api/Customer/update",
"HttpMethod": "PUT",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "customer",
"Type": "Domain.Entities.ECustomer",
"IsRequired": true
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.DbTestController", "ContainingType": "phronCare.API.Controllers.DbTestController",
"Method": "TestEdmxConnection", "Method": "TestEdmxConnection",
@ -288,6 +429,58 @@
"Parameters": [], "Parameters": [],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.DocumentTypeController",
"Method": "GetAll",
"RelativePath": "api/DocumentType/GetAll",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.DocumentTypeController",
"Method": "GetByName",
"RelativePath": "api/DocumentType/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",
"RelativePath": "api/TaxCondition/GetAll",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.TaxConditionController",
"Method": "GetByName",
"RelativePath": "api/TaxCondition/GetByName/{name}",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "name",
"Type": "System.String",
"IsRequired": true
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.TestController", "ContainingType": "phronCare.API.Controllers.TestController",
"Method": "GetAdministradores", "Method": "GetAdministradores",

View File

@ -1,7 +1,9 @@
@page "/sales/customerform" @page "/sales/customerform"
@page "/sales/customerform/{CustomerId:int}" @page "/sales/customerform/{CustomerId:int}"
@using System.ComponentModel.DataAnnotations
@using phronCare.UIBlazor.Pages.Shared.Modals
@using phronCare.UIBlazor.Services.Sales @using phronCare.UIBlazor.Services.Sales
@inject IModalService Modal
@inject HttpClient _httpClient @inject HttpClient _httpClient
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IToastService toastService @inject IToastService toastService
@ -9,7 +11,11 @@
@inject AccountTypeService accountTypeService @inject AccountTypeService accountTypeService
@inject TaxConditionService taxConditionService @inject TaxConditionService taxConditionService
<div class="card " style="zoom:70%">
<div class="card-header">
<h3 class="card-title">Formulario: cliente</h3>
</div>
<div class="card-body">
<EditForm Model="@customer" OnValidSubmit="@HandleValidSubmit"> <EditForm Model="@customer" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator /> <DataAnnotationsValidator />
<ValidationSummary /> <ValidationSummary />
@ -23,7 +29,6 @@
<InputText id="BusinessName" @bind-Value="customer.BusinessName" class="form-control" /> <InputText id="BusinessName" @bind-Value="customer.BusinessName" class="form-control" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<label for="AccounttypesId">Tipo de Cuenta:</label> <label for="AccounttypesId">Tipo de Cuenta:</label>
@ -46,7 +51,6 @@
</InputSelect> </InputSelect>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<label for="HasCreditAccount">¿Cuenta Corriente?</label><br /> <label for="HasCreditAccount">¿Cuenta Corriente?</label><br />
@ -63,7 +67,6 @@
</div> </div>
<hr /> <hr />
<h5>Documentos</h5> <h5>Documentos</h5>
<div class="row align-items-end"> <div class="row align-items-end">
<div class="col-sm-4"> <div class="col-sm-4">
<label for="TipoDocumento">Tipo:</label> <label for="TipoDocumento">Tipo:</label>
@ -83,9 +86,7 @@
<button type="button" class="btn btn-success" @onclick="AddCustomerDocument">Agregar</button> <button type="button" class="btn btn-success" @onclick="AddCustomerDocument">Agregar</button>
</div> </div>
</div> </div>
<br /> <br />
@if (customer.PhSCustomerDocuments?.Any() == true) @if (customer.PhSCustomerDocuments?.Any() == true)
{ {
<table class="table table-sm table-bordered"> <table class="table table-sm table-bordered">
@ -103,16 +104,15 @@
<td>@documentTypes.FirstOrDefault(t => t.Id == doc.DocumenttypesId)?.Name</td> <td>@documentTypes.FirstOrDefault(t => t.Id == doc.DocumenttypesId)?.Name</td>
<td>@doc.DocumentNumber</td> <td>@doc.DocumentNumber</td>
<td> <td>
<button class="btn btn-sm btn-danger" @onclick="() => RemoveCustomerDocument(doc)">🗑</button> <button type="button" class="btn btn-sm btn-danger" @onclick="() => RemoveCustomerDocument(doc)">🗑</button>
</td> </td>
</tr> </tr>
} }
</tbody> </tbody>
</table> </table>
} }
<hr />
<h4 class="mt-4">Direcciones</h4> <h5>Direcciones</h5>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<input class="form-control" placeholder="Nombre de sucursal" @bind="editingAddress.BusinessName" /> <input class="form-control" placeholder="Nombre de sucursal" @bind="editingAddress.BusinessName" />
@ -121,7 +121,6 @@
<input class="form-control" placeholder="Dirección línea 1" @bind="editingAddress.Streetaddress1" /> <input class="form-control" placeholder="Dirección línea 1" @bind="editingAddress.Streetaddress1" />
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<input class="form-control" placeholder="Dirección línea 2" @bind="editingAddress.Streetaddress2" /> <input class="form-control" placeholder="Dirección línea 2" @bind="editingAddress.Streetaddress2" />
@ -130,7 +129,6 @@
<input class="form-control" placeholder="Ciudad" @bind="editingAddress.City" /> <input class="form-control" placeholder="Ciudad" @bind="editingAddress.City" />
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<select class="form-control" value="@editingAddress.Country" @onchange="OnCountryChanged"> <select class="form-control" value="@editingAddress.Country" @onchange="OnCountryChanged">
@ -158,7 +156,6 @@
} }
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4"> <div class="col-md-4">
<input class="form-control" placeholder="Código Postal" @bind="editingAddress.Postalcode" /> <input class="form-control" placeholder="Código Postal" @bind="editingAddress.Postalcode" />
@ -170,20 +167,18 @@
<input class="form-control" placeholder="Email" @bind="editingAddress.Email" /> <input class="form-control" placeholder="Email" @bind="editingAddress.Email" />
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-12"> <div class="col-md-12">
<textarea class="form-control" placeholder="Notas" @bind="editingAddress.Notes"></textarea> <textarea class="form-control" placeholder="Notas" @bind="editingAddress.Notes"></textarea>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<button class="btn btn-sm btn-success" @onclick="AddOrUpdateAddress"> <button type="button" class="btn btn-sm btn-success" @onclick="AddOrUpdateAddress">
@((editingIndex == -1) ? "Agregar dirección" : "Actualizar dirección") @((editingIndex == -1) ? "Agregar dirección" : "Actualizar dirección")
</button> </button>
@if (editingIndex != -1) @if (editingIndex != -1)
{ {
<button class="btn btn-sm btn-secondary ms-2" @onclick="CancelAddressEdit">Cancelar</button> <button type="button" class="btn btn-sm btn-secondary ms-2" @onclick="CancelAddressEdit">Cancelar</button>
} }
</div> </div>
@ -210,8 +205,8 @@
<td>@address.value.Stateprovince</td> <td>@address.value.Stateprovince</td>
<td>@address.value.Country</td> <td>@address.value.Country</td>
<td> <td>
<button class="btn btn-sm btn-primary me-1" @onclick="() => EditAddress(address.index)">Editar</button> <button type="button" class="btn btn-sm btn-primary me-1" @onclick="() => EditAddress(address.index)">Editar</button>
<button class="btn btn-sm btn-danger" @onclick="() => RemoveAddress(address.index)">Eliminar</button> <button type="button" class="btn btn-sm btn-danger" @onclick="() => RemoveAddress(address.index)">Eliminar</button>
</td> </td>
</tr> </tr>
} }
@ -219,6 +214,19 @@
</table> </table>
} }
</EditForm> </EditForm>
</div>
<div class="card-footer">
<div class="row">
<div class="col">
<div class="mt-4">
<button class="btn btn-primary" type="button" @onclick="HandleValidSubmit" disabled="@isSaving">
@(isSaving ? "Guardando..." : "Guardar Cliente")
</button>
</div>
</div>
</div>
</div>
</div>
@code { @code {
[Parameter] [Parameter]
@ -229,6 +237,7 @@
private List<ETaxCondition> taxConditions = new(); private List<ETaxCondition> taxConditions = new();
private List<EDocumentType> documentTypes = new(); private List<EDocumentType> documentTypes = new();
private ECustomerDocument documentFormModel = new(); private ECustomerDocument documentFormModel = new();
private bool isSaving = false;
private string returnUrl = "/sales/customers"; private string returnUrl = "/sales/customers";
@ -262,6 +271,18 @@
private void AddOrUpdateAddress() private void AddOrUpdateAddress()
{ {
var context = new ValidationContext(editingAddress, null, null);
var results = new List<ValidationResult>();
if (!Validator.TryValidateObject(editingAddress, context, results, true))
{
// Mostrar errores en un toast o en pantalla
foreach (var error in results)
{
toastService.ShowError(error.ErrorMessage);
}
return;
}
if (editingIndex == -1) if (editingIndex == -1)
{ {
// Agregar nueva dirección // Agregar nueva dirección
@ -332,7 +353,6 @@
editingIndex = -1; editingIndex = -1;
} }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await LoadAccountTypes(); await LoadAccountTypes();
@ -342,7 +362,7 @@
if (CustomerId.HasValue) if (CustomerId.HasValue)
{ {
// Cargar datos del cliente existente desde la API // Cargar datos del cliente existente desde la API
customer = await _httpClient.GetFromJsonAsync<ECustomer>($"/api/Customer/GetById/{CustomerId.Value}") ?? new(); customer = await _httpClient.GetFromJsonAsync<ECustomer>($"/api/Customer/{CustomerId.Value}") ?? new();
} }
} }
@ -372,7 +392,6 @@
{ {
customer.PhSCustomerDocuments.Remove(document); customer.PhSCustomerDocuments.Remove(document);
} }
private void RemoveCustomerAddress(ECustomerAddress address) private void RemoveCustomerAddress(ECustomerAddress address)
{ {
customer.PhSCustomerAddresses.Remove(address); customer.PhSCustomerAddresses.Remove(address);
@ -380,6 +399,14 @@
private async Task HandleValidSubmit() private async Task HandleValidSubmit()
{ {
var parameters = new ModalParameters();
parameters.Add("Message", "¿Desea guardar los cambios del cliente?");
var modal = Modal.Show<ConfirmModal>("Confirmacion", parameters);
var result = await modal.Result;
if (result.Cancelled)
return;
try try
{ {
HttpResponseMessage response; HttpResponseMessage response;

View File

@ -10,7 +10,7 @@
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Listado de clientes</h3> @* wtf? *@ <h3 class="card-title">Listado de clientes</h3> @* wtf? *@
</div> </div>
<div class="card-body"> <div class="card-body" style="zoom:70%;">
<div class="mb-4 space-y-2"> <div class="mb-4 space-y-2">
<input @bind="SearchParams.Name" placeholder="Nombre" class="border rounded p-1 w-full" /> <input @bind="SearchParams.Name" placeholder="Nombre" class="border rounded p-1 w-full" />
<input @bind="SearchParams.Email" placeholder="Email" class="border rounded p-1 w-full" /> <input @bind="SearchParams.Email" placeholder="Email" class="border rounded p-1 w-full" />
@ -118,10 +118,27 @@
// var filename = $"Tickets_{Group}_{currentDate}.xlsx"; // var filename = $"Tickets_{Group}_{currentDate}.xlsx";
// await js.InvokeAsync<object>("saveAsFile", filename, Convert.ToBase64String(fileBytes)); // await js.InvokeAsync<object>("saveAsFile", filename, Convert.ToBase64String(fileBytes));
} }
List<PhTable.ButtonOptions> botones = new List<PhTable.ButtonOptions>
List<PhTable.ButtonOptions> botones = new();
protected override void OnInitialized()
{ {
new PhTable.ButtonOptions{ Caption="Editar", ElementClass="btn btn-primary btn-sm"} botones = new List<PhTable.ButtonOptions>
{
new PhTable.ButtonOptions
{
Caption = "Editar",
ElementClass = "btn btn-primary btn-sm",
UrlAction = "/sales/customers/edit/",
OnClickAction = async (id) =>
{
if (int.TryParse(id, out var customerId))
{
Navigation.NavigateTo($"/sales/customerform/{customerId}");
}
}
}
}; };
}
private int TotalPaginas => PagedResult is null ? 1 : private int TotalPaginas => PagedResult is null ? 1 :
(int)Math.Ceiling((double)(PagedResult.TotalItems) / SearchParams.PageSize); (int)Math.Ceiling((double)(PagedResult.TotalItems) / SearchParams.PageSize);
private bool PuedeAvanzar => PagedResult != null && SearchParams.Page < TotalPaginas; private bool PuedeAvanzar => PagedResult != null && SearchParams.Page < TotalPaginas;

View File

@ -0,0 +1,30 @@
@using Blazored.Modal
@using Blazored.Modal.Services
@inherits LayoutComponentBase
<div class="modal-body">
<h5>@Title</h5>
<p>@Message</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="Cancel">Cancelar</button>
<button class="btn btn-primary" @onclick="Confirm">Confirmar</button>
</div>
@code {
[CascadingParameter] BlazoredModalInstance ModalInstance { get; set; }
[Parameter] public string Title { get; set; } = string.Empty;
[Parameter] public string Message { get; set; } = string.Empty;
private async Task Confirm()
{
await ModalInstance.CloseAsync(ModalResult.Ok(true));
}
private async Task Cancel()
{
await ModalInstance.CancelAsync();
}
}