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

+566 -39
View File
@@ -5,6 +5,7 @@
@inject IStorageLocationService StorageLocationService
@inject ILocationCodeService LocationCodeService
@inject ITireStackService TireStackService
@inject ITireReportService TireReportService
@inject ICatalogService CatalogService
@inject IFileStorageService FileStorageService
@inject UserSessionService UserSession
@@ -474,6 +475,42 @@ else
</div>
}
@if (!isInventoryMode)
{
@* QR report scan *@
<div class="report-scan-bar">
@if (pendingReport == 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-outline" @onclick="OpenQrScanner" disabled="@isLoadingReport">
@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 dem neuen Stapel gespeichert</strong>
<button type="button" class="btn-link" @onclick="RemovePendingReport">Entfernen</button>
</div>
@ReportSummaryFragment(pendingReport)
</div>
}
</div>
}
@* Tire/Felgen Form *@
<div class="tire-form">
@if (!isFelgenCategory)
@@ -506,6 +543,10 @@ else
else
{
<TireSizeInput Value="@tireStack.TireSize" ValueChanged="OnTireSizeChanged" />
@if (tireSizeFromVehicle && !string.IsNullOrEmpty(tireStack.TireSize))
{
<span class="field-hint">🚗 Vom Fahrzeug übernommen</span>
}
}
</div>
@@ -657,7 +698,7 @@ else
@* Step 3: Storage Location *@
@if (currentStep == 3)
{
<div class="checkin-card">
<div class="checkin-card wide">
<div class="selected-info-bar">
@if (isInventoryMode)
{
@@ -676,32 +717,104 @@ else
<h2>Lagerplatz auswählen</h2>
<div class="location-selector">
@* Area Filter *@
<div class="area-filter">
<button class="filter-btn @(selectedAreaFilter == null ? "active" : "")"
@onclick="() => FilterLocationsByArea(null)">
Alle
</button>
<button class="filter-btn lager @(selectedAreaFilter == AreaType.Lager ? "active" : "")"
@onclick="() => FilterLocationsByArea(AreaType.Lager)">
🏭 Lager
</button>
<button class="filter-btn werkstatt @(selectedAreaFilter == AreaType.Werkstatt ? "active" : "")"
@onclick="() => FilterLocationsByArea(AreaType.Werkstatt)">
🔧 Werkstatt
</button>
</div>
@* Row Filter *@
<div class="row-filter">
<select class="form-control" @bind="selectedRowFilter" @bind:after="ApplyLocationFilter">
<option value="">Alle Reihen</option>
@for (int r = StorageLocation.MinRow; r <= StorageLocation.MaxRowLager; r++)
@if (selectedLocation != null)
{
<div class="checkin-summary">
<h3 class="summary-title">📋 Zusammenfassung</h3>
<div class="summary-grid">
@if (isInventoryMode)
{
<option value="@r">Reihe @r</option>
<div class="summary-item">
<span class="summary-label">Kategorie</span>
<span class="summary-value">📦 @selectedCategory?.Name</span>
</div>
}
</select>
else
{
<div class="summary-item">
<span class="summary-label">Kunde</span>
<span class="summary-value">👤 @selectedCustomer?.FullName</span>
</div>
@if (selectedVehicle != null)
{
<div class="summary-item">
<span class="summary-label">Fahrzeug</span>
<span class="summary-value">🚗 @selectedVehicle.DisplayName</span>
</div>
}
}
<div class="summary-item">
<span class="summary-label">@(isFelgenCategory ? "Felgen" : "Reifen")</span>
<span class="summary-value">
@(isInventoryMode && useCustomItemCount ? tireStack.ItemCount : TireStack.TiresPerStack) Stück
@if (!isFelgenCategory)
{
<text> @(tireStack.Season == TireSeason.Sommer ? "☀️" : tireStack.Season == TireSeason.Winter ? "❄️" : "🔄") @tireStack.Season</text>
}
</span>
</div>
@if (!string.IsNullOrEmpty(tireStack.TireSize))
{
<div class="summary-item">
<span class="summary-label">Größe</span>
<span class="summary-value">@tireStack.TireSize @(tireStack.WithRims && !isFelgenCategory ? "| mit Felgen" : "")</span>
</div>
}
@if (!string.IsNullOrEmpty(tireStack.TireBrand) || !string.IsNullOrEmpty(tireStack.TireModel))
{
<div class="summary-item">
<span class="summary-label">Marke</span>
<span class="summary-value">@tireStack.TireBrand @tireStack.TireModel</span>
</div>
}
<div class="summary-item highlight">
<span class="summary-label">Lagerplatz</span>
<span class="summary-value location">
<LocationDisplay Code="@selectedLocation.Code" DisplayMode="LocationDisplayMode.Full" ShowIcon="true" Size="LocationDisplaySize.Small" />
</span>
</div>
</div>
<button class="btn-clear summary-clear" @onclick="ClearSelectedLocation" title="Lagerplatz-Auswahl aufheben">✕</button>
</div>
}
@if (locationSelectorCollapsed && selectedLocation != null)
{
<button type="button" class="location-collapsed-bar" @onclick="ExpandLocationSelector" title="Lagerplatz ändern">
<span class="collapsed-icon">📍</span>
<span class="collapsed-code">@selectedLocation.Code</span>
<span class="collapsed-hint">Lagerplatz ändern ▾</span>
</button>
}
else
{
<div class="location-selector compact">
@* Area & Row Filter combined *@
<div class="location-filter-row">
<div class="area-filter">
<button class="filter-btn @(selectedAreaFilter == null ? "active" : "")"
@onclick="() => FilterLocationsByArea(null)">
Alle
</button>
<button class="filter-btn lager @(selectedAreaFilter == AreaType.Lager ? "active" : "")"
@onclick="() => FilterLocationsByArea(AreaType.Lager)">
🏭 Lager
</button>
<button class="filter-btn werkstatt @(selectedAreaFilter == AreaType.Werkstatt ? "active" : "")"
@onclick="() => FilterLocationsByArea(AreaType.Werkstatt)">
🔧 Werkstatt
</button>
</div>
<div class="row-filter">
<select class="form-control" @bind="selectedRowFilter" @bind:after="ApplyLocationFilter">
<option value="">Alle Reihen</option>
@for (int r = StorageLocation.MinRow; r <= StorageLocation.MaxRowLager; r++)
{
<option value="@r">Reihe @r</option>
}
</select>
</div>
</div>
@* Location Grid *@
@@ -720,7 +833,7 @@ else
<h3 class="area-title">
@(areaGroup.Key == AreaType.Lager ? "🏭" : "🔧") @areaGroup.Key.GetDisplayName()
</h3>
@foreach (var rowGroup in areaGroup.GroupBy(l => l.Row).OrderBy(g => g.Key))
{
<div class="row-section">
@@ -742,16 +855,6 @@ else
}
}
</div>
@if (selectedLocation != null)
{
<div class="selected-location">
<div class="location-icon">📍</div>
<div class="location-info">
<LocationDisplay Code="@selectedLocation.Code" DisplayMode="LocationDisplayMode.Full" ShowIcon="false" />
</div>
<button class="btn-clear" @onclick="ClearSelectedLocation">✕</button>
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
@@ -873,6 +976,159 @@ else
</div>
</div>
}
<QrScannerModal @bind-IsOpen="showQrScanner"
Title="Reifen-Report scannen"
Subtitle="@(selectedCustomer != null ? $"Kunde: {selectedCustomer.FullName}" : null)"
OnScanned="OnQrScanned" />
@* Report preview / assignment modal *@
@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-choice">
<h4>Wie sollen die Daten verwendet werden?</h4>
<div class="report-choice-buttons">
<button type="button" class="choice-btn @(reportMode == ReportAssignMode.Existing ? "active" : "")"
@onclick="() => SetReportMode(ReportAssignMode.Existing)"
disabled="@(customerStacksForReport.Count == 0)">
<span class="choice-icon">📦</span>
<span class="choice-title">Zu vorhandenem Satz</span>
<span class="choice-sub">@(customerStacksForReport.Count == 0 ? "Keine Sätze vorhanden" : $"{customerStacksForReport.Count} Satz/Sätze des Kunden")</span>
</button>
<button type="button" class="choice-btn @(reportMode == ReportAssignMode.New ? "active" : "")"
@onclick="() => SetReportMode(ReportAssignMode.New)">
<span class="choice-icon">✨</span>
<span class="choice-title">Neuen Satz anlegen</span>
<span class="choice-sub">Daten ins Formular übernehmen</span>
</button>
</div>
@if (reportMode == ReportAssignMode.Existing)
{
<div class="stack-pick-list" role="list">
@foreach (var s in customerStacksForReport)
{
<button type="button" class="stack-pick-item @(reportTargetStackId == s.Id ? "selected" : "")" role="listitem"
@onclick="() => reportTargetStackId = s.Id">
<span class="stack-pick-main">
<span class="mono">@s.TicketNumber</span>
<span>@(s.Season == TireSeason.Sommer ? "☀️" : s.Season == TireSeason.Winter ? "❄️" : "🔄") @s.Season</span>
@if (!string.IsNullOrEmpty(s.TireSize)) { <span>@s.TireSize</span> }
</span>
<span class="stack-pick-sub">
@s.StatusText
@if (s.Vehicle != null) { <text> · 🚗 @s.Vehicle.ShortName</text> }
@if (s.StorageLocation != null) { <text> · @s.StorageLocation.Code</text> }
</span>
</button>
}
</div>
}
</div>
@if (!string.IsNullOrEmpty(reportError))
{
<div class="report-warning">⚠️ @reportError</div>
}
</div>
<div class="report-footer">
<button type="button" class="btn btn-outline" @onclick="CloseReportModal">Abbrechen</button>
@if (reportMode == ReportAssignMode.Existing)
{
<button type="button" class="btn btn-primary" @onclick="AttachReportToExisting" disabled="@(reportTargetStackId == null || isProcessing)">
@if (isProcessing) { <span class="spinner"></span> }
<span>Zum Satz hinzufügen</span>
</button>
}
else
{
<button type="button" class="btn btn-primary" @onclick="ApplyReportToNew">
<span>Für neuen Satz übernehmen</span>
</button>
}
</div>
</div>
</div>
}
@* Report attached to existing stack – success *@
@if (reportAttachedTo != null)
{
<div class="report-overlay" @onclick="() => reportAttachedTo = null">
<div class="report-dialog report-success" role="dialog" aria-modal="true" @onclick:stopPropagation="true">
<div class="success-icon">✓</div>
<h3>Report gespeichert</h3>
<p>Die Messwerte wurden dem Satz <span class="mono">@reportAttachedTo.TicketNumber</span> hinzugefügt.</p>
<div class="report-footer center">
<button type="button" class="btn btn-outline" @onclick="() => reportAttachedTo = null">Weiter erfassen</button>
<button type="button" class="btn btn-primary" @onclick="ResetForm">Fertig</button>
</div>
</div>
</div>
}
}
@code {
@@ -923,6 +1179,179 @@ else
private string? zoomImageUrl;
private string? zoomImageTitle;
// QR report
private enum ReportAssignMode { Existing, New }
private bool showQrScanner;
private bool isLoadingReport;
private TireReportData? scannedReport;
private TireReportData? pendingReport;
private ReportAssignMode reportMode = ReportAssignMode.New;
private List<TireStack> customerStacksForReport = new();
private Guid? reportTargetStackId;
private string? reportError;
private TireStack? reportAttachedTo;
private const int NotesMaxLength = 1000;
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 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 static string Fmt(decimal? mm) => mm.HasValue ? mm.Value.ToString("0.0", System.Globalization.CultureInfo.GetCultureInfo("de-DE")) + " mm" : "–";
private void OpenQrScanner()
{
reportError = null;
showQrScanner = true;
}
private async Task OnQrScanned(string text)
{
showQrScanner = false;
if (selectedCustomer == null)
{
errorMessage = "Bitte zuerst einen Kunden auswählen.";
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
{
var report = await TireReportService.FetchAsync(recordSn);
var stacks = await TireStackService.GetByCustomerAsync(selectedCustomer.Id);
customerStacksForReport = stacks
.Where(s => s.IsActive && s.Status != TireStackStatus.PickedUp)
.OrderByDescending(s => selectedVehicle != null && s.VehicleId == selectedVehicle.Id)
.ThenByDescending(s => s.Status == TireStackStatus.Stored)
.ThenByDescending(s => s.StoredAt)
.ToList();
reportMode = customerStacksForReport.Count > 0 ? ReportAssignMode.Existing : ReportAssignMode.New;
reportTargetStackId = customerStacksForReport.FirstOrDefault()?.Id;
reportError = null;
scannedReport = report;
}
catch (TireReportException ex)
{
errorMessage = ex.Message;
}
catch (Exception ex)
{
errorMessage = $"Report konnte nicht verarbeitet werden: {ex.Message}";
}
finally
{
isLoadingReport = false;
}
}
private void SetReportMode(ReportAssignMode mode)
{
reportMode = mode;
reportError = null;
}
private void CloseReportModal()
{
scannedReport = null;
reportError = null;
}
private async Task AttachReportToExisting()
{
if (scannedReport == null || reportTargetStackId == null) return;
isProcessing = true;
reportError = null;
StateHasChanged();
try
{
scannedReport.RecomputeRatings();
await TireStackService.AddInspectionAsync(scannedReport.ToInspection(reportTargetStackId.Value, UserSession.CurrentUser?.Id));
reportAttachedTo = customerStacksForReport.FirstOrDefault(s => s.Id == reportTargetStackId.Value);
scannedReport = null;
}
catch (Exception ex)
{
reportError = $"Speichern fehlgeschlagen: {ex.Message}";
}
finally
{
isProcessing = false;
}
}
private void ApplyReportToNew()
{
if (scannedReport == null) return;
scannedReport.RecomputeRatings();
pendingReport = scannedReport;
scannedReport = null;
var de = System.Globalization.CultureInfo.GetCultureInfo("de-DE");
var lines = new List<string>();
if (!string.IsNullOrEmpty(pendingReport.LicensePlate)) lines.Add($"Kennzeichen: {pendingReport.LicensePlate}");
if (pendingReport.Mileage.HasValue) lines.Add($"km-Stand: {pendingReport.Mileage.Value.ToString("N0", de)}");
if (pendingReport.HasTreadData)
lines.Add($"Profil VL {Fmt(pendingReport.TreadDepthFL)} / VR {Fmt(pendingReport.TreadDepthFR)} / HL {Fmt(pendingReport.TreadDepthRL)} / HR {Fmt(pendingReport.TreadDepthRR)}");
if (!string.IsNullOrEmpty(pendingReport.Recommendation)) lines.Add(pendingReport.Recommendation);
var block = string.Join("\n", lines);
if (!string.IsNullOrWhiteSpace(block))
{
var merged = string.IsNullOrWhiteSpace(tireStack.Notes) ? block : tireStack.Notes.TrimEnd() + "\n" + block;
tireStack.Notes = merged.Length <= NotesMaxLength ? merged : merged[..NotesMaxLength];
showDetailsExpanded = true;
}
if (pendingReport.HasTreadData)
{
var minDepth = new[] { pendingReport.TreadDepthFL, pendingReport.TreadDepthFR, pendingReport.TreadDepthRL, pendingReport.TreadDepthRR }
.Where(v => v.HasValue).Select(v => v!.Value).Min();
tireStack.Condition = minDepth < TireInspection.DefaultWarningThreshold ? TireCondition.Poor
: minDepth < 5m ? TireCondition.Fair
: TireCondition.Good;
}
_ = SaveDraftAsync();
}
private void RemovePendingReport()
{
pendingReport = null;
_ = SaveDraftAsync();
}
// Computed helpers
private bool isFelgenCategory => isInventoryMode && selectedCategory?.Type == CatalogItemType.Felgen;
@@ -954,8 +1383,16 @@ else
// Cleared on successful CompleteCheckin and on ResetForm.
// ------------------------------------------------------------------
private const string DraftKey = "tire-checkin";
private static readonly System.Text.Json.JsonSerializerOptions DraftJsonOptions = new()
{
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
private bool _draftRestoreAttempted;
private bool _isRestoringDraft;
[SupplyParameterFromQuery(Name = "customerId")]
public Guid? PreselectedCustomerId { get; set; }
private CancellationTokenSource? _draftDebounceCts;
private CancellationTokenSource? _searchDebounceCts;
private readonly SemaphoreSlim _searchLock = new(1, 1);
@@ -965,10 +1402,37 @@ else
if (firstRender && UserSession.IsWorkerOrAdmin && !_draftRestoreAttempted)
{
_draftRestoreAttempted = true;
if (PreselectedCustomerId.HasValue)
{
await PreselectCustomerAsync(PreselectedCustomerId.Value);
return;
}
await TryRestoreDraftAsync();
}
}
private async Task PreselectCustomerAsync(Guid customerId)
{
try
{
var customer = await CustomerService.GetByIdAsync(customerId);
if (customer == null) return;
try { await JS.InvokeVoidAsync("appInterop.clearDraft", DraftKey); } catch { }
isInventoryMode = false;
await SelectCustomer(customer);
GoToStep2();
StateHasChanged();
}
catch (Exception ex)
{
errorMessage = $"Kunde konnte nicht geladen werden: {ex.Message}";
}
}
private async Task TryRestoreDraftAsync()
{
try
@@ -976,7 +1440,7 @@ else
var json = await JS.InvokeAsync<string?>("appInterop.loadDraft", DraftKey);
if (string.IsNullOrWhiteSpace(json)) return;
var draft = System.Text.Json.JsonSerializer.Deserialize<CheckinDraft>(json);
var draft = System.Text.Json.JsonSerializer.Deserialize<CheckinDraft>(json, DraftJsonOptions);
if (draft == null) return;
_isRestoringDraft = true;
@@ -1012,6 +1476,8 @@ else
tireStack = draft.TireStack;
}
pendingReport = draft.PendingReport;
if (draft.SelectedLocationId.HasValue)
{
selectedLocation = allLocations.FirstOrDefault(l => l.Id == draft.SelectedLocationId.Value);
@@ -1083,10 +1549,11 @@ else
SelectedLocationId = selectedLocation?.Id,
UseCustomItemCount = useCustomItemCount,
ShowDetailsExpanded = showDetailsExpanded,
TireStack = tireStack
TireStack = tireStack,
PendingReport = pendingReport
};
var json = System.Text.Json.JsonSerializer.Serialize(draft);
var json = System.Text.Json.JsonSerializer.Serialize(draft, DraftJsonOptions);
await JS.InvokeVoidAsync("appInterop.saveDraft", DraftKey, json);
await JS.InvokeVoidAsync("appInterop.setUnsaved", HasMeaningfulDraft());
}
@@ -1127,6 +1594,7 @@ else
public bool UseCustomItemCount { get; set; }
public bool ShowDetailsExpanded { get; set; }
public TireStack? TireStack { get; set; }
public TireReportData? PendingReport { get; set; }
}
private async Task LoadLocations()
@@ -1251,6 +1719,8 @@ else
selectedVehicle = null;
}
ApplyVehicleTireSize();
_ = SaveDraftAsync();
}
@@ -1258,9 +1728,26 @@ else
{
selectedVehicle = vehicle;
showAddVehicleForm = false;
ApplyVehicleTireSize();
_ = SaveDraftAsync();
}
/// <summary>
/// Übernimmt die Reifengröße des gewählten Fahrzeugs in den Reifenstapel,
/// sofern noch keine (abweichende) Größe manuell erfasst wurde.
/// </summary>
private void ApplyVehicleTireSize()
{
if (isInventoryMode) return;
if (string.IsNullOrWhiteSpace(selectedVehicle?.TireSize)) return;
if (string.IsNullOrWhiteSpace(tireStack.TireSize) || tireSizeFromVehicle)
{
OnTireSizeChanged(selectedVehicle.TireSize);
tireSizeFromVehicle = true;
}
}
private void ShowNewVehicleForm()
{
showAddVehicleForm = true;
@@ -1285,6 +1772,7 @@ else
showAddVehicleForm = false;
newVehicle = new Vehicle();
errorMessage = null;
ApplyVehicleTireSize();
}
catch (Exception ex)
{
@@ -1344,6 +1832,7 @@ else
newCustomer = new Customer();
newVehicle = new Vehicle();
errorMessage = null;
ApplyVehicleTireSize();
}
catch (Exception ex)
{
@@ -1441,6 +1930,7 @@ else
private async Task GoToStep3()
{
await LoadLocations();
locationSelectorCollapsed = selectedLocation != null;
currentStep = 3;
_ = SaveDraftAsync();
}
@@ -1451,18 +1941,27 @@ else
_ = SaveDraftAsync();
}
private bool locationSelectorCollapsed;
private void SelectLocation(StorageLocation location)
{
if (!location.IsOccupied && location.IsAvailable)
{
selectedLocation = location;
locationSelectorCollapsed = true;
_ = SaveDraftAsync();
}
}
private void ExpandLocationSelector()
{
locationSelectorCollapsed = false;
}
private void ClearSelectedLocation()
{
selectedLocation = null;
locationSelectorCollapsed = false;
}
private async Task CompleteCheckin()
@@ -1501,6 +2000,19 @@ else
createdTireStack = created;
if (pendingReport != null)
{
try
{
await TireStackService.AddInspectionAsync(pendingReport.ToInspection(created.Id, UserSession.CurrentUser?.Id));
}
catch (Exception ex)
{
errorMessage = $"Stapel eingelagert, Report konnte aber nicht gespeichert werden: {ex.Message}";
}
pendingReport = null;
}
currentStep = 4;
await ClearDraftAsync();
}
@@ -1533,9 +2045,11 @@ else
showAddVehicleForm = false;
tireStack = new TireStack();
showDetailsExpanded = false;
tireSizeFromVehicle = false;
selectedLocation = null;
selectedAreaFilter = null;
selectedRowFilter = null;
locationSelectorCollapsed = false;
createdTireStack = null;
errorMessage = null;
useCustomItemCount = false;
@@ -1544,6 +2058,13 @@ else
showImageZoom = false;
zoomImageUrl = null;
zoomImageTitle = null;
showQrScanner = false;
scannedReport = null;
pendingReport = null;
reportAttachedTo = null;
reportError = null;
customerStacksForReport.Clear();
reportTargetStackId = null;
_ = ClearDraftAsync();
}
@@ -1563,8 +2084,14 @@ else
tireStack.RimSize = isAlu ? "Alu" : "Stahl";
}
private bool tireSizeFromVehicle;
private void OnTireSizeChanged(string? value)
{
if (tireStack.TireSize != value && value != selectedVehicle?.TireSize)
{
tireSizeFromVehicle = false;
}
tireStack.TireSize = value;
// Auto-derive rim size from the R value of the tire size (e.g. "225/45 R17" -> "17 Zoll")