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:
@@ -1,67 +1,56 @@
|
||||
@inject IJSRuntime Js
|
||||
|
||||
<div class="@(Back ? "" : "container") header">
|
||||
<div class="header-content @(Back ? "with-back" : "no-back")">
|
||||
@if (!SmallHeader)
|
||||
{
|
||||
if (Back)
|
||||
{
|
||||
<div class="left-section">
|
||||
<MudButton StartIcon="@(!Cancel ? Icons.Material.Outlined.ArrowBackIosNew : "")"
|
||||
OnClick="@GoBack"
|
||||
Color="Color.Info"
|
||||
Style="text-transform: none"
|
||||
Variant="Variant.Text">
|
||||
@BackTo
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
@*
|
||||
Testata di schermata (STILE_UI §5): alta, titolo a sinistra, sfondo uguale
|
||||
alla pagina. Non e' una AppBar colorata col titolo centrato: quella e' una
|
||||
firma Android di serie, e il documento la esclude esplicitamente.
|
||||
*@
|
||||
|
||||
<h3 class="page-title">@Title</h3>
|
||||
<header class="appbar @(Compact ? "appbar--compact" : "")">
|
||||
@if (Back || Cancel)
|
||||
{
|
||||
<div class="appbar__lead">
|
||||
<MudIconButton Icon="@(Cancel ? Icons.Material.Rounded.Close : Icons.Material.Rounded.ArrowBack)"
|
||||
Color="Color.Inherit"
|
||||
aria-label="@(Cancel ? "Chiudi" : "Indietro")"
|
||||
title="@(Cancel ? "Chiudi" : "Indietro")"
|
||||
OnClick="@GoBack"/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="right-section">
|
||||
@if (!LabelSave.IsNullOrEmpty())
|
||||
{
|
||||
<MudButton OnClick="@OnSave"
|
||||
Color="Color.Info"
|
||||
Style="text-transform: none"
|
||||
Variant="Variant.Text">
|
||||
@LabelSave
|
||||
</MudButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
<div class="appbar__titles">
|
||||
<h1 class="appbar__title">@Title</h1>
|
||||
@if (!string.IsNullOrWhiteSpace(Subtitle))
|
||||
{
|
||||
<div class="title">
|
||||
<MudText Typo="Typo.h6">
|
||||
<b>@Title</b>
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Rounded.Close" OnClick="@GoBack" />
|
||||
</div>
|
||||
<p class="appbar__subtitle">@Subtitle</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code{
|
||||
[Parameter] public string? Title { get; set; }
|
||||
[Parameter] public bool Back { get; set; }
|
||||
[Parameter] public string BackTo { get; set; } = "";
|
||||
|
||||
[Parameter] public EventCallback OnFilterToggle { get; set; }
|
||||
|
||||
[Parameter] public bool Cancel { get; set; }
|
||||
[Parameter] public EventCallback OnCancel { get; set; }
|
||||
[Parameter] public string? LabelSave { get; set; }
|
||||
[Parameter] public EventCallback OnSave { get; set; }
|
||||
|
||||
[Parameter] public bool SmallHeader { get; set; }
|
||||
|
||||
protected override void OnParametersSet()
|
||||
@if (Actions is not null)
|
||||
{
|
||||
Back = !Back ? !Back && Cancel : Back;
|
||||
BackTo = Cancel ? "Annulla" : BackTo;
|
||||
<div class="appbar__actions">
|
||||
@Actions
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Title { get; set; }
|
||||
[Parameter] public string? Subtitle { get; set; }
|
||||
|
||||
/// Mostra la freccia indietro (navigazione tra schermate).
|
||||
[Parameter] public bool Back { get; set; }
|
||||
|
||||
/// Mostra la X di chiusura e delega a OnCancel (schermate modali).
|
||||
[Parameter] public bool Cancel { get; set; }
|
||||
|
||||
[Parameter] public EventCallback OnCancel { get; set; }
|
||||
|
||||
/// Testata ridotta per le schermate a compito singolo, dove il titolo
|
||||
/// non deve rubare spazio al contenuto.
|
||||
[Parameter] public bool Compact { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? Actions { get; set; }
|
||||
|
||||
private async Task GoBack()
|
||||
{
|
||||
@@ -73,5 +62,4 @@
|
||||
|
||||
await Js.InvokeVoidAsync("goBack");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
.header {
|
||||
min-height: var(--mh-header);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-content > .title { width: 100%; }
|
||||
|
||||
.header-content.with-back .page-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.left-section ::deep button, .right-section ::deep button { font-size: 1.1rem; }
|
||||
|
||||
.left-section ::deep .mud-button-icon-start { margin-right: 3px !important; }
|
||||
|
||||
.header-content.no-back .page-title { margin: 0; }
|
||||
@@ -3,30 +3,24 @@
|
||||
@using SteUp.Shared.Core.Interface.IntegryApi
|
||||
@using SteUp.Shared.Core.Interface.System.Network
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject INetworkService NetworkService
|
||||
@inject IIntegryApiService IntegryApiService
|
||||
|
||||
<MudThemeProvider Theme="_currentTheme" @ref="@_mudThemeProvider" @bind-IsDarkMode="@IsDarkMode" />
|
||||
<MudThemeProvider Theme="_theme" IsDarkMode="false"/>
|
||||
<MudPopoverProvider/>
|
||||
<MudDialogProvider/>
|
||||
<MudDialogProvider CloseButton="false" NoHeader="true" BackdropClick="false"/>
|
||||
<MudSnackbarProvider/>
|
||||
|
||||
<ConnectionState IsNetworkAvailable="IsNetworkAvailable" ServicesIsDown="ServicesIsDown" ShowWarning="ShowWarning" />
|
||||
<ConnectionState IsNetworkAvailable="IsNetworkAvailable" ServicesIsDown="ServicesIsDown" ShowWarning="ShowWarning"/>
|
||||
|
||||
<div class="page">
|
||||
<NavMenu/>
|
||||
|
||||
<main>
|
||||
<article>
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
<div class="app-shell">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
<NavMenu/>
|
||||
|
||||
@code {
|
||||
private MudThemeProvider _mudThemeProvider = null!;
|
||||
private bool IsDarkMode { get; set; }
|
||||
private string _mainContentClass = "";
|
||||
|
||||
//Connection state
|
||||
private bool _isNetworkAvailable;
|
||||
@@ -71,58 +65,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
private readonly MudTheme _currentTheme = new()
|
||||
// La palette resta quella di SteUP. Forma, ritmo e tipografia arrivano dai
|
||||
// token CSS (tokens.css), che pilotano anche le variabili di MudBlazor:
|
||||
// qui si dichiara solo il minimo che Mud deve conoscere lato C#.
|
||||
private readonly MudTheme _theme = new()
|
||||
{
|
||||
PaletteLight = new PaletteLight()
|
||||
PaletteLight = new PaletteLight
|
||||
{
|
||||
Primary = "#ec4c41",
|
||||
Secondary = "#002339",
|
||||
Tertiary = "#dff2ff",
|
||||
TextPrimary = "#000"
|
||||
Success = "#26b050",
|
||||
Error = "#e50000",
|
||||
Warning = "#b26a00",
|
||||
Info = "#1877d8",
|
||||
Background = "#e9edf2",
|
||||
Surface = "#ffffff",
|
||||
TextPrimary = "#000000",
|
||||
TextSecondary = "#465563",
|
||||
LinesDefault = "#d9e0e8",
|
||||
LinesInputs = "#d9e0e8",
|
||||
Divider = "#d9e0e8"
|
||||
},
|
||||
PaletteDark = new PaletteDark
|
||||
LayoutProperties = new LayoutProperties
|
||||
{
|
||||
Primary = "#ec4c41",
|
||||
Secondary = "#002339",
|
||||
Tertiary = "#dff2ff",
|
||||
Surface = "#000406",
|
||||
Background = "#000406",
|
||||
TextPrimary = "#fff",
|
||||
GrayDark = "#E0E0E0"
|
||||
DefaultBorderRadius = "16px"
|
||||
}
|
||||
};
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// if (firstRender)
|
||||
// {
|
||||
// var isDarkMode = LocalStorage.GetString("isDarkMode");
|
||||
|
||||
// if (isDarkMode == null && _mudThemeProvider != null)
|
||||
// {
|
||||
// IsDarkMode = await _mudThemeProvider.GetSystemPreference();
|
||||
// await _mudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
|
||||
// LocalStorage.SetString("isDarkMode", IsDarkMode.ToString());
|
||||
// StateHasChanged();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// IsDarkMode = bool.Parse(isDarkMode!);
|
||||
// }
|
||||
|
||||
// if (IsDarkMode)
|
||||
// {
|
||||
// _mainContentClass += "is-dark";
|
||||
// StateHasChanged();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private async Task OnSystemPreferenceChanged(bool newValue)
|
||||
{
|
||||
IsDarkMode = newValue;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
@@ -170,4 +140,4 @@
|
||||
_cts?.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@using CommunityToolkit.Mvvm.Messaging
|
||||
@using SteUp.Shared.Components.Layout.Sheet
|
||||
@using SteUp.Shared.Components.SingleElements.MessageBox
|
||||
@using SteUp.Shared.Core.Enum
|
||||
@using SteUp.Shared.Core.Interface.LocalDb
|
||||
@@ -9,154 +10,205 @@
|
||||
@inject IMessenger Messenger
|
||||
@inject IIspezioniService IspezioniService
|
||||
@inject IJSRuntime Js
|
||||
@implements IDisposable
|
||||
|
||||
<div
|
||||
class="container animated-navbar @(IsVisible ? "show-nav" : "hide-nav") @(IsVisible ? PlusVisible ? "with-plus" : "without-plus" : "with-plus")">
|
||||
<nav class="navbar @(IsVisible ? PlusVisible ? "with-plus" : "without-plus" : "with-plus")">
|
||||
<div class="container-navbar">
|
||||
<ul class="navbar-nav flex-row nav-justified align-items-center w-100 text-center">
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link" href="ispezioni" Match="NavLinkMatch.All">
|
||||
<div class="d-flex flex-column">
|
||||
<i class="ri-file-list-3-line"></i>
|
||||
<span>Ispezioni</span>
|
||||
</div>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link" href="user" Match="NavLinkMatch.All">
|
||||
<div class="d-flex flex-column">
|
||||
<i class="ri-user-line"></i>
|
||||
<span>Profilo</span>
|
||||
</div>
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@*
|
||||
Navigazione flottante in basso (STILE_UI §6.15): il contenuto le scorre
|
||||
sotto, le destinazioni restano a portata di pollice. Icona outline a
|
||||
riposo, piena da selezionata, etichetta sempre visibile in entrambe.
|
||||
Da 720px diventa una colonna laterale (vedi components.css).
|
||||
*@
|
||||
|
||||
@if (PlusVisible)
|
||||
<nav class="navbar @(IsVisible ? "" : "navbar--hidden")" aria-label="Navigazione principale">
|
||||
<NavLink class="navbar__item pressable" href="ispezioni" Match="NavLinkMatch.All">
|
||||
<span class="navbar__icon">
|
||||
<i class="@(IsOn("ispezioni") ? "ri-file-list-3-fill" : "ri-file-list-3-line")" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="navbar__label">Ispezioni</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="navbar__item pressable" href="user" Match="NavLinkMatch.All">
|
||||
<span class="navbar__icon">
|
||||
<i class="@(IsOn("user") ? "ri-user-fill" : "ri-user-line")" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="navbar__label">Profilo</span>
|
||||
</NavLink>
|
||||
|
||||
@if (ActionsVisible)
|
||||
{
|
||||
<button type="button" class="navbar__fab" @onclick="OpenActions"
|
||||
aria-label="Azioni" title="Azioni">
|
||||
<i class="ri-add-line" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<ActionSheet @bind-Open="_sheetOpen" Title="@SheetTitle" Subtitle="@SheetSubtitle">
|
||||
<div class="stack stack-xs">
|
||||
@if (OnInspectionPage)
|
||||
{
|
||||
<MudFabMenu ButtonClass="custom-plus-button" MenuClass="custom-menu-fab" OnClick="@OnOpenMenu"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
IconColor="Color.Primary" Color="Color.Surface" AlignItems="AlignItems.End"
|
||||
Size="Size.Medium" IconSize="Size.Medium">
|
||||
@if (SchedaVisible)
|
||||
{
|
||||
@if (!_isCompleted)
|
||||
{
|
||||
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@NewScheda"
|
||||
Label="Nuova scheda" Color="Color.Surface"/>
|
||||
}
|
||||
@if (!_isCompleted)
|
||||
{
|
||||
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
|
||||
@onclick="NewScheda">
|
||||
<span class="action-tile__icon action-tile__icon--primary">
|
||||
<i class="ri-add-line" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="action-tile__label">Nuova scheda</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@DeleteInspection"
|
||||
Label="Cancella ispezione" Color="Color.Surface"/>
|
||||
@if (ShowCompleteInspection)
|
||||
{
|
||||
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
|
||||
@onclick="CompleteInspection">
|
||||
<span class="action-tile__icon action-tile__icon--success">
|
||||
<i class="ri-check-double-line" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="action-tile__label">Concludi ispezione</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
if (ShowCompleteInspection)
|
||||
{
|
||||
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@CompleteInspection"
|
||||
Label="Concludi ispezione" Color="Color.Surface"/>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@NewInspection"
|
||||
Label="Nuova ispezione" Color="Color.Surface"/>
|
||||
}
|
||||
</MudFabMenu>
|
||||
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
|
||||
@onclick="DeleteInspection">
|
||||
<span class="action-tile__icon action-tile__icon--danger">
|
||||
<i class="ri-delete-bin-line" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="action-tile__label">Elimina ispezione</span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
|
||||
@onclick="NewInspection">
|
||||
<span class="action-tile__icon action-tile__icon--primary">
|
||||
<i class="ri-add-line" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="action-tile__label">Nuova ispezione</span>
|
||||
</button>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<ConfirmMessageBox @ref="_messageBox" NoText="No" YesText="Si" YesColor="Color.Primary"
|
||||
Message="Completando l'ispezione non sarà più possibile editarla o aggiungere ulteriori segnalazioni. Procedere?"/>
|
||||
@if (!NetworkService.IsNetworkAvailable())
|
||||
{
|
||||
<div class="feedback feedback--warning" role="status">
|
||||
<i class="feedback__icon ri-wifi-off-line" aria-hidden="true"></i>
|
||||
<span>Senza connessione queste azioni non sono disponibili. Il lavoro gia' fatto resta sul dispositivo.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ActionSheet>
|
||||
|
||||
<ConfirmMessageBox @ref="_messageBox" NoText="Annulla" YesText="Concludi" YesColor="Color.Primary"
|
||||
Message="Una volta conclusa, l'ispezione non sara' piu' modificabile e non potrai aggiungere altre schede. Procedere?"/>
|
||||
|
||||
@code
|
||||
{
|
||||
private bool IsVisible { get; set; } = true;
|
||||
private bool PlusVisible { get; set; } = true;
|
||||
private bool ActionsVisible { get; set; } = true;
|
||||
private bool OnInspectionPage { get; set; }
|
||||
private bool ShowCompleteInspection { get; set; }
|
||||
private bool SchedaVisible { get; set; }
|
||||
private bool _isCompleted;
|
||||
private bool _sheetOpen;
|
||||
|
||||
private ConfirmMessageBox _messageBox = null!;
|
||||
|
||||
private static readonly string[] NavigableRoutes = ["ispezioni", "ispezione", "user"];
|
||||
|
||||
private string SheetTitle => OnInspectionPage ? "Azioni ispezione" : "Nuova ispezione";
|
||||
|
||||
private string? SheetSubtitle => OnInspectionPage
|
||||
? SteupDataService.InspectionPageState.Ispezione.CodMdep
|
||||
: null;
|
||||
|
||||
private string CurrentRoute =>
|
||||
NavigationManager.Uri.Remove(0, NavigationManager.BaseUri.Length).Split('?')[0];
|
||||
|
||||
private bool IsOn(string route) => CurrentRoute.EqualsIgnoreCase(route);
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
NavigationManager.LocationChanged += (_, args) =>
|
||||
{
|
||||
var location = args.Location.Remove(0, NavigationManager.BaseUri.Length);
|
||||
|
||||
var newIsVisible = new List<string> { "ispezioni", "ispezione", "user" }
|
||||
.Contains(location);
|
||||
|
||||
var newPlusVisible = new List<string> { "ispezioni", "ispezione", "user" }
|
||||
.Contains(location);
|
||||
|
||||
if (IsVisible == newIsVisible && PlusVisible == newPlusVisible) return;
|
||||
|
||||
IsVisible = newIsVisible;
|
||||
PlusVisible = newPlusVisible;
|
||||
|
||||
if (location.EqualsIgnoreCase("ispezione"))
|
||||
{
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
if (ispezione.Stato == StatusEnum.Completata) PlusVisible = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
};
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
Refresh();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
// Cambiando schermata un menu aperto non ha piu' senso.
|
||||
_sheetOpen = false;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
var route = CurrentRoute;
|
||||
|
||||
IsVisible = NavigableRoutes.Contains(route, StringComparer.OrdinalIgnoreCase);
|
||||
OnInspectionPage = route.EqualsIgnoreCase("ispezione");
|
||||
ActionsVisible = IsVisible && !route.EqualsIgnoreCase("user");
|
||||
|
||||
if (OnInspectionPage)
|
||||
{
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
_isCompleted = ispezione.Stato == StatusEnum.Completata;
|
||||
// "Concludi" non ha senso su un'ispezione mai inviata o gia' conclusa.
|
||||
ShowCompleteInspection = !ispezione.ActivityId.IsNullOrEmpty() && !_isCompleted;
|
||||
|
||||
// Su un'ispezione conclusa resta solo la consultazione.
|
||||
if (_isCompleted) ActionsVisible = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void OpenActions()
|
||||
{
|
||||
Refresh();
|
||||
_sheetOpen = true;
|
||||
}
|
||||
|
||||
private async Task NewInspection()
|
||||
{
|
||||
_sheetOpen = false;
|
||||
|
||||
if (await SteupDataService.CanOpenNewInspection())
|
||||
{
|
||||
_ = Dialog.OpenSelectShop();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Dialog.ShowWarning("Per aprire una nuova ispezione è necessario concludere prima tutte le ispezioni aperte più vecchie di 20 giorni!");
|
||||
await Dialog.ShowWarning(
|
||||
"Per aprire una nuova ispezione devi prima concludere tutte le ispezioni aperte da piu' di 20 giorni.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteInspection()
|
||||
{
|
||||
var result = await _messageBox.ShowAsync();
|
||||
_sheetOpen = false;
|
||||
|
||||
if (result is true)
|
||||
Messenger.Send(new CompleteInspectionMessage());
|
||||
var result = await _messageBox.ShowAsync();
|
||||
if (result is true) Messenger.Send(new CompleteInspectionMessage());
|
||||
}
|
||||
|
||||
private async Task NewScheda()
|
||||
{
|
||||
_sheetOpen = false;
|
||||
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
var modal = await Dialog.OpenFormScheda(ispezione.CodMdep, ispezione.Data, true);
|
||||
|
||||
if (modal is { Canceled: false })
|
||||
Messenger.Send(new NewSchedaMessage());
|
||||
}
|
||||
|
||||
private void OnOpenMenu()
|
||||
{
|
||||
var location = NavigationManager.Uri.Remove(0, NavigationManager.BaseUri.Length);
|
||||
|
||||
SchedaVisible = new List<string> { "ispezione" }.Contains(location);
|
||||
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
_isCompleted = ispezione.Stato == StatusEnum.Completata;
|
||||
// Concludi non ha senso su un'ispezione gia completata
|
||||
ShowCompleteInspection = !ispezione.ActivityId.IsNullOrEmpty() && !_isCompleted;
|
||||
StateHasChanged();
|
||||
if (modal is { Canceled: false }) Messenger.Send(new NewSchedaMessage());
|
||||
}
|
||||
|
||||
private async Task DeleteInspection()
|
||||
{
|
||||
_sheetOpen = false;
|
||||
|
||||
try
|
||||
{
|
||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||
await IspezioniService.DeleteIspezioneAsync(ispezione.CodMdep, ispezione.Data, ispezione.Rilevatore);
|
||||
|
||||
|
||||
await Js.InvokeVoidAsync("goBack");
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -164,4 +216,6 @@
|
||||
await Dialog.ShowError(e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
.animated-navbar {
|
||||
background: transparent;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 1001;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.animated-navbar.show-nav {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.animated-navbar.hide-nav {
|
||||
transform: translateY(150%);
|
||||
}
|
||||
|
||||
.animated-navbar.with-plus {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.navbar.with-plus {
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.navbar.without-plus {
|
||||
transform: translateX(0);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container-navbar {
|
||||
background: var(--mud-palette-surface);
|
||||
border-radius: 50px;
|
||||
padding: 0 10px;
|
||||
box-shadow: var(--custom-box-shadow);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-item.plus-button {
|
||||
position: relative;
|
||||
bottom: 15px;
|
||||
}
|
||||
|
||||
.navbar ::deep .custom-plus-button {
|
||||
background: var(--mud-palette-surface);
|
||||
}
|
||||
|
||||
.navbar ::deep .custom-menu-fab .mud-fab-label {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.navbar ::deep .custom-menu-fab .mud-button-root.mud-fab {
|
||||
background: var(--mud-palette-surface);
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.2;
|
||||
justify-content: center;
|
||||
padding-top: .25rem !important;
|
||||
padding-bottom: .25rem !important;
|
||||
}
|
||||
|
||||
.nav-item ::deep a > div {
|
||||
-webkit-transition: all .1s ease-out;
|
||||
transition: all .1s ease-out;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active > div {
|
||||
color: var(--mud-palette-primary);
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active > div > i {
|
||||
/*background-color: color-mix(in srgb, var(--mud-palette-primary) 20%, transparent);*/
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active > div > span {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:not(.active) > div {
|
||||
color: var(--mud-palette-text-primary);
|
||||
}
|
||||
|
||||
.nav-item ::deep a i {
|
||||
font-size: 1.65rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a span {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.navbar {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,36 @@
|
||||
@using SteUp.Shared.Components.Layout.Spinner
|
||||
@*
|
||||
Overlay di operazione bloccante (invio al server, cancellazione).
|
||||
Mostra sempre cosa sta succedendo: un'attesa muta in campo, con la rete
|
||||
incerta, e' il momento in cui il rilevatore perde fiducia nello strumento.
|
||||
*@
|
||||
|
||||
<MudOverlay Visible="VisibleOverlay" LightBackground="true">
|
||||
@if (SuccessAnimation)
|
||||
{
|
||||
<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>
|
||||
@if (VisibleOverlay)
|
||||
{
|
||||
<div class="overlay" role="status" aria-live="polite">
|
||||
@if (SuccessAnimation)
|
||||
{
|
||||
<div class="checkmark">
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true">
|
||||
<path d="M6 17 L13 24 L26 9"/>
|
||||
</svg>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<p class="overlay-message">@Message</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="overlay-content">
|
||||
<SpinnerLayout/>
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<p class="overlay-message">@Message</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</MudOverlay>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="spinner spinner--lg" aria-hidden="true"></span>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<p class="overlay__message">@Message</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public required bool SuccessAnimation { get; set; }
|
||||
[Parameter] public bool SuccessAnimation { get; set; }
|
||||
[Parameter] public required bool VisibleOverlay { get; set; }
|
||||
|
||||
/// Testo opzionale mostrato sotto lo spinner/spunta (es. "Invio scheda 3 di 7…").
|
||||
/// Testo mostrato sotto lo spinner (es. "Invio scheda 3 di 7…").
|
||||
[Parameter] public string? Message { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
.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;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
box-sizing: content-box;
|
||||
border: 4px solid var(--mud-palette-success);
|
||||
}
|
||||
|
||||
.success-checkmark .check-icon::before {
|
||||
top: 3px;
|
||||
left: -2px;
|
||||
width: 30px;
|
||||
transform-origin: 100% 50%;
|
||||
border-radius: 100px 0 0 100px;
|
||||
}
|
||||
|
||||
.success-checkmark .check-icon::after {
|
||||
top: 0;
|
||||
left: 30px;
|
||||
width: 60px;
|
||||
transform-origin: 0 50%;
|
||||
border-radius: 0 100px 100px 0;
|
||||
animation: rotate-circle 4.25s ease-in;
|
||||
}
|
||||
|
||||
.success-checkmark .check-icon::before,
|
||||
.success-checkmark .check-icon::after {
|
||||
content: '';
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
transform: rotate(-45deg);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.icon-line {
|
||||
height: 5px;
|
||||
background-color: var(--mud-palette-success);
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.icon-line.line-tip {
|
||||
top: 46px;
|
||||
left: 14px;
|
||||
width: 25px;
|
||||
transform: rotate(45deg);
|
||||
animation: icon-line-tip 0.75s;
|
||||
}
|
||||
|
||||
.icon-line.line-long {
|
||||
top: 38px;
|
||||
right: 8px;
|
||||
width: 47px;
|
||||
transform: rotate(-45deg);
|
||||
animation: icon-line-long 0.75s;
|
||||
}
|
||||
|
||||
|
||||
.icon-circle {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
z-index: 10;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
box-sizing: content-box;
|
||||
border: 4px solid var(--mud-palette-success);
|
||||
}
|
||||
|
||||
.icon-fix {
|
||||
top: 8px;
|
||||
width: 5px;
|
||||
left: 26px;
|
||||
z-index: 1;
|
||||
height: 85px;
|
||||
position: absolute;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
@keyframes rotate-circle {
|
||||
0% { transform: rotate(-45deg); }
|
||||
|
||||
5% { transform: rotate(-45deg); }
|
||||
|
||||
12% { transform: rotate(-405deg); }
|
||||
|
||||
100% { transform: rotate(-405deg); }
|
||||
}
|
||||
|
||||
@keyframes icon-line-tip {
|
||||
0% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
|
||||
54% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
|
||||
70% {
|
||||
width: 50px;
|
||||
left: -8px;
|
||||
top: 37px;
|
||||
}
|
||||
|
||||
84% {
|
||||
width: 17px;
|
||||
left: 21px;
|
||||
top: 48px;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 25px;
|
||||
left: 14px;
|
||||
top: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-line-long {
|
||||
0% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
|
||||
65% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
|
||||
84% {
|
||||
width: 55px;
|
||||
right: 0px;
|
||||
top: 35px;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 47px;
|
||||
right: 8px;
|
||||
top: 38px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@*
|
||||
Bottom sheet (STILE_UI §6.14): sale dal basso, maniglia di trascinamento,
|
||||
testata con titolo e chiusura, un'unica linea di struttura, corpo scorrevole.
|
||||
Sostituisce i menu flottanti: stessa funzione, bersagli molto piu' grandi.
|
||||
*@
|
||||
|
||||
@if (Open)
|
||||
{
|
||||
<div class="sheet-scrim" @onclick="Close" aria-hidden="true"></div>
|
||||
|
||||
<div class="sheet @(Tall ? "sheet--tall" : "")" role="dialog" aria-modal="true"
|
||||
aria-label="@Title">
|
||||
<div class="sheet__handle"></div>
|
||||
|
||||
<div class="sheet__head">
|
||||
<div class="sheet__titles">
|
||||
<h2 class="sheet__title">@Title</h2>
|
||||
@if (!string.IsNullOrWhiteSpace(Subtitle))
|
||||
{
|
||||
<p class="sheet__subtitle">@Subtitle</p>
|
||||
}
|
||||
</div>
|
||||
<MudIconButton Icon="@Icons.Material.Rounded.Close" Color="Color.Inherit"
|
||||
aria-label="Chiudi" title="Chiudi" OnClick="Close"/>
|
||||
</div>
|
||||
|
||||
<div class="sheet__divider"></div>
|
||||
|
||||
<div class="sheet__body">
|
||||
@ChildContent
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public bool Open { get; set; }
|
||||
[Parameter] public EventCallback<bool> OpenChanged { get; set; }
|
||||
[Parameter] public string? Title { get; set; }
|
||||
[Parameter] public string? Subtitle { get; set; }
|
||||
|
||||
/// Sheet di compilazione (~92% dello schermo) invece che di consultazione.
|
||||
[Parameter] public bool Tall { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private async Task Close()
|
||||
{
|
||||
Open = false;
|
||||
await OpenChanged.InvokeAsync(false);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<MudOverlay Visible="Visible" DarkBackground="false">
|
||||
<div class="overlay-container">
|
||||
<span>@(Text ?? "Caricamento")</span>
|
||||
|
||||
<MudProgressLinear Color="Color.Primary" Rounded="true" Size="Size.Medium" Indeterminate="true" />
|
||||
</div>
|
||||
</MudOverlay>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public bool Visible { get; set; }
|
||||
[Parameter] public string? Text { get; set; }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
.overlay-container {
|
||||
background: var(--mud-palette-background);
|
||||
width: 20rem;
|
||||
height: 6rem;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--custom-box-shadow);
|
||||
}
|
||||
|
||||
.overlay-container > span {
|
||||
text-align: center;
|
||||
font-size: medium;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
<div class="spinner-container @(FullScreen ? "" : "not-fullScreen")">
|
||||
<span class="loader"></span>
|
||||
@* Indicatore di attesa. A schermo pieno solo quando non c'e' ancora nulla da
|
||||
mostrare; dentro una sezione resta contenuto e non fa saltare il layout. *@
|
||||
|
||||
<div style="display:flex; align-items:center; justify-content:center; @(FullScreen ? "height:100dvh;" : "padding: var(--space-xl) 0;")">
|
||||
<span class="spinner @(FullScreen ? "spinner--lg" : "")" role="status" aria-label="Caricamento"></span>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public bool FullScreen { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 10.1rem);
|
||||
align-items: center;
|
||||
color: var(--mud-palette-primary);
|
||||
}
|
||||
|
||||
.not-fullScreen {
|
||||
height: auto !important;
|
||||
padding: 2rem 0 !important;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 50px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
border: 8px solid #0000;
|
||||
border-right-color: var(--mud-palette-primary);
|
||||
position: relative;
|
||||
animation: l24 1s infinite linear;
|
||||
}
|
||||
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -8px;
|
||||
border-radius: 50%;
|
||||
border: inherit;
|
||||
animation: inherit;
|
||||
animation-duration: 2s;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
animation-duration: 4s;
|
||||
}
|
||||
|
||||
@keyframes l24 {
|
||||
100% {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.action-scheda-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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,47 +1,68 @@
|
||||
@page "/ispezioni"
|
||||
@attribute [Authorize]
|
||||
@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.Core.Entities
|
||||
@using SteUp.Shared.Core.Interface.LocalDb
|
||||
@inject IIspezioniService IspezioniService
|
||||
|
||||
<HeaderLayout Title="Ispezioni"/>
|
||||
<HeaderLayout Title="Ispezioni" Subtitle="@Subtitle"/>
|
||||
|
||||
<div class="container ispezioni pb-safe-area">
|
||||
@if (Ispezioni.IsNullOrEmpty())
|
||||
{
|
||||
<NoDataAvailable Text="Nessuna ispezione effettuata"
|
||||
ImageSource="_content/SteUp.Shared/images/undraw_file-search_cbur.svg"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Virtualize Items="Ispezioni" Context="ispezione">
|
||||
<InspectionCard Class="no-margin" Ispezione="ispezione" CompactView="true" OnClick="@OnClickIspezione"/>
|
||||
</Virtualize>
|
||||
}
|
||||
<div class="app-scroll">
|
||||
<div class="page-width page-body">
|
||||
@if (_loading)
|
||||
{
|
||||
@* Scheletri, non uno spinner in mezzo al contenuto: la pagina ha
|
||||
gia' la forma che avra', e non salta quando i dati arrivano. *@
|
||||
<div class="card-list" aria-hidden="true">
|
||||
@for (var i = 0; i < 3; i++)
|
||||
{
|
||||
<div class="list-card">
|
||||
<div class="skeleton skeleton--line-short"></div>
|
||||
<div class="skeleton skeleton--title" style="margin-top: 10px;"></div>
|
||||
<div class="skeleton skeleton--line" style="margin-top: 12px;"></div>
|
||||
<div class="skeleton skeleton--badge" style="margin-top: 14px;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (Ispezioni.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="empty-state__icon ri-clipboard-line" aria-hidden="true"></i>
|
||||
<p class="empty-state__title">Nessuna ispezione</p>
|
||||
<p class="empty-state__text">
|
||||
Tocca <b>+</b> nella barra in basso per aprire la prima ispezione su un punto vendita.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card-list">
|
||||
<Virtualize Items="Ispezioni" Context="ispezione" OverscanCount="4">
|
||||
<InspectionCard Ispezione="ispezione" OnClick="@OnClickIspezione"/>
|
||||
</Virtualize>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SpinnerOverlay VisibleOverlay="VisibleOverlay"/>
|
||||
|
||||
@code{
|
||||
private List<Ispezione> Ispezioni { get; set; } = [];
|
||||
private bool _loading = true;
|
||||
|
||||
private bool VisibleOverlay { get; set; } = true;
|
||||
private string? Subtitle => _loading || Ispezioni.Count == 0
|
||||
? null
|
||||
: Ispezioni.Count == 1
|
||||
? "1 ispezione"
|
||||
: $"{Ispezioni.Count} ispezioni";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadData();
|
||||
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
Ispezioni = await IspezioniService.GetAllIspezioniWithSchedeAsync(UserSession.User.Username);
|
||||
|
||||
_loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnClickIspezione(Ispezione ispezione)
|
||||
@@ -49,5 +70,4 @@
|
||||
SteupDataService.InspectionPageState.Ispezione = ispezione;
|
||||
NavigationManager.NavigateTo("/ispezione");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.ispezioni{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
@page "/login"
|
||||
@using Microsoft.Extensions.Logging
|
||||
@using SteUp.Shared.Components.Layout.Spinner
|
||||
@using SteUp.Shared.Core.BarcodeReader.Contracts
|
||||
@using SteUp.Shared.Core.Interface.System
|
||||
@using SteUp.Shared.Core.Services
|
||||
@@ -10,68 +9,83 @@
|
||||
@inject IBarcodeManager BarcodeManager
|
||||
@inject ILogger<LoginPage> Logger
|
||||
|
||||
@if (Spinner)
|
||||
{
|
||||
<SpinnerLayout/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="login-page">
|
||||
<div class="container container-top-logo">
|
||||
<img src="_content/SteUp.Shared/images/logo_steup.svg" class="logo" alt="sales book">
|
||||
</div>
|
||||
<div class="container container-login">
|
||||
<div class="login-form-container">
|
||||
<div class="input-group">
|
||||
<MudTextField @bind-Value="UserData.Username" Label="Username" Variant="Variant.Outlined"/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<MudTextField InputType="@_passwordInput" @bind-Value="UserData.Password" Label="Password" Variant="Variant.Outlined" Adornment="Adornment.End" AdornmentIcon="@_passwordInputIcon" OnAdornmentClick="ShowPassword" AdornmentAriaLabel="Show Password"/>
|
||||
</div>
|
||||
<div class="input-group mb-2">
|
||||
<MudTextField @bind-Value="UserData.CodHash" Label="Profilo azienda" Variant="Variant.Outlined"/>
|
||||
</div>
|
||||
@*
|
||||
Accesso (STILE_UI §7): stessa struttura dell'app, registro diverso.
|
||||
Niente accento cromatico — l'attenzione sta sul marchio; il corallo torna
|
||||
dentro l'app, dove significa "qui si agisce".
|
||||
*@
|
||||
|
||||
<MudButton OnClick="@SignInUser" Color="Color.Primary" Variant="Variant.Filled">Login</MudButton>
|
||||
@if (_attemptFailed)
|
||||
{
|
||||
<MudAlert Class="my-3" Dense="true" Severity="Severity.Error" Variant="Variant.Filled">@ErrorMessage</MudAlert>
|
||||
}
|
||||
<div class="onboarding">
|
||||
<form class="onboarding__panel" @onsubmit="SignInUser" @onsubmit:preventDefault="true">
|
||||
<img src="_content/SteUp.Shared/images/logo_steup.svg" class="onboarding__logo" alt="SteUP"/>
|
||||
|
||||
<div class="onboarding__fields">
|
||||
<div class="onboarding__field">
|
||||
<MudTextField @bind-Value="UserData.Username" Label="Username"
|
||||
Variant="Variant.Text" Immediate="true"
|
||||
UserAttributes="@(new() { ["autocomplete"] = "username", ["enterkeyhint"] = "next", ["autocapitalize"] = "none" })"/>
|
||||
</div>
|
||||
|
||||
<div class="my-4 login-footer">
|
||||
<span>@GenericSystemService.GetCurrentAppVersion() | Powered by</span>
|
||||
<img src="_content/SteUp.Shared/images/logoIntegry.svg" class="img-fluid" alt="Integry">
|
||||
<div class="onboarding__field">
|
||||
<MudTextField InputType="@_passwordInput" @bind-Value="UserData.Password" Label="Password"
|
||||
Variant="Variant.Text" Immediate="true"
|
||||
Adornment="Adornment.End" AdornmentIcon="@_passwordInputIcon"
|
||||
OnAdornmentClick="ShowPassword"
|
||||
AdornmentAriaLabel="@(_isShow ? "Nascondi la password" : "Mostra la password")"
|
||||
UserAttributes="@(new() { ["autocomplete"] = "current-password", ["enterkeyhint"] = "next" })"/>
|
||||
</div>
|
||||
|
||||
<div class="onboarding__field">
|
||||
<MudTextField @bind-Value="UserData.CodHash" Label="Profilo azienda"
|
||||
Variant="Variant.Text" Immediate="true"
|
||||
UserAttributes="@(new() { ["autocomplete"] = "organization", ["enterkeyhint"] = "go", ["autocapitalize"] = "none" })"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_attemptFailed)
|
||||
{
|
||||
<p class="onboarding__error" role="alert" aria-live="assertive">@ErrorMessage</p>
|
||||
}
|
||||
|
||||
<button type="submit" class="onboarding__submit" disabled="@(_busy || !CanSubmit)">
|
||||
@if (_busy)
|
||||
{
|
||||
<span class="onboarding__spinner" aria-hidden="true"></span>
|
||||
<span>Accesso in corso…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Accedi</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
<div class="onboarding__footer">
|
||||
<span>@GenericSystemService.GetCurrentAppVersion() · Powered by</span>
|
||||
<img src="_content/SteUp.Shared/images/logoIntegry.svg" alt="Integry"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private SignIn UserData { get; } = new();
|
||||
private bool Spinner { get; set; }
|
||||
private string ErrorMessage { get; set; } = "";
|
||||
private bool _attemptFailed;
|
||||
private bool _busy;
|
||||
|
||||
private bool _isShow;
|
||||
private InputType _passwordInput = InputType.Password;
|
||||
private string _passwordInputIcon = Icons.Material.Rounded.VisibilityOff;
|
||||
|
||||
private bool CanSubmit =>
|
||||
!string.IsNullOrWhiteSpace(UserData.Username) &&
|
||||
!string.IsNullOrWhiteSpace(UserData.Password) &&
|
||||
!string.IsNullOrWhiteSpace(UserData.CodHash);
|
||||
|
||||
private void ShowPassword()
|
||||
{
|
||||
if (_isShow)
|
||||
{
|
||||
_isShow = false;
|
||||
_passwordInputIcon = Icons.Material.Rounded.VisibilityOff;
|
||||
_passwordInput = InputType.Password;
|
||||
}
|
||||
else
|
||||
{
|
||||
_isShow = true;
|
||||
_passwordInputIcon = Icons.Material.Rounded.Visibility;
|
||||
_passwordInput = InputType.Text;
|
||||
}
|
||||
_isShow = !_isShow;
|
||||
_passwordInputIcon = _isShow ? Icons.Material.Rounded.Visibility : Icons.Material.Rounded.VisibilityOff;
|
||||
_passwordInput = _isShow ? InputType.Text : InputType.Password;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
@@ -82,36 +96,34 @@ else
|
||||
|
||||
private async Task SignInUser()
|
||||
{
|
||||
if (_busy || !CanSubmit) return;
|
||||
|
||||
_attemptFailed = false;
|
||||
if (!string.IsNullOrEmpty(UserData.Username) && !string.IsNullOrEmpty(UserData.Password) && !string.IsNullOrEmpty(UserData.CodHash))
|
||||
_busy = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
Spinner = true;
|
||||
SteupDataService.RegisterAppVersion();
|
||||
await UserAccountService.Login(UserData.Username!, UserData.Password!, UserData.CodHash!);
|
||||
AuthenticationStateProvider.NotifyAuthenticationState(); //Chiamato per forzare il refresh
|
||||
await SteupDataService.Init();
|
||||
BarcodeManager.Init();
|
||||
|
||||
LocalStorage.SetString("codHash", UserData.CodHash!);
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorMessage = e.Message;
|
||||
Logger.LogError(e, ErrorMessage);
|
||||
|
||||
_attemptFailed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
SteupDataService.RegisterAppVersion();
|
||||
await UserAccountService.Login(UserData.Username, UserData.Password, UserData.CodHash);
|
||||
AuthenticationStateProvider.NotifyAuthenticationState(); //Chiamato per forzare il refresh
|
||||
await SteupDataService.Init();
|
||||
BarcodeManager.Init();
|
||||
|
||||
LocalStorage.SetString("codHash", UserData.CodHash);
|
||||
NavigationManager.NavigateTo("/");
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorMessage = e.Message;
|
||||
Logger.LogError(e, ErrorMessage);
|
||||
Console.WriteLine(e.Message);
|
||||
|
||||
Spinner = false;
|
||||
StateHasChanged();
|
||||
|
||||
_attemptFailed = true;
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,4 +134,4 @@ else
|
||||
public string? CodHash { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
.login-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--mud-palette-surface);
|
||||
}
|
||||
|
||||
.container-top-logo > .logo {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.container-login > span {
|
||||
font-size: large;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-top-logo {
|
||||
height: 35vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.container-login {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 16px 16px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-footer span {
|
||||
font-size: 9px;
|
||||
color: var(--mud-palette-gray-darker);
|
||||
}
|
||||
|
||||
.login-footer img {
|
||||
height: 15px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
@@ -2,5 +2,19 @@
|
||||
@using SteUp.Shared.Components.Layout
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
<HeaderLayout Title="Pagina non trovata" Back="true"/>
|
||||
|
||||
<div class="app-scroll">
|
||||
<div class="page-width page-body">
|
||||
<div class="empty-state">
|
||||
<i class="empty-state__icon ri-compass-3-line" aria-hidden="true"></i>
|
||||
<h1 class="empty-state__title">Qui non c'è niente</h1>
|
||||
<p class="empty-state__text">
|
||||
La schermata che cercavi non esiste. Torna all'elenco delle ispezioni per riprendere il lavoro.
|
||||
</p>
|
||||
<div class="empty-state__action">
|
||||
<MudButton Variant="Variant.Outlined" Href="/ispezioni">Vai alle ispezioni</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using SteUp.Shared.Components.Layout
|
||||
@using SteUp.Shared.Components.Layout.Overlay
|
||||
@using SteUp.Shared.Components.SingleElements
|
||||
@using SteUp.Shared.Components.SingleElements.MessageBox
|
||||
@using SteUp.Shared.Core.Authorization.Enum
|
||||
@using SteUp.Shared.Core.Dto
|
||||
@using SteUp.Shared.Core.Interface.IntegryApi
|
||||
@@ -10,109 +11,94 @@
|
||||
@using SteUp.Shared.Core.Services
|
||||
@using SteUp.Shared.Core.Utility
|
||||
@inject AppAuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject INetworkService NetworkService
|
||||
@inject IGenericSystemService GenericSystemService
|
||||
@inject IFileManager FileManager
|
||||
@inject IIntegryApiService IntegryApiService
|
||||
|
||||
<HeaderLayout Title="Profilo"/>
|
||||
|
||||
@if (IsLoggedIn)
|
||||
{
|
||||
<div class="container content pb-safe-area">
|
||||
<div class="container-primary-info mud-elevation-1">
|
||||
<div class="section-primary-info">
|
||||
<MudAvatar Style="height: 70px; width: 70px; font-size: 2rem; font-weight: bold"
|
||||
Color="Color.Secondary">
|
||||
@UtilityString.ExtractInitials(UserSession.User.Fullname)
|
||||
</MudAvatar>
|
||||
<div class="app-scroll">
|
||||
<div class="page-width page-body stack stack-md">
|
||||
|
||||
<div class="personal-info">
|
||||
<span class="info-nome">@UserSession.User.Fullname</span>
|
||||
@if (UserSession.User.KeyGroup is not null)
|
||||
{
|
||||
<span
|
||||
class="info-section">@(((KeyGroupEnum)UserSession.User.KeyGroup).ConvertToHumanReadable())</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="section-info">
|
||||
<div class="section-personal-info">
|
||||
<div>
|
||||
<span class="info-title">Profilo azienda</span>
|
||||
<span class="info-text">@CodHash</span>
|
||||
@if (IsLoggedIn)
|
||||
{
|
||||
@* §6.16 Intestazione profilo *@
|
||||
<section class="panel panel--lg">
|
||||
<div class="profile-head">
|
||||
<div class="profile-avatar" aria-hidden="true">
|
||||
@UtilityString.ExtractInitials(UserSession.User.Fullname)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-personal-info">
|
||||
<div>
|
||||
<span class="info-title">Status</span>
|
||||
@if (NetworkService.ConnectionAvailable)
|
||||
<div class="section-header__text">
|
||||
<h2 class="summary__title">@UserSession.User.Fullname</h2>
|
||||
@if (UserSession.User.KeyGroup is not null)
|
||||
{
|
||||
<div class="status online">
|
||||
<i class="ri-wifi-line"></i>
|
||||
<span>Online</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="status offline">
|
||||
<i class="ri-wifi-off-line"></i>
|
||||
<span>Offline</span>
|
||||
</div>
|
||||
<p class="summary__subtitle">
|
||||
@(((KeyGroupEnum)UserSession.User.KeyGroup).ConvertToHumanReadable())
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-button ripple-container mud-elevation-1">
|
||||
<MudButton Class="button-settings red-icon"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.UploadFile"
|
||||
Size="Size.Medium"
|
||||
OnClick="@ExportLog"
|
||||
Variant="Variant.Outlined">
|
||||
Esporta log
|
||||
</MudButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container-button ripple-container mud-elevation-1">
|
||||
<MudButton Class="button-settings green-icon"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Outlined.Sync"
|
||||
Size="Size.Medium"
|
||||
OnClick="@UpdateDb"
|
||||
Variant="Variant.Outlined">
|
||||
Sincronizza ispezioni esportate
|
||||
</MudButton>
|
||||
</div>
|
||||
<section class="panel">
|
||||
<div class="nav-list__item" style="pointer-events: none;">
|
||||
<i class="nav-list__icon ri-building-line" aria-hidden="true"></i>
|
||||
<span class="nav-list__label">Profilo azienda</span>
|
||||
<span class="t-meta">@CodHash</span>
|
||||
</div>
|
||||
|
||||
<div class="container-button ripple-container mud-elevation-1">
|
||||
<MudButton Class="button-settings exit"
|
||||
FullWidth="true"
|
||||
Color="Color.Error"
|
||||
Size="Size.Medium"
|
||||
OnClick="@Logout"
|
||||
Variant="Variant.Outlined">
|
||||
Esci
|
||||
</MudButton>
|
||||
</div>
|
||||
|
||||
<AppVersion/>
|
||||
<div class="nav-list__item" style="pointer-events: none;">
|
||||
<i class="nav-list__icon ri-cloud-line" aria-hidden="true"></i>
|
||||
<span class="nav-list__label">Connessione</span>
|
||||
<Badge Spec="@ConnessioneBadge"/>
|
||||
</div>
|
||||
|
||||
<div class="nav-list__item" style="pointer-events: none;">
|
||||
<i class="nav-list__icon ri-information-line" aria-hidden="true"></i>
|
||||
<span class="nav-list__label">Versione</span>
|
||||
<span class="t-meta">@GenericSystemService.GetCurrentAppVersion()</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<button type="button" class="nav-list__item" @onclick="UpdateDb">
|
||||
<i class="nav-list__icon ri-refresh-line" aria-hidden="true"></i>
|
||||
<span class="nav-list__label">Sincronizza ispezioni esportate</span>
|
||||
<i class="nav-list__chevron ri-arrow-right-s-line" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="nav-list__item" @onclick="ExportLog">
|
||||
<i class="nav-list__icon ri-file-upload-line" aria-hidden="true"></i>
|
||||
<span class="nav-list__label">Invia i log allo sviluppo</span>
|
||||
<i class="nav-list__chevron ri-arrow-right-s-line" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@* Le voci distruttive portano l'icona nel tono danger e non hanno chevron. *@
|
||||
<button type="button" class="nav-list__item nav-list__item--danger" @onclick="ConfirmLogout">
|
||||
<i class="nav-list__icon ri-logout-box-r-line" aria-hidden="true"></i>
|
||||
<span class="nav-list__label">Esci</span>
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<SpinnerOverlay VisibleOverlay="VisibleOverlay"/>
|
||||
<SpinnerOverlay VisibleOverlay="VisibleOverlay" Message="@_progressMessage"/>
|
||||
<ConfirmMessageBox @ref="_logoutBox" NoText="Annulla" YesText="Esci" YesColor="Color.Error"
|
||||
Message="Uscendo servirà di nuovo la connessione per rientrare. Le ispezioni salvate sul dispositivo restano al loro posto."/>
|
||||
|
||||
@code {
|
||||
private bool IsLoggedIn { get; set; }
|
||||
private bool VisibleOverlay { get; set; }
|
||||
private string? _progressMessage;
|
||||
private string? CodHash { get; set; } = "";
|
||||
|
||||
private ConfirmMessageBox _logoutBox = null!;
|
||||
|
||||
private Core.Helpers.StatusBadgeHelper.BadgeSpec ConnessioneBadge => NetworkService.ConnectionAvailable
|
||||
? new Core.Helpers.StatusBadgeHelper.BadgeSpec("success", "ri-cloud-line", "Online")
|
||||
: new Core.Helpers.StatusBadgeHelper.BadgeSpec("danger", "ri-wifi-off-line", "Offline");
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoggedIn = await UserSession.IsLoggedIn();
|
||||
@@ -122,46 +108,72 @@
|
||||
|
||||
private async Task ExportLog()
|
||||
{
|
||||
_progressMessage = "Invio dei log…";
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
|
||||
var profiloAzienda = LocalStorage.GetString("codHash");
|
||||
|
||||
var email = new SendEmailDto
|
||||
|
||||
try
|
||||
{
|
||||
FromName = "Integry Log",
|
||||
To = "developer@integry.it",
|
||||
Subject = $"SteUP - Log del {DateTime.Today:d} di {UserSession.User.Username}",
|
||||
IsHtml = true,
|
||||
MsgText = $"Username: <b>{UserSession.User.Username}</b><br/>" +
|
||||
$"Profilo azienda: <b>{profiloAzienda}</b><br/>" +
|
||||
$"ProfileDb: <b>{UserSession.ProfileDb}</b><br/>" +
|
||||
$"Versione app: <b>{GenericSystemService.GetCurrentAppVersion()}</b>",
|
||||
Attachments = FileManager.GetFileForExport()
|
||||
};
|
||||
|
||||
await IntegryApiService.SendEmail(email);
|
||||
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
var profiloAzienda = LocalStorage.GetString("codHash");
|
||||
|
||||
var email = new SendEmailDto
|
||||
{
|
||||
FromName = "Integry Log",
|
||||
To = "developer@integry.it",
|
||||
Subject = $"SteUP - Log del {DateTime.Today:d} di {UserSession.User.Username}",
|
||||
IsHtml = true,
|
||||
MsgText = $"Username: <b>{UserSession.User.Username}</b><br/>" +
|
||||
$"Profilo azienda: <b>{profiloAzienda}</b><br/>" +
|
||||
$"ProfileDb: <b>{UserSession.ProfileDb}</b><br/>" +
|
||||
$"Versione app: <b>{GenericSystemService.GetCurrentAppVersion()}</b>",
|
||||
Attachments = FileManager.GetFileForExport()
|
||||
};
|
||||
|
||||
await IntegryApiService.SendEmail(email);
|
||||
Snackbar.Add("Log inviati allo sviluppo", Severity.Success);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Snackbar.Add("Invio dei log non riuscito. Controlla la connessione e riprova.", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
_progressMessage = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateDb()
|
||||
{
|
||||
_progressMessage = "Sincronizzazione…";
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
|
||||
await SteupDataService.CheckAndUpdateStatus();
|
||||
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
try
|
||||
{
|
||||
await SteupDataService.CheckAndUpdateStatus();
|
||||
Snackbar.Add("Ispezioni aggiornate", Severity.Success);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Snackbar.Add("Sincronizzazione non riuscita. Controlla la connessione e riprova.", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
_progressMessage = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Logout()
|
||||
private async Task ConfirmLogout()
|
||||
{
|
||||
var result = await _logoutBox.ShowAsync();
|
||||
if (result is not true) return;
|
||||
|
||||
await AuthenticationStateProvider.SignOut();
|
||||
IsLoggedIn = await UserSession.IsLoggedIn();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.container-primary-info {
|
||||
/*background: var(--light-card-background);*/
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.container-primary-info .divider {
|
||||
margin: 0 0 0 7rem;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.section-primary-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: .8rem 1.2rem .4rem;
|
||||
}
|
||||
|
||||
.personal-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.info-nome {
|
||||
color: var(--mud-palette-text-primary);
|
||||
font-weight: 800;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
color: var(--mud-palette-gray-default);
|
||||
font-size: medium;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
padding: .4rem 1.2rem .8rem;
|
||||
}
|
||||
|
||||
.section-personal-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-personal-info > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: normal;
|
||||
margin: .25rem 0;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
color: var(--mud-palette-gray-darker);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: var(--mud-palette-text-secondary);
|
||||
font-weight: 700;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.content ::deep .user-button { border: 1px solid var(--card-border-color) !important; }
|
||||
|
||||
.user-button > i { font-size: large; }
|
||||
|
||||
.user-button > span {
|
||||
font-size: medium;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status { font-weight: 700; }
|
||||
|
||||
.status.online { color: var(--mud-palette-success); }
|
||||
|
||||
.status.offline { color: var(--mud-palette-error); }
|
||||
@@ -1,6 +0,0 @@
|
||||
@using SteUp.Shared.Core.Interface.System
|
||||
@inject IGenericSystemService GenericSystemService
|
||||
|
||||
<div class="app-version">
|
||||
<span>@GenericSystemService.GetCurrentAppVersion()</span>
|
||||
</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
.app-version{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.app-version span{
|
||||
font-size: smaller;
|
||||
color: var(--mud-palette-gray-darker);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@using SteUp.Shared.Core.Helpers
|
||||
|
||||
@*
|
||||
Badge di stato (STILE_UI §6.4). Non riceve colori: riceve uno stato, e la
|
||||
mappatura in StatusBadgeHelper decide tono, icona ed etichetta.
|
||||
*@
|
||||
|
||||
<span class="badge badge--@Spec.Tone" aria-label="@(AriaLabel ?? Spec.Label)">
|
||||
<i class="badge__icon @Spec.Icon" aria-hidden="true"></i>
|
||||
<span>@(Text ?? Spec.Label)</span>
|
||||
</span>
|
||||
|
||||
@code {
|
||||
[Parameter] public required StatusBadgeHelper.BadgeSpec Spec { get; set; }
|
||||
|
||||
/// Testo alternativo, per i casi in cui l'etichetta porta anche un conteggio.
|
||||
[Parameter] public string? Text { get; set; }
|
||||
|
||||
[Parameter] public string? AriaLabel { get; set; }
|
||||
}
|
||||
@@ -1,105 +1,87 @@
|
||||
@using SteUp.Shared.Core.Dto
|
||||
@using SteUp.Shared.Core.Entities
|
||||
@using SteUp.Shared.Core.Helpers
|
||||
|
||||
<div class="container-primary-info mud-elevation-1 @Class @(OnClick.HasDelegate ? "ripple-container" : "")" @onclick="OnCardClick">
|
||||
<div class="section-primary-info">
|
||||
<div class="inspection-info">
|
||||
@if (CompactView)
|
||||
{
|
||||
if (OnLoading)
|
||||
{
|
||||
<MudSkeleton Width="40vw"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="info-title compactView">
|
||||
@PuntoVendita.CodMdep - @PuntoVendita.Descrizione
|
||||
</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OnLoading)
|
||||
{
|
||||
<MudSkeleton Width="40vw"/>
|
||||
<MudSkeleton Width="55vw"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="info-title">
|
||||
@PuntoVendita.CodMdep - @PuntoVendita.Descrizione
|
||||
</span>
|
||||
<span class="info-subtitle">
|
||||
@PuntoVendita.Indirizzo
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@*
|
||||
Card di elenco (STILE_UI §6.5). Il titolo e' l'entita' — il punto vendita —
|
||||
non il tipo di record. I conteggi stanno sempre a vista: il rilevatore deve
|
||||
sapere quanto ha davanti senza contare.
|
||||
*@
|
||||
|
||||
<div class="list-card @(Tappable ? "panel--tappable" : "")"
|
||||
@onclick="OnCardClick"
|
||||
role="@(Tappable ? "button" : null)"
|
||||
tabindex="@(Tappable ? 0 : -1)"
|
||||
aria-label="@Ispezione.ToSemanticLabel()">
|
||||
|
||||
<div class="list-card__head">
|
||||
<div class="list-card__ident">
|
||||
<div class="t-ident">@Ispezione.CodMdep</div>
|
||||
<h3 class="list-card__title">@Descrizione</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
@if (!string.IsNullOrWhiteSpace(Indirizzo))
|
||||
{
|
||||
<p class="list-card__meta">@Indirizzo</p>
|
||||
}
|
||||
|
||||
<div class="section-info">
|
||||
<div class="section-inspection-info">
|
||||
<div>
|
||||
<span class="info-inspection-title">Data ispezione</span>
|
||||
@if (OnLoading)
|
||||
{
|
||||
<MudSkeleton Width="100%"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Default">
|
||||
@Ispezione.Data.ToString("d")
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-card__badges badge-row">
|
||||
<Badge Spec="@Ispezione.Stato.ToBadge()"/>
|
||||
|
||||
<div class="section-inspection-info">
|
||||
<div>
|
||||
<span class="info-inspection-title">Stato ispezione</span>
|
||||
@if (OnLoading)
|
||||
{
|
||||
<MudSkeleton Width="100%"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="@Ispezione.Stato.GetColor()">
|
||||
@Ispezione.Stato.ConvertToHumanReadable()
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (DaInviare > 0)
|
||||
{
|
||||
<Badge Spec="@SyncState.DaInviare.ToBadge()" Text="@($"{DaInviare} da inviare")"/>
|
||||
}
|
||||
|
||||
@if (Bozze > 0)
|
||||
{
|
||||
<Badge Spec="@SyncState.Bozza.ToBadge()" Text="@($"{Bozze} da completare")"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="list-card__footer">
|
||||
<i class="icon ri-calendar-line" aria-hidden="true"></i>
|
||||
<span>@Ispezione.Data.ToString("d MMMM yyyy")</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>@SchedeLabel</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public string Class { get; set; } = string.Empty;
|
||||
[Parameter] public Ispezione Ispezione { get; set; } = new();
|
||||
[Parameter] public bool CompactView { get; set; }
|
||||
[Parameter] public EventCallback<Ispezione> OnClick { get; set; }
|
||||
|
||||
private PuntoVenditaDto PuntoVendita { get; set; } = new();
|
||||
private bool Tappable => OnClick.HasDelegate;
|
||||
|
||||
private bool OnLoading { get; set; } = true;
|
||||
private PuntoVenditaDto? _puntoVendita;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
// Il punto vendita puo' mancare dalla cache di riferimento (primo avvio
|
||||
// offline): si degrada al codice invece di far fallire la card.
|
||||
private string Descrizione => _puntoVendita?.Descrizione.IsNullOrEmpty() == false
|
||||
? _puntoVendita.Descrizione!
|
||||
: Ispezione.CodMdep;
|
||||
|
||||
private string? Indirizzo => _puntoVendita is null
|
||||
? null
|
||||
: string.Join(" · ", new[] { _puntoVendita.Indirizzo, _puntoVendita.Citta }
|
||||
.Where(x => !x.IsNullOrEmpty()));
|
||||
|
||||
private int DaInviare => Ispezione.Schede.CountDaInviare();
|
||||
private int Bozze => Ispezione.Schede.CountBozze();
|
||||
|
||||
private string SchedeLabel => Ispezione.Schede.Count == 1
|
||||
? "1 scheda"
|
||||
: $"{Ispezione.Schede.Count} schede";
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
OnLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var puntoVendita = SteupDataService.PuntiVenditaList.Find(x =>
|
||||
_puntoVendita = SteupDataService.PuntiVenditaList.Find(x =>
|
||||
x.CodMdep != null && x.CodMdep.EqualsIgnoreCase(Ispezione.CodMdep)
|
||||
);
|
||||
await Task.Delay(250);
|
||||
|
||||
PuntoVendita = puntoVendita ?? throw new Exception("Punto vendita non trovato");
|
||||
OnLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private Task OnCardClick() =>
|
||||
OnClick.HasDelegate ? OnClick.InvokeAsync(Ispezione) : Task.CompletedTask;
|
||||
}
|
||||
Tappable ? OnClick.InvokeAsync(Ispezione) : Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
.container-primary-info {
|
||||
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 {
|
||||
margin-bottom: 0 !important;
|
||||
min-height: max-content;
|
||||
}
|
||||
|
||||
.container-primary-info .divider {
|
||||
margin: .25rem 0;
|
||||
}
|
||||
|
||||
.section-primary-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: .8rem 1.2rem .4rem;
|
||||
}
|
||||
|
||||
.personal-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.inspection-info{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
color: var(--mud-palette-text-primary);
|
||||
font-weight: 800;
|
||||
font-size: x-large;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.info-subtitle {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.info-title.compactView{
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.section-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
padding: .4rem 1.2rem .8rem;
|
||||
}
|
||||
|
||||
.section-inspection-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-inspection-info > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: normal;
|
||||
margin: .25rem 0;
|
||||
}
|
||||
|
||||
.info-inspection-title {
|
||||
color: var(--mud-palette-gray-darker);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.info-inspection-text {
|
||||
color: var(--mud-palette-text-secondary);
|
||||
font-weight: 700;
|
||||
font-size: small;
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
<div class="card-form-modal">
|
||||
<div class="card-form-modal-title @ClassTitle">
|
||||
<MudText Style="font-weight: 800" Typo="Typo.subtitle1">@Title</MudText>
|
||||
</div>
|
||||
<div class="card-form-modal-body @ClassBody">
|
||||
@if (Loading)
|
||||
@* Gruppo di campo del form scheda: un pannello con la sua etichetta e,
|
||||
quando serve, il conteggio di cio' che contiene. *@
|
||||
|
||||
<div class="form-group @Class">
|
||||
<div class="form-group__label">
|
||||
<span>@Title</span>
|
||||
@if (!string.IsNullOrWhiteSpace(Count))
|
||||
{
|
||||
<MudSkeleton/>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ChildContent
|
||||
<span class="form-group__count">@Count</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Loading)
|
||||
{
|
||||
<div class="skeleton skeleton--line" style="height: 24px;" aria-hidden="true"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Title { get; set; } = "";
|
||||
[Parameter] public string? Count { get; set; }
|
||||
[Parameter] public bool Loading { get; set; }
|
||||
[Parameter] public string Class { get; set; } = "";
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
[Parameter] public string ClassTitle { get; set; } = "";
|
||||
[Parameter] public string ClassBody { get; set; } = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.card-form-modal {
|
||||
background: var(--mud-palette-table-striped);
|
||||
border-radius: 20px;
|
||||
padding: .5rem 1rem;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.is-dark .card-form-modal {
|
||||
background: var(--mud-palette-background); !important;
|
||||
}
|
||||
|
||||
.card-form-modal-body ::deep .file-upload-input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
small ::deep {
|
||||
color: var(--mud-palette-text-primary) !important;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@using SteUp.Shared.Components.Layout.Overlay
|
||||
@using SteUp.Shared.Components.SingleElements.MessageBox
|
||||
@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
|
||||
@@ -13,74 +13,125 @@
|
||||
@inject IFileManager FileManager
|
||||
@inject ILogger<SchedaCard> Logger
|
||||
|
||||
<div class="scheda-card">
|
||||
<div class="scheda-body-section">
|
||||
<div class="title-section">
|
||||
<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">
|
||||
@Scheda.ImageNames.Count()
|
||||
</MudChip>
|
||||
<MudChip T="string" Icon="@Icons.Material.Rounded.QrCode2" Size="Size.Small" Color="Color.Default">
|
||||
@Scheda.Articoli.Count
|
||||
</MudChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scheda-card-action">
|
||||
@if (Ispezione.Stato != StatusEnum.Completata)
|
||||
{
|
||||
<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.Primary" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.CloudUpload"
|
||||
Label="Invia" aria-label="Invia scheda al server" OnClick="@ExportScheda"/>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<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 class="list-card" aria-label="@SemanticLabel">
|
||||
<div class="list-card__head">
|
||||
<div class="list-card__ident">
|
||||
<div class="t-ident">@Scheda.DescrizioneReparto</div>
|
||||
<h3 class="list-card__title">@Titolo</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-card__badges badge-row">
|
||||
<Badge Spec="@SyncState.ToBadge()"/>
|
||||
<Badge Spec="@ScadenzaBadge" />
|
||||
</div>
|
||||
|
||||
@if (!Scheda.Note.IsNullOrEmpty())
|
||||
{
|
||||
<p class="list-card__meta t-prose">@Scheda.Note</p>
|
||||
}
|
||||
|
||||
<div class="list-card__footer">
|
||||
<i class="icon ri-qr-code-line" aria-hidden="true"></i>
|
||||
<span>@ArticoliLabel</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<i class="icon ri-camera-line" aria-hidden="true"></i>
|
||||
<span>@FotoLabel</span>
|
||||
</div>
|
||||
|
||||
<div class="list-card__actions">
|
||||
@if (ReadOnly)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.Visibility" OnClick="@ViewScheda">
|
||||
Apri
|
||||
</MudButton>
|
||||
|
||||
@if (Scheda.ImageNames?.Count > 0)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.CloudUpload" OnClick="@ExportImg">
|
||||
Invia foto
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.Edit" OnClick="@UpdateScheda">
|
||||
@(IsBozza ? "Completa" : "Modifica")
|
||||
</MudButton>
|
||||
|
||||
@if (NetworkService.IsNetworkAvailable() && SyncState == Core.Helpers.SyncState.DaInviare)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.CloudUpload" OnClick="@ExportScheda">
|
||||
Invia
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!ReadOnly)
|
||||
{
|
||||
<MudButton Class="mt-2" Variant="Variant.Text" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.DeleteOutline"
|
||||
Style="color: var(--danger-ink)" OnClick="@DeleteScheda">
|
||||
Elimina scheda
|
||||
</MudButton>
|
||||
}
|
||||
</div>
|
||||
|
||||
<SpinnerOverlay VisibleOverlay="VisibleOverlay"/>
|
||||
<ConfirmMessageBox @ref="_messageBox" YesText="Cancella" YesColor="Color.Error"
|
||||
Message="Confermi la cancellazione della scheda corrente?"/>
|
||||
<ConfirmMessageBox @ref="_messageBox" NoText="Annulla" YesText="Elimina" YesColor="Color.Error"
|
||||
Message="La scheda verrà eliminata dal dispositivo e, se già inviata, anche dal server. L'operazione non è reversibile."/>
|
||||
|
||||
@code{
|
||||
[Parameter] public Ispezione Ispezione { get; set; } = new();
|
||||
[Parameter] public Ispezione Ispezione { get; set; } = new();
|
||||
[Parameter] public required Scheda Scheda { get; set; }
|
||||
[Parameter] public EventCallback<Scheda> OnSchedaModified { get; set; }
|
||||
[Parameter] public EventCallback<Scheda> OnSchedaDeleted { get; set; }
|
||||
|
||||
private bool VisibleOverlay { get; set; }
|
||||
[Parameter] public EventCallback<bool> OnBusyChanged { get; set; }
|
||||
|
||||
private ConfirmMessageBox _messageBox = null!;
|
||||
private bool _busy;
|
||||
|
||||
private bool ReadOnly => Ispezione.Stato == StatusEnum.Completata;
|
||||
private SyncState SyncState => Scheda.GetSyncState();
|
||||
private bool IsBozza => SyncState == Core.Helpers.SyncState.Bozza;
|
||||
|
||||
// Una bozza puo' non avere ancora il motivo: meglio dirlo che lasciare un vuoto.
|
||||
private string Titolo => Scheda.ActivityTypeId.IsNullOrEmpty()
|
||||
? "Motivo da indicare"
|
||||
: Scheda.ActivityTypeId!;
|
||||
|
||||
private StatusBadgeHelper.BadgeSpec ScadenzaBadge => (ScadenzaEnum)Scheda.Scadenza switch
|
||||
{
|
||||
ScadenzaEnum.Altissima24Ore => new StatusBadgeHelper.BadgeSpec("danger", "ri-alarm-warning-line", "Entro 24 ore"),
|
||||
ScadenzaEnum.Alta1Settimana => new StatusBadgeHelper.BadgeSpec("warning", "ri-time-line", "Entro 1 settimana"),
|
||||
ScadenzaEnum.Media1Mese => new StatusBadgeHelper.BadgeSpec("info", "ri-calendar-line", "Entro 1 mese"),
|
||||
_ => new StatusBadgeHelper.BadgeSpec("neutral", "ri-calendar-line", "Entro 2 mesi")
|
||||
};
|
||||
|
||||
private string ArticoliLabel => Scheda.Articoli.Count == 1
|
||||
? "1 articolo"
|
||||
: $"{Scheda.Articoli.Count} articoli";
|
||||
|
||||
private string FotoLabel => Scheda.ImageNames?.Count switch
|
||||
{
|
||||
null or 0 => "nessuna foto",
|
||||
1 => "1 foto",
|
||||
var n => $"{n} foto"
|
||||
};
|
||||
|
||||
private string SemanticLabel =>
|
||||
$"Scheda {Titolo}, reparto {Scheda.DescrizioneReparto}, {SyncState.ToBadge().Label}, {ArticoliLabel}, {FotoLabel}";
|
||||
|
||||
private async Task SetBusy(bool value)
|
||||
{
|
||||
_busy = value;
|
||||
await OnBusyChanged.InvokeAsync(value);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task UpdateScheda()
|
||||
{
|
||||
@@ -88,15 +139,18 @@
|
||||
if (modal is { Canceled: false, Data: Scheda scheda }) await OnSchedaModified.InvokeAsync(scheda);
|
||||
}
|
||||
|
||||
private void ViewScheda() =>
|
||||
_ = Dialog.OpenFormScheda(Ispezione.CodMdep, Ispezione.Data, false, Scheda, true);
|
||||
|
||||
private async Task DeleteScheda()
|
||||
{
|
||||
var result = await _messageBox.ShowAsync();
|
||||
if (result is not true) return;
|
||||
|
||||
if (result is true)
|
||||
await SetBusy(true);
|
||||
|
||||
try
|
||||
{
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
|
||||
if (Scheda.ActivityId != null)
|
||||
await IntegrySteupService.DeleteScheda(Scheda.ActivityId);
|
||||
|
||||
@@ -110,22 +164,18 @@
|
||||
|
||||
var deleteScheda = await IspezioniService.DeleteSchedaAsync(Scheda.Id);
|
||||
if (deleteScheda) await OnSchedaDeleted.InvokeAsync(Scheda);
|
||||
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await SetBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewScheda() =>
|
||||
_ = Dialog.OpenFormScheda(Ispezione.CodMdep, Ispezione.Data, false, Scheda, true);
|
||||
|
||||
private async Task ExportScheda()
|
||||
{
|
||||
// Guardia anti doppio-tap: invio irreversibile al server.
|
||||
if (VisibleOverlay) return;
|
||||
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
if (_busy) return;
|
||||
await SetBusy(true);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -172,17 +222,14 @@
|
||||
}
|
||||
finally
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
await SetBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportImg()
|
||||
{
|
||||
if (VisibleOverlay || Scheda.ImageNames == null) return;
|
||||
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
if (_busy || Scheda.ImageNames == null) return;
|
||||
await SetBusy(true);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -209,8 +256,7 @@
|
||||
}
|
||||
finally
|
||||
{
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
await SetBusy(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
.scheda-card{
|
||||
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: .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: .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;
|
||||
}
|
||||
@@ -1,60 +1,45 @@
|
||||
@using SteUp.Shared.Components.Layout.Overlay
|
||||
@using SteUp.Shared.Core.Dto
|
||||
@using SteUp.Shared.Core.Dto
|
||||
@using SteUp.Shared.Core.Interface.LocalDb
|
||||
@inject IIspezioniService IspezioniService
|
||||
|
||||
<div class="shop-card ripple-container" @onclick="StartInspection">
|
||||
<div class="shop-body-section">
|
||||
<div class="title-section">
|
||||
<MudText Class="shop-title" Typo="Typo.subtitle1">
|
||||
<b>@PuntoVendita.CodMdep</b> - @PuntoVendita.Descrizione
|
||||
</MudText>
|
||||
</div>
|
||||
<div class="list-card panel--tappable" @onclick="StartInspection" role="button" tabindex="0"
|
||||
aria-label="@($"Apri ispezione su {PuntoVendita.CodMdep} {PuntoVendita.Descrizione}")">
|
||||
|
||||
@if (!PuntoVendita.Indirizzo.IsNullOrEmpty())
|
||||
{
|
||||
<div class="subtitle-section">
|
||||
<MudText Class="shop-title" Typo="Typo.subtitle1">
|
||||
@PuntoVendita.Indirizzo
|
||||
</MudText>
|
||||
</div>
|
||||
<div class="sub-info-section">
|
||||
<MudChip T="string" Icon="@Icons.Material.Rounded.LocationCity" Size="Size.Small" Color="Color.Default">
|
||||
@PuntoVendita.Citta
|
||||
</MudChip>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Default">
|
||||
@PuntoVendita.Cap
|
||||
</MudChip>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Default">
|
||||
@PuntoVendita.Provincia
|
||||
</MudChip>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="t-ident">@PuntoVendita.CodMdep</div>
|
||||
<h3 class="list-card__title">@PuntoVendita.Descrizione</h3>
|
||||
|
||||
@if (!Luogo.IsNullOrEmpty())
|
||||
{
|
||||
<p class="list-card__meta">@Luogo</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<SpinnerOverlay VisibleOverlay="VisibleOverlay"/>
|
||||
|
||||
@code {
|
||||
[Parameter] public PuntoVenditaDto PuntoVendita { get; set; } = null!;
|
||||
|
||||
private bool VisibleOverlay { get; set; }
|
||||
|
||||
[Parameter] public EventCallback OnStarting { get; set; }
|
||||
|
||||
// Indirizzo, citta', CAP e provincia su una riga sola con separatore " · ":
|
||||
// sono metadati, non meritano tre chip che fanno rumore.
|
||||
private string Luogo => string.Join(" · ", new[]
|
||||
{
|
||||
PuntoVendita.Indirizzo,
|
||||
PuntoVendita.Citta,
|
||||
string.Join(" ", new[] { PuntoVendita.Cap, PuntoVendita.Provincia }
|
||||
.Where(x => !x.IsNullOrEmpty()))
|
||||
}
|
||||
.Where(x => !x.IsNullOrEmpty()));
|
||||
|
||||
private async Task StartInspection()
|
||||
{
|
||||
VisibleOverlay = true;
|
||||
StateHasChanged();
|
||||
|
||||
await OnStarting.InvokeAsync();
|
||||
|
||||
SteupDataService.InspectionPageState.Ispezione = await IspezioniService.GetOrCreateIspezioneAsync(
|
||||
PuntoVendita.CodMdep!,
|
||||
DateTime.Now,
|
||||
UserSession.User.Username
|
||||
);
|
||||
|
||||
VisibleOverlay = false;
|
||||
StateHasChanged();
|
||||
|
||||
NavigationManager.NavigateTo("/ispezione");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.shop-card{
|
||||
padding: .5rem 1rem;
|
||||
background-color: var(--mud-palette-background-gray);
|
||||
border-radius: 1em;
|
||||
}
|
||||
@@ -1,26 +1,35 @@
|
||||
<div class="Connection @(ShowWarning ? "Show" : "Hide") @(IsNetworkAvailable? ServicesIsDown ? "ServicesIsDown" : "SystemOk" : "NetworkKo")">
|
||||
@if (IsNetworkAvailable)
|
||||
{
|
||||
if(ServicesIsDown)
|
||||
{
|
||||
<i class="ri-cloud-off-fill"></i>
|
||||
<span>Servizi offline</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="ri-cloud-fill"></i>
|
||||
<span>Online</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="ri-wifi-off-line"></i>
|
||||
<span>Nessuna connessione</span>
|
||||
}
|
||||
@*
|
||||
La firma visiva di "Offline e' la verita'": una striscia sottile ancorata in
|
||||
cima che scende quando lo stato di rete cambia. Mai il solo colore: sempre
|
||||
icona piu' etichetta, leggibile anche in pieno sole.
|
||||
*@
|
||||
|
||||
<div class="connection @(ShowWarning ? "connection--show" : "") @ToneClass"
|
||||
role="status" aria-live="polite">
|
||||
<i class="@IconClass" aria-hidden="true"></i>
|
||||
<span>@Label</span>
|
||||
</div>
|
||||
|
||||
@code{
|
||||
[Parameter] public bool IsNetworkAvailable { get; set; }
|
||||
[Parameter] public bool ServicesIsDown { get; set; }
|
||||
[Parameter] public bool ShowWarning { get; set; }
|
||||
}
|
||||
|
||||
private string ToneClass => !IsNetworkAvailable
|
||||
? "connection--offline"
|
||||
: ServicesIsDown
|
||||
? "connection--down"
|
||||
: "connection--ok";
|
||||
|
||||
private string IconClass => !IsNetworkAvailable
|
||||
? "ri-wifi-off-line"
|
||||
: ServicesIsDown
|
||||
? "ri-cloud-off-line"
|
||||
: "ri-cloud-line";
|
||||
|
||||
private string Label => !IsNetworkAvailable
|
||||
? "Nessuna connessione · il lavoro resta sul dispositivo"
|
||||
: ServicesIsDown
|
||||
? "Servizi non raggiungibili"
|
||||
: "Di nuovo online";
|
||||
}
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
@using Microsoft.Extensions.Logging
|
||||
@inject ILogger<ExceptionModal> Logger
|
||||
|
||||
<div class="container container-modal">
|
||||
<div class="c-modal">
|
||||
<div class="exception-header mb-2">
|
||||
<i class="ri-emotion-unhappy-line"></i>
|
||||
<span>Ops</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
@if (Exception != null)
|
||||
{
|
||||
<code>@Message</code>
|
||||
}
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<div @onclick="OnRetryClick" class="card-button">
|
||||
<span>Riprova</span>
|
||||
@*
|
||||
Schermata di ripiego quando un componente va in errore. Non lascia il
|
||||
rilevatore davanti a uno stack trace: dice cosa è successo in una frase e
|
||||
offre due vie d'uscita. Il dettaglio tecnico resta sotto, per l'assistenza.
|
||||
*@
|
||||
|
||||
<div class="onboarding">
|
||||
<div class="onboarding__panel">
|
||||
<div class="panel panel--lg">
|
||||
<div class="dialog-icon dialog-icon--danger" aria-hidden="true">
|
||||
<i class="ri-error-warning-line"></i>
|
||||
</div>
|
||||
|
||||
<div @onclick="OnContinueClick" class="card-button">
|
||||
<span>Continua</span>
|
||||
|
||||
<h1 class="dialog-title">Qualcosa si è inceppato</h1>
|
||||
<p class="dialog-message">
|
||||
Il lavoro salvato sul dispositivo non è andato perso. Riprova, e se succede di nuovo
|
||||
invia i log dalla schermata Profilo.
|
||||
</p>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<p class="dialog-detail selectable">@Message</p>
|
||||
}
|
||||
|
||||
<div class="action-bar" style="border: none; background: none; padding: var(--space-lg) 0 0;">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@OnContinueClick">Torna all'elenco</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@OnRetryClick">Riprova</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,4 +69,4 @@
|
||||
NavigationManager.NavigateTo("/");
|
||||
await OnRetry.InvokeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
.container-modal {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c-modal {
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--exception-box-shadow);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin: 1.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: medium;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-button {
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
padding: .3rem 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
|
||||
.exception-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.exception-header > i {
|
||||
font-size: 3rem;
|
||||
line-height: normal;
|
||||
color: var(--bs-danger);
|
||||
}
|
||||
|
||||
.exception-header > span {
|
||||
font-size: x-large;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
code {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: var(--bs-gray-dark);
|
||||
}
|
||||
@@ -1,27 +1,24 @@
|
||||
<MudDialog OnBackdropClick="Cancel">
|
||||
@*
|
||||
Dialog informativo (STILE_UI §6.10): cerchio-icona del tono, titolo,
|
||||
messaggio, un solo bottone a piena larghezza.
|
||||
*@
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@if (IsWarning)
|
||||
{
|
||||
<div class="exception-header mb-2">
|
||||
<i class="ri-error-warning-line warning-icon"></i>
|
||||
<span>Attenzione</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="exception-header mb-2">
|
||||
<i class="ri-emotion-unhappy-line error-icon"></i>
|
||||
<span>Ops</span>
|
||||
</div>
|
||||
}
|
||||
<div class="text">
|
||||
<code>@ErrorMessage</code>
|
||||
<div class="dialog-icon dialog-icon--@(IsWarning ? "warning" : "danger")" aria-hidden="true">
|
||||
<i class="@(IsWarning ? "ri-alert-line" : "ri-error-warning-line")"></i>
|
||||
</div>
|
||||
|
||||
<h2 class="dialog-title">@(IsWarning ? "Attenzione" : "Qualcosa non ha funzionato")</h2>
|
||||
<p class="dialog-message">@Message</p>
|
||||
|
||||
@if (ShowDetail)
|
||||
{
|
||||
<p class="dialog-detail">@ErrorMessage</p>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Text" OnClick="@Cancel" Size="Size.Small" Color="Color.Primary">
|
||||
Chiudi
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@Cancel">Ho capito</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@@ -30,5 +27,14 @@
|
||||
[Parameter] public string ErrorMessage { get; set; } = string.Empty;
|
||||
[Parameter] public bool IsWarning { get; set; }
|
||||
|
||||
// Gli avvisi sono gia' scritti per il rilevatore. Gli errori tecnici no:
|
||||
// si mostra una frase comprensibile e il dettaglio resta sotto, per chi
|
||||
// deve riferirlo all'assistenza.
|
||||
private bool ShowDetail => !IsWarning;
|
||||
|
||||
private string Message => IsWarning
|
||||
? ErrorMessage
|
||||
: "Il lavoro resta salvato sul dispositivo. Riprova, e se continua segnala il dettaglio qui sotto.";
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
.exception-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.exception-header > i {
|
||||
font-size: 3rem;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: var(--bs-danger);
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
color: var(--bs-warning);
|
||||
}
|
||||
|
||||
.exception-header > span {
|
||||
font-size: x-large;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: medium;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
code {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: var(--bs-gray-dark);
|
||||
}
|
||||
@@ -1,27 +1,35 @@
|
||||
@using SteUp.Shared.Components.Layout
|
||||
@using SteUp.Shared.Core.Dto
|
||||
@using SteUp.Shared.Core.Dto
|
||||
@using SteUp.Shared.Core.Interface.System
|
||||
@inject IAttachedService AttachedService
|
||||
|
||||
<MudDialog Class="disable-safe-area">
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<HeaderLayout SmallHeader="true" Cancel="true" OnCancel="@(() => MudDialog.Cancel())" Title="Aggiungi allegati"/>
|
||||
<h2 class="dialog-title">Aggiungi una foto</h2>
|
||||
<p class="dialog-message">Scatta sul posto oppure scegli un'immagine già sul dispositivo.</p>
|
||||
|
||||
<div style="margin-bottom: 1rem;" class="content attached">
|
||||
<MudFab Size="Size.Small" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Rounded.CameraAlt"
|
||||
Label="Camera" OnClick="@OnCamera"/>
|
||||
<div class="stack stack-xs" style="margin-top: var(--space-md);">
|
||||
<button type="button" class="action-tile" @onclick="@OnCamera">
|
||||
<span class="action-tile__icon action-tile__icon--primary">
|
||||
<i class="ri-camera-line" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="action-tile__label">Scatta una foto</span>
|
||||
</button>
|
||||
|
||||
<MudFab Size="Size.Small" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Rounded.Image"
|
||||
Label="Galleria" OnClick="@OnGallery"/>
|
||||
<button type="button" class="action-tile" @onclick="@OnGallery">
|
||||
<span class="action-tile__icon action-tile__icon--neutral">
|
||||
<i class="ri-image-line" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="action-tile__label">Scegli dalla galleria</span>
|
||||
</button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@(() => MudDialog.Cancel())">Annulla</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public bool CanAddPosition { get; set; }
|
||||
|
||||
private List<AttachedDto>? Attached { get; set; }
|
||||
|
||||
@@ -48,4 +56,4 @@
|
||||
|
||||
if (Attached != null) MudDialog.Close(Attached);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.content.attached {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 1rem;
|
||||
height: unset;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -24,174 +24,225 @@
|
||||
@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">
|
||||
<HeaderLayout Cancel="true" OnCancel="@Cancel" Title="Scheda"/>
|
||||
<div class="app-shell">
|
||||
|
||||
<div class="content">
|
||||
<HeaderLayout Title="@(IsNew ? "Nuova scheda" : ReadOnly ? "Scheda" : "Modifica scheda")"
|
||||
Subtitle="@Scheda.DescrizioneReparto"
|
||||
Cancel="true" OnCancel="@Cancel" Compact="true"/>
|
||||
|
||||
<CardFormModal Title="Reparto" Loading="SteupDataService.Reparti.IsNullOrEmpty()">
|
||||
<MudSelectExtended T="JtbFasiDto?" Variant="Variant.Text"
|
||||
@bind-Value="Scheda.Reparto" ToStringFunc="@(x => x?.Descrizione)"
|
||||
@bind-Value:after="OnAfterChangeReparto" Required="true"
|
||||
RequiredError="Reparto obbligatorio">
|
||||
@foreach (var fasi in SteupDataService.Reparti)
|
||||
<div class="app-scroll">
|
||||
<div class="page-width stack stack-10" style="padding-bottom: var(--space-lg);">
|
||||
|
||||
@if (IsBozza)
|
||||
{
|
||||
<MudSelectItemExtended Class="custom-item-select" Value="@fasi">
|
||||
@fasi.Descrizione
|
||||
</MudSelectItemExtended>
|
||||
<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>
|
||||
}
|
||||
</MudSelectExtended>
|
||||
</CardFormModal>
|
||||
|
||||
<CardFormModal Title="Motivo" Loading="SteupDataService.TipiAttività.IsNullOrEmpty()">
|
||||
<MudSelectExtended ReadOnly="@Scheda.CodJfas.IsNullOrEmpty()" T="string?"
|
||||
Variant="Variant.Text"
|
||||
@bind-Value="Scheda.ActivityTypeId" @bind-Value:after="OnAfterChangeValue"
|
||||
Required="true" RequiredError="Motivo obbligatorio">
|
||||
@foreach (var type in SteupDataService.TipiAttività.Where(x => x.CodJfas.EqualsIgnoreCase(Scheda.CodJfas!)))
|
||||
{
|
||||
<MudSelectItemExtended Class="custom-item-select"
|
||||
Value="@type.ActivityTypeId">@type.ActivityTypeId</MudSelectItemExtended>
|
||||
}
|
||||
</MudSelectExtended>
|
||||
</CardFormModal>
|
||||
|
||||
@if (FileLoading)
|
||||
{
|
||||
<CardFormModal Title="Immagini">
|
||||
<div class="container-attached">
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-3"/>
|
||||
</div>
|
||||
</CardFormModal>
|
||||
}
|
||||
else if (!AttachedList.IsNullOrEmpty())
|
||||
{
|
||||
<CardFormModal Title="Immagini">
|
||||
<div class="container-attached">
|
||||
<div class="scroll-attached">
|
||||
@foreach (var item in AttachedList!.Select((p, index) => new { p, index }))
|
||||
<CardFormModal Title="Reparto" Loading="SteupDataService.Reparti.IsNullOrEmpty()">
|
||||
<MudSelectExtended T="JtbFasiDto?" Variant="Variant.Text"
|
||||
@bind-Value="Scheda.Reparto" ToStringFunc="@(x => x?.Descrizione)"
|
||||
@bind-Value:after="OnAfterChangeReparto" Required="true"
|
||||
Placeholder="Scegli il reparto"
|
||||
RequiredError="Indica il reparto">
|
||||
@foreach (var fasi in SteupDataService.Reparti)
|
||||
{
|
||||
<MudCard>
|
||||
@if (!item.p.ThumbPath.IsNullOrEmpty())
|
||||
{
|
||||
<MudCardMedia Image="@item.p.ThumbPath" Height="100"/>
|
||||
}
|
||||
<MudCardContent Class="image_card">
|
||||
<MudText Typo="Typo.subtitle1"><b>@item.p.Name</b></MudText>
|
||||
@if (item.p.ToUpload)
|
||||
<MudSelectItemExtended Class="custom-item-select" Value="@fasi">
|
||||
@fasi.Descrizione
|
||||
</MudSelectItemExtended>
|
||||
}
|
||||
</MudSelectExtended>
|
||||
</CardFormModal>
|
||||
|
||||
<CardFormModal Title="Motivo" Loading="SteupDataService.TipiAttività.IsNullOrEmpty()">
|
||||
@if (Scheda.CodJfas.IsNullOrEmpty())
|
||||
{
|
||||
<p class="t-meta">Scegli prima il reparto.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelectExtended T="string?" Variant="Variant.Text"
|
||||
@bind-Value="Scheda.ActivityTypeId"
|
||||
@bind-Value:after="OnAfterChangeValue"
|
||||
Required="true" Placeholder="Scegli il motivo"
|
||||
RequiredError="Indica il motivo">
|
||||
@foreach (var type in SteupDataService.TipiAttività.Where(x => x.CodJfas.EqualsIgnoreCase(Scheda.CodJfas!)))
|
||||
{
|
||||
<MudSelectItemExtended Class="custom-item-select"
|
||||
Value="@type.ActivityTypeId">@type.ActivityTypeId</MudSelectItemExtended>
|
||||
}
|
||||
</MudSelectExtended>
|
||||
}
|
||||
</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)
|
||||
{
|
||||
<MudIconButton Variant="Variant.Outlined"
|
||||
Icon="@Icons.Material.Rounded.Close"
|
||||
Size="Size.Small" Color="Color.Error"
|
||||
OnClick="@(() => OnRemoveAttached(item.index))"/>
|
||||
<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>
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</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>
|
||||
</div>
|
||||
</CardFormModal>
|
||||
}
|
||||
|
||||
</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)
|
||||
{
|
||||
<div class="container-button ripple-container">
|
||||
<MudButton Class="button-settings green-icon"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.AttachFile"
|
||||
Size="Size.Medium"
|
||||
OnClick="@OpenAddAttached"
|
||||
Variant="Variant.Outlined">
|
||||
Aggiungi foto
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
<CardFormModal Title="Articoli">
|
||||
@if (!ReadOnly)
|
||||
{
|
||||
<div class="input-manual-barcode">
|
||||
<MudTextField FullWidth="true" T="string?" Variant="Variant.Text"
|
||||
@bind-Value="ManualBarcode" Placeholder="Digita manualmente il codice"/>
|
||||
<MudIconButton Color="Color.Primary" OnClick="@OnManualBarcodeSet"
|
||||
Size="Size.Small" Icon="@Icons.Material.Rounded.Send"/>
|
||||
</div>
|
||||
}
|
||||
@if (!Scheda.Articoli.IsNullOrEmpty())
|
||||
{
|
||||
<div class="art-list">
|
||||
@foreach (var articolo in Scheda.Articoli)
|
||||
{
|
||||
<MudChip T="string" OnClose="@(() => RemoveArt(articolo.Barcode))"
|
||||
style="height: auto;">
|
||||
<MudStack Direction="Column" Spacing="0" class="py-1">
|
||||
<MudText Typo="Typo.subtitle2" Style="line-height: 1.1; font-weight: 700;">
|
||||
@articolo.Descrizione
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Style="line-height: 1.5;">
|
||||
Barcode: @articolo.Barcode
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</CardFormModal>
|
||||
|
||||
@if (NetworkService.ConnectionAvailable && !ReadOnly)
|
||||
{
|
||||
<div class="container-button ripple-container">
|
||||
<MudButton Class="button-settings red-icon"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.Balance"
|
||||
Size="Size.Medium"
|
||||
OnClick="@OpenSelectArt"
|
||||
Variant="Variant.Outlined">
|
||||
Consulta articoli
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
<CardFormModal Title="Scadenza">
|
||||
<MudSelectExtended FullWidth="true" T="int" Variant="Variant.Text"
|
||||
@bind-Value="@Scheda.Scadenza" @bind-Value:after="OnAfterChangeValue">
|
||||
<MudSelectItemExtended Class="custom-item-select" Text="24H" Value="3"/>
|
||||
<MudSelectItemExtended Class="custom-item-select" Text="1 Settimana" Value="2"/>
|
||||
<MudSelectItemExtended Class="custom-item-select" Text="1 Mese" Value="1"/>
|
||||
<MudSelectItemExtended Class="custom-item-select" Text="2 Mesi" Value="0"/>
|
||||
</MudSelectExtended>
|
||||
</CardFormModal>
|
||||
|
||||
<CardFormModal Title="Responsabile">
|
||||
<MudTextField FullWidth="true" T="string?" Variant="Variant.Text"
|
||||
@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"
|
||||
@bind-Value="Scheda.Note" @bind-Value:after="OnAfterChangeValue"
|
||||
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
|
||||
</CardFormModal>
|
||||
|
||||
<div class="container-button">
|
||||
<MudButton Class="button-settings primary-icon"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Rounded.Description"
|
||||
Size="Size.Medium"
|
||||
OnClick="@SuggestActivityDescription"
|
||||
Variant="Variant.Outlined">
|
||||
Suggerisci note descrittive
|
||||
</MudButton>
|
||||
</div>
|
||||
|
||||
@if (!LabelSave.IsNullOrEmpty())
|
||||
{
|
||||
<div class="container-button">
|
||||
<MudButton OnClick="@Save" Color="Color.Primary" Variant="Variant.Filled" FullWidth="true">
|
||||
@LabelSave
|
||||
@* 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>
|
||||
}
|
||||
@@ -203,7 +254,7 @@
|
||||
</MudDialog>
|
||||
|
||||
<SpinnerOverlay VisibleOverlay="VisibleOverlay" SuccessAnimation="SuccessAnimation"/>
|
||||
<LoaderLayout Visible="OnLoading" Text="@TextLoading"/>
|
||||
<SpinnerOverlay VisibleOverlay="OnLoading" Message="@TextLoading"/>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
@@ -215,6 +266,45 @@
|
||||
|
||||
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; }
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
.container-attached {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.container-attached .divider {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
.input-manual-barcode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.scroll-attached {
|
||||
max-height: 185px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.container-attached ::deep .image_card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.container-button {
|
||||
margin-bottom: unset !important;
|
||||
}
|
||||
|
||||
.input-card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.art-list {
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.art-list ::deep p {
|
||||
white-space: normal;
|
||||
}
|
||||
@@ -1,59 +1,128 @@
|
||||
@using SteUp.Shared.Components.Layout
|
||||
@using SteUp.Shared.Core.Dto
|
||||
|
||||
<MudDialog OnBackdropClick="Cancel">
|
||||
<DialogContent>
|
||||
<HeaderLayout SmallHeader="true" Cancel="true" OnCancel="@(() => MudDialog.Cancel())"
|
||||
Title="Seleziona articoli"/>
|
||||
@*
|
||||
Scelta degli articoli dalla griglia. Al posto del menu a tendina a selezione
|
||||
multipla, un elenco a schermo intero con righe da 56px: si centra con i
|
||||
guanti, e il conteggio dei selezionati resta sempre a vista.
|
||||
*@
|
||||
|
||||
@if (!Articoli.IsNullOrEmpty())
|
||||
{
|
||||
<MudSelectExtended MultiSelection="true" ItemCollection="Articoli" SearchBox="true"
|
||||
SearchBoxAutoFocus="true" @bind-SelectedValues="ArticoliSelected"
|
||||
SearchFunc="SearchFunc" T="ArticoliInGrigliaDto" Virtualize="true"
|
||||
Label="Articoli" AnchorOrigin="Origin.BottomCenter" Variant="Variant.Outlined"
|
||||
SearchBoxPlaceholder="Descrizione articolo" SearchBoxClearable="true"
|
||||
MultiSelectionTextFunc="MultiSelectionTextFunc" ToStringFunc="ToStringFunc"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="spinner-container" style="height: unset !important; margin-bottom: 1rem;">
|
||||
<MudIcon Size="Size.Large" Color="Color.Error" Icon="@Icons.Material.Rounded.Close"/>
|
||||
<MudText>Nessun articolo trovato</MudText>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<div class="app-shell">
|
||||
<HeaderLayout Title="Articoli" Subtitle="@Conteggio" Cancel="true"
|
||||
OnCancel="@Cancel" Compact="true"/>
|
||||
|
||||
@if (!Articoli.IsNullOrEmpty())
|
||||
{
|
||||
<div class="page-width" style="padding-bottom: var(--space-sm);">
|
||||
<div class="search-field">
|
||||
<i class="search-field__icon ri-search-line" aria-hidden="true"></i>
|
||||
<input type="search" placeholder="Cerca per descrizione o codice"
|
||||
autocapitalize="none" autocorrect="off"
|
||||
aria-label="Cerca un articolo"
|
||||
value="@FilterText" @oninput="OnFilterInput"/>
|
||||
@if (!FilterText.IsNullOrEmpty())
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Rounded.Close" Size="Size.Small"
|
||||
aria-label="Cancella la ricerca" OnClick="@ClearFilter"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="app-scroll">
|
||||
<div class="page-width page-body--flush">
|
||||
@if (Articoli.IsNullOrEmpty())
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="empty-state__icon ri-inbox-line" aria-hidden="true"></i>
|
||||
<p class="empty-state__title">Nessun articolo in griglia</p>
|
||||
<p class="empty-state__text">
|
||||
Per questo reparto e motivo non ci sono articoli: usa lo scanner o digita il codice.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else if (Filtrati.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="empty-state__icon ri-search-line" aria-hidden="true"></i>
|
||||
<p class="empty-state__title">Nessun risultato</p>
|
||||
<p class="empty-state__text">Nessun articolo corrisponde alla ricerca.</p>
|
||||
<div class="empty-state__action">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@ClearFilter">
|
||||
Azzera la ricerca
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stack stack-xs">
|
||||
<Virtualize Items="Filtrati" Context="art" OverscanCount="8">
|
||||
<button type="button" class="select-row"
|
||||
aria-pressed="@(IsSelected(art) ? "true" : "false")"
|
||||
@onclick="@(() => Toggle(art))">
|
||||
<span class="select-row__check" aria-hidden="true">
|
||||
<i class="ri-check-line"></i>
|
||||
</span>
|
||||
<span class="select-row__text">
|
||||
<span class="select-row__title">@art.Descrizione</span>
|
||||
<span class="select-row__meta">@art.Barcode</span>
|
||||
</span>
|
||||
</button>
|
||||
</Virtualize>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Articoli.IsNullOrEmpty())
|
||||
{
|
||||
<div class="action-bar">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@Cancel">Annulla</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
Disabled="@(_selected.Count == 0)" OnClick="@Submit">
|
||||
@(_selected.Count == 0 ? "Aggiungi" : $"Aggiungi {_selected.Count}")
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@if (!Articoli.IsNullOrEmpty())
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" OnClick="@Submit" Size="Size.Small" Color="Color.Primary">
|
||||
Seleziona
|
||||
</MudButton>
|
||||
}
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public List<ArticoliInGrigliaDto>? Articoli { get; set; }
|
||||
|
||||
private IEnumerable<ArticoliInGrigliaDto>? ArticoliSelected { get; set; }
|
||||
private readonly List<ArticoliInGrigliaDto> _selected = [];
|
||||
private string? FilterText { get; set; }
|
||||
|
||||
private List<ArticoliInGrigliaDto> Filtrati =>
|
||||
FilterText.IsNullOrEmpty()
|
||||
? Articoli ?? []
|
||||
: (Articoli ?? []).FindAll(x =>
|
||||
x.Descrizione.ContainsIgnoreCase(FilterText!) ||
|
||||
x.Barcode.ContainsIgnoreCase(FilterText!));
|
||||
|
||||
private string? Conteggio => Articoli.IsNullOrEmpty()
|
||||
? null
|
||||
: _selected.Count > 0
|
||||
? $"{_selected.Count} selezionat{(_selected.Count == 1 ? "o" : "i")}"
|
||||
: $"{Articoli!.Count} disponibili";
|
||||
|
||||
private bool IsSelected(ArticoliInGrigliaDto art) => _selected.Contains(art);
|
||||
|
||||
private void Toggle(ArticoliInGrigliaDto art)
|
||||
{
|
||||
if (!_selected.Remove(art)) _selected.Add(art);
|
||||
}
|
||||
|
||||
private void OnFilterInput(ChangeEventArgs e) => FilterText = e.Value?.ToString();
|
||||
|
||||
private void ClearFilter() => FilterText = null;
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private void Submit() =>
|
||||
MudDialog.Close(DialogResult.Ok(ArticoliSelected?.ToList()));
|
||||
|
||||
private static bool SearchFunc(ArticoliInGrigliaDto? obj, string? searchString)
|
||||
{
|
||||
if (searchString.IsNullOrEmpty() || obj == null) return true;
|
||||
|
||||
return obj.Descrizione.ContainsIgnoreCase(searchString!);
|
||||
}
|
||||
|
||||
private static string? ToStringFunc(ArticoliInGrigliaDto? arg) => arg?.Descrizione;
|
||||
|
||||
private static string MultiSelectionTextFunc(List<ArticoliInGrigliaDto?> arg) =>
|
||||
$"{arg.Count} selezionat{(arg.Count == 1 ? "o" : "i")}";
|
||||
|
||||
}
|
||||
private void Submit() => MudDialog.Close(DialogResult.Ok(_selected.ToList()));
|
||||
}
|
||||
|
||||
@@ -1,87 +1,120 @@
|
||||
@using SteUp.Shared.Components.SingleElements.Card
|
||||
@using SteUp.Shared.Components.Layout
|
||||
@using SteUp.Shared.Components.SingleElements.Card
|
||||
@using SteUp.Shared.Core.Dto
|
||||
|
||||
<MudDialog OnBackdropClick="Cancel">
|
||||
@*
|
||||
Scelta del punto vendita: schermata intera, non un riquadro. L'elenco e'
|
||||
lungo e si scorre in piedi: serve tutto lo spazio e un campo di ricerca
|
||||
grande quanto il pollice.
|
||||
*@
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<div class="app-shell">
|
||||
<HeaderLayout Title="Punto vendita" Subtitle="@Conteggio" Cancel="true"
|
||||
OnCancel="@Cancel" Compact="true"/>
|
||||
|
||||
<div class="select-shop-content">
|
||||
<div class="shop-header">
|
||||
<div class="shop-title">
|
||||
<MudText Typo="Typo.h5"><b>Seleziona il negozio</b></MudText>
|
||||
|
||||
<MudIconButton Icon="@Icons.Material.Rounded.Close" Size="Size.Small" OnClick="@Cancel"/>
|
||||
</div>
|
||||
|
||||
<div class="input-card clearButton">
|
||||
<MudTextField T="string?" Placeholder="Cerca..." Variant="Variant.Text"
|
||||
@bind-Value="FilterText" DebounceInterval="500"
|
||||
OnDebounceIntervalElapsed="ApplyFilters"/>
|
||||
<div class="page-width" style="padding-bottom: var(--space-sm);">
|
||||
<div class="search-field">
|
||||
<i class="search-field__icon ri-search-line" aria-hidden="true"></i>
|
||||
<input type="search" placeholder="Cerca per codice, nome o città"
|
||||
autocapitalize="none" autocorrect="off"
|
||||
aria-label="Cerca un punto vendita"
|
||||
value="@FilterText" @oninput="OnFilterInput"/>
|
||||
@if (!FilterText.IsNullOrEmpty())
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Rounded.Close" Size="Size.Small"
|
||||
aria-label="Cancella la ricerca" OnClick="@ClearFilter"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shop-body">
|
||||
@if (_afterRender)
|
||||
{
|
||||
if (FilteredList.IsNullOrEmpty())
|
||||
<div class="app-scroll">
|
||||
<div class="page-width page-body--flush">
|
||||
@if (FilteredList is null)
|
||||
{
|
||||
<MudText Typo="Typo.body2">Nessun negozio trovato</MudText>
|
||||
<div class="card-list" aria-hidden="true">
|
||||
@for (var i = 0; i < 4; i++)
|
||||
{
|
||||
<div class="list-card">
|
||||
<div class="skeleton skeleton--line-short"></div>
|
||||
<div class="skeleton skeleton--title" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (FilteredList.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="empty-state__icon ri-search-line" aria-hidden="true"></i>
|
||||
<p class="empty-state__title">Nessun punto vendita</p>
|
||||
<p class="empty-state__text">Nessun risultato per "@FilterText".</p>
|
||||
<div class="empty-state__action">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@ClearFilter">
|
||||
Azzera la ricerca
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Virtualize Items="FilteredList" Context="puntoVendita">
|
||||
<ShopCard PuntoVendita="puntoVendita"/>
|
||||
</Virtualize>
|
||||
<div class="card-list">
|
||||
<Virtualize Items="FilteredList" Context="puntoVendita" OverscanCount="6">
|
||||
<ShopCard PuntoVendita="puntoVendita" OnStarting="@(() => _busy = true)"/>
|
||||
</Virtualize>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
<SpinnerOverlay VisibleOverlay="_busy" Message="Apertura ispezione…"/>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
private List<PuntoVenditaDto>? FilteredList { get; set; }
|
||||
private string? FilterText { get; set; }
|
||||
private bool _busy;
|
||||
|
||||
private bool _afterRender;
|
||||
private string? Conteggio => FilteredList is null
|
||||
? null
|
||||
: FilteredList.Count == 1 ? "1 punto vendita" : $"{FilteredList.Count} punti vendita";
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override void OnInitialized() => ApplyFilters();
|
||||
|
||||
private void OnFilterInput(ChangeEventArgs e)
|
||||
{
|
||||
_afterRender = false;
|
||||
FilterText = e.Value?.ToString();
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
private void ClearFilter()
|
||||
{
|
||||
if (_afterRender) return;
|
||||
|
||||
_afterRender = true;
|
||||
FilterText = null;
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
private void ApplyFilters()
|
||||
{
|
||||
if (FilterText.IsNullOrEmpty())
|
||||
{
|
||||
FilteredList = SteupDataService.PuntiVenditaList;
|
||||
}
|
||||
else
|
||||
{
|
||||
FilteredList = SteupDataService.PuntiVenditaList.FindAll(x =>
|
||||
(x.Indirizzo != null && x.Indirizzo.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Descrizione != null && x.Descrizione.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.CodMdep != null && x.CodMdep.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Citta != null && x.Citta.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Cap != null && x.Cap.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Provincia != null && x.Provincia.ContainsIgnoreCase(FilterText!))
|
||||
);
|
||||
}
|
||||
var source = SteupDataService.PuntiVenditaList;
|
||||
|
||||
FilteredList = (FilterText.IsNullOrEmpty()
|
||||
? source
|
||||
: source.FindAll(x =>
|
||||
(x.Indirizzo != null && x.Indirizzo.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Descrizione != null && x.Descrizione.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.CodMdep != null && x.CodMdep.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Citta != null && x.Citta.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Cap != null && x.Cap.ContainsIgnoreCase(FilterText!)) ||
|
||||
(x.Provincia != null && x.Provincia.ContainsIgnoreCase(FilterText!))))
|
||||
.OrderBy(x => x.CodMdep)
|
||||
.ToList();
|
||||
|
||||
FilteredList = FilteredList.OrderBy(x => x.CodMdep).ToList();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
.shop-header{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.shop-title{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shop-body{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -1,24 +1,34 @@
|
||||
@using SteUp.Shared.Core.Dto
|
||||
|
||||
<MudDialog OnBackdropClick="Cancel">
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<h2 class="dialog-title">Note descrittive</h2>
|
||||
<p class="dialog-message">Scegli una descrizione ricorrente per questo motivo.</p>
|
||||
|
||||
@if (!ActivityTypers.IsNullOrEmpty())
|
||||
{
|
||||
<MudList T="string" SelectedValueChanged="OnClickItem">
|
||||
<div class="stack stack-xs" style="margin-top: var(--space-md);">
|
||||
@foreach (var item in ActivityTypers!)
|
||||
{
|
||||
<MudListItem Text="@item.ActivityTypeDescription" Value="item.ActivityTypeDescription"/>
|
||||
<button type="button" class="action-tile"
|
||||
@onclick="@(() => OnClickItem(item.ActivityTypeDescription))">
|
||||
<span class="action-tile__label">@item.ActivityTypeDescription</span>
|
||||
</button>
|
||||
}
|
||||
</MudList>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="spinner-container" style="height: unset !important;">
|
||||
<MudIcon Size="Size.Large" Color="Color.Error" Icon="@Icons.Material.Rounded.Close"/>
|
||||
<MudText>Nessuna descrizione consigliata</MudText>
|
||||
<div class="empty-state">
|
||||
<i class="empty-state__icon ri-chat-off-line" aria-hidden="true"></i>
|
||||
<p class="empty-state__title">Nessuna descrizione consigliata</p>
|
||||
<p class="empty-state__text">Per questo motivo non ci sono note ricorrenti: scrivila a mano.</p>
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="@Cancel">Annulla</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
@@ -29,4 +39,4 @@
|
||||
|
||||
private void OnClickItem(string? selectedValue) =>
|
||||
MudDialog.Close(DialogResult.Ok(selectedValue));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<div class="no-data opacity-75 d-flex flex-column align-items-center">
|
||||
<img src="@ImageSource" alt=""/>
|
||||
@* Stato vuoto (STILE_UI §6.12): suggerisce la mossa successiva, non si limita
|
||||
a dire che non c'e' niente. *@
|
||||
|
||||
<p class="mt-3">@Text</p>
|
||||
<div class="empty-state">
|
||||
<i class="empty-state__icon @Icon" aria-hidden="true"></i>
|
||||
<p class="empty-state__title">@Text</p>
|
||||
@if (!string.IsNullOrWhiteSpace(Hint))
|
||||
{
|
||||
<p class="empty-state__text">@Hint</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string ImageSource { get; set; } = string.Empty;
|
||||
[Parameter] public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
[Parameter] public string? Hint { get; set; }
|
||||
[Parameter] public string Icon { get; set; } = "ri-inbox-line";
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.no-data {
|
||||
position: fixed;
|
||||
top: 35%;
|
||||
width: calc(100% - 3rem); /* remove page padding */
|
||||
}
|
||||
|
||||
.no-data img {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.no-data p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
Reference in New Issue
Block a user