diff --git a/PointOfSale/Pos.Models/TotalSaleDetail.cs b/PointOfSale/Pos.Models/TotalSaleDetail.cs index 1ec27fa..b6fc7e6 100644 --- a/PointOfSale/Pos.Models/TotalSaleDetail.cs +++ b/PointOfSale/Pos.Models/TotalSaleDetail.cs @@ -1,15 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; -namespace Pos.Models +namespace Pos.Models; + +public class TotalSaleDetail { - public class TotalSaleDetail - { - public decimal TotalSale { get; set; } - public int TotalCustomer { get; set; } - public List TotalSaleCategories { get; set; } = new(); - } + public decimal TotalSale { get; set; } + public int TotalCustomer { get; set; } + public List TotalSaleCategories { get; set; } = new(); + public List PaymentBreakdown { get; set; } = new(); } diff --git a/PointOfSale/Pos.Models/TotalSalePayment.cs b/PointOfSale/Pos.Models/TotalSalePayment.cs new file mode 100644 index 0000000..60f66cb --- /dev/null +++ b/PointOfSale/Pos.Models/TotalSalePayment.cs @@ -0,0 +1,7 @@ +namespace Pos.Models; + +public class TotalSalePayment +{ + public string PaymentMethod { get; set; } = string.Empty; + public decimal Amount { get; set; } +} diff --git a/PointOfSale/Pos.Service/IProductService.cs b/PointOfSale/Pos.Service/IProductService.cs new file mode 100644 index 0000000..06c9b4b --- /dev/null +++ b/PointOfSale/Pos.Service/IProductService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Database.Models; + +namespace Pos.Service; + +public interface IProductService +{ + List GetProducts(int productGroupId); + ProductEntity GetProduct(int productId); + void AddProduct(string name, int productGroupId); + void UpdateProduct(int productId, int productGroupId, string name); + void ArchiveProduct(int productId); + void UpdateOrder(int productGroupId, IReadOnlyList orderedProductIds); +} diff --git a/PointOfSale/Pos.Service/ProductService.cs b/PointOfSale/Pos.Service/ProductService.cs index 2f8d737..c2388e8 100644 --- a/PointOfSale/Pos.Service/ProductService.cs +++ b/PointOfSale/Pos.Service/ProductService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Database.Models; @@ -62,12 +62,12 @@ namespace Pos.Service if (orderedProductIds.Any(id => id <= 0)) throw new ArgumentException( - "Rækkefølgen indeholder et ugyldigt id.", + "Rækkefølgen indeholder et ugyldigt id.", nameof(orderedProductIds)); if (orderedProductIds.Distinct().Count() != orderedProductIds.Count) throw new ArgumentException( - "Rækkefølgen indeholder dubletter.", + "Rækkefølgen indeholder dubletter.", nameof(orderedProductIds)); _productRepository.SetOrder( @@ -93,3 +93,5 @@ namespace Pos.Service } } } + + diff --git a/PointOfSale/Pos.Service/SaleService.cs b/PointOfSale/Pos.Service/SaleService.cs index 0c3db6c..7bd6c3a 100644 --- a/PointOfSale/Pos.Service/SaleService.cs +++ b/PointOfSale/Pos.Service/SaleService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Configuration; using System.Linq; @@ -103,6 +103,34 @@ namespace Pos.Service foreach (SaleEntity sale in sales) { List saleLineBySaleId = _saleRepository.GetSaleLineBySaleId(sale.Id); + List paymentsBySaleId = + _saleRepository.GetPaymentBySaleId(sale.Id); + + foreach (PaymentEntity payment in paymentsBySaleId) + { + string paymentMethod = string.IsNullOrWhiteSpace(payment.Type) + ? "Ukendt" + : payment.Type; + + TotalSalePayment? paymentSummary = + totalSaleDetail.PaymentBreakdown + .FirstOrDefault(item => + item.PaymentMethod == paymentMethod); + + if (paymentSummary is null) + { + totalSaleDetail.PaymentBreakdown.Add( + new TotalSalePayment + { + PaymentMethod = paymentMethod, + Amount = payment.Amount + }); + } + else + { + paymentSummary.Amount += payment.Amount; + } + } foreach (SaleLineEntity saleLine in saleLineBySaleId) { @@ -303,3 +331,4 @@ namespace Pos.Service } } } + diff --git a/PointOfSale/Pos.Web/Components/Pages/Home.razor b/PointOfSale/Pos.Web/Components/Pages/Home.razor index 2201fc9..d3814f3 100644 --- a/PointOfSale/Pos.Web/Components/Pages/Home.razor +++ b/PointOfSale/Pos.Web/Components/Pages/Home.razor @@ -9,7 +9,7 @@ Kassesalg - + Dagens salg @@ -18,4 +18,4 @@ Indstillinger - \ No newline at end of file + diff --git a/PointOfSale/Pos.Web/Components/Pages/Home.razor.css b/PointOfSale/Pos.Web/Components/Pages/Home.razor.css index 4ba8d18..f86fab8 100644 --- a/PointOfSale/Pos.Web/Components/Pages/Home.razor.css +++ b/PointOfSale/Pos.Web/Components/Pages/Home.razor.css @@ -1,3 +1,4 @@ + .home-container { width: min(100%, 1100px); margin: 32px auto; @@ -11,4 +12,12 @@ min-height: 220px; font-size: 28px; font-weight: 600; -} \ No newline at end of file +} + +@media (max-width: 680px) { + .home-container { + grid-template-columns: 1fr; + margin: 24px 16px; + } +} + diff --git a/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor b/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor index f1f687a..26639cc 100644 --- a/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor +++ b/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor @@ -5,16 +5,14 @@

Indstillinger

-

Administrer data, der bruges i kassesalget.

-
Medarbejder - Vare
-
\ No newline at end of file + + diff --git a/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor.css b/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor.css index 3c7cc52..fe027ad 100644 --- a/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor.css +++ b/PointOfSale/Pos.Web/Components/Pages/Settings/Settings.razor.css @@ -1,3 +1,4 @@ + .settings-page { width: min(100%, 1100px); margin: 0 auto; @@ -22,3 +23,14 @@ font-size: 28px; font-weight: 600; } + +@media (max-width: 680px) { + .settings-page { + padding: 24px 16px; + } + + .settings-grid { + grid-template-columns: 1fr; + } +} + diff --git a/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor b/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor new file mode 100644 index 0000000..200d39e --- /dev/null +++ b/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor @@ -0,0 +1,34 @@ +@page "/todaySales" +@rendermode InteractiveServer +@inject IJSRuntime Js +@inject SaleService SaleService + +
+
+
+ + +
+
+
+

Dagens salg

@Currency(_summary.Total)@_summary.SaleCount salg
+
+
+ + + VaregruppeSalg +
@row.Name@Currency(row.Amount)
+
+
+
+ + +
@row.Name@Currency(row.Amount)
+
+
+ +
+
+
+ + diff --git a/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor.cs b/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor.cs new file mode 100644 index 0000000..01c759e --- /dev/null +++ b/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Pos.Models; +using System.Globalization; + +namespace Pos.Web.Components.Pages.TodaySales; + +public partial class TodaySales +{ + private static readonly CultureInfo Danish = CultureInfo.GetCultureInfo("da-DK"); + private TotalSaleDetail? _totalSaleDetail; + private DateTime _date = DateTime.Now; + private string _range = "1"; + private Summary _summary = new(0m, 0, Array.Empty(), Array.Empty()); + + private async Task DateChanged(ChangeEventArgs e) + { + if (DateTime.TryParse(e.Value?.ToString(), out var date)) + { + _date = date; + await LoadAsync(); + } + } + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + private async Task RangeChanged(ChangeEventArgs e) + { + _range = e.Value?.ToString() ?? "1"; + await LoadAsync(); + } + + private Task LoadAsync() + { + var result = SaleService.TotalSale(_date); + _totalSaleDetail = result; + + _summary = new Summary( + result.TotalSale, + result.TotalCustomer, + result.TotalSaleCategories + .Select(category => new Metric(category.Category, category.Sale)) + .ToList(), + result.PaymentBreakdown + .Select(payment => new Metric(payment.PaymentMethod, payment.Amount)) + .ToList()); + + return Task.CompletedTask; + } + + private async Task PrintAsync() + { + // TODO: Kobl til SaleService.PrintSaleOfDay(_date, _totalSaleDetail) + // eller POST /api/sales/print. + await Js.InvokeVoidAsync("window.print"); + } + + private static string Currency(decimal amount) => + $"{amount.ToString("N2", Danish)} kr."; + + private sealed record Metric(string Name, decimal Amount); + + private sealed record Summary( + decimal Total, + int SaleCount, + IReadOnlyList Categories, + IReadOnlyList Payments); +} diff --git a/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor.css b/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor.css new file mode 100644 index 0000000..e5347af --- /dev/null +++ b/PointOfSale/Pos.Web/Components/Pages/TodaySales/TodaySales.razor.css @@ -0,0 +1,307 @@ + +.today-sales-page { + width: min(100%, 1420px); + min-height: calc(100dvh - 78px); + margin: 0 auto; + padding: 38px 32px 64px; + box-sizing: border-box; +} + +.filters { + display: flex; + align-items: end; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.filter-control { + display: flex; + min-width: 230px; + flex-direction: column; + gap: 6px; + color: #3e444d; + font-size: 14px; +} + +.filter-control.short { + min-width: 140px; +} + +.filter-box { + display: flex; + min-height: 56px; + align-items: center; + gap: 10px; + padding: 0 14px; + border: 1px solid #bfc5cd; + border-radius: 8px; + background: #fff; +} + +.filter-box svg { + width: 23px; + height: 23px; + flex: none; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.7; +} + +.filter-box input, +.filter-box select { + width: 100%; + min-width: 0; + border: 0; + outline: 0; + background: transparent; + font-size: 18px; +} + +.summary-card { + display: flex; + min-height: 170px; + align-items: center; + gap: 32px; + padding: 28px 34px; +} + +.summary-icon { + display: grid; + width: 114px; + height: 114px; + flex: none; + place-items: center; + border-radius: 50%; + color: var(--pos-blue); + background: var(--pos-blue-soft); +} + +.summary-icon svg { + width: 58px; + height: 58px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.summary-card p { + margin: 0 0 4px; + font-size: 19px; +} + +.summary-card strong { + display: block; + font-size: 48px; + line-height: 1.05; + letter-spacing: -.035em; +} + +.summary-card small { + display: block; + margin-top: 6px; + color: var(--pos-muted); + font-size: 18px; +} + +.sales-grid { + display: grid; + grid-template-columns: minmax(0, 1.25fr) minmax(350px, 1fr); + gap: 20px; + align-items: start; + margin-top: 20px; +} + +.sales-right { + display: flex; + flex-direction: column; + gap: 20px; +} + +::deep .ui-card { + overflow: hidden; + border: 1px solid var(--pos-border); + border-radius: 12px; + background: #fff; + box-shadow: 0 1px 2px rgba(16, 24, 40, .03); +} + +::deep .ui-card__header { + padding: 26px 32px 16px; +} + +::deep .ui-card__header h2 { + margin: 0; + font-size: 22px; + font-weight: 650; +} + +::deep .ui-card__body { + padding: 0 32px 28px; +} + +::deep .metric-list__header, +::deep .metric-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 16px; +} + +::deep .metric-list__header { + padding-bottom: 12px; + color: var(--pos-muted); +} + +::deep .metric-row { + min-height: 58px; + align-items: center; + border-top: 1px solid var(--pos-border-soft); + font-size: 19px; +} + +::deep .metric-row > :last-child, +::deep .metric-list__header > :last-child { + text-align: right; + white-space: nowrap; +} + +.print-button { + display: flex; + width: 100%; + min-height: 70px; + align-items: center; + justify-content: center; + gap: 14px; + border: 1.5px solid var(--pos-blue); + border-radius: 9px; + color: var(--pos-blue); + background: #fff; + font-weight: 700; +} + +.print-button:hover { + background: var(--pos-blue-soft); +} + +.print-button svg { + width: 28px; + height: 28px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.8; +} + +@media (min-width: 900px) and (max-height: 850px) { + .today-sales-page { + padding-top: 22px; + padding-bottom: 18px; + } + + .filters { + margin-bottom: 14px; + } + + .filter-box { + min-height: 48px; + } + + .summary-card { + min-height: 130px; + gap: 24px; + padding: 20px 28px; + } + + .summary-icon { + width: 88px; + height: 88px; + } + + .summary-icon svg { + width: 48px; + height: 48px; + } + + .summary-card strong { + font-size: 40px; + } + + .summary-card small { + font-size: 16px; + } + + .sales-grid { + gap: 20px; + margin-top: 16px; + } + + ::deep .ui-card__header { + padding: 18px 28px 10px; + } + + ::deep .ui-card__body { + padding: 0 28px 16px; + } + + ::deep .metric-list__header { + padding-bottom: 7px; + } + + ::deep .metric-row { + min-height: 39px; + font-size: 17px; + } + + .print-button { + min-height: 54px; + } +} + +@media (max-width: 900px) { + .sales-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 680px) { + .today-sales-page { + padding: 24px 16px 40px; + } + + .summary-card { + gap: 18px; + padding: 22px 20px; + } + + .summary-icon { + width: 72px; + height: 72px; + } + + .summary-icon svg { + width: 40px; + height: 40px; + } + + .summary-card strong { + font-size: 34px; + } + + ::deep .ui-card__header { + padding: 22px 20px 14px; + } + + ::deep .ui-card__body { + padding: 0 20px 22px; + } + + ::deep .metric-row { + font-size: 17px; + } +} + diff --git a/PointOfSale/Pos.Web/Components/Shared/Header.razor b/PointOfSale/Pos.Web/Components/Shared/Header.razor index 5ed41c5..df633b7 100644 --- a/PointOfSale/Pos.Web/Components/Shared/Header.razor +++ b/PointOfSale/Pos.Web/Components/Shared/Header.razor @@ -9,4 +9,4 @@ "dddd d. MMMM yyyy · HH:mm", _danishCulture) -
\ No newline at end of file +
diff --git a/PointOfSale/Pos.Web/Components/Shared/Header.razor.cs b/PointOfSale/Pos.Web/Components/Shared/Header.razor.cs index b3b6e94..42461fb 100644 --- a/PointOfSale/Pos.Web/Components/Shared/Header.razor.cs +++ b/PointOfSale/Pos.Web/Components/Shared/Header.razor.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace Pos.Web.Components.Shared { diff --git a/PointOfSale/Pos.Web/Components/Shared/MetricList.razor b/PointOfSale/Pos.Web/Components/Shared/MetricList.razor new file mode 100644 index 0000000..2c368ee --- /dev/null +++ b/PointOfSale/Pos.Web/Components/Shared/MetricList.razor @@ -0,0 +1,10 @@ +@typeparam TItem +
+ @if (HeaderContent is not null) {
@HeaderContent
} + @foreach (var item in Items) { @RowTemplate(item) } +
+@code { + [Parameter] public IReadOnlyList Items { get; set; } = Array.Empty(); + [Parameter] public RenderFragment? HeaderContent { get; set; } + [Parameter] public RenderFragment RowTemplate { get; set; } = default!; +} diff --git a/PointOfSale/Pos.Web/Components/Shared/UiCard.razor b/PointOfSale/Pos.Web/Components/Shared/UiCard.razor new file mode 100644 index 0000000..e6160f0 --- /dev/null +++ b/PointOfSale/Pos.Web/Components/Shared/UiCard.razor @@ -0,0 +1,12 @@ +
+ @if (!string.IsNullOrWhiteSpace(Title)) + { +

@Title

+ } +
@ChildContent
+
+@code { + [Parameter] public string Title { get; set; } = string.Empty; + [Parameter] public string Class { get; set; } = string.Empty; + [Parameter] public RenderFragment? ChildContent { get; set; } +} diff --git a/PointOfSale/Pos.Web/Components/_Imports.razor b/PointOfSale/Pos.Web/Components/_Imports.razor index 8a99947..3b626be 100644 --- a/PointOfSale/Pos.Web/Components/_Imports.razor +++ b/PointOfSale/Pos.Web/Components/_Imports.razor @@ -1,5 +1,6 @@ @using System.Net.Http @using System.Net.Http.Json +@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @@ -11,4 +12,7 @@ @using Pos.Web @using Pos.Web.Components @using Pos.Web.Components.Layout -@using Pos.Web.Components.Shared \ No newline at end of file +@using Pos.Web.Components.Shared +@using Pos.Service + + diff --git a/PointOfSale/Pos.Web/Program.cs b/PointOfSale/Pos.Web/Program.cs index 7126fd1..80cf27d 100644 --- a/PointOfSale/Pos.Web/Program.cs +++ b/PointOfSale/Pos.Web/Program.cs @@ -1,31 +1,33 @@ -using Microsoft.FluentUI.AspNetCore.Components; -using Pos.Web.Components; +using Microsoft.FluentUI.AspNetCore.Components; using Pos.Service; +using Pos.Web.Components; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -// Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); + builder.Services.AddFluentUIComponents(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); app.MapDefaultEndpoints(); -// Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } -app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); -app.UseHttpsRedirection(); +app.UseStatusCodePagesWithReExecute( + "/not-found", + createScopeForStatusCodePages: true); + +app.UseHttpsRedirection(); app.UseAntiforgery(); app.MapStaticAssets(); diff --git a/PointOfSale/Pos.Web/wwwroot/app.css b/PointOfSale/Pos.Web/wwwroot/app.css index c36f4fc..4a283b1 100644 --- a/PointOfSale/Pos.Web/wwwroot/app.css +++ b/PointOfSale/Pos.Web/wwwroot/app.css @@ -1,4 +1,4 @@ -@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; +@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; body { --body-font: "Segoe UI Variable", "Segoe UI", sans-serif; @@ -87,3 +87,51 @@ body { code { color: #c02d76; } + + +:root { + --pos-blue: #155eef; + --pos-blue-soft: #eaf1ff; + --pos-text: #171a1f; + --pos-muted: #707781; + --pos-border: #d9dde3; + --pos-border-soft: #e9ebef; + --pos-bg: #f7f8fa; +} + +html, +body { + min-height: 100%; +} + +body { + margin: 0; + background: var(--pos-bg); + color: var(--pos-text); + font-family: "Segoe UI Variable", "Segoe UI", Arial, sans-serif; +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +.main { + min-height: 100dvh; + background: var(--pos-bg); +} + +.content { + width: 100%; + padding: 0; +} + +code { + color: #c02d76; +} +