101b68123c
La MudSelect apriva un riquadro bianco che galleggiava sopra le card bianche, senza titolo, senza sfondo oscurato, con la barra di scorrimento del desktop e le righe alte quanto una riga di testo. Era la cosa che piu' di tutte faceva sembrare l'app una pagina web. Ora il campo e' una riga tappabile che mostra il valore, e la scelta arriva da un bottom sheet con il titolo del campo e righe da 56 px con il segno tondo della scelta singola: la stessa forma degli altri selettori dell'app, usabile in piedi e con i guanti. Il confronto per capire quale opzione e' quella corrente passa per l'etichetta: Scheda.Reparto ricostruisce l'oggetto al volo da codice e descrizione, quindi non e' mai lo stesso oggetto di quelli in elenco e il confronto di riferimento non avrebbe segnato mai nulla. Reparto e motivo non passano piu' dalla validazione di MudForm, perche' non sono piu' input: il controllo degli obbligatori e' esplicito e l'errore si vede sul gruppo interessato, non solo nel messaggio in cima. Si accende dopo il primo tentativo di salvataggio, non prima: un form rosso all'apertura accusa l'utente di un errore che non ha ancora commesso. La scelta fatta si segna col tono informativo: un bordo corallo intorno a un riempimento azzurro si leggeva come un errore. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1023 lines
38 KiB
Plaintext
1023 lines
38 KiB
Plaintext
@using Microsoft.Extensions.Logging
|
|
@using SteUp.Shared.Components.Layout
|
|
@using SteUp.Shared.Components.Layout.Overlay
|
|
@using SteUp.Shared.Components.Layout.Spinner
|
|
@using SteUp.Shared.Components.SingleElements.Card.ModalForm
|
|
@using SteUp.Shared.Components.SingleElements.MessageBox
|
|
@using SteUp.Shared.Components.SingleElements.Picker
|
|
@using SteUp.Shared.Core.Dto
|
|
@using SteUp.Shared.Core.Entities
|
|
@using SteUp.Shared.Core.Enum
|
|
@using SteUp.Shared.Core.Interface.IntegryApi
|
|
@using SteUp.Shared.Core.Interface.LocalDb
|
|
@using SteUp.Shared.Core.Interface.System
|
|
@using SteUp.Shared.Core.Messages.Scanner
|
|
@using SteUp.Shared.Core.Messages.System
|
|
@inject BackButtonService BackButton
|
|
@inject INetworkService NetworkService
|
|
@inject IDialogService Dialog
|
|
@inject IIntegryApiService IntegryApiService
|
|
@inject IFileManager FileManager
|
|
@inject IIspezioniService IspezioniService
|
|
@inject IIntegrySteupService IntegrySteupService
|
|
@inject OnScannerService OnScannerService
|
|
@inject IAttachedService AttachedService
|
|
@inject ILogger<ModalFormScheda> Logger
|
|
@implements IDisposable
|
|
|
|
@*
|
|
Form scheda: la schermata dove il rilevatore passa la maggior parte del
|
|
turno. Struttura a shell — testata, corpo scorrevole, barra azioni ancorata
|
|
in fondo (STILE_UI §6.13) — così il salvataggio resta sempre sotto il
|
|
pollice invece di stare in fondo a uno scroll lungo.
|
|
*@
|
|
|
|
<MudDialog Class="customDialog-form">
|
|
<DialogContent>
|
|
<MudForm ReadOnly="ReadOnly" @ref="_form">
|
|
<div class="app-shell">
|
|
|
|
<HeaderLayout Title="@(IsNew ? "Nuova scheda" : ReadOnly ? "Scheda" : "Modifica scheda")"
|
|
Subtitle="@Scheda.DescrizioneReparto"
|
|
Cancel="true" OnCancel="@Cancel" Compact="true"/>
|
|
|
|
<div class="app-scroll">
|
|
<div class="page-width stack stack-10" style="padding-bottom: var(--space-lg);">
|
|
|
|
@if (IsBozza)
|
|
{
|
|
<div class="feedback feedback--warning" role="status">
|
|
<i class="feedback__icon ri-edit-line" aria-hidden="true"></i>
|
|
<span>Questa scheda è una bozza: completala per poterla inviare.</span>
|
|
</div>
|
|
}
|
|
|
|
<CardFormModal Title="Reparto" Loading="SteupDataService.Reparti.IsNullOrEmpty()"
|
|
Class="@(_showErrors && RepartoMancante ? "form-group--invalid" : "")">
|
|
<SelectField TValue="JtbFasiDto" Title="Reparto"
|
|
Items="SteupDataService.Reparti"
|
|
ItemLabel="@(x => x.Descrizione)"
|
|
Value="Scheda.Reparto" ValueChanged="OnRepartoChanged"
|
|
Placeholder="Scegli il reparto" ReadOnly="ReadOnly"/>
|
|
|
|
@if (_showErrors && RepartoMancante)
|
|
{
|
|
<p class="form-error" role="alert">
|
|
<i class="ri-error-warning-line" aria-hidden="true"></i>
|
|
Indica il reparto
|
|
</p>
|
|
}
|
|
</CardFormModal>
|
|
|
|
<CardFormModal Title="Motivo" Loading="SteupDataService.TipiAttività.IsNullOrEmpty()"
|
|
Class="@(_showErrors && MotivoMancante ? "form-group--invalid" : "")">
|
|
@if (Scheda.CodJfas.IsNullOrEmpty())
|
|
{
|
|
<p class="t-meta">Scegli prima il reparto.</p>
|
|
}
|
|
else
|
|
{
|
|
<SelectField TValue="string" Title="Motivo"
|
|
Subtitle="@Scheda.DescrizioneReparto"
|
|
Items="MotiviDelReparto"
|
|
ItemLabel="@(x => x)"
|
|
Value="Scheda.ActivityTypeId" ValueChanged="OnMotivoChanged"
|
|
Placeholder="Scegli il motivo" ReadOnly="ReadOnly"/>
|
|
|
|
@if (_showErrors && MotivoMancante)
|
|
{
|
|
<p class="form-error" role="alert">
|
|
<i class="ri-error-warning-line" aria-hidden="true"></i>
|
|
Indica il motivo
|
|
</p>
|
|
}
|
|
}
|
|
</CardFormModal>
|
|
|
|
@* --- Articoli: il compito primario della schermata --- *@
|
|
<CardFormModal Title="Articoli" Count="@ArticoliCount">
|
|
@if (!ReadOnly)
|
|
{
|
|
<div class="barcode-field">
|
|
<input type="text" inputmode="numeric" placeholder="Digita o scansiona un codice"
|
|
aria-label="Codice a barre"
|
|
value="@ManualBarcode" @oninput="OnManualBarcodeInput"
|
|
@onkeydown="OnManualBarcodeKey"/>
|
|
<button type="button" class="barcode-field__send"
|
|
disabled="@(ManualBarcode.IsNullOrEmpty())"
|
|
aria-label="Aggiungi il codice" @onclick="OnManualBarcodeSet">
|
|
<i class="ri-arrow-right-line" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
@if (!Scheda.Articoli.IsNullOrEmpty())
|
|
{
|
|
<div class="art-list" style="margin-top: var(--space-sm);">
|
|
@foreach (var articolo in Scheda.Articoli)
|
|
{
|
|
<div class="art-item" @key="articolo.Barcode">
|
|
<div class="art-item__text">
|
|
<div class="art-item__name">@articolo.Descrizione</div>
|
|
<div class="art-item__code selectable">@articolo.Barcode</div>
|
|
</div>
|
|
@if (!ReadOnly)
|
|
{
|
|
<button type="button" class="art-item__remove"
|
|
aria-label="@($"Rimuovi {articolo.Descrizione}")"
|
|
@onclick="@(() => RemoveArt(articolo.Barcode))">
|
|
<i class="ri-close-line" aria-hidden="true"></i>
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else if (ReadOnly)
|
|
{
|
|
<p class="t-meta">Nessun articolo.</p>
|
|
}
|
|
|
|
@if (NetworkService.ConnectionAvailable && !ReadOnly)
|
|
{
|
|
<button type="button" class="action-tile" style="margin-top: var(--space-sm);"
|
|
@onclick="@OpenSelectArt">
|
|
<span class="action-tile__icon action-tile__icon--neutral">
|
|
<i class="ri-list-check-2" aria-hidden="true"></i>
|
|
</span>
|
|
<span class="action-tile__label">Scegli dalla griglia</span>
|
|
<i class="nav-list__chevron ri-arrow-right-s-line" aria-hidden="true"></i>
|
|
</button>
|
|
}
|
|
</CardFormModal>
|
|
|
|
@* --- Foto --- *@
|
|
<CardFormModal Title="Foto" Count="@FotoCount" Loading="FileLoading">
|
|
@if (!AttachedList.IsNullOrEmpty())
|
|
{
|
|
<div class="attach-strip">
|
|
@foreach (var item in AttachedList!.Select((p, index) => new { p, index }))
|
|
{
|
|
<div class="attach-item">
|
|
@if (!item.p.ThumbPath.IsNullOrEmpty())
|
|
{
|
|
<img class="attach-item__img" src="@item.p.ThumbPath"
|
|
alt="@item.p.Name"/>
|
|
}
|
|
else
|
|
{
|
|
<div class="attach-item__placeholder">
|
|
<span class="spinner" aria-label="Elaborazione"></span>
|
|
</div>
|
|
}
|
|
|
|
@if (item.p.ToUpload && !ReadOnly)
|
|
{
|
|
<button type="button" class="attach-item__remove"
|
|
aria-label="@($"Rimuovi la foto {item.p.Name}")"
|
|
@onclick="@(() => OnRemoveAttached(item.index))">
|
|
<i class="ri-close-line" aria-hidden="true"></i>
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else if (ReadOnly)
|
|
{
|
|
<p class="t-meta">Nessuna foto.</p>
|
|
}
|
|
|
|
@if (!ReadOnly)
|
|
{
|
|
<button type="button" class="action-tile" style="margin-top: var(--space-sm);"
|
|
@onclick="@OpenAddAttached">
|
|
<span class="action-tile__icon action-tile__icon--primary">
|
|
<i class="ri-camera-line" aria-hidden="true"></i>
|
|
</span>
|
|
<span class="action-tile__label">Aggiungi una foto</span>
|
|
</button>
|
|
}
|
|
</CardFormModal>
|
|
|
|
@* --- Urgenza: pillole invece di un menu a tendina --- *@
|
|
<CardFormModal Title="Entro quando">
|
|
<div class="option-row">
|
|
@foreach (var opzione in ScadenzaOptions)
|
|
{
|
|
<button type="button" class="option"
|
|
disabled="@ReadOnly"
|
|
aria-pressed="@(Scheda.Scadenza == opzione.Value ? "true" : "false")"
|
|
@onclick="@(() => SetScadenza(opzione.Value))">
|
|
@opzione.Label
|
|
</button>
|
|
}
|
|
</div>
|
|
</CardFormModal>
|
|
|
|
<CardFormModal Title="Persona di riferimento">
|
|
<MudTextField FullWidth="true" T="string?" Variant="Variant.Text"
|
|
Placeholder="Nome di chi ha seguito il controllo"
|
|
@bind-Value="Scheda.Responsabile" @bind-Value:after="OnAfterChangeValue"
|
|
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
|
|
</CardFormModal>
|
|
|
|
<CardFormModal Title="Note">
|
|
<MudTextField @key="_noteFieldKey" T="string?" Variant="Variant.Text" Lines="3"
|
|
Placeholder="Cosa hai rilevato"
|
|
@bind-Value="Scheda.Note" @bind-Value:after="OnAfterChangeValue"
|
|
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
|
|
|
|
@if (!ReadOnly)
|
|
{
|
|
<button type="button" class="action-tile" style="margin-top: var(--space-sm);"
|
|
@onclick="@SuggestActivityDescription">
|
|
<span class="action-tile__icon action-tile__icon--info">
|
|
<i class="ri-lightbulb-line" aria-hidden="true"></i>
|
|
</span>
|
|
<span class="action-tile__label">Usa una nota ricorrente</span>
|
|
<i class="nav-list__chevron ri-arrow-right-s-line" aria-hidden="true"></i>
|
|
</button>
|
|
}
|
|
</CardFormModal>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!ReadOnly)
|
|
{
|
|
@* Barra azioni ancorata: il salvataggio non si cerca mai. *@
|
|
<div class="action-bar">
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
|
Disabled="@(!IsDirty)" OnClick="@Submit">
|
|
@(LabelSave ?? (IsNew ? "Salva" : "Aggiorna"))
|
|
</MudButton>
|
|
</div>
|
|
}
|
|
</div>
|
|
</MudForm>
|
|
|
|
<ConfirmUpdateActivity @ref="_confirmUpdateMessage"/>
|
|
</DialogContent>
|
|
</MudDialog>
|
|
|
|
<SpinnerOverlay VisibleOverlay="VisibleOverlay" SuccessAnimation="SuccessAnimation"/>
|
|
<SpinnerOverlay VisibleOverlay="OnLoading" Message="@TextLoading"/>
|
|
|
|
@code {
|
|
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
|
[Parameter] public required string CodMdep { get; set; }
|
|
[Parameter] public required DateTime Data { get; set; }
|
|
[Parameter] public bool IsNew { get; set; }
|
|
[Parameter] public bool ReadOnly { get; set; }
|
|
[Parameter] public Scheda Scheda { get; set; } = new();
|
|
|
|
private string? ManualBarcode { get; set; }
|
|
|
|
private bool IsBozza => Scheda.Bozza;
|
|
|
|
private string ArticoliCount => Scheda.Articoli.Count == 0
|
|
? "nessuno"
|
|
: Scheda.Articoli.Count == 1 ? "1" : Scheda.Articoli.Count.ToString();
|
|
|
|
private string FotoCount => AttachedList is null or { Count: 0 }
|
|
? "nessuna"
|
|
: AttachedList.Count.ToString();
|
|
|
|
// I valori sono quelli di ScadenzaEnum. Come pillole invece che in un menu
|
|
// a tendina: si scelgono con un tocco solo e si vede subito quale e' attiva.
|
|
private static readonly (int Value, string Label)[] ScadenzaOptions =
|
|
[
|
|
(3, "24 ore"),
|
|
(2, "1 settimana"),
|
|
(1, "1 mese"),
|
|
(0, "2 mesi")
|
|
];
|
|
|
|
private void SetScadenza(int value)
|
|
{
|
|
if (ReadOnly) return;
|
|
|
|
Scheda.Scadenza = value;
|
|
OnAfterChangeValue();
|
|
}
|
|
|
|
private void OnManualBarcodeInput(ChangeEventArgs e)
|
|
{
|
|
ManualBarcode = e.Value?.ToString();
|
|
}
|
|
|
|
// Invio da tastiera: il codice digitato si conferma senza cercare il pulsante.
|
|
private void OnManualBarcodeKey(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key is "Enter" or "NumpadEnter") OnManualBarcodeSet();
|
|
}
|
|
|
|
//Overlay
|
|
private bool VisibleOverlay { get; set; }
|
|
private bool SuccessAnimation { get; set; }
|
|
private bool OnLoading { get; set; }
|
|
private string? TextLoading { get; set; }
|
|
|
|
private bool FileLoading { get; set; }
|
|
|
|
private ConfirmUpdateActivity _confirmUpdateMessage = null!;
|
|
private MudForm _form = null!;
|
|
|
|
private string? LabelSave { get; set; }
|
|
// Cambiare questa chiave forza Blazor a ricreare il MudTextField delle note,
|
|
// così una nota impostata da codice (suggerimenti) compare subito a video
|
|
// nonostante il DebounceInterval del campo.
|
|
private int _noteFieldKey;
|
|
private bool IsDirty { get; set; }
|
|
private Scheda _originalScheda = null!;
|
|
// Clone() e' un MemberwiseClone, quindi _originalScheda condivide la stessa lista
|
|
// Articoli: per accorgerci degli articoli aggiunti/rimossi serve uno snapshot a parte.
|
|
private List<string> _originalBarcodes = [];
|
|
// Gli allegati vivono in AttachedList finche' non si salva: li tracciamo con un flag.
|
|
private bool _attachmentsDirty;
|
|
private List<AttachedDto>? AttachedList { get; set; }
|
|
|
|
private IDisposable? _backRegistration;
|
|
// Il tasto indietro puo' arrivare mentre il prompt di uscita e' gia' a video.
|
|
private bool _closing;
|
|
// Dialog aperti sopra al form (articoli, suggerimenti, errori).
|
|
private int _childDialogs;
|
|
|
|
private Task HandleBackPressed()
|
|
{
|
|
// Con un dialog figlio a video il back non deve agire sul form sottostante:
|
|
// meglio ignorarlo che far comparire il prompt di uscita sotto un altro dialog.
|
|
if (_childDialogs > 0) return Task.CompletedTask;
|
|
|
|
return Cancel();
|
|
}
|
|
|
|
private async Task<T> WithChildDialog<T>(Func<Task<T>> open)
|
|
{
|
|
_childDialogs++;
|
|
try
|
|
{
|
|
return await open();
|
|
}
|
|
finally
|
|
{
|
|
_childDialogs--;
|
|
}
|
|
}
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
|
|
|
|
OnScannerService.OnNewScanSuccessful += HandleNewScanSuccessful;
|
|
OnScannerService.OnErrorScan += OnErrorScan;
|
|
|
|
_originalScheda = Scheda.Clone();
|
|
_originalBarcodes = Scheda.Articoli.ConvertAll(x => x.Barcode);
|
|
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
|
|
|
|
// Il tasto indietro del dispositivo deve passare da qui, non mandare l'app
|
|
// in background lasciando il lavoro non salvato solo in memoria.
|
|
_backRegistration = BackButton.Register(() => InvokeAsync(HandleBackPressed));
|
|
|
|
LoadAttached();
|
|
}
|
|
|
|
private void LoadAttached()
|
|
{
|
|
if (Scheda.ImageNames == null) return;
|
|
|
|
FileLoading = true;
|
|
StateHasChanged();
|
|
|
|
Task.Run(async () =>
|
|
{
|
|
var fileList = await FileManager.GetInspectionFiles(
|
|
new Ispezione
|
|
{
|
|
CodMdep = CodMdep,
|
|
Data = Data,
|
|
Rilevatore = UserSession.User.Username
|
|
},
|
|
Scheda.ImageNames
|
|
);
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
AttachedList = fileList;
|
|
FileLoading = false;
|
|
|
|
StateHasChanged();
|
|
});
|
|
});
|
|
}
|
|
|
|
private async Task Save()
|
|
{
|
|
VisibleOverlay = true;
|
|
StateHasChanged();
|
|
|
|
// Salvataggio completo: la scheda non è più una bozza.
|
|
Scheda.Bozza = false;
|
|
|
|
SaveSchedaResponseDto? apiResponse = null;
|
|
try
|
|
{
|
|
if (NetworkService.ConnectionAvailable)
|
|
{
|
|
apiResponse = await IntegrySteupService.SaveScheda(
|
|
new SaveRequestDto
|
|
{
|
|
LocalIdScheda = Scheda.Id,
|
|
ActivityId = Scheda.ActivityId,
|
|
ActivityTypeId = Scheda.ActivityTypeId,
|
|
CodJfas = Scheda.CodJfas,
|
|
CodMdep = CodMdep,
|
|
DataCreazione = Data,
|
|
Note = Scheda.Note,
|
|
PersonaRif = Scheda.Responsabile,
|
|
Barcodes = Scheda.Articoli.ConvertAll(x => x.Barcode),
|
|
Scandeza = (ScadenzaEnum)Scheda.Scadenza,
|
|
ParentActivityId = SteupDataService.InspectionPageState.Ispezione.ActivityId
|
|
}
|
|
);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
var message = e.Message;
|
|
|
|
Logger.LogError(e, message);
|
|
Console.WriteLine(message);
|
|
await Dialog.ShowError(message);
|
|
}
|
|
|
|
if (IsNew) await NewSave(apiResponse);
|
|
else await Update(apiResponse);
|
|
|
|
if (Scheda.ActivityId.IsValorized()) await UploadFile(Scheda);
|
|
|
|
await FileManager.CleanTempStorageAsync();
|
|
|
|
SuccessAnimation = true;
|
|
StateHasChanged();
|
|
|
|
await Task.Delay(1250);
|
|
DisposeMessage();
|
|
MudDialog.Close(Scheda);
|
|
}
|
|
|
|
private async Task NewSave(SaveSchedaResponseDto? apiResponse)
|
|
{
|
|
if (!AttachedList.IsNullOrEmpty())
|
|
{
|
|
foreach (var attached in AttachedList!)
|
|
{
|
|
var fileNameAdded = await FileManager.SaveInspectionFile(
|
|
new Ispezione
|
|
{
|
|
CodMdep = CodMdep,
|
|
Data = Data,
|
|
Rilevatore = UserSession.User.Username
|
|
},
|
|
attached.FileBytes!,
|
|
attached.Name!
|
|
);
|
|
|
|
Scheda.ImageNames ??= [];
|
|
if (fileNameAdded != null)
|
|
Scheda.ImageNames.Add(fileNameAdded);
|
|
}
|
|
}
|
|
|
|
if (apiResponse != null)
|
|
{
|
|
await IspezioniService.UpdateActivityIdIspezioneAsync(CodMdep, Data,
|
|
UserSession.User.Username, apiResponse.ActivityIdIspezione
|
|
);
|
|
// Tiene in memoria l'ActivityId dell'ispezione: il prossimo salvataggio nella stessa
|
|
// sessione lo userà come ParentActivityId invece di creare una nuova ispezione.
|
|
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
|
Scheda.ActivityId = apiResponse.ActivityIdScheda;
|
|
}
|
|
|
|
await IspezioniService.AddSchedaAsync(CodMdep, Data, UserSession.User.Username, Scheda);
|
|
}
|
|
|
|
private async Task Update(SaveSchedaResponseDto? apiResponse, bool preserveActivityId = false)
|
|
{
|
|
if (!AttachedList.IsNullOrEmpty())
|
|
{
|
|
foreach (var attached in AttachedList!.Where(x => !x.SavedOnAppData))
|
|
{
|
|
var ispezione = new Ispezione
|
|
{
|
|
CodMdep = CodMdep,
|
|
Data = Data,
|
|
Rilevatore = UserSession.User.Username
|
|
};
|
|
|
|
if (!attached.ToRemove)
|
|
{
|
|
var fileNameAdded = await FileManager.SaveInspectionFile(
|
|
ispezione,
|
|
attached.FileBytes!,
|
|
attached.Name!
|
|
);
|
|
|
|
Scheda.ImageNames ??= [];
|
|
if (fileNameAdded != null)
|
|
Scheda.ImageNames.Add(fileNameAdded);
|
|
}
|
|
else
|
|
_ = FileManager.RemoveInspectionFile(ispezione, attached.Name!);
|
|
}
|
|
}
|
|
|
|
// Salvando una bozza non tocchiamo l'ActivityId: se la scheda era già sul server,
|
|
// al completamento va aggiornata, non ricreata.
|
|
if (!preserveActivityId) Scheda.ActivityId = apiResponse?.ActivityIdScheda;
|
|
|
|
await IspezioniService.UpdateSchedaAsync(Scheda);
|
|
|
|
if (apiResponse != null)
|
|
{
|
|
await IspezioniService.UpdateActivityIdIspezioneAsync(CodMdep, Data,
|
|
UserSession.User.Username, apiResponse.ActivityIdIspezione
|
|
);
|
|
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
|
|
}
|
|
}
|
|
|
|
private async Task UploadFile(Scheda scheda)
|
|
{
|
|
if (AttachedList.IsNullOrEmpty()) return;
|
|
|
|
var ispezione = new Ispezione
|
|
{
|
|
CodMdep = CodMdep,
|
|
Data = Data,
|
|
Rilevatore = UserSession.User.Username
|
|
};
|
|
|
|
foreach (var file in AttachedList!.Where(x => x.ToUpload))
|
|
{
|
|
if (file.FileBytes == null || file.Name == null) continue;
|
|
|
|
await IntegrySteupService.UploadFile(scheda.ActivityId!, file.FileBytes, file.Name);
|
|
var newPath = await FileManager.MoveInspectionFileFromToUploadToFinal(ispezione, file.Name);
|
|
if (newPath == null) continue;
|
|
|
|
var filePathToRemove = FileManager.GetFileToUploadDir(ispezione, file.Name);
|
|
scheda.ImageNames!.Remove(filePathToRemove);
|
|
scheda.ImageNames.Add(newPath);
|
|
}
|
|
|
|
await IspezioniService.UpdateFileListSchedaAsync(scheda.Id, scheda.ImageNames);
|
|
}
|
|
|
|
private async Task Cancel()
|
|
{
|
|
if (_closing) return;
|
|
_closing = true;
|
|
|
|
try
|
|
{
|
|
if (await CheckSavePreAction())
|
|
{
|
|
await FileManager.CleanTempStorageAsync();
|
|
DisposeMessage();
|
|
MudDialog.Cancel();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_closing = false;
|
|
}
|
|
}
|
|
|
|
#region Form
|
|
|
|
/// Gli obbligatori si segnalano solo dopo il primo tentativo di salvataggio:
|
|
/// un form che si tinge di rosso prima che tu abbia fatto qualcosa accusa
|
|
/// l'utente di un errore che non ha ancora commesso.
|
|
private bool _showErrors;
|
|
|
|
private bool RepartoMancante => Scheda.Reparto is null;
|
|
private bool MotivoMancante => Scheda.ActivityTypeId.IsNullOrEmpty();
|
|
|
|
/// I motivi disponibili dipendono dal reparto scelto.
|
|
private List<string> MotiviDelReparto => SteupDataService.TipiAttività
|
|
.Where(x => x.CodJfas.EqualsIgnoreCase(Scheda.CodJfas!))
|
|
.Select(x => x.ActivityTypeId)
|
|
.Where(x => !x.IsNullOrEmpty())
|
|
.Select(x => x!)
|
|
.ToList();
|
|
|
|
private void OnRepartoChanged(JtbFasiDto? reparto)
|
|
{
|
|
if (reparto is null) return;
|
|
|
|
Scheda.Reparto = reparto;
|
|
// Cambiando reparto il motivo precedente non e' piu' fra quelli validi.
|
|
Scheda.ActivityTypeId = null;
|
|
OnAfterChangeValue();
|
|
}
|
|
|
|
private void OnMotivoChanged(string? motivo)
|
|
{
|
|
Scheda.ActivityTypeId = motivo;
|
|
OnAfterChangeValue();
|
|
}
|
|
|
|
private void OnAfterChangeValue()
|
|
{
|
|
RecalcDirty();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void RecalcDirty()
|
|
{
|
|
IsDirty = _attachmentsDirty
|
|
|| !ValueComparer.AreEqual(Scheda, _originalScheda)
|
|
|| !BarcodesUnchanged();
|
|
|
|
if (IsDirty) LabelSave = !IsNew ? "Aggiorna" : "Salva";
|
|
else LabelSave = null;
|
|
}
|
|
|
|
private bool BarcodesUnchanged()
|
|
{
|
|
if (Scheda.Articoli.Count != _originalBarcodes.Count) return false;
|
|
|
|
for (var i = 0; i < _originalBarcodes.Count; i++)
|
|
{
|
|
if (!Scheda.Articoli[i].Barcode.Equals(_originalBarcodes[i], StringComparison.Ordinal))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static class ValueComparer
|
|
{
|
|
public static bool AreEqual(Scheda? a, Scheda? b)
|
|
{
|
|
if (a is null || b is null) return a == b;
|
|
|
|
return
|
|
a.CodJfas == b.CodJfas &&
|
|
a.DescrizioneReparto == b.DescrizioneReparto &&
|
|
a.ActivityTypeId == b.ActivityTypeId &&
|
|
a.Note == b.Note &&
|
|
a.Responsabile == b.Responsabile &&
|
|
a.Scadenza == b.Scadenza;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ritorna true se il form può chiudersi scartando (nessuna modifica in sospeso).
|
|
/// Con del lavoro in corso non si perde mai nulla: o si salva per intero, o si
|
|
/// salva come bozza da riprendere.
|
|
/// </summary>
|
|
private async Task<bool> CheckSavePreAction()
|
|
{
|
|
if (!IsDirty) return true;
|
|
|
|
var result = await WithChildDialog(() => _confirmUpdateMessage.ShowAsync());
|
|
|
|
// Prompt chiuso senza scegliere (tap fuori / indietro): si resta sulla scheda.
|
|
if (result is null) return false;
|
|
|
|
VisibleOverlay = true;
|
|
StateHasChanged();
|
|
|
|
if (result is true) await Submit();
|
|
else await SaveDraft();
|
|
|
|
VisibleOverlay = false;
|
|
StateHasChanged();
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Salva il lavoro in corso solo sul dispositivo, marcato come da completare.
|
|
/// Niente chiamate al server e niente validazione: una bozza può essere incompleta.
|
|
/// </summary>
|
|
private async Task SaveDraft()
|
|
{
|
|
try
|
|
{
|
|
Scheda.Bozza = true;
|
|
|
|
if (IsNew) await NewSave(null);
|
|
else await Update(null, preserveActivityId: true);
|
|
|
|
await FileManager.CleanTempStorageAsync();
|
|
|
|
Snackbar.Clear();
|
|
Snackbar.Add("Scheda salvata come bozza, la ritrovi nell'ispezione", Severity.Success);
|
|
|
|
DisposeMessage();
|
|
MudDialog.Close(Scheda);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Scheda.Bozza = false;
|
|
OnError(e, e.Message);
|
|
}
|
|
}
|
|
|
|
private async Task Submit()
|
|
{
|
|
// Reparto e motivo non passano piu' dalla validazione di MudForm: sono
|
|
// campi tappabili, non input. Si controllano qui, e l'errore si vede sul
|
|
// gruppo interessato oltre che nel messaggio.
|
|
_showErrors = true;
|
|
await _form.Validate();
|
|
|
|
if (!RepartoMancante && !MotivoMancante && _form.IsValid)
|
|
{
|
|
await Save();
|
|
return;
|
|
}
|
|
|
|
// Il form resta aperto sugli errori: senza un avviso il tocco su "Salva"
|
|
// sembra non fare nulla se i campi mancanti sono fuori schermo.
|
|
Snackbar.Clear();
|
|
Snackbar.Add("Compila i campi obbligatori, oppure esci salvando come bozza", Severity.Warning);
|
|
StateHasChanged();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region File
|
|
|
|
private async Task OpenAddAttached()
|
|
{
|
|
var selectImageFromCamera = await AttachedService.SelectImageFromCamera();
|
|
if (selectImageFromCamera == null) return;
|
|
|
|
var attachedList = new List<AttachedDto> { selectImageFromCamera };
|
|
|
|
OnLoading = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
await Task.Yield();
|
|
|
|
// prepara placeholder in UI subito (così vedi le card con spinner)
|
|
AttachedList ??= [];
|
|
foreach (var a in attachedList)
|
|
AttachedList.Add(new AttachedDto { Name = a.Name, MimeType = a.MimeType, FileBytes = a.FileBytes });
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
_attachmentsDirty = true;
|
|
RecalcDirty();
|
|
|
|
// Processa in background e aggiorna UI man mano (o a blocchi)
|
|
_ = Task.Run(async () =>
|
|
{
|
|
for (var i = 0; i < attachedList.Count; i++)
|
|
{
|
|
var a = attachedList[i];
|
|
if (a.FileBytes is null || a.Name is null) continue;
|
|
|
|
var (origUrl, thumbUrl) = await FileManager.SaveAndCreateThumbAsync(a.FileBytes, a.Name);
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
var target = AttachedList![AttachedList.Count - attachedList.Count + i];
|
|
target.TempPath = origUrl;
|
|
target.ThumbPath = thumbUrl;
|
|
target.ToUpload = true;
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
OnLoading = false;
|
|
StateHasChanged();
|
|
});
|
|
});
|
|
}
|
|
|
|
private void OpenSelectArt()
|
|
{
|
|
if (Scheda.CodJfas.IsNullOrEmpty())
|
|
{
|
|
Snackbar.Clear();
|
|
Snackbar.Add("Selezionare prima il reparto", Severity.Error);
|
|
return;
|
|
}
|
|
|
|
OnLoading = true;
|
|
TextLoading = "Download articoli in griglia";
|
|
StateHasChanged();
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
List<ArticoliInGrigliaDto>? articoli;
|
|
try
|
|
{
|
|
articoli = await IntegrySteupService.RetrieveGrigliaPlu(
|
|
new RetrieveGrigliaPluRequestDto
|
|
{
|
|
CodMdep = CodMdep,
|
|
ActivityTypeId = Scheda.ActivityTypeId,
|
|
CodJfas = Scheda.CodJfas
|
|
}
|
|
);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
OnLoading = false;
|
|
TextLoading = null;
|
|
StateHasChanged();
|
|
|
|
OnError(e, e.Message);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
var modal = await WithChildDialog(() => Dialog.OpenSelectArt(articoli));
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
OnLoading = false;
|
|
TextLoading = null;
|
|
|
|
StateHasChanged();
|
|
});
|
|
|
|
if (modal is { Canceled: false, Data: List<ArticoliInGrigliaDto> articoliSelezionati })
|
|
{
|
|
Scheda.Articoli.InsertRange(0, articoliSelezionati.ConvertAll(x => new SchedaArticolo
|
|
{
|
|
Barcode = x.Barcode,
|
|
Descrizione = x.Descrizione
|
|
})
|
|
);
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
RecalcDirty();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnRemoveAttached(int index)
|
|
{
|
|
if (AttachedList is null || index < 0 || index >= AttachedList.Count) return;
|
|
|
|
if (AttachedList[index].SavedOnAppData)
|
|
AttachedList[index].ToRemove = true;
|
|
|
|
AttachedList.RemoveAt(index);
|
|
_attachmentsDirty = true;
|
|
RecalcDirty();
|
|
StateHasChanged();
|
|
}
|
|
|
|
#endregion
|
|
|
|
private async Task SuggestActivityDescription()
|
|
{
|
|
if (Scheda.ActivityTypeId == null)
|
|
{
|
|
Snackbar.Add("Indicare prima il motivo", Severity.Error);
|
|
return;
|
|
}
|
|
|
|
VisibleOverlay = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
var activityDescriptions = await IntegryApiService.SuggestActivityDescription(Scheda.ActivityTypeId);
|
|
|
|
var modal = await WithChildDialog(() => Dialog.OpenSuggestActivityDescription(activityDescriptions));
|
|
|
|
if (modal is { Canceled: false, Data: string descrizione } && !string.IsNullOrWhiteSpace(descrizione))
|
|
{
|
|
Scheda.Note = descrizione;
|
|
_noteFieldKey++; // ricrea il campo note così la nota selezionata compare subito a video
|
|
// Aggiorna anche lo stato (dirty → Salva/Aggiorna) come farebbe l'input manuale.
|
|
OnAfterChangeValue();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
VisibleOverlay = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
#region Scanner
|
|
|
|
private void OnManualBarcodeSet()
|
|
{
|
|
if (ManualBarcode == null) return;
|
|
|
|
HandleNewScanSuccessful(ManualBarcode);
|
|
ManualBarcode = null;
|
|
}
|
|
|
|
private async void HandleNewScanSuccessful(string? value)
|
|
{
|
|
try
|
|
{
|
|
if (value is null) return;
|
|
if (Scheda.Articoli.Any(x => x.Barcode.Equals(value))) return;
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
OnLoading = true;
|
|
StateHasChanged();
|
|
});
|
|
|
|
var art = await IntegrySteupService.RetrieveArtFromBarcode(value);
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
OnLoading = false;
|
|
StateHasChanged();
|
|
|
|
if (art != null)
|
|
{
|
|
Scheda.Articoli.Insert(0, new SchedaArticolo
|
|
{
|
|
Barcode = art.Barcode,
|
|
Descrizione = art.Descrizione
|
|
});
|
|
|
|
RecalcDirty();
|
|
StateHasChanged();
|
|
}
|
|
else
|
|
{
|
|
OnError(null, "Nessun articolo trovato");
|
|
}
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
|
|
await InvokeAsync(() =>
|
|
{
|
|
OnLoading = false;
|
|
StateHasChanged();
|
|
});
|
|
|
|
OnError(e, e.Message);
|
|
}
|
|
}
|
|
|
|
private void OnErrorScan(string? value) => OnError(new Exception(value), value);
|
|
|
|
#endregion
|
|
|
|
private void OnError(Exception? e, string? errorMessage)
|
|
{
|
|
if (e != null) Logger.LogError(e, errorMessage);
|
|
if (errorMessage == null) return;
|
|
|
|
_ = Dialog.ShowError(errorMessage);
|
|
}
|
|
|
|
private void RemoveArt(string barcode)
|
|
{
|
|
var index = Scheda.Articoli.FindIndex(x => x.Barcode.Equals(barcode));
|
|
if (index < 0) return;
|
|
|
|
Scheda.Articoli.RemoveAt(index);
|
|
RecalcDirty();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void DisposeMessage()
|
|
{
|
|
OnScannerService.OnNewScanSuccessful -= HandleNewScanSuccessful;
|
|
OnScannerService.OnErrorScan -= OnErrorScan;
|
|
|
|
_backRegistration?.Dispose();
|
|
_backRegistration = null;
|
|
}
|
|
|
|
// Rete di sicurezza: se il dialog viene chiuso per altre vie, il tasto indietro
|
|
// non deve restare agganciato a un form che non esiste più.
|
|
void IDisposable.Dispose()
|
|
{
|
|
DisposeMessage();
|
|
}
|
|
|
|
} |