18 Commits

Author SHA1 Message Date
770e170d0e Addet a backbutton to home page and fix the pr error 2026-08-27 10:18:48 +02:00
8de6ff9338 Addet VateRate to produktPage 2026-08-27 10:13:08 +02:00
Alex Pedersen
048f45a4a1 Addet docker support 2026-08-06 17:05:25 +02:00
Alex Pedersen
01d81cf14f Merge branch 'feature/blazor' of https://git.maximuss.dk/maximuss/point_of_sale_v2 into feature/blazor 2026-08-06 13:43:16 +02:00
Alex Pedersen
49a9f336b1 Addet ToDaySales page 2026-08-06 13:43:04 +02:00
8953e917e1 Merge pull request 'feature/blazor' (#3) from feature/blazor into development
Reviewed-on: #3
2026-08-03 11:50:52 +00:00
6e14130a9c Finsih Employyepage 2026-08-03 11:01:37 +02:00
f01f05e126 chance database connection string 2026-08-03 10:02:58 +02:00
Alex Pedersen
c1695b4967 Addet Employye get logic 2026-08-03 09:55:41 +02:00
Alex Pedersen
205d27c6c7 Addet employee frontend and move header to a componet 2026-07-31 14:07:43 +02:00
Alex Pedersen
03594bba3f Made home page and startet the layout for settings home page 2026-07-30 15:21:59 +02:00
Alex Pedersen
2e537337f5 Addet aspire support pos.web and also addet the pictures for layout 2026-07-30 13:22:33 +02:00
Alex Pedersen
9497afe262 Rework so employee dont use repos but servies 2026-07-30 10:23:27 +02:00
Alex Pedersen
6df185d519 Move logic out the the class libs 2026-07-30 10:11:54 +02:00
Alex Pedersen
97688542fe moved logic out of SalesWindows.xaml.cs to SaleTransactionServce 2026-07-30 09:41:47 +02:00
Alex Pedersen
236eb6792c addet pos.models and pos.service 2026-07-30 09:22:48 +02:00
Alex Pedersen
d02414edbb modified sln to run the brach 2026-07-30 08:43:30 +02:00
be0350f458 Fluent Blazor web oprettet 2026-07-28 19:53:59 +02:00
119 changed files with 3224 additions and 336 deletions

35
PointOfSale/.dockerignore Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Pos.Ui\Database\Database.csproj" /> <ProjectReference Include="..\EpsonPrinterLinux\EpsonPrinter.csproj" />
<ProjectReference Include="..\Pos.Web\Pos.Web.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using Database.Models;
namespace Pos.Service;
public interface IProductService
{
List<ProductEntity> GetProducts(int productGroupId);
ProductEntity GetProduct(int productId);
void AddProduct(string name, int productGroupId);
void UpdateProduct(int productId, int productGroupId, string name);
void ArchiveProduct(int productId);
void UpdateOrder(int productGroupId, IReadOnlyList<int> orderedProductIds);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Pos.Models;
namespace Pos.Service
{
/// <summary>
/// Holder styr på den igangværende handel og dens beregninger.
/// Servicen har ingen afhængighed til WPF eller web.
/// </summary>
public class SalesTransactionService
{
private readonly List<PriceLineModel> _saleLines = new List<PriceLineModel>();
private readonly List<AmountGridModel> _payments = new List<AmountGridModel>();
public IReadOnlyList<PriceLineModel> SaleLines => _saleLines;
public IReadOnlyList<AmountGridModel> Payments => _payments;
public decimal TotalPrice =>
_saleLines.Sum(line =>
CalculateLineTotal(line.Price, line.NumberProducts));
public decimal AmountPaid =>
_payments.Sum(payment => payment.Amount);
public decimal RemainingAmount =>
Math.Max(TotalPrice - AmountPaid, 0);
public decimal Change =>
Math.Max(AmountPaid - TotalPrice, 0);
public bool IsFullyPaid =>
TotalPrice > 0 && AmountPaid >= TotalPrice;
/// <summary>
/// Beregner totalen for en enkelt salgslinje.
/// Parsing og visning af resultatet er fortsat UI-ansvar.
/// </summary>
public decimal CalculateLineTotal(decimal price, int numberOfProducts)
{
ValidatePriceAndQuantity(price, numberOfProducts);
return price * numberOfProducts;
}
public PriceLineModel AddSaleLine(
int id,
string name,
int numberOfProducts,
decimal price,
bool isProductGroup)
{
if (id <= 0)
throw new ArgumentOutOfRangeException(
nameof(id),
"Id skal være større end 0.");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(
"Navn skal være udfyldt.",
nameof(name));
ValidatePriceAndQuantity(price, numberOfProducts);
PriceLineModel saleLine = new PriceLineModel
{
Id = id,
Name = name,
NumberProducts = numberOfProducts,
Price = price,
IsProductGroup = isProductGroup
};
_saleLines.Add(saleLine);
return saleLine;
}
public void RemoveSaleLineAt(int index)
{
if (index < 0 || index >= _saleLines.Count)
throw new ArgumentOutOfRangeException(nameof(index));
_saleLines.RemoveAt(index);
}
/// <summary>
/// Registrerer en betaling. Hvis amount er null, betales restbeløbet.
/// </summary>
public AmountGridModel AddPayment(
PaymentMethod paymentMethod,
decimal? amount = null)
{
if (_saleLines.Count == 0)
throw new InvalidOperationException(
"Der kan ikke registreres betaling på et tomt salg.");
decimal paymentAmount = amount ?? RemainingAmount;
if (paymentAmount <= 0)
throw new ArgumentOutOfRangeException(
nameof(amount),
"Betalingsbeløbet skal være større end 0.");
AmountGridModel payment = new AmountGridModel
{
Amount = paymentAmount,
PaymentMethodText = GetPaymentMethodText(paymentMethod)
};
_payments.Add(payment);
return payment;
}
public void RemovePaymentAt(int index)
{
if (index < 0 || index >= _payments.Count)
throw new ArgumentOutOfRangeException(nameof(index));
_payments.RemoveAt(index);
}
public void EnsureCanComplete()
{
if (_saleLines.Count == 0)
throw new InvalidOperationException(
"Et tomt salg kan ikke afsluttes.");
if (!IsFullyPaid)
throw new InvalidOperationException(
"Salget er ikke fuldt betalt.");
}
public void Reset()
{
_saleLines.Clear();
_payments.Clear();
}
private static void ValidatePriceAndQuantity(
decimal price,
int numberOfProducts)
{
if (price < 0)
throw new ArgumentOutOfRangeException(
nameof(price),
"Prisen må ikke være negativ.");
if (numberOfProducts <= 0)
throw new ArgumentOutOfRangeException(
nameof(numberOfProducts),
"Antal skal være større end 0.");
}
private static string GetPaymentMethodText(PaymentMethod paymentMethod)
{
switch (paymentMethod)
{
case PaymentMethod.Card:
return "Kort";
case PaymentMethod.Cash:
return "Kontant";
case PaymentMethod.GiftCard:
return "Gavekort";
case PaymentMethod.MobilePay:
return "MobilePay";
default:
throw new ArgumentOutOfRangeException(nameof(paymentMethod));
}
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject> <IsAspireSharedProject>true</IsAspireSharedProject>

View File

@@ -7,6 +7,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.18" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.18" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" 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" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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.FileExtensions" Version="10.0.10" />

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
using Database.Models; using Database.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -17,14 +17,24 @@ namespace Database
public DbSet<SaleLineEntity> SalesLines { get; set; } public DbSet<SaleLineEntity> SalesLines { get; set; }
public DbSet<PaymentEntity> Payment { 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( protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder) DbContextOptionsBuilder optionsBuilder)
{ {
LoadConfig l = new LoadConfig(); LoadConfig l = new LoadConfig();
IConfiguration config = l.ByEnvironment(); IConfiguration config = l.ByEnvironment();
string connectionString = config["SqlServer"].ToString(); string connectionString = System.Environment.GetEnvironmentVariable("SqlServer") ?? config["SqlServer"].ToString();
optionsBuilder optionsBuilder
.UseSqlServer(connectionString) .UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(LoggerFactory.Create(b => b .UseLoggerFactory(LoggerFactory.Create(b => b
.AddFilter(level => level >= LogLevel.Information))) .AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging() .EnableSensitiveDataLogging()
@@ -33,3 +43,6 @@ namespace Database
} }
} }
} }

View File

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

View File

@@ -28,10 +28,11 @@ namespace Database.Repository
return product; return product;
} }
public void Add(string name) public void Add(string name, decimal vatRate)
{ {
ProductGroupEntity productGroup = new ProductGroupEntity(); ProductGroupEntity productGroup = new ProductGroupEntity();
productGroup.Name = name; productGroup.Name = name;
productGroup.VatRate = vatRate;
using PosDbContext context = new PosDbContext(); using PosDbContext context = new PosDbContext();
//Get the highest index //Get the highest index
ProductGroupEntity highest = context.ProductGroups.OrderByDescending(c => c.Index).Take(1).FirstOrDefault(); ProductGroupEntity highest = context.ProductGroups.OrderByDescending(c => c.Index).Take(1).FirstOrDefault();
@@ -45,11 +46,12 @@ namespace Database.Repository
context.SaveChanges(); context.SaveChanges();
} }
public void Edit(string name, int id) public void Edit(string name, decimal vatRate, int id)
{ {
using PosDbContext context = new PosDbContext(); using PosDbContext context = new PosDbContext();
ProductGroupEntity productGroup = context.ProductGroups.Single(c => c.Id == id); ProductGroupEntity productGroup = context.ProductGroups.Single(c => c.Id == id);
productGroup.Name = name; productGroup.Name = name;
productGroup.VatRate = vatRate;
context.SaveChanges(); context.SaveChanges();
} }
@@ -69,10 +71,30 @@ namespace Database.Repository
context.SaveChanges(); 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() public bool Any()
{ {
using PosDbContext context = new PosDbContext(); using PosDbContext context = new PosDbContext();
bool any = context.ProductGroups.Any(); bool any = context.ProductGroups.Any(c => !c.IsArchived);
return any; return any;
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System;
using System.Linq; using System.Linq;
using Database.Models; using Database.Models;
@@ -38,6 +39,33 @@ namespace Database.Repository
context.SaveChanges(); 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) public ProductEntity GetById(int id)
{ {
using PosDbContext context = new PosDbContext(); using PosDbContext context = new PosDbContext();
@@ -72,7 +100,11 @@ namespace Database.Repository
product.ProductGroupId = productGroupId; product.ProductGroupId = productGroupId;
using PosDbContext context = new PosDbContext(); using PosDbContext context = new PosDbContext();
//Get the highest index //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) if (highest == null)
{ {
highest = new ProductEntity(); highest = new ProductEntity();

View File

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

View File

@@ -18,12 +18,10 @@
<None Remove="Icons\settings.png" /> <None Remove="Icons\settings.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="RestSharp" Version="108.0.3" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Database\Database.csproj" /> <ProjectReference Include="..\Database\Database.csproj" />
<ProjectReference Include="..\..\Pos.Models\Pos.Models.csproj" />
<ProjectReference Include="..\..\Pos.Service\Pos.Service.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

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

View File

@@ -29,7 +29,8 @@ namespace Pos
private ObservableCollection<ProductGroupEntity> _productGroups; private ObservableCollection<ProductGroupEntity> _productGroups;
private ObservableCollection<SaleGridModel> _saleGridModels; private ObservableCollection<SaleGridModel> _saleGridModels;
private ObservableCollection<AmountGridModel> _paymentGridModels; private ObservableCollection<AmountGridModel> _paymentGridModels;
private List<PriceLineModel> _priceLineModels; private readonly SalesTransactionService _salesTransactionService =
new SalesTransactionService();
private ProductEntity _selectedProduct; private ProductEntity _selectedProduct;
private ProductGroupEntity _selectedProductGroup; private ProductGroupEntity _selectedProductGroup;
@@ -37,14 +38,10 @@ namespace Pos
private int _employeeId = -1; private int _employeeId = -1;
private int _productGroupId = -1; private int _productGroupId = -1;
private int _productId = -1; private int _productId = -1;
private int _numberOfProducts = 1;
private int _selectedSaleGridIndex = -1; private int _selectedSaleGridIndex = -1;
private int _maxCharOfName = 0; private int _maxCharOfName = 0;
private decimal _price = 0;
private decimal _totalPrice = 0;
private Button _prevEmpButton = new Button(); private Button _prevEmpButton = new Button();
private Button _prevProductGroupButton = new Button(); private Button _prevProductGroupButton = new Button();
private Button _prevProduct = new Button(); private Button _prevProduct = new Button();
@@ -61,7 +58,6 @@ namespace Pos
_employees = CacheService.GetEmployee() as ObservableCollection<EmployeeEntity>; _employees = CacheService.GetEmployee() as ObservableCollection<EmployeeEntity>;
_productGroups = CacheService.GetProductGroupsIncludeProducts() as ObservableCollection<ProductGroupEntity>; _productGroups = CacheService.GetProductGroupsIncludeProducts() as ObservableCollection<ProductGroupEntity>;
_saleGridModels = new ObservableCollection<SaleGridModel>(); _saleGridModels = new ObservableCollection<SaleGridModel>();
_priceLineModels = new List<PriceLineModel>();
_paymentGridModels = new ObservableCollection<AmountGridModel>(); _paymentGridModels = new ObservableCollection<AmountGridModel>();
SaleGrid.ItemsSource = _saleGridModels; SaleGrid.ItemsSource = _saleGridModels;
PaymentGrid.ItemsSource = _paymentGridModels; PaymentGrid.ItemsSource = _paymentGridModels;
@@ -150,11 +146,25 @@ namespace Pos
return; return;
decimal price = Convert.ToDecimal(Price.Text); 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"; NumberOfProducts.Text = "1";
Price.Text = string.Empty; Price.Text = string.Empty;
TotalPrice.Content = "0"; TotalPrice.Content = "0";
BuildPrice(false);
_productId = -1; _productId = -1;
_productGroupId = -1; _productGroupId = -1;
ManipulateBuySection(); ManipulateBuySection();
@@ -162,16 +172,36 @@ namespace Pos
private void Payment_Click(object sender, RoutedEventArgs e) private void Payment_Click(object sender, RoutedEventArgs e)
{ {
//bool isValid = ValidateAmountInput(); decimal? amount = null;
//if (!isValid)
// return;
decimal amount = -1;
if (!string.IsNullOrEmpty(Amount.Text)) if (!string.IsNullOrEmpty(Amount.Text))
{
bool isValid = ValidateAmountInput();
if (!isValid)
return;
amount = Convert.ToDecimal(Amount.Text); amount = Convert.ToDecimal(Amount.Text);
}
Amount.Text = string.Empty; Amount.Text = string.Empty;
Button button = sender as Button; 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) 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(); PriceLineModel priceLineModel;
pModel.NumberProducts = _numberOfProducts;
pModel.Price = _price;
if (_selectedProduct != null) if (_selectedProduct != null)
{ {
pModel.Id = _selectedProduct.Id; priceLineModel = _salesTransactionService.AddSaleLine(
pModel.Name = _selectedProduct.Name; _selectedProduct.Id,
pModel.IsProductGroup = false; _selectedProduct.Name,
numberOfProducts,
price,
false);
} }
else else
{ {
pModel.Id = _selectedProductGroup.Id; priceLineModel = _salesTransactionService.AddSaleLine(
pModel.Name = _selectedProductGroup.Name; _selectedProductGroup.Id,
pModel.IsProductGroup = true; _selectedProductGroup.Name,
numberOfProducts,
price,
true);
} }
BuildPriceDataGrid(pModel); BuildPriceDataGrid(priceLineModel);
//BuildEmployee(); //BuildEmployee();
BuildProductGroup(); BuildProductGroup();
} }
private void CalculateSubPrice() private void CalculateSubPrice()
{ {
_price = 0; bool isValid = decimal.TryParse(Price.Text, out decimal price);
bool isValid = decimal.TryParse(Price.Text, out _price);
if (!isValid) if (!isValid)
return; return;
isValid = Int32.TryParse(NumberOfProducts.Text, out _numberOfProducts); isValid = Int32.TryParse(
NumberOfProducts.Text,
out int numberOfProducts);
if (!isValid) if (!isValid)
return; return;
TotalPrice.Content = _price * _numberOfProducts; try
{
TotalPrice.Content =
_salesTransactionService.CalculateLineTotal(
price,
numberOfProducts);
}
catch (ArgumentOutOfRangeException)
{
TotalPrice.Content = "0";
}
} }
private void CalculateTotalLine() private void CalculateTotalLine()
{ {
_totalPrice = 0; Total.Content =
foreach (PriceLineModel model in _priceLineModels) $"At betale: Kr. {_salesTransactionService.TotalPrice}";
{
_totalPrice += model.Price * model.NumberProducts;
}
Total.Content = $"At betale: Kr. {_totalPrice}";
} }
private void BuildPriceDataGrid(PriceLineModel priceLineModel) private void BuildPriceDataGrid(PriceLineModel priceLineModel)
{ {
_selectedProduct = null; _selectedProduct = null;
_selectedProductGroup = null; _selectedProductGroup = null;
_numberOfProducts = 1;
_priceLineModels.Add(priceLineModel);
CalculateTotalLine(); CalculateTotalLine();
SaleGridModel model = new SaleGridModel(); SaleGridModel model = new SaleGridModel();
@@ -399,38 +437,14 @@ namespace Pos
TotalPrice.IsEnabled = enabled; TotalPrice.IsEnabled = enabled;
} }
private void BuildAmuntDataGrid(decimal amount, string paymentMethod) private void BuildAmuntDataGrid(
decimal? amount,
PaymentMethod paymentMethod)
{ {
if (amount == -1) AmountGridModel amountGridModel =
{ _salesTransactionService.AddPayment(paymentMethod, amount);
amount = 0;
foreach (PriceLineModel lineModel in _priceLineModels)
{
amount += (lineModel.Price * lineModel.NumberProducts);
}
}
AmountGridModel amountGridModel = new AmountGridModel(); amountGridModel.AmountText = $"Kr. {amountGridModel.Amount}";
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;
}
_paymentGridModels.Add(amountGridModel); _paymentGridModels.Add(amountGridModel);
AmountPayed(); AmountPayed();
@@ -438,28 +452,25 @@ namespace Pos
private void AmountPayed() private void AmountPayed()
{ {
decimal amountPayed = 0; if (_salesTransactionService.AmountPaid ==
foreach (AmountGridModel paymentGridModel in _paymentGridModels) _salesTransactionService.TotalPrice)
{
amountPayed += paymentGridModel.Amount;
}
if (amountPayed == _totalPrice)
{ {
btnFinish_Click(null,null); btnFinish_Click(null,null);
return; 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; Style style = this.FindResource("BoldLabelWarningNoMargin") as Style;
lblRecived.Style = style; lblRecived.Style = style;
btnFinish.IsEnabled = true; btnFinish.IsEnabled = true;
} }
else else
{ {
lblRecived.Content = $"At betale: Kr. {_totalPrice - amountPayed}"; lblRecived.Content =
$"At betale: Kr. {_salesTransactionService.RemainingAmount}";
Style style = this.FindResource("BoldLabelNoMargin") as Style; Style style = this.FindResource("BoldLabelNoMargin") as Style;
lblRecived.Style = style; lblRecived.Style = style;
} }
@@ -473,21 +484,37 @@ namespace Pos
if (_selectedSaleGridIndex == -1) if (_selectedSaleGridIndex == -1)
return; return;
int index = _selectedSaleGridIndex; int index = _selectedSaleGridIndex;
//_saleGridModels.RemoveAt(_selectedSaleGridIndex); _salesTransactionService.RemoveSaleLineAt(index);
_priceLineModels.RemoveAt(index); _saleGridModels.RemoveAt(index);
_selectedSaleGridIndex = -1; _selectedSaleGridIndex = -1;
CalculateTotalLine(); CalculateTotalLine();
if (_salesTransactionService.AmountPaid > 0)
AmountPayed();
} }
} }
private void btnFinish_Click(object sender, RoutedEventArgs e) 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 saleService = new SaleService();
saleService.Save(_saleGridModels.ToList(),_paymentGridModels.ToList(),_employeeId); saleService.Save(_saleGridModels.ToList(),_paymentGridModels.ToList(),_employeeId);
_paymentGridModels.Clear(); _paymentGridModels.Clear();
_priceLineModels.Clear(); _salesTransactionService.Reset();
UpdateWindowTitle(); UpdateWindowTitle();
_totalPrice = 0;
_saleGridModels.Clear(); _saleGridModels.Clear();
lblRecived.Content = string.Empty; lblRecived.Content = string.Empty;
_selectedSaleGridIndex = -1; _selectedSaleGridIndex = -1;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
@page "/todaySales"
@rendermode InteractiveServer
@inject IJSRuntime Js
@inject SaleService SaleService
<Header />
<div class="page-container today-sales-page">
<div class="filters">
<label class="filter-control"><span>Dato</span><span class="filter-box"><svg viewBox="0 0 24 24"><rect x="3.5" y="5.5" width="17" height="15" rx="2"/><path d="M7 3.5v4M17 3.5v4M3.5 9.5h17"/></svg><input type="date" value="@_date.ToString("yyyy-MM-dd")" @onchange="DateChanged"/></span></label>
<label class="filter-control short"><span>Periode</span><span class="filter-box"><svg viewBox="0 0 24 24"><rect x="3.5" y="5.5" width="17" height="15" rx="2"/><path d="M7 3.5v4M17 3.5v4M3.5 9.5h17"/></svg><select @onchange="RangeChanged" value="@_range"><option value="1">1 dag</option><option value="7">7 dage</option><option value="30">30 dage</option></select></span></label>
</div>
<section class="ui-card summary-card">
<div class="summary-icon"><svg viewBox="0 0 64 64"><path d="M12 52h40M18 48V35h8v13M29 48V25h8v23M40 48V14h8v34"/></svg></div>
<div><p>Dagens salg</p><strong>@Currency(_summary.Total)</strong><small>@_summary.SaleCount salg</small></div>
</section>
<div class="sales-grid">
<UiCard Title="Salg pr. varegruppe" Class="sales-category-card">
<MetricList TItem="Metric" Items="@_summary.Categories">
<HeaderContent><span>Varegruppe</span><span>Salg</span></HeaderContent>
<RowTemplate Context="row"><div class="metric-row"><span>@row.Name</span><span>@Currency(row.Amount)</span></div></RowTemplate>
</MetricList>
</UiCard>
<div class="sales-right">
<UiCard Title="Betalingsfordeling">
<MetricList TItem="Metric" Items="@_summary.Payments">
<RowTemplate Context="row"><div class="metric-row"><span>@row.Name</span><span>@Currency(row.Amount)</span></div></RowTemplate>
</MetricList>
</UiCard>
<button class="print-button" type="button" @onclick="PrintAsync"><svg viewBox="0 0 24 24"><path d="M6 9V3h12v6M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 14h12v7H6z"/></svg>Print salg</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Pos.Models;
using System.Globalization;
namespace Pos.Web.Components.Pages.TodaySales;
public partial class TodaySales
{
private static readonly CultureInfo Danish = CultureInfo.GetCultureInfo("da-DK");
private TotalSaleDetail? _totalSaleDetail;
private DateTime _date = DateTime.Now;
private string _range = "1";
private Summary _summary = new(0m, 0, Array.Empty<Metric>(), Array.Empty<Metric>());
private async Task DateChanged(ChangeEventArgs e)
{
if (DateTime.TryParse(e.Value?.ToString(), out var date))
{
_date = date;
await LoadAsync();
}
}
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task RangeChanged(ChangeEventArgs e)
{
_range = e.Value?.ToString() ?? "1";
await LoadAsync();
}
private Task LoadAsync()
{
var result = SaleService.TotalSale(_date);
_totalSaleDetail = result;
_summary = new Summary(
result.TotalSale,
result.TotalCustomer,
result.TotalSaleCategories
.Select(category => new Metric(category.Category, category.Sale))
.ToList(),
result.PaymentBreakdown
.Select(payment => new Metric(payment.PaymentMethod, payment.Amount))
.ToList());
return Task.CompletedTask;
}
private async Task PrintAsync()
{
// TODO: Kobl til SaleService.PrintSaleOfDay(_date, _totalSaleDetail)
// eller POST /api/sales/print.
await Js.InvokeVoidAsync("window.print");
}
private static string Currency(decimal amount) =>
$"{amount.ToString("N2", Danish)} kr.";
private sealed record Metric(string Name, decimal Amount);
private sealed record Summary(
decimal Total,
int SaleCount,
IReadOnlyList<Metric> Categories,
IReadOnlyList<Metric> Payments);
}

View File

@@ -0,0 +1,307 @@
.today-sales-page {
width: min(100%, 1420px);
min-height: calc(100dvh - 78px);
margin: 0 auto;
padding: 38px 32px 64px;
box-sizing: border-box;
}
.filters {
display: flex;
align-items: end;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.filter-control {
display: flex;
min-width: 230px;
flex-direction: column;
gap: 6px;
color: #3e444d;
font-size: 14px;
}
.filter-control.short {
min-width: 140px;
}
.filter-box {
display: flex;
min-height: 56px;
align-items: center;
gap: 10px;
padding: 0 14px;
border: 1px solid #bfc5cd;
border-radius: 8px;
background: #fff;
}
.filter-box svg {
width: 23px;
height: 23px;
flex: none;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.7;
}
.filter-box input,
.filter-box select {
width: 100%;
min-width: 0;
border: 0;
outline: 0;
background: transparent;
font-size: 18px;
}
.summary-card {
display: flex;
min-height: 170px;
align-items: center;
gap: 32px;
padding: 28px 34px;
}
.summary-icon {
display: grid;
width: 114px;
height: 114px;
flex: none;
place-items: center;
border-radius: 50%;
color: var(--pos-blue);
background: var(--pos-blue-soft);
}
.summary-icon svg {
width: 58px;
height: 58px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
}
.summary-card p {
margin: 0 0 4px;
font-size: 19px;
}
.summary-card strong {
display: block;
font-size: 48px;
line-height: 1.05;
letter-spacing: -.035em;
}
.summary-card small {
display: block;
margin-top: 6px;
color: var(--pos-muted);
font-size: 18px;
}
.sales-grid {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(350px, 1fr);
gap: 20px;
align-items: start;
margin-top: 20px;
}
.sales-right {
display: flex;
flex-direction: column;
gap: 20px;
}
::deep .ui-card {
overflow: hidden;
border: 1px solid var(--pos-border);
border-radius: 12px;
background: #fff;
box-shadow: 0 1px 2px rgba(16, 24, 40, .03);
}
::deep .ui-card__header {
padding: 26px 32px 16px;
}
::deep .ui-card__header h2 {
margin: 0;
font-size: 22px;
font-weight: 650;
}
::deep .ui-card__body {
padding: 0 32px 28px;
}
::deep .metric-list__header,
::deep .metric-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 16px;
}
::deep .metric-list__header {
padding-bottom: 12px;
color: var(--pos-muted);
}
::deep .metric-row {
min-height: 58px;
align-items: center;
border-top: 1px solid var(--pos-border-soft);
font-size: 19px;
}
::deep .metric-row > :last-child,
::deep .metric-list__header > :last-child {
text-align: right;
white-space: nowrap;
}
.print-button {
display: flex;
width: 100%;
min-height: 70px;
align-items: center;
justify-content: center;
gap: 14px;
border: 1.5px solid var(--pos-blue);
border-radius: 9px;
color: var(--pos-blue);
background: #fff;
font-weight: 700;
}
.print-button:hover {
background: var(--pos-blue-soft);
}
.print-button svg {
width: 28px;
height: 28px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.8;
}
@media (min-width: 900px) and (max-height: 850px) {
.today-sales-page {
padding-top: 22px;
padding-bottom: 18px;
}
.filters {
margin-bottom: 14px;
}
.filter-box {
min-height: 48px;
}
.summary-card {
min-height: 130px;
gap: 24px;
padding: 20px 28px;
}
.summary-icon {
width: 88px;
height: 88px;
}
.summary-icon svg {
width: 48px;
height: 48px;
}
.summary-card strong {
font-size: 40px;
}
.summary-card small {
font-size: 16px;
}
.sales-grid {
gap: 20px;
margin-top: 16px;
}
::deep .ui-card__header {
padding: 18px 28px 10px;
}
::deep .ui-card__body {
padding: 0 28px 16px;
}
::deep .metric-list__header {
padding-bottom: 7px;
}
::deep .metric-row {
min-height: 39px;
font-size: 17px;
}
.print-button {
min-height: 54px;
}
}
@media (max-width: 900px) {
.sales-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 680px) {
.today-sales-page {
padding: 24px 16px 40px;
}
.summary-card {
gap: 18px;
padding: 22px 20px;
}
.summary-icon {
width: 72px;
height: 72px;
}
.summary-icon svg {
width: 40px;
height: 40px;
}
.summary-card strong {
font-size: 34px;
}
::deep .ui-card__header {
padding: 22px 20px 14px;
}
::deep .ui-card__body {
padding: 0 20px 22px;
}
::deep .metric-row {
font-size: 17px;
}
}

View File

@@ -0,0 +1,5 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
</Found>
</Router>

View File

@@ -0,0 +1,26 @@
@rendermode InteractiveServer
@implements IDisposable
@inject NavigationManager NavigationManager
<header class="top-nav">
<div class="nav-start">
@if (!IsHomePage)
{
<button class="home-button"
type="button"
title="Tilbage til forsiden"
aria-label="Tilbage til forsiden"
@onclick="GoToHome">
</button>
}
<div class="brand">Blomster POS</div>
</div>
<time datetime="@_currentTime.ToString("O")">
@_currentTime.ToString(
"dddd d. MMMM yyyy · HH:mm",
_danishCulture)
</time>
</header>

View File

@@ -0,0 +1,42 @@
using System.Globalization;
namespace Pos.Web.Components.Shared
{
public partial class Header
{
private readonly CultureInfo _danishCulture =
CultureInfo.GetCultureInfo("da-DK");
private readonly Timer _timer;
private DateTime _currentTime = DateTime.Now;
private bool IsHomePage =>
string.IsNullOrEmpty(
NavigationManager.ToBaseRelativePath(NavigationManager.Uri));
public Header()
{
_timer = new Timer(
UpdateTime,
null,
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(1));
}
private void UpdateTime(object? state)
{
_currentTime = DateTime.Now;
_ = InvokeAsync(StateHasChanged);
}
private void GoToHome()
{
NavigationManager.NavigateTo("/");
}
public void Dispose()
{
_timer.Dispose();
}
}
}

View File

@@ -0,0 +1,54 @@
.top-nav {
min-height: 72px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 0 32px;
background: var(--colorNeutralBackground1);
border-bottom: 1px solid var(--colorNeutralStroke2);
}
.nav-start {
display: flex;
align-items: center;
gap: 16px;
}
.brand {
color: var(--colorNeutralForeground1);
font-size: 26px;
font-weight: 600;
}
.home-button {
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border: 1px solid var(--colorNeutralStroke1);
border-radius: 8px;
background: transparent;
color: var(--colorNeutralForeground1);
cursor: pointer;
font-family: inherit;
font-size: 28px;
line-height: 1;
}
.home-button:hover {
background: var(--colorNeutralBackground1Hover);
}
.home-button:focus-visible {
outline: 2px solid var(--colorStrokeFocus2);
outline-offset: 2px;
}
time {
color: var(--colorNeutralForeground2);
font-size: 18px;
white-space: nowrap;
}

View File

@@ -0,0 +1,10 @@
@typeparam TItem
<div class="metric-list">
@if (HeaderContent is not null) { <div class="metric-list__header">@HeaderContent</div> }
@foreach (var item in Items) { @RowTemplate(item) }
</div>
@code {
[Parameter] public IReadOnlyList<TItem> Items { get; set; } = Array.Empty<TItem>();
[Parameter] public RenderFragment? HeaderContent { get; set; }
[Parameter] public RenderFragment<TItem> RowTemplate { get; set; } = default!;
}

View File

@@ -0,0 +1,12 @@
<section class="ui-card @Class">
@if (!string.IsNullOrWhiteSpace(Title))
{
<div class="ui-card__header"><h2>@Title</h2></div>
}
<div class="ui-card__body">@ChildContent</div>
</section>
@code {
[Parameter] public string Title { get; set; } = string.Empty;
[Parameter] public string Class { get; set; } = string.Empty;
[Parameter] public RenderFragment? ChildContent { get; set; }
}

View File

@@ -0,0 +1,18 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.FluentUI.AspNetCore.Components
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
@using Microsoft.JSInterop
@using Pos.Web
@using Pos.Web.Components
@using Pos.Web.Components.Layout
@using Pos.Web.Components.Shared
@using Pos.Service

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