From 82fa5d4f9f5738c14d0cc4fb5d8503ef2a5d412b Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Mon, 6 Jan 2025 20:26:04 +0800 Subject: [PATCH 01/32] Step 1: Add the Stock Entity in the CleanAspire.Domain Project --- src/CleanAspire.Domain/Entities/Stock.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/CleanAspire.Domain/Entities/Stock.cs diff --git a/src/CleanAspire.Domain/Entities/Stock.cs b/src/CleanAspire.Domain/Entities/Stock.cs new file mode 100644 index 0000000..d14f03f --- /dev/null +++ b/src/CleanAspire.Domain/Entities/Stock.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Domain.Common; + +namespace CleanAspire.Domain.Entities; +public class Stock : BaseAuditableEntity, IAuditTrial +{ + public string? ProductId { get; set; } + public Product? Product { get; set; } + public int Quantity { get; set; } + public string Location { get; set; }=string.Empty; +} From 485b2a7da36063c13586ac0f0d74d42d0cc7b6dd Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Mon, 6 Jan 2025 20:31:53 +0800 Subject: [PATCH 02/32] create StockConfiguration --- src/CleanAspire.Domain/Entities/Stock.cs | 21 +++++++++- .../Configurations/StockConfiguration.cs | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/CleanAspire.Infrastructure/Persistence/Configurations/StockConfiguration.cs diff --git a/src/CleanAspire.Domain/Entities/Stock.cs b/src/CleanAspire.Domain/Entities/Stock.cs index d14f03f..8f2cc32 100644 --- a/src/CleanAspire.Domain/Entities/Stock.cs +++ b/src/CleanAspire.Domain/Entities/Stock.cs @@ -10,10 +10,29 @@ using CleanAspire.Domain.Common; namespace CleanAspire.Domain.Entities; + +/// +/// Represents a stock entity. +/// public class Stock : BaseAuditableEntity, IAuditTrial { + /// + /// Gets or sets the product ID. + /// public string? ProductId { get; set; } + + /// + /// Gets or sets the product associated with the stock. + /// public Product? Product { get; set; } + + /// + /// Gets or sets the quantity of the stock. + /// public int Quantity { get; set; } - public string Location { get; set; }=string.Empty; + + /// + /// Gets or sets the location of the stock. + /// + public string Location { get; set; } = string.Empty; } diff --git a/src/CleanAspire.Infrastructure/Persistence/Configurations/StockConfiguration.cs b/src/CleanAspire.Infrastructure/Persistence/Configurations/StockConfiguration.cs new file mode 100644 index 0000000..230872e --- /dev/null +++ b/src/CleanAspire.Infrastructure/Persistence/Configurations/StockConfiguration.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using CleanAspire.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanAspire.Infrastructure.Persistence.Configurations; +/// +/// Configures the Stock entity. +/// +public class StockConfiguration : IEntityTypeConfiguration +{ + /// + /// Configures the properties and relationships of the Stock entity. + /// + /// The builder to be used to configure the Stock entity. + public void Configure(EntityTypeBuilder builder) + { + /// + /// Configures the ProductId property of the Stock entity. + /// + builder.Property(x => x.ProductId).HasMaxLength(50).IsRequired(); + + /// + /// Configures the relationship between the Stock and Product entities. + /// + builder.HasOne(x => x.Product).WithMany().HasForeignKey(x => x.ProductId).OnDelete(DeleteBehavior.Cascade); + + /// + /// Configures the Location property of the Stock entity. + /// + builder.Property(x => x.Location).HasMaxLength(12).IsRequired(); + + /// + /// Ignores the DomainEvents property of the Stock entity. + /// + builder.Ignore(e => e.DomainEvents); + } +} From efd5431ccf2245dca9e41a737985a12d68aa8b00 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Mon, 6 Jan 2025 21:28:02 +0800 Subject: [PATCH 03/32] Step 2: Add Stock Features in the CleanAspire.Application Project --- .../Interfaces/IApplicationDbContext.cs | 27 ++++++- .../Commands/StockDispatchingCommand.cs | 66 +++++++++++++++++ .../Stocks/Commands/StockReceivingCommand.cs | 70 +++++++++++++++++++ .../Features/Stocks/DTOs/StockDto.cs | 54 ++++++++++++++ .../Queryies/StocksWithPaginationQuery.cs | 58 +++++++++++++++ .../Persistence/ApplicationDbContext.cs | 40 +++++++++-- 6 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs create mode 100644 src/CleanAspire.Application/Features/Stocks/Commands/StockReceivingCommand.cs create mode 100644 src/CleanAspire.Application/Features/Stocks/DTOs/StockDto.cs create mode 100644 src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs diff --git a/src/CleanAspire.Application/Common/Interfaces/IApplicationDbContext.cs b/src/CleanAspire.Application/Common/Interfaces/IApplicationDbContext.cs index 12b2243..e646d88 100644 --- a/src/CleanAspire.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/CleanAspire.Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,10 +1,35 @@ namespace CleanAspire.Application.Common.Interfaces; +/// +/// Represents the application database context interface. +/// public interface IApplicationDbContext { + /// + /// Gets or sets the Products DbSet. + /// DbSet Products { get; set; } + + /// + /// Gets or sets the AuditTrails DbSet. + /// DbSet AuditTrails { get; set; } + + /// + /// Gets or sets the Tenants DbSet. + /// DbSet Tenants { get; set; } - Task SaveChangesAsync(CancellationToken cancellationToken=default); + + /// + /// Gets or sets the Stocks DbSet. + /// + DbSet Stocks { get; set; } + + /// + /// Saves all changes made in this context to the database. + /// + /// A CancellationToken to observe while waiting for the task to complete. + /// A task that represents the asynchronous save operation. The task result contains the number of state entries written to the database. + Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs b/src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs new file mode 100644 index 0000000..e8633f1 --- /dev/null +++ b/src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Application.Pipeline; + +namespace CleanAspire.Application.Features.Stocks.Commands; +public record StockDispatchingCommand : IFusionCacheRefreshRequest, IRequiresValidation +{ + public string ProductId { get; init; } = string.Empty; + public int Quantity { get; init; } + public string Location { get; init; } = string.Empty; + public IEnumerable? Tags => new[] { "stocks" }; +} +public class StockDispatchingCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public StockDispatchingCommandHandler(IApplicationDbContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async ValueTask Handle(StockDispatchingCommand request, CancellationToken cancellationToken) + { + // Validate that the product exists + var product = await _context.Products + .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); + + if (product == null) + { + throw new KeyNotFoundException($"Product with Product ID '{request.ProductId}' was not found."); + } + + // Check if the stock record exists for the given ProductId and Location + var existingStock = await _context.Stocks + .FirstOrDefaultAsync(s => s.ProductId == request.ProductId && s.Location == request.Location, cancellationToken); + + if (existingStock == null) + { + throw new KeyNotFoundException($"No stock record found for Product ID '{request.ProductId}' at Location '{request.Location}'."); + } + + // Validate that the stock quantity is sufficient + if (existingStock.Quantity < request.Quantity) + { + throw new InvalidOperationException($"Insufficient stock quantity. Available: {existingStock.Quantity}, Requested: {request.Quantity}"); + } + + // Reduce the stock quantity + existingStock.Quantity -= request.Quantity; + + // Update the stock record + _context.Stocks.Update(existingStock); + + // Save changes to the database + await _context.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/src/CleanAspire.Application/Features/Stocks/Commands/StockReceivingCommand.cs b/src/CleanAspire.Application/Features/Stocks/Commands/StockReceivingCommand.cs new file mode 100644 index 0000000..5bc7061 --- /dev/null +++ b/src/CleanAspire.Application/Features/Stocks/Commands/StockReceivingCommand.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Application.Common.Interfaces; +using CleanAspire.Application.Features.Stocks.DTOs; +using CleanAspire.Application.Pipeline; + +namespace CleanAspire.Application.Features.Stocks.Commands; +public record StockReceivingCommand : IFusionCacheRefreshRequest, IRequiresValidation +{ + public string ProductId { get; init; } = string.Empty; + public int Quantity { get; init; } + public string Location { get; init; } = string.Empty; + public IEnumerable? Tags => new[] { "stocks" }; +} +public class StockReceivingCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public StockReceivingCommandHandler(IApplicationDbContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async ValueTask Handle(StockReceivingCommand request, CancellationToken cancellationToken) + { + // Validate that the product exists + var product = await _context.Products + .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); + + if (product == null) + { + throw new KeyNotFoundException($"Product with Product ID '{request.ProductId}' was not found."); + } + + // Check if the stock record already exists for the given ProductId and Location + var existingStock = await _context.Stocks + .FirstOrDefaultAsync(s => s.ProductId == request.ProductId && s.Location == request.Location, cancellationToken); + + if (existingStock != null) + { + // If the stock record exists, update the quantity + existingStock.Quantity += request.Quantity; + _context.Stocks.Update(existingStock); + } + else + { + // If no stock record exists, create a new one + var newStockEntry = new Stock + { + ProductId = request.ProductId, + Location = request.Location, + Quantity = request.Quantity, + }; + + _context.Stocks.Add(newStockEntry); + } + + // Save changes to the database + await _context.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/src/CleanAspire.Application/Features/Stocks/DTOs/StockDto.cs b/src/CleanAspire.Application/Features/Stocks/DTOs/StockDto.cs new file mode 100644 index 0000000..b2b39c6 --- /dev/null +++ b/src/CleanAspire.Application/Features/Stocks/DTOs/StockDto.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Application.Features.Products.DTOs; +using CleanAspire.Domain.Common; + +namespace CleanAspire.Application.Features.Stocks.DTOs; +/// +/// Data Transfer Object for Stock. +/// +public class StockDto +{ + /// + /// Gets or sets the unique identifier for the stock. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the unique identifier for the product. + /// + public string? ProductId { get; set; } + + /// + /// Gets or sets the product details. + /// + public ProductDto? Product { get; set; } + + /// + /// Gets or sets the quantity of the stock. + /// + public int Quantity { get; set; } + + /// + /// Gets or sets the location of the stock. + /// + public string Location { get; set; } = string.Empty; + + /// + /// Gets or sets the date and time when the stock was created. + /// + public DateTime? Created { get; set; } + + /// + /// Gets or sets the date and time when the stock was last modified. + /// + public DateTime? LastModified { get; set; } +} + diff --git a/src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs b/src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs new file mode 100644 index 0000000..e568704 --- /dev/null +++ b/src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Application.Features.Products.DTOs; +using CleanAspire.Application.Features.Stocks.DTOs; + +namespace CleanAspire.Application.Features.Stocks.Queryies; +public record StocksWithPaginationQuery(string Keywords, int PageNumber = 1, int PageSize = 15, string OrderBy = "Id", string SortDirection = "Descending") : IFusionCacheRequest> +{ + public IEnumerable? Tags => new[] { "stocks" }; + public string CacheKey => $"stockswithpagination_{Keywords}_{PageNumber}_{PageSize}_{OrderBy}_{SortDirection}"; +} + +public class StocksWithPaginationQueryHandler : IRequestHandler> +{ + private readonly IApplicationDbContext _context; + + public StocksWithPaginationQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async ValueTask> Handle(StocksWithPaginationQuery request, CancellationToken cancellationToken) + { + var data = await _context.Stocks.OrderBy(request.OrderBy, request.SortDirection) + .ProjectToPaginatedDataAsync( + condition: x => x.Location.Contains(request.Keywords) || (x.Product != null && x.Product.Name.Contains(request.Keywords)), + pageNumber: request.PageNumber, + pageSize: request.PageSize, + mapperFunc: t => new StockDto + { + Id = t.Id, + ProductId = t.ProductId, + Product = t.ProductId != null ? new ProductDto + { + Category = (ProductCategoryDto)t.Product?.Category, + Currency = t.Product?.Currency, + Description = t.Product?.Description, + Id = t.Product?.Id, + Name = t.Product?.Name, + Price = t.Product?.Price ?? 0, + SKU = t.Product?.SKU, + UOM = t.Product?.UOM, + } : null, + Quantity = t.Quantity, + Location = t.Location + }, + cancellationToken: cancellationToken); + + return data; + } +} diff --git a/src/CleanAspire.Infrastructure/Persistence/ApplicationDbContext.cs b/src/CleanAspire.Infrastructure/Persistence/ApplicationDbContext.cs index 95392f6..6a2eb37 100644 --- a/src/CleanAspire.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/CleanAspire.Infrastructure/Persistence/ApplicationDbContext.cs @@ -8,28 +8,58 @@ using System.Reflection; namespace CleanAspire.Infrastructure.Persistence; -#nullable disable +/// +/// Represents the application database context. +/// public class ApplicationDbContext : IdentityDbContext, IApplicationDbContext { + /// + /// Initializes a new instance of the class. + /// + /// The options to be used by a . public ApplicationDbContext(DbContextOptions options) : base(options) { } - public DbSet Tenants { get; set; } + + /// + /// Gets or sets the Tenants DbSet. + /// + public DbSet Tenants { get; set; } + + /// + /// Gets or sets the AuditTrails DbSet. + /// public DbSet AuditTrails { get; set; } + + /// + /// Gets or sets the Products DbSet. + /// public DbSet Products { get; set; } + + /// + /// Gets or sets the Stocks DbSet. + /// + public DbSet Stocks { get; set; } + + /// + /// Configures the schema needed for the identity framework. + /// + /// The builder being used to construct the model for this context. protected override void OnModelCreating(ModelBuilder builder) { - base.OnModelCreating(builder); builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); - } + + /// + /// Configures the conventions to be used for this context. + /// + /// The builder being used to configure conventions for this context. protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { base.ConfigureConventions(configurationBuilder); configurationBuilder.Properties().HaveMaxLength(450); } - } From 766484e51b3ba15f4144b1ccc0efaea131cc0482 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Mon, 6 Jan 2025 21:30:47 +0800 Subject: [PATCH 04/32] create StockDispatchingCommandValidator, StockReceivingCommandValidator --- .../StockDispatchingCommandValidator.cs | 29 +++++++++++++++++++ .../StockReceivingCommandValidator.cs | 29 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/CleanAspire.Application/Features/Stocks/Validators/StockDispatchingCommandValidator.cs create mode 100644 src/CleanAspire.Application/Features/Stocks/Validators/StockReceivingCommandValidator.cs diff --git a/src/CleanAspire.Application/Features/Stocks/Validators/StockDispatchingCommandValidator.cs b/src/CleanAspire.Application/Features/Stocks/Validators/StockDispatchingCommandValidator.cs new file mode 100644 index 0000000..7cfcc35 --- /dev/null +++ b/src/CleanAspire.Application/Features/Stocks/Validators/StockDispatchingCommandValidator.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Application.Features.Stocks.Commands; + +namespace CleanAspire.Application.Features.Stocks.Validators; +public class StockDispatchingCommandValidator : AbstractValidator +{ + public StockDispatchingCommandValidator() + { + RuleFor(x => x.ProductId) + .NotEmpty() + .WithMessage("ProductId is required."); + + RuleFor(x => x.Quantity) + .GreaterThan(0) + .WithMessage("Quantity must be greater than 0."); + + RuleFor(x => x.Location) + .NotEmpty() + .WithMessage("Location is required."); + } +} diff --git a/src/CleanAspire.Application/Features/Stocks/Validators/StockReceivingCommandValidator.cs b/src/CleanAspire.Application/Features/Stocks/Validators/StockReceivingCommandValidator.cs new file mode 100644 index 0000000..43ef171 --- /dev/null +++ b/src/CleanAspire.Application/Features/Stocks/Validators/StockReceivingCommandValidator.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanAspire.Application.Features.Stocks.Commands; + +namespace CleanAspire.Application.Features.Stocks.Validators; +public class StockReceivingCommandValidator : AbstractValidator +{ + public StockReceivingCommandValidator() + { + RuleFor(x => x.ProductId) + .NotEmpty() + .WithMessage("ProductId is required."); + + RuleFor(x => x.Quantity) + .GreaterThan(0) + .WithMessage("Quantity must be greater than 0."); + + RuleFor(x => x.Location) + .NotEmpty() + .WithMessage("Location is required."); + } +} From 8805bdfb344c56f8ae2a984c716161145b4759a2 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Tue, 7 Jan 2025 08:24:26 +0800 Subject: [PATCH 05/32] Step 3: Add and Register the Stock Endpoint in the CleanAspire.Api Project --- .../Endpoints/StockEndpointRegistrar.cs | 47 +++++++++++++++++++ .../ProblemExceptionHandler.cs | 7 +++ src/CleanAspire.Api/Program.cs | 2 +- .../Commands/StockDispatchingCommand.cs | 13 ++++- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 src/CleanAspire.Api/Endpoints/StockEndpointRegistrar.cs diff --git a/src/CleanAspire.Api/Endpoints/StockEndpointRegistrar.cs b/src/CleanAspire.Api/Endpoints/StockEndpointRegistrar.cs new file mode 100644 index 0000000..93e316c --- /dev/null +++ b/src/CleanAspire.Api/Endpoints/StockEndpointRegistrar.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CleanAspire.Application.Common.Models; +using CleanAspire.Application.Features.Stocks.Commands; +using CleanAspire.Application.Features.Stocks.DTOs; +using CleanAspire.Application.Features.Stocks.Queryies; +using Mediator; +using Microsoft.AspNetCore.Mvc; + +namespace CleanAspire.Api.Endpoints; + +public class StockEndpointRegistrar(ILogger logger) : IEndpointRegistrar +{ + public void RegisterRoutes(IEndpointRouteBuilder routes) + { + var group = routes.MapGroup("/stocks").WithTags("stocks").RequireAuthorization(); + + // Dispatch stock + group.MapPost("/dispatch", ([FromServices] IMediator mediator, [FromBody] StockDispatchingCommand command) => mediator.Send(command)) + .Produces(StatusCodes.Status200OK) + .ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity) + .ProducesProblem(StatusCodes.Status400BadRequest) + .ProducesProblem(StatusCodes.Status500InternalServerError) + .WithSummary("Dispatch stock") + .WithDescription("Dispatches a specified quantity of stock from a location."); + + // Receive stock + group.MapPost("/receive", ([FromServices] IMediator mediator, [FromBody] StockReceivingCommand command) => mediator.Send(command)) + .Produces(StatusCodes.Status200OK) + .ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity) + .ProducesProblem(StatusCodes.Status400BadRequest) + .ProducesProblem(StatusCodes.Status500InternalServerError) + .WithSummary("Receive stock") + .WithDescription("Receives a specified quantity of stock into a location."); + + // Get stocks with pagination + group.MapPost("/pagination", ([FromServices] IMediator mediator, [FromBody] StocksWithPaginationQuery query) => mediator.Send(query)) + .Produces>(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status400BadRequest) + .ProducesProblem(StatusCodes.Status500InternalServerError) + .WithSummary("Get stocks with pagination") + .WithDescription("Returns a paginated list of stocks based on search keywords, page size, and sorting options."); + } + +} diff --git a/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs b/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs index 3aad564..6b2fbf2 100644 --- a/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs +++ b/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs @@ -98,6 +98,13 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e Detail = ex.Message, Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}" }, + InvalidOperationException ex => new ProblemDetails + { + Status = StatusCodes.Status400BadRequest, + Title = "Invalid Operation", + Detail = ex.Message, + Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}" + }, _ => new ProblemDetails { Status = StatusCodes.Status500InternalServerError, diff --git a/src/CleanAspire.Api/Program.cs b/src/CleanAspire.Api/Program.cs index b181e0b..43e2284 100644 --- a/src/CleanAspire.Api/Program.cs +++ b/src/CleanAspire.Api/Program.cs @@ -103,7 +103,7 @@ builder.Services.AddProblemDetails(); var app = builder.Build(); -await app.InitializeDatabaseAsync(); +//await app.InitializeDatabaseAsync(); // Configure the HTTP request pipeline. app.UseExceptionHandler(); app.MapEndpointDefinitions(); diff --git a/src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs b/src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs index e8633f1..afa0f5f 100644 --- a/src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs +++ b/src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs @@ -55,12 +55,21 @@ public async ValueTask Handle(StockDispatchingCommand request, Cancellatio // Reduce the stock quantity existingStock.Quantity -= request.Quantity; - // Update the stock record - _context.Stocks.Update(existingStock); + // If stock quantity is zero, remove the stock record + if (existingStock.Quantity == 0) + { + _context.Stocks.Remove(existingStock); + } + else + { + // Update the stock record + _context.Stocks.Update(existingStock); + } // Save changes to the database await _context.SaveChangesAsync(cancellationToken); return Unit.Value; } + } From 23fc94eb60ff58b28984c73c04f0710f5f3a4d91 Mon Sep 17 00:00:00 2001 From: hualin Date: Tue, 7 Jan 2025 09:09:40 +0800 Subject: [PATCH 06/32] add-migration stock --- .../20250107010844_stock.Designer.cs | 541 ++++++++++++++++++ .../Migrations/20250107010844_stock.cs | 51 ++ .../ApplicationDbContextModelSnapshot.cs | 51 ++ 3 files changed, 643 insertions(+) create mode 100644 src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.Designer.cs create mode 100644 src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.cs diff --git a/src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.Designer.cs b/src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.Designer.cs new file mode 100644 index 0000000..955fe1b --- /dev/null +++ b/src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.Designer.cs @@ -0,0 +1,541 @@ +// +using System; +using CleanAspire.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanAspire.Migrators.SQLite.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250107010844_stock")] + partial class stock + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("CleanAspire.Domain.Entities.AuditTrail", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("AffectedColumns") + .HasColumnType("TEXT"); + + b.Property("AuditType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("DebugView") + .HasMaxLength(2147483647) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2147483647) + .HasColumnType("TEXT"); + + b.Property("NewValues") + .HasColumnType("TEXT"); + + b.Property("OldValues") + .HasColumnType("TEXT"); + + b.Property("PrimaryKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TableName") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AuditTrails"); + }); + + modelBuilder.Entity("CleanAspire.Domain.Entities.Product", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("SKU") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("UOM") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("CleanAspire.Domain.Entities.Stock", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("TEXT"); + + b.Property("ProductId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("CleanAspire.Domain.Entities.Tenant", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("CleanAspire.Domain.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AvatarUrl") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LanguageCode") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RefreshTokenExpiryTime") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("SuperiorId") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TimeZoneId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("SuperiorId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CleanAspire.Domain.Entities.AuditTrail", b => + { + b.HasOne("CleanAspire.Domain.Identities.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("CleanAspire.Domain.Entities.Stock", b => + { + b.HasOne("CleanAspire.Domain.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("CleanAspire.Domain.Identities.ApplicationUser", b => + { + b.HasOne("CleanAspire.Domain.Identities.ApplicationUser", "Superior") + .WithMany() + .HasForeignKey("SuperiorId"); + + b.Navigation("Superior"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("CleanAspire.Domain.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("CleanAspire.Domain.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CleanAspire.Domain.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("CleanAspire.Domain.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.cs b/src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.cs new file mode 100644 index 0000000..4b0fc14 --- /dev/null +++ b/src/Migrators/Migrators.SQLite/Migrations/20250107010844_stock.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanAspire.Migrators.SQLite.Migrations +{ + /// + public partial class stock : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Stocks", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 450, nullable: false), + ProductId = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Quantity = table.Column(type: "INTEGER", nullable: false), + Location = table.Column(type: "TEXT", maxLength: 12, nullable: false), + Created = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", maxLength: 450, nullable: true), + LastModified = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 450, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Stocks", x => x.Id); + table.ForeignKey( + name: "FK_Stocks_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Stocks_ProductId", + table: "Stocks", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Stocks"); + } + } +} diff --git a/src/Migrators/Migrators.SQLite/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Migrators/Migrators.SQLite/Migrations/ApplicationDbContextModelSnapshot.cs index d6a3285..9193308 100644 --- a/src/Migrators/Migrators.SQLite/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Migrators/Migrators.SQLite/Migrations/ApplicationDbContextModelSnapshot.cs @@ -123,6 +123,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Products"); }); + modelBuilder.Entity("CleanAspire.Domain.Entities.Stock", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("TEXT"); + + b.Property("ProductId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Stocks"); + }); + modelBuilder.Entity("CleanAspire.Domain.Entities.Tenant", b => { b.Property("Id") @@ -422,6 +462,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Owner"); }); + modelBuilder.Entity("CleanAspire.Domain.Entities.Stock", b => + { + b.HasOne("CleanAspire.Domain.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + modelBuilder.Entity("CleanAspire.Domain.Identities.ApplicationUser", b => { b.HasOne("CleanAspire.Domain.Identities.ApplicationUser", "Superior") From d96f97899771bd39a939d48c7e94ae5947e4b3da Mon Sep 17 00:00:00 2001 From: hualin Date: Tue, 7 Jan 2025 09:54:25 +0800 Subject: [PATCH 07/32] testing api --- .../OpenApiTransformersExtensions.cs | 14 +- src/CleanAspire.Api/Program.cs | 2 +- .../QueryableExtensions.cs | 4 +- .../Queries/ProductsWithPaginationQuery.cs | 2 +- .../Queryies/StocksWithPaginationQuery.cs | 7 +- .../Seed/ApplicationDbContextInitializer.cs | 142 +++++++++++------- 6 files changed, 110 insertions(+), 61 deletions(-) diff --git a/src/CleanAspire.Api/OpenApiTransformersExtensions.cs b/src/CleanAspire.Api/OpenApiTransformersExtensions.cs index 2dc2343..e10f1f1 100644 --- a/src/CleanAspire.Api/OpenApiTransformersExtensions.cs +++ b/src/CleanAspire.Api/OpenApiTransformersExtensions.cs @@ -4,6 +4,7 @@ using Bogus; using CleanAspire.Application.Features.Products.Commands; +using CleanAspire.Application.Features.Stocks.Commands; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.Data; @@ -107,7 +108,18 @@ public ExampleChemaTransformer() ["Currency"] = new OpenApiString("USD"), ["UOM"] = new OpenApiString("PCS") }; - + _examples[typeof(StockDispatchingCommand)] = new OpenApiObject + { + ["ProductId"] = new OpenApiString(Guid.NewGuid().ToString()), + ["Quantity"] = new OpenApiInteger(5), + ["Location"] = new OpenApiString("WH-01"), + }; + _examples[typeof(StockReceivingCommand)] = new OpenApiObject + { + ["ProductId"] = new OpenApiString(Guid.NewGuid().ToString()), + ["Quantity"] = new OpenApiInteger(10), + ["Location"] = new OpenApiString("WH-01"), + }; } public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) { diff --git a/src/CleanAspire.Api/Program.cs b/src/CleanAspire.Api/Program.cs index 43e2284..b181e0b 100644 --- a/src/CleanAspire.Api/Program.cs +++ b/src/CleanAspire.Api/Program.cs @@ -103,7 +103,7 @@ builder.Services.AddProblemDetails(); var app = builder.Build(); -//await app.InitializeDatabaseAsync(); +await app.InitializeDatabaseAsync(); // Configure the HTTP request pipeline. app.UseExceptionHandler(); app.MapEndpointDefinitions(); diff --git a/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs b/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs index 87698a6..43b0e36 100644 --- a/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs +++ b/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs @@ -59,9 +59,9 @@ public static IOrderedQueryable OrderBy(this IQueryable source, string public static async Task> ProjectToPaginatedDataAsync( this IOrderedQueryable query, Expression>? condition, - int pageNumber, + int pageNumber, int pageSize, - Func mapperFunc, + Func mapperFunc, CancellationToken cancellationToken = default) where T : class, IEntity { if (condition != null) diff --git a/src/CleanAspire.Application/Features/Products/Queries/ProductsWithPaginationQuery.cs b/src/CleanAspire.Application/Features/Products/Queries/ProductsWithPaginationQuery.cs index 77f28ad..6cac77d 100644 --- a/src/CleanAspire.Application/Features/Products/Queries/ProductsWithPaginationQuery.cs +++ b/src/CleanAspire.Application/Features/Products/Queries/ProductsWithPaginationQuery.cs @@ -1,7 +1,7 @@ using CleanAspire.Application.Features.Products.DTOs; namespace CleanAspire.Application.Features.Products.Queries; -public record ProductsWithPaginationQuery(string Keywords, int PageNumber = 1, int PageSize = 15, string OrderBy = "Id", string SortDirection = "Descending") : IFusionCacheRequest> +public record ProductsWithPaginationQuery(string Keywords, int PageNumber = 0, int PageSize = 15, string OrderBy = "Id", string SortDirection = "Descending") : IFusionCacheRequest> { public IEnumerable? Tags => new[] { "products" }; public string CacheKey => $"productswithpagination_{Keywords}_{PageNumber}_{PageSize}_{OrderBy}_{SortDirection}"; diff --git a/src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs b/src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs index e568704..d466341 100644 --- a/src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs +++ b/src/CleanAspire.Application/Features/Stocks/Queryies/StocksWithPaginationQuery.cs @@ -11,7 +11,7 @@ using CleanAspire.Application.Features.Stocks.DTOs; namespace CleanAspire.Application.Features.Stocks.Queryies; -public record StocksWithPaginationQuery(string Keywords, int PageNumber = 1, int PageSize = 15, string OrderBy = "Id", string SortDirection = "Descending") : IFusionCacheRequest> +public record StocksWithPaginationQuery(string Keywords, int PageNumber = 0, int PageSize = 15, string OrderBy = "Id", string SortDirection = "Descending") : IFusionCacheRequest> { public IEnumerable? Tags => new[] { "stocks" }; public string CacheKey => $"stockswithpagination_{Keywords}_{PageNumber}_{PageSize}_{OrderBy}_{SortDirection}"; @@ -28,9 +28,9 @@ public StocksWithPaginationQueryHandler(IApplicationDbContext context) public async ValueTask> Handle(StocksWithPaginationQuery request, CancellationToken cancellationToken) { - var data = await _context.Stocks.OrderBy(request.OrderBy, request.SortDirection) + var data = await _context.Stocks.Include(x => x.Product).OrderBy(request.OrderBy, request.SortDirection) .ProjectToPaginatedDataAsync( - condition: x => x.Location.Contains(request.Keywords) || (x.Product != null && x.Product.Name.Contains(request.Keywords)), + condition: x => x.Location.Contains(request.Keywords) || (x.Product != null && (x.Product.Name.Contains(request.Keywords) || x.Product.SKU.Contains(request.Keywords) || x.Product.Description.Contains(request.Keywords))), pageNumber: request.PageNumber, pageSize: request.PageSize, mapperFunc: t => new StockDto @@ -52,7 +52,6 @@ public async ValueTask> Handle(StocksWithPaginationQue Location = t.Location }, cancellationToken: cancellationToken); - return data; } } diff --git a/src/CleanAspire.Infrastructure/Persistence/Seed/ApplicationDbContextInitializer.cs b/src/CleanAspire.Infrastructure/Persistence/Seed/ApplicationDbContextInitializer.cs index 709cc87..9096d14 100644 --- a/src/CleanAspire.Infrastructure/Persistence/Seed/ApplicationDbContextInitializer.cs +++ b/src/CleanAspire.Infrastructure/Persistence/Seed/ApplicationDbContextInitializer.cs @@ -114,60 +114,98 @@ private async Task SeedDataAsync() if (await _context.Products.AnyAsync()) return; _logger.LogInformation("Seeding data..."); var products = new List -{ - new Product -{ - Name = "Sony Bravia 65-inch 4K TV", - Description = "Sony's 65-inch Bravia 4K Ultra HD smart TV with HDR support and X-Motion Clarity. Features a slim bezel, Dolby Vision, and an immersive sound system. Perfect for high-definition streaming and gaming.", - Price = 1200, - SKU = "BRAVIA-65-4K", - UOM = "PCS", - Currency = "USD", - Category = ProductCategory.Electronics -}, -new Product -{ - Name = "Tesla Model S Plaid", - Description = "Tesla's flagship electric vehicle with a top speed of 200 mph and 0-60 in under 2 seconds. Equipped with Autopilot, long-range battery, and premium interior. Suitable for eco-conscious luxury seekers.", - Price = 120000, - SKU = "TESLA-MODEL-S-PLAID", - UOM = "PCS", - Currency = "USD", - Category = ProductCategory.Electronics -}, -new Product -{ - Name = "Apple iPhone 14 Pro Max", - Description = "Apple's latest iPhone featuring a 6.7-inch OLED display, A16 Bionic chip, advanced camera system with 48 MP main camera, and longer battery life. Ideal for photography and heavy app users.", - Price = 1099, - SKU = "IP14PRO-MAX", - UOM = "PCS", - Currency = "USD", - Category = ProductCategory.Electronics -}, -new Product -{ - Name = "Sony WH-1000XM5 Noise Cancelling Headphones", - Description = "Premium noise-cancelling over-ear headphones with 30-hour battery life, adaptive sound control, and Hi-Res audio support. Designed for frequent travelers and audiophiles seeking uninterrupted sound.", - Price = 349, - SKU = "WH-1000XM5", - UOM = "PCS", - Currency = "USD", - Category = ProductCategory.Electronics -}, -new Product -{ - Name = "Apple MacBook Pro 16-inch M2 Max", - Description = "Apple’s most powerful laptop featuring the M2 Max chip, a stunning 16-inch Liquid Retina XDR display, 64GB of unified memory, and up to 8TB SSD storage. Ideal for creative professionals needing high performance.", - Price = 4200, - SKU = "MACBOOK-PRO-M2-MAX", - UOM = "PCS", - Currency = "USD", - Category = ProductCategory.Electronics -} -}; + { + new Product + { + Name = "Ikea LACK Coffee Table", + Description = "Simple and stylish coffee table from Ikea, featuring a modern design and durable surface. Perfect for living rooms or offices.", + Price = 25, + SKU = "LACK-COFFEE-TABLE", + UOM = "PCS", + Currency = "USD", + Category = ProductCategory.Furniture + }, + new Product + { + Name = "Nike Air Zoom Pegasus 40", + Description = "Lightweight and responsive running shoes with advanced cushioning and a breathable mesh upper. Ideal for athletes and daily runners.", + Price = 130, + SKU = "NIKE-PEGASUS-40", + UOM = "PCS", + Currency = "USD", + Category = ProductCategory.Sports + }, + new Product + { + Name = "Adidas Yoga Mat", + Description = "Non-slip yoga mat with a 6mm thickness for optimal cushioning and support during workouts. Suitable for yoga, pilates, or general exercises.", + Price = 45, + SKU = "ADIDAS-YOGA-MAT", + UOM = "PCS", + Currency = "USD", + Category = ProductCategory.Sports + }, + new Product + { + Name = "Ikea HEMNES Bed Frame", + Description = "Solid wood bed frame with a classic design. Offers excellent durability and comfort. Compatible with standard-size mattresses.", + Price = 199, + SKU = "HEMNES-BED-FRAME", + UOM = "PCS", + Currency = "USD", + Category = ProductCategory.Furniture + }, + new Product + { + Name = "Under Armour Men's HeatGear Compression Shirt", + Description = "High-performance compression shirt designed to keep you cool and dry during intense workouts. Made from moisture-wicking fabric.", + Price = 35, + SKU = "UA-HEATGEAR-SHIRT", + UOM = "PCS", + Currency = "USD", + Category = ProductCategory.Sports + }, + new Product + { + Name = "Apple iPhone 15 Pro", + Description = "Apple's latest flagship smartphone featuring a 6.1-inch Super Retina XDR display, A17 Pro chip, titanium frame, and advanced camera system with 5x telephoto lens. Ideal for tech enthusiasts and professional users.", + Price = 1199, + SKU = "IP15PRO", + UOM = "PCS", + Currency = "USD", + Category = ProductCategory.Electronics + } + }; + await _context.Products.AddRangeAsync(products); await _context.SaveChangesAsync(); + var stocks = new List + { + new Stock + { + ProductId = products.FirstOrDefault(p => p.Name == "Ikea LACK Coffee Table")?.Id, + Product = products.FirstOrDefault(p => p.Name == "Ikea LACK Coffee Table"), + Quantity = 50, + Location = "FU-WH-0001" + }, + new Stock + { + ProductId = products.FirstOrDefault(p => p.Name == "Nike Air Zoom Pegasus 40")?.Id, + Product = products.FirstOrDefault(p => p.Name == "Nike Air Zoom Pegasus 40"), + Quantity = 100, + Location = "SP-WH-0001" + }, + new Stock + { + ProductId = products.FirstOrDefault(p => p.Name == "Apple iPhone 15 Pro")?.Id, + Product = products.FirstOrDefault(p => p.Name == "Apple iPhone 15 Pro"), + Quantity = 200, + Location = "EL-WH-0001" + } + }; + + await _context.Stocks.AddRangeAsync(stocks); + await _context.SaveChangesAsync(); } } From 2084320dbb81ae2e9cec9f56b552cb1361a8df49 Mon Sep 17 00:00:00 2001 From: hualin Date: Tue, 7 Jan 2025 13:02:17 +0800 Subject: [PATCH 08/32] add unit test --- .../Common/Models/PaginatedResult.cs | 1 + .../CleanAspire.Tests.csproj | 2 +- tests/CleanAspire.Tests/StockEndpointTests.cs | 149 ++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 tests/CleanAspire.Tests/StockEndpointTests.cs diff --git a/src/CleanAspire.Application/Common/Models/PaginatedResult.cs b/src/CleanAspire.Application/Common/Models/PaginatedResult.cs index b29ec12..c728966 100644 --- a/src/CleanAspire.Application/Common/Models/PaginatedResult.cs +++ b/src/CleanAspire.Application/Common/Models/PaginatedResult.cs @@ -2,6 +2,7 @@ namespace CleanAspire.Application.Common.Models; public class PaginatedResult { + public PaginatedResult() { } public PaginatedResult(IEnumerable items, int total, int pageIndex, int pageSize) { Items = items; diff --git a/tests/CleanAspire.Tests/CleanAspire.Tests.csproj b/tests/CleanAspire.Tests/CleanAspire.Tests.csproj index b30b282..d4deea9 100644 --- a/tests/CleanAspire.Tests/CleanAspire.Tests.csproj +++ b/tests/CleanAspire.Tests/CleanAspire.Tests.csproj @@ -1,4 +1,4 @@ - + net9.0 diff --git a/tests/CleanAspire.Tests/StockEndpointTests.cs b/tests/CleanAspire.Tests/StockEndpointTests.cs new file mode 100644 index 0000000..0902898 --- /dev/null +++ b/tests/CleanAspire.Tests/StockEndpointTests.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net.Http; +using System.Net; +using System.Net.Http.Json; +using Aspire.Hosting.Testing; +using CleanAspire.Application.Common.Models; +using CleanAspire.Application.Features.Stocks.DTOs; +using Projects; +using k8s.KubeConfigModels; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting; +using Newtonsoft.Json.Linq; + +namespace CleanAspire.Tests; + + + +[TestFixture] +public class StockEndpointTests +{ + // Adjust the backend route prefix to match your actual routes + private const string ApiBaseUrl = "/stocks"; + + private HttpClient _httpClient = null!; + private HttpClientHandler _httpClientHandler = null!; + + private DistributedApplication? _app; + private ResourceNotificationService? _resourceNotificationService; + + [SetUp] + public async Task Setup() + { + var appHost = await DistributedApplicationTestingBuilder.CreateAsync(); + appHost.Services.ConfigureHttpClientDefaults(clientBuilder => + { + clientBuilder.AddStandardResilienceHandler(); + }); + + _app = await appHost.BuildAsync(); + _resourceNotificationService = _app.Services.GetRequiredService(); + await _app.StartAsync(); + + _httpClient = _app.CreateHttpClient("apiservice"); + + await _resourceNotificationService + .WaitForResourceAsync("apiservice", KnownResourceStates.Running) + .WaitAsync(TimeSpan.FromSeconds(30)); + + await LoginAsync(); + } + + /// + /// Performs a login request to obtain an authentication cookie. + /// Make sure to modify it to your actual backend login route, for example "/account/login" or "/api/login". + /// Here we use "/login?useCookies=true" as an example. + /// + private async Task LoginAsync() + { + var loginRequest = new + { + Email = "administrator", + Password = "P@ssw0rd!" + }; + + // Ensure that your server-side Minimal API or Controller has defined POST /login + // and that Cookie Auth is enabled + var response = await _httpClient.PostAsJsonAsync("/login?useCookies=true", loginRequest); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), + $"Login should return 200 OK. But was {response.StatusCode}"); + } + + /// + /// This is a complete stock process test: + /// 1) Pagination query of stocks + /// 2) Receiving new stock + /// 3) Dispatching stock + /// + [Test] + public async Task FullStockProcessTest() + { + // -------- STEP 1: Test pagination query -------- + var query = new + { + keywords = "", + pageNumber = 0, + pageSize = 15, + orderBy = "Id", + sortDirection = "Descending", + }; + + var paginationResponse = await _httpClient.PostAsJsonAsync($"{ApiBaseUrl}/pagination", query); + Assert.That(paginationResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK), + $"Pagination query at {ApiBaseUrl}/pagination should return 200 OK, but was {paginationResponse.StatusCode}"); + var jsonString = await paginationResponse.Content.ReadAsStringAsync(); + var paginatedResult = JObject.Parse(jsonString); + Assert.That(paginatedResult, Is.Not.Null, "Pagination result should not be null."); + Assert.That(Convert.ToInt32(paginatedResult["totalItems"]), Is.GreaterThan(0), "Pagination should return at least one item."); + + // Retrieve the first ProductId for subsequent steps + var productId = paginatedResult["items"][0]["productId"]?.ToString(); + Assert.That(productId, Is.Not.Null.And.Not.Empty, "ProductId should not be null or empty."); + + // -------- STEP 2: Test stock receiving -------- + var receiveCommand = new + { + ProductId = productId, + Quantity = 50, + Location = "WH-02" + }; + + var receiveResponse = await _httpClient.PostAsJsonAsync($"{ApiBaseUrl}/receive", receiveCommand); + Assert.That(receiveResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK), + $"Stock receiving at {ApiBaseUrl}/receive should return 200 OK, but was {receiveResponse.StatusCode}"); + + // -------- STEP 3: Test stock dispatching -------- + var dispatchCommand = new + { + ProductId = productId, + Quantity = 20, + Location = "WH-02" + }; + + var dispatchResponse = await _httpClient.PostAsJsonAsync($"{ApiBaseUrl}/dispatch", dispatchCommand); + Assert.That(dispatchResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK), + $"Stock dispatching at {ApiBaseUrl}/dispatch should return 200 OK, but was {dispatchResponse.StatusCode}"); + } + + [TearDown] + public async Task TearDown() + { + // Dispose of the HttpClient and Handler + _httpClient?.Dispose(); + _httpClientHandler?.Dispose(); + + // Dispose of the _app if it's not null + if (_app is not null) + { + await _app.DisposeAsync(); + } + } +} + + + + From c1fdb23330ed1e097201110505bf975993c50649 Mon Sep 17 00:00:00 2001 From: hualin Date: Tue, 7 Jan 2025 13:03:30 +0800 Subject: [PATCH 09/32] Update StockEndpointTests.cs --- tests/CleanAspire.Tests/StockEndpointTests.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/CleanAspire.Tests/StockEndpointTests.cs b/tests/CleanAspire.Tests/StockEndpointTests.cs index 0902898..b1fc399 100644 --- a/tests/CleanAspire.Tests/StockEndpointTests.cs +++ b/tests/CleanAspire.Tests/StockEndpointTests.cs @@ -2,15 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Net.Http; -using System.Net; using System.Net.Http.Json; -using Aspire.Hosting.Testing; -using CleanAspire.Application.Common.Models; -using CleanAspire.Application.Features.Stocks.DTOs; using Projects; -using k8s.KubeConfigModels; -using Aspire.Hosting.ApplicationModel; using Aspire.Hosting; using Newtonsoft.Json.Linq; From 95c0bcda4d7409687bc2e74ebb849833a0c99fe3 Mon Sep 17 00:00:00 2001 From: hualin Date: Tue, 7 Jan 2025 16:11:23 +0800 Subject: [PATCH 10/32] Step 5: Add a Blazor Page for Stock Operations in the CleanAspire.ClientApp Project --- CleanAspire.sln | 6 +- .../Client/.kiota/workspace.json | 5 - src/CleanAspire.ClientApp/Client/ApiClient.cs | 6 + .../Models/PaginatedResultOfStockDto.cs | 85 ++++++++++ .../Client/Models/ProductDto2.cs | 123 ++++++++++++++ .../Client/Models/StockDispatchingCommand.cs | 89 ++++++++++ .../Client/Models/StockDto.cs | 107 ++++++++++++ .../Client/Models/StockReceivingCommand.cs | 89 ++++++++++ .../Models/StocksWithPaginationQuery.cs | 115 +++++++++++++ .../Stocks/Dispatch/DispatchRequestBuilder.cs | 106 ++++++++++++ .../Pagination/PaginationRequestBuilder.cs | 104 ++++++++++++ .../Stocks/Receive/ReceiveRequestBuilder.cs | 106 ++++++++++++ .../Client/Stocks/StocksRequestBuilder.cs | 53 ++++++ .../Autocompletes/ProductAutocomplete.cs | 58 +++++++ .../Pages/Products/Index.razor | 1 - .../Pages/Stocks/Index.razor | 152 ++++++++++++++++++ .../Pages/Stocks/StockDialog.razor | 93 +++++++++++ .../Services/Navigation/NavbarMenu.cs | 2 +- src/CleanAspire.ClientApp/_Imports.razor | 1 + 19 files changed, 1291 insertions(+), 10 deletions(-) delete mode 100644 src/CleanAspire.ClientApp/Client/.kiota/workspace.json create mode 100644 src/CleanAspire.ClientApp/Client/Models/PaginatedResultOfStockDto.cs create mode 100644 src/CleanAspire.ClientApp/Client/Models/ProductDto2.cs create mode 100644 src/CleanAspire.ClientApp/Client/Models/StockDispatchingCommand.cs create mode 100644 src/CleanAspire.ClientApp/Client/Models/StockDto.cs create mode 100644 src/CleanAspire.ClientApp/Client/Models/StockReceivingCommand.cs create mode 100644 src/CleanAspire.ClientApp/Client/Models/StocksWithPaginationQuery.cs create mode 100644 src/CleanAspire.ClientApp/Client/Stocks/Dispatch/DispatchRequestBuilder.cs create mode 100644 src/CleanAspire.ClientApp/Client/Stocks/Pagination/PaginationRequestBuilder.cs create mode 100644 src/CleanAspire.ClientApp/Client/Stocks/Receive/ReceiveRequestBuilder.cs create mode 100644 src/CleanAspire.ClientApp/Client/Stocks/StocksRequestBuilder.cs create mode 100644 src/CleanAspire.ClientApp/Components/Autocompletes/ProductAutocomplete.cs create mode 100644 src/CleanAspire.ClientApp/Pages/Stocks/Index.razor create mode 100644 src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor diff --git a/CleanAspire.sln b/CleanAspire.sln index 5dd7a3b..615ad95 100644 --- a/CleanAspire.sln +++ b/CleanAspire.sln @@ -112,9 +112,9 @@ Global {E29307F2-485B-47B4-9CA7-A7EA6949134B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C379C278-2AFA-4DD5-96F5-34D17AAE1188} - RESX_AutoCreateNewLanguageFiles = True - RESX_ConfirmAddLanguageFile = True RESX_ShowPerformanceTraces = True + RESX_ConfirmAddLanguageFile = True + RESX_AutoCreateNewLanguageFiles = True + SolutionGuid = {C379C278-2AFA-4DD5-96F5-34D17AAE1188} EndGlobalSection EndGlobal diff --git a/src/CleanAspire.ClientApp/Client/.kiota/workspace.json b/src/CleanAspire.ClientApp/Client/.kiota/workspace.json deleted file mode 100644 index 3ce81de..0000000 --- a/src/CleanAspire.ClientApp/Client/.kiota/workspace.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "1.0.0", - "clients": {}, - "plugins": {} -} \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Client/ApiClient.cs b/src/CleanAspire.ClientApp/Client/ApiClient.cs index fde5296..0dca6a0 100644 --- a/src/CleanAspire.ClientApp/Client/ApiClient.cs +++ b/src/CleanAspire.ClientApp/Client/ApiClient.cs @@ -11,6 +11,7 @@ using CleanAspire.Api.Client.Register; using CleanAspire.Api.Client.ResendConfirmationEmail; using CleanAspire.Api.Client.ResetPassword; +using CleanAspire.Api.Client.Stocks; using CleanAspire.Api.Client.Tenants; using CleanAspire.Api.Client.Webpushr; using Microsoft.Kiota.Abstractions.Extensions; @@ -82,6 +83,11 @@ public partial class ApiClient : BaseRequestBuilder { get => new global::CleanAspire.Api.Client.ResetPassword.ResetPasswordRequestBuilder(PathParameters, RequestAdapter); } + /// The stocks property + public global::CleanAspire.Api.Client.Stocks.StocksRequestBuilder Stocks + { + get => new global::CleanAspire.Api.Client.Stocks.StocksRequestBuilder(PathParameters, RequestAdapter); + } /// The tenants property public global::CleanAspire.Api.Client.Tenants.TenantsRequestBuilder Tenants { diff --git a/src/CleanAspire.ClientApp/Client/Models/PaginatedResultOfStockDto.cs b/src/CleanAspire.ClientApp/Client/Models/PaginatedResultOfStockDto.cs new file mode 100644 index 0000000..2780ec1 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/PaginatedResultOfStockDto.cs @@ -0,0 +1,85 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class PaginatedResultOfStockDto : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The currentPage property + public int? CurrentPage { get; set; } + /// The hasNextPage property + public bool? HasNextPage { get; set; } + /// The hasPreviousPage property + public bool? HasPreviousPage { get; set; } + /// The items property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public List? Items { get; set; } +#nullable restore +#else + public List Items { get; set; } +#endif + /// The totalItems property + public int? TotalItems { get; set; } + /// The totalPages property + public int? TotalPages { get; set; } + /// + /// Instantiates a new and sets the default values. + /// + public PaginatedResultOfStockDto() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.PaginatedResultOfStockDto CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.PaginatedResultOfStockDto(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "currentPage", n => { CurrentPage = n.GetIntValue(); } }, + { "hasNextPage", n => { HasNextPage = n.GetBoolValue(); } }, + { "hasPreviousPage", n => { HasPreviousPage = n.GetBoolValue(); } }, + { "items", n => { Items = n.GetCollectionOfObjectValues(global::CleanAspire.Api.Client.Models.StockDto.CreateFromDiscriminatorValue)?.AsList(); } }, + { "totalItems", n => { TotalItems = n.GetIntValue(); } }, + { "totalPages", n => { TotalPages = n.GetIntValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteIntValue("currentPage", CurrentPage); + writer.WriteBoolValue("hasNextPage", HasNextPage); + writer.WriteBoolValue("hasPreviousPage", HasPreviousPage); + writer.WriteCollectionOfObjectValues("items", Items); + writer.WriteIntValue("totalItems", TotalItems); + writer.WriteIntValue("totalPages", TotalPages); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/ProductDto2.cs b/src/CleanAspire.ClientApp/Client/Models/ProductDto2.cs new file mode 100644 index 0000000..909d1b7 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/ProductDto2.cs @@ -0,0 +1,123 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class ProductDto2 : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The category property + public global::CleanAspire.Api.Client.Models.NullableOfProductCategoryDto? Category { get; set; } + /// The currency property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Currency { get; set; } +#nullable restore +#else + public string Currency { get; set; } +#endif + /// The description property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Description { get; set; } +#nullable restore +#else + public string Description { get; set; } +#endif + /// The id property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Id { get; set; } +#nullable restore +#else + public string Id { get; set; } +#endif + /// The name property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Name { get; set; } +#nullable restore +#else + public string Name { get; set; } +#endif + /// The price property + public double? Price { get; set; } + /// The sku property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Sku { get; set; } +#nullable restore +#else + public string Sku { get; set; } +#endif + /// The uom property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Uom { get; set; } +#nullable restore +#else + public string Uom { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public ProductDto2() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.ProductDto2 CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.ProductDto2(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "category", n => { Category = n.GetEnumValue(); } }, + { "currency", n => { Currency = n.GetStringValue(); } }, + { "description", n => { Description = n.GetStringValue(); } }, + { "id", n => { Id = n.GetStringValue(); } }, + { "name", n => { Name = n.GetStringValue(); } }, + { "price", n => { Price = n.GetDoubleValue(); } }, + { "sku", n => { Sku = n.GetStringValue(); } }, + { "uom", n => { Uom = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteEnumValue("category", Category); + writer.WriteStringValue("currency", Currency); + writer.WriteStringValue("description", Description); + writer.WriteStringValue("id", Id); + writer.WriteStringValue("name", Name); + writer.WriteDoubleValue("price", Price); + writer.WriteStringValue("sku", Sku); + writer.WriteStringValue("uom", Uom); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/StockDispatchingCommand.cs b/src/CleanAspire.ClientApp/Client/Models/StockDispatchingCommand.cs new file mode 100644 index 0000000..7e3581b --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/StockDispatchingCommand.cs @@ -0,0 +1,89 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class StockDispatchingCommand : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The location property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Location { get; set; } +#nullable restore +#else + public string Location { get; set; } +#endif + /// The productId property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? ProductId { get; set; } +#nullable restore +#else + public string ProductId { get; set; } +#endif + /// The quantity property + public int? Quantity { get; set; } + /// The tags property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public List? Tags { get; set; } +#nullable restore +#else + public List Tags { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public StockDispatchingCommand() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.StockDispatchingCommand CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.StockDispatchingCommand(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "location", n => { Location = n.GetStringValue(); } }, + { "productId", n => { ProductId = n.GetStringValue(); } }, + { "quantity", n => { Quantity = n.GetIntValue(); } }, + { "tags", n => { Tags = n.GetCollectionOfPrimitiveValues()?.AsList(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("location", Location); + writer.WriteStringValue("productId", ProductId); + writer.WriteIntValue("quantity", Quantity); + writer.WriteCollectionOfPrimitiveValues("tags", Tags); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/StockDto.cs b/src/CleanAspire.ClientApp/Client/Models/StockDto.cs new file mode 100644 index 0000000..57d09dc --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/StockDto.cs @@ -0,0 +1,107 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class StockDto : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The created property + public DateTimeOffset? Created { get; set; } + /// The id property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Id { get; set; } +#nullable restore +#else + public string Id { get; set; } +#endif + /// The lastModified property + public DateTimeOffset? LastModified { get; set; } + /// The location property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Location { get; set; } +#nullable restore +#else + public string Location { get; set; } +#endif + /// The product property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public global::CleanAspire.Api.Client.Models.ProductDto2? Product { get; set; } +#nullable restore +#else + public global::CleanAspire.Api.Client.Models.ProductDto2 Product { get; set; } +#endif + /// The productId property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? ProductId { get; set; } +#nullable restore +#else + public string ProductId { get; set; } +#endif + /// The quantity property + public int? Quantity { get; set; } + /// + /// Instantiates a new and sets the default values. + /// + public StockDto() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.StockDto CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.StockDto(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "created", n => { Created = n.GetDateTimeOffsetValue(); } }, + { "id", n => { Id = n.GetStringValue(); } }, + { "lastModified", n => { LastModified = n.GetDateTimeOffsetValue(); } }, + { "location", n => { Location = n.GetStringValue(); } }, + { "product", n => { Product = n.GetObjectValue(global::CleanAspire.Api.Client.Models.ProductDto2.CreateFromDiscriminatorValue); } }, + { "productId", n => { ProductId = n.GetStringValue(); } }, + { "quantity", n => { Quantity = n.GetIntValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteDateTimeOffsetValue("created", Created); + writer.WriteStringValue("id", Id); + writer.WriteDateTimeOffsetValue("lastModified", LastModified); + writer.WriteStringValue("location", Location); + writer.WriteObjectValue("product", Product); + writer.WriteStringValue("productId", ProductId); + writer.WriteIntValue("quantity", Quantity); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/StockReceivingCommand.cs b/src/CleanAspire.ClientApp/Client/Models/StockReceivingCommand.cs new file mode 100644 index 0000000..9429b52 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/StockReceivingCommand.cs @@ -0,0 +1,89 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class StockReceivingCommand : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The location property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Location { get; set; } +#nullable restore +#else + public string Location { get; set; } +#endif + /// The productId property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? ProductId { get; set; } +#nullable restore +#else + public string ProductId { get; set; } +#endif + /// The quantity property + public int? Quantity { get; set; } + /// The tags property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public List? Tags { get; set; } +#nullable restore +#else + public List Tags { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public StockReceivingCommand() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.StockReceivingCommand CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.StockReceivingCommand(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "location", n => { Location = n.GetStringValue(); } }, + { "productId", n => { ProductId = n.GetStringValue(); } }, + { "quantity", n => { Quantity = n.GetIntValue(); } }, + { "tags", n => { Tags = n.GetCollectionOfPrimitiveValues()?.AsList(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("location", Location); + writer.WriteStringValue("productId", ProductId); + writer.WriteIntValue("quantity", Quantity); + writer.WriteCollectionOfPrimitiveValues("tags", Tags); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Models/StocksWithPaginationQuery.cs b/src/CleanAspire.ClientApp/Client/Models/StocksWithPaginationQuery.cs new file mode 100644 index 0000000..68fadca --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Models/StocksWithPaginationQuery.cs @@ -0,0 +1,115 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace CleanAspire.Api.Client.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class StocksWithPaginationQuery : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The cacheKey property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? CacheKey { get; set; } +#nullable restore +#else + public string CacheKey { get; set; } +#endif + /// The keywords property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Keywords { get; set; } +#nullable restore +#else + public string Keywords { get; set; } +#endif + /// The orderBy property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? OrderBy { get; set; } +#nullable restore +#else + public string OrderBy { get; set; } +#endif + /// The pageNumber property + public int? PageNumber { get; set; } + /// The pageSize property + public int? PageSize { get; set; } + /// The sortDirection property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? SortDirection { get; set; } +#nullable restore +#else + public string SortDirection { get; set; } +#endif + /// The tags property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public List? Tags { get; set; } +#nullable restore +#else + public List Tags { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public StocksWithPaginationQuery() + { + AdditionalData = new Dictionary(); + OrderBy = "Id"; + SortDirection = "Descending"; + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::CleanAspire.Api.Client.Models.StocksWithPaginationQuery CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::CleanAspire.Api.Client.Models.StocksWithPaginationQuery(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "cacheKey", n => { CacheKey = n.GetStringValue(); } }, + { "keywords", n => { Keywords = n.GetStringValue(); } }, + { "orderBy", n => { OrderBy = n.GetStringValue(); } }, + { "pageNumber", n => { PageNumber = n.GetIntValue(); } }, + { "pageSize", n => { PageSize = n.GetIntValue(); } }, + { "sortDirection", n => { SortDirection = n.GetStringValue(); } }, + { "tags", n => { Tags = n.GetCollectionOfPrimitiveValues()?.AsList(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("cacheKey", CacheKey); + writer.WriteStringValue("keywords", Keywords); + writer.WriteStringValue("orderBy", OrderBy); + writer.WriteIntValue("pageNumber", PageNumber); + writer.WriteIntValue("pageSize", PageSize); + writer.WriteStringValue("sortDirection", SortDirection); + writer.WriteCollectionOfPrimitiveValues("tags", Tags); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Stocks/Dispatch/DispatchRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Stocks/Dispatch/DispatchRequestBuilder.cs new file mode 100644 index 0000000..dc64245 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Stocks/Dispatch/DispatchRequestBuilder.cs @@ -0,0 +1,106 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Stocks.Dispatch +{ + /// + /// Builds and executes requests for operations under \stocks\dispatch + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class DispatchRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public DispatchRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks/dispatch", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public DispatchRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks/dispatch", rawUrl) + { + } + /// + /// Dispatches a specified quantity of stock from a location. + /// + /// A + /// The request body + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 422 status code + /// When receiving a 500 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task PostAsync(global::CleanAspire.Api.Client.Models.StockDispatchingCommand body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task PostAsync(global::CleanAspire.Api.Client.Models.StockDispatchingCommand body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = ToPostRequestInformation(body, requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "422", global::CleanAspire.Api.Client.Models.HttpValidationProblemDetails.CreateFromDiscriminatorValue }, + { "500", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.Unit.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// Dispatches a specified quantity of stock from a location. + /// + /// A + /// The request body + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.StockDispatchingCommand body, Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.StockDispatchingCommand body, Action> requestConfiguration = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/json"); + requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Stocks.Dispatch.DispatchRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Stocks.Dispatch.DispatchRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class DispatchRequestBuilderPostRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Stocks/Pagination/PaginationRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Stocks/Pagination/PaginationRequestBuilder.cs new file mode 100644 index 0000000..efe88c0 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Stocks/Pagination/PaginationRequestBuilder.cs @@ -0,0 +1,104 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Stocks.Pagination +{ + /// + /// Builds and executes requests for operations under \stocks\pagination + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class PaginationRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public PaginationRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks/pagination", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public PaginationRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks/pagination", rawUrl) + { + } + /// + /// Returns a paginated list of stocks based on search keywords, page size, and sorting options. + /// + /// A + /// The request body + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 500 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task PostAsync(global::CleanAspire.Api.Client.Models.StocksWithPaginationQuery body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task PostAsync(global::CleanAspire.Api.Client.Models.StocksWithPaginationQuery body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = ToPostRequestInformation(body, requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "500", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.PaginatedResultOfStockDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// Returns a paginated list of stocks based on search keywords, page size, and sorting options. + /// + /// A + /// The request body + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.StocksWithPaginationQuery body, Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.StocksWithPaginationQuery body, Action> requestConfiguration = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/json"); + requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Stocks.Pagination.PaginationRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Stocks.Pagination.PaginationRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class PaginationRequestBuilderPostRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Stocks/Receive/ReceiveRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Stocks/Receive/ReceiveRequestBuilder.cs new file mode 100644 index 0000000..d984bbc --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Stocks/Receive/ReceiveRequestBuilder.cs @@ -0,0 +1,106 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace CleanAspire.Api.Client.Stocks.Receive +{ + /// + /// Builds and executes requests for operations under \stocks\receive + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class ReceiveRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public ReceiveRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks/receive", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public ReceiveRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks/receive", rawUrl) + { + } + /// + /// Receives a specified quantity of stock into a location. + /// + /// A + /// The request body + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 422 status code + /// When receiving a 500 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task PostAsync(global::CleanAspire.Api.Client.Models.StockReceivingCommand body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task PostAsync(global::CleanAspire.Api.Client.Models.StockReceivingCommand body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = ToPostRequestInformation(body, requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "422", global::CleanAspire.Api.Client.Models.HttpValidationProblemDetails.CreateFromDiscriminatorValue }, + { "500", global::CleanAspire.Api.Client.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendAsync(requestInfo, global::CleanAspire.Api.Client.Models.Unit.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// + /// Receives a specified quantity of stock into a location. + /// + /// A + /// The request body + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.StockReceivingCommand body, Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToPostRequestInformation(global::CleanAspire.Api.Client.Models.StockReceivingCommand body, Action> requestConfiguration = default) + { +#endif + _ = body ?? throw new ArgumentNullException(nameof(body)); + var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/json"); + requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::CleanAspire.Api.Client.Stocks.Receive.ReceiveRequestBuilder WithUrl(string rawUrl) + { + return new global::CleanAspire.Api.Client.Stocks.Receive.ReceiveRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class ReceiveRequestBuilderPostRequestConfiguration : RequestConfiguration + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Client/Stocks/StocksRequestBuilder.cs b/src/CleanAspire.ClientApp/Client/Stocks/StocksRequestBuilder.cs new file mode 100644 index 0000000..720db63 --- /dev/null +++ b/src/CleanAspire.ClientApp/Client/Stocks/StocksRequestBuilder.cs @@ -0,0 +1,53 @@ +// +#pragma warning disable CS0618 +using CleanAspire.Api.Client.Stocks.Dispatch; +using CleanAspire.Api.Client.Stocks.Pagination; +using CleanAspire.Api.Client.Stocks.Receive; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System; +namespace CleanAspire.Api.Client.Stocks +{ + /// + /// Builds and executes requests for operations under \stocks + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class StocksRequestBuilder : BaseRequestBuilder + { + /// The dispatch property + public global::CleanAspire.Api.Client.Stocks.Dispatch.DispatchRequestBuilder Dispatch + { + get => new global::CleanAspire.Api.Client.Stocks.Dispatch.DispatchRequestBuilder(PathParameters, RequestAdapter); + } + /// The pagination property + public global::CleanAspire.Api.Client.Stocks.Pagination.PaginationRequestBuilder Pagination + { + get => new global::CleanAspire.Api.Client.Stocks.Pagination.PaginationRequestBuilder(PathParameters, RequestAdapter); + } + /// The receive property + public global::CleanAspire.Api.Client.Stocks.Receive.ReceiveRequestBuilder Receive + { + get => new global::CleanAspire.Api.Client.Stocks.Receive.ReceiveRequestBuilder(PathParameters, RequestAdapter); + } + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public StocksRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public StocksRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/stocks", rawUrl) + { + } + } +} +#pragma warning restore CS0618 diff --git a/src/CleanAspire.ClientApp/Components/Autocompletes/ProductAutocomplete.cs b/src/CleanAspire.ClientApp/Components/Autocompletes/ProductAutocomplete.cs new file mode 100644 index 0000000..5d68a6a --- /dev/null +++ b/src/CleanAspire.ClientApp/Components/Autocompletes/ProductAutocomplete.cs @@ -0,0 +1,58 @@ + + +using CleanAspire.Api.Client; +using CleanAspire.Api.Client.Models; +using CleanAspire.ClientApp.Services; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CleanAspire.ClientApp.Components.Autocompletes; + +public class ProductAutocomplete : MudAutocomplete +{ + public ProductAutocomplete() + { + SearchFunc = SearchKeyValues; + ToStringFunc = dto => dto?.Name; + Dense = true; + ResetValueOnEmptyText = true; + ShowProgressIndicator = true; + } + [Parameter] public string? DefaultProductId { get; set; } + public List? Products { get; set; } = new(); + [Inject] private ApiClient ApiClient { get; set; } = default!; + [Inject] private ApiClientServiceProxy ApiClientServiceProxy { get; set; } = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + Products = await ApiClientServiceProxy.QueryAsync("_allproducts", () => ApiClient.Products.GetAsync(), tags: null, expiration: TimeSpan.FromMinutes(60)); + if (!string.IsNullOrEmpty(DefaultProductId)) + { + var defaultProduct = Products?.FirstOrDefault(p => p.Id == DefaultProductId); + if (defaultProduct != null) + { + Value = defaultProduct; + await ValueChanged.InvokeAsync(Value); + } + } + StateHasChanged(); // Trigger a re-render after the tenants are loaded + } + } + private async Task> SearchKeyValues(string? value, CancellationToken cancellation) + { + IEnumerable result; + + if (string.IsNullOrWhiteSpace(value)) + result = Products ?? new List(); + else + result = Products? + .Where(x => x.Name?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true || + x.Sku?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true || + x.Description?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true) + .ToList() ?? new List(); + + return await Task.FromResult(result); + } +} diff --git a/src/CleanAspire.ClientApp/Pages/Products/Index.razor b/src/CleanAspire.ClientApp/Pages/Products/Index.razor index 5eece75..0c87761 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Index.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Index.razor @@ -1,5 +1,4 @@ @page "/products/index" -@using System.Globalization @using CleanAspire.ClientApp.Pages.Products.Components @using CleanAspire.ClientApp.Services.Products diff --git a/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor b/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor new file mode 100644 index 0000000..6cf3ef8 --- /dev/null +++ b/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor @@ -0,0 +1,152 @@ +@page "/stocks/index" +@Title + + + + + + + + @L[Title] + @L["Check product stock levels."] + + + + + + + @L["Refresh"] + + + + @L["Receiving"] + + + + + + + + + + + + + + + + + + @context.Item.Product?.Sku + + + + + @context.Item.Product?.Name + @context.Item.Product?.Description + + + + + + + @context.Item.Product?.Uom + + + + + @($"{((context.Item.Quantity ?? 0) * (context.Item.Product?.Price ?? 0)).ToString("#,#")} {context.Item.Product?.Currency}") + + + + + + + +@code { + public string Title = "Stock Inquiry"; + private HashSet _selectedItems = new(); + private StockDto _currentDto = new(); + private MudDataGrid _table = default!; + private int _defaultPageSize = 10; + private string _keywords = string.Empty; + private bool _loading = false; + private readonly string[] tags = new[] { "stocks" }; + private readonly TimeSpan timeSpan = TimeSpan.FromSeconds(30); + private async Task> ServerReload(GridState state) + { + try + { + _loading = true; + var query = new StocksWithPaginationQuery(); + query.PageNumber = state.Page; + query.PageSize = state.PageSize; + query.Keywords = _keywords; + query.OrderBy = state.SortDefinitions.FirstOrDefault()?.SortBy ?? "Id"; + query.SortDirection = state.SortDefinitions.FirstOrDefault()?.Descending ?? true ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(); + var cacheKey = $"_{query.Keywords}_{query.PageSize}_{query.PageNumber}_{query.OrderBy}_{query.SortDirection}"; + var result = await ApiClientServiceProxy.QueryAsync(cacheKey, () => ApiClient.Stocks.Pagination.PostAsync(query), tags, timeSpan); + return new GridData { TotalItems = (int)result.TotalItems, Items = result.Items }; + } + finally + { + _loading = false; + } + } + AggregateDefinition _qtyAggregation = new AggregateDefinition + { + Type = AggregateType.Sum, + NumberFormat = "#,#", + DisplayFormat = "Total quantity is {value}" + }; + private async Task OnSearch(string text) + { + _selectedItems = new HashSet(); + _keywords = text; + await _table.ReloadServerData(); + } + private async Task Receive() + { + var parameters = new DialogParameters + { + {x=>x.Inbound, true} + }; + await DialogServiceHelper.ShowDialogAsync(L["Stock receiving"], parameters, new DialogOptions() { MaxWidth = MaxWidth.Small }, + async (state) => + { + if (state is not null && !state.Canceled) + { + await ApiClientServiceProxy.ClearCache(tags); + await _table.ReloadServerData(); + _selectedItems = new(); + } + }); + } + private async Task Dispatch(StockDto item) + { + var parameters = new DialogParameters + { + {x=>x.Inbound, false}, + {x=>x.ProductId,item.Product?.Id }, + {x=>x.model, new StockDialog.Model(){ Location = item.Location, Quantity=item.Quantity } } + }; + await DialogServiceHelper.ShowDialogAsync(L["Stock dispatching"], parameters, new DialogOptions() { MaxWidth = MaxWidth.Small }, + async (state) => + { + if (state is not null && !state.Canceled) + { + await ApiClientServiceProxy.ClearCache(tags); + await _table.ReloadServerData(); + _selectedItems = new(); + } + }); + } +} diff --git a/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor b/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor new file mode 100644 index 0000000..4d82950 --- /dev/null +++ b/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor @@ -0,0 +1,93 @@ +@using System.ComponentModel.DataAnnotations +@using CleanAspire.ClientApp.Components.Autocompletes + + + +
+ + + +
+
+
+ + @L["Cancel"] + @L["Save"] + +
+ +@code { + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!; + [Parameter] + public string? ProductId { get; set; } + [Parameter] + public bool Inbound { get; set; } + [Parameter] + public Model model { get; set; } = new Model(); + private MudForm editForm = default!; + public string Localtion { get; set; } = string.Empty; + private bool _saving = false; + private void Cancel() => MudDialog.Cancel(); + private async Task Submit() + { + await editForm.Validate(); // Validate manually before submitting. + if (editForm.IsValid) + { + _saving = true; + if (Inbound) + { + var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.Stocks.Receive.PostAsync(new StockReceivingCommand () { ProductId = model.Product.Id, Location = model.Location, Quantity = model.Quantity })); + result.Switch( + ok => + { + Snackbar.Add(L["Stock received successfully."], Severity.Success); + }, + invalid => + { + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); + }, + error => + { + Snackbar.Add(L["Stock receiving failed. Please try again."], Severity.Error); + } + ); + } + else + { + var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.Stocks.Dispatch.PostAsync(new StockDispatchingCommand() { ProductId = model.Product.Id, Location = model.Location, Quantity = model.Quantity })); + result.Switch( + ok => + { + Snackbar.Add(L["Stock dispatched successfully."], Severity.Success); + }, + invalid => + { + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); + }, + error => + { + Snackbar.Add(L["Stock dispatching failed. Please try again."], Severity.Error); + } + ); + } + MudDialog.Close(DialogResult.Ok(model)); + _saving = false; + } + } + + private IEnumerable ValidateQuantity(int? value) + { + if (!value.HasValue || value <= 0) + { + yield return "Quantity must be greater than 0."; + } + } + public class Model + { + [Required(ErrorMessage = "Product id is required.")] + public ProductDto? Product { get; set; } + [Required(ErrorMessage = "Location id is required.")] + public string Location { get; set; } + public int? Quantity { get; set; } + } +} diff --git a/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs b/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs index eb5903f..9c2efdc 100644 --- a/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs +++ b/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs @@ -32,7 +32,7 @@ public static class NavbarMenu new MenuItem { Label = "Stock Inquiry", - Href = "", + Href = "/stocks/index", Status = PageStatus.Completed, Description = "Check product stock levels." }, diff --git a/src/CleanAspire.ClientApp/_Imports.razor b/src/CleanAspire.ClientApp/_Imports.razor index 4dc2b75..498fb8b 100644 --- a/src/CleanAspire.ClientApp/_Imports.razor +++ b/src/CleanAspire.ClientApp/_Imports.razor @@ -1,5 +1,6 @@ @using System.Net.Http @using System.Net.Http.Json +@using System.Globalization @using CleanAspire.Api.Client @using CleanAspire.Api.Client.Models @using CleanAspire.ClientApp From e77e5b8c6b039d979939a4c4b9394416d52d6635 Mon Sep 17 00:00:00 2001 From: hualin Date: Tue, 7 Jan 2025 16:25:24 +0800 Subject: [PATCH 11/32] ver 0.0.62 --- README.md | 4 ++-- src/CleanAspire.ClientApp/wwwroot/appsettings.json | 2 +- src/CleanAspire.WebApp/appsettings.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index be57dd1..06a7904 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ By incorporating robust offline capabilities, CleanAspire empowers developers to version: '3.8' services: apiservice: - image: blazordevlab/cleanaspire-api:0.0.61 + image: blazordevlab/cleanaspire-api:0.0.62 environment: - ASPNETCORE_ENVIRONMENT=Development - AllowedHosts=* @@ -110,7 +110,7 @@ services: blazorweb: - image: blazordevlab/cleanaspire-webapp:0.0.61 + image: blazordevlab/cleanaspire-webapp:0.0.62 environment: - ASPNETCORE_ENVIRONMENT=Production - AllowedHosts=* diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.json index ad8d29d..e83e9a6 100644 --- a/src/CleanAspire.ClientApp/wwwroot/appsettings.json +++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.json @@ -7,7 +7,7 @@ }, "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.61", + "Version": "v0.0.62", "ServiceBaseUrl": "https://apiservice.blazorserver.com" } } diff --git a/src/CleanAspire.WebApp/appsettings.json b/src/CleanAspire.WebApp/appsettings.json index 1f91fcd..05f0e46 100644 --- a/src/CleanAspire.WebApp/appsettings.json +++ b/src/CleanAspire.WebApp/appsettings.json @@ -8,7 +8,7 @@ "AllowedHosts": "*", "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.61", + "Version": "v0.0.62", "ServiceBaseUrl": "https://apiservice.blazorserver.com" } } From 1b29c8d73ee988263e7a31f893ebb97cb43f83f1 Mon Sep 17 00:00:00 2001 From: hualin Date: Tue, 7 Jan 2025 16:42:30 +0800 Subject: [PATCH 12/32] add language resource --- .../Pages/Stocks/Index.razor | 2 +- .../Pages/Stocks/StockDialog.razor | 8 ++--- .../Resources/App.de-DE.resx | 36 +++++++++++++++++++ .../Resources/App.es-ES.resx | 36 +++++++++++++++++++ .../Resources/App.fr-FR.resx | 36 +++++++++++++++++++ .../Resources/App.ja-JP.resx | 36 +++++++++++++++++++ .../Resources/App.ko-KR.resx | 36 +++++++++++++++++++ .../Resources/App.pt-BR.resx | 36 +++++++++++++++++++ src/CleanAspire.ClientApp/Resources/App.resx | 36 +++++++++++++++++++ .../Resources/App.zh-CN.resx | 36 +++++++++++++++++++ 10 files changed, 293 insertions(+), 5 deletions(-) diff --git a/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor b/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor index 6cf3ef8..06ae9d7 100644 --- a/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor +++ b/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor @@ -38,7 +38,7 @@ - + diff --git a/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor b/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor index 4d82950..4051800 100644 --- a/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor +++ b/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor @@ -4,9 +4,9 @@
- - - + + +
@@ -79,7 +79,7 @@ { if (!value.HasValue || value <= 0) { - yield return "Quantity must be greater than 0."; + yield return L["Quantity must be greater than 0."]; } } public class Model diff --git a/src/CleanAspire.ClientApp/Resources/App.de-DE.resx b/src/CleanAspire.ClientApp/Resources/App.de-DE.resx index 6e004f6..122d18a 100644 --- a/src/CleanAspire.ClientApp/Resources/App.de-DE.resx +++ b/src/CleanAspire.ClientApp/Resources/App.de-DE.resx @@ -648,4 +648,40 @@ Blazor Aspire + + Produkt ist erforderlich + + + Standort ist erforderlich + + + Menge muss größer als 0 sein + + + Lagerbestand erfolgreich erhalten + + + Lagerbestand erfolgreich versendet + + + Befehle + + + Produktname + + + Standort + + + Menge + + + Empfang + + + Lagerbestandsempfang + + + Lagerbestandversand + \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Resources/App.es-ES.resx b/src/CleanAspire.ClientApp/Resources/App.es-ES.resx index fe47e20..e4201a4 100644 --- a/src/CleanAspire.ClientApp/Resources/App.es-ES.resx +++ b/src/CleanAspire.ClientApp/Resources/App.es-ES.resx @@ -648,4 +648,40 @@ Blazor Aspire + + El producto es obligatorio + + + La ubicación es obligatoria + + + La cantidad debe ser mayor que 0 + + + Stock recibido con éxito + + + Stock enviado con éxito + + + Comandos + + + Nombre del producto + + + Ubicación + + + Cantidad + + + Recepción + + + Recepción de stock + + + Despacho de stock + \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Resources/App.fr-FR.resx b/src/CleanAspire.ClientApp/Resources/App.fr-FR.resx index 41d574d..8b36922 100644 --- a/src/CleanAspire.ClientApp/Resources/App.fr-FR.resx +++ b/src/CleanAspire.ClientApp/Resources/App.fr-FR.resx @@ -648,4 +648,40 @@ Blazor Aspire + + Le produit est requis + + + L'emplacement est requis + + + La quantité doit être supérieure à 0 + + + Stock reçu avec succès + + + Stock expédié avec succès + + + Commandes + + + Nom du produit + + + Emplacement + + + Quantité + + + Réception + + + Réception de stock + + + Expédition de stock + \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Resources/App.ja-JP.resx b/src/CleanAspire.ClientApp/Resources/App.ja-JP.resx index ba5a713..19f67bc 100644 --- a/src/CleanAspire.ClientApp/Resources/App.ja-JP.resx +++ b/src/CleanAspire.ClientApp/Resources/App.ja-JP.resx @@ -648,4 +648,40 @@ ブレイザー アスパイア + + 製品が必要です + + + 場所が必要です + + + 数量は0より大きくなければなりません + + + 在庫を正常に受け取りました + + + 在庫を正常に出荷しました + + + コマンド + + + 製品名 + + + 場所 + + + 数量 + + + 受け取り + + + 在庫受け取り + + + 在庫出荷 + \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Resources/App.ko-KR.resx b/src/CleanAspire.ClientApp/Resources/App.ko-KR.resx index 5203bbe..e5c297b 100644 --- a/src/CleanAspire.ClientApp/Resources/App.ko-KR.resx +++ b/src/CleanAspire.ClientApp/Resources/App.ko-KR.resx @@ -648,4 +648,40 @@ 블레이저 어스파이어 + + 제품이 필요합니다 + + + 위치가 필요합니다 + + + 수량은 0보다 커야 합니다 + + + 재고가 성공적으로 입고되었습니다 + + + 재고가 성공적으로 출고되었습니다 + + + 명령 + + + 제품 이름 + + + 위치 + + + 수량 + + + 수령 + + + 재고 수령 + + + 재고 출고 + \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Resources/App.pt-BR.resx b/src/CleanAspire.ClientApp/Resources/App.pt-BR.resx index f66ea7c..68ac05d 100644 --- a/src/CleanAspire.ClientApp/Resources/App.pt-BR.resx +++ b/src/CleanAspire.ClientApp/Resources/App.pt-BR.resx @@ -648,4 +648,40 @@ Blazor Aspire + + O produto é obrigatório + + + A localização é obrigatória + + + A quantidade deve ser maior que 0 + + + Estoque recebido com sucesso + + + Estoque despachado com sucesso + + + Comandos + + + Nome do produto + + + Localização + + + Quantidade + + + Recebimento + + + Recebimento de estoque + + + Despacho de estoque + \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Resources/App.resx b/src/CleanAspire.ClientApp/Resources/App.resx index 3566018..98a473a 100644 --- a/src/CleanAspire.ClientApp/Resources/App.resx +++ b/src/CleanAspire.ClientApp/Resources/App.resx @@ -648,4 +648,40 @@ Blazor Aspire + + Product is required. + + + Location is required. + + + Quantity must be greater than 0. + + + Stock received successfully. + + + Stock dispatched successfully. + + + Commands + + + Product Name + + + Location + + + Quantity + + + Receiving + + + Stock receiving + + + Stock dispatching + \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Resources/App.zh-CN.resx b/src/CleanAspire.ClientApp/Resources/App.zh-CN.resx index 77155d7..e025907 100644 --- a/src/CleanAspire.ClientApp/Resources/App.zh-CN.resx +++ b/src/CleanAspire.ClientApp/Resources/App.zh-CN.resx @@ -648,4 +648,40 @@ Blazor 渴望 + + 产品是必需的 + + + 位置是必需的 + + + 数量必须大于0 + + + 库存接收成功 + + + 库存发货成功 + + + 命令 + + + 产品名称 + + + 位置 + + + 数量 + + + 收货 + + + 收货 + + + 发货 + \ No newline at end of file From b4eca5fb4cf85293133498ff392731e9b913ce1c Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Tue, 7 Jan 2025 19:56:00 +0800 Subject: [PATCH 13/32] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 06a7904..ecbda54 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ By incorporating robust offline capabilities, CleanAspire empowers developers to - The system detects the online/offline status and fetches data from **IndexedDB** when offline, ensuring uninterrupted access to key features. +### How to Create a New Object in a CRUD Application: A Step-by-Step Guide +https://github.com/neozhu/cleanaspire/issues/34 ### 🌟 Why Choose CleanAspire? From fb32ea62f886264d54c35244277554bd1cafd596 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Tue, 7 Jan 2025 20:41:55 +0800 Subject: [PATCH 14/32] Update NavbarMenu.cs --- src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs b/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs index 9c2efdc..b5adbb4 100644 --- a/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs +++ b/src/CleanAspire.ClientApp/Services/Navigation/NavbarMenu.cs @@ -26,14 +26,14 @@ public static class NavbarMenu { Label = "All Products", Href = "/products/index", - Status = PageStatus.New, + Status = PageStatus.Completed, Description = "View all available products in our inventory." }, new MenuItem { Label = "Stock Inquiry", Href = "/stocks/index", - Status = PageStatus.Completed, + Status = PageStatus.New, Description = "Check product stock levels." }, new MenuItem From 7bf43ca34b16b9221fcfb285b7431805e7c3380b Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Wed, 8 Jan 2025 08:28:45 +0800 Subject: [PATCH 15/32] commit --- src/CleanAspire.ClientApp/Pages/Stocks/Index.razor | 3 +-- src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor | 5 ++--- src/CleanAspire.ClientApp/Program.cs | 9 +++++++-- src/CleanAspire.ClientApp/Properties/launchSettings.json | 6 ++++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor b/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor index 06ae9d7..1cedda7 100644 --- a/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor +++ b/src/CleanAspire.ClientApp/Pages/Stocks/Index.razor @@ -135,8 +135,7 @@ var parameters = new DialogParameters { {x=>x.Inbound, false}, - {x=>x.ProductId,item.Product?.Id }, - {x=>x.model, new StockDialog.Model(){ Location = item.Location, Quantity=item.Quantity } } + {x=>x.model, new StockDialog.Model(){ Location = item.Location, Quantity=item.Quantity, ProductId=item.ProductId } } }; await DialogServiceHelper.ShowDialogAsync(L["Stock dispatching"], parameters, new DialogOptions() { MaxWidth = MaxWidth.Small }, async (state) => diff --git a/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor b/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor index 4051800..7c7b810 100644 --- a/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor +++ b/src/CleanAspire.ClientApp/Pages/Stocks/StockDialog.razor @@ -4,7 +4,7 @@
- +
@@ -19,8 +19,6 @@ @code { [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!; [Parameter] - public string? ProductId { get; set; } - [Parameter] public bool Inbound { get; set; } [Parameter] public Model model { get; set; } = new Model(); @@ -84,6 +82,7 @@ } public class Model { + public string? ProductId { get; set; } [Required(ErrorMessage = "Product id is required.")] public ProductDto? Product { get; set; } [Required(ErrorMessage = "Location id is required.")] diff --git a/src/CleanAspire.ClientApp/Program.cs b/src/CleanAspire.ClientApp/Program.cs index 160a715..b5470b3 100644 --- a/src/CleanAspire.ClientApp/Program.cs +++ b/src/CleanAspire.ClientApp/Program.cs @@ -3,12 +3,17 @@ using Microsoft.JSInterop; using System.Globalization; using CleanAspire.ClientApp.Services.Interfaces; +using Microsoft.AspNetCore.Components.Web; var builder = WebAssemblyHostBuilder.CreateDefault(args); -//builder.RootComponents.Add("#app"); -//builder.RootComponents.Add("head::after"); +var renderMode = Environment.GetEnvironmentVariable("BLAZOR_RENDER_MODE"); +if (renderMode?.Equals("Standalone", StringComparison.OrdinalIgnoreCase) == true) +{ + builder.RootComponents.Add("#app"); + builder.RootComponents.Add("head::after"); +} // register the cookie handler builder.Services.AddCoreServices(builder.Configuration); builder.Services.AddHttpClients(builder.Configuration); diff --git a/src/CleanAspire.ClientApp/Properties/launchSettings.json b/src/CleanAspire.ClientApp/Properties/launchSettings.json index 05afea0..422339c 100644 --- a/src/CleanAspire.ClientApp/Properties/launchSettings.json +++ b/src/CleanAspire.ClientApp/Properties/launchSettings.json @@ -8,7 +8,8 @@ "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5180", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "BLAZOR_RENDER_MODE": "Standalone" } }, "https": { @@ -18,7 +19,8 @@ "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:7123;http://localhost:5180", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "BLAZOR_RENDER_MODE": "Standalone" } } } From a7595e1233056706f003e14acc16f6279eb0b915 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 10:11:55 +0800 Subject: [PATCH 16/32] add build blazor webassembly standalone image --- .github/workflows/docker.yml | 5 +- src/CleanAspire.ClientApp/Dockerfile | 48 +++++++++++++++++++ src/CleanAspire.ClientApp/Program.cs | 6 +-- .../Properties/launchSettings.json | 25 +++++----- src/CleanAspire.ClientApp/nginx.conf | 31 ++++++++++++ 5 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 src/CleanAspire.ClientApp/Dockerfile create mode 100644 src/CleanAspire.ClientApp/nginx.conf diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0edcb7a..de5f59d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -31,7 +31,10 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - + - name: Build and push CleanAspire.Standalone image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-standalone:${{ steps.version.outputs.version }} -f src/CleanAspire.ClientApp/Dockerfile . + docker push ${{ secrets.DOCKER_USERNAME }}/cleanaspire-standalone:${{ steps.version.outputs.version }} - name: Build and push CleanAspire.WebApp image run: | docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-webapp:${{ steps.version.outputs.version }} -f src/CleanAspire.WebApp/Dockerfile . diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile new file mode 100644 index 0000000..c8731df --- /dev/null +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -0,0 +1,48 @@ +# Stage 1: Build the Blazor Client Application +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Set ASPNETCORE_ENVIRONMENT to Standalone during build +ENV ASPNETCORE_ENVIRONMENT=Standalone + +# Install Python for AOT compilation +RUN apt-get update && apt-get install -y python3 python3-pip && ln -s /usr/bin/python3 /usr/bin/python + +# Copy the project files and restore dependencies +COPY ["src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj", "src/CleanAspire.ClientApp/"] +RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" + +# Install wasm-tools for AOT +RUN dotnet workload install wasm-tools --skip-manifest-update +RUN dotnet workload update + +# Copy the entire source code and build the application in Release mode +COPY . . +RUN dotnet publish -c Release -o /app/publish + +# Stage 2: Serve the Blazor Client Application using Nginx +FROM nginx:alpine AS final +WORKDIR /usr/share/nginx/html + +# Set ASPNETCORE_ENVIRONMENT to Standalone for the runtime environment +ENV ASPNETCORE_ENVIRONMENT=Standalone + +# Install OpenSSL to create a self-signed certificate +RUN apk add --no-cache openssl && \ + openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt -subj "/CN=localhost" + +# Clean the default nginx content +RUN rm -rf ./* + +# Copy the build output from the previous stage +COPY --from=build /app/publish/wwwroot . + +# Copy the generated self-signed certificate and configure Nginx for HTTPS +COPY src/CleanAspire.ClientApp/nginx.conf /etc/nginx/nginx.conf + +# Expose port 80 for HTTP traffic and 443 for HTTPS traffic +EXPOSE 80 +EXPOSE 443 + +# Start Nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/Program.cs b/src/CleanAspire.ClientApp/Program.cs index b5470b3..42dd616 100644 --- a/src/CleanAspire.ClientApp/Program.cs +++ b/src/CleanAspire.ClientApp/Program.cs @@ -1,14 +1,10 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using CleanAspire.ClientApp; -using Microsoft.JSInterop; -using System.Globalization; -using CleanAspire.ClientApp.Services.Interfaces; using Microsoft.AspNetCore.Components.Web; var builder = WebAssemblyHostBuilder.CreateDefault(args); -var renderMode = Environment.GetEnvironmentVariable("BLAZOR_RENDER_MODE"); - +var renderMode = builder.HostEnvironment.Environment; if (renderMode?.Equals("Standalone", StringComparison.OrdinalIgnoreCase) == true) { builder.RootComponents.Add("#app"); diff --git a/src/CleanAspire.ClientApp/Properties/launchSettings.json b/src/CleanAspire.ClientApp/Properties/launchSettings.json index 422339c..f052442 100644 --- a/src/CleanAspire.ClientApp/Properties/launchSettings.json +++ b/src/CleanAspire.ClientApp/Properties/launchSettings.json @@ -1,27 +1,26 @@ { - "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "http://localhost:5180", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "BLAZOR_RENDER_MODE": "Standalone" - } + }, + "dotnetRunMessages": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5180" }, "https": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:7123;http://localhost:5180", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "BLAZOR_RENDER_MODE": "Standalone" - } + "ASPNETCORE_ENVIRONMENT": "Standalone" + }, + "dotnetRunMessages": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7123;http://localhost:5180" } - } -} + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/nginx.conf b/src/CleanAspire.ClientApp/nginx.conf new file mode 100644 index 0000000..8061dbe --- /dev/null +++ b/src/CleanAspire.ClientApp/nginx.conf @@ -0,0 +1,31 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Define a server block here + server { + listen 80; + listen 443 ssl; + + ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; + ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; + + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + error_page 404 /404.html; + location = /40x.html { + } + } +} \ No newline at end of file From 07f302814c87230f74614204339752f91def093e Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 10:19:07 +0800 Subject: [PATCH 17/32] fix dockerfile --- .github/workflows/docker.yml | 1 + src/CleanAspire.ClientApp/Dockerfile | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index de5f59d..fb60f14 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,6 +35,7 @@ jobs: run: | docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-standalone:${{ steps.version.outputs.version }} -f src/CleanAspire.ClientApp/Dockerfile . docker push ${{ secrets.DOCKER_USERNAME }}/cleanaspire-standalone:${{ steps.version.outputs.version }} + - name: Build and push CleanAspire.WebApp image run: | docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-webapp:${{ steps.version.outputs.version }} -f src/CleanAspire.WebApp/Dockerfile . diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index c8731df..61819c9 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -16,6 +16,10 @@ RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" RUN dotnet workload install wasm-tools --skip-manifest-update RUN dotnet workload update +# Clean and build +RUN dotnet clean "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" +RUN dotnet workload install wasm-tools --skip-manifest-update + # Copy the entire source code and build the application in Release mode COPY . . RUN dotnet publish -c Release -o /app/publish From 025548155b9eca94c9039d6ab7487c1be88eaaf8 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 10:43:08 +0800 Subject: [PATCH 18/32] Update Program.cs --- src/CleanAspire.ClientApp/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CleanAspire.ClientApp/Program.cs b/src/CleanAspire.ClientApp/Program.cs index 42dd616..5f369e0 100644 --- a/src/CleanAspire.ClientApp/Program.cs +++ b/src/CleanAspire.ClientApp/Program.cs @@ -5,6 +5,7 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args); var renderMode = builder.HostEnvironment.Environment; +Console.WriteLine($"Environment: {renderMode}"); if (renderMode?.Equals("Standalone", StringComparison.OrdinalIgnoreCase) == true) { builder.RootComponents.Add("#app"); From 5ffa9a278cf0234a703d2db9584e4b3a1eee4405 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 10:50:07 +0800 Subject: [PATCH 19/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index 61819c9..12b3e06 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -49,4 +49,4 @@ EXPOSE 80 EXPOSE 443 # Start Nginx -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["sh", "-c", "export ASPNETCORE_ENVIRONMENT=Standalone && nginx -g 'daemon off;'"] \ No newline at end of file From f4885637ba4bf175cebc86d5246e4738505ccf10 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 10:55:32 +0800 Subject: [PATCH 20/32] Update CleanAspire.ClientApp.csproj --- .../CleanAspire.ClientApp.csproj | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj index 3f3d870..ea5182f 100644 --- a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj +++ b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj @@ -11,14 +11,6 @@ CleanAspire.ClientApp CleanAspire.ClientApp
- @@ -41,8 +33,4 @@ - - - -
From e910eac76cb0601e5a582bf8f26a89e8281060ed Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 11:11:09 +0800 Subject: [PATCH 21/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index 12b3e06..d9b7d7a 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -18,7 +18,6 @@ RUN dotnet workload update # Clean and build RUN dotnet clean "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" -RUN dotnet workload install wasm-tools --skip-manifest-update # Copy the entire source code and build the application in Release mode COPY . . From 0b67a77a6c59766d0412b34063288816517099d4 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 11:23:36 +0800 Subject: [PATCH 22/32] Update index.html --- src/CleanAspire.ClientApp/wwwroot/index.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/CleanAspire.ClientApp/wwwroot/index.html b/src/CleanAspire.ClientApp/wwwroot/index.html index 4734b38..d8b3904 100644 --- a/src/CleanAspire.ClientApp/wwwroot/index.html +++ b/src/CleanAspire.ClientApp/wwwroot/index.html @@ -227,6 +227,17 @@ 🗙 + From 3051b3c15a741532b505457c1deb9dfd83551be6 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 12:59:00 +0800 Subject: [PATCH 23/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index d9b7d7a..1cbf4a2 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -23,29 +23,29 @@ RUN dotnet clean "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" COPY . . RUN dotnet publish -c Release -o /app/publish -# Stage 2: Serve the Blazor Client Application using Nginx -FROM nginx:alpine AS final -WORKDIR /usr/share/nginx/html +# Stage 2: Configure and Run the Blazor WebAssembly Host +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app # Set ASPNETCORE_ENVIRONMENT to Standalone for the runtime environment ENV ASPNETCORE_ENVIRONMENT=Standalone -# Install OpenSSL to create a self-signed certificate -RUN apk add --no-cache openssl && \ - openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt -subj "/CN=localhost" - -# Clean the default nginx content -RUN rm -rf ./* - # Copy the build output from the previous stage -COPY --from=build /app/publish/wwwroot . +COPY --from=build /app/publish . + +# Install OpenSSL to create a self-signed certificate +RUN apt-get update && apt-get install -y openssl && \ + mkdir /https && \ + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /https/aspnetcore.key -out /https/aspnetcore.crt \ + -subj "/CN=localhost" -# Copy the generated self-signed certificate and configure Nginx for HTTPS -COPY src/CleanAspire.ClientApp/nginx.conf /etc/nginx/nginx.conf +# Configure Kestrel to use HTTPS with the self-signed certificate +ENV ASPNETCORE_URLS="http://+:80;https://+:443" -# Expose port 80 for HTTP traffic and 443 for HTTPS traffic +# Expose ports for HTTP and HTTPS EXPOSE 80 EXPOSE 443 -# Start Nginx -CMD ["sh", "-c", "export ASPNETCORE_ENVIRONMENT=Standalone && nginx -g 'daemon off;'"] \ No newline at end of file +# Start the application using WebAssemblyHost +ENTRYPOINT ["dotnet", "CleanAspire.ClientApp.dll"] From 2059c1c8dcb533462c166d04d76907cc4e200592 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 13:07:45 +0800 Subject: [PATCH 24/32] Update CleanAspire.ClientApp.csproj --- src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj index ea5182f..787a2e4 100644 --- a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj +++ b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj @@ -4,6 +4,8 @@ net9.0 enable enable + true + false true Default true From d9d7e5ca115a6d1c47a3e9eb9851419f2d92622d Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 13:10:57 +0800 Subject: [PATCH 25/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index 1cbf4a2..b00f99a 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -10,18 +10,12 @@ RUN apt-get update && apt-get install -y python3 python3-pip && ln -s /usr/bin/p # Copy the project files and restore dependencies COPY ["src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj", "src/CleanAspire.ClientApp/"] -RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" - -# Install wasm-tools for AOT RUN dotnet workload install wasm-tools --skip-manifest-update -RUN dotnet workload update - -# Clean and build -RUN dotnet clean "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" +RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" # Copy the entire source code and build the application in Release mode COPY . . -RUN dotnet publish -c Release -o /app/publish +RUN dotnet publish "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" -c Release -o /app/publish # Stage 2: Configure and Run the Blazor WebAssembly Host FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final From fcf4fb9ee6e1a0b84ae9159b53e28c54b58e33a8 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 13:20:43 +0800 Subject: [PATCH 26/32] Update CleanAspire.ClientApp.csproj --- src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj index 787a2e4..ea5182f 100644 --- a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj +++ b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj @@ -4,8 +4,6 @@ net9.0 enable enable - true - false true Default true From 0e5b3ac40005ea14646a800b76ca964b1e78f8d9 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 13:41:19 +0800 Subject: [PATCH 27/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 45 +++++++++++++++++----------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index b00f99a..500e73b 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -1,41 +1,50 @@ # Stage 1: Build the Blazor Client Application FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release WORKDIR /src -# Set ASPNETCORE_ENVIRONMENT to Standalone during build -ENV ASPNETCORE_ENVIRONMENT=Standalone - # Install Python for AOT compilation RUN apt-get update && apt-get install -y python3 python3-pip && ln -s /usr/bin/python3 /usr/bin/python # Copy the project files and restore dependencies COPY ["src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj", "src/CleanAspire.ClientApp/"] RUN dotnet workload install wasm-tools --skip-manifest-update -RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" - -# Copy the entire source code and build the application in Release mode +RUN dotnet restore "./src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" COPY . . -RUN dotnet publish "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" -c Release -o /app/publish +WORKDIR "/src/src/CleanAspire.ClientApp" +RUN dotnet publish "./CleanAspire.ClientApp.csproj" -c Release -o /app/publish + + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./CleanAspire.ClientApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + # Stage 2: Configure and Run the Blazor WebAssembly Host FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final WORKDIR /app +COPY --from=publish /app/publish . -# Set ASPNETCORE_ENVIRONMENT to Standalone for the runtime environment -ENV ASPNETCORE_ENVIRONMENT=Standalone +# Install OpenSSL +RUN apt-get update && apt-get install -y openssl -# Copy the build output from the previous stage -COPY --from=build /app/publish . +# Generate a self-signed certificate +RUN mkdir -p /app/https && \ + openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ + -keyout /app/https/private.key -out /app/https/certificate.crt \ + -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" && \ + openssl pkcs12 -export -out /app/https/aspnetapp.pfx \ + -inkey /app/https/private.key -in /app/https/certificate.crt \ + -password pass:CREDENTIAL_PLACEHOLDER -# Install OpenSSL to create a self-signed certificate -RUN apt-get update && apt-get install -y openssl && \ - mkdir /https && \ - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout /https/aspnetcore.key -out /https/aspnetcore.crt \ - -subj "/CN=localhost" # Configure Kestrel to use HTTPS with the self-signed certificate -ENV ASPNETCORE_URLS="http://+:80;https://+:443" +# Setup environment variables for the application to find the certificate +ENV ASPNETCORE_ENVIRONMENT=Standalone +ENV ASPNETCORE_URLS=http://+:80;https://+:443 +ENV ASPNETCORE_Kestrel__Certificates__Default__Password="CREDENTIAL_PLACEHOLDER" +ENV ASPNETCORE_Kestrel__Certificates__Default__Path="/app/https/aspnetapp.pfx" # Expose ports for HTTP and HTTPS EXPOSE 80 From 536ccec3d2ca9bb3294b6577274f33777ade029f Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 13:45:06 +0800 Subject: [PATCH 28/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index 500e73b..498f6f9 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -12,7 +12,7 @@ RUN dotnet workload install wasm-tools --skip-manifest-update RUN dotnet restore "./src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" COPY . . WORKDIR "/src/src/CleanAspire.ClientApp" -RUN dotnet publish "./CleanAspire.ClientApp.csproj" -c Release -o /app/publish +RUN dotnet publish "./CleanAspire.ClientApp.csproj" -c Release -o /app/build # This stage is used to publish the service project to be copied to the final stage From b7976e679d03e370378de78a5b542046cf030a60 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 13:47:37 +0800 Subject: [PATCH 29/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index 498f6f9..9bdc366 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -12,7 +12,7 @@ RUN dotnet workload install wasm-tools --skip-manifest-update RUN dotnet restore "./src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" COPY . . WORKDIR "/src/src/CleanAspire.ClientApp" -RUN dotnet publish "./CleanAspire.ClientApp.csproj" -c Release -o /app/build +RUN dotnet build "./CleanAspire.ClientApp.csproj" -c $BUILD_CONFIGURATION -o /app/build # This stage is used to publish the service project to be copied to the final stage From 33441fa59e3543adab5867d56d16543d40a469a2 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 13:54:24 +0800 Subject: [PATCH 30/32] Update Dockerfile --- src/CleanAspire.ClientApp/Dockerfile | 56 +++++++++++----------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index 9bdc366..9f2253a 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -1,6 +1,5 @@ # Stage 1: Build the Blazor Client Application FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -ARG BUILD_CONFIGURATION=Release WORKDIR /src # Install Python for AOT compilation @@ -8,47 +7,36 @@ RUN apt-get update && apt-get install -y python3 python3-pip && ln -s /usr/bin/p # Copy the project files and restore dependencies COPY ["src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj", "src/CleanAspire.ClientApp/"] -RUN dotnet workload install wasm-tools --skip-manifest-update -RUN dotnet restore "./src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" -COPY . . -WORKDIR "/src/src/CleanAspire.ClientApp" -RUN dotnet build "./CleanAspire.ClientApp.csproj" -c $BUILD_CONFIGURATION -o /app/build - +RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" -# This stage is used to publish the service project to be copied to the final stage -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./CleanAspire.ClientApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +# Install wasm-tools for AOT +RUN dotnet workload install wasm-tools --skip-manifest-update +RUN dotnet workload update +# Copy the entire source code and build the application in Release mode +COPY . . +RUN dotnet publish -c Release -o /app/publish -# Stage 2: Configure and Run the Blazor WebAssembly Host -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final -WORKDIR /app -COPY --from=publish /app/publish . +# Stage 2: Serve the Blazor Client Application using Nginx +FROM nginx:alpine AS final +WORKDIR /usr/share/nginx/html -# Install OpenSSL -RUN apt-get update && apt-get install -y openssl +# Install OpenSSL to create a self-signed certificate +RUN apk add --no-cache openssl && \ + openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt -subj "/CN=localhost" -# Generate a self-signed certificate -RUN mkdir -p /app/https && \ - openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ - -keyout /app/https/private.key -out /app/https/certificate.crt \ - -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" && \ - openssl pkcs12 -export -out /app/https/aspnetapp.pfx \ - -inkey /app/https/private.key -in /app/https/certificate.crt \ - -password pass:CREDENTIAL_PLACEHOLDER +# Clean the default nginx content +RUN rm -rf ./* +# Copy the build output from the previous stage +COPY --from=build /app/publish/wwwroot . -# Configure Kestrel to use HTTPS with the self-signed certificate -# Setup environment variables for the application to find the certificate -ENV ASPNETCORE_ENVIRONMENT=Standalone -ENV ASPNETCORE_URLS=http://+:80;https://+:443 -ENV ASPNETCORE_Kestrel__Certificates__Default__Password="CREDENTIAL_PLACEHOLDER" -ENV ASPNETCORE_Kestrel__Certificates__Default__Path="/app/https/aspnetapp.pfx" +# Copy the generated self-signed certificate and configure Nginx for HTTPS +COPY src/CleanAspire.ClientApp/nginx.conf /etc/nginx/nginx.conf -# Expose ports for HTTP and HTTPS +# Expose port 80 for HTTP traffic and 443 for HTTPS traffic EXPOSE 80 EXPOSE 443 -# Start the application using WebAssemblyHost -ENTRYPOINT ["dotnet", "CleanAspire.ClientApp.dll"] +# Start Nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file From 3448142ea6a55954d687a3cde8037cafa37b5021 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 14:35:16 +0800 Subject: [PATCH 31/32] commit --- src/CleanAspire.ClientApp/Dockerfile | 2 +- src/CleanAspire.ClientApp/Program.cs | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile index 9f2253a..10ef2f6 100644 --- a/src/CleanAspire.ClientApp/Dockerfile +++ b/src/CleanAspire.ClientApp/Dockerfile @@ -15,7 +15,7 @@ RUN dotnet workload update # Copy the entire source code and build the application in Release mode COPY . . -RUN dotnet publish -c Release -o /app/publish +RUN dotnet publish -c Release -o /app/publish -p:DefineConstants="STANDALONE" # Stage 2: Serve the Blazor Client Application using Nginx FROM nginx:alpine AS final diff --git a/src/CleanAspire.ClientApp/Program.cs b/src/CleanAspire.ClientApp/Program.cs index 5f369e0..b022bc5 100644 --- a/src/CleanAspire.ClientApp/Program.cs +++ b/src/CleanAspire.ClientApp/Program.cs @@ -4,13 +4,10 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args); -var renderMode = builder.HostEnvironment.Environment; -Console.WriteLine($"Environment: {renderMode}"); -if (renderMode?.Equals("Standalone", StringComparison.OrdinalIgnoreCase) == true) -{ - builder.RootComponents.Add("#app"); - builder.RootComponents.Add("head::after"); -} +#if STANDALONE +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); +#endif // register the cookie handler builder.Services.AddCoreServices(builder.Configuration); builder.Services.AddHttpClients(builder.Configuration); From e82d6ef604c3094aa1c556b3ed84ac06965f3022 Mon Sep 17 00:00:00 2001 From: hualin Date: Wed, 8 Jan 2025 14:43:28 +0800 Subject: [PATCH 32/32] Update CleanAspire.ClientApp.csproj --- src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj index ea5182f..9c26d00 100644 --- a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj +++ b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj @@ -1,6 +1,7 @@  + DEFAULT net9.0 enable enable