Migliora IspezionePage e form scheda (design review impeccable)
Ispezione: - Stato sync visibile: badge per-scheda (Da inviare/Inviato) e contatore "N da inviare" per reparto (nuovo SchedaSyncHelper) - Completamento: progresso reale nell'overlay + spunta di successo; NON completa se una scheda fallisce (resta editabile, schede "Da inviare") - Export reparto/completamento blindati: conferma + guardia anti doppio-tap, try/finally che libera sempre l'overlay - Accento corallo unico per l'azione "invia"; target di tocco >=44px; empty state; solo il primo pannello espanso di default - Tasto "Invia" nascosto sulle schede gia inviate - Messaggio d'errore comprensibile in campo (dettaglio tecnico nei log) - Fix OnSchedaModified (index >= 0) Aspetto card: - SchedaCard e InspectionCard: superficie Carta + hairline border + ombra morbida (nuovo token --card-box-shadow); pagina con tinta Grigio Campo - SchedaCard compattata Form scheda (ModalFormScheda): - Tasto Salva/Aggiorna full-width - Fix "Suggerisci note descrittive": la nota selezionata ora viene inserita nel campo (metodo async sul thread UI + OnAfterChangeValue) StatusEnum: etichette in sentence-case Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,22 +3,37 @@
|
||||
<MudOverlay Visible="VisibleOverlay" LightBackground="true">
|
||||
@if (SuccessAnimation)
|
||||
{
|
||||
<div class="success-checkmark">
|
||||
<div class="check-icon">
|
||||
<span class="icon-line line-tip"></span>
|
||||
<span class="icon-line line-long"></span>
|
||||
<div class="icon-circle"></div>
|
||||
<div class="icon-fix"></div>
|
||||
<div class="overlay-content">
|
||||
<div class="success-checkmark">
|
||||
<div class="check-icon">
|
||||
<span class="icon-line line-tip"></span>
|
||||
<span class="icon-line line-long"></span>
|
||||
<div class="icon-circle"></div>
|
||||
<div class="icon-fix"></div>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<p class="overlay-message">@Message</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SpinnerLayout/>
|
||||
<div class="overlay-content">
|
||||
<SpinnerLayout/>
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<p class="overlay-message">@Message</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</MudOverlay>
|
||||
|
||||
@code {
|
||||
[Parameter] public required bool SuccessAnimation { get; set; }
|
||||
[Parameter] public required bool VisibleOverlay { get; set; }
|
||||
|
||||
/// Testo opzionale mostrato sotto lo spinner/spunta (es. "Invio scheda 3 di 7…").
|
||||
[Parameter] public string? Message { get; set; }
|
||||
}
|
||||
@@ -1,9 +1,37 @@
|
||||
.overlay-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 0 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overlay-message {
|
||||
margin: 0;
|
||||
max-width: 30ch;
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.4;
|
||||
/* Inchiostro su fondo chiaro dell'overlay: leggibile anche al sole */
|
||||
color: var(--mud-palette-text-primary);
|
||||
}
|
||||
|
||||
.success-checkmark {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Reduced motion: mostra la spunta di successo statica, senza coreografia */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.success-checkmark .check-icon::after,
|
||||
.icon-line.line-tip,
|
||||
.icon-line.line-long {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.success-checkmark .check-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using SteUp.Shared.Components.Layout
|
||||
@using SteUp.Shared.Components.Layout.Overlay
|
||||
@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
|
||||
@@ -28,36 +29,47 @@
|
||||
@if (!SchedeGrouped.IsNullOrEmpty())
|
||||
{
|
||||
<MudExpansionPanels MultiExpansion="true">
|
||||
@{
|
||||
var panelIndex = 0;
|
||||
}
|
||||
@foreach (var group in SchedeGrouped)
|
||||
{
|
||||
<MudExpansionPanel Class="expansion-panel" Expanded="true" Text="@group.Key.Descrizione">
|
||||
var isFirstPanel = panelIndex == 0;
|
||||
panelIndex++;
|
||||
<MudExpansionPanel Class="expansion-panel" Expanded="isFirstPanel" Text="@group.Key.Descrizione">
|
||||
<TitleContent>
|
||||
@{
|
||||
var daInviare = group.Value.CountDaInviare();
|
||||
}
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
@if (SteupDataService.InspectionPageState.Ispezione.Stato != StatusEnum.Completata)
|
||||
{
|
||||
<div class="action-scheda-group">
|
||||
@if (NetworkService.IsNetworkAvailable())
|
||||
{
|
||||
<MudFab StartIcon="@Icons.Material.Rounded.Add"
|
||||
Color="Color.Warning" Size="Size.Small"
|
||||
OnClick="@(() => CreateNewScheda(group.Key))"/>
|
||||
<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))"/>
|
||||
|
||||
<MudFab StartIcon="@Icons.Material.Rounded.CloudSync" Label="Esporta reparto"
|
||||
Color="Color.Success" Size="Size.Small"
|
||||
OnClick="@(() => ExportReparto(group.Key))"/>
|
||||
}
|
||||
else
|
||||
@if (NetworkService.IsNetworkAvailable() && daInviare > 0)
|
||||
{
|
||||
<MudFab StartIcon="@Icons.Material.Rounded.Add"
|
||||
Label="@($"Nuova scheda su {group.Key.Descrizione}")"
|
||||
Color="Color.Warning" Size="Size.Medium"
|
||||
OnClick="@(() => CreateNewScheda(group.Key))"/>
|
||||
<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>
|
||||
}
|
||||
@@ -75,15 +87,35 @@
|
||||
}
|
||||
</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>
|
||||
|
||||
<SpinnerOverlay VisibleOverlay="VisibleOverlay"/>
|
||||
<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?"/>
|
||||
|
||||
@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!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -110,6 +142,10 @@
|
||||
|
||||
private async void HandleCompleteInspection()
|
||||
{
|
||||
// Guardia anti doppio-invio: il completamento è irreversibile.
|
||||
if (_isBusy) return;
|
||||
_isBusy = true;
|
||||
|
||||
try
|
||||
{
|
||||
await InvokeAsync(() =>
|
||||
@@ -120,8 +156,16 @@
|
||||
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
|
||||
foreach (var scheda in ispezione.Schede.Where(x => x.ActivityId == null))
|
||||
var daInviare = ispezione.Schede.Where(x => x.ActivityId == null).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
|
||||
{
|
||||
@@ -138,7 +182,12 @@
|
||||
}
|
||||
);
|
||||
|
||||
if (apiResponse == null) continue;
|
||||
// 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);
|
||||
@@ -149,20 +198,47 @@
|
||||
ispezione.Rilevatore,
|
||||
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,
|
||||
@@ -170,29 +246,42 @@
|
||||
ispezione.Rilevatore,
|
||||
StatusEnum.Completata
|
||||
);
|
||||
|
||||
|
||||
SteupDataService.InspectionPageState.Ispezione.Stato = StatusEnum.Completata;
|
||||
|
||||
// Riscontro al momento più importante: la spunta di successo prima di chiudere l'overlay.
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
_progressMessage = "Ispezione inviata";
|
||||
_success = true;
|
||||
StateHasChanged();
|
||||
});
|
||||
await Task.Delay(1800);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
|
||||
OnError(e, e.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isBusy = false;
|
||||
_success = false;
|
||||
_progressMessage = null;
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
});
|
||||
|
||||
OnError(e, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private Task SetProgress(string message) =>
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
_progressMessage = message;
|
||||
StateHasChanged();
|
||||
});
|
||||
|
||||
private void GroupSchede()
|
||||
{
|
||||
SchedeGrouped = SchedeList
|
||||
@@ -207,7 +296,7 @@
|
||||
private void OnSchedaModified(Scheda obj)
|
||||
{
|
||||
var index = SchedeList.FindIndex(x => x.Id.Equals(obj.Id));
|
||||
if (index > 0) SchedeList[index] = obj;
|
||||
if (index >= 0) SchedeList[index] = obj;
|
||||
|
||||
GroupSchede();
|
||||
StateHasChanged();
|
||||
@@ -234,66 +323,83 @@
|
||||
|
||||
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();
|
||||
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
|
||||
var saveRequest = SchedeGrouped[jtbFasi].ConvertAll(x =>
|
||||
try
|
||||
{
|
||||
return new SaveRequestDto
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
|
||||
var saveRequest = SchedeGrouped[jtbFasi].ConvertAll(x =>
|
||||
{
|
||||
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,
|
||||
ParentActivityId = x.Ispezione?.ActivityId
|
||||
};
|
||||
});
|
||||
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,
|
||||
ParentActivityId = x.Ispezione?.ActivityId
|
||||
};
|
||||
});
|
||||
|
||||
var apiResponse = await IntegrySteupService.SaveMultipleSchede(saveRequest);
|
||||
var apiResponse = await IntegrySteupService.SaveMultipleSchede(saveRequest);
|
||||
|
||||
if (apiResponse != null)
|
||||
{
|
||||
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
||||
|
||||
await IspezioniService.UpdateActivityIdIspezioneAsync(ispezione.CodMdep, ispezione.Data,
|
||||
UserSession.User.Username, apiResponse.ActivityIdIspezione
|
||||
);
|
||||
|
||||
if (apiResponse.ActivityIdSchedaList.IsNullOrEmpty()) return;
|
||||
|
||||
foreach (var scheda in SchedeGrouped[jtbFasi])
|
||||
if (apiResponse != null)
|
||||
{
|
||||
scheda.ActivityId = apiResponse.ActivityIdScheda;
|
||||
await IspezioniService.UpdateActivityIdSchedaAsync(scheda.Id, scheda.ActivityId);
|
||||
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
||||
|
||||
var activityId = apiResponse.ActivityIdSchedaList!.Find(x =>
|
||||
x.LocalId != null && x.LocalId == scheda.Id
|
||||
)?.ActivityId;
|
||||
await IspezioniService.UpdateActivityIdIspezioneAsync(ispezione.CodMdep, ispezione.Data,
|
||||
UserSession.User.Username, apiResponse.ActivityIdIspezione
|
||||
);
|
||||
|
||||
if (activityId == null) return;
|
||||
if (!apiResponse.ActivityIdSchedaList.IsNullOrEmpty())
|
||||
{
|
||||
foreach (var scheda in SchedeGrouped[jtbFasi])
|
||||
{
|
||||
var activityId = apiResponse.ActivityIdSchedaList!.Find(x =>
|
||||
x.LocalId != null && x.LocalId == scheda.Id
|
||||
)?.ActivityId;
|
||||
|
||||
scheda.ActivityId = activityId;
|
||||
await IspezioniService.UpdateActivityIdSchedaAsync(scheda.Id, scheda.ActivityId);
|
||||
if (activityId == null) continue;
|
||||
|
||||
scheda.ActivityId = activityId;
|
||||
await IspezioniService.UpdateActivityIdSchedaAsync(scheda.Id, scheda.ActivityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
catch (Exception e)
|
||||
{
|
||||
OnError(e, e.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isBusy = false;
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnError(Exception? e, string? errorMessage)
|
||||
private void OnError(Exception? e, string? technicalMessage)
|
||||
{
|
||||
if (e != null) Logger.LogError(e, errorMessage);
|
||||
if (errorMessage == null) return;
|
||||
// Il dettaglio tecnico va nei log, non davanti al rilevatore in campo.
|
||||
if (e != null) Logger.LogError(e, technicalMessage);
|
||||
|
||||
_ = Dialog.ShowError(errorMessage);
|
||||
_ = Dialog.ShowError(
|
||||
"Invio non riuscito. Il lavoro resta salvato sul dispositivo: controlla la connessione e riprova."
|
||||
);
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
.ispezione ::deep .expansion-panel .mud-expand-panel-content {
|
||||
/* Tinta di fondo tenue: le card e i pannelli bianchi (Carta) risaltano sul Grigio Campo */
|
||||
.ispezione {
|
||||
background-color: var(--light-card-background);
|
||||
}
|
||||
|
||||
.ispezione ::deep .expansion-panel .mud-expand-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
@@ -7,5 +12,36 @@
|
||||
.action-scheda-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
justify-content: flex-end;
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: .75rem;
|
||||
margin-top: 3rem;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.empty-state .empty-state-icon {
|
||||
color: var(--mud-palette-primary);
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
.empty-state .empty-state-title {
|
||||
margin: 0;
|
||||
font-weight: 800;
|
||||
font-size: 1.25rem;
|
||||
color: var(--mud-palette-text-primary);
|
||||
}
|
||||
|
||||
.empty-state .empty-state-text {
|
||||
margin: 0;
|
||||
max-width: 34ch;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
color: var(--mud-palette-gray-darker);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
.container-primary-info {
|
||||
/*background: var(--light-card-background);*/
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 20px;
|
||||
background: var(--mud-palette-surface);
|
||||
border: 1px solid var(--card-border-color);
|
||||
box-shadow: var(--card-box-shadow);
|
||||
}
|
||||
|
||||
.container-primary-info.no-margin {
|
||||
@@ -42,7 +44,8 @@
|
||||
}
|
||||
|
||||
.info-subtitle {
|
||||
color: var(--mud-palette-gray-default);
|
||||
/* gray-darker (non gray-default) per superare il contrasto AA e restare leggibile al sole */
|
||||
color: var(--mud-palette-gray-darker);
|
||||
font-size: medium;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
|
||||
@@ -14,9 +14,19 @@
|
||||
<div class="scheda-card">
|
||||
<div class="scheda-body-section">
|
||||
<div class="title-section">
|
||||
<MudText Class="scheda-title" Typo="Typo.subtitle1">
|
||||
<b>@Scheda.ActivityTypeId</b>
|
||||
</MudText>
|
||||
<div class="scheda-title-group">
|
||||
<MudText Class="scheda-title" Typo="Typo.subtitle1">
|
||||
<b>@Scheda.ActivityTypeId</b>
|
||||
</MudText>
|
||||
|
||||
@{
|
||||
var syncState = Scheda.GetSyncState();
|
||||
}
|
||||
<MudChip T="string" Class="sync-badge" Icon="@syncState.GetIcon()"
|
||||
Size="Size.Small" Variant="Variant.Filled" Color="@syncState.GetColor()">
|
||||
@syncState.ToLabel()
|
||||
</MudChip>
|
||||
</div>
|
||||
|
||||
<div class="sub-info-section">
|
||||
<MudChip T="string" Icon="@Icons.Material.Rounded.PhotoCamera" Size="Size.Small" Color="Color.Default">
|
||||
@@ -31,29 +41,26 @@
|
||||
<div class="scheda-card-action">
|
||||
@if (Ispezione.Stato != StatusEnum.Completata)
|
||||
{
|
||||
if (NetworkService.IsNetworkAvailable())
|
||||
<div class="action-secondary">
|
||||
<MudFab Color="Color.Default" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.Edit"
|
||||
aria-label="Modifica scheda" OnClick="@UpdateScheda"/>
|
||||
<MudFab Color="Color.Error" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.Delete"
|
||||
aria-label="Elimina scheda" OnClick="@DeleteScheda"/>
|
||||
</div>
|
||||
@if (NetworkService.IsNetworkAvailable() && syncState == SyncState.DaInviare)
|
||||
{
|
||||
<MudFab Color="Color.Info" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Edit"
|
||||
OnClick="@UpdateScheda"/>
|
||||
<MudFab Color="Color.Error" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Delete"
|
||||
OnClick="@DeleteScheda"/>
|
||||
<MudFab Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Rounded.CloudSync"
|
||||
OnClick="@ExportScheda"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudFab Color="Color.Info" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Edit"
|
||||
OnClick="@UpdateScheda"/>
|
||||
<MudFab Color="Color.Error" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Delete"
|
||||
OnClick="@DeleteScheda"/>
|
||||
<MudFab Color="Color.Primary" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.CloudUpload"
|
||||
Label="Invia" aria-label="Invia scheda al server" OnClick="@ExportScheda"/>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudFab Color="Color.Info" Size="Size.Small" StartIcon="@Icons.Material.Rounded.RemoveRedEye"
|
||||
OnClick="@ViewScheda"/>
|
||||
<MudFab Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Rounded.CloudSync"
|
||||
OnClick="@ExportImg"/>
|
||||
<div class="action-secondary">
|
||||
<MudFab Color="Color.Default" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.RemoveRedEye"
|
||||
aria-label="Visualizza scheda" OnClick="@ViewScheda"/>
|
||||
</div>
|
||||
<MudFab Color="Color.Primary" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.CloudUpload"
|
||||
Label="Invia foto" aria-label="Invia foto al server" OnClick="@ExportImg"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,52 +119,74 @@
|
||||
|
||||
private async Task ExportScheda()
|
||||
{
|
||||
// Guardia anti doppio-tap: invio irreversibile al server.
|
||||
if (VisibleOverlay) return;
|
||||
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
|
||||
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,
|
||||
ParentActivityId = Scheda.Ispezione?.ActivityId
|
||||
}
|
||||
);
|
||||
|
||||
if (apiResponse != null)
|
||||
try
|
||||
{
|
||||
Scheda.ActivityId = apiResponse.ActivityIdScheda;
|
||||
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
||||
|
||||
await IspezioniService.UpdateActivityIdIspezioneAsync(Ispezione.CodMdep, Ispezione.Data,
|
||||
UserSession.User.Username, apiResponse.ActivityIdIspezione
|
||||
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,
|
||||
ParentActivityId = Scheda.Ispezione?.ActivityId
|
||||
}
|
||||
);
|
||||
await IspezioniService.UpdateActivityIdSchedaAsync(Scheda.Id, Scheda.ActivityId);
|
||||
}
|
||||
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
if (apiResponse != null)
|
||||
{
|
||||
Scheda.ActivityId = apiResponse.ActivityIdScheda;
|
||||
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
||||
|
||||
await IspezioniService.UpdateActivityIdIspezioneAsync(Ispezione.CodMdep, Ispezione.Data,
|
||||
UserSession.User.Username, apiResponse.ActivityIdIspezione
|
||||
);
|
||||
await IspezioniService.UpdateActivityIdSchedaAsync(Scheda.Id, Scheda.ActivityId);
|
||||
|
||||
// Notifica il genitore così il contatore "da inviare" del reparto si aggiorna.
|
||||
await OnSchedaModified.InvokeAsync(Scheda);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportImg()
|
||||
{
|
||||
if (Scheda.ImageNames == null) return;
|
||||
if (VisibleOverlay || Scheda.ImageNames == null) return;
|
||||
|
||||
var fileList = (await FileManager.GetInspectionFiles(Ispezione, Scheda.ImageNames))?
|
||||
.Where(x => x.ToUpload).ToList();
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
|
||||
if (fileList == null) return;
|
||||
|
||||
foreach (var file in fileList)
|
||||
try
|
||||
{
|
||||
await IntegrySteupService.UploadFile(Scheda.ActivityId!, file.FileBytes!, file.Name!);
|
||||
var fileList = (await FileManager.GetInspectionFiles(Ispezione, Scheda.ImageNames))?
|
||||
.Where(x => x.ToUpload).ToList();
|
||||
|
||||
if (fileList == null) return;
|
||||
|
||||
foreach (var file in fileList)
|
||||
{
|
||||
await IntegrySteupService.UploadFile(Scheda.ActivityId!, file.FileBytes!, file.Name!);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,48 @@
|
||||
.scheda-card{
|
||||
padding: .75rem 1.25rem;
|
||||
background-color: var(--light-card-background);
|
||||
padding: .55rem .85rem;
|
||||
background-color: var(--mud-palette-surface);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 1em;
|
||||
box-shadow: var(--card-box-shadow);
|
||||
}
|
||||
|
||||
.scheda-body-section{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.title-section{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.scheda-title-group{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: .15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sync-badge{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scheda-card-action{
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Azioni secondarie (modifica/elimina) raggruppate e ancorate a sinistra;
|
||||
l'azione primaria "Invia" (corallo) resta separata e a portata di pollice a destra. */
|
||||
.action-secondary{
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -187,7 +187,7 @@
|
||||
@if (!LabelSave.IsNullOrEmpty())
|
||||
{
|
||||
<div class="container-button">
|
||||
<MudButton OnClick="@Save" Color="Color.Primary" Variant="Variant.Filled">
|
||||
<MudButton OnClick="@Save" Color="Color.Primary" Variant="Variant.Filled" FullWidth="true">
|
||||
@LabelSave
|
||||
</MudButton>
|
||||
</div>
|
||||
@@ -626,7 +626,7 @@
|
||||
|
||||
#endregion
|
||||
|
||||
private void SuggestActivityDescription()
|
||||
private async Task SuggestActivityDescription()
|
||||
{
|
||||
if (Scheda.ActivityTypeId == null)
|
||||
{
|
||||
@@ -637,18 +637,25 @@
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
try
|
||||
{
|
||||
var activityDescriptions = await IntegryApiService.SuggestActivityDescription(Scheda.ActivityTypeId);
|
||||
|
||||
var modal = await Dialog.OpenSuggestActivityDescription(activityDescriptions);
|
||||
|
||||
if (modal is { Canceled: false, Data: not null })
|
||||
Scheda.Note = modal.Data!.ToString();
|
||||
|
||||
if (modal is { Canceled: false, Data: string descrizione } && !string.IsNullOrWhiteSpace(descrizione))
|
||||
{
|
||||
Scheda.Note = descrizione;
|
||||
// Mostra la nota selezionata nel campo e aggiorna lo stato (dirty → Salva/Aggiorna),
|
||||
// come farebbe l'input manuale dell'utente.
|
||||
OnAfterChangeValue();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#region Scanner
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using MudBlazor;
|
||||
using SteUp.Shared.Core.Entities;
|
||||
|
||||
namespace SteUp.Shared.Core.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Stato di sincronizzazione di una scheda rispetto al server Integry.
|
||||
/// Principio "Offline è la verità": il rilevatore deve sempre vedere cosa è
|
||||
/// ancora solo sul dispositivo e cosa è già arrivato al server.
|
||||
/// </summary>
|
||||
public enum SyncState
|
||||
{
|
||||
/// Salvata solo in locale, non ancora inviata al server.
|
||||
DaInviare = 0,
|
||||
|
||||
/// Inviata al server (ha un ActivityId).
|
||||
Inviato = 1
|
||||
}
|
||||
|
||||
public static class SchedaSyncHelper
|
||||
{
|
||||
public static SyncState GetSyncState(this Scheda scheda) =>
|
||||
string.IsNullOrEmpty(scheda.ActivityId) ? SyncState.DaInviare : SyncState.Inviato;
|
||||
|
||||
public static string ToLabel(this SyncState state) =>
|
||||
state switch
|
||||
{
|
||||
SyncState.DaInviare => "Da inviare",
|
||||
SyncState.Inviato => "Inviato",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Colore del badge. Corallo (Primary) = azione richiesta (da inviare);
|
||||
/// verde (Success) = fatto (inviato). Rispetta la Regola dell'Unico Accento.
|
||||
/// </summary>
|
||||
public static Color GetColor(this SyncState state) =>
|
||||
state switch
|
||||
{
|
||||
SyncState.DaInviare => Color.Primary,
|
||||
SyncState.Inviato => Color.Success,
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
public static string GetIcon(this SyncState state) =>
|
||||
state switch
|
||||
{
|
||||
SyncState.DaInviare => Icons.Material.Rounded.PhonelinkOff,
|
||||
SyncState.Inviato => Icons.Material.Rounded.CloudDone,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
/// Numero di schede non ancora inviate in una lista.
|
||||
public static int CountDaInviare(this IEnumerable<Scheda> schede) =>
|
||||
schede.Count(s => s.GetSyncState() == SyncState.DaInviare);
|
||||
}
|
||||
@@ -7,12 +7,13 @@ public static class StatusEnumHelper
|
||||
{
|
||||
public static string ConvertToHumanReadable(this StatusEnum enumValue)
|
||||
{
|
||||
// Sentence-case: l'enfasi la dà il peso del chip, non il maiuscolo (Regola del Peso, non del Maiuscolo).
|
||||
return enumValue switch
|
||||
{
|
||||
StatusEnum.InCorso => "IN CORSO",
|
||||
StatusEnum.Completata => "COMPLETATA",
|
||||
StatusEnum.Verifica => "VERIFICA",
|
||||
StatusEnum.Annullata => "ANNULLATA",
|
||||
StatusEnum.InCorso => "In corso",
|
||||
StatusEnum.Completata => "Completata",
|
||||
StatusEnum.Verifica => "Verifica",
|
||||
StatusEnum.Annullata => "Annullata",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(enumValue), enumValue, null)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
/*Utility*/
|
||||
--exception-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.3);
|
||||
--custom-box-shadow: 1px 2px 5px var(--gray-for-shadow);
|
||||
/* Ombra morbida a due livelli, tinta col navy del brand: stacca le card dallo sfondo senza pesare */
|
||||
--card-box-shadow: 0 1px 2px rgba(0, 35, 57, 0.08), 0 6px 16px rgba(0, 35, 57, 0.10);
|
||||
--mud-default-borderradius: 20px !important;
|
||||
--m-page-x: 1rem;
|
||||
--mh-header: 4rem;
|
||||
|
||||
Reference in New Issue
Block a user