From 97688542fe789adcda8e8ba4535445fc0ab3678b Mon Sep 17 00:00:00 2001 From: Alex Pedersen <64533006+Allemand1001DK@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:41:47 +0200 Subject: [PATCH] moved logic out of SalesWindows.xaml.cs to SaleTransactionServce --- PointOfSale/Pos.Ui/Database/PosDbContext.cs | 10 + PointOfSale/Pos.Ui/Pos/SalesWindow.xaml.cs | 189 ++++++++++-------- .../Pos/Service/SalesTransactionService.cs | 171 ++++++++++++++++ 3 files changed, 289 insertions(+), 81 deletions(-) create mode 100644 PointOfSale/Pos.Ui/Pos/Service/SalesTransactionService.cs diff --git a/PointOfSale/Pos.Ui/Database/PosDbContext.cs b/PointOfSale/Pos.Ui/Database/PosDbContext.cs index 9f316c4..ecb64a0 100644 --- a/PointOfSale/Pos.Ui/Database/PosDbContext.cs +++ b/PointOfSale/Pos.Ui/Database/PosDbContext.cs @@ -17,6 +17,16 @@ namespace Database public DbSet SalesLines { get; set; } public DbSet Payment { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .Property(saleLine => saleLine.Pieces) + .HasConversion() + .HasColumnType("smallint"); + + base.OnModelCreating(modelBuilder); + } + protected override void OnConfiguring( DbContextOptionsBuilder optionsBuilder) { diff --git a/PointOfSale/Pos.Ui/Pos/SalesWindow.xaml.cs b/PointOfSale/Pos.Ui/Pos/SalesWindow.xaml.cs index 62a5a4c..22d8c81 100644 --- a/PointOfSale/Pos.Ui/Pos/SalesWindow.xaml.cs +++ b/PointOfSale/Pos.Ui/Pos/SalesWindow.xaml.cs @@ -29,7 +29,8 @@ namespace Pos private ObservableCollection _productGroups; private ObservableCollection _saleGridModels; private ObservableCollection _paymentGridModels; - private List _priceLineModels; + private readonly SalesTransactionService _salesTransactionService = + new SalesTransactionService(); private ProductEntity _selectedProduct; private ProductGroupEntity _selectedProductGroup; @@ -37,14 +38,10 @@ namespace Pos private int _employeeId = -1; private int _productGroupId = -1; private int _productId = -1; - private int _numberOfProducts = 1; private int _selectedSaleGridIndex = -1; private int _maxCharOfName = 0; - private decimal _price = 0; - private decimal _totalPrice = 0; - private Button _prevEmpButton = new Button(); private Button _prevProductGroupButton = new Button(); private Button _prevProduct = new Button(); @@ -61,7 +58,6 @@ namespace Pos _employees = CacheService.GetEmployee() as ObservableCollection; _productGroups = CacheService.GetProductGroupsIncludeProducts() as ObservableCollection; _saleGridModels = new ObservableCollection(); - _priceLineModels = new List(); _paymentGridModels = new ObservableCollection(); SaleGrid.ItemsSource = _saleGridModels; PaymentGrid.ItemsSource = _paymentGridModels; @@ -150,11 +146,25 @@ namespace Pos return; decimal price = Convert.ToDecimal(Price.Text); - int number = Convert.ToInt32(NumberOfProducts.Text); + int numberOfProducts = Convert.ToInt32(NumberOfProducts.Text); + + try + { + BuildPrice(numberOfProducts, price); + } + catch (ArgumentException exception) + { + MessageBox.Show( + exception.Message, + "Ugyldig salgslinje", + MessageBoxButton.OK, + MessageBoxImage.Error); + return; + } + NumberOfProducts.Text = "1"; Price.Text = string.Empty; TotalPrice.Content = "0"; - BuildPrice(false); _productId = -1; _productGroupId = -1; ManipulateBuySection(); @@ -162,16 +172,36 @@ namespace Pos private void Payment_Click(object sender, RoutedEventArgs e) { - //bool isValid = ValidateAmountInput(); - //if (!isValid) - // return; - decimal amount = -1; + decimal? amount = null; if (!string.IsNullOrEmpty(Amount.Text)) + { + bool isValid = ValidateAmountInput(); + if (!isValid) + return; + amount = Convert.ToDecimal(Amount.Text); + } Amount.Text = string.Empty; Button button = sender as Button; - BuildAmuntDataGrid(amount,(string)button.Tag); + string paymentMethodName = + ((string)button.Tag).Replace("PaymentMethod.", string.Empty); + + if (!Enum.TryParse(paymentMethodName, out PaymentMethod paymentMethod)) + return; + + try + { + BuildAmuntDataGrid(amount, paymentMethod); + } + catch (ArgumentException exception) + { + MessageBox.Show( + exception.Message, + "Ugyldig betaling", + MessageBoxButton.OK, + MessageBoxImage.Error); + } } private void NumberOfProducts_LostFocus(object sender, RoutedEventArgs e) @@ -249,61 +279,69 @@ namespace Pos } } - private void BuildPrice(bool returnProduct) + private void BuildPrice(int numberOfProducts, decimal price) { - PriceLineModel pModel = new PriceLineModel(); - pModel.NumberProducts = _numberOfProducts; - pModel.Price = _price; + PriceLineModel priceLineModel; if (_selectedProduct != null) { - pModel.Id = _selectedProduct.Id; - pModel.Name = _selectedProduct.Name; - pModel.IsProductGroup = false; + priceLineModel = _salesTransactionService.AddSaleLine( + _selectedProduct.Id, + _selectedProduct.Name, + numberOfProducts, + price, + false); } else { - pModel.Id = _selectedProductGroup.Id; - pModel.Name = _selectedProductGroup.Name; - pModel.IsProductGroup = true; + priceLineModel = _salesTransactionService.AddSaleLine( + _selectedProductGroup.Id, + _selectedProductGroup.Name, + numberOfProducts, + price, + true); } - BuildPriceDataGrid(pModel); + BuildPriceDataGrid(priceLineModel); //BuildEmployee(); BuildProductGroup(); } private void CalculateSubPrice() { - _price = 0; - bool isValid = decimal.TryParse(Price.Text, out _price); + bool isValid = decimal.TryParse(Price.Text, out decimal price); if (!isValid) return; - isValid = Int32.TryParse(NumberOfProducts.Text, out _numberOfProducts); + isValid = Int32.TryParse( + NumberOfProducts.Text, + out int numberOfProducts); if (!isValid) return; - TotalPrice.Content = _price * _numberOfProducts; + try + { + TotalPrice.Content = + _salesTransactionService.CalculateLineTotal( + price, + numberOfProducts); + } + catch (ArgumentOutOfRangeException) + { + TotalPrice.Content = "0"; + } } private void CalculateTotalLine() { - _totalPrice = 0; - foreach (PriceLineModel model in _priceLineModels) - { - _totalPrice += model.Price * model.NumberProducts; - } - - Total.Content = $"At betale: Kr. {_totalPrice}"; + Total.Content = + $"At betale: Kr. {_salesTransactionService.TotalPrice}"; } private void BuildPriceDataGrid(PriceLineModel priceLineModel) { _selectedProduct = null; _selectedProductGroup = null; - _numberOfProducts = 1; - _priceLineModels.Add(priceLineModel); CalculateTotalLine(); SaleGridModel model = new SaleGridModel(); @@ -399,38 +437,14 @@ namespace Pos TotalPrice.IsEnabled = enabled; } - private void BuildAmuntDataGrid(decimal amount, string paymentMethod) + private void BuildAmuntDataGrid( + decimal? amount, + PaymentMethod paymentMethod) { - if (amount == -1) - { - amount = 0; - foreach (PriceLineModel lineModel in _priceLineModels) - { - amount += (lineModel.Price * lineModel.NumberProducts); - } - } + AmountGridModel amountGridModel = + _salesTransactionService.AddPayment(paymentMethod, amount); - AmountGridModel amountGridModel = new AmountGridModel(); - amountGridModel.Amount = amount; - amountGridModel.AmountText = $"Kr. {amount}"; - switch (paymentMethod) - { - case "PaymentMethod.Card": - amountGridModel.PaymentMethodText = "Kort"; - break; - - case "PaymentMethod.Cash": - amountGridModel.PaymentMethodText = "Kontant"; - break; - - case "PaymentMethod.GiftCard": - amountGridModel.PaymentMethodText = "Gavekort"; - break; - - case "PaymentMethod.MobilePay": - amountGridModel.PaymentMethodText = "MobilePay"; - break; - } + amountGridModel.AmountText = $"Kr. {amountGridModel.Amount}"; _paymentGridModels.Add(amountGridModel); AmountPayed(); @@ -438,28 +452,25 @@ namespace Pos private void AmountPayed() { - decimal amountPayed = 0; - foreach (AmountGridModel paymentGridModel in _paymentGridModels) - { - amountPayed += paymentGridModel.Amount; - } - - if (amountPayed == _totalPrice) + if (_salesTransactionService.AmountPaid == + _salesTransactionService.TotalPrice) { btnFinish_Click(null,null); return; } - if (amountPayed > _totalPrice) + if (_salesTransactionService.Change > 0) { - lblRecived.Content = $"Byttepenge: Kr. {amountPayed - _totalPrice}"; + lblRecived.Content = + $"Byttepenge: Kr. {_salesTransactionService.Change}"; Style style = this.FindResource("BoldLabelWarningNoMargin") as Style; lblRecived.Style = style; btnFinish.IsEnabled = true; } else { - lblRecived.Content = $"At betale: Kr. {_totalPrice - amountPayed}"; + lblRecived.Content = + $"At betale: Kr. {_salesTransactionService.RemainingAmount}"; Style style = this.FindResource("BoldLabelNoMargin") as Style; lblRecived.Style = style; } @@ -473,21 +484,37 @@ namespace Pos if (_selectedSaleGridIndex == -1) return; int index = _selectedSaleGridIndex; - //_saleGridModels.RemoveAt(_selectedSaleGridIndex); - _priceLineModels.RemoveAt(index); + _salesTransactionService.RemoveSaleLineAt(index); + _saleGridModels.RemoveAt(index); _selectedSaleGridIndex = -1; CalculateTotalLine(); + + if (_salesTransactionService.AmountPaid > 0) + AmountPayed(); } } private void btnFinish_Click(object sender, RoutedEventArgs e) { + try + { + _salesTransactionService.EnsureCanComplete(); + } + catch (InvalidOperationException exception) + { + MessageBox.Show( + exception.Message, + "Salget kan ikke afsluttes", + MessageBoxButton.OK, + MessageBoxImage.Error); + return; + } + SaleService saleService = new SaleService(); saleService.Save(_saleGridModels.ToList(),_paymentGridModels.ToList(),_employeeId); _paymentGridModels.Clear(); - _priceLineModels.Clear(); + _salesTransactionService.Reset(); UpdateWindowTitle(); - _totalPrice = 0; _saleGridModels.Clear(); lblRecived.Content = string.Empty; _selectedSaleGridIndex = -1; diff --git a/PointOfSale/Pos.Ui/Pos/Service/SalesTransactionService.cs b/PointOfSale/Pos.Ui/Pos/Service/SalesTransactionService.cs new file mode 100644 index 0000000..3815746 --- /dev/null +++ b/PointOfSale/Pos.Ui/Pos/Service/SalesTransactionService.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Pos.Models; + +namespace Pos.Service +{ + /// + /// Holder styr på den igangværende handel og dens beregninger. + /// Servicen har ingen afhængighed til WPF eller web. + /// + public class SalesTransactionService + { + private readonly List _saleLines = new List(); + private readonly List _payments = new List(); + + public IReadOnlyList SaleLines => _saleLines; + public IReadOnlyList Payments => _payments; + + public decimal TotalPrice => + _saleLines.Sum(line => + CalculateLineTotal(line.Price, line.NumberProducts)); + + public decimal AmountPaid => + _payments.Sum(payment => payment.Amount); + + public decimal RemainingAmount => + Math.Max(TotalPrice - AmountPaid, 0); + + public decimal Change => + Math.Max(AmountPaid - TotalPrice, 0); + + public bool IsFullyPaid => + TotalPrice > 0 && AmountPaid >= TotalPrice; + + /// + /// Beregner totalen for en enkelt salgslinje. + /// Parsing og visning af resultatet er fortsat UI-ansvar. + /// + public decimal CalculateLineTotal(decimal price, int numberOfProducts) + { + ValidatePriceAndQuantity(price, numberOfProducts); + return price * numberOfProducts; + } + + public PriceLineModel AddSaleLine( + int id, + string name, + int numberOfProducts, + decimal price, + bool isProductGroup) + { + if (id <= 0) + throw new ArgumentOutOfRangeException( + nameof(id), + "Id skal være større end 0."); + + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException( + "Navn skal være udfyldt.", + nameof(name)); + + ValidatePriceAndQuantity(price, numberOfProducts); + + PriceLineModel saleLine = new PriceLineModel + { + Id = id, + Name = name, + NumberProducts = numberOfProducts, + Price = price, + IsProductGroup = isProductGroup + }; + + _saleLines.Add(saleLine); + return saleLine; + } + + public void RemoveSaleLineAt(int index) + { + if (index < 0 || index >= _saleLines.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + _saleLines.RemoveAt(index); + } + + /// + /// Registrerer en betaling. Hvis amount er null, betales restbeløbet. + /// + public AmountGridModel AddPayment( + PaymentMethod paymentMethod, + decimal? amount = null) + { + if (_saleLines.Count == 0) + throw new InvalidOperationException( + "Der kan ikke registreres betaling på et tomt salg."); + + decimal paymentAmount = amount ?? RemainingAmount; + + if (paymentAmount <= 0) + throw new ArgumentOutOfRangeException( + nameof(amount), + "Betalingsbeløbet skal være større end 0."); + + AmountGridModel payment = new AmountGridModel + { + Amount = paymentAmount, + PaymentMethodText = GetPaymentMethodText(paymentMethod) + }; + + _payments.Add(payment); + return payment; + } + + public void RemovePaymentAt(int index) + { + if (index < 0 || index >= _payments.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + _payments.RemoveAt(index); + } + + public void EnsureCanComplete() + { + if (_saleLines.Count == 0) + throw new InvalidOperationException( + "Et tomt salg kan ikke afsluttes."); + + if (!IsFullyPaid) + throw new InvalidOperationException( + "Salget er ikke fuldt betalt."); + } + + public void Reset() + { + _saleLines.Clear(); + _payments.Clear(); + } + + private static void ValidatePriceAndQuantity( + decimal price, + int numberOfProducts) + { + if (price < 0) + throw new ArgumentOutOfRangeException( + nameof(price), + "Prisen må ikke være negativ."); + + if (numberOfProducts <= 0) + throw new ArgumentOutOfRangeException( + nameof(numberOfProducts), + "Antal skal være større end 0."); + } + + private static string GetPaymentMethodText(PaymentMethod paymentMethod) + { + switch (paymentMethod) + { + case PaymentMethod.Card: + return "Kort"; + case PaymentMethod.Cash: + return "Kontant"; + case PaymentMethod.GiftCard: + return "Gavekort"; + case PaymentMethod.MobilePay: + return "MobilePay"; + default: + throw new ArgumentOutOfRangeException(nameof(paymentMethod)); + } + } + } +}