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
{
Task<ECustomer> AddAsync(ECustomer entity);
Task<ECustomer> CreateAsync(ECustomer entity);
Task<bool> DeleteAsync(int id);
Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50);
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 Domain.Entities;
using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
@ -12,22 +16,11 @@ namespace Core.Services
{
#region Declaraciones y Constructor
private readonly IPhSCustomerRepository _repository;
public CustomerService(IPhSCustomerRepository customerRepository)
{
_repository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
}
#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)
{
try
@ -40,13 +33,34 @@ namespace Core.Services
throw new Exception($"{methodName} Message: {ex.Message}", ex);
}
}
public Task<ECustomer?> GetByIdAsync(int id)
public async 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
{
@ -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();
}

View File

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

View File

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

View File

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

View File

@ -65,7 +65,21 @@ namespace Models.Helpers
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)
{
if (!type.IsGenericType) return false;

View File

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

View File

@ -13,8 +13,6 @@ namespace Models.Repositories
{
#region Declaraciones y Constructor
private readonly PhronCareOperationsHubContext _context = context;
private readonly ILogger<PhSCustomerRepository> _logger = logger;
#endregion
public async Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50)
{
@ -34,7 +32,6 @@ namespace Models.Repositories
PageSize = pagedEntities.PageSize
};
}
public async Task<ECustomer?> GetByIdAsync(int id)
{
var customer = await _context.PhSCustomers
@ -89,22 +86,87 @@ namespace Models.Repositories
PageSize = pagedEntities.PageSize
};
}
public async Task<ECustomer> AddAsync(ECustomer entity)
public async Task<ECustomer> CreateAsync(ECustomer entity)
{
var customer = EntityMapper.MapEntity<ECustomer, PhSCustomer>(entity);
await _context.PhSCustomers.AddAsync(customer);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSCustomer, ECustomer>(customer);
if (entity == null)
{
throw new ArgumentNullException(nameof(entity), "El cliente no puede ser nulo.");
}
try
{
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.SaveChangesAsync();
// Mapear y devolver el cliente con la estructura de dominio
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)
{
var customer = await _context.PhSCustomers.FindAsync(entity.Id);
if (customer == null) return false;
if (entity == null)
throw new ArgumentNullException(nameof(entity));
_context.Entry(customer).CurrentValues.SetValues(entity);
await _context.SaveChangesAsync();
return true;
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);
}
await _context.SaveChangesAsync();
return true;
}
catch (Exception ex)
{
// Podés loguear el error si querés
return false;
}
}
public async Task<bool> DeleteAsync(int id)
{

View File

@ -1,4 +1,5 @@
using Core.Interfaces;
using Domain.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
@ -14,6 +15,7 @@ namespace phronCare.API.Controllers.Sales
{
_customerService = customerService ?? throw new ArgumentNullException(nameof(customerService));
}
[HttpGet("all")]
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}");
}
}
[HttpGet("search")]
public async Task<IActionResult> Search(
[FromQuery] string? name,
@ -47,20 +50,67 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
//[HttpGet("search")]
//public async Task<IActionResult> Search([FromQuery] string? name, [FromQuery] string? email, [FromQuery] string? document)
//{
// try
// {
// var result = await _customerService.SearchAsync(name, email, document);
// 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<ECustomer>> GetById(int id)
{
try
{
var customer = await _customerService.GetByIdAsync(id);
if (customer == null)
return NotFound();
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": []
},
{
"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",
"Method": "ConfirmEmail",
@ -278,6 +304,121 @@
],
"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",
"Method": "TestEdmxConnection",
@ -288,6 +429,58 @@
"Parameters": [],
"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",
"Method": "GetAdministradores",

View File

@ -1,7 +1,9 @@
@page "/sales/customerform"
@page "/sales/customerform/{CustomerId:int}"
@using System.ComponentModel.DataAnnotations
@using phronCare.UIBlazor.Pages.Shared.Modals
@using phronCare.UIBlazor.Services.Sales
@inject IModalService Modal
@inject HttpClient _httpClient
@inject NavigationManager Navigation
@inject IToastService toastService
@ -9,216 +11,222 @@
@inject AccountTypeService accountTypeService
@inject TaxConditionService taxConditionService
<EditForm Model="@customer" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="col-md-6">
<label for="Name">Nombre / Razón Social:</label>
<InputText id="Name" @bind-Value="customer.Name" class="form-control" />
</div>
<div class="col-md-6">
<label for="BusinessName">Sucursal / Nombre Comercial:</label>
<InputText id="BusinessName" @bind-Value="customer.BusinessName" class="form-control" />
</div>
<div class="card " style="zoom:70%">
<div class="card-header">
<h3 class="card-title">Formulario: cliente</h3>
</div>
<div class="row">
<div class="col-md-6">
<label for="AccounttypesId">Tipo de Cuenta:</label>
<InputSelect id="AccounttypesId" @bind-Value="customer.AccounttypesId" class="form-control">
<option value="">-- Seleccionar --</option>
@foreach (var type in accountTypes)
{
<option value="@type.Id">@type.Name</option>
}
</InputSelect>
</div>
<div class="col-md-6">
<label for="TaxConditionId">Condición Fiscal:</label>
<InputSelect id="TaxConditionId" @bind-Value="customer.TaxConditionId" class="form-control">
<option value="">-- Seleccionar --</option>
@foreach (var tax in taxConditions)
{
<option value="@tax.Id">@tax.Description</option>
}
</InputSelect>
</div>
</div>
<div class="row">
<div class="col-md-4">
<label for="HasCreditAccount">¿Cuenta Corriente?</label><br />
<InputCheckbox id="HasCreditAccount" @bind-Value="customer.HasCreditAccount" />
</div>
<div class="col-md-4">
<label for="CreditLimit">Límite de Crédito:</label>
<InputNumber id="CreditLimit" @bind-Value="customer.CreditLimit" class="form-control" />
</div>
<div class="col-md-4">
<label for="Active">Activo:</label><br />
<InputCheckbox id="Active" @bind-Value="customer.Active" />
</div>
</div>
<hr />
<h5>Documentos</h5>
<div class="row align-items-end">
<div class="col-sm-4">
<label for="TipoDocumento">Tipo:</label>
<InputSelect id="TipoDocumento" class="form-control" @bind-Value="documentFormModel.DocumenttypesId">
<option value="">Seleccione</option>
@foreach (var tipo in documentTypes)
{
<option value="@tipo.Id">@tipo.Name</option>
}
</InputSelect>
</div>
<div class="col-sm-4">
<label for="NumeroDocumento">Número:</label>
<InputText id="NumeroDocumento" class="form-control" @bind-Value="documentFormModel.DocumentNumber" />
</div>
<div class="col-sm-2">
<button type="button" class="btn btn-success" @onclick="AddCustomerDocument">Agregar</button>
</div>
</div>
<br />
@if (customer.PhSCustomerDocuments?.Any() == true)
{
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Tipo</th>
<th>Número</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody>
@foreach (var doc in customer.PhSCustomerDocuments)
{
<tr>
<td>@documentTypes.FirstOrDefault(t => t.Id == doc.DocumenttypesId)?.Name</td>
<td>@doc.DocumentNumber</td>
<td>
<button class="btn btn-sm btn-danger" @onclick="() => RemoveCustomerDocument(doc)">🗑</button>
</td>
</tr>
}
</tbody>
</table>
}
<h4 class="mt-4">Direcciones</h4>
<div class="row mb-3">
<div class="col-md-6">
<input class="form-control" placeholder="Nombre de sucursal" @bind="editingAddress.BusinessName" />
</div>
<div class="col-md-6">
<input class="form-control" placeholder="Dirección línea 1" @bind="editingAddress.Streetaddress1" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<input class="form-control" placeholder="Dirección línea 2" @bind="editingAddress.Streetaddress2" />
</div>
<div class="col-md-6">
<input class="form-control" placeholder="Ciudad" @bind="editingAddress.City" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<select class="form-control" value="@editingAddress.Country" @onchange="OnCountryChanged">
<option value="">Seleccionar país</option>
@foreach (var country in countries)
{
<option value="@country">@country</option>
}
</select>
</div>
<div class="col-md-6">
@if (editingAddress.Country == "Argentina" && provincesByCountry.TryGetValue("Argentina", out var provincias))
<div class="card-body">
<EditForm Model="@customer" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="col-md-6">
<label for="Name">Nombre / Razón Social:</label>
<InputText id="Name" @bind-Value="customer.Name" class="form-control" />
</div>
<div class="col-md-6">
<label for="BusinessName">Sucursal / Nombre Comercial:</label>
<InputText id="BusinessName" @bind-Value="customer.BusinessName" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="AccounttypesId">Tipo de Cuenta:</label>
<InputSelect id="AccounttypesId" @bind-Value="customer.AccounttypesId" class="form-control">
<option value="">-- Seleccionar --</option>
@foreach (var type in accountTypes)
{
<option value="@type.Id">@type.Name</option>
}
</InputSelect>
</div>
<div class="col-md-6">
<label for="TaxConditionId">Condición Fiscal:</label>
<InputSelect id="TaxConditionId" @bind-Value="customer.TaxConditionId" class="form-control">
<option value="">-- Seleccionar --</option>
@foreach (var tax in taxConditions)
{
<option value="@tax.Id">@tax.Description</option>
}
</InputSelect>
</div>
</div>
<div class="row">
<div class="col-md-4">
<label for="HasCreditAccount">¿Cuenta Corriente?</label><br />
<InputCheckbox id="HasCreditAccount" @bind-Value="customer.HasCreditAccount" />
</div>
<div class="col-md-4">
<label for="CreditLimit">Límite de Crédito:</label>
<InputNumber id="CreditLimit" @bind-Value="customer.CreditLimit" class="form-control" />
</div>
<div class="col-md-4">
<label for="Active">Activo:</label><br />
<InputCheckbox id="Active" @bind-Value="customer.Active" />
</div>
</div>
<hr />
<h5>Documentos</h5>
<div class="row align-items-end">
<div class="col-sm-4">
<label for="TipoDocumento">Tipo:</label>
<InputSelect id="TipoDocumento" class="form-control" @bind-Value="documentFormModel.DocumenttypesId">
<option value="">Seleccione</option>
@foreach (var tipo in documentTypes)
{
<option value="@tipo.Id">@tipo.Name</option>
}
</InputSelect>
</div>
<div class="col-sm-4">
<label for="NumeroDocumento">Número:</label>
<InputText id="NumeroDocumento" class="form-control" @bind-Value="documentFormModel.DocumentNumber" />
</div>
<div class="col-sm-2">
<button type="button" class="btn btn-success" @onclick="AddCustomerDocument">Agregar</button>
</div>
</div>
<br />
@if (customer.PhSCustomerDocuments?.Any() == true)
{
<select class="form-control" @bind="editingAddress.Stateprovince">
<option value="">Seleccionar provincia</option>
@foreach (var provincia in provincias)
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Tipo</th>
<th>Número</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody>
@foreach (var doc in customer.PhSCustomerDocuments)
{
<tr>
<td>@documentTypes.FirstOrDefault(t => t.Id == doc.DocumenttypesId)?.Name</td>
<td>@doc.DocumentNumber</td>
<td>
<button type="button" class="btn btn-sm btn-danger" @onclick="() => RemoveCustomerDocument(doc)">🗑</button>
</td>
</tr>
}
</tbody>
</table>
}
<hr />
<h5>Direcciones</h5>
<div class="row mb-3">
<div class="col-md-6">
<input class="form-control" placeholder="Nombre de sucursal" @bind="editingAddress.BusinessName" />
</div>
<div class="col-md-6">
<input class="form-control" placeholder="Dirección línea 1" @bind="editingAddress.Streetaddress1" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<input class="form-control" placeholder="Dirección línea 2" @bind="editingAddress.Streetaddress2" />
</div>
<div class="col-md-6">
<input class="form-control" placeholder="Ciudad" @bind="editingAddress.City" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<select class="form-control" value="@editingAddress.Country" @onchange="OnCountryChanged">
<option value="">Seleccionar país</option>
@foreach (var country in countries)
{
<option value="@country">@country</option>
}
</select>
</div>
<div class="col-md-6">
@if (editingAddress.Country == "Argentina" && provincesByCountry.TryGetValue("Argentina", out var provincias))
{
<option value="@provincia">@provincia</option>
<select class="form-control" @bind="editingAddress.Stateprovince">
<option value="">Seleccionar provincia</option>
@foreach (var provincia in provincias)
{
<option value="@provincia">@provincia</option>
}
</select>
}
</select>
}
else
{
<input class="form-control" placeholder="Estado/Provincia" @bind="editingAddress.Stateprovince" />
}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<input class="form-control" placeholder="Código Postal" @bind="editingAddress.Postalcode" />
</div>
<div class="col-md-4">
<input class="form-control" placeholder="Teléfono" @bind="editingAddress.Phonenumber" />
</div>
<div class="col-md-4">
<input class="form-control" placeholder="Email" @bind="editingAddress.Email" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<textarea class="form-control" placeholder="Notas" @bind="editingAddress.Notes"></textarea>
</div>
</div>
<div class="mb-3">
<button class="btn btn-sm btn-success" @onclick="AddOrUpdateAddress">
@((editingIndex == -1) ? "Agregar dirección" : "Actualizar dirección")
</button>
@if (editingIndex != -1)
{
<button class="btn btn-sm btn-secondary ms-2" @onclick="CancelAddressEdit">Cancelar</button>
}
</div>
@if (customer.PhSCustomerAddresses.Any())
{
<table class="table table-bordered">
<thead>
<tr>
<th>Sucursal</th>
<th>Dirección</th>
<th>Ciudad</th>
<th>Provincia</th>
<th>País</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var address in customer.PhSCustomerAddresses.Select((value, index) => new { value, index }))
else
{
<input class="form-control" placeholder="Estado/Provincia" @bind="editingAddress.Stateprovince" />
}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<input class="form-control" placeholder="Código Postal" @bind="editingAddress.Postalcode" />
</div>
<div class="col-md-4">
<input class="form-control" placeholder="Teléfono" @bind="editingAddress.Phonenumber" />
</div>
<div class="col-md-4">
<input class="form-control" placeholder="Email" @bind="editingAddress.Email" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<textarea class="form-control" placeholder="Notas" @bind="editingAddress.Notes"></textarea>
</div>
</div>
<div class="mb-3">
<button type="button" class="btn btn-sm btn-success" @onclick="AddOrUpdateAddress">
@((editingIndex == -1) ? "Agregar dirección" : "Actualizar dirección")
</button>
@if (editingIndex != -1)
{
<tr>
<td>@address.value.BusinessName</td>
<td>@address.value.Streetaddress1</td>
<td>@address.value.City</td>
<td>@address.value.Stateprovince</td>
<td>@address.value.Country</td>
<td>
<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>
</td>
</tr>
<button type="button" class="btn btn-sm btn-secondary ms-2" @onclick="CancelAddressEdit">Cancelar</button>
}
</tbody>
</table>
}
</EditForm>
</div>
@if (customer.PhSCustomerAddresses.Any())
{
<table class="table table-bordered">
<thead>
<tr>
<th>Sucursal</th>
<th>Dirección</th>
<th>Ciudad</th>
<th>Provincia</th>
<th>País</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var address in customer.PhSCustomerAddresses.Select((value, index) => new { value, index }))
{
<tr>
<td>@address.value.BusinessName</td>
<td>@address.value.Streetaddress1</td>
<td>@address.value.City</td>
<td>@address.value.Stateprovince</td>
<td>@address.value.Country</td>
<td>
<button type="button" class="btn btn-sm btn-primary me-1" @onclick="() => EditAddress(address.index)">Editar</button>
<button type="button" class="btn btn-sm btn-danger" @onclick="() => RemoveAddress(address.index)">Eliminar</button>
</td>
</tr>
}
</tbody>
</table>
}
</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 {
[Parameter]
@ -229,6 +237,7 @@
private List<ETaxCondition> taxConditions = new();
private List<EDocumentType> documentTypes = new();
private ECustomerDocument documentFormModel = new();
private bool isSaving = false;
private string returnUrl = "/sales/customers";
@ -262,6 +271,18 @@
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)
{
// Agregar nueva dirección
@ -332,7 +353,6 @@
editingIndex = -1;
}
protected override async Task OnInitializedAsync()
{
await LoadAccountTypes();
@ -342,7 +362,7 @@
if (CustomerId.HasValue)
{
// 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);
}
private void RemoveCustomerAddress(ECustomerAddress address)
{
customer.PhSCustomerAddresses.Remove(address);
@ -380,6 +399,14 @@
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
{
HttpResponseMessage response;

View File

@ -10,7 +10,7 @@
<div class="card-header">
<h3 class="card-title">Listado de clientes</h3> @* wtf? *@
</div>
<div class="card-body">
<div class="card-body" style="zoom:70%;">
<div class="mb-4 space-y-2">
<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" />
@ -118,10 +118,27 @@
// var filename = $"Tickets_{Group}_{currentDate}.xlsx";
// 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 :
(int)Math.Ceiling((double)(PagedResult.TotalItems) / SearchParams.PageSize);
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();
}
}