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

+515 -222
View File
@@ -40,120 +40,101 @@ else
</div>
</header>
@* Statistics *@
<section class="stats-grid" aria-label="Lagerstatistik">
<article class="stat-card">
<span class="stat-value" aria-label="Anzahl">@stats.TotalStacksStored</span>
<span class="stat-label">Stapel (@stats.TotalTiresStored Reifen)</span>
</article>
<article class="stat-card">
<span class="stat-value" aria-label="Anzahl">@stats.CustomerStackCount</span>
<span class="stat-label"><span aria-hidden="true">👤</span> Kundenstapel</span>
</article>
<article class="stat-card">
<span class="stat-value" aria-label="Anzahl">@stats.InventoryStackCount</span>
<span class="stat-label"><span aria-hidden="true">📦</span> Lagerbestand</span>
</article>
<article class="stat-card">
<span class="stat-value" aria-label="Anzahl">@stats.AvailableLocations</span>
<span class="stat-label">Freie Plätze</span>
</article>
@* 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>
@* Type Filter (Customer vs Inventory) *@
<nav class="type-filter" role="tablist" aria-label="Filter nach Typ">
<button class="type-btn @(typeFilter == null ? "active" : "")"
@onclick="() => SetTypeFilter(null)"
role="tab"
aria-selected="@(typeFilter == null)">
Alle
</button>
<button class="type-btn @(typeFilter == false ? "active" : "")"
@onclick="() => SetTypeFilter(false)"
role="tab"
aria-selected="@(typeFilter == false)">
<span aria-hidden="true">👤</span> Kundenstapel
</button>
<button class="type-btn @(typeFilter == true ? "active" : "")"
@onclick="() => SetTypeFilter(true)"
role="tab"
aria-selected="@(typeFilter == true)">
<span aria-hidden="true">📦</span> Lagerbestand
</button>
</nav>
@* View Toggle (List / Rack Grid) *@
<nav class="view-toggle" role="tablist" aria-label="Ansicht wählen">
<button class="view-btn @(showGridView ? "" : "active")"
@onclick="() => showGridView = false"
role="tab"
aria-selected="@(!showGridView)">
<span aria-hidden="true">☰</span> Liste
</button>
<button class="view-btn @(showGridView ? "active" : "")"
@onclick="() => showGridView = true"
role="tab"
aria-selected="@showGridView">
<span aria-hidden="true">▦</span> Regalansicht
</button>
</nav>
@* Search & Filter *@
<section class="filter-bar" aria-label="Suche und Filter">
<label for="search-input" class="visually-hidden">Suche</label>
<input type="search"
id="search-input"
class="search-input"
placeholder="Suchen (Name, Fahrzeug, Kategorie, 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>
<fieldset class="filter-buttons">
<legend class="visually-hidden">Nach Saison filtern</legend>
<button class="filter-btn @(seasonFilter == null ? "active" : "")"
@onclick="() => SetSeasonFilter(null)"
aria-pressed="@(seasonFilter == null)">
Alle
</button>
<button class="filter-btn @(seasonFilter == TireSeason.Sommer ? "active summer" : "")"
@onclick="() => SetSeasonFilter(TireSeason.Sommer)"
aria-pressed="@(seasonFilter == TireSeason.Sommer)"
aria-label="Sommerreifen">
<span aria-hidden="true">☀️</span>
</button>
<button class="filter-btn @(seasonFilter == TireSeason.Winter ? "active winter" : "")"
@onclick="() => SetSeasonFilter(TireSeason.Winter)"
aria-pressed="@(seasonFilter == TireSeason.Winter)"
aria-label="Winterreifen">
<span aria-hidden="true">❄️</span>
</button>
<button class="filter-btn @(seasonFilter == TireSeason.Allrad ? "active allseason" : "")"
@onclick="() => SetSeasonFilter(TireSeason.Allrad)"
aria-pressed="@(seasonFilter == TireSeason.Allrad)"
aria-label="Ganzjahresreifen">
<span aria-hidden="true">🔄</span>
</button>
</fieldset>
</section>
@* Category Filter (only when showing inventory) *@
@if (typeFilter == true && categories.Count > 0)
{
<section class="category-filter">
<label for="category-select" class="visually-hidden">Kategorie auswählen</label>
<select id="category-select" class="form-control" @bind="selectedCategoryId" @bind:after="FilterTireStacks">
<option value="">-- Alle Kategorien --</option>
@foreach (var cat in categories)
{
<option value="@cat.Id">@cat.Name</option>
}
</select>
</section>
}
@* Tire Stacks List *@
@if (isLoading)
{
@@ -224,24 +205,22 @@ else
@if (selectedLocation.IsOccupied && selectedLocation.OccupiedTireStack != null)
{
var stack = selectedLocation.OccupiedTireStack;
<dl class="detail-grid">
<div class="detail-item">
<dt class="label">Ticket-Nr.</dt>
<dd class="value">@stack.TicketNumber</dd>
@StackDetailLists(stack)
@if (!string.IsNullOrEmpty(stack.Notes))
{
<div class="notes-section">
<span class="label">Notizen:</span>
<span class="notes-text">@stack.Notes</span>
</div>
<div class="detail-item">
<dt class="label">@(stack.IsInventory ? "Kategorie" : "Kunde")</dt>
<dd class="value">@(stack.IsInventory ? stack.CatalogItem?.Name ?? "Lagerbestand" : stack.Customer?.FullName ?? "-")</dd>
</div>
<div class="detail-item">
<dt class="label">Reifen</dt>
<dd class="value">@(stack.TireSize ?? "-") @(stack.WithRims ? "| mit Felgen" : "")</dd>
</div>
<div class="detail-item">
<dt class="label">Eingelagert</dt>
<dd class="value">@stack.StoredAt.ToLocalTime().ToString("dd.MM.yyyy")</dd>
</div>
</dl>
}
<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)
{
@@ -270,110 +249,171 @@ else
}
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" : "")"
@onclick="() => ToggleDetails(ts.Id)"
<article class="tireset-card @(ts.IsInventory ? "inventory" : "") @(selectedStack?.Id == ts.Id ? "selected" : "")"
@onclick="() => SelectStack(ts)"
role="listitem"
aria-expanded="@(expandedId == ts.Id)">
<div class="tireset-main">
<div class="tireset-left">
<div 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 ? "❄️" : "🔄")
</div>
<div class="tireset-info">
@if (ts.IsInventory)
{
<span class="customer-name inventory-name">
<span aria-hidden="true">📦</span> @(ts.CatalogItem?.Name ?? "Lagerbestand")
</span>
}
else
{
<span class="customer-name">@ts.Customer?.FullName</span>
}
<span class="tire-details">1 Stapel (@TireStack.TiresPerStack Reifen) @(ts.TireSize != null ? $"| {ts.TireSize}" : "") @(ts.WithRims ? "| mit Felgen" : "")</span>
</div>
</div>
<div class="tireset-right">
<span class="location-code">@(ts.StorageLocation?.Code ?? "-")</span>
@if (!ts.IsInventory && ts.Vehicle != null)
{
<span class="vehicle-info">@ts.Vehicle.ShortName</span>
}
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>
@if (expandedId == ts.Id)
{
<div class="tireset-details">
<dl class="detail-grid">
<div class="detail-item">
<dt class="label">Ticket-Nr.</dt>
<dd class="value">@ts.TicketNumber</dd>
</div>
<div class="detail-item">
<dt class="label">Reifenstapel</dt>
<dd class="value">1 Stapel (@TireStack.TiresPerStack Reifen)</dd>
</div>
@if (ts.IsInventory && ts.CatalogItem != null)
{
<div class="detail-item">
<dt class="label">Kategorie</dt>
<dd class="value">@ts.CatalogItem.Name</dd>
</div>
}
<div class="detail-item">
<dt class="label">Marke</dt>
<dd class="value">@(ts.TireBrand ?? "-")</dd>
</div>
<div class="detail-item">
<dt class="label">Modell</dt>
<dd class="value">@(ts.TireModel ?? "-")</dd>
</div>
<div class="detail-item">
<dt class="label">Zustand</dt>
<dd class="value">@ts.ConditionText</dd>
</div>
<div class="detail-item">
<dt class="label">Eingelagert</dt>
<dd class="value"><time datetime="@ts.StoredAt.ToString("yyyy-MM-dd")">@ts.StoredAt.ToLocalTime().ToString("dd.MM.yyyy")</time></dd>
</div>
<div class="detail-item">
<dt class="label">Dauer</dt>
<dd class="value">@ts.DaysStored Tage</dd>
</div>
</dl>
@if (!string.IsNullOrEmpty(ts.Notes))
{
<div class="notes-section">
<span class="label">Notizen:</span>
<span class="notes-text">@ts.Notes</span>
</div>
}
<nav class="detail-actions" aria-label="Aktionen für diesen Stapel">
@if (!ts.IsInventory)
{
<a href="/tire-checkout" class="btn btn-sm btn-primary">Abholen</a>
}
else
{
<a href="/tire-checkout" class="btn btn-sm btn-primary">Entnehmen</a>
}
</nav>
</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>
@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)
{
@@ -394,6 +434,136 @@ else
}
@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>
}
@if (latestInspections.TryGetValue(stack.Id, out var insp) && insp != null)
{
<div class="detail-row">
<dt>Letzte Messung</dt>
<dd>
<div class="tread-chips">
<span class="tread-chip rating-@RatingClass(insp.RatingFL)">VL @FmtMm(insp.TreadDepthFL)</span>
<span class="tread-chip rating-@RatingClass(insp.RatingFR)">VR @FmtMm(insp.TreadDepthFR)</span>
<span class="tread-chip rating-@RatingClass(insp.RatingRL)">HL @FmtMm(insp.TreadDepthRL)</span>
<span class="tread-chip rating-@RatingClass(insp.RatingRR)">HR @FmtMm(insp.TreadDepthRR)</span>
</div>
<span class="muted">
@((insp.MeasuredAt ?? insp.CreatedAt).ToLocalTime().ToString("dd.MM.yyyy"))
@if (insp.Mileage.HasValue) { <text> · @insp.Mileage.Value.ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("de-DE")) km</text> }
@if (!string.IsNullOrEmpty(insp.LicensePlate)) { <text> · @insp.LicensePlate</text> }
</span>
@if (!string.IsNullOrEmpty(insp.ReportUrl))
{
<a class="report-link" href="@insp.ReportUrl" target="_blank" rel="noopener">Report öffnen ↗</a>
}
</dd>
</div>
}
</dl>
</section>
</div>
};
private List<TireStack> allTireStacks = new();
private List<TireStack> filteredTireStacks = new();
private List<TireStack> expiringPickups = new();
@@ -404,7 +574,36 @@ else
private TireSeason? seasonFilter;
private bool? typeFilter; // null = all, false = customer, true = inventory
private string selectedCategoryId = "";
private Guid? expandedId;
private TireStack? selectedStack;
private readonly Dictionary<Guid, TireInspection?> latestInspections = new();
private static string RatingClass(TireMeasureRating r) => r switch
{
TireMeasureRating.Ok => "ok",
TireMeasureRating.Warning => "warn",
TireMeasureRating.Critical => "crit",
_ => "none"
};
private static string FmtMm(decimal? mm) => mm.HasValue ? mm.Value.ToString("0.0", System.Globalization.CultureInfo.GetCultureInfo("de-DE")) + " mm" : "–";
private async Task LoadLatestInspection(TireStack ts)
{
if (latestInspections.ContainsKey(ts.Id)) return;
try
{
latestInspections[ts.Id] = await TireStackService.GetLatestInspectionAsync(ts.Id);
}
catch
{
latestInspections[ts.Id] = null;
}
}
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();
@@ -487,6 +686,19 @@ else
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();
@@ -532,9 +744,86 @@ else
filteredTireStacks = query.OrderBy(ts => ts.StorageLocation?.Code).ToList();
}
private void ToggleDetails(Guid id)
private async Task SelectStack(TireStack ts)
{
expandedId = expandedId == id ? null : id;
selectedStack = selectedStack?.Id == ts.Id ? null : ts;
if (selectedStack != null)
{
await LoadLatestInspection(selectedStack);
}
}
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)
@@ -542,10 +831,14 @@ else
return locationLookup.TryGetValue((area, row, pos, slot), out var loc) ? loc : null;
}
private void SelectLocation(StorageLocation? location)
private async Task SelectLocation(StorageLocation? location)
{
if (location == null) return;
selectedLocation = selectedLocation?.Id == location.Id ? null : location;
if (selectedLocation?.OccupiedTireStack != null)
{
await LoadLatestInspection(selectedLocation.OccupiedTireStack);
}
}
private string GetCellTooltip(StorageLocation? loc, AreaType area, int row, ShelfPosition pos, int slot)