Release 1.1.0: QR-Reportimport, Regal-Auswahlmodal, Einlagern/Auslagern-Verknuepfungen aus Reifenlager, Stabilitaetsfixes
This commit is contained in:
1 parent
cba9414e92
commit
3404fb9875
25 files changed
+5106
-744
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user