Compare commits
6 Commits
8953e917e1
...
a6b87525b3
| Author | SHA1 | Date | |
|---|---|---|---|
| a6b87525b3 | |||
| 770e170d0e | |||
| 8de6ff9338 | |||
|
|
048f45a4a1 | ||
|
|
01d81cf14f | ||
|
|
49a9f336b1 |
35
PointOfSale/.dockerignore
Normal file
35
PointOfSale/.dockerignore
Normal file
@@ -0,0 +1,35 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
database-backup/
|
||||
.env
|
||||
|
||||
database-data/
|
||||
|
||||
7
PointOfSale/.gitignore
vendored
7
PointOfSale/.gitignore
vendored
@@ -360,3 +360,10 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# Local Docker database data and secrets
|
||||
.env
|
||||
database-backup/
|
||||
|
||||
database-data/
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pos.Models
|
||||
namespace Pos.Models;
|
||||
|
||||
public class TotalSaleDetail
|
||||
{
|
||||
public class TotalSaleDetail
|
||||
{
|
||||
public decimal TotalSale { get; set; }
|
||||
public int TotalCustomer { get; set; }
|
||||
public List<TotalSaleCategory> TotalSaleCategories { get; set; } = new();
|
||||
}
|
||||
public decimal TotalSale { get; set; }
|
||||
public int TotalCustomer { get; set; }
|
||||
public List<TotalSaleCategory> TotalSaleCategories { get; set; } = new();
|
||||
public List<TotalSalePayment> PaymentBreakdown { get; set; } = new();
|
||||
}
|
||||
|
||||
7
PointOfSale/Pos.Models/TotalSalePayment.cs
Normal file
7
PointOfSale/Pos.Models/TotalSalePayment.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Pos.Models;
|
||||
|
||||
public class TotalSalePayment
|
||||
{
|
||||
public string PaymentMethod { get; set; } = string.Empty;
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
14
PointOfSale/Pos.Service/IProductService.cs
Normal file
14
PointOfSale/Pos.Service/IProductService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using Database.Models;
|
||||
|
||||
namespace Pos.Service;
|
||||
|
||||
public interface IProductService
|
||||
{
|
||||
List<ProductEntity> GetProducts(int productGroupId);
|
||||
ProductEntity GetProduct(int productId);
|
||||
void AddProduct(string name, int productGroupId);
|
||||
void UpdateProduct(int productId, int productGroupId, string name);
|
||||
void ArchiveProduct(int productId);
|
||||
void UpdateOrder(int productGroupId, IReadOnlyList<int> orderedProductIds);
|
||||
}
|
||||
@@ -21,17 +21,34 @@ namespace Pos.Service
|
||||
return _productGroupRepository.Any();
|
||||
}
|
||||
|
||||
public void AddProductGroup(string name, decimal vatRate)
|
||||
{
|
||||
_productGroupRepository.Add(
|
||||
ValidateName(name),
|
||||
ValidateVatRate(vatRate));
|
||||
CacheService.Invalidate();
|
||||
}
|
||||
|
||||
public void AddProductGroup(string name)
|
||||
{
|
||||
_productGroupRepository.Add(ValidateName(name));
|
||||
CacheService.Invalidate();
|
||||
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();
|
||||
}
|
||||
@@ -66,6 +83,16 @@ namespace Pos.Service
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Database.Models;
|
||||
@@ -93,3 +93,5 @@ namespace Pos.Service
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Linq;
|
||||
@@ -103,6 +103,34 @@ namespace Pos.Service
|
||||
foreach (SaleEntity sale in sales)
|
||||
{
|
||||
List<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)
|
||||
{
|
||||
|
||||
@@ -303,3 +331,4 @@ namespace Pos.Service
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.18" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.18" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.18">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.10" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -58,3 +58,7 @@ namespace Pos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(PosDbContext))]
|
||||
[Migration("20260827000000_AddProductGroupVatRate")]
|
||||
public class AddProductGroupVatRate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "VatRate",
|
||||
table: "productgroup",
|
||||
type: "decimal(5,2)",
|
||||
nullable: false,
|
||||
defaultValue: 25m);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "VatRate",
|
||||
table: "productgroup");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ namespace Database.Models
|
||||
public string Name { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
public int Index { get; set; }
|
||||
public decimal VatRate { get; set; }
|
||||
|
||||
public ICollection<ProductEntity> Products { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Database.Models;
|
||||
using Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -32,9 +32,9 @@ namespace Database
|
||||
{
|
||||
LoadConfig l = new LoadConfig();
|
||||
IConfiguration config = l.ByEnvironment();
|
||||
string connectionString = config["SqlServer"].ToString();
|
||||
string connectionString = System.Environment.GetEnvironmentVariable("SqlServer") ?? config["SqlServer"].ToString();
|
||||
optionsBuilder
|
||||
.UseSqlServer(connectionString)
|
||||
.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure())
|
||||
.UseLoggerFactory(LoggerFactory.Create(b => b
|
||||
.AddFilter(level => level >= LogLevel.Information)))
|
||||
.EnableSensitiveDataLogging()
|
||||
@@ -43,3 +43,6 @@ namespace Database
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,10 +28,11 @@ namespace Database.Repository
|
||||
return product;
|
||||
}
|
||||
|
||||
public void Add(string name)
|
||||
public void Add(string name, decimal vatRate)
|
||||
{
|
||||
ProductGroupEntity productGroup = new ProductGroupEntity();
|
||||
productGroup.Name = name;
|
||||
productGroup.VatRate = vatRate;
|
||||
using PosDbContext context = new PosDbContext();
|
||||
//Get the highest index
|
||||
ProductGroupEntity highest = context.ProductGroups.OrderByDescending(c => c.Index).Take(1).FirstOrDefault();
|
||||
@@ -45,11 +46,12 @@ namespace Database.Repository
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public void Edit(string name, int id)
|
||||
public void Edit(string name, decimal vatRate, int id)
|
||||
{
|
||||
using PosDbContext context = new PosDbContext();
|
||||
ProductGroupEntity productGroup = context.ProductGroups.Single(c => c.Id == id);
|
||||
productGroup.Name = name;
|
||||
productGroup.VatRate = vatRate;
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<FluentButton Class="menu-button" Size="ButtonSize.Large">
|
||||
Kassesalg
|
||||
</FluentButton>
|
||||
<FluentButton Class="menu-button" Size="ButtonSize.Large">
|
||||
<FluentButton Class="menu-button" Size="ButtonSize.Large" onclick="@(() => Nav.NavigateTo("/todaySales"))">
|
||||
Dagens salg
|
||||
</FluentButton>
|
||||
<FluentButton Class="menu-button" Size="ButtonSize.Large">
|
||||
@@ -18,4 +18,4 @@
|
||||
<FluentButton Class="menu-button" Size="ButtonSize.Large" onclick="@(() => Nav.NavigateTo("/settings"))">
|
||||
Indstillinger
|
||||
</FluentButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
.home-container {
|
||||
width: min(100%, 1100px);
|
||||
margin: 32px auto;
|
||||
@@ -11,4 +12,12 @@
|
||||
min-height: 220px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.home-container {
|
||||
grid-template-columns: 1fr;
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@using Database.Models
|
||||
@using Pos.Service
|
||||
@inject ProductGroupService ProductGroupService
|
||||
|
||||
@if (IsOpen)
|
||||
{
|
||||
<div class="dialog-overlay">
|
||||
<div class="product-dialog" role="dialog" aria-modal="true" aria-labelledby="product-dialog-title">
|
||||
<div class="dialog-header">
|
||||
<h2 id="product-dialog-title">@(_productGroupId.HasValue ? "Redigér vare" : "Opret vare")</h2>
|
||||
<button class="close-button" type="button" aria-label="Luk" @onclick="CloseAsync">×</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<label for="product-name">Navn</label>
|
||||
<input id="product-name" class="dialog-input" type="text" placeholder="Indtast navn"
|
||||
@bind="_name" @bind:event="oninput" />
|
||||
|
||||
<label for="product-vat">Moms i procent</label>
|
||||
<input id="product-vat" class="dialog-input" type="number" min="0" max="100" step="0.01"
|
||||
@bind="_vatRate" />
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button class="cancel-button" type="button" @onclick="CloseAsync">Annuller</button>
|
||||
<button class="save-button" type="button" disabled="@(!CanSave)" @onclick="SaveAsync">
|
||||
@(_productGroupId.HasValue ? "Gem ændringer" : "Opret vare")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Database.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Pos.Web.Components.Pages.Settings
|
||||
{
|
||||
public partial class ProductGroupDialog
|
||||
{
|
||||
[Parameter] public bool IsOpen { get; set; }
|
||||
[Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
|
||||
[Parameter] public ProductGroupEntity? ProductGroup { get; set; }
|
||||
[Parameter] public EventCallback ProductGroupSaved { get; set; }
|
||||
|
||||
private int? _productGroupId;
|
||||
private string _name = string.Empty;
|
||||
private decimal _vatRate = 25m;
|
||||
private bool _wasOpen;
|
||||
private bool CanSave => !string.IsNullOrWhiteSpace(_name) && _vatRate >= 0 && _vatRate <= 100;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (IsOpen && !_wasOpen)
|
||||
{
|
||||
_productGroupId = ProductGroup?.Id;
|
||||
_name = ProductGroup?.Name ?? string.Empty;
|
||||
_vatRate = ProductGroup?.VatRate ?? 25m;
|
||||
}
|
||||
_wasOpen = IsOpen;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (!CanSave)
|
||||
return;
|
||||
|
||||
if (_productGroupId.HasValue)
|
||||
ProductGroupService.UpdateProductGroup(_productGroupId.Value, _name, _vatRate);
|
||||
else
|
||||
ProductGroupService.AddProductGroup(_name, _vatRate);
|
||||
|
||||
await ProductGroupSaved.InvokeAsync();
|
||||
await CloseAsync();
|
||||
}
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
_wasOpen = false;
|
||||
await IsOpenChanged.InvokeAsync(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.dialog-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 24px; background: rgb(15 23 42 / 45%); }
|
||||
.product-dialog { width: min(100%, 650px); overflow: hidden; border-radius: 12px; background: white; color: #242424; color-scheme: light; }
|
||||
.dialog-header { display: flex; align-items: center; justify-content: space-between; padding: 28px 40px 18px; }
|
||||
.dialog-header h2 { margin: 0; font-size: 28px; }
|
||||
.close-button { border: 0; background: transparent; color: #334155; cursor: pointer; font-size: 36px; line-height: 1; }
|
||||
.dialog-content { display: grid; gap: 10px; padding: 18px 40px 36px; }
|
||||
.dialog-content label { margin-top: 8px; font-size: 16px; font-weight: 500; }
|
||||
.dialog-input { box-sizing: border-box; width: 100%; height: 48px; padding: 0 14px; border: 1px solid #8a8a8a; border-radius: 6px; outline: none; background: #fff; color: #242424; font: inherit; }
|
||||
.dialog-input:focus { border-color: #0f6cbd; box-shadow: 0 0 0 1px #0f6cbd; }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 16px; padding: 24px 40px; border-top: 1px solid #e2e8f0; }
|
||||
.cancel-button, .save-button { min-height: 42px; padding: 0 22px; border-radius: 6px; cursor: pointer; font: inherit; font-weight: 500; }
|
||||
.cancel-button { border: 1px solid #616161; background: #fff; color: #242424; }
|
||||
.save-button { border: 1px solid #0f6cbd; background: #0f6cbd; color: #fff; }
|
||||
.save-button:disabled { border-color: #d1d1d1; background: #e0e0e0; color: #707070; cursor: not-allowed; }
|
||||
77
PointOfSale/Pos.Web/Components/Pages/Settings/Products.razor
Normal file
77
PointOfSale/Pos.Web/Components/Pages/Settings/Products.razor
Normal file
@@ -0,0 +1,77 @@
|
||||
@page "/settings/products"
|
||||
@rendermode InteractiveServer
|
||||
@using Database.Models
|
||||
@using Pos.Service
|
||||
@inject IDialogService DialogService
|
||||
@inject ProductGroupService ProductGroupService
|
||||
|
||||
<Header />
|
||||
|
||||
<div class="products-page">
|
||||
<div class="products-header">
|
||||
<h1>Vare</h1>
|
||||
</div>
|
||||
|
||||
<div class="products-toolbar">
|
||||
<label class="search-field">
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<input type="search" placeholder="Søg vare" aria-label="Søg vare"
|
||||
@bind="_searchText" @bind:event="oninput" />
|
||||
</label>
|
||||
|
||||
<FluentButton Class="create-button" Appearance="@ButtonAppearance.Primary"
|
||||
OnClick="OpenCreateDialog">
|
||||
<span class="button-icon" aria-hidden="true">+</span> Opret vare
|
||||
</FluentButton>
|
||||
|
||||
<FluentButton Class="save-order-button" Appearance="@ButtonAppearance.Outline"
|
||||
Disabled="@(!_orderHasChanged)" OnClick="SaveOrder">
|
||||
<span class="button-icon" aria-hidden="true">⇅</span> Gem rækkefølge
|
||||
</FluentButton>
|
||||
</div>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<FluentSpinner />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="products-table">
|
||||
<div class="products-row products-column-header">
|
||||
<span>Rækkefølge</span><span>Navn</span><span>Moms</span><span>Status</span><span>Handling</span>
|
||||
</div>
|
||||
<div class="products-list">
|
||||
@foreach (ProductGroupEntity productGroup in FilteredProductGroups)
|
||||
{
|
||||
<div class="products-row product-row @GetDragClass(productGroup)" @key="productGroup.Id"
|
||||
draggable="true" @ondragstart="@(() => StartDragging(productGroup))"
|
||||
@ondragover:preventDefault="true" @ondrop="@(() => DropProductGroup(productGroup))"
|
||||
@ondragend="StopDragging">
|
||||
<span class="drag-handle" title="Træk for at ændre rækkefølge">⠿</span>
|
||||
<span class="product-name">@productGroup.Name</span>
|
||||
<span>@productGroup.VatRate.ToString("0.##") %</span>
|
||||
<span class="active-status">Aktiv</span>
|
||||
<span class="row-actions">
|
||||
<FluentButton Appearance="@ButtonAppearance.Outline"
|
||||
OnClick="@(() => OpenEditDialog(productGroup))">Redigér</FluentButton>
|
||||
<FluentButton Class="delete-button" Appearance="@ButtonAppearance.Outline"
|
||||
OnClick="@(() => ConfirmDeactivation(productGroup))">Slet</FluentButton>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (!FilteredProductGroups.Any())
|
||||
{
|
||||
<p class="empty-state">Ingen varer matcher din søgning.</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="products-help"><span aria-hidden="true">ⓘ</span>
|
||||
Træk i håndtaget for at ændre rækkefølgen. Deaktiverede varer vises ikke i kassesalget.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProductGroupDialog @bind-IsOpen="_showProductGroupDialog"
|
||||
ProductGroup="_productGroupToEdit"
|
||||
ProductGroupSaved="ProductGroupSaved" />
|
||||
@@ -0,0 +1,93 @@
|
||||
using Database.Models;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
|
||||
namespace Pos.Web.Components.Pages.Settings
|
||||
{
|
||||
public partial class Products
|
||||
{
|
||||
private List<ProductGroupEntity> _productGroups = [];
|
||||
private ProductGroupEntity? _draggedProductGroup;
|
||||
private ProductGroupEntity? _productGroupToEdit;
|
||||
private string _searchText = string.Empty;
|
||||
private bool _isLoading = true;
|
||||
private bool _showProductGroupDialog;
|
||||
private bool _orderHasChanged;
|
||||
|
||||
private IEnumerable<ProductGroupEntity> FilteredProductGroups =>
|
||||
_productGroups.Where(productGroup =>
|
||||
string.IsNullOrWhiteSpace(_searchText) ||
|
||||
productGroup.Name.Contains(_searchText, StringComparison.CurrentCultureIgnoreCase));
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
LoadProductGroups();
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
private void LoadProductGroups() =>
|
||||
_productGroups = ProductGroupService.GetProductGroups();
|
||||
|
||||
private void OpenCreateDialog()
|
||||
{
|
||||
_productGroupToEdit = null;
|
||||
_showProductGroupDialog = true;
|
||||
}
|
||||
|
||||
private void OpenEditDialog(ProductGroupEntity productGroup)
|
||||
{
|
||||
_productGroupToEdit = productGroup;
|
||||
_showProductGroupDialog = true;
|
||||
}
|
||||
|
||||
private void ProductGroupSaved()
|
||||
{
|
||||
LoadProductGroups();
|
||||
_orderHasChanged = false;
|
||||
}
|
||||
|
||||
private void StartDragging(ProductGroupEntity productGroup) =>
|
||||
_draggedProductGroup = productGroup;
|
||||
|
||||
private void DropProductGroup(ProductGroupEntity target)
|
||||
{
|
||||
if (_draggedProductGroup == null || _draggedProductGroup.Id == target.Id ||
|
||||
!string.IsNullOrWhiteSpace(_searchText))
|
||||
{
|
||||
StopDragging();
|
||||
return;
|
||||
}
|
||||
|
||||
int oldIndex = _productGroups.IndexOf(_draggedProductGroup);
|
||||
int newIndex = _productGroups.IndexOf(target);
|
||||
_productGroups.RemoveAt(oldIndex);
|
||||
_productGroups.Insert(newIndex, _draggedProductGroup);
|
||||
_orderHasChanged = true;
|
||||
StopDragging();
|
||||
}
|
||||
|
||||
private void StopDragging() => _draggedProductGroup = null;
|
||||
|
||||
private string GetDragClass(ProductGroupEntity productGroup) =>
|
||||
_draggedProductGroup?.Id == productGroup.Id ? "dragging" : string.Empty;
|
||||
|
||||
private void SaveOrder()
|
||||
{
|
||||
ProductGroupService.UpdateOrder(_productGroups.Select(item => item.Id).ToList());
|
||||
_orderHasChanged = false;
|
||||
}
|
||||
|
||||
private async Task ConfirmDeactivation(ProductGroupEntity productGroup)
|
||||
{
|
||||
DialogResult result = await DialogService.ShowConfirmationAsync(
|
||||
$"Er du sikker på, at du vil slette {productGroup.Name}?",
|
||||
"Bekræft sletning", "Slet", "Annuller");
|
||||
|
||||
if (!result.Cancelled)
|
||||
{
|
||||
ProductGroupService.ArchiveProductGroup(productGroup.Id);
|
||||
LoadProductGroups();
|
||||
_orderHasChanged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; } }
|
||||
@@ -5,16 +5,14 @@
|
||||
<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">
|
||||
<FluentButton Class="settings-button" onclick="@(() => Nav.NavigateTo("/settings/products"))">
|
||||
Vare
|
||||
</FluentButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
.settings-page {
|
||||
width: min(100%, 1100px);
|
||||
margin: 0 auto;
|
||||
@@ -22,3 +23,14 @@
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.settings-page {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
@rendermode InteractiveServer
|
||||
@rendermode InteractiveServer
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<header class="top-nav">
|
||||
<div class="brand">Blomster POS</div>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
@@ -5,11 +5,15 @@ namespace Pos.Web.Components.Shared
|
||||
public partial class Header
|
||||
{
|
||||
private readonly CultureInfo _danishCulture =
|
||||
CultureInfo.GetCultureInfo("da-DK");
|
||||
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(
|
||||
@@ -25,6 +29,11 @@ namespace Pos.Web.Components.Shared
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void GoToHome()
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Dispose();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* CSS for Header component */
|
||||
.top-nav {
|
||||
min-height: 72px;
|
||||
box-sizing: border-box;
|
||||
@@ -11,14 +10,45 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
10
PointOfSale/Pos.Web/Components/Shared/MetricList.razor
Normal file
10
PointOfSale/Pos.Web/Components/Shared/MetricList.razor
Normal 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!;
|
||||
}
|
||||
12
PointOfSale/Pos.Web/Components/Shared/UiCard.razor
Normal file
12
PointOfSale/Pos.Web/Components/Shared/UiCard.razor
Normal 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; }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@@ -11,4 +12,7 @@
|
||||
@using Pos.Web
|
||||
@using Pos.Web.Components
|
||||
@using Pos.Web.Components.Layout
|
||||
@using Pos.Web.Components.Shared
|
||||
@using Pos.Web.Components.Shared
|
||||
@using Pos.Service
|
||||
|
||||
|
||||
|
||||
34
PointOfSale/Pos.Web/Dockerfile
Normal file
34
PointOfSale/Pos.Web/Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
# This stage is used when running from VS in fast mode (Default for Debug configuration)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
|
||||
# This stage is used to build the service project
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Pos.Web/Pos.Web.csproj", "Pos.Web/"]
|
||||
COPY ["Pos.Models/Pos.Models.csproj", "Pos.Models/"]
|
||||
COPY ["Pos.ServiceDefaults/Pos.ServiceDefaults.csproj", "Pos.ServiceDefaults/"]
|
||||
COPY ["Pos.Service/Pos.Service.csproj", "Pos.Service/"]
|
||||
COPY ["Pos.Ui/Database/Database.csproj", "Pos.Ui/Database/"]
|
||||
RUN dotnet restore "./Pos.Web/Pos.Web.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Pos.Web"
|
||||
RUN dotnet build "./Pos.Web.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# This stage is used to publish the service project to be copied to the final stage
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./Pos.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Pos.Web.dll"]
|
||||
@@ -5,11 +5,14 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
<UserSecretsId>1b2d8446-d538-488a-94a9-4568bdc91cd3</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="5.0.0-rc.4-26180.1" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.14.2" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using Pos.Web.Components;
|
||||
using Pos.Service;
|
||||
using Pos.Web.Components;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddFluentUIComponents();
|
||||
builder.Services.AddScoped<EmployeeService>();
|
||||
builder.Services.AddScoped<SaleService>();
|
||||
builder.Services.AddScoped<ProductGroupService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseStatusCodePagesWithReExecute(
|
||||
"/not-found",
|
||||
createScopeForStatusCodePages: true);
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapStaticAssets();
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5281",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7102;http://localhost:5281",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5281"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7102;http://localhost:5281"
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080",
|
||||
"SqlServer": "Server=host.docker.internal;Database=PointOfSale;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css';
|
||||
@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css';
|
||||
|
||||
body {
|
||||
--body-font: "Segoe UI Variable", "Segoe UI", sans-serif;
|
||||
@@ -87,3 +87,51 @@ body {
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--pos-blue: #155eef;
|
||||
--pos-blue-soft: #eaf1ff;
|
||||
--pos-text: #171a1f;
|
||||
--pos-muted: #707781;
|
||||
--pos-border: #d9dde3;
|
||||
--pos-border-soft: #e9ebef;
|
||||
--pos-bg: #f7f8fa;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--pos-bg);
|
||||
color: var(--pos-text);
|
||||
font-family: "Segoe UI Variable", "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100dvh;
|
||||
background: var(--pos-bg);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.4.11620.152
|
||||
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pos.Models", "Pos.Models\Po
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pos.ServiceDefaults", "Pos.ServiceDefaults\Pos.ServiceDefaults.csproj", "{EDC882E6-4983-35BD-3357-E433A3E64369}"
|
||||
EndProject
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{B7F5A7E1-7A2B-4F3A-9D77-5C9C8A4B2E31}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -67,6 +69,10 @@ Global
|
||||
{EDC882E6-4983-35BD-3357-E433A3E64369}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EDC882E6-4983-35BD-3357-E433A3E64369}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EDC882E6-4983-35BD-3357-E433A3E64369}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B7F5A7E1-7A2B-4F3A-9D77-5C9C8A4B2E31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B7F5A7E1-7A2B-4F3A-9D77-5C9C8A4B2E31}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B7F5A7E1-7A2B-4F3A-9D77-5C9C8A4B2E31}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B7F5A7E1-7A2B-4F3A-9D77-5C9C8A4B2E31}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
8
PointOfSale/docker-compose.dcproj
Normal file
8
PointOfSale/docker-compose.dcproj
Normal file
@@ -0,0 +1,8 @@
|
||||
<Project ToolsVersion="Current" Sdk="Microsoft.Docker.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectVersion>2.1</ProjectVersion>
|
||||
<DockerDevelopmentMode>Regular</DockerDevelopmentMode>
|
||||
<DockerTargetOS>Linux</DockerTargetOS>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
35
PointOfSale/docker-compose.yml
Normal file
35
PointOfSale/docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
services:
|
||||
db:
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_PID: "Developer"
|
||||
MSSQL_SA_PASSWORD: "${MSSQL_SA_PASSWORD}"
|
||||
ports:
|
||||
- "11433:1433"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$${MSSQL_SA_PASSWORD}\" -C -Q \"SELECT 1\" || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
volumes:
|
||||
- mssql-system:/var/opt/mssql
|
||||
- ./database-data/data:/var/opt/mssql/data
|
||||
- ./database-backup:/var/opt/mssql/backup:ro
|
||||
|
||||
pointofsale:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Pos.Web/Dockerfile
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
SqlServer: "Server=db,1433;Database=pointofsale;User Id=sa;Password=${MSSQL_SA_PASSWORD};TrustServerCertificate=True;"
|
||||
|
||||
volumes:
|
||||
mssql-system:
|
||||
Reference in New Issue
Block a user