TireInspectionPanel: aufklappbare, editierbare Profiltiefen-/QR-Report-Ansicht in allen Reifenstapel-Ansichten
This commit is contained in:
1 parent
766703524a
commit
7a0406eaa2
9 files changed
+825
-58
No files matched your search
@@ -175,6 +175,8 @@ else
|
||||
</span>
|
||||
<span class="info">@ts.TireSize (@TireStack.TiresPerStack Reifen)</span>
|
||||
<span class="location"><span aria-hidden="true">📍</span> @(ts.StorageLocation?.Code ?? "-")</span>
|
||||
<TireInspectionPanel TireStackId="@ts.Id" AllowEdit="@UserSession.IsWorkerOrAdmin" Compact="true"
|
||||
ScannerSubtitle="@($"Kunde: {customer.FullName}")" />
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -512,6 +512,7 @@
|
||||
.mini-tireset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
@@ -521,6 +522,11 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.mini-tireset ::deep .insp-panel {
|
||||
flex-basis: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.mini-tireset {
|
||||
gap: 10px;
|
||||
|
||||
@@ -590,6 +590,8 @@ else
|
||||
<div class="tirestack-info">
|
||||
<span class="vehicle">@(ts.TireStack.Vehicle?.ShortName ?? "Ohne KFZ")</span>
|
||||
<span class="details">@(ts.TireStack.TireSize ?? "?") | @(ts.TireStack.StorageLocation?.Code ?? "-")</span>
|
||||
<TireInspectionPanel TireStackId="@ts.TireStack.Id" AllowEdit="true" Compact="true"
|
||||
ScannerSubtitle="@ts.TireStack.TicketNumber" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -949,6 +949,12 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (createdTireStack != null && !isInventoryMode)
|
||||
{
|
||||
<TireInspectionPanel TireStackId="@createdTireStack.Id" AllowEdit="true" InitiallyOpen="@(pendingReportSaved)"
|
||||
ScannerSubtitle="@(selectedCustomer != null ? $"Kunde: {selectedCustomer.FullName}" : null)" />
|
||||
}
|
||||
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-outline" @onclick="ResetForm">
|
||||
@(isInventoryMode ? "Weiteren Stapel einlagern" : "Neuen Reifenstapel einlagern")
|
||||
@@ -1185,6 +1191,7 @@ else
|
||||
private bool isLoadingReport;
|
||||
private TireReportData? scannedReport;
|
||||
private TireReportData? pendingReport;
|
||||
private bool pendingReportSaved;
|
||||
private ReportAssignMode reportMode = ReportAssignMode.New;
|
||||
private List<TireStack> customerStacksForReport = new();
|
||||
private Guid? reportTargetStackId;
|
||||
@@ -1999,12 +2006,14 @@ else
|
||||
UserSession.CurrentUser?.Id);
|
||||
|
||||
createdTireStack = created;
|
||||
pendingReportSaved = false;
|
||||
|
||||
if (pendingReport != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await TireStackService.AddInspectionAsync(pendingReport.ToInspection(created.Id, UserSession.CurrentUser?.Id));
|
||||
pendingReportSaved = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -105,7 +105,7 @@ else
|
||||
</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)
|
||||
@@ -152,6 +152,8 @@ else
|
||||
<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>
|
||||
}
|
||||
@@ -205,6 +207,8 @@ else
|
||||
}
|
||||
</dd></div>
|
||||
</dl>
|
||||
<TireInspectionPanel TireStackId="@swapOutgoing.Id" AllowEdit="true" Compact="true"
|
||||
ScannerSubtitle="@(swapOutgoing.Customer != null ? $"Kunde: {swapOutgoing.Customer.FullName}" : swapOutgoing.TicketNumber)" />
|
||||
</div>
|
||||
|
||||
@* Incoming *@
|
||||
|
||||
@@ -399,6 +399,8 @@ else
|
||||
<textarea id="edit-notes" class="form-control" rows="3" @bind="editStack.Notes"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<TireInspectionPanel TireStackId="@editStack.Id" AllowEdit="true" Compact="true"
|
||||
ScannerSubtitle="@editStack.TicketNumber" />
|
||||
@if (!string.IsNullOrEmpty(editError))
|
||||
{
|
||||
<p class="edit-error" role="alert">@editError</p>
|
||||
@@ -536,32 +538,11 @@ else
|
||||
<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>
|
||||
<TireInspectionPanel TireStackId="@stack.Id" AllowEdit="@UserSession.IsWorkerOrAdmin" Compact="true"
|
||||
ScannerSubtitle="@(stack.Customer != null ? $"Kunde: {stack.Customer.FullName}" : stack.TicketNumber)" />
|
||||
};
|
||||
|
||||
private List<TireStack> allTireStacks = new();
|
||||
@@ -575,30 +556,6 @@ else
|
||||
private bool? typeFilter; // null = all, false = customer, true = inventory
|
||||
private string selectedCategoryId = "";
|
||||
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;
|
||||
@@ -744,13 +701,9 @@ else
|
||||
filteredTireStacks = query.OrderBy(ts => ts.StorageLocation?.Code).ToList();
|
||||
}
|
||||
|
||||
private async Task SelectStack(TireStack ts)
|
||||
private void SelectStack(TireStack ts)
|
||||
{
|
||||
selectedStack = selectedStack?.Id == ts.Id ? null : ts;
|
||||
if (selectedStack != null)
|
||||
{
|
||||
await LoadLatestInspection(selectedStack);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenEditModal()
|
||||
@@ -831,14 +784,10 @@ else
|
||||
return locationLookup.TryGetValue((area, row, pos, slot), out var loc) ? loc : null;
|
||||
}
|
||||
|
||||
private async Task SelectLocation(StorageLocation? location)
|
||||
private void 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)
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
@inject ITireStackService TireStackService
|
||||
@inject ITireReportService TireReportService
|
||||
@inject UserSessionService UserSession
|
||||
|
||||
<section class="insp-panel @(Compact ? "compact" : "")" aria-label="Profiltiefen-Messungen">
|
||||
<button type="button" class="insp-toggle" @onclick="Toggle" aria-expanded="@isOpen" aria-controls="@panelId">
|
||||
<span class="insp-toggle-icon" aria-hidden="true">📋</span>
|
||||
<span class="insp-toggle-title">Profiltiefen / Reifen-Report</span>
|
||||
@if (latest != null)
|
||||
{
|
||||
<span class="insp-toggle-summary">
|
||||
<span class="insp-chip rating-@RatingClass(latest.RatingFL)">VL @Fmt(latest.TreadDepthFL)</span>
|
||||
<span class="insp-chip rating-@RatingClass(latest.RatingFR)">VR @Fmt(latest.TreadDepthFR)</span>
|
||||
<span class="insp-chip rating-@RatingClass(latest.RatingRL)">HL @Fmt(latest.TreadDepthRL)</span>
|
||||
<span class="insp-chip rating-@RatingClass(latest.RatingRR)">HR @Fmt(latest.TreadDepthRR)</span>
|
||||
</span>
|
||||
}
|
||||
else if (loaded)
|
||||
{
|
||||
<span class="insp-toggle-summary muted">Keine Messung</span>
|
||||
}
|
||||
<span class="insp-chevron @(isOpen ? "open" : "")" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
|
||||
@if (isOpen)
|
||||
{
|
||||
<div id="@panelId" class="insp-body">
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="insp-status"><span class="spinner"></span> Messungen werden geladen…</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
<div class="insp-error" role="alert">⚠️ @error</div>
|
||||
}
|
||||
|
||||
@if (AllowEdit)
|
||||
{
|
||||
<div class="insp-actions">
|
||||
<button type="button" class="btn btn-sm btn-outline" @onclick="OpenScanner" disabled="@(isBusy || editing != null)">
|
||||
@if (isFetching) { <span class="spinner"></span><span>Report wird geladen…</span> }
|
||||
else { <span>📷 QR-Report scannen</span> }
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" @onclick="StartManual" disabled="@(isBusy || editing != null)">
|
||||
✎ Manuell erfassen
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (editing != null)
|
||||
{
|
||||
<div class="insp-edit">
|
||||
<h4>@(isNew ? "Neue Messung" : "Messung bearbeiten")</h4>
|
||||
<div class="insp-meta">
|
||||
<div class="form-group">
|
||||
<label>Datum</label>
|
||||
<input type="date" class="form-control" value="@(editing.MeasuredAt?.ToLocalTime().ToString("yyyy-MM-dd"))" @onchange="OnDateChanged" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kennzeichen</label>
|
||||
<input type="text" class="form-control" @bind="editing.LicensePlate" placeholder="z.B. ND-XX 123" maxlength="20" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kilometerstand</label>
|
||||
<input type="number" class="form-control" @bind="editing.Mileage" min="0" placeholder="km" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tread-grid" role="group" aria-label="Profiltiefen">
|
||||
<div class="tread-cell rating-@RatingClass(TireInspection.RateDepth(editing.TreadDepthFL))">
|
||||
<label>Vorne links</label>
|
||||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="editing.TreadDepthFL" /><span>mm</span></div>
|
||||
</div>
|
||||
<div class="tread-cell rating-@RatingClass(TireInspection.RateDepth(editing.TreadDepthFR))">
|
||||
<label>Vorne rechts</label>
|
||||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="editing.TreadDepthFR" /><span>mm</span></div>
|
||||
</div>
|
||||
<div class="tread-cell rating-@RatingClass(TireInspection.RateDepth(editing.TreadDepthRL))">
|
||||
<label>Hinten links</label>
|
||||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="editing.TreadDepthRL" /><span>mm</span></div>
|
||||
</div>
|
||||
<div class="tread-cell rating-@RatingClass(TireInspection.RateDepth(editing.TreadDepthRR))">
|
||||
<label>Hinten rechts</label>
|
||||
<div class="tread-input"><input type="number" step="0.1" min="0" max="20" class="form-control" @bind="editing.TreadDepthRR" /><span>mm</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Empfehlung / Bemerkung</label>
|
||||
<textarea class="form-control" rows="2" @bind="editing.Recommendation" maxlength="1000"></textarea>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(editing.ReportUrl))
|
||||
{
|
||||
<a class="insp-link" href="@editing.ReportUrl" target="_blank" rel="noopener">Original-Report öffnen ↗</a>
|
||||
}
|
||||
<div class="insp-edit-footer">
|
||||
<button type="button" class="btn btn-sm btn-outline" @onclick="CancelEdit" disabled="@isBusy">Abbrechen</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" @onclick="SaveEdit" disabled="@isBusy">
|
||||
@if (isBusy) { <span class="spinner"></span> }
|
||||
<span>Speichern</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (inspections.Count == 0)
|
||||
{
|
||||
@if (editing == null)
|
||||
{
|
||||
<p class="insp-empty">Noch keine Profiltiefen-Messung für diesen Satz vorhanden.</p>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="insp-list" role="list">
|
||||
@foreach (var insp in inspections)
|
||||
{
|
||||
<li @key="insp.Id" class="insp-item @(insp == latest ? "latest" : "")">
|
||||
<div class="insp-item-head">
|
||||
<span class="insp-date">
|
||||
@((insp.MeasuredAt ?? insp.CreatedAt).ToLocalTime().ToString("dd.MM.yyyy HH:mm"))
|
||||
@if (insp == latest) { <span class="insp-badge">aktuell</span> }
|
||||
</span>
|
||||
<span class="insp-source muted">@insp.Source@(string.IsNullOrEmpty(insp.RecordSn) ? "" : $" · {insp.RecordSn}")</span>
|
||||
@if (AllowEdit)
|
||||
{
|
||||
<span class="insp-item-actions">
|
||||
<button type="button" class="btn-link" @onclick="() => StartEdit(insp)" disabled="@(isBusy || editing != null)">Bearbeiten</button>
|
||||
@if (confirmDeleteId == insp.Id)
|
||||
{
|
||||
<button type="button" class="btn-link danger" @onclick="() => Delete(insp)" disabled="@isBusy">Wirklich löschen</button>
|
||||
<button type="button" class="btn-link" @onclick="() => confirmDeleteId = null" disabled="@isBusy">Nein</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn-link danger" @onclick="() => confirmDeleteId = insp.Id" disabled="@(isBusy || editing != null)">Löschen</button>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="insp-chips">
|
||||
<span class="insp-chip rating-@RatingClass(insp.RatingFL)">VL @Fmt(insp.TreadDepthFL)</span>
|
||||
<span class="insp-chip rating-@RatingClass(insp.RatingFR)">VR @Fmt(insp.TreadDepthFR)</span>
|
||||
<span class="insp-chip rating-@RatingClass(insp.RatingRL)">HL @Fmt(insp.TreadDepthRL)</span>
|
||||
<span class="insp-chip rating-@RatingClass(insp.RatingRR)">HR @Fmt(insp.TreadDepthRR)</span>
|
||||
</div>
|
||||
<div class="insp-details muted">
|
||||
@if (!string.IsNullOrEmpty(insp.LicensePlate)) { <span>🚗 @insp.LicensePlate</span> }
|
||||
@if (insp.Mileage.HasValue) { <span>@insp.Mileage.Value.ToString("N0", De) km</span> }
|
||||
@if (!string.IsNullOrEmpty(insp.ReportUrl)) { <a class="insp-link" href="@insp.ReportUrl" target="_blank" rel="noopener">Report ↗</a> }
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(insp.Recommendation))
|
||||
{
|
||||
<div class="insp-recommendation">💡 @insp.Recommendation</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (AllowEdit)
|
||||
{
|
||||
<QrScannerModal @bind-IsOpen="showScanner"
|
||||
Title="Reifen-Report scannen"
|
||||
Subtitle="@ScannerSubtitle"
|
||||
OnScanned="OnQrScanned" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public Guid TireStackId { get; set; }
|
||||
[Parameter] public bool AllowEdit { get; set; }
|
||||
[Parameter] public bool Compact { get; set; }
|
||||
[Parameter] public bool InitiallyOpen { get; set; }
|
||||
[Parameter] public string? ScannerSubtitle { get; set; }
|
||||
[Parameter] public EventCallback<TireInspection?> OnLatestChanged { get; set; }
|
||||
|
||||
private static readonly System.Globalization.CultureInfo De = System.Globalization.CultureInfo.GetCultureInfo("de-DE");
|
||||
private readonly string panelId = $"insp-{Guid.NewGuid():N}";
|
||||
|
||||
private Guid loadedFor;
|
||||
private bool isOpen;
|
||||
private bool loaded;
|
||||
private bool isLoading;
|
||||
private bool isBusy;
|
||||
private bool isFetching;
|
||||
private bool showScanner;
|
||||
private string? error;
|
||||
private List<TireInspection> inspections = new();
|
||||
private TireInspection? latest;
|
||||
private TireInspection? editing;
|
||||
private bool isNew;
|
||||
private Guid? confirmDeleteId;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (loadedFor != TireStackId)
|
||||
{
|
||||
loadedFor = TireStackId;
|
||||
inspections = new();
|
||||
latest = null;
|
||||
loaded = false;
|
||||
editing = null;
|
||||
confirmDeleteId = null;
|
||||
error = null;
|
||||
isOpen = InitiallyOpen;
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
inspections = await TireStackService.GetInspectionsAsync(TireStackId);
|
||||
latest = inspections.FirstOrDefault();
|
||||
loaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"Messungen konnten nicht geladen werden: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Toggle() => isOpen = !isOpen;
|
||||
|
||||
private static string RatingClass(TireMeasureRating r) => r switch
|
||||
{
|
||||
TireMeasureRating.Ok => "ok",
|
||||
TireMeasureRating.Warning => "warn",
|
||||
TireMeasureRating.Critical => "crit",
|
||||
_ => "none"
|
||||
};
|
||||
|
||||
private static string Fmt(decimal? mm) => mm.HasValue ? mm.Value.ToString("0.0", De) + " mm" : "–";
|
||||
|
||||
private void OpenScanner()
|
||||
{
|
||||
error = null;
|
||||
showScanner = true;
|
||||
}
|
||||
|
||||
private async Task OnQrScanned(string text)
|
||||
{
|
||||
showScanner = false;
|
||||
|
||||
if (!TireReportService.TryParseReportUrl(text, out var recordSn, out _))
|
||||
{
|
||||
error = "Der QR-Code enthält keinen gültigen Reifen-Report-Link.";
|
||||
return;
|
||||
}
|
||||
|
||||
isFetching = true;
|
||||
error = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var report = await TireReportService.FetchAsync(recordSn);
|
||||
report.RecomputeRatings();
|
||||
editing = report.ToInspection(TireStackId, UserSession.CurrentUser?.Id);
|
||||
isNew = true;
|
||||
}
|
||||
catch (TireReportException ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"Report konnte nicht verarbeitet werden: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isFetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartManual()
|
||||
{
|
||||
error = null;
|
||||
confirmDeleteId = null;
|
||||
isNew = true;
|
||||
editing = new TireInspection
|
||||
{
|
||||
TireStackId = TireStackId,
|
||||
Source = "Manuell",
|
||||
MeasuredAt = DateTime.UtcNow,
|
||||
CreatedByUserId = UserSession.CurrentUser?.Id
|
||||
};
|
||||
}
|
||||
|
||||
private void StartEdit(TireInspection insp)
|
||||
{
|
||||
error = null;
|
||||
confirmDeleteId = null;
|
||||
isNew = false;
|
||||
editing = new TireInspection
|
||||
{
|
||||
Id = insp.Id,
|
||||
TireStackId = insp.TireStackId,
|
||||
Source = insp.Source,
|
||||
RecordSn = insp.RecordSn,
|
||||
ReportUrl = insp.ReportUrl,
|
||||
MeasuredAt = insp.MeasuredAt,
|
||||
LicensePlate = insp.LicensePlate,
|
||||
Mileage = insp.Mileage,
|
||||
TreadDepthFL = insp.TreadDepthFL,
|
||||
TreadDepthFR = insp.TreadDepthFR,
|
||||
TreadDepthRL = insp.TreadDepthRL,
|
||||
TreadDepthRR = insp.TreadDepthRR,
|
||||
Recommendation = insp.Recommendation,
|
||||
RawJson = insp.RawJson,
|
||||
CreatedAt = insp.CreatedAt,
|
||||
CreatedByUserId = insp.CreatedByUserId
|
||||
};
|
||||
}
|
||||
|
||||
private void OnDateChanged(ChangeEventArgs e)
|
||||
{
|
||||
if (editing == null) return;
|
||||
if (DateTime.TryParse(e.Value?.ToString(), out var d))
|
||||
editing.MeasuredAt = DateTime.SpecifyKind(d, DateTimeKind.Local).ToUniversalTime();
|
||||
else
|
||||
editing.MeasuredAt = null;
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
editing = null;
|
||||
error = null;
|
||||
}
|
||||
|
||||
private async Task SaveEdit()
|
||||
{
|
||||
if (editing == null) return;
|
||||
|
||||
isBusy = true;
|
||||
error = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
editing.RatingFL = TireInspection.RateDepth(editing.TreadDepthFL);
|
||||
editing.RatingFR = TireInspection.RateDepth(editing.TreadDepthFR);
|
||||
editing.RatingRL = TireInspection.RateDepth(editing.TreadDepthRL);
|
||||
editing.RatingRR = TireInspection.RateDepth(editing.TreadDepthRR);
|
||||
editing.LicensePlate = string.IsNullOrWhiteSpace(editing.LicensePlate) ? null : editing.LicensePlate.Trim();
|
||||
editing.Recommendation = string.IsNullOrWhiteSpace(editing.Recommendation) ? null : editing.Recommendation.Trim();
|
||||
|
||||
if (isNew)
|
||||
await TireStackService.AddInspectionAsync(editing);
|
||||
else
|
||||
await TireStackService.UpdateInspectionAsync(editing);
|
||||
|
||||
editing = null;
|
||||
await LoadAsync();
|
||||
await OnLatestChanged.InvokeAsync(latest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"Speichern fehlgeschlagen: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Delete(TireInspection insp)
|
||||
{
|
||||
isBusy = true;
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
await TireStackService.DeleteInspectionAsync(insp.Id);
|
||||
confirmDeleteId = null;
|
||||
await LoadAsync();
|
||||
await OnLatestChanged.InvokeAsync(latest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"Löschen fehlgeschlagen: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
.insp-panel {
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
margin-top: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.insp-toggle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
}
|
||||
|
||||
.insp-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.insp-toggle-icon {
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.insp-toggle-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.insp-toggle-summary {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.insp-toggle-summary.muted {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.insp-chevron {
|
||||
transition: transform 0.15s;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.insp-chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.insp-body {
|
||||
padding: 0 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
border-top: 0.5px solid rgba(255, 255, 255, 0.08);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.insp-panel.compact .insp-toggle {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.insp-panel.compact .insp-body {
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.insp-status,
|
||||
.insp-empty {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.insp-error {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
border: 0.5px solid rgba(239, 68, 68, 0.4);
|
||||
color: #fca5a5;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.insp-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.insp-actions .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.insp-chip {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.12);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.insp-chip.rating-ok { background: rgba(34, 197, 94, 0.15); border-color: rgba(34, 197, 94, 0.4); color: #86efac; }
|
||||
.insp-chip.rating-warn { background: rgba(250, 204, 21, 0.15); border-color: rgba(250, 204, 21, 0.4); color: #fde047; }
|
||||
.insp-chip.rating-crit { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.4); color: #fca5a5; }
|
||||
|
||||
.insp-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.insp-item {
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.insp-item.latest {
|
||||
border-color: rgba(96, 165, 250, 0.4);
|
||||
}
|
||||
|
||||
.insp-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.insp-date {
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.insp-badge {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(96, 165, 250, 0.2);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.insp-item-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: #60a5fa;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-link.danger {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.btn-link:hover:not(:disabled) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-link:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.insp-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.insp-details {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.insp-link {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.insp-recommendation {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: rgba(96, 165, 250, 0.1);
|
||||
border: 0.5px solid rgba(96, 165, 250, 0.25);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
/* Edit form */
|
||||
.insp-edit {
|
||||
border: 0.5px solid rgba(96, 165, 250, 0.35);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: rgba(96, 165, 250, 0.05);
|
||||
}
|
||||
|
||||
.insp-edit h4 {
|
||||
margin: 0;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.insp-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-group label,
|
||||
.tread-cell label {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
padding: 6px 10px;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.tread-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tread-cell {
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.tread-cell.rating-ok { border-color: rgba(34, 197, 94, 0.5); background: rgba(34, 197, 94, 0.08); }
|
||||
.tread-cell.rating-warn { border-color: rgba(250, 204, 21, 0.5); background: rgba(250, 204, 21, 0.08); }
|
||||
.tread-cell.rating-crit { border-color: rgba(239, 68, 68, 0.5); background: rgba(239, 68, 68, 0.08); }
|
||||
|
||||
.tread-cell label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tread-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tread-input .form-control {
|
||||
max-width: 100px;
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tread-input span {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.insp-edit-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.insp-edit-footer .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: insp-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes insp-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.insp-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.insp-toggle-summary {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user