@using Lagerbestand.Models @using Lagerbestand.Services @inject ILocationCodeService LocationCodeService

Stellplatz-Daten

@* Area Selection - Required *@
@if (areaError != null) { ? @areaError } else if (areaTouched) { ? }
@* Row Selection - Required *@
@if (rowError != null) { ? @rowError } else if (rowTouched) { ? }
@* Position Selection (Seite/Ebene) - Required *@
@* Slot Input - Required *@
@StorageLocation.MinSlot - @StorageLocation.MaxSlot @if (slotError != null) { ? @slotError } else if (slotTouched) { ? }
@* Location Code Preview - Prominent Display *@
Stellplatz-Code
@(IsFormValid ? GeneratedCode : "----")
@if (IsFormValid) { @DisplayText } else { Bitte alle Felder ausfüllen }
@* Availability Status *@ @if (ShowAvailability && IsFormValid) {
@if (isChecking) { Verfügbarkeit wird geprüft... } else if (isAvailable) { ? Verfügbar } else if (locationExists) { ? Dieser Stellplatz ist bereits belegt } else { + Neuer Stellplatz }
}
@code { private ElementReference areaSelectRef; // Internal state for two-way binding private AreaType InternalArea { get; set; } = AreaType.Lager; private int InternalRow { get; set; } = 1; private ShelfPosition InternalPosition { get; set; } = ShelfPosition.LinksOben; private int InternalSlot { get; set; } = 1; // Touch/validation state private bool areaTouched; private bool rowTouched; private bool slotTouched; // Field-level errors private string? areaError; private string? rowError; private string? slotError; // Availability check state private bool isChecking; private bool isAvailable; private bool locationExists; private CancellationTokenSource? _checkCts; [Parameter] public AreaType SelectedArea { get; set; } = AreaType.Lager; [Parameter] public EventCallback SelectedAreaChanged { get; set; } [Parameter] public int SelectedRow { get; set; } = 1; [Parameter] public EventCallback SelectedRowChanged { get; set; } [Parameter] public ShelfPosition SelectedPosition { get; set; } = ShelfPosition.LinksOben; [Parameter] public EventCallback SelectedPositionChanged { get; set; } [Parameter] public int SelectedSlot { get; set; } = 1; [Parameter] public EventCallback SelectedSlotChanged { get; set; } [Parameter] public string? LocationCode { get; set; } [Parameter] public EventCallback LocationCodeChanged { get; set; } [Parameter] public bool ShowAvailability { get; set; } = true; [Parameter] public EventCallback OnAvailabilityChanged { get; set; } [Parameter] public EventCallback OnValidationChanged { get; set; } private string GeneratedCode => LocationCodeService.GenerateLocationCode(InternalArea, InternalRow, InternalPosition, InternalSlot); private string DisplayText => $"{InternalArea.GetDisplayName()}, Reihe {InternalRow}, {InternalPosition.GetDisplayName()}, Platz {InternalSlot}"; private bool IsFormValid => areaError == null && rowError == null && slotError == null && InternalSlot >= StorageLocation.MinSlot && InternalSlot <= StorageLocation.MaxSlot; private bool HasAnyError => areaError != null || rowError != null || slotError != null; protected override void OnInitialized() { // Initialize internal state from parameters InternalArea = SelectedArea; InternalRow = SelectedRow; InternalPosition = SelectedPosition; InternalSlot = SelectedSlot; } protected override async Task OnParametersSetAsync() { // Sync internal state with parameters if (InternalArea != SelectedArea) InternalArea = SelectedArea; if (InternalRow != SelectedRow) InternalRow = SelectedRow; if (InternalPosition != SelectedPosition) InternalPosition = SelectedPosition; if (InternalSlot != SelectedSlot) InternalSlot = SelectedSlot; // Mark as touched if values are set if (SelectedArea != default) areaTouched = true; if (SelectedRow > 0) rowTouched = true; if (SelectedSlot > 0) slotTouched = true; // Only check availability if we have valid values to check // Skip the DB call during initial parameter binding to avoid concurrency issues if (areaTouched && InternalRow > 0 && InternalSlot > 0) { await CheckAvailabilityAsync(); } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { // Auto-focus the first field try { await areaSelectRef.FocusAsync(); } catch { // Focus may fail in some scenarios, ignore } } } private string GetAvailabilityClass() { if (isChecking) return "checking"; if (isAvailable) return "available"; if (locationExists) return "unavailable"; return "new"; } private async Task OnAreaChanged() { areaTouched = true; // Clamp row to the new building's max row if (InternalRow > InternalArea.GetMaxRow()) InternalRow = InternalArea.GetMaxRow(); ValidateArea(); ValidateRow(); await EmitChanges(); } private async Task OnPositionChanged() { await EmitChanges(); } private async Task OnRowChanged() { rowTouched = true; ValidateRow(); await EmitChanges(); } private async Task OnSlotChanged() { slotTouched = true; ValidateSlot(); await EmitChanges(); } private void ValidateArea() { areaError = null; // Area is always valid since it's a select with predefined options } private void ValidateRow() { rowError = null; var maxRow = InternalArea.GetMaxRow(); if (InternalRow < StorageLocation.MinRow || InternalRow > maxRow) { rowError = $"Reihe muss für {InternalArea.GetDisplayName()} zwischen {StorageLocation.MinRow} und {maxRow} liegen"; } } private void ValidateSlot() { slotError = null; if (InternalSlot < StorageLocation.MinSlot || InternalSlot > StorageLocation.MaxSlot) { slotError = $"Platz muss zwischen {StorageLocation.MinSlot} und {StorageLocation.MaxSlot} liegen"; } } public bool ValidateAll() { areaTouched = true; rowTouched = true; slotTouched = true; ValidateArea(); ValidateRow(); ValidateSlot(); StateHasChanged(); return IsFormValid; } private async Task EmitChanges() { await SelectedAreaChanged.InvokeAsync(InternalArea); await SelectedRowChanged.InvokeAsync(InternalRow); await SelectedPositionChanged.InvokeAsync(InternalPosition); await SelectedSlotChanged.InvokeAsync(InternalSlot); await LocationCodeChanged.InvokeAsync(GeneratedCode); await OnValidationChanged.InvokeAsync(IsFormValid); await CheckAvailabilityAsync(); } private async Task CheckAvailabilityAsync() { if (!ShowAvailability || !IsFormValid) { isAvailable = false; locationExists = false; return; } // Cancel previous check _checkCts?.Cancel(); _checkCts = new CancellationTokenSource(); var token = _checkCts.Token; isChecking = true; StateHasChanged(); try { // Debounce delay await Task.Delay(300, token); var code = GeneratedCode; locationExists = await LocationCodeService.CodeExistsAsync(code); isAvailable = !locationExists || await LocationCodeService.IsLocationAvailableAsync(code); await OnAvailabilityChanged.InvokeAsync(isAvailable && !locationExists); } catch (OperationCanceledException) { // Cancelled, ignore } finally { isChecking = false; StateHasChanged(); } } public void Reset() { InternalArea = AreaType.Lager; InternalRow = 1; InternalPosition = ShelfPosition.LinksOben; InternalSlot = 1; areaTouched = false; rowTouched = false; slotTouched = false; areaError = null; rowError = null; slotError = null; isAvailable = false; locationExists = false; StateHasChanged(); } public void SetCode(string code) { if (LocationCodeService.TryParseLocationCode(code, out var parsed) && parsed != null) { InternalArea = parsed.Area; InternalRow = parsed.Row; InternalPosition = parsed.Position; InternalSlot = parsed.SlotNumber; areaTouched = true; rowTouched = true; slotTouched = true; ValidateAll(); } } }