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 ICustomerService CustomerService
|
||||||
@inject ITireStackService TireStackService
|
@inject ITireStackService TireStackService
|
||||||
@inject IStorageLocationService StorageLocationService
|
@inject IStorageLocationService StorageLocationService
|
||||||
|
@inject ITireReportService TireReportService
|
||||||
@inject UserSessionService UserSession
|
@inject UserSessionService UserSession
|
||||||
|
|
||||||
<PageTitle>Reifenstapel Abholen - Reifen Späth Lagerverwaltung</PageTitle>
|
<PageTitle>Reifenstapel Abholen - Reifen Späth Lagerverwaltung</PageTitle>
|
||||||
@@ -210,6 +211,39 @@ else
|
|||||||
<div class="swap-box incoming">
|
<div class="swap-box incoming">
|
||||||
<h3 class="swap-box-title"><span aria-hidden="true">⬇️</span> Einlagerung (kommt vom Fahrzeug)</h3>
|
<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">
|
<div class="swap-tabs" role="tablist">
|
||||||
<button type="button" class="swap-tab @(swapUseHistory ? "active" : "")" role="tab" aria-selected="@swapUseHistory"
|
<button type="button" class="swap-tab @(swapUseHistory ? "active" : "")" role="tab" aria-selected="@swapUseHistory"
|
||||||
@onclick="() => swapUseHistory = true" disabled="@(swapHistory.Count == 0)">
|
@onclick="() => swapUseHistory = true" disabled="@(swapHistory.Count == 0)">
|
||||||
@@ -402,6 +436,91 @@ else
|
|||||||
HighlightLocationId="@swapOutgoing.StorageLocationId"
|
HighlightLocationId="@swapOutgoing.StorageLocationId"
|
||||||
Subtitle="@($"Einlagerung für {swapOutgoing.Customer?.FullName} – {SeasonLabel(swapUseHistory ? (swapSelectedHistory?.Season ?? swapNew.Season) : swapNew.Season)}")"
|
Subtitle="@($"Einlagerung für {swapOutgoing.Customer?.FullName} – {SeasonLabel(swapUseHistory ? (swapSelectedHistory?.Season ?? swapNew.Season) : swapNew.Season)}")"
|
||||||
OnSelected="OnTargetLocationSelected" />
|
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 *@
|
@* Action Chooser Modal *@
|
||||||
@@ -457,6 +576,14 @@ else
|
|||||||
{
|
{
|
||||||
<p class="success-detail">Lagerplatz <strong>@swapResult.FreedLocationCode</strong> ist wieder frei.</p>
|
<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>
|
<button class="btn btn-primary" @onclick="CloseSwapSuccess">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -536,6 +663,15 @@ else
|
|||||||
private Guid? swapTargetLocationId => swapTargetLocation?.Id;
|
private Guid? swapTargetLocationId => swapTargetLocation?.Id;
|
||||||
private SwapResult? swapResult;
|
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));
|
private bool CanSwap => swapOutgoing != null && (swapUseHistory ? swapSelectedHistory != null : !string.IsNullOrWhiteSpace(swapNew.TireSize));
|
||||||
|
|
||||||
[SupplyParameterFromQuery(Name = "stackId")]
|
[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 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 static string SeasonLabel(TireSeason s) => s == TireSeason.Sommer ? "☀️ Sommer" : s == TireSeason.Winter ? "❄️ Winter" : "🔄 Allrad";
|
||||||
|
|
||||||
@@ -609,6 +850,11 @@ else
|
|||||||
showLocationPicker = false;
|
showLocationPicker = false;
|
||||||
swapTargetLocation = outgoing.StorageLocation;
|
swapTargetLocation = outgoing.StorageLocation;
|
||||||
swapSelectedHistory = null;
|
swapSelectedHistory = null;
|
||||||
|
swapReport = null;
|
||||||
|
scannedReport = null;
|
||||||
|
showQrScanner = false;
|
||||||
|
swapReportSaved = false;
|
||||||
|
swapReportError = null;
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
successMessage = null;
|
successMessage = null;
|
||||||
|
|
||||||
@@ -669,6 +915,9 @@ else
|
|||||||
swapHistory.Clear();
|
swapHistory.Clear();
|
||||||
swapSelectedHistory = null;
|
swapSelectedHistory = null;
|
||||||
showLocationPicker = false;
|
showLocationPicker = false;
|
||||||
|
showQrScanner = false;
|
||||||
|
scannedReport = null;
|
||||||
|
swapReport = null;
|
||||||
swapTargetLocation = null;
|
swapTargetLocation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,13 +943,31 @@ else
|
|||||||
: swapNew;
|
: swapNew;
|
||||||
|
|
||||||
var targetLocation = swapTargetLocationId == swapOutgoing.StorageLocationId ? null : swapTargetLocationId;
|
var targetLocation = swapTargetLocationId == swapOutgoing.StorageLocationId ? null : swapTargetLocationId;
|
||||||
|
var report = swapReport;
|
||||||
|
|
||||||
swapResult = await TireStackService.SwapAsync(
|
var result = await TireStackService.SwapAsync(
|
||||||
swapOutgoing.Id,
|
swapOutgoing.Id,
|
||||||
incoming,
|
incoming,
|
||||||
targetLocation,
|
targetLocation,
|
||||||
UserSession.CurrentUser?.Id);
|
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();
|
CancelSwap();
|
||||||
searchResults.Clear();
|
searchResults.Clear();
|
||||||
searchTerm = "";
|
searchTerm = "";
|
||||||
|
|||||||
@@ -1102,4 +1102,276 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
QR Report (swap flow)
|
||||||
|
======================================== */
|
||||||
|
.report-scan-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 0.5px dashed rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-scan-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-scan-icon {
|
||||||
|
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-scan-bar .btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-attached {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-attached-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #60a5fa;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-chip {
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 9px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-chip.rating-ok { background: rgba(34, 197, 94, 0.15); border-color: rgba(34, 197, 94, 0.4); color: #86efac; }
|
||||||
|
.report-chip.rating-warn { background: rgba(250, 204, 21, 0.15); border-color: rgba(250, 204, 21, 0.4); color: #fde047; }
|
||||||
|
.report-chip.rating-crit { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.4); color: #fca5a5; }
|
||||||
|
|
||||||
|
.report-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
z-index: 1150;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-dialog {
|
||||||
|
background: #1c1c1e;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
max-height: 92vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-subtitle {
|
||||||
|
margin: 0.2rem 0 0;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-body {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-warning {
|
||||||
|
background: rgba(250, 204, 21, 0.1);
|
||||||
|
border: 0.5px solid rgba(250, 204, 21, 0.35);
|
||||||
|
color: #fde047;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-warning.info {
|
||||||
|
background: rgba(96, 165, 250, 0.1);
|
||||||
|
border-color: rgba(96, 165, 250, 0.3);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-meta .form-group {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tread-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tread-cell {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
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;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tread-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tread-input .form-control {
|
||||||
|
max-width: 110px;
|
||||||
|
font-family: 'Orbitron', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tread-input span {
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-recommendation {
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
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: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-link {
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #60a5fa;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0.85rem 1.25rem;
|
||||||
|
border-top: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-detail.warn {
|
||||||
|
color: #fde047;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.report-meta,
|
||||||
|
.tread-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Reduced motion: inherited from app.css (section 17) */
|
/* Reduced motion: inherited from app.css (section 17) */
|
||||||
Reference in new issue
Block a user