26 Commits

Author SHA1 Message Date
770e170d0e Addet a backbutton to home page and fix the pr error 2026-08-27 10:18:48 +02:00
8de6ff9338 Addet VateRate to produktPage 2026-08-27 10:13:08 +02:00
Alex Pedersen
048f45a4a1 Addet docker support 2026-08-06 17:05:25 +02:00
Alex Pedersen
01d81cf14f Merge branch 'feature/blazor' of https://git.maximuss.dk/maximuss/point_of_sale_v2 into feature/blazor 2026-08-06 13:43:16 +02:00
Alex Pedersen
49a9f336b1 Addet ToDaySales page 2026-08-06 13:43:04 +02:00
8953e917e1 Merge pull request 'feature/blazor' (#3) from feature/blazor into development
Reviewed-on: #3
2026-08-03 11:50:52 +00:00
6e14130a9c Finsih Employyepage 2026-08-03 11:01:37 +02:00
f01f05e126 chance database connection string 2026-08-03 10:02:58 +02:00
Alex Pedersen
c1695b4967 Addet Employye get logic 2026-08-03 09:55:41 +02:00
Alex Pedersen
205d27c6c7 Addet employee frontend and move header to a componet 2026-07-31 14:07:43 +02:00
Alex Pedersen
03594bba3f Made home page and startet the layout for settings home page 2026-07-30 15:21:59 +02:00
Alex Pedersen
2e537337f5 Addet aspire support pos.web and also addet the pictures for layout 2026-07-30 13:22:33 +02:00
Alex Pedersen
9497afe262 Rework so employee dont use repos but servies 2026-07-30 10:23:27 +02:00
Alex Pedersen
6df185d519 Move logic out the the class libs 2026-07-30 10:11:54 +02:00
Alex Pedersen
97688542fe moved logic out of SalesWindows.xaml.cs to SaleTransactionServce 2026-07-30 09:41:47 +02:00
Alex Pedersen
236eb6792c addet pos.models and pos.service 2026-07-30 09:22:48 +02:00
Alex Pedersen
d02414edbb modified sln to run the brach 2026-07-30 08:43:30 +02:00
be0350f458 Fluent Blazor web oprettet 2026-07-28 19:53:59 +02:00
3414400efe Merge pull request 'migrate-mysql-to-mssql' (#2) from migrate-mysql-to-mssql into development
Reviewed-on: #2
2026-07-28 17:35:16 +00:00
6d55a6e840 Addet integration test 2026-07-28 12:02:03 +02:00
a30bd769fb Change sql driver 2026-07-27 13:38:56 +02:00
9e077f6993 New test comment 2026-07-22 14:30:29 +02:00
83d85daa29 Add a test comment 2026-07-22 14:29:24 +02:00
5de0f416ba Addet Aspire 2026-07-22 14:24:55 +02:00
26c6c651b1 Downgrade EntityFrameworkCore to version 9.0.18 to make it compatible with Pomelo.EntityFrameworkCore.MySql. 2026-07-22 13:07:10 +02:00
547e574e6a updated projects from .net 6 to .net 10 2026-07-22 13:00:21 +02:00
130 changed files with 3533 additions and 337 deletions

35
PointOfSale/.dockerignore Normal file
View File

@@ -0,0 +1,35 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**
database-backup/
.env
database-data/

View File

@@ -360,3 +360,10 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# Local Docker database data and secrets
.env
database-backup/
database-data/

View File

@@ -20,4 +20,8 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="..\Pos.ServiceDefaults\Pos.ServiceDefaults.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,6 +4,8 @@ using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
@@ -13,6 +15,8 @@ builder.Services.AddScoped<EpsonPrintService>();
var app = builder.Build();
app.MapDefaultEndpoints();
// Swagger (altid aktiv vi er på en Pi, ikke i produktion)
app.UseSwagger();
app.UseSwaggerUI();

View File

@@ -19,7 +19,7 @@
},
"EpsonPrinter": {
"commandName": "Project",
"dotnetRunMessages": "true",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",

View File

@@ -10,3 +10,9 @@ Buket..............1 á 50,50 DKK : 50,50 DKK
Total: 50,50 DKK
Moms udgør 25% : 12,63 DKK
Tak for handlen
Dato: 30-07-2026
-----------------------------------
string : .00
-----------------------------------
Total salg: .00
Antal kunder: 0

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Pos.Ui\Database\Database.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
using Database;
using Database.Repository;
using Microsoft.EntityFrameworkCore;
using Xunit.Abstractions;
namespace IntegrationTests;
public class PosDbContextTests
{
private readonly ITestOutputHelper _output;
public PosDbContextTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void EmployeeRepository_ShouldReturnEmployees()
{
using var repository = new EmployeeRepository();
var employees = repository.GetAll();
_output.WriteLine(
$"Antal medarbejdere hentet: {employees.Count}");
Assert.NotEmpty(employees);
}
}

View File

@@ -0,0 +1,3 @@
{
"SqlServer": "Server=(localdb)\\MSSQLLocalDB;Database=pointofsale;Trusted_Connection=True;TrustServerCertificate=True;"
}

View File

@@ -0,0 +1,11 @@
var builder = DistributedApplication.CreateBuilder(args);
var epsonPrinter =
builder.AddProject<Projects.EpsonPrinter>("epsonprinter");
builder
.AddProject<Projects.Pos_Web>("pos-web")
.WithReference(epsonPrinter)
.WaitFor(epsonPrinter);
builder.Build().Run();

View File

@@ -0,0 +1,16 @@
<Project Sdk="Aspire.AppHost.Sdk/13.2.4">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>bf471f42-700e-491e-9def-519245baaaa1</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\EpsonPrinterLinux\EpsonPrinter.csproj" />
<ProjectReference Include="..\Pos.Web\Pos.Web.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17208;http://localhost:15295",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21197",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23254",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22138"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15295",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19286",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18003",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20235"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}

View File

@@ -8,8 +8,8 @@ namespace Pos.Models
{
public class AmountGridModel
{
public string PaymentMethodText { get; set; }
public string PaymentMethodText { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string AmountText { get; set; }
public string AmountText { get; set; } = string.Empty;
}
}

View File

@@ -6,4 +6,4 @@ public enum PaymentMethod
Card,
GiftCard,
MobilePay
}
}

View File

@@ -8,11 +8,11 @@ namespace Pos.Json
{
public class BodyModel
{
public ProductModel[] products { get; set; }
public ProductModel[] products { get; set; } = Array.Empty<ProductModel>();
public float totalPrice { get; set; }
public float totalVat { get; set; }
public int receiptNumber { get; set; }
public string receiptTime { get; set; }
public string staff { get; set; }
public string receiptTime { get; set; } = string.Empty;
public string staff { get; set; } = string.Empty;
}
}

View File

@@ -8,8 +8,8 @@ namespace Pos.Json
{
public class FooterModel
{
public string value { get; set; }
public PrintStylesModel printStyles { get; set; }
public string value { get; set; } = string.Empty;
public PrintStylesModel printStyles { get; set; } = new();
public int textAlignment { get; set; }
public int feedingLines { get; set; }
}

View File

@@ -8,8 +8,8 @@ namespace Pos.Json
{
public class HeaderModel
{
public string value { get; set; }
public PrintStylesModel printStyles { get; set; }
public string value { get; set; } = string.Empty;
public PrintStylesModel printStyles { get; set; } = new();
public int textAlignment { get; set; }
public int feedingLines { get; set; }
}

View File

@@ -8,9 +8,9 @@ namespace Pos.Json
{
public class PosReceipt
{
public string logoBase64 { get; set; }
public HeaderModel[] header { get; set; }
public string logoBase64 { get; set; } = string.Empty;
public HeaderModel[] header { get; set; } = Array.Empty<HeaderModel>();
public BodyModel bodyModel { get; set; } = new();
public FooterModel[] footer { get; set; }
public FooterModel[] footer { get; set; } = Array.Empty<FooterModel>();
}
}

View File

@@ -8,8 +8,8 @@ namespace Pos.Json
{
public class ProductModel
{
public string product { get; set; }
public string noOfProduct { get; set; }
public string product { get; set; } = string.Empty;
public string noOfProduct { get; set; } = string.Empty;
public float price { get; set; }
public float totalPrice { get; set; }
}

View File

@@ -13,7 +13,7 @@ namespace Pos.Json
public class SaleOfDayDetail
{
public string Category { get; set; }
public string Category { get; set; } = string.Empty;
public decimal TotalSale { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -8,7 +8,7 @@ namespace Pos.Models
{
public class PriceLineModel
{
public string Name { get; set; }
public string Name { get; set; } = string.Empty;
public int NumberProducts { get; set; }
public decimal Price { get; set; }
public int Id { get; set; }

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Pos.Models
{
public class ReceiptDetailModel
{
public int SaleId { get; set; }
public List<ReceiptLineModel> Lines { get; set; } = new();
public decimal Total { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace Pos.Models
{
public class ReceiptLineModel
{
public string Product { get; set; } = string.Empty;
public int Pieces { get; set; }
public decimal Price { get; set; }
public decimal Total { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace Pos.Models
{
public class ReceiptSummaryModel
{
public int SaleId { get; set; }
public DateTime SaleTime { get; set; }
public decimal Total { get; set; }
}
}

View File

@@ -8,10 +8,10 @@ namespace Pos.Models
{
public class SaleGridModel
{
public string Navn { get; set; }
public string Stk { get; set; }
public string Pris { get; set; }
public string Total { get; set; }
public SaleGridInternalModel SaleGridInternalModel { get; set; }
public string Navn { get; set; } = string.Empty;
public string Stk { get; set; } = string.Empty;
public string Pris { get; set; } = string.Empty;
public string Total { get; set; } = string.Empty;
public SaleGridInternalModel SaleGridInternalModel { get; set; } = new();
}
}

View File

@@ -8,8 +8,8 @@ namespace Pos.Models
{
public class TotalSaleCategory
{
public string Category { get; set; }
public string Category { get; set; } = string.Empty;
public decimal Sale { get; set; }
public string SaleString { get; set; }
public string SaleString { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Pos.Models;
public class TotalSaleDetail
{
public decimal TotalSale { get; set; }
public int TotalCustomer { get; set; }
public List<TotalSaleCategory> TotalSaleCategories { get; set; } = new();
public List<TotalSalePayment> PaymentBreakdown { get; set; } = new();
}

View File

@@ -0,0 +1,7 @@
namespace Pos.Models;
public class TotalSalePayment
{
public string PaymentMethod { get; set; } = string.Empty;
public decimal Amount { get; set; }
}

View File

@@ -10,8 +10,8 @@ namespace Pos.Service
private static bool _invalidEmployee = false;
private static bool _invalidProductGroup = false;
private static List<EmployeeEntity> _employees;
private static List<ProductGroupEntity> _productGroups;
private static List<EmployeeEntity> _employees = new();
private static List<ProductGroupEntity> _productGroups = new();
public static void Invalidate()

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using Database.Models;
using Database.Repository;
namespace Pos.Service
{
public class EmployeeService
{
private readonly EmployeeRepository _employeeRepository =
new EmployeeRepository();
public List<EmployeeEntity> GetEmployees()
{
return _employeeRepository.GetAll();
}
public void AddEmployee(string name)
{
_employeeRepository.Add(ValidateName(name));
CacheService.Invalidate();
}
public void RenameEmployee(int employeeId, string name)
{
ValidateId(employeeId);
_employeeRepository.Edit(employeeId, ValidateName(name));
CacheService.Invalidate();
}
public void ArchiveEmployee(int employeeId)
{
ValidateId(employeeId);
_employeeRepository.Delete(employeeId);
CacheService.Invalidate();
}
private static string ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(
"Medarbejderens navn skal være udfyldt.",
nameof(name));
return name.Trim();
}
private static void ValidateId(int employeeId)
{
if (employeeId <= 0)
throw new ArgumentOutOfRangeException(nameof(employeeId));
}
public async Task<List<EmployeeEntity>> GetEmployeesAsync()
{
return await _employeeRepository.GetAllAsync();
}
public async Task<EmployeeEntity> AddEmployeeAsync(string name)
{
EmployeeEntity employee =
await _employeeRepository.AddAsync(ValidateName(name));
CacheService.Invalidate();
return employee;
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using Database.Models;
namespace Pos.Service;
public interface IProductService
{
List<ProductEntity> 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<int> orderedProductIds);
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageReference Include="RestSharp" Version="108.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Pos.Models\Pos.Models.csproj" />
<ProjectReference Include="..\Pos.Ui\Database\Database.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Database.Models;
using Database.Repository;
namespace Pos.Service
{
public class ProductGroupService
{
private readonly ProductGroupRepository _productGroupRepository =
new ProductGroupRepository();
public List<ProductGroupEntity> GetProductGroups()
{
return _productGroupRepository.GetAll();
}
public bool HasProductGroups()
{
return _productGroupRepository.Any();
}
public void AddProductGroup(string name, decimal vatRate)
{
_productGroupRepository.Add(
ValidateName(name),
ValidateVatRate(vatRate));
CacheService.Invalidate();
}
public void AddProductGroup(string name)
{
AddProductGroup(name, 25m);
}
public void RenameProductGroup(int productGroupId, string name)
{
ProductGroupEntity productGroup = _productGroupRepository.Get(productGroupId);
UpdateProductGroup(productGroupId, name, productGroup.VatRate);
}
public void UpdateProductGroup(
int productGroupId,
string name,
decimal vatRate)
{
ValidateId(productGroupId);
_productGroupRepository.Edit(
ValidateName(name),
ValidateVatRate(vatRate),
productGroupId);
CacheService.Invalidate();
}
public void ArchiveProductGroup(int productGroupId)
{
ValidateId(productGroupId);
_productGroupRepository.Archive(productGroupId);
CacheService.Invalidate();
}
public void UpdateOrder(IReadOnlyList<int> orderedProductGroupIds)
{
ValidateOrder(orderedProductGroupIds);
_productGroupRepository.SetOrder(orderedProductGroupIds);
CacheService.Invalidate();
}
private static string ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(
"Produktgruppen skal have et navn.",
nameof(name));
return name.Trim();
}
private static void ValidateId(int productGroupId)
{
if (productGroupId <= 0)
throw new ArgumentOutOfRangeException(nameof(productGroupId));
}
private static decimal ValidateVatRate(decimal vatRate)
{
if (vatRate < 0 || vatRate > 100)
throw new ArgumentOutOfRangeException(
nameof(vatRate),
"Momsen skal være mellem 0 og 100 procent.");
return decimal.Round(vatRate, 2);
}
private static void ValidateOrder(IReadOnlyList<int> orderedIds)
{
if (orderedIds == null)
throw new ArgumentNullException(nameof(orderedIds));
if (orderedIds.Any(id => id <= 0))
throw new ArgumentException(
"Rækkefølgen indeholder et ugyldigt id.",
nameof(orderedIds));
if (orderedIds.Distinct().Count() != orderedIds.Count)
throw new ArgumentException(
"Rækkefølgen indeholder dubletter.",
nameof(orderedIds));
}
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Database.Models;
using Database.Repository;
namespace Pos.Service
{
public class ProductService
{
private readonly ProductRepository _productRepository =
new ProductRepository();
public List<ProductEntity> GetProducts(int productGroupId)
{
ValidateId(productGroupId, nameof(productGroupId));
return _productRepository.GetByProductGroup(productGroupId);
}
public ProductEntity GetProduct(int productId)
{
ValidateId(productId, nameof(productId));
return _productRepository.GetById(productId);
}
public void AddProduct(string name, int productGroupId)
{
ValidateId(productGroupId, nameof(productGroupId));
_productRepository.Add(ValidateName(name), productGroupId);
CacheService.Invalidate();
}
public void UpdateProduct(
int productId,
int productGroupId,
string name)
{
ValidateId(productId, nameof(productId));
ValidateId(productGroupId, nameof(productGroupId));
_productRepository.Update(
productId,
productGroupId,
ValidateName(name));
CacheService.Invalidate();
}
public void ArchiveProduct(int productId)
{
ValidateId(productId, nameof(productId));
_productRepository.Archive(productId);
CacheService.Invalidate();
}
public void UpdateOrder(
int productGroupId,
IReadOnlyList<int> orderedProductIds)
{
ValidateId(productGroupId, nameof(productGroupId));
if (orderedProductIds == null)
throw new ArgumentNullException(nameof(orderedProductIds));
if (orderedProductIds.Any(id => id <= 0))
throw new ArgumentException(
"Rækkefølgen indeholder et ugyldigt id.",
nameof(orderedProductIds));
if (orderedProductIds.Distinct().Count() != orderedProductIds.Count)
throw new ArgumentException(
"Rækkefølgen indeholder dubletter.",
nameof(orderedProductIds));
_productRepository.SetOrder(
productGroupId,
orderedProductIds);
CacheService.Invalidate();
}
private static string ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(
"Produktet skal have et navn.",
nameof(name));
return name.Trim();
}
private static void ValidateId(int id, string parameterName)
{
if (id <= 0)
throw new ArgumentOutOfRangeException(parameterName);
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Database.Models;
using Database.Repository;
using Pos.Models;
namespace Pos.Service
{
public class SaleHistoryService
{
private readonly SaleRepository _saleRepository =
new SaleRepository();
public List<ReceiptSummaryModel> Search(
DateTime startDate,
DateTime endDate)
{
if (endDate.Date < startDate.Date)
throw new ArgumentException(
"Slutdatoen må ikke ligge før startdatoen.");
List<SaleEntity> sales =
_saleRepository.GetByDateRange(startDate, endDate);
List<ReceiptSummaryModel> receipts =
new List<ReceiptSummaryModel>();
foreach (SaleEntity sale in sales)
{
decimal total = _saleRepository
.GetSaleLineBySaleId(sale.Id)
.Sum(line => line.Total);
receipts.Add(new ReceiptSummaryModel
{
SaleId = sale.Id,
SaleTime = sale.Time,
Total = total
});
}
return receipts;
}
public ReceiptDetailModel GetReceipt(int saleId)
{
if (saleId <= 0)
throw new ArgumentOutOfRangeException(nameof(saleId));
List<SaleLineEntity> saleLines =
_saleRepository.GetSaleLineBySaleId(saleId);
ReceiptDetailModel receipt = new ReceiptDetailModel
{
SaleId = saleId
};
foreach (SaleLineEntity saleLine in saleLines)
{
receipt.Lines.Add(new ReceiptLineModel
{
Product = saleLine.Product,
Pieces = saleLine.Pieces,
Price = saleLine.Price,
Total = saleLine.Total
});
}
receipt.Total = receipt.Lines.Sum(line => line.Total);
return receipt;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
@@ -84,7 +84,9 @@ namespace Pos.Service
LoadConfig loadConfig = new LoadConfig();
IConfiguration config = loadConfig.ByEnvironment();
string url = config["API:URL"];
string url = config["API:URL"]
?? throw new InvalidOperationException(
"API:URL mangler i konfigurationen.");
RestClient restClient = new RestClient($"{url}/api/PosPrinter/SaleOfDay");
RestRequest request = new RestRequest();
@@ -101,6 +103,34 @@ namespace Pos.Service
foreach (SaleEntity sale in sales)
{
List<SaleLineEntity> saleLineBySaleId = _saleRepository.GetSaleLineBySaleId(sale.Id);
List<PaymentEntity> 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)
{
@@ -159,7 +189,9 @@ namespace Pos.Service
LoadConfig loadConfig = new LoadConfig();
IConfiguration config = loadConfig.ByEnvironment();
string apiUrl = config["API:URL"];
string apiUrl = config["API:URL"]
?? throw new InvalidOperationException(
"API:URL mangler i konfigurationen.");
string url = $"{apiUrl}/api/PosPrinter/Receipt";
RestClient restClient = new RestClient(url);
@@ -299,3 +331,4 @@ namespace Pos.Service
}
}
}

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Pos.Models;
namespace Pos.Service
{
/// <summary>
/// Holder styr på den igangværende handel og dens beregninger.
/// Servicen har ingen afhængighed til WPF eller web.
/// </summary>
public class SalesTransactionService
{
private readonly List<PriceLineModel> _saleLines = new List<PriceLineModel>();
private readonly List<AmountGridModel> _payments = new List<AmountGridModel>();
public IReadOnlyList<PriceLineModel> SaleLines => _saleLines;
public IReadOnlyList<AmountGridModel> 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;
/// <summary>
/// Beregner totalen for en enkelt salgslinje.
/// Parsing og visning af resultatet er fortsat UI-ansvar.
/// </summary>
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);
}
/// <summary>
/// Registrerer en betaling. Hvis amount er null, betales restbeløbet.
/// </summary>
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));
}
}
}
}

View File

@@ -0,0 +1,127 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ServiceDiscovery;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
public static class Extensions
{
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();
// Turn on service discovery by default
http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation(tracing =>
// Exclude health check requests from tracing
tracing.Filter = context =>
!context.Request.Path.StartsWithSegments(HealthEndpointPath)
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
)
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}
return builder;
}
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks(HealthEndpointPath);
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.2.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.2.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
</ItemGroup>
</Project>

View File

@@ -1,16 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.18" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.18" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.18">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.10" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -58,3 +58,7 @@ namespace Pos
}
}
}

View File

@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Database.Migrations
{
[DbContext(typeof(PosDbContext))]
[Migration("20260827000000_AddProductGroupVatRate")]
public class AddProductGroupVatRate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "VatRate",
table: "productgroup",
type: "decimal(5,2)",
nullable: false,
defaultValue: 25m);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "VatRate",
table: "productgroup");
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Database.Models
[Key]
public int Id { get; set; }
public int SaleId { get; set; }
public decimal Amount { get; set;}
public decimal Amount { get; set;} // Test2
public string Type { get; set; }
}

View File

@@ -16,6 +16,7 @@ namespace Database.Models
public string Name { get; set; }
public bool IsArchived { get; set; }
public int Index { get; set; }
public decimal VatRate { get; set; }
public ICollection<ProductEntity> Products { get; set; }
}

View File

@@ -1,4 +1,4 @@
using Database.Models;
using Database.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
@@ -17,14 +17,24 @@ namespace Database
public DbSet<SaleLineEntity> SalesLines { get; set; }
public DbSet<PaymentEntity> Payment { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SaleLineEntity>()
.Property(saleLine => saleLine.Pieces)
.HasConversion<short>()
.HasColumnType("smallint");
base.OnModelCreating(modelBuilder);
}
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
LoadConfig l = new LoadConfig();
IConfiguration config = l.ByEnvironment();
string connectionString = config["MariaSqlServer"].ToString();
string connectionString = System.Environment.GetEnvironmentVariable("SqlServer") ?? config["SqlServer"].ToString();
optionsBuilder
.UseMySql(connectionString,ServerVersion.AutoDetect(connectionString))
.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(LoggerFactory.Create(b => b
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
@@ -33,3 +43,6 @@ namespace Database
}
}
}

View File

@@ -1,10 +1,11 @@
using System;
using Database.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Database.Models;
using Microsoft.Extensions.Configuration;
namespace Database.Repository
{
@@ -58,5 +59,32 @@ namespace Database.Repository
public void Dispose()
{
}
public async Task<List<EmployeeEntity>> GetAllAsync()
{
await using PosDbContext context = new PosDbContext();
return await context.Employee
.AsNoTracking()
.Where(employee => !employee.IsArchived)
.OrderBy(employee => employee.Name)
.ToListAsync();
}
public async Task<EmployeeEntity> AddAsync(string name)
{
EmployeeEntity employee = new EmployeeEntity
{
Name = name,
IsModified = true
};
await using PosDbContext context = new PosDbContext();
context.Employee.Add(employee);
await context.SaveChangesAsync();
return employee;
}
}
}

View File

@@ -28,10 +28,11 @@ namespace Database.Repository
return product;
}
public void Add(string name)
public void Add(string name, decimal vatRate)
{
ProductGroupEntity productGroup = new ProductGroupEntity();
productGroup.Name = name;
productGroup.VatRate = vatRate;
using PosDbContext context = new PosDbContext();
//Get the highest index
ProductGroupEntity highest = context.ProductGroups.OrderByDescending(c => c.Index).Take(1).FirstOrDefault();
@@ -45,11 +46,12 @@ namespace Database.Repository
context.SaveChanges();
}
public void Edit(string name, int id)
public void Edit(string name, decimal vatRate, int id)
{
using PosDbContext context = new PosDbContext();
ProductGroupEntity productGroup = context.ProductGroups.Single(c => c.Id == id);
productGroup.Name = name;
productGroup.VatRate = vatRate;
context.SaveChanges();
}
@@ -69,10 +71,30 @@ namespace Database.Repository
context.SaveChanges();
}
public void SetOrder(IReadOnlyList<int> orderedIds)
{
using PosDbContext context = new PosDbContext();
List<ProductGroupEntity> productGroups = context.ProductGroups
.Where(productGroup => orderedIds.Contains(productGroup.Id))
.ToList();
if (productGroups.Count != orderedIds.Count)
throw new InvalidOperationException(
"En eller flere produktgrupper findes ikke.");
Dictionary<int, ProductGroupEntity> productGroupsById =
productGroups.ToDictionary(productGroup => productGroup.Id);
for (int index = 0; index < orderedIds.Count; index++)
productGroupsById[orderedIds[index]].Index = index;
context.SaveChanges();
}
public bool Any()
{
using PosDbContext context = new PosDbContext();
bool any = context.ProductGroups.Any();
bool any = context.ProductGroups.Any(c => !c.IsArchived);
return any;
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System;
using System.Linq;
using Database.Models;
@@ -38,6 +39,33 @@ namespace Database.Repository
context.SaveChanges();
}
public void SetOrder(
int productGroupId,
IReadOnlyList<int> orderedIds)
{
using PosDbContext context = new PosDbContext();
List<ProductEntity> products = context.Products
.Where(product => product.ProductGroupId == productGroupId)
.Where(product => orderedIds.Contains(product.Id))
.ToList();
if (products.Count != orderedIds.Count)
throw new InvalidOperationException(
"Et eller flere produkter findes ikke i produktgruppen.");
Dictionary<int, ProductEntity> productsById =
products.ToDictionary(product => product.Id);
for (int index = 0; index < orderedIds.Count; index++)
{
ProductEntity product = productsById[orderedIds[index]];
product.Index = index;
product.IsModified = true;
}
context.SaveChanges();
}
public ProductEntity GetById(int id)
{
using PosDbContext context = new PosDbContext();
@@ -72,7 +100,11 @@ namespace Database.Repository
product.ProductGroupId = productGroupId;
using PosDbContext context = new PosDbContext();
//Get the highest index
ProductEntity highest = context.Products.OrderByDescending(c => c.Index).Take(1).FirstOrDefault();
ProductEntity highest = context.Products
.Where(c => c.ProductGroupId == productGroupId)
.OrderByDescending(c => c.Index)
.Take(1)
.FirstOrDefault();
if (highest == null)
{
highest = new ProductEntity();

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pos.Models
{
public class TotalSaleDetail
{
public decimal TotalSale { get; set; }
public int TotalCustomer { get; set; }
public List<TotalSaleCategory> TotalSaleCategories { get; set; } = new();
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net10.0-windows7.0</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
@@ -18,12 +18,10 @@
<None Remove="Icons\settings.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="RestSharp" Version="108.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Database\Database.csproj" />
<ProjectReference Include="..\..\Pos.Models\Pos.Models.csproj" />
<ProjectReference Include="..\..\Pos.Service\Pos.Service.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -11,8 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Models;
using Database.Repository;
using Pos.Models;
using Pos.Service;
namespace Pos
@@ -25,6 +24,9 @@ namespace Pos
private List<ListReceiptModel> _receiptList = new List<ListReceiptModel>();
private bool _init = false;
private int _saleId = 0;
private readonly SaleHistoryService _saleHistoryService =
new SaleHistoryService();
private readonly SaleService _saleService = new SaleService();
public PrintReceiptWindow()
{
@@ -38,28 +40,39 @@ namespace Pos
private void Search_OnClick(object sender, RoutedEventArgs e)
{
DateTime startDate = (DateTime)StartDatePicker.SelectedDate;
DateTime endDate = (DateTime)EndDatePicker.SelectedDate;
SaleRepository saleRepository = new SaleRepository();
List<SaleEntity> saleEntities = saleRepository.GetByDateRange(startDate, endDate);
foreach (SaleEntity entity in saleEntities)
{
List<SaleLineEntity> saleLineBySaleId = saleRepository.GetSaleLineBySaleId(entity.Id);
decimal total = 0;
foreach (SaleLineEntity lineEntity in saleLineBySaleId)
{
total += lineEntity.Total;
}
if (StartDatePicker.SelectedDate is not DateTime startDate ||
EndDatePicker.SelectedDate is not DateTime endDate)
return;
List<ReceiptSummaryModel> receipts;
try
{
receipts = _saleHistoryService.Search(startDate, endDate);
}
catch (ArgumentException exception)
{
MessageBox.Show(
exception.Message,
"Ugyldigt datointerval",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
}
_receiptList.Clear();
foreach (ReceiptSummaryModel receipt in receipts)
{
ListReceiptModel receiptModel = new ListReceiptModel
{
Id = entity.Id,
Content = $"{entity.Time:dd-MM-yyyy HH:mm} - Pris: {total:0.00}"
Id = receipt.SaleId,
Content =
$"{receipt.SaleTime:dd-MM-yyyy HH:mm} - Pris: {receipt.Total:0.00}"
};
_receiptList.Add(receiptModel);
}
_init = true;
ListReciept.ItemsSource = null;
ListReciept.ItemsSource = _receiptList;
_init = false;
}
@@ -78,23 +91,21 @@ namespace Pos
private void ListReciept_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_init)
if (_init || e.AddedItems.Count == 0)
return;
ListReceiptModel data = (ListReceiptModel) e.AddedItems[0];
_saleId = data.Id;
SaleRepository saleRepository = new SaleRepository();
List<SaleLineEntity> saleLineBySaleId = saleRepository.GetSaleLineBySaleId(data.Id);
ReceiptDetailModel receipt =
_saleHistoryService.GetReceipt(data.Id);
StringBuilder sb = new StringBuilder();
decimal total = 0;
foreach (SaleLineEntity saleLine in saleLineBySaleId)
foreach (ReceiptLineModel saleLine in receipt.Lines)
{
total += saleLine.Total;
sb.AppendLine(
$"{saleLine.Product} - {saleLine.Pieces} stk. a' {saleLine.Price.ToString("0.00")} = {saleLine.Total.ToString("0.00")}");
$"{saleLine.Product} - {saleLine.Pieces} stk. a' {saleLine.Price:0.00} = {saleLine.Total:0.00}");
}
sb.AppendLine($"Total: {total.ToString("0.00")} kr.");
sb.AppendLine($"Total: {receipt.Total:0.00} kr.");
PrintReceipt.Visibility = Visibility.Visible;
SaleLines.Clear();
SaleLines.Text = sb.ToString();
@@ -102,8 +113,7 @@ namespace Pos
private void PrintReceipt_OnClick(object sender, RoutedEventArgs e)
{
SaleService saleService = new SaleService();
saleService.PrintReceipt(_saleId);
_saleService.PrintReceipt(_saleId);
}
}
}

View File

@@ -13,11 +13,11 @@ namespace Pos
{
private bool _initialLoad = true;
private TotalSaleDetail totalSaleDetail;
private readonly SaleService _saleService = new SaleService();
public SaleOfDay()
{
InitializeComponent();
SaleService saleService = new SaleService();
totalSaleDetail = saleService.TotalSale(DateTime.Now);
totalSaleDetail = _saleService.TotalSale(DateTime.Now);
TotalSale.Content = $"Dagens salg: kr. {totalSaleDetail.TotalSale:0.00}";
StartDatePicker.SelectedDate = DateTime.Now;
GridSaleDetail.ItemsSource = totalSaleDetail.TotalSaleCategories;
@@ -29,18 +29,20 @@ namespace Pos
if (_initialLoad)
return;
_initialLoad = false;
DateTime selectedDate = DateTime.Parse(sender.ToString());
SaleService saleService = new SaleService();
totalSaleDetail = saleService.TotalSale(selectedDate);
if (StartDatePicker.SelectedDate is not DateTime selectedDate)
return;
totalSaleDetail = _saleService.TotalSale(selectedDate);
TotalSale.Content = $"Dagens salg: kr. {totalSaleDetail.TotalSale:0.00}";
GridSaleDetail.ItemsSource = totalSaleDetail.TotalSaleCategories;
}
private void PrintSale_OnClick(object sender, RoutedEventArgs e)
{
SaleService saleService = new SaleService();
saleService.PrintSaleOfDay(StartDatePicker.SelectedDate.Value,totalSaleDetail);
if (StartDatePicker.SelectedDate is not DateTime selectedDate)
return;
_saleService.PrintSaleOfDay(selectedDate, totalSaleDetail);
}
}
}

View File

@@ -29,7 +29,8 @@ namespace Pos
private ObservableCollection<ProductGroupEntity> _productGroups;
private ObservableCollection<SaleGridModel> _saleGridModels;
private ObservableCollection<AmountGridModel> _paymentGridModels;
private List<PriceLineModel> _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<EmployeeEntity>;
_productGroups = CacheService.GetProductGroupsIncludeProducts() as ObservableCollection<ProductGroupEntity>;
_saleGridModels = new ObservableCollection<SaleGridModel>();
_priceLineModels = new List<PriceLineModel>();
_paymentGridModels = new ObservableCollection<AmountGridModel>();
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;

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting
{
@@ -20,6 +20,9 @@ namespace Pos.Setting
/// </summary>
public partial class AddEmployeeWindow : Window
{
private readonly EmployeeService _employeeService =
new EmployeeService();
public AddEmployeeWindow()
{
InitializeComponent();
@@ -27,9 +30,19 @@ namespace Pos.Setting
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
EmployeeRepository employeeRepository = new EmployeeRepository();
employeeRepository.Add(txtStaff.Text);
this.Close();
try
{
_employeeService.AddEmployee(txtStaff.Text);
Close();
}
catch (ArgumentException exception)
{
MessageBox.Show(
exception.Message,
"Ugyldig medarbejder",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
}
}

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting
{
@@ -21,6 +21,8 @@ namespace Pos.Setting
public partial class DeleteEmployeeWindow : Window
{
private int _employeeId;
private readonly EmployeeService _employeeService =
new EmployeeService();
public DeleteEmployeeWindow(int employeeId, string staffName)
{
@@ -31,9 +33,8 @@ namespace Pos.Setting
private void btnYes_Click(object sender, RoutedEventArgs e)
{
EmployeeRepository employeeRepository = new EmployeeRepository();
employeeRepository.Delete(_employeeId);
this.Close();
_employeeService.ArchiveEmployee(_employeeId);
Close();
}
private void btnNo_Click(object sender, RoutedEventArgs e)

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting
{
@@ -21,6 +21,8 @@ namespace Pos.Setting
public partial class EditEmployeeWindow : Window
{
private int _employeeId;
private readonly EmployeeService _employeeService =
new EmployeeService();
public EditEmployeeWindow(int employeeId, string staffName)
{
@@ -31,9 +33,21 @@ namespace Pos.Setting
private void btnEdit_Click(object sender, RoutedEventArgs e)
{
EmployeeRepository employeeRepository = new EmployeeRepository();
employeeRepository.Edit(_employeeId,txtEmployee.Text);
this.Close();
try
{
_employeeService.RenameEmployee(
_employeeId,
txtEmployee.Text);
Close();
}
catch (ArgumentException exception)
{
MessageBox.Show(
exception.Message,
"Ugyldig medarbejder",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
}
}

View File

@@ -13,7 +13,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Models;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting
{
@@ -24,6 +24,8 @@ namespace Pos.Setting
{
private int _selectedEmployee = -1;
private string _employeeName = string.Empty;
private readonly EmployeeService _employeeService =
new EmployeeService();
public EmployeeWindow()
{
InitializeComponent();
@@ -62,15 +64,14 @@ namespace Pos.Setting
deleteEmployeeWindow.Show();
}
private void WindowClosed(object? sender, EventArgs e)
private void WindowClosed(object sender, EventArgs e)
{
LoadData();
}
private void LoadData()
{
using EmployeeRepository employeeRepository = new EmployeeRepository();
List<EmployeeEntity> allStaff = employeeRepository.GetAll();
List<EmployeeEntity> allStaff = _employeeService.GetEmployees();
LstStaff.Items.Clear();
foreach (EmployeeEntity staff in allStaff)
{

View File

@@ -1,8 +1,8 @@
using Database.Repository;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using Pos.Service;
namespace Pos.Setting.ProductGroup
{
@@ -11,6 +11,11 @@ namespace Pos.Setting.ProductGroup
/// </summary>
public partial class AddProductWindow : Window
{
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
private readonly ProductService _productService =
new ProductService();
public AddProductWindow()
{
InitializeComponent();
@@ -19,13 +24,16 @@ namespace Pos.Setting.ProductGroup
private void LoadData(int selectedIndex)
{
ProductGroupRepository productGroupRepository = new ProductGroupRepository();
List<Database.Models.ProductGroupEntity> products = productGroupRepository.GetAll();
foreach (Database.Models.ProductGroupEntity productGroup in products)
List<Database.Models.ProductGroupEntity> productGroups =
_productGroupService.GetProductGroups();
foreach (Database.Models.ProductGroupEntity productGroup in productGroups)
{
ComboBoxItem comboBoxItem = new ComboBoxItem();
comboBoxItem.Tag = productGroup.Id;
comboBoxItem.Content = productGroup.Name;
ComboBoxItem comboBoxItem = new ComboBoxItem
{
Tag = productGroup.Id,
Content = productGroup.Name
};
cmbProductGroup.Items.Add(comboBoxItem);
}
@@ -34,12 +42,23 @@ namespace Pos.Setting.ProductGroup
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
ComboBoxItem item = (ComboBoxItem) cmbProductGroup.SelectedItem;
string name = txtProduct.Text;
int id = Convert.ToInt32(item.Tag);
ProductRepository productRepository = new ProductRepository();
productRepository.Add(name,id);
this.Close();
ComboBoxItem item = (ComboBoxItem)cmbProductGroup.SelectedItem;
try
{
_productService.AddProduct(
txtProduct.Text,
Convert.ToInt32(item.Tag));
Close();
}
catch (ArgumentException exception)
{
MessageBox.Show(
exception.Message,
"Ugyldigt produkt",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
}
}

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting.Product
{
@@ -21,20 +21,21 @@ namespace Pos.Setting.Product
public partial class ArchiveProductWindow : Window
{
private int _productId;
private readonly ProductService _productService =
new ProductService();
public ArchiveProductWindow(int productId)
{
InitializeComponent();
_productId = productId;
ProductRepository productRepository = new ProductRepository();
Database.Models.ProductEntity product = productRepository.GetById(_productId);
Database.Models.ProductEntity product =
_productService.GetProduct(_productId);
txtDelProduct.Text = product.Name;
}
private void btnYes_Click(object sender, RoutedEventArgs e)
{
ProductRepository productRepository = new ProductRepository();
productRepository.Archive(_productId);
this.Close();
_productService.ArchiveProduct(_productId);
Close();
}
private void btnNo_Click(object sender, RoutedEventArgs e)

View File

@@ -12,7 +12,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting.Product
{
@@ -23,6 +23,8 @@ namespace Pos.Setting.Product
{
ObservableCollection<Database.Models.ProductEntity> _productList = new ObservableCollection<Database.Models.ProductEntity>();
private int _productGroupId;
private readonly ProductService _productService =
new ProductService();
public ChangeProductOrderWindow(int productGroupId)
{
InitializeComponent();
@@ -32,8 +34,8 @@ namespace Pos.Setting.Product
private void LoadData()
{
ProductRepository productRepository = new ProductRepository();
List<Database.Models.ProductEntity> products = productRepository.GetByProductGroup(_productGroupId);
List<Database.Models.ProductEntity> products =
_productService.GetProducts(_productGroupId);
foreach (Database.Models.ProductEntity product in products)
{
_productList.Add(product);
@@ -86,13 +88,10 @@ namespace Pos.Setting.Product
private void btnSave_Click(object sender, RoutedEventArgs e)
{
ProductRepository productRepository = new ProductRepository();
for (int i = 0; i < _productList.Count; i++)
{
Database.Models.ProductEntity product = _productList[i];
productRepository.SetIndex(product.Id, i);
}
this.Close();
_productService.UpdateOrder(
_productGroupId,
_productList.Select(product => product.Id).ToList());
Close();
}
}

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting.Product
{
@@ -23,6 +23,10 @@ namespace Pos.Setting.Product
private int _productId;
private int _selectedProductGroupId;
private Database.Models.ProductEntity _product;
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
private readonly ProductService _productService =
new ProductService();
public EditProductWindow(int productId, int selectedProductGroupId)
{
@@ -34,8 +38,8 @@ namespace Pos.Setting.Product
private void Init()
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
List<Database.Models.ProductGroupEntity> productGroups = categoryRepository.GetAll();
List<Database.Models.ProductGroupEntity> productGroups =
_productGroupService.GetProductGroups();
foreach (Database.Models.ProductGroupEntity productGroup in productGroups)
{
@@ -46,17 +50,29 @@ namespace Pos.Setting.Product
}
cmbProductGroup.SelectedIndex = _selectedProductGroupId;
ProductRepository productRepository = new ProductRepository();
_product = productRepository.GetById(_productId);
_product = _productService.GetProduct(_productId);
txtProduct.Text = _product.Name;
}
private void btnEdit_Click(object sender, RoutedEventArgs e)
{
ComboBoxItem item = (ComboBoxItem)cmbProductGroup.SelectedItem;
ProductRepository productRepository = new ProductRepository();
productRepository.Update(_product.Id,(int)item.Tag,txtProduct.Text);
this.Close();
try
{
_productService.UpdateProduct(
_product.Id,
(int)item.Tag,
txtProduct.Text);
Close();
}
catch (ArgumentException exception)
{
MessageBox.Show(
exception.Message,
"Ugyldigt produkt",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
}
}

View File

@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using Database.Repository;
using Pos.Service;
using Pos.Setting.Product;
namespace Pos.Setting.ProductGroup
@@ -14,7 +14,10 @@ namespace Pos.Setting.ProductGroup
public partial class ProductWindow : Window
{
private List<Database.Models.ProductGroupEntity> _productGroups;
private int _productId;
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
private readonly ProductService _productService =
new ProductService();
public ProductWindow()
{
@@ -26,8 +29,7 @@ namespace Pos.Setting.ProductGroup
private void Init()
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
_productGroups = categoryRepository.GetAll();
_productGroups = _productGroupService.GetProductGroups();
cmbCat.Items.Clear();
foreach (Database.Models.ProductGroupEntity productGroup in _productGroups)
{
@@ -38,14 +40,19 @@ namespace Pos.Setting.ProductGroup
cmbCat.Items.Add(comboBoxItem);
}
cmbCat.SelectedIndex = 0;
if (_productGroups.Count > 0)
cmbCat.SelectedIndex = 0;
}
private void LoadData()
{
ProductRepository productRepository = new ProductRepository();
List<Database.Models.ProductEntity> products = productRepository.GetByProductGroup(_productGroups[cmbCat.SelectedIndex].Id);
if (cmbCat.SelectedIndex < 0)
return;
List<Database.Models.ProductEntity> products =
_productService.GetProducts(
_productGroups[cmbCat.SelectedIndex].Id);
lstProductGroup.Items.Clear();
foreach (Database.Models.ProductEntity product in products)
{

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting.Category
{
@@ -20,6 +20,9 @@ namespace Pos.Setting.Category
/// </summary>
public partial class AddProductGroupWindow : Window
{
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
public AddProductGroupWindow()
{
InitializeComponent();
@@ -27,9 +30,19 @@ namespace Pos.Setting.Category
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
categoryRepository.Add(txtProductGroup.Text);
this.Close();
try
{
_productGroupService.AddProductGroup(txtProductGroup.Text);
Close();
}
catch (ArgumentException exception)
{
MessageBox.Show(
exception.Message,
"Ugyldig produktgruppe",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
}
}

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting.Category
{
@@ -21,6 +21,8 @@ namespace Pos.Setting.Category
public partial class ArchiveProductGroupWindow : Window
{
private readonly int _id;
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
public ArchiveProductGroupWindow(string name, int id)
{
InitializeComponent();
@@ -36,9 +38,8 @@ namespace Pos.Setting.Category
private void btnYes_Click(object sender, RoutedEventArgs e)
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
categoryRepository.Archive(_id);
this.Close();
_productGroupService.ArchiveProductGroup(_id);
Close();
}
}
}

View File

@@ -12,7 +12,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting.Category
{
@@ -22,6 +22,8 @@ namespace Pos.Setting.Category
public partial class ChangeProductGroupOrderWindow : Window
{
ObservableCollection<Database.Models.ProductGroupEntity> _prodGroupList = new ObservableCollection<Database.Models.ProductGroupEntity>();
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
public ChangeProductGroupOrderWindow()
{
InitializeComponent();
@@ -30,8 +32,8 @@ namespace Pos.Setting.Category
private void LoadData()
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
List<Database.Models.ProductGroupEntity> categories = categoryRepository.GetAll();
List<Database.Models.ProductGroupEntity> categories =
_productGroupService.GetProductGroups();
foreach (Database.Models.ProductGroupEntity category in categories)
{
_prodGroupList.Add(category);
@@ -84,13 +86,9 @@ namespace Pos.Setting.Category
private void btnSave_Click(object sender, RoutedEventArgs e)
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
for (int i = 0; i < _prodGroupList.Count; i++)
{
Database.Models.ProductGroupEntity productGroup = _prodGroupList[i];
categoryRepository.SetIndex(productGroup.Id,i);
}
this.Close();
_productGroupService.UpdateOrder(
_prodGroupList.Select(productGroup => productGroup.Id).ToList());
Close();
}
}
}

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
namespace Pos.Setting.Category
{
@@ -21,6 +21,8 @@ namespace Pos.Setting.Category
public partial class EditProductGroupWindow : Window
{
private readonly int _id;
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
public EditProductGroupWindow(string name, int id)
{
@@ -31,9 +33,21 @@ namespace Pos.Setting.Category
private void btnEdit_Click(object sender, RoutedEventArgs e)
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
categoryRepository.Edit(txtProductGroup.Text,_id);
this.Close();
try
{
_productGroupService.RenameProductGroup(
_id,
txtProductGroup.Text);
Close();
}
catch (ArgumentException exception)
{
MessageBox.Show(
exception.Message,
"Ugyldig produktgruppe",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
}
}

View File

@@ -11,7 +11,7 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Service;
using Pos.Setting.Category;
namespace Pos.Category
@@ -23,6 +23,8 @@ namespace Pos.Category
{
private int _id = -1;
private string _name = string.Empty;
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
public ProductGroupWindow()
{
@@ -53,8 +55,8 @@ namespace Pos.Category
private void LoadData()
{
ProductGroupRepository categoryRepository = new ProductGroupRepository();
List<Database.Models.ProductGroupEntity> categories = categoryRepository.GetAll();
List<Database.Models.ProductGroupEntity> categories =
_productGroupService.GetProductGroups();
LstCategory.Items.Clear();
foreach (Database.Models.ProductGroupEntity category in categories)
{

View File

@@ -11,8 +11,8 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Database.Repository;
using Pos.Category;
using Pos.Service;
using Pos.Setting.ProductGroup;
namespace Pos.Setting
@@ -22,6 +22,9 @@ namespace Pos.Setting
/// </summary>
public partial class SettingWindow : Window
{
private readonly ProductGroupService _productGroupService =
new ProductGroupService();
public SettingWindow()
{
InitializeComponent();
@@ -42,10 +45,8 @@ namespace Pos.Setting
private void btnProductGroups_Click(object sender, RoutedEventArgs e)
{
ProductWindow productWindow = new ProductWindow();
//Check if there is any categories, if not close this window with a message.
ProductGroupRepository categoryRepository = new ProductGroupRepository();
bool anyExist = categoryRepository.Any();
bool anyExist = _productGroupService.HasProductGroups();
if (!anyExist)
{
MessageBox.Show("Der er ingen kategorier, opret dem først", "Kategorier", MessageBoxButton.OK,
@@ -53,6 +54,7 @@ namespace Pos.Setting
}
else
{
ProductWindow productWindow = new ProductWindow();
productWindow.Show();
}
}

View File

@@ -1,5 +1,5 @@
{
"MariaSqlServer": "Data Source=localhost;Initial Catalog=PointOfSale;Persist Security Info=False;User ID=root;Password=Maxbp6703",
"MariaSqlServer": "Data Source=localhost;Initial Catalog=PointOfSale;Persist Security Info=False;User ID=root;Password=",
"PrintSettings": {
"ComPort": "COM5",
"BaudRate": 115200,

View File

@@ -1,5 +1,5 @@
{
"MariaSqlServer": "Data Source=localhost;Initial Catalog=PointOfSale;Persist Security Info=False;User ID=root;Password=Maxbp6703",
"SqlServer": "Server=localhost;Database=PointOfSale;Trusted_Connection=True;TrustServerCertificate=True;",
"PrintSettings": {
"ComPort": "COM5",
"BaudRate": 115200,

View File

@@ -1,60 +0,0 @@
//public class Rootobject
//{
// public string logoBase64 { get; set; }
// public Header[] header { get; set; }
// public Bodymodel bodyModel { get; set; }
// public Footer[] footer { get; set; }
//}
//public class Bodymodel
//{
// public Product[] products { get; set; }
// public int totalPrice { get; set; }
// public int totalVat { get; set; }
// public int receiptNumber { get; set; }
// public string receiptTime { get; set; }
// public string staff { get; set; }
//}
//public class Product
//{
// public string product { get; set; }
// public string noOfProduct { get; set; }
// public float price { get; set; }
// public float totalPrice { get; set; }
//}
//public class Header
//{
// public string value { get; set; }
// public Printstyles printStyles { get; set; }
// public int textAlignment { get; set; }
// public int feedingLines { get; set; }
//}
//public class Printstyles
//{
// public bool fontB { get; set; }
// public bool bold { get; set; }
// public bool doubleHeight { get; set; }
// public bool doubleWidth { get; set; }
// public bool underline { get; set; }
//}
//public class Footer
//{
// public string value { get; set; }
// public Printstyles1 printStyles { get; set; }
// public int textAlignment { get; set; }
// public int feedingLines { get; set; }
//}
//public class Printstyles1
//{
// public bool fontB { get; set; }
// public bool bold { get; set; }
// public bool doubleHeight { get; set; }
// public bool doubleWidth { get; set; }
// public bool underline { get; set; }
//}

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["Pos.Web.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
@inherits LayoutComponentBase
<FluentLayout>
<FluentLayoutItem Area="LayoutArea.Content" Class="main">
<div class="content">
@Body
</div>
</FluentLayoutItem>
</FluentLayout>
<FluentDialogProvider />
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

View File

@@ -0,0 +1,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>

View File

@@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}

View File

@@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,21 @@
@rendermode InteractiveServer
@inject NavigationManager Nav
@page "/"
<Header/>
<div class="home-container">
<FluentButton Class="menu-button" Size="ButtonSize.Large">
Kassesalg
</FluentButton>
<FluentButton Class="menu-button" Size="ButtonSize.Large" onclick="@(() => Nav.NavigateTo("/todaySales"))">
Dagens salg
</FluentButton>
<FluentButton Class="menu-button" Size="ButtonSize.Large">
Print Bon
</FluentButton>
<FluentButton Class="menu-button" Size="ButtonSize.Large" onclick="@(() => Nav.NavigateTo("/settings"))">
Indstillinger
</FluentButton>
</div>

View File

@@ -0,0 +1,23 @@
.home-container {
width: min(100%, 1100px);
margin: 32px auto;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
.home-container ::deep .menu-button {
width: 100%;
min-height: 220px;
font-size: 28px;
font-weight: 600;
}
@media (max-width: 680px) {
.home-container {
grid-template-columns: 1fr;
margin: 24px 16px;
}
}

View File

@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

View File

@@ -0,0 +1,44 @@
@using Pos.Service
@inject EmployeeService EmployeeService
@if (IsOpen)
{
<div class="dialog-overlay">
<div class="employee-dialog" role="dialog" aria-modal="true" aria-labelledby="create-employee-title">
<div class="dialog-header">
<h2 id="create-employee-title">Opret medarbejder</h2>
<button class="close-button"
type="button"
aria-label="Luk"
@onclick="CloseAsync">
&times;
</button>
</div>
<div class="dialog-content">
<label for="employee-name">Navn</label>
<input id="employee-name"
class="name-input"
type="text"
placeholder="Indtast navn"
@bind="_name"
@bind:event="oninput" />
<p>Medarbejderen bliver aktiv med det samme.</p>
</div>
<div class="dialog-actions">
<button class="cancel-button"
type="button"
@onclick="CloseAsync">
Annuller
</button>
<button class="create-button"
type="button"
disabled="@string.IsNullOrWhiteSpace(_name)"
@onclick="CreateEmployeeAsync">
Opret medarbejder
</button>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,35 @@
using Database.Models;
using Microsoft.AspNetCore.Components;
using Pos.Service;
namespace Pos.Web.Components.Pages.Settings
{
public partial class CreateEmployeeDialog
{
[Parameter]
public bool IsOpen { get; set; }
[Parameter]
public EventCallback<bool> IsOpenChanged { get; set; }
[Parameter]
public EventCallback<EmployeeEntity> EmployeeCreated { get; set; }
private string _name = string.Empty;
private async Task CreateEmployeeAsync()
{
EmployeeEntity employee =
await EmployeeService.AddEmployeeAsync(_name);
await EmployeeCreated.InvokeAsync(employee);
await CloseAsync();
}
private async Task CloseAsync()
{
_name = string.Empty;
await IsOpenChanged.InvokeAsync(false);
}
}
}

View File

@@ -0,0 +1,132 @@
.dialog-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 24px;
background: rgb(15 23 42 / 45%);
}
.employee-dialog {
width: min(100%, 650px);
overflow: hidden;
border-radius: 12px;
background: white;
color: #242424;
color-scheme: light;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28px 40px 18px;
}
.dialog-header h2 {
margin: 0;
font-size: 28px;
}
.close-button {
border: 0;
background: transparent;
color: #334155;
cursor: pointer;
font-size: 36px;
line-height: 1;
}
.dialog-content {
display: grid;
gap: 10px;
padding: 18px 40px 36px;
}
.dialog-content label {
color: #242424;
font-size: 16px;
font-weight: 500;
}
.name-input {
box-sizing: border-box;
width: 100%;
height: 48px;
padding: 0 14px;
border: 1px solid #8a8a8a;
border-radius: 6px;
outline: none;
background: #ffffff;
color: #242424;
font: inherit;
}
.name-input::placeholder {
color: #707070;
}
.name-input:focus {
border-color: #0f6cbd;
box-shadow: 0 0 0 1px #0f6cbd;
}
.dialog-content p {
margin: 6px 0 0;
color: #64748b;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
padding: 24px 40px;
border-top: 1px solid #e2e8f0;
}
.cancel-button,
.create-button {
min-height: 42px;
padding: 0 22px;
border-radius: 6px;
cursor: pointer;
font: inherit;
font-weight: 500;
}
.cancel-button {
border: 1px solid #616161;
background: #ffffff;
color: #242424;
}
.cancel-button:hover {
background: #f5f5f5;
}
.create-button {
border: 1px solid #0f6cbd;
background: #0f6cbd;
color: #ffffff;
}
.create-button:hover:not(:disabled) {
background: #115ea3;
}
.create-button:disabled {
border-color: #d1d1d1;
background: #e0e0e0;
color: #707070;
cursor: not-allowed;
}
@media (max-width: 600px) {
.dialog-header,
.dialog-content,
.dialog-actions {
padding-right: 24px;
padding-left: 24px;
}
}

View File

@@ -0,0 +1,45 @@
@page "/settings/employees"
@inject IDialogService DialogService
@rendermode InteractiveServer
@using Database.Models
@using Pos.Service
@inject EmployeeService EmployeeService
<Header />
<div class="employees-page">
<div class="employees-header">
<h1>Medarbejdere</h1>
<FluentButton Appearance="@ButtonAppearance.Primary"
OnClick="@(() => _showCreateDialog = true)">
Opret medarbejder
</FluentButton>
</div>
@if (_isLoading)
{
<FluentProgressRing />
}
else
{
<FluentDataGrid TGridItem="EmployeeEntity"
Items="@_employees.AsQueryable()"
GridTemplateColumns="1fr 180px">
<PropertyColumn Property="@(item => item.Name)"
Title="Navn"
Sortable="true" />
<TemplateColumn Title="Handling" Context="employee">
<FluentButton Class="deactivate-button"
Appearance="@ButtonAppearance.Outline"
OnClick="@(() => ConfirmDeactivation(employee))">
Deaktivér
</FluentButton>
</TemplateColumn>
</FluentDataGrid>
}
</div>
<CreateEmployeeDialog @bind-IsOpen="_showCreateDialog"
EmployeeCreated="EmployeeCreated" />

View File

@@ -0,0 +1,53 @@
using Database.Models;
using Microsoft.FluentUI.AspNetCore.Components;
namespace Pos.Web.Components.Pages.Settings
{
public partial class Employees
{
private List<EmployeeEntity> _employees = [];
private bool _isLoading = true;
private bool _showCreateDialog;
protected override async Task OnInitializedAsync()
{
_employees = await EmployeeService.GetEmployeesAsync();
_isLoading = false;
}
private void LoadEmployees()
{
_employees = EmployeeService.GetEmployees();
}
private async Task ConfirmDeactivation(EmployeeEntity employee)
{
DialogResult result = await DialogService.ShowConfirmationAsync(
$"Er du sikker på, at du vil deaktivere {employee.Name}?",
"Bekræft deaktivering",
"Annuller",
"Deaktivér");
if (result.Cancelled)
{
DeactivateEmployee(employee);
}
}
private void DeactivateEmployee(EmployeeEntity employee)
{
EmployeeService.ArchiveEmployee(employee.Id);
LoadEmployees();
}
private class EmployeeRow
{
public string Name { get; set; } = string.Empty;
}
private void EmployeeCreated(EmployeeEntity employee)
{
_employees.Add(employee);
}
}
}

View File

@@ -0,0 +1,26 @@
/* CSS for Employees component */
.employees-page {
width: min(100%, 1100px);
margin: 0 auto;
padding: 32px;
}
.employees-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.employees-header h1 {
margin: 0;
}
.employees-page ::deep fluent-data-grid {
width: 100%;
}
.employees-page ::deep .deactivate-button {
color: #b10e1e;
border-color: #b10e1e;
}

View File

@@ -0,0 +1,30 @@
@using Database.Models
@using Pos.Service
@inject ProductGroupService ProductGroupService
@if (IsOpen)
{
<div class="dialog-overlay">
<div class="product-dialog" role="dialog" aria-modal="true" aria-labelledby="product-dialog-title">
<div class="dialog-header">
<h2 id="product-dialog-title">@(_productGroupId.HasValue ? "Redigér vare" : "Opret vare")</h2>
<button class="close-button" type="button" aria-label="Luk" @onclick="CloseAsync">&times;</button>
</div>
<div class="dialog-content">
<label for="product-name">Navn</label>
<input id="product-name" class="dialog-input" type="text" placeholder="Indtast navn"
@bind="_name" @bind:event="oninput" />
<label for="product-vat">Moms i procent</label>
<input id="product-vat" class="dialog-input" type="number" min="0" max="100" step="0.01"
@bind="_vatRate" />
</div>
<div class="dialog-actions">
<button class="cancel-button" type="button" @onclick="CloseAsync">Annuller</button>
<button class="save-button" type="button" disabled="@(!CanSave)" @onclick="SaveAsync">
@(_productGroupId.HasValue ? "Gem ændringer" : "Opret vare")
</button>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,50 @@
using Database.Models;
using Microsoft.AspNetCore.Components;
namespace Pos.Web.Components.Pages.Settings
{
public partial class ProductGroupDialog
{
[Parameter] public bool IsOpen { get; set; }
[Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
[Parameter] public ProductGroupEntity? ProductGroup { get; set; }
[Parameter] public EventCallback ProductGroupSaved { get; set; }
private int? _productGroupId;
private string _name = string.Empty;
private decimal _vatRate = 25m;
private bool _wasOpen;
private bool CanSave => !string.IsNullOrWhiteSpace(_name) && _vatRate >= 0 && _vatRate <= 100;
protected override void OnParametersSet()
{
if (IsOpen && !_wasOpen)
{
_productGroupId = ProductGroup?.Id;
_name = ProductGroup?.Name ?? string.Empty;
_vatRate = ProductGroup?.VatRate ?? 25m;
}
_wasOpen = IsOpen;
}
private async Task SaveAsync()
{
if (!CanSave)
return;
if (_productGroupId.HasValue)
ProductGroupService.UpdateProductGroup(_productGroupId.Value, _name, _vatRate);
else
ProductGroupService.AddProductGroup(_name, _vatRate);
await ProductGroupSaved.InvokeAsync();
await CloseAsync();
}
private async Task CloseAsync()
{
_wasOpen = false;
await IsOpenChanged.InvokeAsync(false);
}
}
}

View File

@@ -0,0 +1,14 @@
.dialog-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 24px; background: rgb(15 23 42 / 45%); }
.product-dialog { width: min(100%, 650px); overflow: hidden; border-radius: 12px; background: white; color: #242424; color-scheme: light; }
.dialog-header { display: flex; align-items: center; justify-content: space-between; padding: 28px 40px 18px; }
.dialog-header h2 { margin: 0; font-size: 28px; }
.close-button { border: 0; background: transparent; color: #334155; cursor: pointer; font-size: 36px; line-height: 1; }
.dialog-content { display: grid; gap: 10px; padding: 18px 40px 36px; }
.dialog-content label { margin-top: 8px; font-size: 16px; font-weight: 500; }
.dialog-input { box-sizing: border-box; width: 100%; height: 48px; padding: 0 14px; border: 1px solid #8a8a8a; border-radius: 6px; outline: none; background: #fff; color: #242424; font: inherit; }
.dialog-input:focus { border-color: #0f6cbd; box-shadow: 0 0 0 1px #0f6cbd; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 16px; padding: 24px 40px; border-top: 1px solid #e2e8f0; }
.cancel-button, .save-button { min-height: 42px; padding: 0 22px; border-radius: 6px; cursor: pointer; font: inherit; font-weight: 500; }
.cancel-button { border: 1px solid #616161; background: #fff; color: #242424; }
.save-button { border: 1px solid #0f6cbd; background: #0f6cbd; color: #fff; }
.save-button:disabled { border-color: #d1d1d1; background: #e0e0e0; color: #707070; cursor: not-allowed; }

View File

@@ -0,0 +1,77 @@
@page "/settings/products"
@rendermode InteractiveServer
@using Database.Models
@using Pos.Service
@inject IDialogService DialogService
@inject ProductGroupService ProductGroupService
<Header />
<div class="products-page">
<div class="products-header">
<h1>Vare</h1>
</div>
<div class="products-toolbar">
<label class="search-field">
<span aria-hidden="true">⌕</span>
<input type="search" placeholder="Søg vare" aria-label="Søg vare"
@bind="_searchText" @bind:event="oninput" />
</label>
<FluentButton Class="create-button" Appearance="@ButtonAppearance.Primary"
OnClick="OpenCreateDialog">
<span class="button-icon" aria-hidden="true"></span> Opret vare
</FluentButton>
<FluentButton Class="save-order-button" Appearance="@ButtonAppearance.Outline"
Disabled="@(!_orderHasChanged)" OnClick="SaveOrder">
<span class="button-icon" aria-hidden="true">⇅</span> Gem rækkefølge
</FluentButton>
</div>
@if (_isLoading)
{
<FluentSpinner />
}
else
{
<div class="products-table">
<div class="products-row products-column-header">
<span>Rækkefølge</span><span>Navn</span><span>Moms</span><span>Status</span><span>Handling</span>
</div>
<div class="products-list">
@foreach (ProductGroupEntity productGroup in FilteredProductGroups)
{
<div class="products-row product-row @GetDragClass(productGroup)" @key="productGroup.Id"
draggable="true" @ondragstart="@(() => StartDragging(productGroup))"
@ondragover:preventDefault="true" @ondrop="@(() => DropProductGroup(productGroup))"
@ondragend="StopDragging">
<span class="drag-handle" title="Træk for at ændre rækkefølge">⠿</span>
<span class="product-name">@productGroup.Name</span>
<span>@productGroup.VatRate.ToString("0.##") %</span>
<span class="active-status">Aktiv</span>
<span class="row-actions">
<FluentButton Appearance="@ButtonAppearance.Outline"
OnClick="@(() => OpenEditDialog(productGroup))">Redigér</FluentButton>
<FluentButton Class="delete-button" Appearance="@ButtonAppearance.Outline"
OnClick="@(() => ConfirmDeactivation(productGroup))">Slet</FluentButton>
</span>
</div>
}
@if (!FilteredProductGroups.Any())
{
<p class="empty-state">Ingen varer matcher din søgning.</p>
}
</div>
</div>
}
<p class="products-help"><span aria-hidden="true">ⓘ</span>
Træk i håndtaget for at ændre rækkefølgen. Deaktiverede varer vises ikke i kassesalget.
</p>
</div>
<ProductGroupDialog @bind-IsOpen="_showProductGroupDialog"
ProductGroup="_productGroupToEdit"
ProductGroupSaved="ProductGroupSaved" />

View File

@@ -0,0 +1,93 @@
using Database.Models;
using Microsoft.FluentUI.AspNetCore.Components;
namespace Pos.Web.Components.Pages.Settings
{
public partial class Products
{
private List<ProductGroupEntity> _productGroups = [];
private ProductGroupEntity? _draggedProductGroup;
private ProductGroupEntity? _productGroupToEdit;
private string _searchText = string.Empty;
private bool _isLoading = true;
private bool _showProductGroupDialog;
private bool _orderHasChanged;
private IEnumerable<ProductGroupEntity> FilteredProductGroups =>
_productGroups.Where(productGroup =>
string.IsNullOrWhiteSpace(_searchText) ||
productGroup.Name.Contains(_searchText, StringComparison.CurrentCultureIgnoreCase));
protected override void OnInitialized()
{
LoadProductGroups();
_isLoading = false;
}
private void LoadProductGroups() =>
_productGroups = ProductGroupService.GetProductGroups();
private void OpenCreateDialog()
{
_productGroupToEdit = null;
_showProductGroupDialog = true;
}
private void OpenEditDialog(ProductGroupEntity productGroup)
{
_productGroupToEdit = productGroup;
_showProductGroupDialog = true;
}
private void ProductGroupSaved()
{
LoadProductGroups();
_orderHasChanged = false;
}
private void StartDragging(ProductGroupEntity productGroup) =>
_draggedProductGroup = productGroup;
private void DropProductGroup(ProductGroupEntity target)
{
if (_draggedProductGroup == null || _draggedProductGroup.Id == target.Id ||
!string.IsNullOrWhiteSpace(_searchText))
{
StopDragging();
return;
}
int oldIndex = _productGroups.IndexOf(_draggedProductGroup);
int newIndex = _productGroups.IndexOf(target);
_productGroups.RemoveAt(oldIndex);
_productGroups.Insert(newIndex, _draggedProductGroup);
_orderHasChanged = true;
StopDragging();
}
private void StopDragging() => _draggedProductGroup = null;
private string GetDragClass(ProductGroupEntity productGroup) =>
_draggedProductGroup?.Id == productGroup.Id ? "dragging" : string.Empty;
private void SaveOrder()
{
ProductGroupService.UpdateOrder(_productGroups.Select(item => item.Id).ToList());
_orderHasChanged = false;
}
private async Task ConfirmDeactivation(ProductGroupEntity productGroup)
{
DialogResult result = await DialogService.ShowConfirmationAsync(
$"Er du sikker på, at du vil slette {productGroup.Name}?",
"Bekræft sletning", "Slet", "Annuller");
if (!result.Cancelled)
{
ProductGroupService.ArchiveProductGroup(productGroup.Id);
LoadProductGroups();
_orderHasChanged = false;
}
}
}
}

View File

@@ -0,0 +1,24 @@
.products-page { width: min(100%, 1400px); margin: 0 auto; padding: 32px; }
.products-header { padding-bottom: 20px; border-bottom: 1px solid #e2e8f0; }
.products-header h1 { margin: 0; }
.products-toolbar { display: grid; grid-template-columns: minmax(280px, 1.6fr) minmax(210px, .8fr) minmax(230px, .9fr); gap: 32px; margin: 32px 0; }
.search-field { display: flex; align-items: center; gap: 16px; box-sizing: border-box; height: 56px; padding: 0 20px; border: 1px solid #cbd5e1; border-radius: 8px; color: #475569; }
.search-field span { font-size: 30px; }
.search-field input { width: 100%; border: 0; outline: 0; background: transparent; color: inherit; font: inherit; font-size: 18px; }
.products-toolbar ::deep fluent-button { width: 100%; min-height: 56px; font-size: 18px; }
.button-icon { margin-right: 10px; font-size: 26px; }
.products-table { overflow: hidden; border: 1px solid #cbd5e1; border-radius: 10px; }
.products-list { max-height: 535px; overflow-y: auto; }
.products-row { display: grid; grid-template-columns: 150px minmax(190px, 1.3fr) 110px 120px minmax(250px, 1fr); align-items: center; min-height: 76px; padding: 0 32px; border-bottom: 1px solid #e2e8f0; }
.products-column-header { min-height: 56px; color: #334155; font-weight: 600; }
.product-row:last-child { border-bottom: 0; }
.product-row.dragging { opacity: .45; }
.drag-handle { width: fit-content; color: #64748b; cursor: grab; font-size: 30px; }
.product-name { font-size: 19px; }
.active-status { color: #16a34a; }
.row-actions { display: flex; gap: 12px; }
.products-page ::deep .delete-button { color: #d13438; border-color: #d13438; }
.products-help { display: flex; gap: 12px; margin: 32px 6px 0; color: #64748b; }
.products-help span { font-size: 21px; }
.empty-state { margin: 0; padding: 36px; color: #64748b; text-align: center; }
@media (max-width: 900px) { .products-toolbar { grid-template-columns: 1fr; gap: 12px; } .products-table { overflow-x: auto; } .products-row { min-width: 900px; } }

View File

@@ -0,0 +1,18 @@
@rendermode InteractiveServer
@inject NavigationManager Nav
@page "/settings"
<Header />
<div class="settings-page">
<h1>Indstillinger</h1>
<p>Administrer data, der bruges i kassesalget.</p>
<div class="settings-grid">
<FluentButton Class="settings-button" onclick="@(() => Nav.NavigateTo("/settings/employees"))">
Medarbejder
</FluentButton>
<FluentButton Class="settings-button" onclick="@(() => Nav.NavigateTo("/settings/products"))">
Vare
</FluentButton>
</div>
</div>

View File

@@ -0,0 +1,36 @@
.settings-page {
width: min(100%, 1100px);
margin: 0 auto;
padding: 32px;
}
.settings-page h1 {
margin-bottom: 12px;
color: var(--colorNeutralForeground1);
}
.settings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
margin-top: 32px;
}
.settings-grid ::deep .settings-button {
width: 100%;
min-height: 220px;
font-size: 28px;
font-weight: 600;
}
@media (max-width: 680px) {
.settings-page {
padding: 24px 16px;
}
.settings-grid {
grid-template-columns: 1fr;
}
}

Some files were not shown because too many files have changed in this diff Show More