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
[AllowAnonymous] [HttpPost("login")] public async Task 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 { 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 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 = """ Logout @@ -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; + } } diff --git a/WerksverkaufScanner/WerksverkaufScanner/Data/Filiale.cs b/WerksverkaufScanner/WerksverkaufScanner/Data/Filiale.cs new file mode 100644 index 0000000..78b7ed5 --- /dev/null +++ b/WerksverkaufScanner/WerksverkaufScanner/Data/Filiale.cs @@ -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; } + } +} diff --git a/WerksverkaufScanner/WerksverkaufScanner/Data/InventurErfassung.cs b/WerksverkaufScanner/WerksverkaufScanner/Data/InventurErfassung.cs index c033684..160a4c4 100644 --- a/WerksverkaufScanner/WerksverkaufScanner/Data/InventurErfassung.cs +++ b/WerksverkaufScanner/WerksverkaufScanner/Data/InventurErfassung.cs @@ -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; } diff --git a/WerksverkaufScanner/WerksverkaufScanner/Data/PreisAenderung.cs b/WerksverkaufScanner/WerksverkaufScanner/Data/PreisAenderung.cs new file mode 100644 index 0000000..8aa0283 --- /dev/null +++ b/WerksverkaufScanner/WerksverkaufScanner/Data/PreisAenderung.cs @@ -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; } + + /// Preis in Cent (z. B. 599 = 5,99 €) + public int PreisInCents { get; set; } + + public string Benutzer { get; set; } = default!; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/WerksverkaufScanner/WerksverkaufScanner/Data/ScannerDb.cs b/WerksverkaufScanner/WerksverkaufScanner/Data/ScannerDb.cs index 263027b..f51b1dc 100644 --- a/WerksverkaufScanner/WerksverkaufScanner/Data/ScannerDb.cs +++ b/WerksverkaufScanner/WerksverkaufScanner/Data/ScannerDb.cs @@ -6,10 +6,9 @@ namespace WerksverkaufScanner.Data; public class ScannerDb : DbContext { public ScannerDb(DbContextOptions options) : base(options) { } - public DbSet Artikel => Set(); public DbSet InventurErfassung => Set(); public DbSet Kassierer { get; set; } = null!; - - + public DbSet PreisAenderungen => Set(); + public DbSet Filialen => Set(); } diff --git a/WerksverkaufScanner/WerksverkaufScanner/Pages/Einstellungen.razor b/WerksverkaufScanner/WerksverkaufScanner/Pages/Einstellungen.razor new file mode 100644 index 0000000..4eb1789 --- /dev/null +++ b/WerksverkaufScanner/WerksverkaufScanner/Pages/Einstellungen.razor @@ -0,0 +1,61 @@ +@page "/einstellungen" +@attribute [Authorize] + +ScannerPilot - Einstellungen + +

Einstellungen

+ +@if (loading) +{ +

Lade aktuelle Einstellungen …

+} +else +{ +
+
Aktuelle Filial‑ID
+
@currentFilialId (@currentFilialName)
+ +
Angemeldeter Benutzer
+
@currentUser
+ +
Client‑IP
+
@currentIp
+
+} + +@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("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; + } +} diff --git a/WerksverkaufScanner/WerksverkaufScanner/Pages/Index.razor b/WerksverkaufScanner/WerksverkaufScanner/Pages/Index.razor index a10c4b8..eb4ca24 100644 --- a/WerksverkaufScanner/WerksverkaufScanner/Pages/Index.razor +++ b/WerksverkaufScanner/WerksverkaufScanner/Pages/Index.razor @@ -18,16 +18,27 @@ - @*
+
+
+
+ +
Stammdaten
+

Hier können z.B. Preise etc. angepasst werden

+ Öffnen +
+
+
+ +
Einstellungen
-

Verwalte Filiale und Inventurparameter.

- Anpassen +

Hier können Einstellungen wie z.B. die aktuelle IP angezeigt werden.

+ Öffnen
-
*@ +
diff --git a/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/Inventur.razor b/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/Inventur.razor index 04d9ef7..cdb04a8 100644 --- a/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/Inventur.razor +++ b/WerksverkaufScanner/WerksverkaufScanner/Pages/Inventur/Inventur.razor @@ -5,9 +5,27 @@

Inventur

-

-

-

+
+ @*
+
+
+ +
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
+