Reifenwechsel: QR-Report-Integration fuer eingelagerten Satz
This commit is contained in:
1 parent
3404fb9875
commit
766703524a
2 files changed
+540
-1
No files matched your search
@@ -4,6 +4,7 @@
|
||||
@inject ICustomerService CustomerService
|
||||
@inject ITireStackService TireStackService
|
||||
@inject IStorageLocationService StorageLocationService
|
||||
@inject ITireReportService TireReportService
|
||||
@inject UserSessionService UserSession
|
||||
|
||||
<PageTitle>Reifenstapel Abholen - Reifen Späth Lagerverwaltung</PageTitle>
|
||||
@@ -210,6 +211,39 @@ else
|
||||
<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)">
|
||||
@@ -402,6 +436,91 @@ else
|
||||
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 *@
|
||||
@@ -457,6 +576,14 @@ else
|
||||
{
|
||||
<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>
|
||||
@@ -536,6 +663,15 @@ else
|
||||
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")]
|
||||
@@ -563,6 +699,111 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
@@ -609,6 +850,11 @@ else
|
||||
showLocationPicker = false;
|
||||
swapTargetLocation = outgoing.StorageLocation;
|
||||
swapSelectedHistory = null;
|
||||
swapReport = null;
|
||||
scannedReport = null;
|
||||
showQrScanner = false;
|
||||
swapReportSaved = false;
|
||||
swapReportError = null;
|
||||
errorMessage = null;
|
||||
successMessage = null;
|
||||
|
||||
@@ -669,6 +915,9 @@ else
|
||||
swapHistory.Clear();
|
||||
swapSelectedHistory = null;
|
||||
showLocationPicker = false;
|
||||
showQrScanner = false;
|
||||
scannedReport = null;
|
||||
swapReport = null;
|
||||
swapTargetLocation = null;
|
||||
}
|
||||
|
||||
@@ -694,13 +943,31 @@ else
|
||||
: swapNew;
|
||||
|
||||
var targetLocation = swapTargetLocationId == swapOutgoing.StorageLocationId ? null : swapTargetLocationId;
|
||||
var report = swapReport;
|
||||
|
||||
swapResult = await TireStackService.SwapAsync(
|
||||
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 = "";
|
||||
|
||||
Reference in new issue
Block a user