1138 lines
50 KiB
Plaintext
1138 lines
50 KiB
Plaintext
@page "/tire-checkout"
|
||
@using Lagerbestand.Components.Shared
|
||
@implements IAsyncDisposable
|
||
@inject ICustomerService CustomerService
|
||
@inject ITireStackService TireStackService
|
||
@inject IStorageLocationService StorageLocationService
|
||
@inject ITireReportService TireReportService
|
||
@inject UserSessionService UserSession
|
||
|
||
<PageTitle>Reifenstapel Abholen - Reifen Späth Lagerverwaltung</PageTitle>
|
||
|
||
<SeoMetaTags
|
||
Title="Reifenstapel Abholen - Reifen Späth Lagerverwaltung"
|
||
Description="Reifen abholen in der Reifen Späth Lagerverwaltung. Schnelle Abwicklung von Kundenabholungen."
|
||
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
|
||
{
|
||
<main class="checkout-page" role="main">
|
||
<header class="page-header">
|
||
<h1>Reifenstapel Abholen</h1>
|
||
<p>Suchen Sie nach Ticket-Nummer, Kundenname, Fahrzeug oder Lagerplatz-Code</p>
|
||
</header>
|
||
|
||
@* Search Section *@
|
||
<section class="checkout-card" aria-labelledby="search-heading">
|
||
<h2 id="search-heading" class="visually-hidden">Suche</h2>
|
||
<div class="search-section">
|
||
<label for="search-input" class="visually-hidden">Suche nach Ticket-Nr., Kunde, Fahrzeug oder Lagerplatz</label>
|
||
<input type="search"
|
||
id="search-input"
|
||
class="form-control search-input"
|
||
placeholder="Ticket-Nr., Kunde, Fahrzeug oder Lagerplatz (z.B. 2135)..."
|
||
@bind="searchTerm"
|
||
@bind:event="oninput"
|
||
@onkeyup="SearchTireStacks" />
|
||
</div>
|
||
|
||
@if (!string.IsNullOrEmpty(errorMessage))
|
||
{
|
||
<div class="alert alert-danger" role="alert" aria-live="assertive">@errorMessage</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrEmpty(successMessage))
|
||
{
|
||
<div class="alert alert-success" role="status" aria-live="polite">@successMessage</div>
|
||
}
|
||
</section>
|
||
|
||
@* Scanned Items List *@
|
||
@if (scannedTireStacks.Count > 0)
|
||
{
|
||
<div class="scanned-section" aria-labelledby="scanned-items-heading">
|
||
<div class="scanned-header">
|
||
<h2 id="scanned-items-heading">
|
||
<span class="scan-count">@scannedTireStacks.Count</span>
|
||
Reifenstapel zum Abholen
|
||
</h2>
|
||
<button class="btn btn-sm btn-outline-danger" @onclick="ClearAllScanned">
|
||
Alle entfernen
|
||
</button>
|
||
</div>
|
||
|
||
<div class="scanned-summary" role="complementary">
|
||
<div class="summary-item">
|
||
<span class="summary-value">@scannedTireStacks.Count</span>
|
||
<span class="summary-label">Stapel</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-value">@(scannedTireStacks.Count * TireStack.TiresPerStack)</span>
|
||
<span class="summary-label">Reifen gesamt</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-value">@scannedTireStacks.Select(ts => ts.StorageLocation?.Code).Distinct().Count()</span>
|
||
<span class="summary-label">Lagerplätze</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="scanned-list" role="list">
|
||
@foreach (var ts in scannedTireStacks)
|
||
{
|
||
<div class="scanned-item @(ts == lastAddedTireStack ? "highlight" : "")" role="listitem">
|
||
<div class="scanned-item-header">
|
||
<div class="scanned-item-main">
|
||
<span class="ticket-number">@ts.TicketNumber</span>
|
||
@if (ts.StorageLocation != null)
|
||
{
|
||
<LocationDisplay Code="@ts.StorageLocation.Code"
|
||
DisplayMode="LocationDisplayMode.Compact"
|
||
Size="LocationDisplaySize.Small" />
|
||
}
|
||
else
|
||
{
|
||
<span class="location-badge">📍 -</span>
|
||
}
|
||
</div>
|
||
<button class="btn-remove" @onclick="() => RemoveScannedItem(ts)" title="Entfernen" aria-label="Entfernen von @ts.TicketNumber">✕</button>
|
||
</div>
|
||
|
||
<div class="scanned-item-body">
|
||
<div class="scanned-customer">
|
||
@if (ts.Customer != null)
|
||
{
|
||
<span class="customer-name">@ts.Customer.FullName</span>
|
||
@if (ts.Vehicle != null)
|
||
{
|
||
<span class="vehicle-info">@ts.Vehicle.ShortName</span>
|
||
}
|
||
}
|
||
else if (ts.CatalogItem != null)
|
||
{
|
||
<span class="customer-name inventory">📦 @ts.CatalogItem.Name</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="customer-name inventory">📦 Lagerbestand</span>
|
||
}
|
||
</div>
|
||
|
||
<div class="scanned-details">
|
||
<span class="tire-count">1 Stapel (@TireStack.TiresPerStack Reifen)</span>
|
||
<span class="tire-season">
|
||
@(ts.Season == TireSeason.Sommer ? "☀️" : ts.Season == TireSeason.Winter ? "❄️" : "🔄")
|
||
</span>
|
||
@if (!string.IsNullOrEmpty(ts.TireSize))
|
||
{
|
||
<span class="tire-size">@ts.TireSize</span>
|
||
}
|
||
@if (ts.WithRims)
|
||
{
|
||
<span class="with-rims">mit Felgen</span>
|
||
}
|
||
</div>
|
||
|
||
@if (!string.IsNullOrEmpty(ts.TireBrand))
|
||
{
|
||
<div class="scanned-brand">
|
||
@ts.TireBrand @ts.TireModel
|
||
</div>
|
||
}
|
||
|
||
<div class="scanned-meta">
|
||
<span class="stored-date">Eingelagert: @ts.StoredAt.ToLocalTime().ToString("dd.MM.yyyy")</span>
|
||
<span class="days-stored">(@ts.DaysStored Tage)</span>
|
||
</div>
|
||
<TireInspectionPanel TireStackId="@ts.Id" AllowEdit="true" Compact="true"
|
||
ScannerSubtitle="@(ts.Customer != null ? $"Kunde: {ts.Customer.FullName}" : ts.TicketNumber)" />
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="scanned-actions">
|
||
<button class="btn btn-primary btn-lg w-100"
|
||
@onclick="ProcessBatchPickup"
|
||
disabled="@(isProcessing || scannedTireStacks.Count == 0)">
|
||
@if (isProcessing)
|
||
{
|
||
<span class="spinner"></span>
|
||
}
|
||
<span>@scannedTireStacks.Count Stapel abholen (@(scannedTireStacks.Count * TireStack.TiresPerStack) Reifen)</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@* Tire Swap Panel *@
|
||
@if (swapOutgoing != null)
|
||
{
|
||
<section class="swap-panel" aria-labelledby="swap-heading">
|
||
<header class="swap-header">
|
||
<h2 id="swap-heading"><span aria-hidden="true">🔄</span> Reifenwechsel</h2>
|
||
<button type="button" class="btn btn-sm btn-outline" @onclick="CancelSwap" disabled="@isProcessing">Abbrechen</button>
|
||
</header>
|
||
|
||
<div class="swap-columns">
|
||
@* Outgoing *@
|
||
<div class="swap-box outgoing">
|
||
<h3 class="swap-box-title"><span aria-hidden="true">⬆️</span> Herausgabe (wird montiert)</h3>
|
||
<dl class="swap-rows">
|
||
<div class="swap-row"><dt>Ticket</dt><dd class="mono">@swapOutgoing.TicketNumber</dd></div>
|
||
<div class="swap-row"><dt>Kunde</dt><dd>@swapOutgoing.Customer?.FullName</dd></div>
|
||
@if (swapOutgoing.Vehicle != null)
|
||
{
|
||
<div class="swap-row"><dt>Fahrzeug</dt><dd>@swapOutgoing.Vehicle.Make @swapOutgoing.Vehicle.Model</dd></div>
|
||
}
|
||
<div class="swap-row"><dt>Saison</dt><dd>@SeasonLabel(swapOutgoing.Season)</dd></div>
|
||
<div class="swap-row"><dt>Größe</dt><dd class="mono">@(swapOutgoing.TireSize ?? "-")</dd></div>
|
||
<div class="swap-row"><dt>Marke / Modell</dt><dd>@(string.IsNullOrWhiteSpace(swapOutgoing.TireBrand) && string.IsNullOrWhiteSpace(swapOutgoing.TireModel) ? "-" : $"{swapOutgoing.TireBrand} {swapOutgoing.TireModel}".Trim())</dd></div>
|
||
<div class="swap-row"><dt>Lagerplatz</dt><dd>
|
||
@if (swapOutgoing.StorageLocation != null)
|
||
{
|
||
<LocationDisplay Code="@swapOutgoing.StorageLocation.Code" DisplayMode="LocationDisplayMode.Compact" Size="LocationDisplaySize.Small" />
|
||
}
|
||
else
|
||
{
|
||
<span>-</span>
|
||
}
|
||
</dd></div>
|
||
</dl>
|
||
<TireInspectionPanel TireStackId="@swapOutgoing.Id" AllowEdit="true" Compact="true"
|
||
ScannerSubtitle="@(swapOutgoing.Customer != null ? $"Kunde: {swapOutgoing.Customer.FullName}" : swapOutgoing.TicketNumber)" />
|
||
</div>
|
||
|
||
@* Incoming *@
|
||
<div class="swap-box incoming">
|
||
<h3 class="swap-box-title"><span aria-hidden="true">⬇️</span> Einlagerung (kommt vom Fahrzeug)</h3>
|
||
|
||
@* QR report scan for the incoming set *@
|
||
<div class="report-scan-bar">
|
||
@if (swapReport == null)
|
||
{
|
||
<div class="report-scan-text">
|
||
<span class="report-scan-icon" aria-hidden="true">📷</span>
|
||
<span>Profiltiefen-Report (Launch TTS01) per QR-Code übernehmen</span>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-outline" @onclick="OpenQrScanner" disabled="@(isLoadingReport || isProcessing)">
|
||
@if (isLoadingReport)
|
||
{
|
||
<span class="spinner"></span>
|
||
<span>Report wird geladen…</span>
|
||
}
|
||
else
|
||
{
|
||
<span>QR-Code scannen</span>
|
||
}
|
||
</button>
|
||
}
|
||
else
|
||
{
|
||
<div class="report-attached">
|
||
<div class="report-attached-head">
|
||
<span class="report-scan-icon" aria-hidden="true">📋</span>
|
||
<strong>Report wird mit der Einlagerung gespeichert</strong>
|
||
<button type="button" class="btn-link" @onclick="RemoveSwapReport">Entfernen</button>
|
||
</div>
|
||
@ReportSummaryFragment(swapReport)
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="swap-tabs" role="tablist">
|
||
<button type="button" class="swap-tab @(swapUseHistory ? "active" : "")" role="tab" aria-selected="@swapUseHistory"
|
||
@onclick="() => swapUseHistory = true" disabled="@(swapHistory.Count == 0)">
|
||
Aus Historie (@swapHistory.Count)
|
||
</button>
|
||
<button type="button" class="swap-tab @(!swapUseHistory ? "active" : "")" role="tab" aria-selected="@(!swapUseHistory)"
|
||
@onclick="() => swapUseHistory = false">
|
||
Neu erfassen
|
||
</button>
|
||
</div>
|
||
|
||
@if (swapUseHistory)
|
||
{
|
||
<div class="history-list" role="list">
|
||
@foreach (var h in swapHistory)
|
||
{
|
||
var isSameSeason = h.Season == swapOutgoing.Season;
|
||
<button type="button" class="history-item @(swapSelectedHistory?.Id == h.Id ? "selected" : "") @(isSameSeason ? "same-season" : "")"
|
||
role="listitem" @onclick="() => swapSelectedHistory = h">
|
||
<span class="history-season">@SeasonIcon(h.Season)</span>
|
||
<span class="history-main">
|
||
<span class="mono">@h.TicketNumber</span>
|
||
<span class="history-detail">@(h.TireSize ?? "-") @(string.IsNullOrWhiteSpace(h.TireBrand) ? "" : $"| {h.TireBrand} {h.TireModel}".Trim()) @(h.WithRims ? "| mit Felgen" : "")</span>
|
||
<span class="history-meta">@h.StatusText @(h.PickedUpAt.HasValue ? $"seit {h.PickedUpAt.Value.ToLocalTime():dd.MM.yyyy}" : "")</span>
|
||
</span>
|
||
@if (isSameSeason)
|
||
{
|
||
<span class="history-warn" title="Gleiche Saison wie Herausgabe">⚠️</span>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label for="swap-season">Saison</label>
|
||
<select id="swap-season" class="form-control" @bind="swapNew.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="swap-size">Reifengröße</label>
|
||
<input id="swap-size" class="form-control" @bind="swapNew.TireSize" placeholder="z.B. 225/45R17" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="swap-brand">Marke</label>
|
||
<input id="swap-brand" class="form-control" @bind="swapNew.TireBrand" placeholder="z.B. Michelin" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="swap-model">Modell</label>
|
||
<input id="swap-model" class="form-control" @bind="swapNew.TireModel" placeholder="z.B. Pilot Sport 4" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="swap-rims">Felgen</label>
|
||
<select id="swap-rims" class="form-control" @bind="swapNew.WithRims">
|
||
<option value="true">Mit Felgen</option>
|
||
<option value="false">Ohne Felgen</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="swap-condition">Zustand</label>
|
||
<select id="swap-condition" class="form-control" @bind="swapNew.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 form-group-wide">
|
||
<label for="swap-notes">Notizen</label>
|
||
<textarea id="swap-notes" class="form-control" rows="2" @bind="swapNew.Notes"></textarea>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
@* Target location *@
|
||
<div class="swap-location">
|
||
<span class="swap-location-label">Lagerplatz für Einlagerung:</span>
|
||
@if (swapTargetLocation != null)
|
||
{
|
||
<LocationDisplay Code="@swapTargetLocation.Code" DisplayMode="LocationDisplayMode.Compact" Size="LocationDisplaySize.Small" />
|
||
@if (swapTargetLocation.Id == swapOutgoing.StorageLocationId)
|
||
{
|
||
<span class="swap-location-hint">(frei werdender Platz)</span>
|
||
}
|
||
else
|
||
{
|
||
<button type="button" class="btn btn-sm btn-outline" @onclick="ResetTargetLocation">Zurück zum alten Platz</button>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<span class="swap-location-hint">Kein Lagerplatz gewählt</span>
|
||
}
|
||
<button type="button" class="btn btn-sm btn-outline" @onclick="() => showLocationPicker = true">
|
||
<span aria-hidden="true">▦</span> Anderen Platz wählen
|
||
</button>
|
||
</div>
|
||
|
||
<div class="swap-actions">
|
||
<button type="button" class="btn btn-primary btn-lg w-100" @onclick="ProcessSwap" disabled="@(isProcessing || !CanSwap)">
|
||
@if (isProcessing)
|
||
{
|
||
<span class="spinner"></span>
|
||
}
|
||
<span>Reifenwechsel durchführen</span>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
}
|
||
|
||
@* Search Results *@
|
||
@if (searchResults.Count > 0)
|
||
{
|
||
<div class="results-section" aria-labelledby="search-results-heading">
|
||
<h2 id="search-results-heading" class="results-count">
|
||
@searchResults.Count Ergebnis(se)
|
||
</h2>
|
||
|
||
<div class="tireset-list" role="list">
|
||
@foreach (var ts in searchResults)
|
||
{
|
||
var isAlreadyScanned = scannedTireStacks.Any(s => s.Id == ts.Id);
|
||
<div class="tireset-card @(isAlreadyScanned ? "already-scanned" : "")"
|
||
@onclick="() => OnResultClicked(ts)" role="listitem">
|
||
<div class="tireset-header">
|
||
<span class="ticket-number">@ts.TicketNumber</span>
|
||
<span class="status-badge @ts.Status.ToString().ToLower()">
|
||
@if (isAlreadyScanned)
|
||
{
|
||
<span>✓ Ausgewählt</span>
|
||
}
|
||
else
|
||
{
|
||
@ts.StatusText
|
||
}
|
||
</span>
|
||
</div>
|
||
<div class="tireset-body">
|
||
<div class="customer-info">
|
||
@if (ts.Customer != null)
|
||
{
|
||
<span class="customer-name">@ts.Customer.FullName</span>
|
||
@if (ts.Vehicle != null)
|
||
{
|
||
<span class="vehicle-info">@ts.Vehicle.ShortName</span>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<span class="customer-name">📦 @(ts.CatalogItem?.Name ?? "Lagerbestand")</span>
|
||
}
|
||
</div>
|
||
<div class="tire-info">
|
||
<span class="tire-count">1 Stapel (@TireStack.TiresPerStack Reifen)</span>
|
||
<span class="tire-details">@ts.Season @(ts.TireSize != null ? $"| {ts.TireSize}" : "")</span>
|
||
</div>
|
||
<div class="location-info">
|
||
@if (ts.StorageLocation != null)
|
||
{
|
||
<LocationDisplay Code="@ts.StorageLocation.Code"
|
||
DisplayMode="LocationDisplayMode.Full"
|
||
Size="LocationDisplaySize.Small" />
|
||
}
|
||
else
|
||
{
|
||
<span class="location">📍 Kein Lagerplatz</span>
|
||
}
|
||
<span class="stored-date">Seit @ts.StoredAt.ToLocalTime().ToString("dd.MM.yyyy")</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@* Storage location picker (rack view) for swap *@
|
||
@if (swapOutgoing != null)
|
||
{
|
||
<StorageLocationPickerModal @bind-IsOpen="showLocationPicker"
|
||
SelectedLocationId="@swapTargetLocation?.Id"
|
||
HighlightLocationId="@swapOutgoing.StorageLocationId"
|
||
Subtitle="@($"Einlagerung für {swapOutgoing.Customer?.FullName} – {SeasonLabel(swapUseHistory ? (swapSelectedHistory?.Season ?? swapNew.Season) : swapNew.Season)}")"
|
||
OnSelected="OnTargetLocationSelected" />
|
||
|
||
<QrScannerModal @bind-IsOpen="showQrScanner"
|
||
Title="Reifen-Report scannen"
|
||
Subtitle="@(swapOutgoing.Customer != null ? $"Kunde: {swapOutgoing.Customer.FullName}" : null)"
|
||
OnScanned="OnQrScanned" />
|
||
}
|
||
|
||
@* Report preview modal (swap) *@
|
||
@if (scannedReport != null)
|
||
{
|
||
<div class="report-overlay" @onclick="CloseReportModal">
|
||
<div class="report-dialog" role="dialog" aria-modal="true" aria-labelledby="report-title" @onclick:stopPropagation="true">
|
||
<div class="report-header">
|
||
<div>
|
||
<h3 id="report-title">📋 Reifen-Report</h3>
|
||
<p class="report-subtitle">
|
||
Nr. <span class="mono">@scannedReport.RecordSn</span>
|
||
@if (scannedReport.MeasuredAt.HasValue)
|
||
{
|
||
<text> · gemessen am @scannedReport.MeasuredAt.Value.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</text>
|
||
}
|
||
</p>
|
||
</div>
|
||
<button type="button" class="report-close" @onclick="CloseReportModal" aria-label="Schließen">✕</button>
|
||
</div>
|
||
|
||
<div class="report-body">
|
||
@if (!scannedReport.HasTreadData)
|
||
{
|
||
<div class="report-warning">
|
||
⚠️ Der Report enthält keine Profiltiefen-Werte. Werte können unten manuell ergänzt werden.
|
||
</div>
|
||
}
|
||
|
||
<div class="report-meta">
|
||
<div class="form-group">
|
||
<label>Kennzeichen</label>
|
||
<input type="text" class="form-control" @bind="scannedReport.LicensePlate" placeholder="z.B. ND-XX 123" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Kilometerstand</label>
|
||
<input type="number" class="form-control" @bind="scannedReport.Mileage" min="0" placeholder="km" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tread-grid" role="group" aria-label="Profiltiefen">
|
||
<div class="tread-cell rating-@RatingClass(scannedReport, scannedReport.TreadDepthFL)">
|
||
<label>Vorne links</label>
|
||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="scannedReport.TreadDepthFL" /><span>mm</span></div>
|
||
</div>
|
||
<div class="tread-cell rating-@RatingClass(scannedReport, scannedReport.TreadDepthFR)">
|
||
<label>Vorne rechts</label>
|
||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="scannedReport.TreadDepthFR" /><span>mm</span></div>
|
||
</div>
|
||
<div class="tread-cell rating-@RatingClass(scannedReport, scannedReport.TreadDepthRL)">
|
||
<label>Hinten links</label>
|
||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="scannedReport.TreadDepthRL" /><span>mm</span></div>
|
||
</div>
|
||
<div class="tread-cell rating-@RatingClass(scannedReport, scannedReport.TreadDepthRR)">
|
||
<label>Hinten rechts</label>
|
||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="scannedReport.TreadDepthRR" /><span>mm</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
@if (!string.IsNullOrEmpty(scannedReport.Recommendation))
|
||
{
|
||
<div class="report-recommendation">💡 @scannedReport.Recommendation</div>
|
||
}
|
||
|
||
<a class="report-link" href="@scannedReport.ReportUrl" target="_blank" rel="noopener">Original-Report öffnen ↗</a>
|
||
|
||
<div class="report-warning info">
|
||
Die Messwerte werden dem Satz zugeordnet, der beim Reifenwechsel <strong>eingelagert</strong> wird
|
||
(@(swapUseHistory ? (swapSelectedHistory?.TicketNumber ?? "Satz aus Historie") : "neu erfasster Satz")).
|
||
</div>
|
||
</div>
|
||
|
||
<div class="report-footer">
|
||
<button type="button" class="btn btn-outline" @onclick="CloseReportModal">Verwerfen</button>
|
||
<button type="button" class="btn btn-primary" @onclick="ApplySwapReport">
|
||
<span>Für Einlagerung übernehmen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@* Action Chooser Modal *@
|
||
@if (actionCandidate != null)
|
||
{
|
||
<div class="success-overlay" @onclick="CloseActionChooser">
|
||
<div class="success-modal action-modal" @onclick:stopPropagation="true" role="dialog" aria-modal="true" aria-labelledby="action-title">
|
||
<h2 id="action-title">Aktion wählen</h2>
|
||
<p class="action-subject">
|
||
<strong>@actionCandidate.Customer?.FullName</strong>
|
||
@if (actionCandidate.Vehicle != null)
|
||
{
|
||
<span class="vehicle-info">@actionCandidate.Vehicle.Make @actionCandidate.Vehicle.Model</span>
|
||
}
|
||
<br />
|
||
<span class="mono">@actionCandidate.TicketNumber</span> · @SeasonLabel(actionCandidate.Season) @(actionCandidate.TireSize != null ? $"· {actionCandidate.TireSize}" : "")
|
||
</p>
|
||
<div class="action-buttons">
|
||
<button type="button" class="action-btn" @onclick="ChoosePickup">
|
||
<span class="action-icon" aria-hidden="true">📤</span>
|
||
<span class="action-label">Abholen</span>
|
||
<span class="action-desc">Kunde nimmt die Reifen mit</span>
|
||
</button>
|
||
<button type="button" class="action-btn swap" @onclick="ChooseSwap">
|
||
<span class="action-icon" aria-hidden="true">🔄</span>
|
||
<span class="action-label">Reifenwechsel</span>
|
||
<span class="action-desc">Satz montieren, anderen Satz einlagern</span>
|
||
</button>
|
||
</div>
|
||
<button type="button" class="btn btn-outline" @onclick="CloseActionChooser">Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@* Swap Success Modal *@
|
||
@if (swapResult != null)
|
||
{
|
||
<div class="success-overlay" @onclick="CloseSwapSuccess">
|
||
<div class="success-modal" @onclick:stopPropagation="true">
|
||
<div class="success-icon">✓</div>
|
||
<h2>Reifenwechsel erfolgreich!</h2>
|
||
<div class="pickup-list" role="list">
|
||
<div class="pickup-item" role="listitem">
|
||
<span class="ticket">⬆️ @swapResult.Outgoing.TicketNumber</span>
|
||
<span class="info">@SeasonLabel(swapResult.Outgoing.Season) montiert</span>
|
||
</div>
|
||
<div class="pickup-item" role="listitem">
|
||
<span class="ticket">⬇️ @swapResult.Incoming.TicketNumber</span>
|
||
<span class="info">@SeasonLabel(swapResult.Incoming.Season) eingelagert auf <strong>@swapResult.NewLocationCode</strong></span>
|
||
</div>
|
||
</div>
|
||
@if (swapResult.FreedLocationCode != null && swapResult.FreedLocationCode != swapResult.NewLocationCode)
|
||
{
|
||
<p class="success-detail">Lagerplatz <strong>@swapResult.FreedLocationCode</strong> ist wieder frei.</p>
|
||
}
|
||
@if (swapReportSaved)
|
||
{
|
||
<p class="success-detail">📋 Profiltiefen-Report wurde am eingelagerten Satz gespeichert.</p>
|
||
}
|
||
@if (!string.IsNullOrEmpty(swapReportError))
|
||
{
|
||
<p class="success-detail warn">⚠️ @swapReportError</p>
|
||
}
|
||
<button class="btn btn-primary" @onclick="CloseSwapSuccess">OK</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@* Success Modal *@
|
||
@if (showSuccess)
|
||
{
|
||
<div class="success-overlay" @onclick="CloseSuccess">
|
||
<div class="success-modal" @onclick:stopPropagation="true">
|
||
<div class="success-icon">✓</div>
|
||
<h2>Abholung erfolgreich!</h2>
|
||
|
||
@if (pickedUpTireStacks.Count == 1)
|
||
{
|
||
<p><strong>@pickedUpTireStacks[0].TicketNumber</strong> wurde abgeholt.</p>
|
||
<p class="pickup-detail">1 Stapel (@TireStack.TiresPerStack Reifen) abgeholt</p>
|
||
}
|
||
else
|
||
{
|
||
<p><strong>@pickedUpTireStacks.Count Reifenstapel</strong> wurden abgeholt.</p>
|
||
<p class="pickup-detail">@(pickedUpTireStacks.Count * TireStack.TiresPerStack) Reifen insgesamt</p>
|
||
|
||
<div class="pickup-list" role="list">
|
||
@foreach (var ts in pickedUpTireStacks)
|
||
{
|
||
<div class="pickup-item" role="listitem">
|
||
<span class="ticket">@ts.TicketNumber</span>
|
||
<span class="info">1 Stapel | @(ts.Customer?.FullName ?? ts.CatalogItem?.Name ?? "Lagerbestand")</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<p class="success-detail">
|
||
@if (freedLocations.Count == 1)
|
||
{
|
||
<span>Lagerplatz <strong>@freedLocations[0]</strong> ist wieder frei.</span>
|
||
}
|
||
else if (freedLocations.Count > 1)
|
||
{
|
||
<span><strong>@freedLocations.Count Lagerplätze</strong> sind wieder frei.</span>
|
||
}
|
||
</p>
|
||
|
||
<button class="btn btn-primary" @onclick="CloseSuccess">OK</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
</main>
|
||
}
|
||
|
||
@code {
|
||
private bool isProcessing;
|
||
private bool showSuccess;
|
||
private string searchTerm = "";
|
||
private string? errorMessage;
|
||
private string? successMessage;
|
||
|
||
private List<TireStack> searchResults = new();
|
||
private List<TireStack> scannedTireStacks = new();
|
||
private List<TireStack> pickedUpTireStacks = new();
|
||
private List<string> freedLocations = new();
|
||
private TireStack? lastAddedTireStack;
|
||
private CancellationTokenSource? _searchDebounceCts;
|
||
private readonly SemaphoreSlim _searchLock = new(1, 1);
|
||
|
||
// Action chooser / swap flow
|
||
private TireStack? actionCandidate;
|
||
private TireStack? swapOutgoing;
|
||
private List<TireStack> swapHistory = new();
|
||
private TireStack? swapSelectedHistory;
|
||
private bool swapUseHistory;
|
||
private TireStack swapNew = new();
|
||
private bool showLocationPicker;
|
||
private StorageLocation? swapTargetLocation;
|
||
private Guid? swapTargetLocationId => swapTargetLocation?.Id;
|
||
private SwapResult? swapResult;
|
||
|
||
// QR report for the incoming (stored) set
|
||
private bool showQrScanner;
|
||
private bool isLoadingReport;
|
||
private TireReportData? scannedReport;
|
||
private TireReportData? swapReport;
|
||
private bool swapReportSaved;
|
||
private string? swapReportError;
|
||
private const int NotesMaxLength = 1000;
|
||
|
||
private bool CanSwap => swapOutgoing != null && (swapUseHistory ? swapSelectedHistory != null : !string.IsNullOrWhiteSpace(swapNew.TireSize));
|
||
|
||
[SupplyParameterFromQuery(Name = "stackId")]
|
||
public Guid? PreselectedStackId { get; set; }
|
||
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
if (!PreselectedStackId.HasValue || !UserSession.IsWorkerOrAdmin) return;
|
||
|
||
try
|
||
{
|
||
var stack = await TireStackService.GetByIdAsync(PreselectedStackId.Value);
|
||
if (stack == null)
|
||
{
|
||
errorMessage = "Der gewählte Reifenstapel wurde nicht gefunden.";
|
||
return;
|
||
}
|
||
|
||
searchResults = new List<TireStack> { stack };
|
||
OnResultClicked(stack);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = $"Reifenstapel konnte nicht geladen werden: {ex.Message}";
|
||
}
|
||
}
|
||
|
||
private static string RatingClass(TireMeasureRating r) => r switch
|
||
{
|
||
TireMeasureRating.Ok => "ok",
|
||
TireMeasureRating.Warning => "warn",
|
||
TireMeasureRating.Critical => "crit",
|
||
_ => "none"
|
||
};
|
||
|
||
private static string RatingClass(TireReportData r, decimal? mm) => RatingClass(TireInspection.RateDepth(mm, r.CriticalThreshold, r.WarningThreshold));
|
||
|
||
private static string Fmt(decimal? mm) => mm.HasValue ? mm.Value.ToString("0.0", System.Globalization.CultureInfo.GetCultureInfo("de-DE")) + " mm" : "–";
|
||
|
||
private RenderFragment<TireReportData> ReportSummaryFragment => report => __builder =>
|
||
{
|
||
<div class="report-summary">
|
||
@if (!string.IsNullOrEmpty(report.LicensePlate)) { <span class="report-chip">🚗 @report.LicensePlate</span> }
|
||
@if (report.Mileage.HasValue) { <span class="report-chip">@report.Mileage.Value.ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("de-DE")) km</span> }
|
||
<span class="report-chip rating-@RatingClass(report, report.TreadDepthFL)">VL @Fmt(report.TreadDepthFL)</span>
|
||
<span class="report-chip rating-@RatingClass(report, report.TreadDepthFR)">VR @Fmt(report.TreadDepthFR)</span>
|
||
<span class="report-chip rating-@RatingClass(report, report.TreadDepthRL)">HL @Fmt(report.TreadDepthRL)</span>
|
||
<span class="report-chip rating-@RatingClass(report, report.TreadDepthRR)">HR @Fmt(report.TreadDepthRR)</span>
|
||
</div>
|
||
};
|
||
|
||
private void OpenQrScanner()
|
||
{
|
||
errorMessage = null;
|
||
showQrScanner = true;
|
||
}
|
||
|
||
private async Task OnQrScanned(string text)
|
||
{
|
||
showQrScanner = false;
|
||
|
||
if (swapOutgoing == null) return;
|
||
|
||
if (!TireReportService.TryParseReportUrl(text, out var recordSn, out _))
|
||
{
|
||
errorMessage = "Der QR-Code enthält keinen gültigen Reifen-Report-Link.";
|
||
return;
|
||
}
|
||
|
||
isLoadingReport = true;
|
||
errorMessage = null;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
scannedReport = await TireReportService.FetchAsync(recordSn);
|
||
}
|
||
catch (TireReportException ex)
|
||
{
|
||
errorMessage = ex.Message;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = $"Report konnte nicht verarbeitet werden: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
isLoadingReport = false;
|
||
}
|
||
}
|
||
|
||
private void CloseReportModal() => scannedReport = null;
|
||
|
||
private void ApplySwapReport()
|
||
{
|
||
if (scannedReport == null) return;
|
||
|
||
scannedReport.RecomputeRatings();
|
||
swapReport = scannedReport;
|
||
scannedReport = null;
|
||
|
||
// Prefill the manually captured set with the measured data
|
||
if (!swapUseHistory)
|
||
{
|
||
var de = System.Globalization.CultureInfo.GetCultureInfo("de-DE");
|
||
var lines = new List<string>();
|
||
if (!string.IsNullOrEmpty(swapReport.LicensePlate)) lines.Add($"Kennzeichen: {swapReport.LicensePlate}");
|
||
if (swapReport.Mileage.HasValue) lines.Add($"km-Stand: {swapReport.Mileage.Value.ToString("N0", de)}");
|
||
if (swapReport.HasTreadData)
|
||
lines.Add($"Profil VL {Fmt(swapReport.TreadDepthFL)} / VR {Fmt(swapReport.TreadDepthFR)} / HL {Fmt(swapReport.TreadDepthRL)} / HR {Fmt(swapReport.TreadDepthRR)}");
|
||
if (!string.IsNullOrEmpty(swapReport.Recommendation)) lines.Add(swapReport.Recommendation);
|
||
|
||
var block = string.Join("\n", lines);
|
||
if (!string.IsNullOrWhiteSpace(block))
|
||
{
|
||
var merged = string.IsNullOrWhiteSpace(swapNew.Notes) ? block : swapNew.Notes.TrimEnd() + "\n" + block;
|
||
swapNew.Notes = merged.Length <= NotesMaxLength ? merged : merged[..NotesMaxLength];
|
||
}
|
||
|
||
if (swapReport.HasTreadData)
|
||
{
|
||
var minDepth = new[] { swapReport.TreadDepthFL, swapReport.TreadDepthFR, swapReport.TreadDepthRL, swapReport.TreadDepthRR }
|
||
.Where(v => v.HasValue).Select(v => v!.Value).Min();
|
||
swapNew.Condition = minDepth < TireInspection.DefaultWarningThreshold ? TireCondition.Poor
|
||
: minDepth < 5m ? TireCondition.Fair
|
||
: TireCondition.Good;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void RemoveSwapReport() => swapReport = null;
|
||
|
||
private static string SeasonIcon(TireSeason s) => s == TireSeason.Sommer ? "☀️" : s == TireSeason.Winter ? "❄️" : "🔄";
|
||
private static string SeasonLabel(TireSeason s) => s == TireSeason.Sommer ? "☀️ Sommer" : s == TireSeason.Winter ? "❄️ Winter" : "🔄 Allrad";
|
||
|
||
private void OnResultClicked(TireStack ts)
|
||
{
|
||
if (ts.Status != TireStackStatus.Stored)
|
||
{
|
||
errorMessage = $"'{ts.TicketNumber}' ist nicht eingelagert";
|
||
return;
|
||
}
|
||
|
||
// Inventory stacks and already selected stacks toggle directly
|
||
if (ts.IsInventory || scannedTireStacks.Any(s => s.Id == ts.Id))
|
||
{
|
||
AddToScannedList(ts);
|
||
return;
|
||
}
|
||
|
||
actionCandidate = ts;
|
||
errorMessage = null;
|
||
}
|
||
|
||
private void CloseActionChooser() => actionCandidate = null;
|
||
|
||
private void ChoosePickup()
|
||
{
|
||
if (actionCandidate == null) return;
|
||
AddToScannedList(actionCandidate);
|
||
actionCandidate = null;
|
||
}
|
||
|
||
private async Task ChooseSwap()
|
||
{
|
||
if (actionCandidate == null) return;
|
||
var ts = actionCandidate;
|
||
actionCandidate = null;
|
||
await StartSwap(ts);
|
||
}
|
||
|
||
private async Task StartSwap(TireStack outgoing)
|
||
{
|
||
swapOutgoing = outgoing;
|
||
swapResult = null;
|
||
showLocationPicker = false;
|
||
swapTargetLocation = outgoing.StorageLocation;
|
||
swapSelectedHistory = null;
|
||
swapReport = null;
|
||
scannedReport = null;
|
||
showQrScanner = false;
|
||
swapReportSaved = false;
|
||
swapReportError = null;
|
||
errorMessage = null;
|
||
successMessage = null;
|
||
|
||
var oppositeSeason = outgoing.Season == TireSeason.Sommer ? TireSeason.Winter
|
||
: outgoing.Season == TireSeason.Winter ? TireSeason.Sommer
|
||
: TireSeason.Allrad;
|
||
|
||
swapNew = new TireStack
|
||
{
|
||
Season = oppositeSeason,
|
||
TireSize = outgoing.Vehicle?.TireSize ?? outgoing.TireSize,
|
||
WithRims = outgoing.WithRims,
|
||
Condition = TireCondition.Good,
|
||
CustomerId = outgoing.CustomerId,
|
||
VehicleId = outgoing.VehicleId
|
||
};
|
||
|
||
await _searchLock.WaitAsync();
|
||
try
|
||
{
|
||
var history = outgoing.CustomerId.HasValue
|
||
? await TireStackService.GetByCustomerAsync(outgoing.CustomerId.Value)
|
||
: new List<TireStack>();
|
||
|
||
swapHistory = history
|
||
.Where(h => h.Id != outgoing.Id && (h.Status == TireStackStatus.OnVehicle || h.Status == TireStackStatus.PickedUp))
|
||
.OrderBy(h => h.Season == outgoing.Season)
|
||
.ThenByDescending(h => h.PickedUpAt)
|
||
.ToList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
swapHistory = new();
|
||
errorMessage = $"Historie konnte nicht geladen werden: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
_searchLock.Release();
|
||
}
|
||
|
||
swapUseHistory = swapHistory.Count > 0;
|
||
swapSelectedHistory = swapHistory.FirstOrDefault(h => h.Season != outgoing.Season) ?? swapHistory.FirstOrDefault();
|
||
}
|
||
|
||
private void OnTargetLocationSelected(StorageLocation loc)
|
||
{
|
||
swapTargetLocation = loc;
|
||
}
|
||
|
||
private void ResetTargetLocation()
|
||
{
|
||
swapTargetLocation = swapOutgoing?.StorageLocation;
|
||
}
|
||
|
||
private void CancelSwap()
|
||
{
|
||
swapOutgoing = null;
|
||
swapHistory.Clear();
|
||
swapSelectedHistory = null;
|
||
showLocationPicker = false;
|
||
showQrScanner = false;
|
||
scannedReport = null;
|
||
swapReport = null;
|
||
swapTargetLocation = null;
|
||
}
|
||
|
||
private async Task ProcessSwap()
|
||
{
|
||
if (swapOutgoing == null || !CanSwap) return;
|
||
|
||
isProcessing = true;
|
||
errorMessage = null;
|
||
StateHasChanged();
|
||
|
||
await _searchLock.WaitAsync();
|
||
try
|
||
{
|
||
var incoming = swapUseHistory
|
||
? new TireStack
|
||
{
|
||
Id = swapSelectedHistory!.Id,
|
||
Condition = swapSelectedHistory.Condition,
|
||
Notes = swapSelectedHistory.Notes,
|
||
VehicleId = swapOutgoing.VehicleId
|
||
}
|
||
: swapNew;
|
||
|
||
var targetLocation = swapTargetLocationId == swapOutgoing.StorageLocationId ? null : swapTargetLocationId;
|
||
var report = swapReport;
|
||
|
||
var result = await TireStackService.SwapAsync(
|
||
swapOutgoing.Id,
|
||
incoming,
|
||
targetLocation,
|
||
UserSession.CurrentUser?.Id);
|
||
|
||
swapReportSaved = false;
|
||
swapReportError = null;
|
||
|
||
if (report != null)
|
||
{
|
||
try
|
||
{
|
||
await TireStackService.AddInspectionAsync(report.ToInspection(result.Incoming.Id, UserSession.CurrentUser?.Id));
|
||
swapReportSaved = true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
swapReportError = $"Reifenwechsel erfolgreich, aber der Report konnte nicht gespeichert werden: {ex.Message}";
|
||
}
|
||
}
|
||
|
||
swapResult = result;
|
||
CancelSwap();
|
||
searchResults.Clear();
|
||
searchTerm = "";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = $"Fehler beim Reifenwechsel: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
_searchLock.Release();
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
private void CloseSwapSuccess() => swapResult = null;
|
||
|
||
private async Task SearchTireStacks()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(searchTerm) || searchTerm.Length < 2)
|
||
{
|
||
searchResults.Clear();
|
||
return;
|
||
}
|
||
|
||
// Debounce: cancel previous pending search, wait briefly for typing to settle
|
||
_searchDebounceCts?.Cancel();
|
||
_searchDebounceCts?.Dispose();
|
||
var cts = new CancellationTokenSource();
|
||
_searchDebounceCts = cts;
|
||
|
||
try
|
||
{
|
||
await Task.Delay(300, cts.Token);
|
||
}
|
||
catch (TaskCanceledException)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Serialize DB access: scoped DbContext must not be used concurrently
|
||
await _searchLock.WaitAsync();
|
||
try
|
||
{
|
||
if (cts.IsCancellationRequested) return;
|
||
|
||
searchResults = await TireStackService.SearchAsync(searchTerm);
|
||
errorMessage = null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = $"Suchfehler: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
_searchLock.Release();
|
||
}
|
||
}
|
||
|
||
private void AddToScannedList(TireStack tireStack)
|
||
{
|
||
if (tireStack.Status != TireStackStatus.Stored)
|
||
{
|
||
errorMessage = $"'{tireStack.TicketNumber}' ist nicht eingelagert";
|
||
return;
|
||
}
|
||
|
||
if (scannedTireStacks.Any(ts => ts.Id == tireStack.Id))
|
||
{
|
||
// Remove if already in list (toggle behavior)
|
||
scannedTireStacks.RemoveAll(ts => ts.Id == tireStack.Id);
|
||
if (lastAddedTireStack?.Id == tireStack.Id)
|
||
{
|
||
lastAddedTireStack = null;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
scannedTireStacks.Add(tireStack);
|
||
lastAddedTireStack = tireStack;
|
||
}
|
||
|
||
errorMessage = null;
|
||
}
|
||
|
||
private void RemoveScannedItem(TireStack tireStack)
|
||
{
|
||
scannedTireStacks.Remove(tireStack);
|
||
if (lastAddedTireStack?.Id == tireStack.Id)
|
||
{
|
||
lastAddedTireStack = null;
|
||
}
|
||
}
|
||
|
||
private void ClearAllScanned()
|
||
{
|
||
scannedTireStacks.Clear();
|
||
lastAddedTireStack = null;
|
||
errorMessage = null;
|
||
successMessage = null;
|
||
}
|
||
|
||
private async Task ProcessBatchPickup()
|
||
{
|
||
if (scannedTireStacks.Count == 0) return;
|
||
|
||
isProcessing = true;
|
||
errorMessage = null;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
pickedUpTireStacks.Clear();
|
||
freedLocations.Clear();
|
||
|
||
foreach (var tireStack in scannedTireStacks)
|
||
{
|
||
var locationCode = tireStack.StorageLocation?.Code;
|
||
if (!string.IsNullOrEmpty(locationCode) && !freedLocations.Contains(locationCode))
|
||
{
|
||
freedLocations.Add(locationCode);
|
||
}
|
||
}
|
||
|
||
var pickedUp = await TireStackService.PickupBatchAsync(
|
||
scannedTireStacks.Select(ts => ts.Id),
|
||
UserSession.CurrentUser?.Id);
|
||
|
||
pickedUpTireStacks.AddRange(pickedUp);
|
||
|
||
showSuccess = true;
|
||
scannedTireStacks.Clear();
|
||
searchResults.Clear();
|
||
searchTerm = "";
|
||
lastAddedTireStack = null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = $"Fehler bei Abholung: {ex.Message}";
|
||
pickedUpTireStacks.Clear();
|
||
freedLocations.Clear();
|
||
}
|
||
finally
|
||
{
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
private void CloseSuccess()
|
||
{
|
||
showSuccess = false;
|
||
pickedUpTireStacks.Clear();
|
||
freedLocations.Clear();
|
||
}
|
||
|
||
public ValueTask DisposeAsync()
|
||
{
|
||
_searchDebounceCts?.Cancel();
|
||
_searchDebounceCts?.Dispose();
|
||
_searchLock.Dispose();
|
||
return ValueTask.CompletedTask;
|
||
}
|
||
}
|