diff --git a/Bimix.API/Controllers/ProductsController.cs b/Bimix.API/Controllers/ProductsController.cs index 573f12a..eb8e48d 100644 --- a/Bimix.API/Controllers/ProductsController.cs +++ b/Bimix.API/Controllers/ProductsController.cs @@ -1,3 +1,5 @@ +using Bimix.Application.DTOModels; +using Bimix.Application.DTOModels.Common; using Microsoft.AspNetCore.Mvc; using Bimix.Infrastructure.Data; using Bimix.Domain.Entities; @@ -12,8 +14,59 @@ public class ProductsController(BimixDbContext context) : ControllerBase private readonly BimixDbContext _context = context; [HttpGet] - public async Task>> GetProducts() + public async Task>> GetProducts([FromQuery] ProductFilterRequest request) { - return await _context.Products.ToListAsync(); + var query = _context.Products.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var searchTerm = request.Search.ToLower(); + query = query.Where(x => + x.Name.ToLower().Contains(searchTerm) || + (x.Code != null && x.Code.ToLower().Contains(searchTerm)) || + (x.Ean != null && x.Ean.ToLower().Contains(searchTerm)) + ); + } + + if (!string.IsNullOrWhiteSpace(request.Name)) + { + query = query.Where(x => x.Name.ToLower().Contains(request.Name.ToLower())); + } + + if (!string.IsNullOrWhiteSpace(request.Code)) + { + query = query.Where(x => x.Code != null && x.Code.ToLower().Contains(request.Code.ToLower())); + } + + if (!string.IsNullOrWhiteSpace(request.Ean)) + { + query = query.Where(x => x.Ean != null && x.Ean.ToLower().Contains(request.Ean.ToLower())); + } + + var totalCount = await query.CountAsync(); + + var items = await query + .OrderBy(x => x.Name) + .Skip((request.Page -1) * request.PageSize) + .Take(request.PageSize) + .Select(x => new ProductDto + { + Id = x.Id, + Name = x.Name, + Code = x.Code ?? string.Empty, + Ean = x.Ean ?? string.Empty, + StockAddresses = x.StockAddresses ?? string.Empty, + CreatedAt = x.CreatedAt, + UpdatedAt = x.UpdatedAt + }) + .ToListAsync(); + + return Ok(new PagedResult + { + Items = items, + TotalCount = totalCount, + Page = request.Page, + PageSize = request.PageSize, + }); } } \ No newline at end of file diff --git a/Bimix.API/Properties/launchSettings.json b/Bimix.API/Properties/launchSettings.json index 8e97836..6fe39d5 100644 --- a/Bimix.API/Properties/launchSettings.json +++ b/Bimix.API/Properties/launchSettings.json @@ -5,7 +5,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7142;http://localhost:5142", + "applicationUrl": "http://localhost:7142;http://0.0.0.0:7142", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Bimix.Application/Bimix.Application.csproj b/Bimix.Application/Bimix.Application.csproj index 53e54b7..e6b0ca9 100644 --- a/Bimix.Application/Bimix.Application.csproj +++ b/Bimix.Application/Bimix.Application.csproj @@ -4,10 +4,6 @@ - - - - net8.0 enable diff --git a/Bimix.Application/DTOModels/Common/PagedResult.cs b/Bimix.Application/DTOModels/Common/PagedResult.cs new file mode 100644 index 0000000..4c8a50b --- /dev/null +++ b/Bimix.Application/DTOModels/Common/PagedResult.cs @@ -0,0 +1,12 @@ +namespace Bimix.Application.DTOModels.Common; + +public class PagedResult +{ + public List Items { get; set; } = new(); + public int TotalCount { get; set; } + public int PageSize { get; set; } + public int Page { get; set; } + public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); + public bool HasPreviousPage => Page > 1; + public bool HasNextPage => Page < TotalPages; +} \ No newline at end of file diff --git a/Bimix.Application/DTOModels/ProductDto.cs b/Bimix.Application/DTOModels/ProductDto.cs new file mode 100644 index 0000000..6f71deb --- /dev/null +++ b/Bimix.Application/DTOModels/ProductDto.cs @@ -0,0 +1,22 @@ +namespace Bimix.Application.DTOModels; + +public class ProductDto +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string Ean { get; set; } = string.Empty; + public string StockAddresses { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} + +public class ProductFilterRequest +{ + public string? Search { get; set; } + public string? Name { get; set; } + public string? Code { get; set; } + public string? Ean { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 20; +} \ No newline at end of file diff --git a/Bimix.Infrastructure/Sync/ProductSyncService.cs b/Bimix.Infrastructure/Sync/ProductSyncService.cs index 3681c99..a83426f 100644 --- a/Bimix.Infrastructure/Sync/ProductSyncService.cs +++ b/Bimix.Infrastructure/Sync/ProductSyncService.cs @@ -1,5 +1,7 @@ using System.Reflection.Metadata.Ecma335; using System.Text.Json; +using System.Text.RegularExpressions; +using System.Web; using Bimix.Domain.Entities; using Bimix.Infrastructure.Data; using Microsoft.Extensions.Configuration; @@ -8,17 +10,23 @@ namespace Bimix.Infrastructure.Sync; public class ProductSyncService(HttpClient httpClient, BimixDbContext db, IConfiguration configuration) { - private readonly HttpClient _httpClient = httpClient; - private readonly BimixDbContext _db = db; - private readonly IConfiguration _configuration = configuration; + /// + /// Dekoduje encje HTML w ciągu znaków (np. " na ") + /// + private string DecodeHtmlEntities(string text) + { + if (string.IsNullOrEmpty(text)) + return text; + return HttpUtility.HtmlDecode(text); + } public async Task RunAsync() { - var apiKey = _configuration["E5_CRM:ApiKey"]; - var syncState = _db.SyncStates.FirstOrDefault(x => x.Entity == "Product") ?? new SyncState { Entity = "Product", LastSynced = 0}; + var apiKey = configuration["E5_CRM:ApiKey"]; + var syncState = db.SyncStates.FirstOrDefault(x => x.Entity == "Product") ?? new SyncState { Entity = "Product", LastSynced = 0}; var url = $"https://crm.e5.pl/REST/index.php?key={apiKey}&action=export.products.list&since={syncState.LastSynced}"; - var response = await _httpClient.GetStringAsync(url); + var response = await httpClient.GetStringAsync(url); var products = JsonSerializer.Deserialize>(response); if (products == null) return; @@ -28,10 +36,10 @@ public class ProductSyncService(HttpClient httpClient, BimixDbContext db, IConfi foreach (var p in products) { var idStr = p.GetProperty("id").GetString() ?? ""; - var name = p.GetProperty("name").GetString() ?? ""; - var code = p.GetProperty("code").GetString() ?? ""; - var stockAddresses = p.GetProperty("stock_addresses").GetString() ?? ""; - var ean = p.GetProperty("ean").GetString() ?? ""; + var name = DecodeHtmlEntities(p.GetProperty("name").GetString() ?? ""); + var code = DecodeHtmlEntities(p.GetProperty("code").GetString() ?? ""); + var stockAddresses = DecodeHtmlEntities(p.GetProperty("stock_addresses").GetString() ?? ""); + var ean = DecodeHtmlEntities(p.GetProperty("ean").GetString() ?? ""); if (!Guid.TryParse(idStr, out Guid id)) { @@ -39,7 +47,7 @@ public class ProductSyncService(HttpClient httpClient, BimixDbContext db, IConfi continue; } - var existing = _db.Products.FirstOrDefault(x => x.Id == id); + var existing = db.Products.FirstOrDefault(x => x.Id == id); if (existing == null) { @@ -53,7 +61,7 @@ public class ProductSyncService(HttpClient httpClient, BimixDbContext db, IConfi CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; - _db.Products.Add(product); + db.Products.Add(product); } else { @@ -65,17 +73,17 @@ public class ProductSyncService(HttpClient httpClient, BimixDbContext db, IConfi var exportedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var updateUrl = $"https://crm.e5.pl/REST/index.php?key={apiKey}&action=export.products.setExportedAt&id={id}&exportedAt={exportedAt}"; - await _httpClient.GetAsync(updateUrl); + await httpClient.GetAsync(updateUrl); } syncState.LastSynced = now; - if (_db.SyncStates.FirstOrDefault(x => x.Entity == "Product") == null) + if (db.SyncStates.FirstOrDefault(x => x.Entity == "Product") == null) { - _db.SyncStates.Add(syncState); + db.SyncStates.Add(syncState); } else { - _db.SyncStates.Update(syncState); + db.SyncStates.Update(syncState); } - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); } } \ No newline at end of file diff --git a/Bimix.UI.Mobile/Bimix.UI.Mobile.csproj b/Bimix.UI.Mobile/Bimix.UI.Mobile.csproj index a0e4a2f..bafaaea 100644 --- a/Bimix.UI.Mobile/Bimix.UI.Mobile.csproj +++ b/Bimix.UI.Mobile/Bimix.UI.Mobile.csproj @@ -66,6 +66,7 @@ + diff --git a/Bimix.UI.Mobile/MauiProgram.cs b/Bimix.UI.Mobile/MauiProgram.cs index be2c635..b0d9168 100644 --- a/Bimix.UI.Mobile/MauiProgram.cs +++ b/Bimix.UI.Mobile/MauiProgram.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Bimix.UI.Shared.Extensions; +using Microsoft.Extensions.Logging; using MudBlazor.Services; namespace Bimix.UI.Mobile; @@ -17,7 +18,8 @@ public static class MauiProgram builder.Services.AddMudServices(); var baseUrl = GetApiBaseUrl(); - builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(baseUrl) }); + builder.Services.AddSharedServices(baseUrl); + #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); @@ -30,9 +32,9 @@ public static class MauiProgram { #if IOS // iOS symulator - użyj swojego IP - return "http://192.168.1.100:5015/"; // Zastąp swoim IP + return "http://192.168.13.44:7142/"; // Zastąp swoim IP #else - return "https://localhost:7015/"; + return "https://localhost:7142/"; #endif } diff --git a/Bimix.UI.Shared/Bimix.UI.Shared.csproj b/Bimix.UI.Shared/Bimix.UI.Shared.csproj index a0bad0f..672ca7e 100644 --- a/Bimix.UI.Shared/Bimix.UI.Shared.csproj +++ b/Bimix.UI.Shared/Bimix.UI.Shared.csproj @@ -14,6 +14,8 @@ + + @@ -21,6 +23,10 @@ + + + + diff --git a/Bimix.UI.Shared/Components/ProductList.razor b/Bimix.UI.Shared/Components/ProductList.razor deleted file mode 100644 index 87dd5c8..0000000 --- a/Bimix.UI.Shared/Components/ProductList.razor +++ /dev/null @@ -1,5 +0,0 @@ -@page "/products" -@using MudBlazor - -Produkty -

Lista produktów zostanie tutaj zaimplementowana

\ No newline at end of file diff --git a/Bimix.UI.Shared/Components/ProductListComponent.razor b/Bimix.UI.Shared/Components/ProductListComponent.razor new file mode 100644 index 0000000..44275ea --- /dev/null +++ b/Bimix.UI.Shared/Components/ProductListComponent.razor @@ -0,0 +1,111 @@ +@using MudBlazor.Internal +Lista Produktów + + + + + + + + + + + + + + + + + + + + + + + + + Wyczyść filtry + + + + + + + + + + + Nazwa + Kod + EAN + Akcje + + + @context.Name + @context.Code.Trim() + + + + + + + + Brak produktów do wyświetlenia + + + Ładowanie... + + + + @if (products.TotalCount > 0) + { + + + + Wyniki @((products.Page - 1) * products.PageSize + 1) - @Math.Min(products.Page * products.PageSize, products.TotalCount) + z @products.TotalCount + + + + + + + } diff --git a/Bimix.UI.Shared/Components/ProductListComponent.razor.cs b/Bimix.UI.Shared/Components/ProductListComponent.razor.cs new file mode 100644 index 0000000..cd1f5a8 --- /dev/null +++ b/Bimix.UI.Shared/Components/ProductListComponent.razor.cs @@ -0,0 +1,77 @@ +using Bimix.Application.DTOModels; +using Bimix.Application.DTOModels.Common; +using Bimix.Domain.Entities; +using Bimix.UI.Shared.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; + +namespace Bimix.UI.Shared.Components; + +public partial class ProductListComponent : ComponentBase +{ + [Inject] private ProductService ProductService { get; set; } = default!; + + private PagedResult products = new(); + private ProductFilterRequest filterRequest = new(); + private bool isLoading = false; + + protected override async Task OnInitializedAsync() + { + await LoadProducts(); + } + + private async Task StartBarcodeScanner() + { + + } + + private async Task LoadProducts() + { + isLoading = true; + + try + { + products = await ProductService.GetProductsAsync(filterRequest); + } + catch (Exception ex) + { + Console.WriteLine($"Loading products failed: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task SearchProducts() + { + filterRequest.Page = 1; + await LoadProducts(); + } + + private async Task OnPageChanged(int page) + { + filterRequest.Page = page; + await LoadProducts(); + } + + private async Task ClearFilters() + { + filterRequest = new ProductFilterRequest(); + await LoadProducts(); + } + + private async Task EditProduct(Guid productId) + { + // TODO + Console.WriteLine($"Edytuj produkt: {productId}"); + } + + private async Task DeleteProduct(Guid productId) + { + // TODO + Console.WriteLine($"Usuń produkt: {productId}"); + } + +} \ No newline at end of file diff --git a/Bimix.UI.Shared/Extensions/ServiceCollectionExtensions.cs b/Bimix.UI.Shared/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..5cf395f --- /dev/null +++ b/Bimix.UI.Shared/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Bimix.UI.Shared.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Bimix.UI.Shared.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddSharedServices(this IServiceCollection services, string apiBaseUrl) + { + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(apiBaseUrl); + }); + return services; + } + +} \ No newline at end of file diff --git a/Bimix.UI.Shared/MainLayout.razor b/Bimix.UI.Shared/MainLayout.razor index 122aa31..ab0bade 100644 --- a/Bimix.UI.Shared/MainLayout.razor +++ b/Bimix.UI.Shared/MainLayout.razor @@ -31,7 +31,7 @@ - + @Body diff --git a/Bimix.UI.Shared/Pages/ProductListPage.razor b/Bimix.UI.Shared/Pages/ProductListPage.razor new file mode 100644 index 0000000..6f64a3b --- /dev/null +++ b/Bimix.UI.Shared/Pages/ProductListPage.razor @@ -0,0 +1,7 @@ +@page "/products" + +Produkty + + + + \ No newline at end of file diff --git a/Bimix.UI.Shared/Services/ProductService.cs b/Bimix.UI.Shared/Services/ProductService.cs new file mode 100644 index 0000000..0f8d2b2 --- /dev/null +++ b/Bimix.UI.Shared/Services/ProductService.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using Bimix.Application.DTOModels; +using Bimix.Application.DTOModels.Common; +using Microsoft.AspNetCore.WebUtilities; + +namespace Bimix.UI.Shared.Services; + +public class ProductService(HttpClient httpClient) +{ + private readonly HttpClient _httpClient = httpClient; + private readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public async Task> GetProductsAsync(ProductFilterRequest request) + { + var queryParams = new Dictionary + { + ["page"] = request.Page.ToString(), + ["pageSize"] = request.PageSize.ToString(), + }; + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + queryParams["search"] = request.Search; + } + if (!string.IsNullOrWhiteSpace(request.Name)) + { + queryParams["name"] = request.Name; + } + if (!string.IsNullOrWhiteSpace(request.Code)) + { + queryParams["code"] = request.Code; + } + + if (!string.IsNullOrWhiteSpace(request.Ean)) + { + queryParams["ean"] = request.Ean; + } + + var uri = QueryHelpers.AddQueryString("api/products", queryParams); + var response = await _httpClient.GetAsync(uri); + + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize>(json, _jsonOptions); + + return result ?? new PagedResult(); + } +} \ No newline at end of file diff --git a/Bimix.UI.Web/Program.cs b/Bimix.UI.Web/Program.cs index 08ddc3c..7b4649e 100644 --- a/Bimix.UI.Web/Program.cs +++ b/Bimix.UI.Web/Program.cs @@ -1,4 +1,5 @@ using Bimix.UI.Shared; +using Bimix.UI.Shared.Extensions; using Bimix.UI.Web.Components; using MudBlazor.Services; @@ -7,10 +8,9 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); builder.Services.AddMudServices(); -builder.Services.AddHttpClient("BimixAPI", client => -{ - client.BaseAddress = new Uri("https://localhost:7071"); -}); + +builder.Services.AddSharedServices("http://localhost:7142"); + var app = builder.Build();