- Case numbers SUP-000001, SUP-000002 … per customer, unique per customer (new index; a migration step renames any old duplicates first). The portal suggests the next number on the finding page and under the new "Ny sag" on the customer page; an empty field gets the next number, a typed one that is taken is refused. Concurrent creates retry. - SupportCase gains Source (Staff/Customer), description, contact name, e-mail, phone, the customer's Windows user and ClientRequestId. - POST/GET /api/v1/cases for the customer's agent (license with priority support): creates a case idempotently on ClientRequestId, links the finding it came from, lists the customer's cases incl. staff-created. Customer/license creation is shared with the findings endpoint. - Optional mail to support for customer cases (MailKit; prefers AUTH LOGIN as Microsoft 365 needs). A failed mail is logged, never fails the call. deploy-hub.yml writes optional SUPPORT_SMTP_* secrets to .env. - Supportrapport: created, closed, open now, average/median resolution time, oldest open case and customer-created share per customer for a period (default 90 days), plus the cases; CSV for Danish Excel. - Cases page: newest first, customer cases marked, contact/description. - Customer page: findings can be filtered to one instance (?instans=id, ignored for another customer's instance); active findings per instance by severity. - dotnet-ef as a local tool (dotnet-tools.json) for the migration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
123 lines
5.3 KiB
Text
123 lines
5.3 KiB
Text
@page "/portal/rapporter/support"
|
||
@attribute [Authorize]
|
||
@using SQLInsight.Hub.Support
|
||
@inject PortalQueries Queries
|
||
|
||
<PageTitle>Supportrapport · SQLInsight Hub</PageTitle>
|
||
@if (_report is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
<h1>Supportrapport</h1>
|
||
<p class="lead">
|
||
Sager oprettet og lukket i perioden, åbne sager nu og hvor hurtigt sagerne blev løst.
|
||
Løsningstid er tiden fra oprettelse til lukning for sager lukket i perioden. Tider er UTC.
|
||
</p>
|
||
|
||
<form method="get" action="/portal/rapporter/support" class="inline-form report-filter">
|
||
<label>Fra <input type="date" name="fra" value="@_report.From.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)" /></label>
|
||
<label>Til og med <input type="date" name="til" value="@_report.To.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)" /></label>
|
||
<label>Kunde
|
||
<select name="kunde">
|
||
<option value="">Alle kunder</option>
|
||
@foreach (var (id, name) in _customers)
|
||
{
|
||
<option value="@id" selected="@(id == Kunde)">@name</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
<button type="submit" class="primary">Vis</button>
|
||
<a class="theme-toggle" href="@CsvUrl">Hent CSV</a>
|
||
</form>
|
||
|
||
<section class="kpis">
|
||
<div class="kpi"><div><strong>@_report.Total.Created</strong><span>oprettet</span></div></div>
|
||
<div class="kpi"><div><strong>@_report.Total.Closed</strong><span>lukket</span></div></div>
|
||
<div class="kpi"><div><strong>@_report.Total.OpenNow</strong><span>åbne nu</span></div></div>
|
||
<div class="kpi"><div><strong>@SupportReport.Duration(_report.Total.MedianResolution)</strong><span>median løsningstid</span></div></div>
|
||
<div class="kpi"><div><strong>@SupportReport.Duration(_report.Total.AverageResolution)</strong><span>gennemsnitlig løsningstid</span></div></div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="card-head"><h2>Pr. kunde</h2></div>
|
||
@if (_report.Customers.Count == 0)
|
||
{
|
||
<p class="empty">Ingen sager i perioden.</p>
|
||
}
|
||
else
|
||
{
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Kunde</th><th class="num">Oprettet</th><th class="num">Lukket</th><th class="num">Åbne nu</th>
|
||
<th>Gennemsnit</th><th>Median</th><th>Ældste åbne</th><th class="num">Oprettet af kunden</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var c in _report.Customers.Append(_report.Total))
|
||
{
|
||
<tr class="@(c.CustomerId is null ? "total" : "")">
|
||
<td>@if (c.CustomerId is { } id) { <a href="/portal/kunder/@id">@c.Customer</a> } else { <strong>@c.Customer</strong> }</td>
|
||
<td class="num">@c.Created</td>
|
||
<td class="num">@c.Closed</td>
|
||
<td class="num">@c.OpenNow</td>
|
||
<td class="nowrap">@SupportReport.Duration(c.AverageResolution)</td>
|
||
<td class="nowrap">@SupportReport.Duration(c.MedianResolution)</td>
|
||
<td class="nowrap">@SupportReport.Duration(c.OldestOpenAge)</td>
|
||
<td class="num">@(c.CustomerCreatedShare is { } share ? share.ToString("P0", Danish) : "–")</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
}
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="card-head"><h2>Sager i perioden</h2></div>
|
||
@if (_report.Cases.Count == 0)
|
||
{
|
||
<p class="empty">Ingen.</p>
|
||
}
|
||
else
|
||
{
|
||
<table>
|
||
<thead><tr><th>Sag</th><th>Kunde</th><th>Titel</th><th>Kilde</th><th>Oprettet</th><th>Lukket</th><th>Løsningstid</th></tr></thead>
|
||
<tbody>
|
||
@foreach (var c in _report.Cases)
|
||
{
|
||
<tr>
|
||
<td class="mono nowrap">@c.Reference</td>
|
||
<td><a href="/portal/kunder/@c.CustomerId">@c.Customer</a></td>
|
||
<td>@c.Title</td>
|
||
<td>@(c.Source == SQLInsight.Contracts.SupportCaseSources.Customer ? "Kunde" : "SEOSoft")</td>
|
||
<td class="nowrap">@c.CreatedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)</td>
|
||
<td class="nowrap">@c.ClosedAt?.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)</td>
|
||
<td class="nowrap">@(c.ClosedAt is null ? "åben" : SupportReport.Duration(c.ResolutionTime))</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
}
|
||
</section>
|
||
|
||
@code {
|
||
private static readonly CultureInfo Danish = CultureInfo.GetCultureInfo("da-DK");
|
||
|
||
[SupplyParameterFromQuery] public DateTime? Fra { get; set; }
|
||
[SupplyParameterFromQuery] public DateTime? Til { get; set; }
|
||
[SupplyParameterFromQuery] public int? Kunde { get; set; }
|
||
|
||
private SupportReportResult? _report;
|
||
private IReadOnlyList<(int Id, string Name)> _customers = [];
|
||
|
||
private string CsvUrl =>
|
||
$"/portal/actions/reports/support.csv?fra={_report!.From:yyyy-MM-dd}&til={_report.To.AddDays(-1):yyyy-MM-dd}{(Kunde is { } k ? $"&kunde={k}" : "")}";
|
||
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
var (from, to) = SupportReportPeriod.Resolve(Fra, Til, Queries.Now);
|
||
_report = await Queries.SupportReportAsync(from, to, Kunde);
|
||
_customers = await Queries.CustomerNamesAsync();
|
||
}
|
||
}
|