WerksverkaufScanner:
-Inventur vorbereiten entfernt -Einstellungsseite ergänzt -Filial-Id anhand IP laden
This commit is contained in:
parent
19fe31233d
commit
1729550f95
@ -1,25 +1,49 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
using WerksverkaufScanner.Services; // <-- für IpFilialeService
|
||||
|
||||
[ApiController]
|
||||
[Route("auth")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly AuthService _auth;
|
||||
public AuthController(AuthService auth) => _auth = auth;
|
||||
private readonly IpFilialeService _ipFiliale;
|
||||
|
||||
public AuthController(AuthService auth, IpFilialeService ipFiliale)
|
||||
{
|
||||
_auth = auth;
|
||||
_ipFiliale = ipFiliale;
|
||||
}
|
||||
|
||||
// Form-Login: aus <form method="post" action="/auth/login?returnUrl=...">
|
||||
[AllowAnonymous]
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromForm] string kassiererCode, [FromQuery] string? returnUrl)
|
||||
{
|
||||
var user = await _auth.ValidateAsync(kassiererCode ?? "");
|
||||
var user = await _auth.ValidateAsync(kassiererCode ?? string.Empty);
|
||||
if (user is null)
|
||||
return Unauthorized("Ungültiger Code oder Benutzer inaktiv.");
|
||||
|
||||
// 1) Client-IP ermitteln (Reverse-Proxy: X-Forwarded-For beachten)
|
||||
|
||||
|
||||
#if DEBUG
|
||||
var clientIp = IPAddress.Parse("192.168.118.5");
|
||||
#else
|
||||
var clientIp = GetClientIp(HttpContext);
|
||||
#endif
|
||||
|
||||
|
||||
// 2) IP -> FilialId (optional)
|
||||
int? filialId = null;
|
||||
if (clientIp != null)
|
||||
filialId = await _ipFiliale.TryMatchFilialeAsync(clientIp);
|
||||
|
||||
// 3) Claims zusammenstellen
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.KassiererId.ToString()),
|
||||
@ -28,12 +52,15 @@ public class AuthController : ControllerBase
|
||||
new Claim("rollenId", user.RollenId.ToString())
|
||||
};
|
||||
|
||||
if (filialId.HasValue)
|
||||
claims.Add(new Claim("filialId", filialId.Value.ToString()));
|
||||
|
||||
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
|
||||
|
||||
// Nach Erfolg zurück zur gewünschten Seite
|
||||
// 4) Zurück zur Zielseite
|
||||
if (string.IsNullOrWhiteSpace(returnUrl) || !Url.IsLocalUrl(returnUrl))
|
||||
return LocalRedirect("/");
|
||||
|
||||
@ -42,13 +69,13 @@ public class AuthController : ControllerBase
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("/auth/logout")]
|
||||
public IActionResult Logout()
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
// Serverseitig abmelden
|
||||
HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
// Serverseitig abmelden (await nicht vergessen!)
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
// Antwort mit JS, das localStorage löscht und weiterleitet
|
||||
var html = """
|
||||
// Client: localStorage aufräumen & weiterleiten
|
||||
const string html = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head><meta charset="utf-8"><title>Logout</title></head>
|
||||
@ -67,4 +94,18 @@ public class AuthController : ControllerBase
|
||||
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
private static IPAddress? GetClientIp(HttpContext ctx)
|
||||
{
|
||||
// Falls hinter Reverse-Proxy: erstes X-Forwarded-For nehmen
|
||||
var xff = ctx.Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(xff))
|
||||
{
|
||||
var first = xff.Split(',').Select(s => s.Trim()).FirstOrDefault();
|
||||
if (IPAddress.TryParse(first, out var ipFromHeader))
|
||||
return ipFromHeader;
|
||||
}
|
||||
|
||||
return ctx.Connection.RemoteIpAddress;
|
||||
}
|
||||
}
|
||||
|
||||
23
WerksverkaufScanner/WerksverkaufScanner/Data/Filiale.cs
Normal file
23
WerksverkaufScanner/WerksverkaufScanner/Data/Filiale.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WerksverkaufScanner.Data
|
||||
{
|
||||
[Table("FilialienMA", Schema = "KS")]
|
||||
public class Filiale
|
||||
{
|
||||
[Key]
|
||||
public int FilialId { get; set; }
|
||||
public int MandantenId { get; set; }
|
||||
public string? Bezeichnung { get; set; }
|
||||
public string? Level { get; set; }
|
||||
public string? Straße { get; set; }
|
||||
public string? PLZ { get; set; }
|
||||
public string? Ort { get; set; }
|
||||
public string? UstId { get; set; }
|
||||
public string? StNr { get; set; }
|
||||
public int WerkNr { get; set; }
|
||||
public string? Telefon { get; set; }
|
||||
public string? Filialleitung { get; set; }
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ namespace WerksverkaufScanner.Data;
|
||||
public class InventurErfassung
|
||||
{
|
||||
[Key] public int Id { get; set; }
|
||||
public int InventurId { get; set; }
|
||||
public int InventurId { get; set; } = 0;
|
||||
|
||||
public string FilialId { get; set; }
|
||||
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace WerksverkaufScanner.Data;
|
||||
|
||||
public class PreisAenderung
|
||||
{
|
||||
[Key] public int Id { get; set; }
|
||||
|
||||
public string FilialId { get; set; } = default!;
|
||||
|
||||
public int ArtikelId { get; set; }
|
||||
public int ArtikelVariantenId { get; set; }
|
||||
public int Variante { get; set; }
|
||||
|
||||
/// <summary>Preis in Cent (z. B. 599 = 5,99 €)</summary>
|
||||
public int PreisInCents { get; set; }
|
||||
|
||||
public string Benutzer { get; set; } = default!;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@ -6,10 +6,9 @@ namespace WerksverkaufScanner.Data;
|
||||
public class ScannerDb : DbContext
|
||||
{
|
||||
public ScannerDb(DbContextOptions<ScannerDb> options) : base(options) { }
|
||||
|
||||
public DbSet<Artikel> Artikel => Set<Artikel>();
|
||||
public DbSet<InventurErfassung> InventurErfassung => Set<InventurErfassung>();
|
||||
public DbSet<Kassierer> Kassierer { get; set; } = null!;
|
||||
|
||||
|
||||
public DbSet<PreisAenderung> PreisAenderungen => Set<PreisAenderung>();
|
||||
public DbSet<Filiale> Filialen => Set<Filiale>();
|
||||
}
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
@page "/einstellungen"
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>ScannerPilot - Einstellungen</PageTitle>
|
||||
|
||||
<h2>Einstellungen</h2>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p>Lade aktuelle Einstellungen …</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Aktuelle Filial‑ID</dt>
|
||||
<dd class="col-sm-9">@currentFilialId (@currentFilialName)</dd>
|
||||
|
||||
<dt class="col-sm-3">Angemeldeter Benutzer</dt>
|
||||
<dd class="col-sm-9">@currentUser</dd>
|
||||
|
||||
<dt class="col-sm-3">Client‑IP</dt>
|
||||
<dd class="col-sm-9">@currentIp</dd>
|
||||
</dl>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Inject] private FilialService FilialService { get; set; } = default!;
|
||||
[Inject] private AuthenticationStateProvider AuthProvider { get; set; } = default!;
|
||||
[Inject] private IJSRuntime JS { get; set; } = default!;
|
||||
[Inject] private IHttpContextAccessor HttpContextAccessor { get; set; } = default!;
|
||||
|
||||
private string? currentUser;
|
||||
private string? currentIp;
|
||||
private int? currentFilialId;
|
||||
private string? currentFilialName;
|
||||
private bool loading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Angemeldeten Benutzer ermitteln
|
||||
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
||||
currentUser = authState.User.Identity?.Name;
|
||||
|
||||
// Filial‑ID aus localStorage lesen
|
||||
var filialStr = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
|
||||
if (!string.IsNullOrWhiteSpace(filialStr) && int.TryParse(filialStr.Trim('"'), out var filialId))
|
||||
{
|
||||
currentFilialId = filialId;
|
||||
|
||||
// Filialname via DB laden
|
||||
var filiale = await FilialService.GetFilialeAsync(filialId);
|
||||
currentFilialName = filiale?.Bezeichnung;
|
||||
}
|
||||
|
||||
// IP aus HttpContext/ForwardedHeaders
|
||||
var ip = HttpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString();
|
||||
currentIp = ip;
|
||||
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
@ -18,16 +18,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* <div class="col">
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-gear display-5 text-primary mb-2"></i>
|
||||
<h5>Stammdaten</h5>
|
||||
<p class="text-muted">Hier können z.B. Preise etc. angepasst werden</p>
|
||||
<a href="/stammdaten" class="btn btn-primary">Öffnen</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-gear display-5 text-primary mb-2"></i>
|
||||
<h5>Einstellungen</h5>
|
||||
<p class="text-muted">Verwalte Filiale und Inventurparameter.</p>
|
||||
<a href="/inventur/vorbereiten" class="btn btn-outline-primary">Anpassen</a>
|
||||
<p class="text-muted">Hier können Einstellungen wie z.B. die aktuelle IP angezeigt werden.</p>
|
||||
<a href="/einstellungen" class="btn btn-primary">Öffnen</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> *@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -5,9 +5,27 @@
|
||||
|
||||
<h2>Inventur</h2>
|
||||
|
||||
<p>
|
||||
<ul>
|
||||
<li><a href="/inventur/vorbereiten">Inventur vorbereiten</a> - Hier müssen die aktuellen Inventureinstellungen gescannt werden.</li>
|
||||
<li><a href="/inventur/scannen">Inventur scannen</a> - Hier kannst du die Artikel in die Inventurerfassung scannen.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<div class="row row-cols-1 row-cols-md-2 g-3 mt-2">
|
||||
@* <div class="col">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-box-seam display-5 text-primary mb-2"></i>
|
||||
<h5>Inventur vorbereiten</h5>
|
||||
<p class="text-muted">Hier müssen die aktuellen Inventureinstellungen gescannt werden.</p>
|
||||
<a href="/inventur/vorbereiten" class="btn btn-primary">Weiter</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> *@
|
||||
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-gear display-5 text-primary mb-2"></i>
|
||||
<h5>Inventur scannen</h5>
|
||||
<p class="text-muted">Hier kannst du die Artikel in die Inventurerfassung scannen.</p>
|
||||
<a href="/inventur/scannen" class="btn btn-primary">Weiter</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -114,9 +114,8 @@ else
|
||||
try
|
||||
{
|
||||
var f = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
|
||||
var i = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.inventurId");
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(f) || !String.IsNullOrWhiteSpace(i))
|
||||
if (!String.IsNullOrWhiteSpace(f))
|
||||
{
|
||||
await LoadConfigAsync();
|
||||
|
||||
@ -142,7 +141,6 @@ else
|
||||
{
|
||||
needsSetup = true;
|
||||
filialId = null;
|
||||
inventurId = null;
|
||||
}
|
||||
|
||||
}
|
||||
@ -152,25 +150,19 @@ else
|
||||
try
|
||||
{
|
||||
var f = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
|
||||
var i = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.inventurId");
|
||||
var u = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.user");
|
||||
|
||||
filialId = f?.Trim('"');
|
||||
currentUser = u?.Trim('"');
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(i) && int.TryParse(i.Trim('"'), out var inv))
|
||||
inventurId = inv;
|
||||
else
|
||||
inventurId = null;
|
||||
|
||||
// needsSetup = true, wenn irgendwas fehlt/ungültig
|
||||
needsSetup = string.IsNullOrWhiteSpace(filialId) || inventurId is null;
|
||||
needsSetup = string.IsNullOrWhiteSpace(filialId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
needsSetup = true;
|
||||
filialId = null;
|
||||
inventurId = null;
|
||||
currentUser = null;
|
||||
}
|
||||
}
|
||||
@ -278,17 +270,14 @@ else
|
||||
try
|
||||
{
|
||||
// Sicherheitsnetz – noch einmal kurz prüfen (kann durch Tabwechsel gelöscht sein)
|
||||
if (filialId is null || inventurId is null)
|
||||
if (filialId is null)
|
||||
await LoadConfigAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filialId))
|
||||
throw new InvalidOperationException("Filial-ID fehlt.");
|
||||
if (inventurId is null)
|
||||
throw new InvalidOperationException("Inventur-ID fehlt oder ungültig.");
|
||||
|
||||
var rows = await Inventur.SaveAsync(
|
||||
filialId,
|
||||
inventurId.Value,
|
||||
gefunden.ArtikelId,
|
||||
gefunden.ArtikelVariantenId,
|
||||
gefunden.Variante,
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
@page "/stammdaten"
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>ScannerPilot - Stammdaten</PageTitle>
|
||||
|
||||
<h2>Stammdatenänderungen</h2>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2 g-3 mt-2">
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-box-seam display-5 text-primary mb-2"></i>
|
||||
<h5>Preisänderung</h5>
|
||||
<p class="text-muted">Hier können die Preise der Artikel geändert werden.</p>
|
||||
<a href="/stammdaten/preisaenderung" class="btn btn-primary">Weiter</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,332 @@
|
||||
@page "/stammdaten/preisaenderung"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using System.Globalization
|
||||
@using WerksverkaufScanner.Data
|
||||
@using WerksverkaufScanner.Services
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
|
||||
@inject StammdatenCache Cache
|
||||
@inject PreisAenderungSqlService Preise
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<h2>Preisänderung</h2>
|
||||
|
||||
<!-- 1) Barcode ODER Artikelnummer -->
|
||||
<input @ref="barcodeRef" id="barcode"
|
||||
class="form-control form-control-lg mb-2"
|
||||
placeholder="Barcode scannen oder Artikelnummer eingeben"
|
||||
@bind="scanText" @bind:event="oninput"
|
||||
@onkeydown="OnScanKey" />
|
||||
|
||||
@if (varianten is { Count: > 1 })
|
||||
{
|
||||
<div class="mb-3">
|
||||
<div class="mb-1">Varianten gefunden – bitte auswählen:</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@foreach (var art in varianten)
|
||||
{
|
||||
var isActive = ReferenceEquals(gefunden, art);
|
||||
<button type="button"
|
||||
class="btn btn-lg"
|
||||
style="@GetButtonStyle(art, isActive)"
|
||||
@onclick="@(() => VarianteAuswaehlen(art))">
|
||||
@GetVarianteLabel(art)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (gefunden is not null)
|
||||
{
|
||||
<div class="alert alert-info py-2 mb-2">
|
||||
<strong>@gefunden.Bezeichnung</strong>
|
||||
<span class="text-muted">
|
||||
· @gefunden.ArtikelNummer
|
||||
· Variante @gefunden.Variante
|
||||
@if (!string.IsNullOrWhiteSpace(gefunden.Farbe))
|
||||
{
|
||||
<text> · @gefunden.Farbe</text>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(gefunden.Text))
|
||||
{
|
||||
<text> · @gefunden.Text</text>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 2) Preis-Eingabe (masked: 599 -> 5,99) -->
|
||||
<div class="input-group input-group-lg mb-2">
|
||||
<span class="input-group-text">€</span>
|
||||
<input id="preis"
|
||||
class="form-control text-end"
|
||||
inputmode="numeric" pattern="[0-9]*"
|
||||
placeholder="z. B. 599 → 5,99"
|
||||
@bind="priceText" @bind:event="oninput" @bind:after="OnPriceChanged"
|
||||
@onkeydown="OnPriceKey" />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary"
|
||||
disabled="@(!priceCents.HasValue)"
|
||||
@onclick="SpeichernAsync">
|
||||
Speichern
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(status))
|
||||
{
|
||||
<div class="mt-2 @(ok ? "text-success" : "text-danger")">@status</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
// Eingaben/Status
|
||||
private string? scanText;
|
||||
private Artikel? gefunden;
|
||||
private List<Artikel>? varianten;
|
||||
|
||||
private string? priceText; // angezeigter Text ("5,99")
|
||||
private int? priceCents; // intern in Cent (599)
|
||||
private string? status;
|
||||
private bool ok;
|
||||
|
||||
// Fokussteuerung
|
||||
private ElementReference barcodeRef, priceRef;
|
||||
private bool focusBarcode = true;
|
||||
private bool focusPrice;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Cache.GetArtikel().Count == 0)
|
||||
await Cache.RefreshAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (focusBarcode)
|
||||
{
|
||||
focusBarcode = false;
|
||||
await barcodeRef.FocusAsync();
|
||||
}
|
||||
|
||||
if (focusPrice)
|
||||
{
|
||||
focusPrice = false;
|
||||
await JS.InvokeVoidAsync("setTimeout", (Action)(async () =>
|
||||
{
|
||||
await JS.InvokeVoidAsync("document.getElementById", "preis");
|
||||
}), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnScanKey(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
await SucheArtikelAsync();
|
||||
}
|
||||
|
||||
private async Task SucheArtikelAsync()
|
||||
{
|
||||
status = null;
|
||||
ok = false;
|
||||
gefunden = null;
|
||||
varianten = null;
|
||||
priceText = null;
|
||||
priceCents = null;
|
||||
|
||||
var input = scanText?.Trim();
|
||||
scanText = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
focusBarcode = true;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Cache.GetArtikel().Count == 0)
|
||||
await Cache.RefreshAsync();
|
||||
|
||||
var alle = Cache.GetArtikel();
|
||||
|
||||
var matches = alle
|
||||
.Where(a =>
|
||||
string.Equals(a.Barcode, input, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(a.ArtikelNummer, input, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(a => a.Variante)
|
||||
.ThenBy(a => a.ArtikelVariantenId)
|
||||
.ToList();
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
status = $"Kein Artikel gefunden für „{input}“.";
|
||||
focusBarcode = true;
|
||||
}
|
||||
else if (matches.Count == 1)
|
||||
{
|
||||
gefunden = matches[0];
|
||||
varianten = null;
|
||||
status = $"„{gefunden.Bezeichnung}“ – Variante {GetVarianteLabel(gefunden)} ausgewählt. Bitte Preis eingeben.";
|
||||
focusPrice = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
varianten = matches;
|
||||
status = $"{matches.Count} Varianten gefunden – bitte auswählen.";
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void VarianteAuswaehlen(Artikel art)
|
||||
{
|
||||
gefunden = art;
|
||||
status = $"Variante {GetVarianteLabel(art)} ausgewählt. Bitte Preis eingeben.";
|
||||
focusPrice = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
// ---------- Preis-Masking: "599" -> "5,99" (de-DE) ----------
|
||||
private void OnPriceChanged()
|
||||
{
|
||||
var raw = priceText ?? string.Empty;
|
||||
|
||||
// nur Ziffern nehmen
|
||||
var digits = new string(raw.Where(char.IsDigit).ToArray());
|
||||
|
||||
if (string.IsNullOrEmpty(digits))
|
||||
{
|
||||
priceCents = null;
|
||||
priceText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
// führende Nullen tolerieren
|
||||
if (digits.Length > 1)
|
||||
digits = digits.TrimStart('0').PadLeft(1, '0');
|
||||
|
||||
if (!int.TryParse(digits, out var cents))
|
||||
{
|
||||
priceCents = null;
|
||||
return;
|
||||
}
|
||||
|
||||
priceCents = cents;
|
||||
|
||||
// Format de-DE mit Komma
|
||||
var de = CultureInfo.GetCultureInfo("de-DE");
|
||||
priceText = (cents / 100m).ToString("N2", de);
|
||||
}
|
||||
|
||||
private async Task OnPriceKey(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
await SpeichernAsync();
|
||||
}
|
||||
|
||||
private async Task SpeichernAsync()
|
||||
{
|
||||
status = null;
|
||||
ok = false;
|
||||
|
||||
if (gefunden is null)
|
||||
{
|
||||
status = "Bitte zuerst scannen und eine Variante auswählen.";
|
||||
focusBarcode = true;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!priceCents.HasValue || priceCents.Value <= 0)
|
||||
{
|
||||
status = "Bitte einen gültigen Preis eingeben.";
|
||||
focusPrice = true;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Filiale + User aus localStorage
|
||||
var filialIdStr = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
|
||||
var user = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.user");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filialIdStr))
|
||||
throw new InvalidOperationException("Filial-ID fehlt.");
|
||||
if (string.IsNullOrWhiteSpace(user))
|
||||
throw new InvalidOperationException("Benutzername fehlt.");
|
||||
|
||||
filialIdStr = filialIdStr.Trim('"');
|
||||
user = user?.Trim('"');
|
||||
|
||||
var rows = await Preise.SaveAsync(
|
||||
filialIdStr,
|
||||
gefunden.ArtikelId,
|
||||
gefunden.ArtikelVariantenId,
|
||||
gefunden.Variante,
|
||||
priceCents!.Value,
|
||||
user!
|
||||
);
|
||||
|
||||
ok = rows > 0;
|
||||
status = ok
|
||||
? $"Preis gespeichert: {gefunden.ArtikelNummer} – Variante {GetVarianteLabel(gefunden)} · {FormatEuro(priceCents!.Value)}."
|
||||
: "Nichts gespeichert.";
|
||||
|
||||
// Reset für nächste Eingabe
|
||||
priceText = null;
|
||||
priceCents = null;
|
||||
gefunden = null;
|
||||
varianten = null;
|
||||
focusBarcode = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ok = false;
|
||||
status = "Fehler beim Speichern: " + ex.Message;
|
||||
focusPrice = true;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
// ---------- Helfer ----------
|
||||
|
||||
private string GetVarianteLabel(Artikel art)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(art.Text)) return art.Text;
|
||||
if (!string.IsNullOrWhiteSpace(art.Farbe)) return art.Farbe;
|
||||
return $"Variante {art.Variante}";
|
||||
}
|
||||
|
||||
private string GetButtonStyle(Artikel art, bool isActive)
|
||||
{
|
||||
var bg = GetCssColorFromFarbe(art.Farbe);
|
||||
var border = isActive ? "3px solid black" : "1px solid #666";
|
||||
var color = NeedsDarkText(art.Farbe) ? "black" : "white";
|
||||
return $"background-color:{bg};color:{color};border:{border};min-width:6rem;";
|
||||
}
|
||||
|
||||
private string GetCssColorFromFarbe(string? farbe)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(farbe)) return "#e0e0e0";
|
||||
switch (farbe.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "rot": return "#dc3545";
|
||||
case "blau": return "#007bff";
|
||||
case "gruen":
|
||||
case "grün": return "#28a745";
|
||||
case "gelb": return "#ffc107";
|
||||
case "orange": return "#fd7e14";
|
||||
case "lila": return "#6f42c1";
|
||||
default: return "#e0e0e0";
|
||||
}
|
||||
}
|
||||
|
||||
private bool NeedsDarkText(string? farbe)
|
||||
=> string.IsNullOrWhiteSpace(farbe) || farbe.Trim().ToLowerInvariant() is "gelb" or "orange";
|
||||
|
||||
private static string FormatEuro(int cents)
|
||||
=> (cents / 100m).ToString("C", CultureInfo.GetCultureInfo("de-DE"));
|
||||
}
|
||||
@ -1,94 +1,18 @@
|
||||
//using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
//using Microsoft.AspNetCore.HttpOverrides;
|
||||
//using Microsoft.EntityFrameworkCore;
|
||||
//using WerksverkaufScanner.Data;
|
||||
//using WerksverkaufScanner.Services;
|
||||
|
||||
//var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
//// Optional: explizit Port/Bind-Adresse setzen
|
||||
//builder.WebHost.UseUrls("https://0.0.0.0:3300");
|
||||
////builder.WebHost.UseUrls("http://0.0.0.0:3300");
|
||||
|
||||
//// 1) ConnectionString prüfen
|
||||
//var cs = builder.Configuration.GetConnectionString("Default");
|
||||
//if (string.IsNullOrWhiteSpace(cs))
|
||||
// throw new InvalidOperationException("ConnectionStrings:Default fehlt oder ist leer.");
|
||||
|
||||
//// 2) Framework-Services
|
||||
//builder.Services.AddRazorPages();
|
||||
//builder.Services.AddServerSideBlazor();
|
||||
//builder.Services.AddControllers(); // für AuthController (Login/Logout)
|
||||
//builder.Services.AddHttpClient(); // für Logout/Calls aus Layout/Login
|
||||
//builder.Services.AddHttpContextAccessor(); // falls Services HttpContext brauchen
|
||||
|
||||
//// 3) Auth/Authorization (Cookie)
|
||||
//builder.Services
|
||||
// .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
// .AddCookie(o =>
|
||||
// {
|
||||
// o.LoginPath = "/login"; // ohne Auth -> Redirect hierher
|
||||
// o.AccessDeniedPath = "/login";
|
||||
// o.ReturnUrlParameter = "returnUrl";
|
||||
// o.SlidingExpiration = true;
|
||||
// o.ExpireTimeSpan = TimeSpan.FromDays(7);
|
||||
|
||||
// // Cookie-Härtung
|
||||
// o.Cookie.Name = "Werksverkauf.Auth";
|
||||
// o.Cookie.HttpOnly = true;
|
||||
// o.Cookie.SecurePolicy = CookieSecurePolicy.None; // nur über HTTPS eigentlich .Always
|
||||
// o.Cookie.SameSite = SameSiteMode.Lax; // für normale Navigations-Requests
|
||||
// });
|
||||
//builder.Services.AddAuthorization();
|
||||
|
||||
//// 4) App-Services & Datenzugriff
|
||||
//builder.Services.AddDbContextFactory<ScannerDb>(opt => opt.UseSqlServer(cs));
|
||||
//builder.Services.AddSingleton<StammdatenCache>();
|
||||
//builder.Services.AddScoped<InventurService>();
|
||||
//builder.Services.AddScoped<AuthService>(); // prüft KassenLogin in DB
|
||||
|
||||
//// (Optional) Forwarded Headers, wenn hinter Proxy/LoadBalancer (NGINX/IIS/K8s)
|
||||
//builder.Services.Configure<ForwardedHeadersOptions>(opt =>
|
||||
//{
|
||||
// opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
// // opt.KnownProxies.Add(IPAddress.Parse("127.0.0.1")); // bei Bedarf setzen
|
||||
//});
|
||||
|
||||
//var app = builder.Build();
|
||||
|
||||
//// 5) Middleware-Pipeline
|
||||
//app.UseForwardedHeaders(); // vor allem anderen, wenn Proxy im Spiel
|
||||
//app.UseStaticFiles();
|
||||
|
||||
//app.UseRouting();
|
||||
|
||||
//app.UseAuthentication(); // erst Auth…
|
||||
//app.UseAuthorization(); // …dann Authorization
|
||||
|
||||
//// 6) Endpoints
|
||||
//app.MapControllers(); // /auth/login, /auth/logout, /auth/me
|
||||
//app.MapBlazorHub();
|
||||
//app.MapFallbackToPage("/_Host");
|
||||
|
||||
//app.Run();
|
||||
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WerksverkaufScanner.Data;
|
||||
using WerksverkaufScanner.Services;
|
||||
// using System.Net; // nur nötig, wenn du KnownProxies/KnownNetworks setzt
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// --- WICHTIG FÜR IIS: KEIN eigenes HTTPS/UseUrls hier setzen! ---
|
||||
// Kestrel lauscht hinter IIS nicht selbst auf :443. TLS terminiert im IIS.
|
||||
Entfernt: builder.WebHost.UseUrls("https://0.0.0.0:3300");
|
||||
// --- WICHTIG FÜR IIS/REVERSE PROXY ---
|
||||
// KEIN eigenes HTTPS/UseUrls setzen; TLS terminiert im IIS/Proxy.
|
||||
// Falls du explizit klarstellen willst, dass IIS verwendet wird:
|
||||
builder.WebHost.UseUrls("https://0.0.0.0:3300");
|
||||
//builder.WebHost.UseIIS();
|
||||
|
||||
// Optional (schadet nicht, macht die Absicht klar):
|
||||
builder.WebHost.UseIIS();
|
||||
|
||||
// 1) ConnectionString prüfen
|
||||
// 1) ConnectionString prüfen
|
||||
var cs = builder.Configuration.GetConnectionString("Default");
|
||||
if (string.IsNullOrWhiteSpace(cs))
|
||||
throw new InvalidOperationException("ConnectionStrings:Default fehlt oder ist leer.");
|
||||
@ -96,9 +20,9 @@ if (string.IsNullOrWhiteSpace(cs))
|
||||
// 2) Framework-Services
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddServerSideBlazor();
|
||||
builder.Services.AddControllers(); // für AuthController (Login/Logout)
|
||||
builder.Services.AddHttpClient(); // für Logout/Calls aus Layout/Login
|
||||
builder.Services.AddHttpContextAccessor(); // falls Services HttpContext brauchen
|
||||
builder.Services.AddControllers(); // AuthController (Login/Logout)
|
||||
builder.Services.AddHttpClient(); // HTTP-Calls (optional)
|
||||
builder.Services.AddHttpContextAccessor(); // wenn Services HttpContext brauchen
|
||||
|
||||
// 3) Auth/Authorization (Cookie)
|
||||
builder.Services
|
||||
@ -109,13 +33,12 @@ builder.Services
|
||||
o.AccessDeniedPath = "/login";
|
||||
o.ReturnUrlParameter = "returnUrl";
|
||||
o.SlidingExpiration = true;
|
||||
o.ExpireTimeSpan = TimeSpan.FromHours(15); //somit AutoLogoff nach 15 Stunden
|
||||
o.ExpireTimeSpan = TimeSpan.FromHours(15); // Auto-Logout nach 15h
|
||||
|
||||
// Cookie-Härtung:
|
||||
// Cookie-Härtung:
|
||||
o.Cookie.Name = "Werksverkauf.Auth";
|
||||
o.Cookie.HttpOnly = true;
|
||||
// Hinter IIS wird über HTTPS ausgeliefert -> Always ist korrekt
|
||||
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
o.Cookie.SecurePolicy = CookieSecurePolicy.Always; // hinter IIS ok
|
||||
o.Cookie.SameSite = SameSiteMode.Lax;
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
@ -125,20 +48,23 @@ builder.Services.AddDbContextFactory<ScannerDb>(opt => opt.UseSqlServer(cs));
|
||||
builder.Services.AddSingleton<StammdatenCache>();
|
||||
builder.Services.AddScoped<InventurService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<PreisAenderungSqlService>();
|
||||
builder.Services.AddScoped<IpFilialeService>(); // <— für IP→Filiale-Mapping
|
||||
builder.Services.AddScoped<FilialService>();
|
||||
|
||||
// (Optional) Forwarded Headers – sinnvoll hinter IIS/Proxy
|
||||
// (Optional) Forwarded Headers – sinnvoll hinter IIS/Proxy
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(opt =>
|
||||
{
|
||||
opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
// Falls du strikte Proxy-Liste willst:
|
||||
// opt.KnownProxies.Add(IPAddress.Parse("10.250.1.30"));
|
||||
// Falls du strikt nur bestimmte Proxys erlauben willst:
|
||||
// opt.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 5) Middleware-Pipeline
|
||||
app.UseForwardedHeaders(); // vor allem anderen, wenn Proxy im Spiel
|
||||
app.UseHttpsRedirection(); // ok hinter IIS, nutzt X-Forwarded-Proto
|
||||
app.UseForwardedHeaders(); // vor HttpsRedirection/Authentication
|
||||
app.UseHttpsRedirection(); // nutzt X-Forwarded-Proto hinter Proxy
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
@ -151,11 +77,10 @@ app.MapControllers();
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
||||
// --- Für lokalen Start (ohne IIS) z. B. auf HTTP-Port 3300 testen ---
|
||||
// --- Nur für lokalen Start OHNE IIS, wenn du Kestrel direkt nutzen willst ---
|
||||
// if (!app.Environment.IsProduction())
|
||||
// {
|
||||
// app.Urls.Add("http://localhost:3300");
|
||||
// }
|
||||
|
||||
app.Run();
|
||||
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WerksverkaufScanner.Data;
|
||||
|
||||
namespace WerksverkaufScanner.Services
|
||||
{
|
||||
public class FilialService
|
||||
{
|
||||
private readonly IDbContextFactory<ScannerDb> _dbFactory;
|
||||
|
||||
public FilialService(IDbContextFactory<ScannerDb> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task<Filiale?> GetFilialeAsync(int filialId)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
return await db.Set<Filiale>()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(f => f.FilialId == filialId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,7 @@ public class InventurService
|
||||
_filialId = config.GetValue<int>("FilialId");
|
||||
}
|
||||
|
||||
public async Task<int> SaveAsync(string filialId, int inventurId, int artikelId, int artikelVariantenId, int variante, int menge, string user)
|
||||
public async Task<int> SaveAsync(string filialId, int artikelId, int artikelVariantenId, int variante, int menge, string user)
|
||||
{
|
||||
var clientIp = GetClientIp();
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
@ -29,7 +29,7 @@ public class InventurService
|
||||
{
|
||||
|
||||
FilialId = filialId,
|
||||
InventurId = inventurId,
|
||||
InventurId = 0,
|
||||
ArtikelId = artikelId,
|
||||
ArtikelVariantenId = artikelVariantenId,
|
||||
Variante = variante,
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
// Services/IpFilialeService.cs
|
||||
using System.Net;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using System.Data;
|
||||
|
||||
namespace WerksverkaufScanner.Services;
|
||||
|
||||
public class IpFilialeService
|
||||
{
|
||||
private readonly string _cs;
|
||||
public IpFilialeService(IConfiguration cfg)
|
||||
=> _cs = cfg.GetConnectionString("Default")
|
||||
?? throw new InvalidOperationException("ConnectionStrings:Default fehlt.");
|
||||
|
||||
public async Task<int?> TryMatchFilialeAsync(IPAddress clientIp)
|
||||
{
|
||||
if (clientIp == null) return null;
|
||||
|
||||
await using var con = new SqlConnection(_cs);
|
||||
await con.OpenAsync();
|
||||
|
||||
const string sql = @"
|
||||
SELECT FilialId, IpStart, IpEnd
|
||||
FROM [KassensystemTest].[KS].[FilialIpRange] WITH (NOLOCK);";
|
||||
|
||||
var ranges = new List<(int FilialId, IPAddress Start, IPAddress End)>();
|
||||
|
||||
await using (var cmd = new SqlCommand(sql, con))
|
||||
await using (var r = await cmd.ExecuteReaderAsync())
|
||||
{
|
||||
while (await r.ReadAsync())
|
||||
{
|
||||
var fid = r.GetInt32(0);
|
||||
if (IPAddress.TryParse(r.GetString(1), out var start) &&
|
||||
IPAddress.TryParse(r.GetString(2), out var end))
|
||||
{
|
||||
ranges.Add((fid, start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var rg in ranges)
|
||||
{
|
||||
if (IsInRange(clientIp, rg.Start, rg.End))
|
||||
return rg.FilialId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsInRange(IPAddress ip, IPAddress start, IPAddress end)
|
||||
{
|
||||
// nur IPv4: in Bytes vergleichen
|
||||
var ipBytes = ip.GetAddressBytes();
|
||||
var startBytes = start.GetAddressBytes();
|
||||
var endBytes = end.GetAddressBytes();
|
||||
|
||||
if (ipBytes.Length != startBytes.Length || ipBytes.Length != endBytes.Length)
|
||||
return false; // IPv4/IPv6 mismatch
|
||||
|
||||
bool gteStart = true, lteEnd = true;
|
||||
for (int i = 0; i < ipBytes.Length; i++)
|
||||
{
|
||||
if (ipBytes[i] < startBytes[i]) { gteStart = false; break; }
|
||||
if (ipBytes[i] > startBytes[i]) { break; }
|
||||
}
|
||||
for (int i = 0; i < ipBytes.Length; i++)
|
||||
{
|
||||
if (ipBytes[i] > endBytes[i]) { lteEnd = false; break; }
|
||||
if (ipBytes[i] < endBytes[i]) { break; }
|
||||
}
|
||||
return gteStart && lteEnd;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace WerksverkaufScanner.Services;
|
||||
|
||||
public class PreisAenderungSqlService
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public PreisAenderungSqlService(IConfiguration cfg)
|
||||
{
|
||||
_connectionString = cfg.GetConnectionString("Default")
|
||||
?? throw new InvalidOperationException("Connection string 'Default' fehlt.");
|
||||
}
|
||||
|
||||
public async Task<int> SaveAsync(
|
||||
string filialId,
|
||||
int artikelId,
|
||||
int artikelVariantenId,
|
||||
int variante,
|
||||
int preisInCents,
|
||||
string benutzer)
|
||||
{
|
||||
// Preis aus Cent zu Euro (decimal)
|
||||
var neuerPreis = preisInCents / 100m;
|
||||
|
||||
await using var con = new SqlConnection(_connectionString);
|
||||
await con.OpenAsync();
|
||||
|
||||
await using var tx = await con.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// 1️⃣ Alten Preis auslesen
|
||||
decimal alterPreis = 0;
|
||||
const string selectSql = @"
|
||||
SELECT ISNULL(VerkaufsPreis, 0)
|
||||
FROM [Kassensystem].[KS].[ArtikelVariantenFI]
|
||||
WHERE FilialId = @FilialId AND ArtikelVariantenId = @ArtikelVariantenId;";
|
||||
|
||||
await using (var selectCmd = new SqlCommand(selectSql, con, (SqlTransaction)tx))
|
||||
{
|
||||
selectCmd.Parameters.Add("@FilialId", SqlDbType.NVarChar, 20).Value = filialId;
|
||||
selectCmd.Parameters.Add("@ArtikelVariantenId", SqlDbType.Int).Value = artikelVariantenId;
|
||||
|
||||
var result = await selectCmd.ExecuteScalarAsync();
|
||||
if (result != null && result != DBNull.Value)
|
||||
alterPreis = Convert.ToDecimal(result);
|
||||
}
|
||||
|
||||
// 2️⃣ Update im ArtikelVariantenFI
|
||||
const string updateSql = @"
|
||||
UPDATE [Kassensystem].[KS].[ArtikelVariantenFI]
|
||||
SET VerkaufsPreis = @NeuerPreis,
|
||||
LetzteÄnderung = SYSUTCDATETIME(),
|
||||
VonUser = @User
|
||||
WHERE FilialId = @FilialId AND ArtikelVariantenId = @ArtikelVariantenId;";
|
||||
|
||||
await using (var updateCmd = new SqlCommand(updateSql, con, (SqlTransaction)tx))
|
||||
{
|
||||
updateCmd.Parameters.Add("@NeuerPreis", SqlDbType.Decimal).Value = neuerPreis;
|
||||
updateCmd.Parameters.Add("@User", SqlDbType.NVarChar, 100).Value = benutzer;
|
||||
updateCmd.Parameters.Add("@FilialId", SqlDbType.NVarChar, 20).Value = filialId;
|
||||
updateCmd.Parameters.Add("@ArtikelVariantenId", SqlDbType.Int).Value = artikelVariantenId;
|
||||
|
||||
await updateCmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// 3️⃣ Insert in PreisHistorie
|
||||
const string insertSql = @"
|
||||
INSERT INTO [Kassensystem].[KS].[PreisHistorie]
|
||||
(FilialId, ArtikelVariantenId, AlterPreis, NeuerPreis, Zeitpunkt, Anwender)
|
||||
VALUES
|
||||
(@FilialId, @ArtikelVariantenId, @AlterPreis, @NeuerPreis, SYSUTCDATETIME(), @User);";
|
||||
|
||||
await using (var insertCmd = new SqlCommand(insertSql, con, (SqlTransaction)tx))
|
||||
{
|
||||
insertCmd.Parameters.Add("@FilialId", SqlDbType.NVarChar, 20).Value = filialId;
|
||||
insertCmd.Parameters.Add("@ArtikelVariantenId", SqlDbType.Int).Value = artikelVariantenId;
|
||||
insertCmd.Parameters.Add("@AlterPreis", SqlDbType.Decimal).Value = alterPreis;
|
||||
insertCmd.Parameters.Add("@NeuerPreis", SqlDbType.Decimal).Value = neuerPreis;
|
||||
insertCmd.Parameters.Add("@User", SqlDbType.NVarChar, 100).Value = benutzer;
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
return 1; // Erfolg
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,10 @@
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="app-shell">
|
||||
<!-- Topbar -->
|
||||
<nav class="topbar navbar navbar-expand-md navbar-dark topbar-gradient">
|
||||
<div class="container-fluid">
|
||||
<!-- Toggler: öffnet auf Mobil das Offcanvas #sideMenu -->
|
||||
<!-- Mobile: Offcanvas-Toggler -->
|
||||
<button class="navbar-toggler d-inline-flex d-md-none"
|
||||
type="button"
|
||||
data-bs-toggle="offcanvas"
|
||||
@ -33,7 +34,7 @@
|
||||
</nav>
|
||||
|
||||
<div class="body-row">
|
||||
<!-- DESKTOP-SIDEBAR (ab md sichtbar) -->
|
||||
<!-- Sidebar (Desktop) -->
|
||||
<aside class="sidebar border-end d-none d-md-block">
|
||||
<div class="p-3">
|
||||
<ul class="nav flex-column gap-1">
|
||||
@ -42,12 +43,18 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/inventur" Match="NavLinkMatch.Prefix">Inventur</NavLink>
|
||||
</li>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/stammdaten" Match="NavLinkMatch.Prefix">Stammdaten</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/einstellungen" Match="NavLinkMatch.Prefix">Einstellungen</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- MOBILE-OFFCANVAS (unter md als Overlay) -->
|
||||
<!-- Offcanvas (Mobil) -->
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div id="sideMenu"
|
||||
@ -69,6 +76,14 @@
|
||||
<NavLink class="nav-link text-white" href="/inventur" Match="NavLinkMatch.Prefix"
|
||||
@onclick="CloseMenu">Inventur</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/stammdaten" Match="NavLinkMatch.Prefix"
|
||||
@onclick="CloseMenu">Stammdaten</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/einstellungen" Match="NavLinkMatch.Prefix"
|
||||
@onclick="CloseMenu">Einstellungen</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-auto pt-3 border-top border-light">
|
||||
@ -82,7 +97,7 @@
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
|
||||
<!-- Content -->
|
||||
<main class="content d-flex flex-column">
|
||||
<div class="flex-grow-1 container-fluid p-3 p-md-4">
|
||||
@Body
|
||||
@ -97,26 +112,55 @@
|
||||
@code {
|
||||
private async Task CloseMenu()
|
||||
{
|
||||
// Offcanvas schließen, Navigation des NavLink bleibt erhalten
|
||||
// Offcanvas schließen (NavLink-Navigation bleibt erhalten)
|
||||
await JS.InvokeVoidAsync("hideOffcanvas", "#sideMenu");
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
if (!firstRender) return;
|
||||
|
||||
if (user.Identity is { IsAuthenticated: true })
|
||||
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
|
||||
if (user.Identity is { IsAuthenticated: true })
|
||||
{
|
||||
// 1) Benutzername in localStorage schreiben
|
||||
var username = user.Identity?.Name ?? "(unbekannt)";
|
||||
try
|
||||
{
|
||||
var username = user.Identity?.Name ?? "(unbekannt)";
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "scannerpilot.user", username);
|
||||
}
|
||||
else
|
||||
catch { /* no-op */ }
|
||||
|
||||
// 2) Falls der Login/Controller eine Filial-ID als Claim mitliefert,
|
||||
// schreiben wir sie NUR dann in localStorage, wenn dort noch nichts steht.
|
||||
// -> QR-Konfiguration hat immer Vorrang!
|
||||
var filialClaim = user.FindFirst("filialId")?.Value
|
||||
?? user.FindFirst("filiale")?.Value; // fallback, falls anders benannt
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filialClaim))
|
||||
{
|
||||
try
|
||||
{
|
||||
var existingFil = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
|
||||
if (string.IsNullOrWhiteSpace(existingFil))
|
||||
{
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "scannerpilot.filialId", filialClaim);
|
||||
}
|
||||
}
|
||||
catch { /* no-op */ }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Abgemeldet: lokale Infos säubern (nur user hier; Filial/Inventur bleiben
|
||||
// bewusst bestehen, damit jemand sich neu anmelden kann ohne QR zu verlieren)
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("localStorage.removeItem", "scannerpilot.user");
|
||||
}
|
||||
catch { /* no-op */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user