Release 1.1.0: QR-Reportimport, Regal-Auswahlmodal, Einlagern/Auslagern-Verknuepfungen aus Reifenlager, Stabilitaetsfixes

This commit is contained in:
spaethf committed 2026-09-26 23:24:14 +02:00
1 parent cba9414e92
commit 3404fb9875
25 files changed
+5106 -744

No files matched your search

@@ -0,0 +1,190 @@
@using Lagerbestand.Models
@using Lagerbestand.Services
@inject IStorageLocationService StorageLocationService
@if (IsOpen)
{
<div class="picker-overlay" @onclick="Cancel">
<div class="picker-dialog" @onclick:stopPropagation="true" role="dialog" aria-modal="true" aria-labelledby="picker-title">
<header class="picker-header">
<div>
<h2 id="picker-title">Lagerplatz wählen</h2>
@if (!string.IsNullOrEmpty(Subtitle))
{
<p class="picker-subtitle">@Subtitle</p>
}
</div>
<button type="button" class="picker-close" @onclick="Cancel" aria-label="Schließen">✕</button>
</header>
<div class="picker-body">
@if (isLoading)
{
<div class="picker-loading" role="status">Lade Lagerplätze...</div>
}
else
{
<div class="rack-view">
@foreach (var area in new[] { AreaType.Werkstatt, AreaType.Lager })
{
<div class="building-section" @key="area">
<h3 class="building-title">@area.GetBuildingLabel()</h3>
@for (int row = StorageLocation.MinRow; row <= area.GetMaxRow(); row++)
{
var currentRow = row;
<div class="rack-row" @key="@($"{area}-{currentRow}")">
<h4 class="row-title">Reihe @currentRow</h4>
<div class="rack-sides">
@foreach (var side in new[] { "Links", "Rechts" })
{
<div class="rack-side" @key="side">
<span class="side-label">@side</span>
@foreach (var pos in ShelfPositionExtensions.All.Where(p => p.IsLeft() == (side == "Links")).OrderByDescending(p => p.IsTop()))
{
<div class="rack-level" @key="pos">
<span class="level-label">@(pos.IsTop() ? "Oben" : "Unten")</span>
<div class="rack-cells">
@for (int slot = StorageLocation.MinSlot; slot <= StorageLocation.MaxSlot; slot++)
{
var loc = GetLocation(area, currentRow, pos, slot);
var isHighlighted = loc != null && HighlightLocationId == loc.Id;
var isSelectable = loc != null && (isHighlighted || (loc.IsAvailable && !loc.IsOccupied));
var cellClass = loc == null ? "missing" : isHighlighted ? "freeing" : !loc.IsAvailable ? "blocked" : loc.IsOccupied ? "occupied" : "free";
<button type="button"
class="rack-cell @cellClass @(pendingLocation?.Id == loc?.Id ? "selected" : "")"
@key="slot"
disabled="@(!isSelectable)"
title="@GetTooltip(loc, area, currentRow, pos, slot, isHighlighted)"
@onclick="() => Select(loc)">
@slot
</button>
}
</div>
</div>
}
</div>
}
</div>
</div>
}
</div>
}
</div>
<div class="rack-legend" aria-hidden="true">
<span class="legend-item"><span class="legend-swatch free"></span> Frei</span>
@if (HighlightLocationId.HasValue)
{
<span class="legend-item"><span class="legend-swatch freeing"></span> Wird frei</span>
}
<span class="legend-item"><span class="legend-swatch occupied"></span> Belegt</span>
<span class="legend-item"><span class="legend-swatch blocked"></span> Gesperrt</span>
</div>
}
</div>
<footer class="picker-footer">
<div class="picker-selection">
@if (pendingLocation != null)
{
<span class="picker-selection-label">Ausgewählt:</span>
<LocationDisplay Code="@pendingLocation.Code" DisplayMode="LocationDisplayMode.Full" Size="LocationDisplaySize.Small" />
}
else
{
<span class="picker-selection-hint">Bitte einen freien Platz anklicken</span>
}
</div>
<div class="picker-actions">
<button type="button" class="btn btn-outline" @onclick="Cancel">Abbrechen</button>
<button type="button" class="btn btn-primary" @onclick="Confirm" disabled="@(pendingLocation == null)">Übernehmen</button>
</div>
</footer>
</div>
</div>
}
@code {
[Parameter] public bool IsOpen { get; set; }
[Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
/// <summary>Currently selected location (pre-selected when opening).</summary>
[Parameter] public Guid? SelectedLocationId { get; set; }
/// <summary>Location that is about to be freed (e.g. during a swap) – shown as selectable.</summary>
[Parameter] public Guid? HighlightLocationId { get; set; }
[Parameter] public string? Subtitle { get; set; }
[Parameter] public EventCallback<StorageLocation> OnSelected { get; set; }
private bool isLoading;
private bool wasOpen;
private List<StorageLocation> allLocations = new();
private Dictionary<(AreaType, int, ShelfPosition, int), StorageLocation> lookup = new();
private StorageLocation? pendingLocation;
protected override async Task OnParametersSetAsync()
{
if (IsOpen && !wasOpen)
{
wasOpen = true;
await LoadAsync();
}
else if (!IsOpen)
{
wasOpen = false;
}
}
private async Task LoadAsync()
{
isLoading = true;
try
{
allLocations = await StorageLocationService.GetAllAsync();
lookup = allLocations.ToDictionary(l => (l.Area, l.Row, l.Position, l.SlotNumber));
pendingLocation = SelectedLocationId.HasValue
? allLocations.FirstOrDefault(l => l.Id == SelectedLocationId.Value)
: null;
}
finally
{
isLoading = false;
}
}
private StorageLocation? GetLocation(AreaType area, int row, ShelfPosition pos, int slot)
=> lookup.TryGetValue((area, row, pos, slot), out var loc) ? loc : null;
private void Select(StorageLocation? loc)
{
if (loc == null) return;
pendingLocation = loc;
}
private async Task Confirm()
{
if (pendingLocation == null) return;
await OnSelected.InvokeAsync(pendingLocation);
await Close();
}
private Task Cancel() => Close();
private async Task Close()
{
IsOpen = false;
await IsOpenChanged.InvokeAsync(false);
}
private string GetTooltip(StorageLocation? loc, AreaType area, int row, ShelfPosition pos, int slot, bool isHighlighted)
{
var baseText = $"{area.GetDisplayName()}, Reihe {row}, {pos.GetDisplayName()}, Platz {slot}";
if (loc == null) return $"{baseText} – nicht angelegt";
if (isHighlighted) return $"{baseText} – wird frei";
if (!loc.IsAvailable) return $"{baseText} – gesperrt";
if (loc.IsOccupied) return $"{baseText} – belegt";
return $"{baseText} – frei";
}
}