2a0054aa30
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>
505 lines
19 KiB
Plaintext
505 lines
19 KiB
Plaintext
@page "/ispezione"
|
|
@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
|
|
@using SteUp.Shared.Core.Messages.Ispezione
|
|
@using SteUp.Shared.Core.Messages.Scheda
|
|
@inject NewSchedaService NewScheda
|
|
@inject CompleteInspectionService CompleteInspection
|
|
@inject IIspezioniService IspezioniService
|
|
@inject IDialogService Dialog
|
|
@inject IIntegrySteupService IntegrySteupService
|
|
@inject IFileManager FileManager
|
|
@inject ILogger<IspezionePage> Logger
|
|
@implements IDisposable
|
|
|
|
<HeaderLayout Title="@Titolo" Subtitle="@Sottotitolo" Back="true"/>
|
|
|
|
<div class="app-scroll">
|
|
<div class="page-width page-body stack stack-lg">
|
|
|
|
@* §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 daInviare = group.Value.CountDaInviare();
|
|
var daCompletare = group.Value.CountBozze();
|
|
|
|
<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>
|
|
|
|
@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" Ispezione="Ispezione"
|
|
OnSchedaDeleted="OnSchedaDeleted"
|
|
OnSchedaModified="OnSchedaModified"
|
|
OnBusyChanged="@(b => VisibleOverlay = b)"/>
|
|
}
|
|
</div>
|
|
</section>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<SpinnerOverlay VisibleOverlay="VisibleOverlay" SuccessAnimation="_success" Message="@_progressMessage"/>
|
|
<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; } = [];
|
|
private Dictionary<JtbFasiDto, List<Scheda>> SchedeGrouped { get; set; } = [];
|
|
|
|
private bool VisibleOverlay { get; set; }
|
|
private bool _isBusy;
|
|
private bool _success;
|
|
private string? _progressMessage;
|
|
|
|
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;
|
|
CompleteInspection.OnComplete += HandleCompleteInspection;
|
|
|
|
LoadSchede();
|
|
}
|
|
|
|
private void LoadSchede()
|
|
{
|
|
var ispezione = Ispezione;
|
|
|
|
InvokeAsync(async () =>
|
|
{
|
|
SchedeList = await IspezioniService.GetAllSchedeOfIspezioneAsync(
|
|
ispezione.CodMdep, ispezione.Data, ispezione.Rilevatore
|
|
);
|
|
|
|
GroupSchede();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
|
|
private async void HandleCompleteInspection()
|
|
{
|
|
// Guardia anti doppio-invio: il completamento è irreversibile.
|
|
if (_isBusy) return;
|
|
_isBusy = true;
|
|
|
|
try
|
|
{
|
|
await InvokeAsync(() =>
|
|
{
|
|
VisibleOverlay = true;
|
|
StateHasChanged();
|
|
});
|
|
|
|
var ispezione = Ispezione;
|
|
|
|
// Le bozze non sono inviabili: l'ispezione non si chiude finché ci sono.
|
|
var bozze = SchedeList.CountBozze();
|
|
if (bozze > 0)
|
|
{
|
|
await InvokeAsync(() =>
|
|
{
|
|
VisibleOverlay = false;
|
|
StateHasChanged();
|
|
});
|
|
|
|
await Dialog.ShowWarning(
|
|
bozze == 1
|
|
? "C'è 1 scheda da completare. Completala o eliminala prima di concludere l'ispezione."
|
|
: $"Ci sono {bozze} schede da completare. Completale o eliminale prima di concludere l'ispezione."
|
|
);
|
|
return;
|
|
}
|
|
|
|
var daInviare = ispezione.Schede.Where(x => x.ActivityId == null && !x.Bozza).ToList();
|
|
var totale = daInviare.Count;
|
|
var indice = 0;
|
|
var fallite = 0;
|
|
|
|
foreach (var scheda in daInviare)
|
|
{
|
|
indice++;
|
|
await SetProgress($"Invio scheda {indice} di {totale}…");
|
|
|
|
var apiResponse = await IntegrySteupService.SaveScheda(
|
|
new SaveRequestDto
|
|
{
|
|
LocalIdScheda = scheda.Id,
|
|
ActivityTypeId = scheda.ActivityTypeId,
|
|
CodJfas = scheda.CodJfas,
|
|
CodMdep = ispezione.CodMdep,
|
|
DataCreazione = ispezione.Data,
|
|
Note = scheda.Note,
|
|
PersonaRif = scheda.Responsabile,
|
|
Barcodes = scheda.Articoli.ConvertAll(x => x.Barcode),
|
|
Scandeza = (ScadenzaEnum)scheda.Scadenza,
|
|
// Aggancia all'ispezione esistente: dopo la prima scheda l'id e' noto,
|
|
// così le successive non creano nuove ispezioni.
|
|
ParentActivityId = ispezione.ActivityId
|
|
}
|
|
);
|
|
|
|
// Fallita: resta "Da inviare". Non completeremo l'ispezione finché tutte non sono arrivate.
|
|
if (apiResponse == null)
|
|
{
|
|
fallite++;
|
|
continue;
|
|
}
|
|
|
|
scheda.ActivityId = apiResponse.ActivityIdScheda;
|
|
await IspezioniService.UpdateSchedaAsync(scheda);
|
|
|
|
await IspezioniService.UpdateActivityIdIspezioneAsync(
|
|
ispezione.CodMdep,
|
|
ispezione.Data,
|
|
ispezione.Rilevatore,
|
|
apiResponse.ActivityIdIspezione
|
|
);
|
|
// Aggiorna l'id in memoria per l'iterazione successiva.
|
|
ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
|
|
|
if (scheda.ImageNames == null) continue;
|
|
|
|
var fileList = (await FileManager.GetInspectionFiles(ispezione, scheda.ImageNames))?
|
|
.Where(x => x.ToUpload).ToList();
|
|
|
|
if (fileList == null) continue;
|
|
|
|
var totFoto = fileList.Count;
|
|
var indiceFoto = 0;
|
|
|
|
foreach (var file in fileList)
|
|
{
|
|
indiceFoto++;
|
|
await SetProgress($"Scheda {indice}/{totale} · foto {indiceFoto}/{totFoto}…");
|
|
await IntegrySteupService.UploadFile(scheda.ActivityId!, file.FileBytes!, file.Name!);
|
|
}
|
|
}
|
|
|
|
// Integrità dati: se anche una sola scheda non è arrivata al server, NON completare.
|
|
// L'ispezione resta editabile e le schede fallite restano "Da inviare".
|
|
if (fallite > 0)
|
|
{
|
|
await InvokeAsync(() =>
|
|
{
|
|
_progressMessage = null;
|
|
StateHasChanged();
|
|
});
|
|
|
|
LoadSchede(); // rinfresca i badge: riuscite → "Inviato", fallite → restano "Da inviare"
|
|
|
|
await Dialog.ShowWarning(
|
|
fallite == 1
|
|
? "1 scheda non è stata inviata e resta salvata sul dispositivo. Controlla la connessione e riprova prima di concludere l'ispezione."
|
|
: $"{fallite} schede non sono state inviate e restano salvate sul dispositivo. Controlla la connessione e riprova prima di concludere l'ispezione."
|
|
);
|
|
return;
|
|
}
|
|
|
|
await SetProgress("Completamento ispezione…");
|
|
|
|
await IntegrySteupService.CompleteInspection(ispezione.ActivityId!);
|
|
await IspezioniService.UpdateStatoIspezioneAsync(
|
|
ispezione.CodMdep,
|
|
ispezione.Data,
|
|
ispezione.Rilevatore,
|
|
StatusEnum.Completata
|
|
);
|
|
|
|
ispezione.Stato = StatusEnum.Completata;
|
|
|
|
// Riscontro al momento più importante: la spunta di successo prima di chiudere l'overlay.
|
|
await InvokeAsync(() =>
|
|
{
|
|
_progressMessage = "Ispezione inviata";
|
|
_success = true;
|
|
StateHasChanged();
|
|
});
|
|
await Task.Delay(1800);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
OnError(e, e.Message);
|
|
}
|
|
finally
|
|
{
|
|
_isBusy = false;
|
|
_success = false;
|
|
_progressMessage = null;
|
|
await InvokeAsync(() =>
|
|
{
|
|
VisibleOverlay = false;
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
}
|
|
|
|
private Task SetProgress(string message) =>
|
|
InvokeAsync(() =>
|
|
{
|
|
_progressMessage = message;
|
|
StateHasChanged();
|
|
});
|
|
|
|
private void GroupSchede()
|
|
{
|
|
SchedeGrouped = SchedeList
|
|
.Where(s => s.Reparto != null)
|
|
.GroupBy(s => s.CodJfas)
|
|
.ToDictionary(
|
|
g => g.First().Reparto!,
|
|
g => g.ToList()
|
|
);
|
|
}
|
|
|
|
private void OnSchedaModified(Scheda obj)
|
|
{
|
|
var index = SchedeList.FindIndex(x => x.Id.Equals(obj.Id));
|
|
if (index >= 0) SchedeList[index] = obj;
|
|
|
|
GroupSchede();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void OnSchedaDeleted(Scheda obj)
|
|
{
|
|
SchedeList.Remove(obj);
|
|
GroupSchede();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task ExportReparto(JtbFasiDto jtbFasi)
|
|
{
|
|
// Guardia anti doppio-tap: un secondo tocco durante l'invio non ri-esporta il batch.
|
|
if (_isBusy) return;
|
|
|
|
var confirmed = await _exportRepartoBox.ShowAsync();
|
|
if (confirmed is not true) return;
|
|
|
|
_isBusy = true;
|
|
VisibleOverlay = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
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 inviare in questo reparto", Severity.Info);
|
|
return;
|
|
}
|
|
|
|
var saveRequest = daEsportare.ConvertAll(x =>
|
|
{
|
|
return new SaveRequestDto
|
|
{
|
|
LocalIdScheda = x.Id,
|
|
ActivityTypeId = x.ActivityTypeId,
|
|
CodJfas = x.CodJfas,
|
|
CodMdep = ispezione.CodMdep,
|
|
DataCreazione = ispezione.Data,
|
|
Note = x.Note,
|
|
PersonaRif = x.Responsabile,
|
|
Barcodes = x.Articoli.ConvertAll(y => y.Barcode),
|
|
Scandeza = (ScadenzaEnum)x.Scadenza,
|
|
// Aggancia all'ispezione esistente sul server (evita di crearne una nuova a ogni export).
|
|
ParentActivityId = ispezione.ActivityId
|
|
};
|
|
});
|
|
|
|
var apiResponse = await IntegrySteupService.SaveMultipleSchede(saveRequest);
|
|
|
|
if (apiResponse == null)
|
|
{
|
|
Snackbar.Add("Invio non riuscito. Le schede restano sul dispositivo, riprova.", Severity.Error);
|
|
return;
|
|
}
|
|
|
|
ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
|
|
|
await IspezioniService.UpdateActivityIdIspezioneAsync(ispezione.CodMdep, ispezione.Data,
|
|
UserSession.User.Username, apiResponse.ActivityIdIspezione
|
|
);
|
|
|
|
if (!apiResponse.ActivityIdSchedaList.IsNullOrEmpty())
|
|
{
|
|
foreach (var scheda in daEsportare)
|
|
{
|
|
var activityId = apiResponse.ActivityIdSchedaList!.Find(x =>
|
|
x.LocalId != null && x.LocalId == scheda.Id
|
|
)?.ActivityId;
|
|
|
|
if (activityId == null) continue;
|
|
|
|
scheda.ActivityId = activityId;
|
|
await IspezioniService.UpdateActivityIdSchedaAsync(scheda.Id, scheda.ActivityId);
|
|
}
|
|
}
|
|
|
|
Snackbar.Add(daEsportare.Count == 1 ? "Scheda inviata" : $"{daEsportare.Count} schede inviate",
|
|
Severity.Success);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
OnError(e, e.Message);
|
|
}
|
|
finally
|
|
{
|
|
_isBusy = false;
|
|
VisibleOverlay = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private void OnError(Exception? e, string? technicalMessage)
|
|
{
|
|
// Il dettaglio tecnico va nei log, non davanti al rilevatore in campo.
|
|
if (e != null) Logger.LogError(e, technicalMessage);
|
|
|
|
_ = Dialog.ShowError(
|
|
"Invio non riuscito. Il lavoro resta salvato sul dispositivo: controlla la connessione e riprova."
|
|
);
|
|
}
|
|
|
|
void IDisposable.Dispose()
|
|
{
|
|
NewScheda.OnNewScheda -= LoadSchede;
|
|
CompleteInspection.OnComplete -= HandleCompleteInspection;
|
|
}
|
|
|
|
}
|