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.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Security.Claims;
|
using WerksverkaufScanner.Services; // <-- für IpFilialeService
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("auth")]
|
[Route("auth")]
|
||||||
public class AuthController : ControllerBase
|
public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AuthService _auth;
|
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=...">
|
// Form-Login: aus <form method="post" action="/auth/login?returnUrl=...">
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<IActionResult> Login([FromForm] string kassiererCode, [FromQuery] string? returnUrl)
|
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)
|
if (user is null)
|
||||||
return Unauthorized("Ungültiger Code oder Benutzer inaktiv.");
|
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>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.NameIdentifier, user.KassiererId.ToString()),
|
new Claim(ClaimTypes.NameIdentifier, user.KassiererId.ToString()),
|
||||||
@ -28,12 +52,15 @@ public class AuthController : ControllerBase
|
|||||||
new Claim("rollenId", user.RollenId.ToString())
|
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 identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
var principal = new ClaimsPrincipal(identity);
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
|
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))
|
if (string.IsNullOrWhiteSpace(returnUrl) || !Url.IsLocalUrl(returnUrl))
|
||||||
return LocalRedirect("/");
|
return LocalRedirect("/");
|
||||||
|
|
||||||
@ -42,13 +69,13 @@ public class AuthController : ControllerBase
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpPost("/auth/logout")]
|
[HttpPost("/auth/logout")]
|
||||||
public IActionResult Logout()
|
public async Task<IActionResult> Logout()
|
||||||
{
|
{
|
||||||
// Serverseitig abmelden
|
// Serverseitig abmelden (await nicht vergessen!)
|
||||||
HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
// Antwort mit JS, das localStorage löscht und weiterleitet
|
// Client: localStorage aufräumen & weiterleiten
|
||||||
var html = """
|
const string html = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head><meta charset="utf-8"><title>Logout</title></head>
|
<head><meta charset="utf-8"><title>Logout</title></head>
|
||||||
@ -67,4 +94,18 @@ public class AuthController : ControllerBase
|
|||||||
|
|
||||||
return Content(html, "text/html");
|
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
|
public class InventurErfassung
|
||||||
{
|
{
|
||||||
[Key] public int Id { get; set; }
|
[Key] public int Id { get; set; }
|
||||||
public int InventurId { get; set; }
|
public int InventurId { get; set; } = 0;
|
||||||
|
|
||||||
public string FilialId { get; set; }
|
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 class ScannerDb : DbContext
|
||||||
{
|
{
|
||||||
public ScannerDb(DbContextOptions<ScannerDb> options) : base(options) { }
|
public ScannerDb(DbContextOptions<ScannerDb> options) : base(options) { }
|
||||||
|
|
||||||
public DbSet<Artikel> Artikel => Set<Artikel>();
|
public DbSet<Artikel> Artikel => Set<Artikel>();
|
||||||
public DbSet<InventurErfassung> InventurErfassung => Set<InventurErfassung>();
|
public DbSet<InventurErfassung> InventurErfassung => Set<InventurErfassung>();
|
||||||
public DbSet<Kassierer> Kassierer { get; set; } = null!;
|
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>
|
</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 h-100 shadow-sm border-0">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<i class="bi bi-gear display-5 text-primary mb-2"></i>
|
<i class="bi bi-gear display-5 text-primary mb-2"></i>
|
||||||
<h5>Einstellungen</h5>
|
<h5>Einstellungen</h5>
|
||||||
<p class="text-muted">Verwalte Filiale und Inventurparameter.</p>
|
<p class="text-muted">Hier können Einstellungen wie z.B. die aktuelle IP angezeigt werden.</p>
|
||||||
<a href="/inventur/vorbereiten" class="btn btn-outline-primary">Anpassen</a>
|
<a href="/einstellungen" class="btn btn-primary">Öffnen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> *@
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,27 @@
|
|||||||
|
|
||||||
<h2>Inventur</h2>
|
<h2>Inventur</h2>
|
||||||
|
|
||||||
<p>
|
<div class="row row-cols-1 row-cols-md-2 g-3 mt-2">
|
||||||
<ul>
|
@* <div class="col">
|
||||||
<li><a href="/inventur/vorbereiten">Inventur vorbereiten</a> - Hier müssen die aktuellen Inventureinstellungen gescannt werden.</li>
|
<div class="card h-100 shadow-sm border-0">
|
||||||
<li><a href="/inventur/scannen">Inventur scannen</a> - Hier kannst du die Artikel in die Inventurerfassung scannen.</li>
|
<div class="card-body text-center">
|
||||||
</ul>
|
<i class="bi bi-box-seam display-5 text-primary mb-2"></i>
|
||||||
</p>
|
<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
|
try
|
||||||
{
|
{
|
||||||
var f = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
|
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();
|
await LoadConfigAsync();
|
||||||
|
|
||||||
@ -142,7 +141,6 @@ else
|
|||||||
{
|
{
|
||||||
needsSetup = true;
|
needsSetup = true;
|
||||||
filialId = null;
|
filialId = null;
|
||||||
inventurId = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -152,25 +150,19 @@ else
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var f = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.filialId");
|
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");
|
var u = await JS.InvokeAsync<string?>("localStorage.getItem", "scannerpilot.user");
|
||||||
|
|
||||||
filialId = f?.Trim('"');
|
filialId = f?.Trim('"');
|
||||||
currentUser = u?.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 = true, wenn irgendwas fehlt/ungültig
|
||||||
needsSetup = string.IsNullOrWhiteSpace(filialId) || inventurId is null;
|
needsSetup = string.IsNullOrWhiteSpace(filialId);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
needsSetup = true;
|
needsSetup = true;
|
||||||
filialId = null;
|
filialId = null;
|
||||||
inventurId = null;
|
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,17 +270,14 @@ else
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Sicherheitsnetz – noch einmal kurz prüfen (kann durch Tabwechsel gelöscht sein)
|
// 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();
|
await LoadConfigAsync();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(filialId))
|
if (string.IsNullOrWhiteSpace(filialId))
|
||||||
throw new InvalidOperationException("Filial-ID fehlt.");
|
throw new InvalidOperationException("Filial-ID fehlt.");
|
||||||
if (inventurId is null)
|
|
||||||
throw new InvalidOperationException("Inventur-ID fehlt oder ungültig.");
|
|
||||||
|
|
||||||
var rows = await Inventur.SaveAsync(
|
var rows = await Inventur.SaveAsync(
|
||||||
filialId,
|
filialId,
|
||||||
inventurId.Value,
|
|
||||||
gefunden.ArtikelId,
|
gefunden.ArtikelId,
|
||||||
gefunden.ArtikelVariantenId,
|
gefunden.ArtikelVariantenId,
|
||||||
gefunden.Variante,
|
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.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.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using WerksverkaufScanner.Data;
|
using WerksverkaufScanner.Data;
|
||||||
using WerksverkaufScanner.Services;
|
using WerksverkaufScanner.Services;
|
||||||
// using System.Net; // nur nötig, wenn du KnownProxies/KnownNetworks setzt
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// --- WICHTIG FÜR IIS: KEIN eigenes HTTPS/UseUrls hier setzen! ---
|
// --- WICHTIG FÜR IIS/REVERSE PROXY ---
|
||||||
// Kestrel lauscht hinter IIS nicht selbst auf :443. TLS terminiert im IIS.
|
// KEIN eigenes HTTPS/UseUrls setzen; TLS terminiert im IIS/Proxy.
|
||||||
Entfernt: builder.WebHost.UseUrls("https://0.0.0.0:3300");
|
// 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):
|
// 1) ConnectionString prüfen
|
||||||
builder.WebHost.UseIIS();
|
|
||||||
|
|
||||||
// 1) ConnectionString prüfen
|
|
||||||
var cs = builder.Configuration.GetConnectionString("Default");
|
var cs = builder.Configuration.GetConnectionString("Default");
|
||||||
if (string.IsNullOrWhiteSpace(cs))
|
if (string.IsNullOrWhiteSpace(cs))
|
||||||
throw new InvalidOperationException("ConnectionStrings:Default fehlt oder ist leer.");
|
throw new InvalidOperationException("ConnectionStrings:Default fehlt oder ist leer.");
|
||||||
@ -96,9 +20,9 @@ if (string.IsNullOrWhiteSpace(cs))
|
|||||||
// 2) Framework-Services
|
// 2) Framework-Services
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
builder.Services.AddServerSideBlazor();
|
builder.Services.AddServerSideBlazor();
|
||||||
builder.Services.AddControllers(); // für AuthController (Login/Logout)
|
builder.Services.AddControllers(); // AuthController (Login/Logout)
|
||||||
builder.Services.AddHttpClient(); // für Logout/Calls aus Layout/Login
|
builder.Services.AddHttpClient(); // HTTP-Calls (optional)
|
||||||
builder.Services.AddHttpContextAccessor(); // falls Services HttpContext brauchen
|
builder.Services.AddHttpContextAccessor(); // wenn Services HttpContext brauchen
|
||||||
|
|
||||||
// 3) Auth/Authorization (Cookie)
|
// 3) Auth/Authorization (Cookie)
|
||||||
builder.Services
|
builder.Services
|
||||||
@ -109,13 +33,12 @@ builder.Services
|
|||||||
o.AccessDeniedPath = "/login";
|
o.AccessDeniedPath = "/login";
|
||||||
o.ReturnUrlParameter = "returnUrl";
|
o.ReturnUrlParameter = "returnUrl";
|
||||||
o.SlidingExpiration = true;
|
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.Name = "Werksverkauf.Auth";
|
||||||
o.Cookie.HttpOnly = true;
|
o.Cookie.HttpOnly = true;
|
||||||
// Hinter IIS wird über HTTPS ausgeliefert -> Always ist korrekt
|
o.Cookie.SecurePolicy = CookieSecurePolicy.Always; // hinter IIS ok
|
||||||
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
|
||||||
o.Cookie.SameSite = SameSiteMode.Lax;
|
o.Cookie.SameSite = SameSiteMode.Lax;
|
||||||
});
|
});
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
@ -125,20 +48,23 @@ builder.Services.AddDbContextFactory<ScannerDb>(opt => opt.UseSqlServer(cs));
|
|||||||
builder.Services.AddSingleton<StammdatenCache>();
|
builder.Services.AddSingleton<StammdatenCache>();
|
||||||
builder.Services.AddScoped<InventurService>();
|
builder.Services.AddScoped<InventurService>();
|
||||||
builder.Services.AddScoped<AuthService>();
|
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 =>
|
builder.Services.Configure<ForwardedHeadersOptions>(opt =>
|
||||||
{
|
{
|
||||||
opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
// Falls du strikte Proxy-Liste willst:
|
// Falls du strikt nur bestimmte Proxys erlauben willst:
|
||||||
// opt.KnownProxies.Add(IPAddress.Parse("10.250.1.30"));
|
// opt.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// 5) Middleware-Pipeline
|
// 5) Middleware-Pipeline
|
||||||
app.UseForwardedHeaders(); // vor allem anderen, wenn Proxy im Spiel
|
app.UseForwardedHeaders(); // vor HttpsRedirection/Authentication
|
||||||
app.UseHttpsRedirection(); // ok hinter IIS, nutzt X-Forwarded-Proto
|
app.UseHttpsRedirection(); // nutzt X-Forwarded-Proto hinter Proxy
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
@ -151,11 +77,10 @@ app.MapControllers();
|
|||||||
app.MapBlazorHub();
|
app.MapBlazorHub();
|
||||||
app.MapFallbackToPage("/_Host");
|
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())
|
// if (!app.Environment.IsProduction())
|
||||||
// {
|
// {
|
||||||
// app.Urls.Add("http://localhost:3300");
|
// app.Urls.Add("http://localhost:3300");
|
||||||
// }
|
// }
|
||||||
|
|
||||||
app.Run();
|
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");
|
_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();
|
var clientIp = GetClientIp();
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@ -29,7 +29,7 @@ public class InventurService
|
|||||||
{
|
{
|
||||||
|
|
||||||
FilialId = filialId,
|
FilialId = filialId,
|
||||||
InventurId = inventurId,
|
InventurId = 0,
|
||||||
ArtikelId = artikelId,
|
ArtikelId = artikelId,
|
||||||
ArtikelVariantenId = artikelVariantenId,
|
ArtikelVariantenId = artikelVariantenId,
|
||||||
Variante = variante,
|
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
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
|
<!-- Topbar -->
|
||||||
<nav class="topbar navbar navbar-expand-md navbar-dark topbar-gradient">
|
<nav class="topbar navbar navbar-expand-md navbar-dark topbar-gradient">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Toggler: öffnet auf Mobil das Offcanvas #sideMenu -->
|
<!-- Mobile: Offcanvas-Toggler -->
|
||||||
<button class="navbar-toggler d-inline-flex d-md-none"
|
<button class="navbar-toggler d-inline-flex d-md-none"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="offcanvas"
|
data-bs-toggle="offcanvas"
|
||||||
@ -33,7 +34,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="body-row">
|
<div class="body-row">
|
||||||
<!-- DESKTOP-SIDEBAR (ab md sichtbar) -->
|
<!-- Sidebar (Desktop) -->
|
||||||
<aside class="sidebar border-end d-none d-md-block">
|
<aside class="sidebar border-end d-none d-md-block">
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<ul class="nav flex-column gap-1">
|
<ul class="nav flex-column gap-1">
|
||||||
@ -43,11 +44,17 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<NavLink class="nav-link text-white" href="/inventur" Match="NavLinkMatch.Prefix">Inventur</NavLink>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- MOBILE-OFFCANVAS (unter md als Overlay) -->
|
<!-- Offcanvas (Mobil) -->
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<div id="sideMenu"
|
<div id="sideMenu"
|
||||||
@ -69,6 +76,14 @@
|
|||||||
<NavLink class="nav-link text-white" href="/inventur" Match="NavLinkMatch.Prefix"
|
<NavLink class="nav-link text-white" href="/inventur" Match="NavLinkMatch.Prefix"
|
||||||
@onclick="CloseMenu">Inventur</NavLink>
|
@onclick="CloseMenu">Inventur</NavLink>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
|
|
||||||
<div class="mt-auto pt-3 border-top border-light">
|
<div class="mt-auto pt-3 border-top border-light">
|
||||||
@ -82,7 +97,7 @@
|
|||||||
</Authorized>
|
</Authorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
<main class="content d-flex flex-column">
|
<main class="content d-flex flex-column">
|
||||||
<div class="flex-grow-1 container-fluid p-3 p-md-4">
|
<div class="flex-grow-1 container-fluid p-3 p-md-4">
|
||||||
@Body
|
@Body
|
||||||
@ -97,26 +112,55 @@
|
|||||||
@code {
|
@code {
|
||||||
private async Task CloseMenu()
|
private async Task CloseMenu()
|
||||||
{
|
{
|
||||||
// Offcanvas schließen, Navigation des NavLink bleibt erhalten
|
// Offcanvas schließen (NavLink-Navigation bleibt erhalten)
|
||||||
await JS.InvokeVoidAsync("hideOffcanvas", "#sideMenu");
|
await JS.InvokeVoidAsync("hideOffcanvas", "#sideMenu");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (!firstRender) return;
|
||||||
{
|
|
||||||
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
|
||||||
var user = authState.User;
|
|
||||||
|
|
||||||
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);
|
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");
|
await JS.InvokeVoidAsync("localStorage.removeItem", "scannerpilot.user");
|
||||||
}
|
}
|
||||||
|
catch { /* no-op */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user