diff --git a/SteUp.Shared/Components/SingleElements/Modal/ModalFormScheda.razor b/SteUp.Shared/Components/SingleElements/Modal/ModalFormScheda.razor index 4238a85..1a93399 100644 --- a/SteUp.Shared/Components/SingleElements/Modal/ModalFormScheda.razor +++ b/SteUp.Shared/Components/SingleElements/Modal/ModalFormScheda.razor @@ -4,6 +4,7 @@ @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 @@ -51,39 +52,45 @@ } - - - @foreach (var fasi in SteupDataService.Reparti) - { - - @fasi.Descrizione - - } - + + + + @if (_showErrors && RepartoMancante) + { + + } - + @if (Scheda.CodJfas.IsNullOrEmpty()) {

Scegli prima il reparto.

} else { - - @foreach (var type in SteupDataService.TipiAttività.Where(x => x.CodJfas.EqualsIgnoreCase(Scheda.CodJfas!))) - { - @type.ActivityTypeId - } - + + + @if (_showErrors && MotivoMancante) + { + + } }
@@ -591,12 +598,38 @@ #region Form - private void OnAfterChangeReparto() + /// 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 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(); @@ -698,9 +731,13 @@ 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 (_form.IsValid) + if (!RepartoMancante && !MotivoMancante && _form.IsValid) { await Save(); return; @@ -710,6 +747,7 @@ // 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 diff --git a/SteUp.Shared/Components/SingleElements/Picker/SelectField.razor b/SteUp.Shared/Components/SingleElements/Picker/SelectField.razor new file mode 100644 index 0000000..55c767a --- /dev/null +++ b/SteUp.Shared/Components/SingleElements/Picker/SelectField.razor @@ -0,0 +1,87 @@ +@typeparam TValue +@using SteUp.Shared.Components.Layout.Sheet + +@* + Scelta a valore singolo. Al posto della tendina: una riga tappabile che + apre un bottom sheet con le opzioni a tutta larghezza. + + La tendina di serie e' un residuo del web — galleggia bianca su bianco + sopra le card, si porta dietro la barra di scorrimento del desktop e offre + bersagli alti quanto una riga di testo. Qui l'elenco arriva dal basso, ha + un titolo, righe da 56 px e la scelta corrente segnata: si usa in piedi, + con una mano, anche con i guanti. +*@ + + + + +
+ @foreach (var item in Items) + { + var selected = IsSelected(item); + + } +
+
+ +@code { + /// Titolo dello sheet: e' il nome del campo ("Reparto", "Motivo"). + [Parameter, EditorRequired] public string Title { get; set; } = ""; + + [Parameter] public string? Subtitle { get; set; } + + [Parameter, EditorRequired] public IEnumerable Items { get; set; } = []; + + [Parameter] public TValue? Value { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + + /// Come si scrive un'opzione. Deve essere fornita: il ToString() di un DTO + /// non e' un'etichetta. + [Parameter, EditorRequired] public Func ItemLabel { get; set; } = _ => null; + + [Parameter] public string Placeholder { get; set; } = "Scegli"; + [Parameter] public bool ReadOnly { get; set; } + + private bool _open; + + private bool HasValue => Value is not null; + private string? SelectedLabel => Value is null ? null : ItemLabel(Value); + + // Il confronto passa per l'etichetta, non per l'oggetto: il valore corrente + // arriva ricostruito dall'entita' salvata (Scheda.Reparto lo crea al volo da + // codice e descrizione), quindi non e' mai lo stesso oggetto di quelli in + // elenco e il confronto di riferimento non segnerebbe mai nulla. + private bool IsSelected(TValue item) => + Value is not null && + string.Equals(ItemLabel(item), SelectedLabel, StringComparison.Ordinal); + + private void Open() + { + if (!ReadOnly) _open = true; + } + + private async Task Choose(TValue item) + { + _open = false; + Value = item; + await ValueChanged.InvokeAsync(item); + } +} diff --git a/SteUp.Shared/wwwroot/css/components.css b/SteUp.Shared/wwwroot/css/components.css index 387aab3..1a260a3 100644 --- a/SteUp.Shared/wwwroot/css/components.css +++ b/SteUp.Shared/wwwroot/css/components.css @@ -387,8 +387,8 @@ .chip[aria-pressed="true"], .chip--selected { background-color: var(--info-surface); - border-color: var(--primary); - color: var(--primary-dark); + border-color: var(--info-solid); + color: var(--info-ink); } /* ----------------------------------------------------------------------------- diff --git a/SteUp.Shared/wwwroot/css/forms.css b/SteUp.Shared/wwwroot/css/forms.css index 92dbc78..4d044cc 100644 --- a/SteUp.Shared/wwwroot/css/forms.css +++ b/SteUp.Shared/wwwroot/css/forms.css @@ -99,10 +99,14 @@ .option:active { transform: scale(0.97); } +/* La scelta fatta si segna col tono informativo, non con laccento: un bordo + corallo intorno a un riempimento azzurro si legge come un errore, non come + "questa e la risposta". Laccento resta del solo pulsante primario. */ .option[aria-pressed="true"] { + font-weight: 600; background-color: var(--info-surface); - border-color: var(--primary); - color: var(--primary-dark); + border-color: var(--info-solid); + color: var(--info-ink); } @media (prefers-reduced-motion: reduce) { @@ -389,14 +393,56 @@ .select-row__title { font-size: var(--body-lg-size); line-height: 1.3; - font-weight: 600; + font-weight: 500; color: var(--ink); } .select-row__meta { margin-top: var(--space-2); font-size: var(--label-md-size); - font-weight: 600; + font-weight: 400; color: var(--ink-muted); font-variant-numeric: tabular-nums; } + +/* ----------------------------------------------------------------------------- + Campo a scelta singola — la riga che apre il bottom sheet + Ha l'aspetto di un valore, non di un controllo: il chevron basta a dire + che si tocca. + -------------------------------------------------------------------------- */ + +.select-field { + display: flex; + align-items: center; + gap: var(--space-xs); + width: 100%; + min-height: var(--touch-min); + padding: var(--space-xs) 0; + border: none; + background: none; + font-family: inherit; + text-align: left; + transition: opacity var(--duration-fast) var(--ease-out); +} + +.select-field:active { opacity: 0.6; } +.select-field:disabled { opacity: 1; } + +.select-field__value { + flex: 1 1 auto; + min-width: 0; + font-size: var(--body-lg-size); + line-height: 1.3; + font-weight: 500; + color: var(--ink); +} + +/* Il segnaposto e' testo secondario, non un valore: si distingue a colpo + d'occhio da un campo gia' compilato. */ +.select-field__value--empty { font-weight: 400; color: var(--ink-muted); } + +.select-field__icon { flex: 0 0 auto; font-size: 22px; color: var(--ink-muted); } + +/* Scelta singola: il segno e' tondo come un radio, non quadrato come una + casella. La forma dice gia' che si sceglie una cosa sola. */ +.select-row--single .select-row__check { border-radius: var(--radius-pill); }