From 1729550f95e2ad82d8d0eba884c6e69f12b176cc Mon Sep 17 00:00:00 2001
From: Christopher Meinhold
Date: Wed, 3 Dec 2025 14:11:03 +0100
Subject: [PATCH] =?UTF-8?q?WerksverkaufScanner:=20-Inventur=20vorbereiten?=
=?UTF-8?q?=20entfernt=20-Einstellungsseite=20erg=C3=A4nzt=20-Filial-Id=20?=
=?UTF-8?q?anhand=20IP=20laden?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Controllers/AuthController.cs | 61 +++-
.../WerksverkaufScanner/Data/Filiale.cs | 23 ++
.../Data/InventurErfassung.cs | 2 +-
.../Data/PreisAenderung.cs | 21 ++
.../WerksverkaufScanner/Data/ScannerDb.cs | 5 +-
.../Pages/Einstellungen.razor | 61 ++++
.../WerksverkaufScanner/Pages/Index.razor | 19 +-
.../Pages/Inventur/Inventur.razor | 30 +-
.../Pages/Inventur/InventurScan.razor | 17 +-
.../Pages/Stammdaten/Stammdaten.razor | 20 ++
.../Stammdaten/StammdatenPreisÄnderung.razor | 332 ++++++++++++++++++
.../WerksverkaufScanner/Program.cs | 119 ++-----
.../Services/FilialService.cs | 23 ++
.../Services/InventurService.cs | 4 +-
.../Services/IpFilialeService.cs | 74 ++++
.../Services/PreisAenderungSqlService.cs | 97 +++++
.../Shared/MainLayout.razor | 70 +++-
17 files changed, 828 insertions(+), 150 deletions(-)
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Data/Filiale.cs
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Data/PreisAenderung.cs
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Pages/Einstellungen.razor
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/Stammdaten.razor
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/StammdatenPreisÄnderung.razor
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Services/FilialService.cs
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Services/IpFilialeService.cs
create mode 100644 WerksverkaufScanner/WerksverkaufScanner/Services/PreisAenderungSqlService.cs
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs b/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs
index 5a44bc7..da98aed 100644
--- a/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs
+++ b/WerksverkaufScanner/WerksverkaufScanner/Controllers/AuthController.cs
@@ -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
+
+ @*
+
+
+
+
Inventur vorbereiten
+
Hier müssen die aktuellen Inventureinstellungen gescannt werden.
+
Weiter
+
+
+
*@
+
+
+
+
+
+
Inventur scannen
+
Hier kannst du die Artikel in die Inventurerfassung scannen.
+
Weiter
+
+
+
+
+
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor b/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor
index cc63079..ccae5e6 100644
--- a/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor
+++ b/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/InventurScan.razor
@@ -114,9 +114,8 @@ else
try
{
var f = await JS.InvokeAsync("localStorage.getItem", "scannerpilot.filialId");
- var i = await JS.InvokeAsync("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("localStorage.getItem", "scannerpilot.filialId");
- var i = await JS.InvokeAsync("localStorage.getItem", "scannerpilot.inventurId");
var u = await JS.InvokeAsync("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,
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/Stammdaten.razor b/WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/Stammdaten.razor
new file mode 100644
index 0000000..fce7a61
--- /dev/null
+++ b/WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/Stammdaten.razor
@@ -0,0 +1,20 @@
+@page "/stammdaten"
+@attribute [Authorize]
+
+ScannerPilot - Stammdaten
+
+Stammdatenänderungen
+
+
+
+
+
+
+
Preisänderung
+
Hier können die Preise der Artikel geändert werden.
+
Weiter
+
+
+
+
+
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/StammdatenPreisÄnderung.razor b/WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/StammdatenPreisÄnderung.razor
new file mode 100644
index 0000000..328aa9d
--- /dev/null
+++ b/WerksverkaufScanner/WerksverkaufScanner/Pages/Stammdaten/StammdatenPreisÄnderung.razor
@@ -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
+
+Preisänderung
+
+
+
+
+@if (varianten is { Count: > 1 })
+{
+
+
Varianten gefunden – bitte auswählen:
+
+
+ @foreach (var art in varianten)
+ {
+ var isActive = ReferenceEquals(gefunden, art);
+
+ }
+
+
+}
+
+@if (gefunden is not null)
+{
+
+ @gefunden.Bezeichnung
+
+ · @gefunden.ArtikelNummer
+ · Variante @gefunden.Variante
+ @if (!string.IsNullOrWhiteSpace(gefunden.Farbe))
+ {
+ · @gefunden.Farbe
+ }
+ @if (!string.IsNullOrWhiteSpace(gefunden.Text))
+ {
+ · @gefunden.Text
+ }
+
+
+
+
+
+ €
+
+
+
+
+}
+
+@if (!string.IsNullOrEmpty(status))
+{
+ @status
+}
+
+@code {
+ // Eingaben/Status
+ private string? scanText;
+ private Artikel? gefunden;
+ private List? 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("localStorage.getItem", "scannerpilot.filialId");
+ var user = await JS.InvokeAsync("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"));
+}
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Program.cs b/WerksverkaufScanner/WerksverkaufScanner/Program.cs
index ecb3627..4e9ac73 100644
--- a/WerksverkaufScanner/WerksverkaufScanner/Program.cs
+++ b/WerksverkaufScanner/WerksverkaufScanner/Program.cs
@@ -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 prfen
-//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(); // fr AuthController (Login/Logout)
-//builder.Services.AddHttpClient(); // fr 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-Hrtung
-// o.Cookie.Name = "Werksverkauf.Auth";
-// o.Cookie.HttpOnly = true;
-// o.Cookie.SecurePolicy = CookieSecurePolicy.None; // nur ber HTTPS eigentlich .Always
-// o.Cookie.SameSite = SameSiteMode.Lax; // fr normale Navigations-Requests
-// });
-//builder.Services.AddAuthorization();
-
-//// 4) App-Services & Datenzugriff
-//builder.Services.AddDbContextFactory(opt => opt.UseSqlServer(cs));
-//builder.Services.AddSingleton();
-//builder.Services.AddScoped();
-//builder.Services.AddScoped(); // prft KassenLogin in DB
-
-//// (Optional) Forwarded Headers, wenn hinter Proxy/LoadBalancer (NGINX/IIS/K8s)
-//builder.Services.Configure(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 ntig, wenn du KnownProxies/KnownNetworks setzt
var builder = WebApplication.CreateBuilder(args);
-// --- WICHTIG FR 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 prfen
+// 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(); // fr AuthController (Login/Logout)
-builder.Services.AddHttpClient(); // fr 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-Hrtung:
+ // 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(opt => opt.UseSqlServer(cs));
builder.Services.AddSingleton();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped(); // <— für IP→Filiale-Mapping
+builder.Services.AddScoped();
-// (Optional) Forwarded Headers sinnvoll hinter IIS/Proxy
+// (Optional) Forwarded Headers – sinnvoll hinter IIS/Proxy
builder.Services.Configure(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");
-// --- Fr 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();
-
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Services/FilialService.cs b/WerksverkaufScanner/WerksverkaufScanner/Services/FilialService.cs
new file mode 100644
index 0000000..1a6bb21
--- /dev/null
+++ b/WerksverkaufScanner/WerksverkaufScanner/Services/FilialService.cs
@@ -0,0 +1,23 @@
+using Microsoft.EntityFrameworkCore;
+using WerksverkaufScanner.Data;
+
+namespace WerksverkaufScanner.Services
+{
+ public class FilialService
+ {
+ private readonly IDbContextFactory _dbFactory;
+
+ public FilialService(IDbContextFactory dbFactory)
+ {
+ _dbFactory = dbFactory;
+ }
+
+ public async Task GetFilialeAsync(int filialId)
+ {
+ await using var db = await _dbFactory.CreateDbContextAsync();
+ return await db.Set()
+ .AsNoTracking()
+ .FirstOrDefaultAsync(f => f.FilialId == filialId);
+ }
+ }
+}
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Services/InventurService.cs b/WerksverkaufScanner/WerksverkaufScanner/Services/InventurService.cs
index 6221611..6cec9eb 100644
--- a/WerksverkaufScanner/WerksverkaufScanner/Services/InventurService.cs
+++ b/WerksverkaufScanner/WerksverkaufScanner/Services/InventurService.cs
@@ -20,7 +20,7 @@ public class InventurService
_filialId = config.GetValue("FilialId");
}
- public async Task SaveAsync(string filialId, int inventurId, int artikelId, int artikelVariantenId, int variante, int menge, string user)
+ public async Task 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,
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Services/IpFilialeService.cs b/WerksverkaufScanner/WerksverkaufScanner/Services/IpFilialeService.cs
new file mode 100644
index 0000000..8065c4a
--- /dev/null
+++ b/WerksverkaufScanner/WerksverkaufScanner/Services/IpFilialeService.cs
@@ -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 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;
+ }
+}
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Services/PreisAenderungSqlService.cs b/WerksverkaufScanner/WerksverkaufScanner/Services/PreisAenderungSqlService.cs
new file mode 100644
index 0000000..6ad9561
--- /dev/null
+++ b/WerksverkaufScanner/WerksverkaufScanner/Services/PreisAenderungSqlService.cs
@@ -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 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;
+ }
+ }
+}
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Shared/MainLayout.razor b/WerksverkaufScanner/WerksverkaufScanner/Shared/MainLayout.razor
index 22f2f65..0de0b50 100644
--- a/WerksverkaufScanner/WerksverkaufScanner/Shared/MainLayout.razor
+++ b/WerksverkaufScanner/WerksverkaufScanner/Shared/MainLayout.razor
@@ -5,9 +5,10 @@
@inject IJSRuntime JS