Rifacimento della UI secondo STILE_UI.md

Adotta lo stile condiviso delle app Integry: superfici piatte, raggi ampi,
spaziature a scala fissa, gerarchia data dallo spazio prima che dalla
decorazione. La palette storica di SteUP resta intatta; sono stati aggiunti
solo i ruoli che mancavano (workspace, surfaceMuted, divider, inkMuted e le
triplette dei toni semantici).

Fondamenta
- tokens.css: ruoli colore, scale di spazio/raggi/tipografia, curve di
  movimento e scala z-index semantica. Unica fonte di verita': pilotano
  anche le variabili CSS di MudBlazor.
- Inter variabile ospitata in locale (133KB) al posto di Nunito da Google
  Fonts: l'app lavora offline, un font che arriva dalla rete in campo non
  arriva.
- base.css: shell applicativa e comportamenti da app nativa — niente
  selezione testo, niente menu al tocco lungo, niente lampo grigio sul tap,
  niente rimbalzo elastico, niente zoom a doppio tocco, scroll con inerzia.
- mudblazor-overrides.css: MudBlazor appiattito (elevation 0, nessun tint,
  target 44/48, etichette in caso naturale).
- Rimossi Bootstrap (CSS, icone e JS) e i 996 righe di CSS con ambito per
  componente del vecchio sistema: erano tutti inutilizzati.

Struttura
- Navigazione flottante in basso con icona piena sulla voce attiva; da 720px
  diventa colonna laterale. Il MudFabMenu e' sostituito da un bottom sheet:
  stessa funzione, bersagli molto piu' grandi.
- Testata alta col titolo a sinistra, fusa con la pagina.
- Nel form scheda il salvataggio sta in una barra ancorata in fondo, non in
  coda a uno scroll lungo.
- Scelta di punto vendita e articoli a schermo intero con ricerca; la
  selezione multipla usa righe da 56px invece di un menu a tendina.
- Urgenza scelta a pillole invece che da una select.

Stato e riscontro
- StatusBadgeHelper: tono, icona ed etichetta di ogni stato in un punto solo,
  cosi' lo stesso stato si presenta identico ovunque.
- Scheletri al posto degli spinner; l'overlay bloccante dice sempre cosa sta
  facendo; gli stati vuoti suggeriscono la mossa successiva.
- Rimosso il ritardo artificiale di 250ms in InspectionCard, che non
  attendeva nulla.

DESIGN.md riscritto sul nuovo sistema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 11:09:26 +02:00
parent 3a9174f6fc
commit 2a0054aa30
77 changed files with 4595 additions and 3060 deletions
+146 -98
View File
@@ -2,11 +2,13 @@
@using Microsoft.Extensions.Logging
@using SteUp.Shared.Components.Layout
@using SteUp.Shared.Components.Layout.Overlay
@using SteUp.Shared.Components.SingleElements
@using SteUp.Shared.Components.SingleElements.Card
@using SteUp.Shared.Components.SingleElements.MessageBox
@using SteUp.Shared.Core.Dto
@using SteUp.Shared.Core.Entities
@using SteUp.Shared.Core.Enum
@using SteUp.Shared.Core.Helpers
@using SteUp.Shared.Core.Interface.IntegryApi
@using SteUp.Shared.Core.Interface.LocalDb
@using SteUp.Shared.Core.Interface.System
@@ -21,98 +23,133 @@
@inject ILogger<IspezionePage> Logger
@implements IDisposable
<HeaderLayout Title="Ispezione" BackTo="Indietro" Back="true"/>
<HeaderLayout Title="@Titolo" Subtitle="@Sottotitolo" Back="true"/>
<div class="container content pb-safe-area ispezione">
<InspectionCard Ispezione="SteupDataService.InspectionPageState.Ispezione"/>
<div class="app-scroll">
<div class="page-width page-body stack stack-lg">
@if (!SchedeGrouped.IsNullOrEmpty())
{
<MudExpansionPanels MultiExpansion="true">
@{
var panelIndex = 0;
@* §6.6 Pannello di riepilogo: primo elemento della pagina, superficie
tenue, con lo stato del documento e i conteggi sempre a vista. *@
<section class="panel panel--info panel--lg">
<div class="summary__head">
<div class="section-header__text">
<h2 class="summary__title">@Titolo</h2>
<p class="summary__subtitle">@Ispezione.Data.ToString("d MMMM yyyy")</p>
</div>
<Badge Spec="@Ispezione.Stato.ToBadge()"/>
</div>
<div class="summary__metrics">
<div class="summary__metric">
<i class="summary__metric-icon ri-file-list-3-line" aria-hidden="true"></i>
<div class="summary__metric-value">@SchedeList.Count</div>
<div class="summary__metric-label">@(SchedeList.Count == 1 ? "scheda" : "schede")</div>
</div>
<div class="summary__metric">
<i class="summary__metric-icon ri-smartphone-line" aria-hidden="true"></i>
<div class="summary__metric-value">@DaInviare</div>
<div class="summary__metric-label">da inviare</div>
</div>
<div class="summary__metric">
<i class="summary__metric-icon ri-edit-line" aria-hidden="true"></i>
<div class="summary__metric-value">@Bozze</div>
<div class="summary__metric-label">da completare</div>
</div>
</div>
@if (SchedeList.Count > 0)
{
<div class="summary__progress-head">
<span class="summary__progress-count">@Inviate di @SchedeList.Count inviate</span>
<span class="summary__progress-pct">@PercentualeInviate%</span>
</div>
<div class="progress-track" role="progressbar" aria-valuenow="@PercentualeInviate"
aria-valuemin="0" aria-valuemax="100" aria-label="Schede inviate al server">
<div class="progress-fill" style="--progress: @Progresso"></div>
</div>
}
</section>
@if (Bozze > 0)
{
@* Feedback inline: resta a video finche' il problema esiste, a
differenza del toast che sparisce. *@
<div class="feedback feedback--warning" role="status" aria-live="polite">
<i class="feedback__icon ri-edit-line" aria-hidden="true"></i>
<span>
@(Bozze == 1 ? "1 scheda è rimasta a metà" : $"{Bozze} schede sono rimaste a metà").
Completale o eliminale prima di concludere l'ispezione.
</span>
</div>
}
@if (SchedeGrouped.Count == 0)
{
<div class="empty-state">
<i class="empty-state__icon ri-clipboard-line" aria-hidden="true"></i>
<p class="empty-state__title">Nessuna scheda</p>
@if (!IsCompleted)
{
<p class="empty-state__text">
Tocca <b>+</b> nella barra in basso per creare la prima scheda di questa ispezione.
</p>
}
</div>
}
else
{
@foreach (var group in SchedeGrouped)
{
var isFirstPanel = panelIndex == 0;
panelIndex++;
<MudExpansionPanel Class="expansion-panel" Expanded="isFirstPanel" Text="@group.Key.Descrizione">
<TitleContent>
@{
var daInviare = group.Value.CountDaInviare();
var daCompletare = group.Value.CountBozze();
}
<div class="header-scheda-group">
<div class="title">
<MudText Typo="Typo.h6"><b>@group.Key.Descrizione</b></MudText>
<MudChip T="string" Size="Size.Small" Color="Color.Default">
@($"{group.Value.Count} sched{(group.Value.Count == 1 ? "a" : "e")}")
</MudChip>
@if (daInviare > 0)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled"
Color="Color.Primary" Icon="@Icons.Material.Rounded.PhonelinkOff">
@($"{daInviare} da inviare")
</MudChip>
}
@if (daCompletare > 0)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled"
Color="Color.Warning" Icon="@Icons.Material.Rounded.EditNote">
@($"{daCompletare} da completare")
</MudChip>
}
</div>
@if (SteupDataService.InspectionPageState.Ispezione.Stato != StatusEnum.Completata)
{
<div class="action-scheda-group">
<MudFab StartIcon="@Icons.Material.Rounded.Add"
Color="Color.Default" Size="Size.Medium"
Disabled="!NetworkService.IsNetworkAvailable()"
aria-label="@($"Nuova scheda su {group.Key.Descrizione}")"
OnClick="@(() => CreateNewScheda(group.Key))"/>
var daInviare = group.Value.CountDaInviare();
var daCompletare = group.Value.CountBozze();
@if (NetworkService.IsNetworkAvailable() && daInviare > 0)
{
<MudFab StartIcon="@Icons.Material.Rounded.CloudUpload" Label="Esporta reparto"
Color="Color.Primary" Size="Size.Medium"
aria-label="@($"Esporta il reparto {group.Key.Descrizione} al server")"
OnClick="@(() => ExportReparto(group.Key))"/>
}
</div>
}
<section>
<div class="section-header">
<div class="section-header__text">
<h2 class="section-header__title">@group.Key.Descrizione</h2>
<p class="section-header__desc">
@(group.Value.Count == 1 ? "1 scheda" : $"{group.Value.Count} schede")
</p>
</div>
</TitleContent>
<ChildContent>
@if (!IsCompleted && NetworkService.IsNetworkAvailable() && daInviare > 0)
{
<div class="section-header__aside">
<MudButton Variant="Variant.Outlined" Size="Size.Small"
StartIcon="@Icons.Material.Rounded.CloudUpload"
aria-label="@($"Invia le schede del reparto {group.Key.Descrizione}")"
OnClick="@(() => ExportReparto(group.Key))">
Invia @daInviare
</MudButton>
</div>
}
</div>
@if (daCompletare > 0)
{
<div class="badge-row" style="margin-bottom: var(--space-10);">
<Badge Spec="@SyncState.Bozza.ToBadge()" Text="@($"{daCompletare} da completare")"/>
</div>
}
<div class="card-list">
@foreach (var scheda in group.Value)
{
<SchedaCard Scheda="scheda" OnSchedaDeleted="OnSchedaDeleted"
<SchedaCard Scheda="scheda" Ispezione="Ispezione"
OnSchedaDeleted="OnSchedaDeleted"
OnSchedaModified="OnSchedaModified"
Ispezione="@SteupDataService.InspectionPageState.Ispezione"/>
OnBusyChanged="@(b => VisibleOverlay = b)"/>
}
</ChildContent>
</MudExpansionPanel>
</div>
</section>
}
</MudExpansionPanels>
}
else
{
<div class="empty-state">
<MudIcon Icon="@Icons.Material.Rounded.AssignmentLate" Class="empty-state-icon" Size="Size.Large"/>
<p class="empty-state-title">Nessuna scheda ancora</p>
@if (SteupDataService.InspectionPageState.Ispezione.Stato != StatusEnum.Completata)
{
<p class="empty-state-text">
Tocca <b>+</b> in basso per creare la prima scheda di questa ispezione.
</p>
}
</div>
}
}
</div>
</div>
<SpinnerOverlay VisibleOverlay="VisibleOverlay" SuccessAnimation="_success" Message="@_progressMessage"/>
<ConfirmMessageBox @ref="_exportRepartoBox" NoText="Annulla" YesText="Esporta" YesColor="Color.Primary"
Message="Le schede di questo reparto verranno inviate al server. Procedere?"/>
<ConfirmMessageBox @ref="_exportRepartoBox" NoText="Annulla" YesText="Invia" YesColor="Color.Primary"
Message="Le schede complete di questo reparto verranno inviate al server. Procedere?"/>
@code {
private List<Scheda> SchedeList { get; set; } = [];
@@ -125,6 +162,28 @@
private ConfirmMessageBox _exportRepartoBox = null!;
private Ispezione Ispezione => SteupDataService.InspectionPageState.Ispezione;
private bool IsCompleted => Ispezione.Stato == StatusEnum.Completata;
private int DaInviare => SchedeList.CountDaInviare();
private int Bozze => SchedeList.CountBozze();
private int Inviate => SchedeList.Count(s => s.GetSyncState() == SyncState.Inviato);
private int PercentualeInviate => SchedeList.Count == 0
? 0
: (int)Math.Round(Inviate * 100.0 / SchedeList.Count);
private string Progresso => (PercentualeInviate / 100.0).ToString("0.###",
System.Globalization.CultureInfo.InvariantCulture);
private string Titolo => SteupDataService.PuntiVenditaList
.Find(x => x.CodMdep != null && x.CodMdep.EqualsIgnoreCase(Ispezione.CodMdep))?
.Descrizione is { Length: > 0 } descrizione
? descrizione
: Ispezione.CodMdep;
private string Sottotitolo => Ispezione.CodMdep;
protected override void OnInitialized()
{
NewScheda.OnNewScheda += LoadSchede;
@@ -135,7 +194,7 @@
private void LoadSchede()
{
var ispezione = SteupDataService.InspectionPageState.Ispezione;
var ispezione = Ispezione;
InvokeAsync(async () =>
{
@@ -162,7 +221,7 @@
StateHasChanged();
});
var ispezione = SteupDataService.InspectionPageState.Ispezione;
var ispezione = Ispezione;
// Le bozze non sono inviabili: l'ispezione non si chiude finché ci sono.
var bozze = SchedeList.CountBozze();
@@ -277,7 +336,7 @@
StatusEnum.Completata
);
SteupDataService.InspectionPageState.Ispezione.Stato = StatusEnum.Completata;
ispezione.Stato = StatusEnum.Completata;
// Riscontro al momento più importante: la spunta di successo prima di chiudere l'overlay.
await InvokeAsync(() =>
@@ -339,18 +398,6 @@
StateHasChanged();
}
private async Task CreateNewScheda(JtbFasiDto jtbFasi)
{
var modal = await Dialog.OpenFormScheda(
SteupDataService.InspectionPageState.Ispezione.CodMdep,
SteupDataService.InspectionPageState.Ispezione.Data,
true,
new Scheda { Reparto = jtbFasi }
);
if (modal is { Canceled: false }) LoadSchede();
}
private async Task ExportReparto(JtbFasiDto jtbFasi)
{
// Guardia anti doppio-tap: un secondo tocco durante l'invio non ri-esporta il batch.
@@ -365,14 +412,14 @@
try
{
var ispezione = SteupDataService.InspectionPageState.Ispezione;
var ispezione = Ispezione;
// Le bozze restano sul dispositivo: si esporta solo ciò che è completo.
var daEsportare = SchedeGrouped[jtbFasi].FindAll(x => !x.Bozza);
if (daEsportare.Count == 0)
{
Snackbar.Add("Nessuna scheda completa da esportare in questo reparto", Severity.Info);
Snackbar.Add("Nessuna scheda completa da inviare in questo reparto", Severity.Info);
return;
}
@@ -398,11 +445,11 @@
if (apiResponse == null)
{
Snackbar.Add("Esportazione non riuscita. Le schede restano sul dispositivo, riprova.", Severity.Error);
Snackbar.Add("Invio non riuscito. Le schede restano sul dispositivo, riprova.", Severity.Error);
return;
}
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
ispezione.ActivityId = apiResponse.ActivityIdIspezione;
await IspezioniService.UpdateActivityIdIspezioneAsync(ispezione.CodMdep, ispezione.Data,
UserSession.User.Username, apiResponse.ActivityIdIspezione
@@ -423,7 +470,8 @@
}
}
Snackbar.Add("Reparto esportato", Severity.Success);
Snackbar.Add(daEsportare.Count == 1 ? "Scheda inviata" : $"{daEsportare.Count} schede inviate",
Severity.Success);
}
catch (Exception e)
{
@@ -453,4 +501,4 @@
CompleteInspection.OnComplete -= HandleCompleteInspection;
}
}
}