Files

807 lines
38 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/tire-storage"
@implements IAsyncDisposable
@inject ITireStackService TireStackService
@inject IStorageLocationService StorageLocationService
@inject ICatalogService CatalogService
@inject UserSessionService UserSession
@inject IJSRuntime JS
@using Lagerbestand.Components.Shared
<PageTitle>Lagerübersicht - Reifen Späth Lagerverwaltung</PageTitle>
<SeoMetaTags
Title="Lagerübersicht - Reifen Späth Lagerverwaltung"
Description="Übersicht aller eingelagerten Reifenstapel in der Reifen Späth Lagerverwaltung."
Robots="noindex, nofollow" />
@if (!UserSession.IsWorkerOrAdmin)
{
<section class="access-denied" role="alert" aria-labelledby="access-denied-title">
<div class="access-denied-icon" aria-hidden="true">🔒</div>
<h1 id="access-denied-title">Zugriff verweigert</h1>
<p>Diese Seite ist nur für Mitarbeiter und Administratoren zugänglich.</p>
<a href="/" class="btn btn-primary">Zur Startseite</a>
</section>
}
else
{
<div class="storage-page">
<header class="page-header">
<div class="header-content">
<h1>Lagerübersicht</h1>
<nav class="header-actions" aria-label="Schnellaktionen">
<a href="/tire-checkin" class="btn btn-primary">
<span aria-hidden="true">+</span> Einlagern
</a>
<a href="/tire-checkout" class="btn btn-outline">
Abholen
</a>
</nav>
</div>
</header>
@* Compact Toolbar: Stats as clickable filter chips + controls *@
<section class="toolbar" aria-label="Statistik, Suche und Filter">
<div class="stat-chips" role="tablist" aria-label="Filter nach Typ">
<button class="stat-chip @(typeFilter == null ? "active" : "")"
@onclick="() => SetTypeFilter(null)"
role="tab" aria-selected="@(typeFilter == null)"
title="Alle Stapel anzeigen">
<span class="chip-value">@stats.TotalStacksStored</span> Stapel (@stats.TotalTiresStored Reifen)
</button>
<button class="stat-chip customer @(typeFilter == false ? "active" : "")"
@onclick="() => SetTypeFilter(typeFilter == false ? null : false)"
role="tab" aria-selected="@(typeFilter == false)"
title="Nur Kundenstapel anzeigen">
<span class="chip-value">@stats.CustomerStackCount</span> <span aria-hidden="true">👤</span> Kunden
</button>
<button class="stat-chip inventory @(typeFilter == true ? "active" : "")"
@onclick="() => SetTypeFilter(typeFilter == true ? null : true)"
role="tab" aria-selected="@(typeFilter == true)"
title="Nur Lagerbestand anzeigen">
<span class="chip-value">@stats.InventoryStackCount</span> <span aria-hidden="true">📦</span> Lager
</button>
<span class="stat-chip static" title="Freie Lagerplätze">
<span class="chip-value free">@stats.AvailableLocations</span> Frei
</span>
</div>
<div class="toolbar-controls">
<div class="search-box">
<label for="search-input" class="visually-hidden">Suche</label>
<input type="search"
id="search-input"
class="search-input"
placeholder="🔍 Suchen (Name, Fahrzeug, Ticket-Nr.)..."
@bind="searchTerm"
@bind:event="oninput"
@onkeyup="FilterTireStacks"
aria-describedby="search-hint" />
<span id="search-hint" class="visually-hidden">Durchsucht Namen, Fahrzeuge, Kategorien und Ticket-Nummern</span>
</div>
<fieldset class="filter-buttons">
<legend class="visually-hidden">Nach Saison filtern</legend>
<button class="filter-btn @(seasonFilter == TireSeason.Sommer ? "active summer" : "")"
@onclick="() => SetSeasonFilter(seasonFilter == TireSeason.Sommer ? null : TireSeason.Sommer)"
aria-pressed="@(seasonFilter == TireSeason.Sommer)"
aria-label="Sommerreifen" title="Sommerreifen">
<span aria-hidden="true">☀️</span>
</button>
<button class="filter-btn @(seasonFilter == TireSeason.Winter ? "active winter" : "")"
@onclick="() => SetSeasonFilter(seasonFilter == TireSeason.Winter ? null : TireSeason.Winter)"
aria-pressed="@(seasonFilter == TireSeason.Winter)"
aria-label="Winterreifen" title="Winterreifen">
<span aria-hidden="true">❄️</span>
</button>
<button class="filter-btn @(seasonFilter == TireSeason.Allrad ? "active allseason" : "")"
@onclick="() => SetSeasonFilter(seasonFilter == TireSeason.Allrad ? null : TireSeason.Allrad)"
aria-pressed="@(seasonFilter == TireSeason.Allrad)"
aria-label="Ganzjahresreifen" title="Ganzjahresreifen">
<span aria-hidden="true">🔄</span>
</button>
</fieldset>
@if (typeFilter == true && categories.Count > 0)
{
<select id="category-select" class="form-control category-select" @bind="selectedCategoryId" @bind:after="FilterTireStacks" title="Kategorie auswählen">
<option value="">Alle Kategorien</option>
@foreach (var cat in categories)
{
<option value="@cat.Id">@cat.Name</option>
}
</select>
}
<div class="view-toggle" role="tablist" aria-label="Ansicht wählen">
<button class="view-btn @(showGridView ? "" : "active")"
@onclick="() => showGridView = false"
role="tab" aria-selected="@(!showGridView)" title="Listenansicht">
<span aria-hidden="true">☰</span> Liste
</button>
<button class="view-btn @(showGridView ? "active" : "")"
@onclick="() => showGridView = true"
role="tab" aria-selected="@showGridView" title="Regalansicht">
<span aria-hidden="true">▦</span> Regal
</button>
</div>
@if (HasActiveFilters)
{
<button class="btn btn-outline btn-sm clear-filters" @onclick="ResetFilters" title="Filter zurücksetzen">
✕ Filter löschen
</button>
}
</div>
</section>
@* Tire Stacks List *@
@if (isLoading)
{
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<span>Lade Daten...</span>
</div>
}
else if (showGridView)
{
<section class="rack-view" aria-label="Regalansicht der Lagerplätze">
@foreach (var area in new[] { AreaType.Werkstatt, AreaType.Lager })
{
<div class="building-section" @key="area">
<h2 class="building-title">@area.GetBuildingLabel()</h2>
@for (int row = StorageLocation.MinRow; row <= area.GetMaxRow(); row++)
{
var currentRow = row;
<div class="rack-row" @key="@($"{area}-{currentRow}")">
<h3 class="row-title">Reihe @currentRow</h3>
<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 cellClass = loc == null ? "missing" : !loc.IsAvailable ? "blocked" : loc.IsOccupied ? "occupied" : "free";
<button type="button"
class="rack-cell @cellClass @(selectedLocation?.Id == loc?.Id ? "selected" : "")"
@key="slot"
disabled="@(loc == null)"
title="@GetCellTooltip(loc, area, currentRow, pos, slot)"
@onclick="() => SelectLocation(loc)">
@slot
</button>
}
</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>
<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>
@if (selectedLocation != null)
{
<aside class="cell-details" aria-label="Details zum gewählten Lagerplatz">
<header class="cell-details-header">
<LocationDisplay Code="@selectedLocation.Code" DisplayMode="LocationDisplayMode.Full" IsOccupied="@selectedLocation.IsOccupied" ShowStatus="true" />
<button type="button" class="btn btn-sm btn-outline" @onclick="() => selectedLocation = null">Schließen</button>
</header>
@if (selectedLocation.IsOccupied && selectedLocation.OccupiedTireStack != null)
{
var stack = selectedLocation.OccupiedTireStack;
@StackDetailLists(stack)
@if (!string.IsNullOrEmpty(stack.Notes))
{
<div class="notes-section">
<span class="label">Notizen:</span>
<span class="notes-text">@stack.Notes</span>
</div>
}
<nav class="detail-actions" aria-label="Aktionen für diesen Stapel">
<button type="button" class="btn btn-sm btn-primary" @onclick="() => OpenEditModalFor(stack)">✏️ Bearbeiten</button>
@if (stack.CustomerId.HasValue)
{
<a href="@CheckinUrl(stack)" class="btn btn-sm btn-outline">+ Einlagern</a>
}
<a href="@CheckoutUrl(stack)" class="btn btn-sm btn-outline">@(stack.IsInventory ? "Entnehmen" : "Abholen")</a>
</nav>
}
else if (!selectedLocation.IsAvailable)
{
<p class="cell-details-text">Dieser Lagerplatz ist gesperrt. @selectedLocation.Notes</p>
}
else
{
<p class="cell-details-text">Dieser Lagerplatz ist frei.</p>
<a href="/tire-checkin" class="btn btn-sm btn-primary">Einlagern</a>
}
</aside>
}
</section>
}
else if (filteredTireStacks.Count == 0)
{
<section class="empty-state" aria-labelledby="empty-title">
<div class="empty-icon" aria-hidden="true">📦</div>
<h2 id="empty-title">Keine Einträge</h2>
<p>@(string.IsNullOrEmpty(searchTerm) ? "Noch keine Reifenstapel eingelagert." : "Keine Ergebnisse für Ihre Suche.")</p>
@if (string.IsNullOrEmpty(searchTerm))
{
<a href="/tire-checkin" class="btn btn-primary">Reifenstapel einlagern</a>
}
</section>
}
else
{
@if (selectedStack != null)
{
<aside class="stack-details-panel" aria-label="Details zum gewählten Reifenstapel">
<header class="stack-details-header">
<div class="stack-details-title">
@if (selectedStack.IsInventory)
{
<h2><span aria-hidden="true">📦</span> @(selectedStack.CatalogItem?.Name ?? "Lagerbestand")</h2>
}
else
{
<h2>@selectedStack.Customer?.FullName</h2>
@if (selectedStack.Vehicle != null)
{
<span class="vehicle-badge">@selectedStack.Vehicle.Make @selectedStack.Vehicle.Model</span>
}
}
</div>
<div class="stack-details-actions">
<button type="button" class="btn btn-sm btn-primary" @onclick="OpenEditModal">✏️ Bearbeiten</button>
<button type="button" class="btn btn-sm btn-outline" @onclick="() => selectedStack = null">Schließen</button>
</div>
</header>
@StackDetailLists(selectedStack)
@if (!string.IsNullOrEmpty(selectedStack.Notes))
{
<div class="notes-section">
<span class="label">Notizen:</span>
<span class="notes-text">@selectedStack.Notes</span>
</div>
}
<nav class="detail-actions" aria-label="Aktionen für diesen Stapel">
@if (selectedStack.CustomerId.HasValue)
{
<a href="@CheckinUrl(selectedStack)" class="btn btn-sm btn-outline">+ Einlagern</a>
}
<a href="@CheckoutUrl(selectedStack)" class="btn btn-sm btn-primary">@(selectedStack.IsInventory ? "Entnehmen" : "Abholen")</a>
</nav>
</aside>
}
<section class="tireset-list" aria-label="Liste der Reifenstapel" role="list">
@foreach (var ts in filteredTireStacks)
{
<article class="tireset-card @(ts.IsInventory ? "inventory" : "") @(selectedStack?.Id == ts.Id ? "selected" : "")"
@onclick="() => SelectStack(ts)"
role="listitem"
aria-selected="@(selectedStack?.Id == ts.Id)">
<div class="card-row card-row-primary">
<div class="card-identity">
@if (ts.IsInventory)
{
<span class="customer-name inventory-name">
<span aria-hidden="true">📦</span> @(ts.CatalogItem?.Name ?? "Lagerbestand")
</span>
<span class="inventory-badge">Lager</span>
}
else
{
<span class="customer-name">@ts.Customer?.FullName</span>
@if (ts.Vehicle != null)
{
<span class="vehicle-badge">@ts.Vehicle.Make @ts.Vehicle.Model</span>
}
}
</div>
<div class="card-location">
@if (ts.StorageLocation != null)
{
<span class="location-indicator">
<span class="loc-area @(ts.StorageLocation.Area == AreaType.Werkstatt ? "werkstatt" : "lager")">@ts.StorageLocation.Area.GetDisplayName()</span>
<span class="loc-part">Regal @ts.StorageLocation.Row</span>
<span class="loc-part">@ts.StorageLocation.Position.GetShortName()</span>
<span class="loc-part">P@(ts.StorageLocation.SlotNumber)</span>
</span>
}
else
{
<span class="location-code">-</span>
}
</div>
</div>
<div class="card-row card-row-secondary">
<span class="season-indicator @ts.Season.ToString().ToLower()"
aria-label="@(ts.Season == TireSeason.Sommer ? "Sommerreifen" : ts.Season == TireSeason.Winter ? "Winterreifen" : "Ganzjahresreifen")">
@(ts.Season == TireSeason.Sommer ? "☀️" : ts.Season == TireSeason.Winter ? "❄️" : "🔄")
</span>
<span class="tire-details">@ts.ItemCount Reifen @(ts.TireSize != null ? $"| {ts.TireSize}" : "") @(ts.TireModel != null ? $"| {ts.TireModel}" : "") @(ts.WithRims ? "| mit Felgen" : "")</span>
</div>
</article>
}
</section>
}
@if (showEditModal && editStack != null)
{
<div class="modal-overlay" @onclick="CloseEditModal">
<div class="modal-dialog" @onclick:stopPropagation="true" role="dialog" aria-modal="true" aria-labelledby="edit-modal-title">
<header class="modal-header">
<h2 id="edit-modal-title">Reifenstapel bearbeiten – @editStack.TicketNumber</h2>
<button type="button" class="modal-close" @onclick="CloseEditModal" aria-label="Schließen">✕</button>
</header>
<div class="modal-body">
<div class="form-grid">
<div class="form-group">
<label for="edit-size">Reifengröße</label>
<input id="edit-size" class="form-control" @bind="editStack.TireSize" placeholder="z.B. 225/45R17" />
</div>
<div class="form-group">
<label for="edit-brand">Marke</label>
<input id="edit-brand" class="form-control" @bind="editStack.TireBrand" placeholder="z.B. Michelin" />
</div>
<div class="form-group">
<label for="edit-model">Modell</label>
<input id="edit-model" class="form-control" @bind="editStack.TireModel" placeholder="z.B. Pilot Sport 4" />
</div>
<div class="form-group">
<label for="edit-count">Anzahl</label>
<input id="edit-count" type="number" min="1" max="8" class="form-control" @bind="editStack.ItemCount" />
</div>
<div class="form-group">
<label for="edit-season">Saison</label>
<select id="edit-season" class="form-control" @bind="editStack.Season">
<option value="@TireSeason.Sommer">☀️ Sommer</option>
<option value="@TireSeason.Winter">❄️ Winter</option>
<option value="@TireSeason.Allrad">🔄 Allrad</option>
</select>
</div>
<div class="form-group">
<label for="edit-condition">Zustand</label>
<select id="edit-condition" class="form-control" @bind="editStack.Condition">
<option value="@TireCondition.Excellent">Sehr gut</option>
<option value="@TireCondition.Good">Gut</option>
<option value="@TireCondition.Fair">Ausreichend</option>
<option value="@TireCondition.Poor">Mangelhaft</option>
<option value="@TireCondition.Critical">Kritisch</option>
</select>
</div>
<div class="form-group">
<label for="edit-rims">Felgen</label>
<select id="edit-rims" class="form-control" @bind="editStack.WithRims">
<option value="true">Mit Felgen</option>
<option value="false">Ohne Felgen</option>
</select>
</div>
<div class="form-group form-group-wide">
<label for="edit-notes">Notizen</label>
<textarea id="edit-notes" class="form-control" rows="3" @bind="editStack.Notes"></textarea>
</div>
</div>
<TireInspectionPanel TireStackId="@editStack.Id" AllowEdit="true" Compact="true"
ScannerSubtitle="@editStack.TicketNumber" />
@if (!string.IsNullOrEmpty(editError))
{
<p class="edit-error" role="alert">@editError</p>
}
</div>
<footer class="modal-footer">
<button type="button" class="btn btn-outline" @onclick="CloseEditModal" disabled="@isSaving">Abbrechen</button>
<button type="button" class="btn btn-primary" @onclick="SaveEdit" disabled="@isSaving">
@(isSaving ? "Speichern..." : "Speichern")
</button>
</footer>
</div>
</div>
}
@* Expiring Pickups Warning (only for customer stacks) *@
@if (expiringPickups.Count > 0)
{
<aside class="expiring-section" aria-labelledby="expiring-title" role="alert">
<h2 id="expiring-title"><span aria-hidden="true">⚠️</span> Bevorstehende Abholungen</h2>
<ul class="expiring-list">
@foreach (var ts in expiringPickups)
{
<li class="expiring-item">
<span class="customer">@ts.Customer?.FullName</span>
<span class="date">Erwartet: <time datetime="@ts.ExpectedPickupDate?.ToString("yyyy-MM-dd")">@ts.ExpectedPickupDate?.ToLocalTime().ToString("dd.MM.yyyy")</time></span>
</li>
}
</ul>
</aside>
}
</div>
}
@code {
private static string CheckinUrl(TireStack stack) => $"/tire-checkin?customerId={stack.CustomerId}";
private static string CheckoutUrl(TireStack stack) => $"/tire-checkout?stackId={stack.Id}";
private RenderFragment<TireStack> StackDetailLists => stack => __builder =>
{
<div class="detail-columns">
<section class="detail-list" aria-labelledby="detail-list-customer">
<h3 id="detail-list-customer" class="detail-list-title"><span aria-hidden="true">👤</span> @(stack.IsInventory ? "Bestand & Lagerplatz" : "Kunde & Lagerplatz")</h3>
<dl class="detail-rows">
<div class="detail-row">
<dt>Ticket-Nr.</dt>
<dd class="mono">@stack.TicketNumber</dd>
</div>
@if (stack.IsInventory)
{
<div class="detail-row">
<dt>Kategorie</dt>
<dd>@(stack.CatalogItem?.Name ?? "Lagerbestand")</dd>
</div>
}
else
{
<div class="detail-row">
<dt>Kunde</dt>
<dd>@(stack.Customer?.FullName ?? "-")</dd>
</div>
<div class="detail-row">
<dt>Kundennr.</dt>
<dd class="mono">@(stack.Customer?.CustomerNumber ?? "-")</dd>
</div>
<div class="detail-row">
<dt>Telefon</dt>
<dd>@(stack.Customer?.Phone ?? "-")</dd>
</div>
<div class="detail-row">
<dt>Fahrzeug</dt>
<dd>@(stack.Vehicle != null ? $"{stack.Vehicle.Make} {stack.Vehicle.Model}".Trim() : "-")</dd>
</div>
}
<div class="detail-row">
<dt>Lagerplatz</dt>
<dd>
@if (stack.StorageLocation != null)
{
<span class="location-indicator">
<span class="loc-area @(stack.StorageLocation.Area == AreaType.Werkstatt ? "werkstatt" : "lager")">@stack.StorageLocation.Area.GetDisplayName()</span>
<span class="loc-part">Regal @stack.StorageLocation.Row</span>
<span class="loc-part">@stack.StorageLocation.Position.GetShortName()</span>
<span class="loc-part">P@(stack.StorageLocation.SlotNumber)</span>
</span>
}
else
{
<span>-</span>
}
</dd>
</div>
<div class="detail-row">
<dt>Code</dt>
<dd class="mono">@(stack.StorageLocation?.Code ?? "-")</dd>
</div>
</dl>
</section>
<section class="detail-list" aria-labelledby="detail-list-tires">
<h3 id="detail-list-tires" class="detail-list-title"><span aria-hidden="true">🛞</span> Reifen & Lagerung</h3>
<dl class="detail-rows">
<div class="detail-row">
<dt>Anzahl</dt>
<dd>@stack.ItemCount Reifen</dd>
</div>
<div class="detail-row">
<dt>Größe</dt>
<dd class="mono">@(stack.TireSize ?? "-")</dd>
</div>
<div class="detail-row">
<dt>Marke / Modell</dt>
<dd>@(string.Join(" ", new[] { stack.TireBrand, stack.TireModel }.Where(s => !string.IsNullOrWhiteSpace(s))) is { Length: > 0 } bm ? bm : "-")</dd>
</div>
<div class="detail-row">
<dt>Saison</dt>
<dd>@(stack.Season == TireSeason.Sommer ? "☀️ Sommer" : stack.Season == TireSeason.Winter ? "❄️ Winter" : "🔄 Allrad")</dd>
</div>
<div class="detail-row">
<dt>Felgen</dt>
<dd>@(stack.WithRims ? "Mit Felgen" : "Ohne Felgen")</dd>
</div>
<div class="detail-row">
<dt>Zustand</dt>
<dd>@stack.ConditionText</dd>
</div>
<div class="detail-row">
<dt>Eingelagert</dt>
<dd><time datetime="@stack.StoredAt.ToString("yyyy-MM-dd")">@stack.StoredAt.ToLocalTime().ToString("dd.MM.yyyy")</time> <span class="muted">(@stack.DaysStored Tage)</span></dd>
</div>
@if (stack.ExpectedPickupDate.HasValue)
{
<div class="detail-row">
<dt>Abholung erwartet</dt>
<dd><time datetime="@stack.ExpectedPickupDate.Value.ToString("yyyy-MM-dd")">@stack.ExpectedPickupDate.Value.ToLocalTime().ToString("dd.MM.yyyy")</time></dd>
</div>
}
</dl>
</section>
</div>
<TireInspectionPanel TireStackId="@stack.Id" AllowEdit="@UserSession.IsWorkerOrAdmin" Compact="true"
ScannerSubtitle="@(stack.Customer != null ? $"Kunde: {stack.Customer.FullName}" : stack.TicketNumber)" />
};
private List<TireStack> allTireStacks = new();
private List<TireStack> filteredTireStacks = new();
private List<TireStack> expiringPickups = new();
private List<CatalogItem> categories = new();
private StorageStatistics stats = new();
private string searchTerm = "";
private TireSeason? seasonFilter;
private bool? typeFilter; // null = all, false = customer, true = inventory
private string selectedCategoryId = "";
private TireStack? selectedStack;
private TireStack? editStack;
private TireStack? editSource;
private bool showEditModal;
private bool isSaving;
private string? editError;
private bool isLoading = true;
private bool showGridView;
private List<StorageLocation> allLocations = new();
private Dictionary<(AreaType, int, ShelfPosition, int), StorageLocation> locationLookup = new();
private StorageLocation? selectedLocation;
protected override async Task OnInitializedAsync()
{
if (UserSession.IsWorkerOrAdmin)
{
await LoadData();
}
}
private DotNetObjectReference<TireStorage>? _visibilityRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && UserSession.IsWorkerOrAdmin)
{
_visibilityRef = DotNetObjectReference.Create(this);
await VisibilityRefresh.RegisterAsync(JS, _visibilityRef, nameof(OnVisibilityReturn), 120);
}
}
[JSInvokable]
public async Task OnVisibilityReturn(int hiddenSeconds)
{
if (!UserSession.IsWorkerOrAdmin) return;
await LoadData();
await InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
await VisibilityRefresh.UnregisterAsync(JS);
_visibilityRef?.Dispose();
}
private async Task LoadData()
{
isLoading = true;
StateHasChanged();
try
{
allTireStacks = await TireStackService.GetAllAsync(TireStackStatus.Stored);
stats = await TireStackService.GetStatisticsAsync();
expiringPickups = await TireStackService.GetExpiringPickupsAsync();
categories = await CatalogService.GetAllAsync();
allLocations = await StorageLocationService.GetAllAsync();
locationLookup = allLocations.ToDictionary(l => (l.Area, l.Row, l.Position, l.SlotNumber));
if (selectedLocation != null)
{
selectedLocation = allLocations.FirstOrDefault(l => l.Id == selectedLocation.Id);
}
ApplyFilters();
}
finally
{
isLoading = false;
}
}
private void FilterTireStacks()
{
ApplyFilters();
}
private void SetSeasonFilter(TireSeason? season)
{
seasonFilter = season;
ApplyFilters();
}
private void SetTypeFilter(bool? isInventory)
{
typeFilter = isInventory;
selectedCategoryId = ""; // Reset category filter when changing type
ApplyFilters();
}
private bool HasActiveFilters =>
typeFilter != null || seasonFilter != null ||
!string.IsNullOrEmpty(selectedCategoryId) || !string.IsNullOrWhiteSpace(searchTerm);
private void ResetFilters()
{
typeFilter = null;
seasonFilter = null;
selectedCategoryId = "";
searchTerm = "";
ApplyFilters();
}
private void ApplyFilters()
{
var query = allTireStacks.AsEnumerable();
// Type filter (customer vs inventory)
if (typeFilter.HasValue)
{
if (typeFilter.Value)
query = query.Where(ts => ts.CustomerId == null);
else
query = query.Where(ts => ts.CustomerId != null);
}
// Season filter
if (seasonFilter.HasValue)
{
query = query.Where(ts => ts.Season == seasonFilter.Value);
}
// Category filter (for inventory)
if (!string.IsNullOrEmpty(selectedCategoryId) && Guid.TryParse(selectedCategoryId, out var catId))
{
query = query.Where(ts => ts.CatalogItemId == catId);
}
// Search term
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(ts =>
(ts.Customer?.FullName?.ToLower().Contains(term) ?? false) ||
(ts.Vehicle?.Make?.ToLower().Contains(term) ?? false) ||
(ts.Vehicle?.Model?.ToLower().Contains(term) ?? false) ||
(ts.Customer?.CustomerNumber?.ToLower().Contains(term) ?? false) ||
(ts.CatalogItem?.Name?.ToLower().Contains(term) ?? false) ||
(ts.StorageLocation?.Code?.ToLower().Contains(term) ?? false) ||
ts.TicketNumber.ToLower().Contains(term) ||
(ts.TireBrand?.ToLower().Contains(term) ?? false) ||
(ts.TireSize?.ToLower().Contains(term) ?? false)
);
}
filteredTireStacks = query.OrderBy(ts => ts.StorageLocation?.Code).ToList();
}
private void SelectStack(TireStack ts)
{
selectedStack = selectedStack?.Id == ts.Id ? null : ts;
}
private void OpenEditModal()
{
if (selectedStack == null) return;
OpenEditModalFor(selectedStack);
}
private void OpenEditModalFor(TireStack source)
{
editSource = source;
editStack = new TireStack
{
Id = source.Id,
TireSize = source.TireSize,
TireBrand = source.TireBrand,
TireModel = source.TireModel,
ItemCount = source.ItemCount,
Season = source.Season,
Condition = source.Condition,
WithRims = source.WithRims,
Notes = source.Notes,
TicketNumber = source.TicketNumber
};
editError = null;
showEditModal = true;
}
private void CloseEditModal()
{
if (isSaving) return;
showEditModal = false;
editStack = null;
editSource = null;
editError = null;
}
private async Task SaveEdit()
{
if (editStack == null || editSource == null) return;
isSaving = true;
editError = null;
try
{
editSource.TireSize = editStack.TireSize;
editSource.TireBrand = editStack.TireBrand;
editSource.TireModel = editStack.TireModel;
editSource.ItemCount = editStack.ItemCount;
editSource.Season = editStack.Season;
editSource.Condition = editStack.Condition;
editSource.WithRims = editStack.WithRims;
editSource.Notes = editStack.Notes;
await TireStackService.UpdateAsync(editSource);
showEditModal = false;
editStack = null;
editSource = null;
var selectedId = selectedStack?.Id;
await LoadData();
if (selectedId.HasValue)
{
selectedStack = allTireStacks.FirstOrDefault(ts => ts.Id == selectedId.Value);
}
}
catch (Exception ex)
{
editError = $"Fehler beim Speichern: {ex.Message}";
}
finally
{
isSaving = false;
}
}
private StorageLocation? GetLocation(AreaType area, int row, ShelfPosition pos, int slot)
{
return locationLookup.TryGetValue((area, row, pos, slot), out var loc) ? loc : null;
}
private void SelectLocation(StorageLocation? location)
{
if (location == null) return;
selectedLocation = selectedLocation?.Id == location.Id ? null : location;
}
private string GetCellTooltip(StorageLocation? loc, AreaType area, int row, ShelfPosition pos, int slot)
{
var baseText = $"{area.GetDisplayName()}, Reihe {row}, {pos.GetDisplayName()}, Platz {slot}";
if (loc == null) return $"{baseText} – nicht angelegt";
if (!loc.IsAvailable) return $"{baseText} – gesperrt";
if (loc.IsOccupied)
{
var stack = loc.OccupiedTireStack;
var who = stack == null ? "belegt" : stack.IsInventory ? stack.CatalogItem?.Name ?? "Lagerbestand" : stack.Customer?.FullName ?? "belegt";
return $"{baseText} – {who}";
}
return $"{baseText} – frei";
}
}