Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4623750255 | |||
| bde55cf8d5 | |||
| 770e170d0e | |||
| 8de6ff9338 | |||
|
|
048f45a4a1 | ||
|
|
01d81cf14f | ||
|
|
49a9f336b1 | ||
| 8953e917e1 | |||
| 6e14130a9c | |||
| f01f05e126 | |||
|
|
c1695b4967 | ||
|
|
205d27c6c7 | ||
|
|
03594bba3f | ||
|
|
2e537337f5 | ||
|
|
9497afe262 | ||
|
|
6df185d519 | ||
|
|
97688542fe | ||
|
|
236eb6792c | ||
|
|
d02414edbb | ||
| be0350f458 | |||
| 3414400efe | |||
| 6d55a6e840 | |||
| a30bd769fb | |||
| 9e077f6993 | |||
| 83d85daa29 | |||
| 5de0f416ba | |||
| 26c6c651b1 | |||
| 547e574e6a |
35
PointOfSale/.dockerignore
Normal file
35
PointOfSale/.dockerignore
Normal 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/
|
||||
|
||||
7
PointOfSale/.gitignore
vendored
7
PointOfSale/.gitignore
vendored
@@ -360,3 +360,10 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# Local Docker database data and secrets
|
||||
.env
|
||||
database-backup/
|
||||
|
||||
database-data/
|
||||
|
||||
|
||||
@@ -20,4 +20,8 @@
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Pos.ServiceDefaults\Pos.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"EpsonPrinter": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
32
PointOfSale/IntegrationTests/IntegrationTests.csproj
Normal file
32
PointOfSale/IntegrationTests/IntegrationTests.csproj
Normal 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>
|
||||
29
PointOfSale/IntegrationTests/PosDbContextTests.cs
Normal file
29
PointOfSale/IntegrationTests/PosDbContextTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
3
PointOfSale/IntegrationTests/appsettings.json
Normal file
3
PointOfSale/IntegrationTests/appsettings.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"SqlServer": "Server=(localdb)\\MSSQLLocalDB;Database=pointofsale;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||
}
|
||||
11
PointOfSale/Pos.AppHost/AppHost.cs
Normal file
11
PointOfSale/Pos.AppHost/AppHost.cs
Normal 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();
|
||||
16
PointOfSale/Pos.AppHost/Pos.AppHost.csproj
Normal file
16
PointOfSale/Pos.AppHost/Pos.AppHost.csproj
Normal 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>
|
||||
31
PointOfSale/Pos.AppHost/Properties/launchSettings.json
Normal file
31
PointOfSale/Pos.AppHost/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
PointOfSale/Pos.AppHost/appsettings.Development.json
Normal file
8
PointOfSale/Pos.AppHost/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
PointOfSale/Pos.AppHost/appsettings.json
Normal file
9
PointOfSale/Pos.AppHost/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Aspire.Hosting.Dcp": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,4 @@ public enum PaymentMethod
|
||||
Card,
|
||||
GiftCard,
|
||||
MobilePay
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
9
PointOfSale/Pos.Models/Pos.Models.csproj
Normal file
9
PointOfSale/Pos.Models/Pos.Models.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -8,9 +8,10 @@ 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 decimal VatRate { get; set; }
|
||||
public int Id { get; set; }
|
||||
public bool IsProductGroup { get; set; }
|
||||
}
|
||||
11
PointOfSale/Pos.Models/ReceiptDetailModel.cs
Normal file
11
PointOfSale/Pos.Models/ReceiptDetailModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
10
PointOfSale/Pos.Models/ReceiptLineModel.cs
Normal file
10
PointOfSale/Pos.Models/ReceiptLineModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
11
PointOfSale/Pos.Models/ReceiptSummaryModel.cs
Normal file
11
PointOfSale/Pos.Models/ReceiptSummaryModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
11
PointOfSale/Pos.Models/TotalSaleDetail.cs
Normal file
11
PointOfSale/Pos.Models/TotalSaleDetail.cs
Normal 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();
|
||||
}
|
||||
7
PointOfSale/Pos.Models/TotalSalePayment.cs
Normal file
7
PointOfSale/Pos.Models/TotalSalePayment.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Pos.Models;
|
||||
|
||||
public class TotalSalePayment
|
||||
{
|
||||
public string PaymentMethod { get; set; } = string.Empty;
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
@@ -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()
|
||||
67
PointOfSale/Pos.Service/EmployeeService.cs
Normal file
67
PointOfSale/Pos.Service/EmployeeService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
PointOfSale/Pos.Service/IProductService.cs
Normal file
14
PointOfSale/Pos.Service/IProductService.cs
Normal 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);
|
||||
}
|
||||
19
PointOfSale/Pos.Service/Pos.Service.csproj
Normal file
19
PointOfSale/Pos.Service/Pos.Service.csproj
Normal 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>
|
||||
112
PointOfSale/Pos.Service/ProductGroupService.cs
Normal file
112
PointOfSale/Pos.Service/ProductGroupService.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
97
PointOfSale/Pos.Service/ProductService.cs
Normal file
97
PointOfSale/Pos.Service/ProductService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
74
PointOfSale/Pos.Service/SaleHistoryService.cs
Normal file
74
PointOfSale/Pos.Service/SaleHistoryService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
178
PointOfSale/Pos.Service/SalesTransactionService.cs
Normal file
178
PointOfSale/Pos.Service/SalesTransactionService.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
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,
|
||||
decimal vatRate = 25m)
|
||||
{
|
||||
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);
|
||||
|
||||
if (vatRate < 0 || vatRate > 100)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(vatRate),
|
||||
"Momsen skal være mellem 0 og 100 procent.");
|
||||
|
||||
PriceLineModel saleLine = new PriceLineModel
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
NumberProducts = numberOfProducts,
|
||||
Price = price,
|
||||
VatRate = vatRate,
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
PointOfSale/Pos.ServiceDefaults/Extensions.cs
Normal file
127
PointOfSale/Pos.ServiceDefaults/Extensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
22
PointOfSale/Pos.ServiceDefaults/Pos.ServiceDefaults.csproj
Normal file
22
PointOfSale/Pos.ServiceDefaults/Pos.ServiceDefaults.csproj
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -58,3 +58,7 @@ namespace Pos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
//}
|
||||
23
PointOfSale/Pos.Web/Components/App.razor
Normal file
23
PointOfSale/Pos.Web/Components/App.razor
Normal 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>
|
||||
|
||||
16
PointOfSale/Pos.Web/Components/Layout/MainLayout.razor
Normal file
16
PointOfSale/Pos.Web/Components/Layout/MainLayout.razor
Normal 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>
|
||||
31
PointOfSale/Pos.Web/Components/Layout/ReconnectModal.razor
Normal file
31
PointOfSale/Pos.Web/Components/Layout/ReconnectModal.razor
Normal 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>
|
||||
157
PointOfSale/Pos.Web/Components/Layout/ReconnectModal.razor.css
Normal file
157
PointOfSale/Pos.Web/Components/Layout/ReconnectModal.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
36
PointOfSale/Pos.Web/Components/Pages/Error.razor
Normal file
36
PointOfSale/Pos.Web/Components/Pages/Error.razor
Normal 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;
|
||||
}
|
||||
21
PointOfSale/Pos.Web/Components/Pages/Home.razor
Normal file
21
PointOfSale/Pos.Web/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,21 @@
|
||||
@rendermode InteractiveServer
|
||||
@inject NavigationManager Nav
|
||||
@page "/"
|
||||
|
||||
<Header/>
|
||||
|
||||
|
||||
<div class="home-container">
|
||||
<FluentButton Class="menu-button" Size="ButtonSize.Large" onclick="@(() => Nav.NavigateTo("/sale"))">
|
||||
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>
|
||||
23
PointOfSale/Pos.Web/Components/Pages/Home.razor.css
Normal file
23
PointOfSale/Pos.Web/Components/Pages/Home.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
5
PointOfSale/Pos.Web/Components/Pages/NotFound.razor
Normal file
5
PointOfSale/Pos.Web/Components/Pages/NotFound.razor
Normal 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>
|
||||
144
PointOfSale/Pos.Web/Components/Pages/Sale/Sale.razor
Normal file
144
PointOfSale/Pos.Web/Components/Pages/Sale/Sale.razor
Normal file
@@ -0,0 +1,144 @@
|
||||
@page "/sale"
|
||||
@rendermode InteractiveServer
|
||||
@using Database.Models
|
||||
@using Pos.Models
|
||||
@using Pos.Service
|
||||
@inject EmployeeService EmployeeService
|
||||
@inject ProductGroupService ProductGroupService
|
||||
@inject SalesTransactionService TransactionService
|
||||
@inject SaleService SaleService
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<main class="sale-page">
|
||||
<section class="current-sale">
|
||||
<div class="sale-title">
|
||||
<button class="back-button"
|
||||
type="button"
|
||||
title="Tilbage til forsiden"
|
||||
aria-label="Tilbage til forsiden"
|
||||
@onclick="@(() => Nav.NavigateTo("/"))">←</button>
|
||||
<h1>Aktuelt salg</h1>
|
||||
</div>
|
||||
|
||||
<div class="sale-lines-header sale-line-grid">
|
||||
<span>Vare</span><span>Antal</span><span>Pris</span><span>Moms</span><span>I alt</span>
|
||||
</div>
|
||||
|
||||
<div class="sale-lines">
|
||||
@foreach (PriceLineModel line in TransactionService.SaleLines)
|
||||
{
|
||||
<div class="sale-line sale-line-grid">
|
||||
<span>@line.Name</span>
|
||||
<span>@line.NumberProducts</span>
|
||||
<span>@Currency(line.Price)</span>
|
||||
<span>@line.VatRate.ToString("0.##") %</span>
|
||||
<span>@Currency(line.Price * line.NumberProducts)</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TransactionService.SaleLines.Count == 0)
|
||||
{
|
||||
<p class="empty-sale">Vælg en varegruppe og tilføj den til salget.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="sale-summary">
|
||||
<span>Total</span>
|
||||
<strong>@Currency(TransactionService.TotalPrice)</strong>
|
||||
</div>
|
||||
|
||||
<div class="registered-payments">
|
||||
<h2>Registrerede betalinger</h2>
|
||||
@if (_payments.Count == 0)
|
||||
{
|
||||
<p class="empty-payments">Der er endnu ikke registreret betalinger.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (RegisteredPayment payment in _payments)
|
||||
{
|
||||
<div class="payment-row">
|
||||
<span class="payment-check">✓</span>
|
||||
<span>@payment.Method</span>
|
||||
<span>@Currency(payment.Amount)</span>
|
||||
<time>@payment.Time.ToString("HH:mm")</time>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<button class="finish-button" type="button"
|
||||
disabled="@(!TransactionService.IsFullyPaid || _selectedEmployeeId <= 0)"
|
||||
@onclick="CompleteSale">
|
||||
Afslut salg
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="sale-controls">
|
||||
<div class="selection-panel">
|
||||
<label class="employee-field">
|
||||
<span>Medarbejder</span>
|
||||
<select @bind="_selectedEmployeeId">
|
||||
<option value="0">Vælg medarbejder</option>
|
||||
@foreach (EmployeeEntity employee in _employees)
|
||||
{
|
||||
<option value="@employee.Id">@employee.Name</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<h2>Varegruppe</h2>
|
||||
<div class="product-groups">
|
||||
@foreach (ProductGroupEntity productGroup in _productGroups)
|
||||
{
|
||||
<button type="button"
|
||||
class="product-group-button @(productGroup.Id == _selectedProductGroupId ? "selected" : null)"
|
||||
disabled="@(_selectedEmployeeId <= 0)"
|
||||
@onclick="@(() => SelectProductGroup(productGroup.Id))">
|
||||
@productGroup.Name
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manual-product">
|
||||
<h2>Tilføj vare manuelt</h2>
|
||||
<div class="manual-grid">
|
||||
<label><span>Antal</span>
|
||||
<input type="number" min="1" step="1" @bind="_quantity" />
|
||||
</label>
|
||||
<label><span>Pris</span>
|
||||
<div class="price-input"><input type="number" min="0" step="0.01" @bind="_price" /><span>kr.</span></div>
|
||||
</label>
|
||||
<label><span>Linjetotal</span>
|
||||
<output>@Currency(LineTotal)</output>
|
||||
</label>
|
||||
<button class="add-button" type="button" disabled="@(!CanAddLine)" @onclick="AddSaleLine">Tilføj</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-panel">
|
||||
<h2>Betaling</h2>
|
||||
<label class="paid-amount"><span>Betalt beløb</span>
|
||||
<div><input type="number" min="0" step="0.01" placeholder="@RemainingAmountText" @bind="_paymentAmount" /><span>kr.</span></div>
|
||||
</label>
|
||||
|
||||
<div class="payment-buttons">
|
||||
<button type="button" disabled="@(TransactionService.TotalPrice <= 0)" @onclick="@(() => AddPayment(PaymentMethod.Cash))">▣ <span>Kontant</span></button>
|
||||
<button type="button" disabled="@(TransactionService.TotalPrice <= 0)" @onclick="@(() => AddPayment(PaymentMethod.Card))">▤ <span>Kort</span></button>
|
||||
<button type="button" disabled="@(TransactionService.TotalPrice <= 0)" @onclick="@(() => AddPayment(PaymentMethod.MobilePay))">▯ <span>MobilePay</span></button>
|
||||
</div>
|
||||
|
||||
<div class="change-row @(TransactionService.Change > 0 ? "has-change" : null)">
|
||||
<span>Byttepenge</span><strong>@Currency(TransactionService.Change)</strong>
|
||||
</div>
|
||||
|
||||
<button class="print-button" type="button" @onclick="PrintReceipt">▣ <span>Print bon</span></button>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_message))
|
||||
{
|
||||
<p class="sale-message @(_isError ? "error" : "success")" role="status">@_message</p>
|
||||
}
|
||||
</section>
|
||||
</main>
|
||||
148
PointOfSale/Pos.Web/Components/Pages/Sale/Sale.razor.cs
Normal file
148
PointOfSale/Pos.Web/Components/Pages/Sale/Sale.razor.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System.Globalization;
|
||||
using Database.Models;
|
||||
using Pos.Models;
|
||||
|
||||
namespace Pos.Web.Components.Pages.Sale
|
||||
{
|
||||
public partial class Sale
|
||||
{
|
||||
private static readonly CultureInfo Danish = CultureInfo.GetCultureInfo("da-DK");
|
||||
private List<EmployeeEntity> _employees = [];
|
||||
private List<ProductGroupEntity> _productGroups = [];
|
||||
private readonly List<RegisteredPayment> _payments = [];
|
||||
private int _selectedEmployeeId;
|
||||
private int _selectedProductGroupId;
|
||||
private int _quantity = 1;
|
||||
private decimal? _price;
|
||||
private decimal? _paymentAmount;
|
||||
private string _message = string.Empty;
|
||||
private bool _isError;
|
||||
|
||||
private decimal LineTotal => (_price ?? 0) * _quantity;
|
||||
private bool CanAddLine => _selectedProductGroupId > 0 && _quantity > 0 && _price >= 0;
|
||||
private string RemainingAmountText => TransactionService.RemainingAmount.ToString("0.00", Danish);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_employees = await EmployeeService.GetEmployeesAsync();
|
||||
_productGroups = ProductGroupService.GetProductGroups();
|
||||
_selectedEmployeeId = _employees.FirstOrDefault()?.Id ?? 0;
|
||||
}
|
||||
|
||||
private void SelectProductGroup(int productGroupId)
|
||||
{
|
||||
_selectedProductGroupId = productGroupId;
|
||||
ClearMessage();
|
||||
}
|
||||
|
||||
private void AddSaleLine()
|
||||
{
|
||||
ProductGroupEntity? productGroup = _productGroups
|
||||
.FirstOrDefault(item => item.Id == _selectedProductGroupId);
|
||||
|
||||
if (productGroup == null || !CanAddLine)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
TransactionService.AddSaleLine(
|
||||
productGroup.Id,
|
||||
productGroup.Name,
|
||||
_quantity,
|
||||
_price ?? 0,
|
||||
true,
|
||||
productGroup.VatRate);
|
||||
|
||||
_quantity = 1;
|
||||
_price = null;
|
||||
_selectedProductGroupId = 0;
|
||||
ClearMessage();
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
ShowError(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPayment(PaymentMethod paymentMethod)
|
||||
{
|
||||
try
|
||||
{
|
||||
AmountGridModel payment = TransactionService.AddPayment(paymentMethod, _paymentAmount);
|
||||
_payments.Add(new RegisteredPayment(payment.PaymentMethodText, payment.Amount, DateTime.Now));
|
||||
_paymentAmount = null;
|
||||
ClearMessage();
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
|
||||
{
|
||||
ShowError(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteSale()
|
||||
{
|
||||
try
|
||||
{
|
||||
TransactionService.EnsureCanComplete();
|
||||
SaleService.Save(
|
||||
TransactionService.SaleLines.Select(ToSaleGridModel).ToList(),
|
||||
TransactionService.Payments.ToList(),
|
||||
_selectedEmployeeId);
|
||||
|
||||
TransactionService.Reset();
|
||||
_payments.Clear();
|
||||
_selectedProductGroupId = 0;
|
||||
_paymentAmount = null;
|
||||
_message = "Salget er afsluttet og gemt.";
|
||||
_isError = false;
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
|
||||
{
|
||||
ShowError(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintReceipt()
|
||||
{
|
||||
try
|
||||
{
|
||||
SaleService.PrintReceipt();
|
||||
_message = "Bonen er sendt til printeren.";
|
||||
_isError = false;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ShowError(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static SaleGridModel ToSaleGridModel(PriceLineModel line)
|
||||
{
|
||||
return new SaleGridModel
|
||||
{
|
||||
Navn = line.Name,
|
||||
Stk = line.NumberProducts.ToString(Danish),
|
||||
Pris = Currency(line.Price),
|
||||
Total = Currency(line.Price * line.NumberProducts),
|
||||
SaleGridInternalModel = new SaleGridInternalModel
|
||||
{
|
||||
InternPrice = line.Price,
|
||||
InternPieces = line.NumberProducts
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string Currency(decimal amount) =>
|
||||
$"{amount.ToString("N2", Danish)} kr.";
|
||||
|
||||
private void ClearMessage() => _message = string.Empty;
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
_message = message;
|
||||
_isError = true;
|
||||
}
|
||||
|
||||
private sealed record RegisteredPayment(string Method, decimal Amount, DateTime Time);
|
||||
}
|
||||
}
|
||||
569
PointOfSale/Pos.Web/Components/Pages/Sale/Sale.razor.css
Normal file
569
PointOfSale/Pos.Web/Components/Pages/Sale/Sale.razor.css
Normal file
@@ -0,0 +1,569 @@
|
||||
.sale-page {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(420px, .92fr) minmax(560px, 1.28fr);
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
min-height: 720px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--colorNeutralStroke2);
|
||||
background: var(--colorNeutralBackground1);
|
||||
}
|
||||
|
||||
.current-sale,
|
||||
.sale-controls {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.current-sale {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
border-right: 1px solid var(--colorNeutralStroke2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sale-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--colorNeutralStroke1);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--colorNeutralForeground1);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: var(--colorNeutralBackground1Hover);
|
||||
}
|
||||
|
||||
.back-button:focus-visible {
|
||||
outline: 2px solid var(--colorStrokeFocus2);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sale-line-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 50px 82px 58px 90px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sale-lines-header {
|
||||
padding: 0 8px 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sale-lines-header span:not(:first-child),
|
||||
.sale-line span:not(:first-child) {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sale-line-grid > span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sale-line > span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sale-lines {
|
||||
min-height: 280px;
|
||||
max-height: 42dvh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-top: 1px solid var(--colorNeutralStroke2);
|
||||
}
|
||||
|
||||
.sale-line {
|
||||
min-height: 50px;
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid var(--colorNeutralStroke2);
|
||||
}
|
||||
|
||||
.empty-sale,
|
||||
.empty-payments {
|
||||
color: var(--colorNeutralForeground3);
|
||||
}
|
||||
|
||||
.empty-sale {
|
||||
margin: 30px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sale-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 8px;
|
||||
border-top: 1px solid var(--colorNeutralStroke2);
|
||||
border-bottom: 1px solid var(--colorNeutralStroke2);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.sale-summary strong {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.registered-payments {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.payment-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 1fr 120px 60px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-height: 52px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--colorNeutralStroke2);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.payment-row:last-child {
|
||||
border-bottom: 1px solid var(--colorNeutralStroke2);
|
||||
}
|
||||
|
||||
.payment-row > :nth-child(3),
|
||||
.payment-row time {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.payment-check {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid #16a34a;
|
||||
border-radius: 50%;
|
||||
color: #16a34a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.finish-button {
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
margin-top: auto;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: #0f5bd3;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.finish-button:disabled {
|
||||
background: #b7c9e8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sale-controls {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) auto auto;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.selection-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selection-panel,
|
||||
.manual-product,
|
||||
.payment-panel {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--colorNeutralStroke2);
|
||||
}
|
||||
|
||||
.employee-field {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.employee-field select,
|
||||
input,
|
||||
output {
|
||||
box-sizing: border-box;
|
||||
height: 42px;
|
||||
border: 1px solid var(--colorNeutralStroke1);
|
||||
border-radius: 5px;
|
||||
background: var(--colorNeutralBackground1);
|
||||
color: var(--colorNeutralForeground1);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.employee-field select,
|
||||
input {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.product-groups {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 9px 14px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.product-group-button {
|
||||
min-height: 42px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--colorNeutralStroke2);
|
||||
border-radius: 4px;
|
||||
background: var(--colorNeutralBackground1);
|
||||
color: var(--colorNeutralForeground1);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.product-group-button:hover:not(:disabled) {
|
||||
background: var(--colorNeutralBackground1Hover);
|
||||
}
|
||||
|
||||
.product-group-button.selected {
|
||||
border-color: #0f5bd3;
|
||||
background: #0f5bd3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.product-group-button:disabled {
|
||||
opacity: .55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.manual-grid {
|
||||
display: grid;
|
||||
grid-template-columns: .8fr 1fr 1fr .8fr;
|
||||
gap: 24px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.manual-grid label,
|
||||
.paid-amount {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.price-input,
|
||||
.paid-amount > div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.price-input input,
|
||||
.paid-amount input {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
output {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
background: var(--colorNeutralBackground2);
|
||||
}
|
||||
|
||||
.add-button,
|
||||
.payment-buttons button,
|
||||
.print-button {
|
||||
min-height: 44px;
|
||||
border: 1px solid #0f5bd3;
|
||||
border-radius: 5px;
|
||||
background: var(--colorNeutralBackground1);
|
||||
color: #0f5bd3;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-button:disabled,
|
||||
.payment-buttons button:disabled {
|
||||
border-color: var(--colorNeutralStroke2);
|
||||
color: var(--colorNeutralForegroundDisabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.paid-amount > div {
|
||||
grid-template-columns: 1fr auto;
|
||||
height: 58px;
|
||||
padding-right: 14px;
|
||||
border: 1px solid var(--colorNeutralStroke1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.paid-amount input {
|
||||
height: 56px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.payment-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.payment-buttons button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 52px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.payment-buttons button span {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.change-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 58px;
|
||||
margin-top: 14px;
|
||||
padding: 0 18px;
|
||||
border-radius: 4px;
|
||||
background: var(--colorNeutralBackground3);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.change-row strong {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.change-row.has-change {
|
||||
background: #e30000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.print-button {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
.print-button span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.sale-message {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 76px;
|
||||
left: 24px;
|
||||
z-index: 2;
|
||||
margin: 0;
|
||||
padding: 10px 14px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.sale-message.error {
|
||||
background: #fde7e9;
|
||||
color: #b10e1e;
|
||||
}
|
||||
|
||||
.sale-message.success {
|
||||
background: #dff6dd;
|
||||
color: #107c10;
|
||||
}
|
||||
|
||||
@media (max-width: 1050px) {
|
||||
.sale-page {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.current-sale {
|
||||
min-height: 700px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--colorNeutralStroke2);
|
||||
}
|
||||
|
||||
.sale-controls {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.product-groups {
|
||||
max-height: 275px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet i liggende format, f.eks. 1280 x 800. */
|
||||
@media (min-width: 1051px) and (max-width: 1400px) and (max-height: 850px) {
|
||||
.sale-page {
|
||||
grid-template-columns: minmax(440px, .9fr) minmax(600px, 1.25fr);
|
||||
min-height: 640px;
|
||||
}
|
||||
|
||||
.current-sale {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.sale-title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sale-lines {
|
||||
min-height: 210px;
|
||||
max-height: 34dvh;
|
||||
}
|
||||
|
||||
.sale-summary {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.registered-payments {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.payment-row {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.finish-button {
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.selection-panel,
|
||||
.manual-product,
|
||||
.payment-panel {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.employee-field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.product-groups {
|
||||
gap: 7px 10px;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.product-group-button {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.manual-grid {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.employee-field select,
|
||||
input,
|
||||
output,
|
||||
.add-button {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.paid-amount > div,
|
||||
.paid-amount input {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.paid-amount input {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.payment-buttons {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.payment-buttons button {
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.change-row {
|
||||
min-height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.change-row strong {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.print-button {
|
||||
min-height: 46px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.sale-page {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.current-sale,
|
||||
.selection-panel,
|
||||
.manual-product,
|
||||
.payment-panel {
|
||||
padding: 18px 14px;
|
||||
}
|
||||
|
||||
.product-groups,
|
||||
.manual-grid,
|
||||
.payment-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.sale-line-grid {
|
||||
grid-template-columns: minmax(0, 1fr) 40px 70px 50px 74px;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
×
|
||||
</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>
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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">×</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>
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
77
PointOfSale/Pos.Web/Components/Pages/Settings/Products.razor
Normal file
77
PointOfSale/Pos.Web/Components/Pages/Settings/Products.razor
Normal 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" />
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user