From b7955f0e1f03e972ef641c902d07353e8868b996 Mon Sep 17 00:00:00 2001 From: MarcoE Date: Mon, 8 Jun 2026 09:25:29 +0200 Subject: [PATCH] =?UTF-8?q?Prima=20implementazione=20pagina=20"Attivit?= =?UTF-8?q?=C3=A0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Fixiy.Maui/Fixiy.Maui.csproj | 1 + Fixiy.Maui/MauiProgram.cs | 4 + Fixiy.Maui/wwwroot/index.html | 3 + .../Components/Layout/MainLayout.razor | 9 +- .../Components/Layout/MainLayout.razor.css | 68 ----- Fixiy.Shared/Components/Layout/NavBar.razor | 40 --- .../Components/Layout/NavBar.razor.css | 81 ------ Fixiy.Shared/Components/Layout/NavMenu.razor | 54 ++-- .../Components/Layout/NavMenu.razor.css | 108 +++++--- Fixiy.Shared/Components/Pages/Attivita.razor | 126 +++++++++ .../Components/Pages/Attivita.razor.css | 81 ++++++ .../Components/Pages/SchedaViaggi.razor | 11 + .../SingleElements/AllegatiModal.razor | 75 +++++ .../SingleElements/AllegatiModal.razor.css | 77 ++++++ .../SingleElements/AttivitaCard.razor | 125 +++++++++ .../SingleElements/AttivitaCard.razor.css | 209 ++++++++++++++ .../SingleElements/ChiusuraModal.razor | 259 ++++++++++++++++++ .../SingleElements/ChiusuraModal.razor.css | 88 ++++++ .../SingleElements/SignaturePad.razor | 43 +++ .../SingleElements/SignaturePad.razor.css | 35 +++ Fixiy.Shared/Fixiy.Shared.csproj | 1 + Fixiy.Shared/Models/Allegato.cs | 8 + Fixiy.Shared/Models/AttivitaItem.cs | 18 ++ Fixiy.Shared/Models/OperatoreItem.cs | 6 + Fixiy.Shared/Models/PrioritaAttivita.cs | 8 + Fixiy.Shared/Models/StatoAttivita.cs | 8 + Fixiy.Shared/Models/TipoAllegato.cs | 9 + Fixiy.Shared/Services/MockAttivitaService.cs | 120 ++++++++ Fixiy.Shared/_Imports.razor | 4 + Fixiy.Shared/wwwroot/css/app.css | 83 +++++- Fixiy.Shared/wwwroot/js/signaturePad.js | 78 ++++++ Fixiy.Web/Components/App.razor | 3 + Fixiy.Web/Fixiy.Web.csproj | 1 + Fixiy.Web/Program.cs | 4 + 34 files changed, 1582 insertions(+), 266 deletions(-) delete mode 100644 Fixiy.Shared/Components/Layout/NavBar.razor delete mode 100644 Fixiy.Shared/Components/Layout/NavBar.razor.css create mode 100644 Fixiy.Shared/Components/Pages/Attivita.razor create mode 100644 Fixiy.Shared/Components/Pages/Attivita.razor.css create mode 100644 Fixiy.Shared/Components/Pages/SchedaViaggi.razor create mode 100644 Fixiy.Shared/Components/SingleElements/AllegatiModal.razor create mode 100644 Fixiy.Shared/Components/SingleElements/AllegatiModal.razor.css create mode 100644 Fixiy.Shared/Components/SingleElements/AttivitaCard.razor create mode 100644 Fixiy.Shared/Components/SingleElements/AttivitaCard.razor.css create mode 100644 Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor create mode 100644 Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor.css create mode 100644 Fixiy.Shared/Components/SingleElements/SignaturePad.razor create mode 100644 Fixiy.Shared/Components/SingleElements/SignaturePad.razor.css create mode 100644 Fixiy.Shared/Models/Allegato.cs create mode 100644 Fixiy.Shared/Models/AttivitaItem.cs create mode 100644 Fixiy.Shared/Models/OperatoreItem.cs create mode 100644 Fixiy.Shared/Models/PrioritaAttivita.cs create mode 100644 Fixiy.Shared/Models/StatoAttivita.cs create mode 100644 Fixiy.Shared/Models/TipoAllegato.cs create mode 100644 Fixiy.Shared/Services/MockAttivitaService.cs create mode 100644 Fixiy.Shared/wwwroot/js/signaturePad.js diff --git a/Fixiy.Maui/Fixiy.Maui.csproj b/Fixiy.Maui/Fixiy.Maui.csproj index fb59589..2d07c28 100644 --- a/Fixiy.Maui/Fixiy.Maui.csproj +++ b/Fixiy.Maui/Fixiy.Maui.csproj @@ -101,6 +101,7 @@ + diff --git a/Fixiy.Maui/MauiProgram.cs b/Fixiy.Maui/MauiProgram.cs index d756f69..79af873 100644 --- a/Fixiy.Maui/MauiProgram.cs +++ b/Fixiy.Maui/MauiProgram.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Logging; using Fixiy.Maui.Services; using Fixiy.Shared; using Fixiy.Shared.Interfaces; +using Fixiy.Shared.Services; +using MudBlazor.Services; namespace Fixiy.Maui { @@ -31,6 +33,8 @@ namespace Fixiy.Maui #endif builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddMudServices(); return builder.Build(); } diff --git a/Fixiy.Maui/wwwroot/index.html b/Fixiy.Maui/wwwroot/index.html index 58d92b0..7bf66fc 100644 --- a/Fixiy.Maui/wwwroot/index.html +++ b/Fixiy.Maui/wwwroot/index.html @@ -16,6 +16,7 @@ + @@ -35,6 +36,8 @@ + + diff --git a/Fixiy.Shared/Components/Layout/MainLayout.razor b/Fixiy.Shared/Components/Layout/MainLayout.razor index 3da63fd..00ed1aa 100644 --- a/Fixiy.Shared/Components/Layout/MainLayout.razor +++ b/Fixiy.Shared/Components/Layout/MainLayout.razor @@ -1,10 +1,11 @@ @inherits LayoutComponentBase -
- @**@ + + + + +
diff --git a/Fixiy.Shared/Components/Layout/MainLayout.razor.css b/Fixiy.Shared/Components/Layout/MainLayout.razor.css index 8521145..116575a 100644 --- a/Fixiy.Shared/Components/Layout/MainLayout.razor.css +++ b/Fixiy.Shared/Components/Layout/MainLayout.razor.css @@ -7,71 +7,3 @@ 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; - } -} diff --git a/Fixiy.Shared/Components/Layout/NavBar.razor b/Fixiy.Shared/Components/Layout/NavBar.razor deleted file mode 100644 index a9c1b7b..0000000 --- a/Fixiy.Shared/Components/Layout/NavBar.razor +++ /dev/null @@ -1,40 +0,0 @@ -
- -
- -@code { - -} \ No newline at end of file diff --git a/Fixiy.Shared/Components/Layout/NavBar.razor.css b/Fixiy.Shared/Components/Layout/NavBar.razor.css deleted file mode 100644 index 841769e..0000000 --- a/Fixiy.Shared/Components/Layout/NavBar.razor.css +++ /dev/null @@ -1,81 +0,0 @@ -a { - text-decoration: none; - color: inherit; -} - -ul { - list-style-type: none; -} - -.container { - max-width: 100%; - margin: 0 auto; - padding: 0; -} - -nav { - position: fixed; - bottom: 0; - width: 100%; - background-color: var(--ligther-color); - margin: 0; - display: flex; - border-radius: 40px 40px 0px 0px; - box-shadow: rgb(50 50 93 / 25%) 0 50px 100px 10px, - rgb(0 0 0 / 30%) 0 30px 60px -30px; -} - -nav ul { - display: inline-flex; - align-items: center; - padding: 0; - flex: 0 0 25%; - justify-content: center; -} - -nav :where(li a) { - position: relative; -} - -nav ul li a { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column-reverse; - padding: 1em; - line-height: 1.4; - -webkit-transition: all .3s ease-out; - transition: all .3s ease-out; -} - -nav ul li a:hover { - color: var(--primary-color); -} - -nav ul li a i { - font-size: 1.5rem; -} - -nav ul li a span { - font-size: 0.9rem; -} - -/* animations */ - -nav li.active a::before, nav li.active a::after { - content: ""; - position: absolute; - background-color: var(--primary-color); - z-index: -1; -} - -nav li.active a::before { - top: 5%; - width: calc(100% - 0px); - height: 100%; - border-radius: 25px; -} - -nav li.active a { - color: var(--ligther-color); -} \ No newline at end of file diff --git a/Fixiy.Shared/Components/Layout/NavMenu.razor b/Fixiy.Shared/Components/Layout/NavMenu.razor index ea9c171..e36c9e5 100644 --- a/Fixiy.Shared/Components/Layout/NavMenu.razor +++ b/Fixiy.Shared/Components/Layout/NavMenu.razor @@ -1,38 +1,22 @@ - \ No newline at end of file diff --git a/Fixiy.Shared/Components/Layout/NavMenu.razor.css b/Fixiy.Shared/Components/Layout/NavMenu.razor.css index 83500fa..d097ea4 100644 --- a/Fixiy.Shared/Components/Layout/NavMenu.razor.css +++ b/Fixiy.Shared/Components/Layout/NavMenu.razor.css @@ -1,50 +1,92 @@ -.navbar { - background-color: var(--ligther-color); - border-radius: 50px 50px 0 0; - border: #eceff2 solid 1px; +/* ───────────────────────────────────────────────────────────── + NavMenu — bottom nav, soft UI + I .nav-item sono resi da NavLink (componente figlio): vanno + raggiunti con ::deep perché non ricevono l'attributo di scope. + ───────────────────────────────────────────────────────────── */ + +.bottom-nav { + display: flex; + justify-content: space-around; + align-items: center; position: fixed; - bottom: 0; - width: 100%; + bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + left: 1rem; + right: 1rem; + height: 4.5rem; + padding: 0 0.4rem; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border: 1px solid rgba(31, 30, 90, 0.05); + border-radius: 26px; + box-shadow: 0 12px 34px rgba(31, 30, 90, 0.14), 0 2px 8px rgba(31, 30, 90, 0.06); + z-index: 100; } -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; +.bottom-nav ::deep .nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + gap: 0.28rem; + padding: 0.4rem 0; + color: #9a9aae; + text-decoration: none; + transition: color 0.2s ease; + cursor: pointer; + -webkit-tap-highlight-color: transparent; } -.nav-item ::deep a { - color: var(--darker-color); +.bottom-nav ::deep .nav-item:hover { + color: #6b6b7b; +} + +.bottom-nav ::deep .nav-item.active { + color: var(--primary-color); +} + +.bottom-nav ::deep .nav-icon-wrap { display: flex; align-items: center; - line-height: 1.4; justify-content: center; -} - -.nav-item ::deep a > div { - width: 4.3rem; - height: 4.3rem; + width: 3.2rem; + height: 2.3rem; border-radius: 100px; + background: transparent; + transition: background 0.22s ease, transform 0.22s ease; +} + +.bottom-nav ::deep .nav-item.active .nav-icon-wrap { + background: rgba(83, 82, 237, 0.12); + transform: translateY(-2px); +} + +.bottom-nav ::deep .nav-item:hover:not(.active) .nav-icon-wrap { + background: rgba(31, 30, 90, 0.05); +} + +.bottom-nav ::deep .nav-item i { + font-size: 1.45rem; + line-height: 1; + display: flex; + align-items: center; justify-content: center; - padding-bottom: 0.1rem; - -webkit-transition: all .3s ease-out; - transition: all .3s ease-out; + transition: transform 0.22s ease; } -.nav-item ::deep a.active > div { - background-color: var(--primary-color); - color: white; +.bottom-nav ::deep .nav-item.active i { + transform: scale(1.05); } -/*.nav-item a:hover :not(.active) {*/ -/* background-color: rgba(255, 255, 255, 0.1);*/ -/* color: var(--primary-color);*/ -/*}*/ - - -.nav-item ::deep a i { - font-size: 2rem; +.bottom-nav ::deep .nav-label { + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.01em; + line-height: 1; + text-align: center; } -.nav-item ::deep a span { - font-size: 0.9rem; +.bottom-nav ::deep .nav-item.active .nav-label { + font-weight: 700; } diff --git a/Fixiy.Shared/Components/Pages/Attivita.razor b/Fixiy.Shared/Components/Pages/Attivita.razor new file mode 100644 index 0000000..f591bbe --- /dev/null +++ b/Fixiy.Shared/Components/Pages/Attivita.razor @@ -0,0 +1,126 @@ +@page "/attivita" +@rendermode @(InteractiveServer) +@inject MockAttivitaService MockService + +
+ + + + @if (!_attivita.Any()) + { + + } + else + { +
+ @foreach (var item in AttivitaOrdinata) + { + + } +
+ } +
+ + + + + +@code { + List _attivita = []; + AttivitaItem? _attivitaSelezionata; + bool _chiusuraVisible; + bool _allegatiVisible; + AttivitaItem? _dragSource; + + IEnumerable AttivitaOrdinata => + _attivita.OrderBy(a => a.Priorita switch + { + PrioritaAttivita.Emergenza => 0, + PrioritaAttivita.Alta => 1, + _ => 2 + }) + .ThenBy(a => a.Ordine); + + protected override void OnInitialized() + { + _attivita = MockService.GetAttivitaOggi(); + } + + void ApriChiusura(AttivitaItem item) + { + _attivitaSelezionata = item; + _chiusuraVisible = true; + } + + void ApriAllegati(AttivitaItem item) + { + _attivitaSelezionata = item; + _allegatiVisible = true; + } + + void OnConfermaChiusura((AttivitaItem item, StatoAttivita stato) args) + { + var idx = _attivita.IndexOf(args.item); + if (idx >= 0) + _attivita[idx] = args.item with { Stato = args.stato }; + + _chiusuraVisible = false; + } + + void OnDragStart(AttivitaItem item) + { + if (!item.IsLocked) + _dragSource = item; + } + + void OnDrop(AttivitaItem target) + { + if (_dragSource is null || _dragSource == target || target.IsLocked) + return; + + var srcIdx = _attivita.IndexOf(_dragSource); + var tgtIdx = _attivita.IndexOf(target); + + if (srcIdx < 0 || tgtIdx < 0) return; + + _attivita.RemoveAt(srcIdx); + _attivita.Insert(tgtIdx, _dragSource); + + for (int i = 0; i < _attivita.Count; i++) + _attivita[i] = _attivita[i] with { Ordine = i }; + + _dragSource = null; + } + + void FineViaggio() + { + // TODO: generare e inviare PDF/CSV via email + } +} + diff --git a/Fixiy.Shared/Components/Pages/Attivita.razor.css b/Fixiy.Shared/Components/Pages/Attivita.razor.css new file mode 100644 index 0000000..932c915 --- /dev/null +++ b/Fixiy.Shared/Components/Pages/Attivita.razor.css @@ -0,0 +1,81 @@ +.attivita-page { + padding: 0; + padding-top: calc(0.75rem + env(safe-area-inset-top, 0px)); +} + +.page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 1.4rem; +} + +.page-header-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; + flex-shrink: 0; +} + +.page-date { + font-size: 0.78rem; + color: #999; + text-transform: capitalize; + margin-top: 0.15rem; +} + +.counter-badge { + background: var(--primary-color); + color: #fff; + font-size: 0.72rem; + font-weight: 700; + padding: 0.3rem 0.85rem; + border-radius: 100px; + white-space: nowrap; + box-shadow: 0 4px 12px rgba(83, 82, 237, 0.28); +} + +.attivita-list { + display: flex; + flex-direction: column; + gap: 1rem; + /* spazio per la bottom nav */ + padding-bottom: 7rem; +} + +/* Bottone "Fine Viaggio" integrato nell'header */ +.btn-fine-viaggio { + background: linear-gradient(135deg, #5352ed, #3f3bc4); + color: #fff; + border: none; + border-radius: 100px; + padding: 0.5rem 1.1rem; + font-family: inherit; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.01em; + white-space: nowrap; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + box-shadow: 0 5px 16px rgba(83, 82, 237, 0.32); + cursor: pointer; + transition: box-shadow 0.18s ease, transform 0.15s ease; + -webkit-tap-highlight-color: transparent; +} + +.btn-fine-viaggio i { + font-size: 1.05rem; + line-height: 1; +} + +.btn-fine-viaggio:hover { + box-shadow: 0 7px 20px rgba(83, 82, 237, 0.42); +} + +.btn-fine-viaggio:active { + transform: scale(0.97); +} diff --git a/Fixiy.Shared/Components/Pages/SchedaViaggi.razor b/Fixiy.Shared/Components/Pages/SchedaViaggi.razor new file mode 100644 index 0000000..8c17417 --- /dev/null +++ b/Fixiy.Shared/Components/Pages/SchedaViaggi.razor @@ -0,0 +1,11 @@ +@page "/scheda-viaggi" + + + +
+ +
diff --git a/Fixiy.Shared/Components/SingleElements/AllegatiModal.razor b/Fixiy.Shared/Components/SingleElements/AllegatiModal.razor new file mode 100644 index 0000000..eb6a563 --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/AllegatiModal.razor @@ -0,0 +1,75 @@ + + + @if (Attivita is not null) + { +
+
+ + + Allegati — @Attivita.PuntoVendita + + +
+ } +
+ + @if (Attivita is not null) + { +
+ } + + + +@code { + [Parameter] public AttivitaItem? Attivita { get; set; } + [Parameter] public bool Visible { get; set; } + [Parameter] public EventCallback OnClosed { get; set; } + + static readonly DialogOptions BottomSheetOptions = new() + { + Position = DialogPosition.BottomCenter, + FullWidth = true, + MaxWidth = MaxWidth.Medium, + CloseButton = false, + BackdropClick = true, + CloseOnEscapeKey = true + }; + + Task Chiudi() => OnClosed.InvokeAsync(); + + async Task OnVisibleChanged(bool visible) + { + if (!visible) + await Chiudi(); + } +} diff --git a/Fixiy.Shared/Components/SingleElements/AllegatiModal.razor.css b/Fixiy.Shared/Components/SingleElements/AllegatiModal.razor.css new file mode 100644 index 0000000..f3af635 --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/AllegatiModal.razor.css @@ -0,0 +1,77 @@ +/* Allegati list */ +.allegati-list { + overflow-y: auto; + flex: 1; + min-height: 0; + padding: 0.8rem 1rem 1.5rem; + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.allegato-item { + display: flex; + flex-direction: column; + border-radius: 14px; + overflow: hidden; + border: 1px solid #f0f0f0; + flex-shrink: 0; +} + +.allegato-preview { + position: relative; + height: 180px; + overflow: hidden; +} + +.allegato-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.allegato-type-badge { + position: absolute; + top: 0.5rem; + left: 0.5rem; + font-size: 0.65rem; + font-weight: 700; + padding: 0.15rem 0.5rem; + border-radius: 100px; +} + +.badge-foto { background: rgba(83, 82, 237, 0.85); color: #fff; } +.badge-piantina { background: rgba(0, 150, 136, 0.85); color: #fff; } + +.allegato-icon-box { + height: 80px; + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; +} + +.icon-email { background: #e3f2fd; color: #1565c0; } +.icon-doc { background: #e8f5e9; color: #2e7d32; } + +.allegato-info { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0.75rem; + background: #fafafa; +} + +.allegato-nome { + font-size: 0.8rem; + color: #555; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 80%; +} + +.allegato-download { + font-size: 1rem; + color: var(--primary-color); +} diff --git a/Fixiy.Shared/Components/SingleElements/AttivitaCard.razor b/Fixiy.Shared/Components/SingleElements/AttivitaCard.razor new file mode 100644 index 0000000..346b308 --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/AttivitaCard.razor @@ -0,0 +1,125 @@ +@if (Attivita is not null) +{ +
+ + + +
+ +
+ + + @Attivita.Priorita.ToString().ToUpper() + + + @if (Attivita.IsLocked) + { + + } + else + { + + } +
+ +

+ + @Attivita.PuntoVendita +

+ + + +
+ + @Attivita.Luogo +
+ +

@Attivita.Descrizione

+ + @if (Attivita.Allegati.Count > 0) + { + + + @Attivita.Allegati.Count allegat@(Attivita.Allegati.Count == 1 ? "o" : "i") + + } +
+ +
+ @if (Attivita.Allegati.Count > 0) + { + + } + @if (Attivita.Stato == StatoAttivita.Aperta) + { + + } + else + { + + + @Attivita.Stato + + } +
+
+} + +@code { + [Parameter, EditorRequired] public AttivitaItem? Attivita { get; set; } + [Parameter] public EventCallback OnChiudi { get; set; } + [Parameter] public EventCallback OnVisualizzaAllegati { get; set; } + [Parameter] public EventCallback OnDragStart { get; set; } + [Parameter] public EventCallback OnDrop { get; set; } + + bool IsDraggable => Attivita?.Priorita == PrioritaAttivita.Normale && Attivita.Stato == StatoAttivita.Aperta; + + string CardClass => $"attivita-card {PrioritaClass}{(Attivita?.Stato != StatoAttivita.Aperta ? " card-chiusa" : "")}"; + + string PrioritaClass => Attivita?.Priorita switch + { + PrioritaAttivita.Emergenza => "priorita-emergenza", + PrioritaAttivita.Alta => "priorita-alta", + _ => "priorita-normale" + }; + + string PrioritaIcon => Attivita?.Priorita switch + { + PrioritaAttivita.Emergenza => Icons.Material.Filled.Warning, + PrioritaAttivita.Alta => Icons.Material.Filled.ErrorOutline, + _ => Icons.Material.Filled.CheckCircleOutline + }; + + string StatoClass => Attivita?.Stato switch + { + StatoAttivita.Chiusa => "stato-chiusa", + StatoAttivita.Rimandata => "stato-rimandata", + _ => "stato-aperta" + }; + + string StatoIcon => Attivita?.Stato switch + { + StatoAttivita.Chiusa => Icons.Material.Filled.CheckCircleOutline, + _ => Icons.Material.Filled.Schedule + }; +} diff --git a/Fixiy.Shared/Components/SingleElements/AttivitaCard.razor.css b/Fixiy.Shared/Components/SingleElements/AttivitaCard.razor.css new file mode 100644 index 0000000..e7600fa --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/AttivitaCard.razor.css @@ -0,0 +1,209 @@ +/* ───────────────────────────────────────────────────────────── + AttivitaCard — soft UI + ───────────────────────────────────────────────────────────── */ + +.attivita-card { + position: relative; + display: flex; + flex-direction: column; + border-radius: 20px; + background: #ffffff; + border: 1px solid rgba(31, 30, 90, 0.05); + box-shadow: 0 6px 22px rgba(31, 30, 90, 0.07), 0 1px 3px rgba(31, 30, 90, 0.04); + overflow: hidden; + transition: transform 0.22s ease, box-shadow 0.22s ease, opacity 0.22s ease; +} + +.attivita-card:hover { + transform: translateY(-3px); + box-shadow: 0 14px 34px rgba(31, 30, 90, 0.13), 0 2px 6px rgba(31, 30, 90, 0.06); +} + +/* Accent bar — priority */ +.accent-bar { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 5px; + border-radius: 20px 0 0 20px; +} + +.attivita-card.priorita-emergenza .accent-bar { background: linear-gradient(180deg, #ff6b6b, #d32f2f); } +.attivita-card.priorita-alta .accent-bar { background: linear-gradient(180deg, #ffb74d, #ef6c00); } +.attivita-card.priorita-normale .accent-bar { background: linear-gradient(180deg, #8c8bff, var(--primary-color)); } + +/* Closed / non-open cards */ +.attivita-card.card-chiusa { + opacity: 0.62; + box-shadow: 0 2px 10px rgba(31, 30, 90, 0.05); +} + +.attivita-card.card-chiusa:hover { + transform: none; + box-shadow: 0 2px 10px rgba(31, 30, 90, 0.05); +} + +/* Drag affordance */ +.attivita-card[draggable="true"] { cursor: grab; } +.attivita-card[draggable="true"]:active { cursor: grabbing; } + +/* ─── Body ─── */ + +.card-body { + padding: 1rem 1.1rem 0.4rem 1.25rem; +} + +.card-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.priorita-pill { + display: inline-flex; + align-items: center; + gap: 0.3rem; + height: 26px; + padding: 0 0.7rem; + border-radius: 100px; + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.04em; + line-height: 1; +} + +.priorita-pill ::deep .mud-icon-root { font-size: 0.95rem; } + +.priorita-pill.priorita-emergenza { background: #ffe9e9; color: #c62828; } +.priorita-pill.priorita-alta { background: #fff2df; color: #e65100; } +.priorita-pill.priorita-normale { background: #ecebff; color: #3f3bc4; } + +.card-top ::deep .handle-icon { font-size: 1.2rem; } +.card-top ::deep .lock-icon { color: #e53935; opacity: 0.75; } +.card-top ::deep .drag-icon { color: #c4c4d4; } + +.card-store { + display: flex; + align-items: center; + gap: 0.45rem; + margin: 0 0 0.4rem; + font-size: 0.98rem; + font-weight: 600; + color: var(--darker-color); +} + +.card-store ::deep .mud-icon-root { color: var(--primary-color); } + +.card-breadcrumb { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.15rem; + margin-bottom: 0.35rem; + font-size: 0.72rem; + color: #9a9aae; +} + +.card-breadcrumb ::deep .mud-icon-root { font-size: 0.9rem; color: #c7c7d6; } + +.card-location { + display: flex; + align-items: center; + gap: 0.35rem; + margin-bottom: 0.55rem; + font-size: 0.76rem; + color: #6b6b7b; +} + +.card-location ::deep .mud-icon-root { font-size: 1rem; color: #9a9aae; } + +.card-desc { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + margin: 0 0 0.6rem; + font-size: 0.85rem; + line-height: 1.5; + color: #4a4a57; +} + +.card-allegati-pill { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.25rem 0.65rem; + border-radius: 100px; + background: rgba(83, 82, 237, 0.08); + color: var(--primary-color); + font-size: 0.72rem; + font-weight: 600; +} + +.card-allegati-pill ::deep .mud-icon-root { font-size: 0.95rem; } + +/* ─── Actions ─── */ + +.card-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.4rem 1.1rem 1rem 1.25rem; +} + +.btn-card { + display: inline-flex; + align-items: center; + gap: 0.35rem; + height: 36px; + padding: 0 1rem; + border-radius: 100px; + border: none; + font-family: inherit; + font-size: 0.78rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; + -webkit-tap-highlight-color: transparent; +} + +.btn-card ::deep .mud-icon-root { font-size: 1.05rem; } + +.btn-card:active { transform: scale(0.96); } + +.btn-card-ghost { + background: #f3f3fb; + color: var(--primary-color); +} + +.btn-card-ghost:hover { background: #e9e9fb; } + +.btn-card-primary { + background: linear-gradient(135deg, #5352ed, #3f3bc4); + color: #fff; + box-shadow: 0 5px 16px rgba(83, 82, 237, 0.32); +} + +.btn-card-primary:hover { box-shadow: 0 7px 20px rgba(83, 82, 237, 0.42); } + +/* Stato pill (non-open cards) */ +.stato-pill { + display: inline-flex; + align-items: center; + gap: 0.35rem; + height: 30px; + padding: 0 0.85rem; + border-radius: 100px; + font-size: 0.74rem; + font-weight: 700; +} + +.stato-pill ::deep .mud-icon-root { font-size: 1rem; } + +.stato-pill.stato-chiusa { background: #e6f6ec; color: #2e7d32; } +.stato-pill.stato-rimandata { background: #fff4e0; color: #e08600; } +.stato-pill.stato-aperta { background: #ecebff; color: #3f3bc4; } diff --git a/Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor b/Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor new file mode 100644 index 0000000..6d72fee --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor @@ -0,0 +1,259 @@ + + + @if (Attivita is not null) + { +
+
+ + + @Attivita.PuntoVendita + + +
+ } +
+ + @if (Attivita is not null) + { +
+ + +
+ +
+ + @if (_tab == "chiudi") + { +
+ + +
+ +
+ Operatori +
+ @for (int i = 0; i < _operatori.Count; i++) + { + int idx = i; +
+ + @if (_operatori.Count > 1) + { + + } +
+ } + + Aggiungi operatore + +
+
+ + + +
+ + Firma Capo Negozio * + + + @if (_validato && string.IsNullOrWhiteSpace(_firma)) + { + Firma obbligatoria + } +
+ +
+ Foto lavoro svolto + + @if (_fotoNomi.Any()) + { +
+ @foreach (var nome in _fotoNomi) + { + @nome + } +
+ } + +
+ + + Chiudi Attivita + + } + else + { + + + + Nessuna + @foreach (var az in _aziendeTerme) + { + @az + } + + + @if (_aziendaTerza == "Altro") + { + + } + +
+ + Foto KO * + + + @if (_fotoRimandoNomi.Any()) + { +
+ @foreach (var nome in _fotoRimandoNomi) + { + @nome + } +
+ } + @if (_validato && !_fotoRimandoNomi.Any()) + { + Almeno una foto e obbligatoria + } +
+ + + Rimanda Attivita + + } +
+ } +
+
+ +@code { + [Parameter] public AttivitaItem? Attivita { get; set; } + [Parameter] public bool Visible { get; set; } + [Parameter] public EventCallback OnClosed { get; set; } + [Parameter] public EventCallback<(AttivitaItem, StatoAttivita)> OnConferma { get; set; } + + string _tab = "chiudi"; + bool _validato; + + static readonly DialogOptions BottomSheetOptions = new() + { + Position = DialogPosition.BottomCenter, + FullWidth = true, + MaxWidth = MaxWidth.Medium, + CloseButton = false, + BackdropClick = true, + CloseOnEscapeKey = true + }; + + async Task OnVisibleChanged(bool visible) + { + if (!visible) + await Annulla(); + } + + TimeSpan? _oraIngresso = DateTime.Now.TimeOfDay; + TimeSpan? _oraFine = DateTime.Now.TimeOfDay; + + List _operatori = ["Marco Esposito"]; + string _descrizioneIntervento = string.Empty; + string _firma = string.Empty; + string _commentoFoto = string.Empty; + List _fotoNomi = []; + + string _motivoRimando = string.Empty; + string _aziendaTerza = string.Empty; + string _aziendaAltra = string.Empty; + List _fotoRimandoNomi = []; + + readonly List _aziendeTerme = ["Arneg", "Desich", "Idracol", "Carrier", "Danfoss", "Alfa Laval", "Altro"]; + + void OnFotoChiusura(InputFileChangeEventArgs e) => + _fotoNomi = e.GetMultipleFiles().Select(f => f.Name).ToList(); + + void OnFotoRimando(InputFileChangeEventArgs e) => + _fotoRimandoNomi = e.GetMultipleFiles().Select(f => f.Name).ToList(); + + async Task ConfermaChiusura() + { + _validato = true; + if (string.IsNullOrWhiteSpace(_descrizioneIntervento) || string.IsNullOrWhiteSpace(_firma)) + return; + if (Attivita is not null) + await OnConferma.InvokeAsync((Attivita, StatoAttivita.Chiusa)); + Reset(); + } + + async Task ConfermaRimando() + { + _validato = true; + if (string.IsNullOrWhiteSpace(_motivoRimando) || !_fotoRimandoNomi.Any()) + return; + if (Attivita is not null) + await OnConferma.InvokeAsync((Attivita, StatoAttivita.Rimandata)); + Reset(); + } + + Task Annulla() + { + Reset(); + return OnClosed.InvokeAsync(); + } + + void Reset() + { + _validato = false; + _tab = "chiudi"; + _descrizioneIntervento = string.Empty; + _firma = string.Empty; + _commentoFoto = string.Empty; + _fotoNomi = []; + _motivoRimando = string.Empty; + _aziendaTerza = string.Empty; + _aziendaAltra = string.Empty; + _fotoRimandoNomi = []; + _operatori = ["Marco Esposito"]; + } +} \ No newline at end of file diff --git a/Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor.css b/Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor.css new file mode 100644 index 0000000..f4c7c40 --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/ChiusuraModal.razor.css @@ -0,0 +1,88 @@ +.tab-bar { + display: flex; + padding: 0 1rem; + gap: 0; + border-bottom: 1px solid #f0f0f0; + flex-shrink: 0; +} + +.tab-btn { + flex: 1; + background: none; + border: none; + border-bottom: 3px solid transparent; + padding: 0.7rem 0.3rem; + font-size: 0.85rem; + font-weight: 600; + color: #aaa; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 0.3rem; + transition: color 0.2s, border-color 0.2s; +} + +.tab-btn.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); +} + +.sheet-body { + overflow-y: auto; + flex: 1; + min-height: 0; + padding: 1rem 1.1rem 1.5rem; + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.form-row { + display: flex; + gap: 0.8rem; +} + +.form-row .form-group { + flex: 1; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.form-label { + font-weight: 600 !important; + color: var(--darker-color) !important; +} + +.required { color: #e53935; } + +.operatori-list { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.operatore-row { + display: flex; + gap: 0.4rem; + align-items: center; +} + +.form-input-file { + font-size: 0.78rem; + color: #666; + padding: 0.4rem 0; +} + +.foto-preview-list { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.3rem; +} + +.mt-2 { margin-top: 0.5rem !important; } diff --git a/Fixiy.Shared/Components/SingleElements/SignaturePad.razor b/Fixiy.Shared/Components/SingleElements/SignaturePad.razor new file mode 100644 index 0000000..7011fe2 --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/SignaturePad.razor @@ -0,0 +1,43 @@ +@inject IJSRuntime JS +@implements IAsyncDisposable + +
+ +
+ +
+
+ +@code { + [Parameter] public EventCallback OnFirmaCambiata { get; set; } + + readonly string _canvasId = $"sig-{Guid.NewGuid():N}"; + DotNetObjectReference? _dotNetRef; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _dotNetRef = DotNetObjectReference.Create(this); + await JS.InvokeVoidAsync("signaturePad.init", _canvasId, _dotNetRef); + } + } + + [JSInvokable] + public Task OnSignatureChanged(string dataUrl) => OnFirmaCambiata.InvokeAsync(dataUrl); + + async Task CancellaFirma() + { + await JS.InvokeVoidAsync("signaturePad.clear", _canvasId); + await OnFirmaCambiata.InvokeAsync(string.Empty); + } + + public async ValueTask DisposeAsync() + { + _dotNetRef?.Dispose(); + await ValueTask.CompletedTask; + } +} diff --git a/Fixiy.Shared/Components/SingleElements/SignaturePad.razor.css b/Fixiy.Shared/Components/SingleElements/SignaturePad.razor.css new file mode 100644 index 0000000..0205dd6 --- /dev/null +++ b/Fixiy.Shared/Components/SingleElements/SignaturePad.razor.css @@ -0,0 +1,35 @@ +.signature-container { + border: 2px dashed #d0d0e8; + border-radius: 14px; + overflow: hidden; + background: #fafaff; +} + +.signature-canvas { + display: block; + width: 100%; + height: 160px; + touch-action: none; + cursor: crosshair; +} + +.signature-actions { + display: flex; + justify-content: flex-end; + padding: 0.4rem 0.6rem; + border-top: 1px solid #ebebf5; +} + +.btn-clear-sig { + background: none; + border: none; + font-size: 0.75rem; + color: #999; + display: flex; + align-items: center; + gap: 0.25rem; + cursor: pointer; + padding: 0.2rem 0.4rem; +} + +.btn-clear-sig:hover { color: #e53935; } diff --git a/Fixiy.Shared/Fixiy.Shared.csproj b/Fixiy.Shared/Fixiy.Shared.csproj index 5605918..da57d02 100644 --- a/Fixiy.Shared/Fixiy.Shared.csproj +++ b/Fixiy.Shared/Fixiy.Shared.csproj @@ -13,6 +13,7 @@ + diff --git a/Fixiy.Shared/Models/Allegato.cs b/Fixiy.Shared/Models/Allegato.cs new file mode 100644 index 0000000..f9767bf --- /dev/null +++ b/Fixiy.Shared/Models/Allegato.cs @@ -0,0 +1,8 @@ +namespace Fixiy.Shared.Models; + +public record Allegato( + string Id, + string Nome, + string Url, + TipoAllegato Tipo +); diff --git a/Fixiy.Shared/Models/AttivitaItem.cs b/Fixiy.Shared/Models/AttivitaItem.cs new file mode 100644 index 0000000..c78675e --- /dev/null +++ b/Fixiy.Shared/Models/AttivitaItem.cs @@ -0,0 +1,18 @@ +namespace Fixiy.Shared.Models; + +public record AttivitaItem( + string Id, + PrioritaAttivita Priorita, + string PuntoVendita, + string Categoria, + string Sottocategoria, + string Reparto, + string Luogo, + string Descrizione, + List Allegati, + StatoAttivita Stato = StatoAttivita.Aperta, + int Ordine = 0 +) +{ + public bool IsLocked => Priorita == PrioritaAttivita.Emergenza; +} diff --git a/Fixiy.Shared/Models/OperatoreItem.cs b/Fixiy.Shared/Models/OperatoreItem.cs new file mode 100644 index 0000000..fbe203c --- /dev/null +++ b/Fixiy.Shared/Models/OperatoreItem.cs @@ -0,0 +1,6 @@ +namespace Fixiy.Shared.Models; + +public record OperatoreItem( + string Id, + string Nome +); diff --git a/Fixiy.Shared/Models/PrioritaAttivita.cs b/Fixiy.Shared/Models/PrioritaAttivita.cs new file mode 100644 index 0000000..dd16d72 --- /dev/null +++ b/Fixiy.Shared/Models/PrioritaAttivita.cs @@ -0,0 +1,8 @@ +namespace Fixiy.Shared.Models; + +public enum PrioritaAttivita +{ + Normale, + Alta, + Emergenza +} diff --git a/Fixiy.Shared/Models/StatoAttivita.cs b/Fixiy.Shared/Models/StatoAttivita.cs new file mode 100644 index 0000000..e68a041 --- /dev/null +++ b/Fixiy.Shared/Models/StatoAttivita.cs @@ -0,0 +1,8 @@ +namespace Fixiy.Shared.Models; + +public enum StatoAttivita +{ + Aperta, + Chiusa, + Rimandata +} diff --git a/Fixiy.Shared/Models/TipoAllegato.cs b/Fixiy.Shared/Models/TipoAllegato.cs new file mode 100644 index 0000000..b312b01 --- /dev/null +++ b/Fixiy.Shared/Models/TipoAllegato.cs @@ -0,0 +1,9 @@ +namespace Fixiy.Shared.Models; + +public enum TipoAllegato +{ + Immagine, + Piantina, + Documento, + Email +} diff --git a/Fixiy.Shared/Services/MockAttivitaService.cs b/Fixiy.Shared/Services/MockAttivitaService.cs new file mode 100644 index 0000000..e7b2b2a --- /dev/null +++ b/Fixiy.Shared/Services/MockAttivitaService.cs @@ -0,0 +1,120 @@ +using Fixiy.Shared.Models; + +namespace Fixiy.Shared.Services; + +public class MockAttivitaService +{ + public List GetAttivitaOggi() => + [ + new( + Id: "ATT-001", + Priorita: PrioritaAttivita.Emergenza, + PuntoVendita: "Carrefour Roma Prati", + Categoria: "Impianti", + Sottocategoria: "Elettrico", + Reparto: "Casse", + Luogo: "Banco casse centrali", + Descrizione: "Guasto al quadro elettrico secondario delle casse. Interruzione totale delle attività di vendita. Intervento urgente richiesto.", + Allegati: + [ + new("A1", "foto_quadro_01.jpg", "https://picsum.photos/seed/quadro/400/300", TipoAllegato.Immagine), + new("A2", "piantina_casse.png", "https://picsum.photos/seed/piantina/400/300", TipoAllegato.Piantina), + new("A3", "segnalazione_urgente.eml", "#", TipoAllegato.Email) + ] + ), + new( + Id: "ATT-002", + Priorita: PrioritaAttivita.Emergenza, + PuntoVendita: "Esselunga Milano Viale Certosa", + Categoria: "Refrigerazione", + Sottocategoria: "Banco frigo", + Reparto: "Latticini e Salumi", + Luogo: "Corsia B — banco frigo lungo", + Descrizione: "Perdita di gas refrigerante dal banco frigo. Temperatura in risalita, rischio deperimento merce. Evacuazione parziale del reparto in corso.", + Allegati: + [ + new("A4", "allarme_temp_log.eml", "#", TipoAllegato.Email), + new("A5", "foto_perdita.jpg", "https://picsum.photos/seed/frigo/400/300", TipoAllegato.Immagine) + ] + ), + new( + Id: "ATT-003", + Priorita: PrioritaAttivita.Alta, + PuntoVendita: "Ipercoop Torino Lingotto", + Categoria: "Edilizia", + Sottocategoria: "Impermeabilizzazione", + Reparto: "Magazzino", + Luogo: "Soffitto magazzino merci — zona nord", + Descrizione: "Infiltrazione d'acqua dal soffitto a causa delle piogge. Presenza di umidità sulle scaffalature. Necessario intervento entro 24 ore.", + Allegati: + [ + new("A6", "foto_infiltrazione_01.jpg", "https://picsum.photos/seed/infiltr/400/300", TipoAllegato.Immagine), + new("A7", "foto_infiltrazione_02.jpg", "https://picsum.photos/seed/infiltr2/400/300", TipoAllegato.Immagine), + new("A8", "piantina_magazzino.png", "https://picsum.photos/seed/mag/400/300", TipoAllegato.Piantina), + new("A9", "relazione_tecnica.docx", "#", TipoAllegato.Documento) + ] + ), + new( + Id: "ATT-004", + Priorita: PrioritaAttivita.Alta, + PuntoVendita: "Coop Firenze Gavinana", + Categoria: "Sicurezza", + Sottocategoria: "Antincendio", + Reparto: "Tutto il punto vendita", + Luogo: "Centrale antincendio — piano interrato", + Descrizione: "Segnalazione di falso allarme ripetuto dalla centrale antincendio. Verifica e reset del sistema richiesti prima dell'apertura.", + Allegati: + [ + new("A10", "storico_allarmi.eml", "#", TipoAllegato.Email) + ] + ), + new( + Id: "ATT-005", + Priorita: PrioritaAttivita.Normale, + PuntoVendita: "Pam Panorama Venezia Mestre", + Categoria: "Manutenzione", + Sottocategoria: "Pavimentazione", + Reparto: "Ortofrutta", + Luogo: "Corsie 3-4", + Descrizione: "Sostituzione mattonelle scheggiate nella zona ortofrutta. Intervento programmato.", + Allegati: + [ + new("A11", "foto_pavimento.jpg", "https://picsum.photos/seed/pav/400/300", TipoAllegato.Immagine), + new("A12", "preventivo_materiali.docx", "#", TipoAllegato.Documento) + ], + Ordine: 0 + ), + new( + Id: "ATT-006", + Priorita: PrioritaAttivita.Normale, + PuntoVendita: "Simply Market Bologna Corticella", + Categoria: "Impianti", + Sottocategoria: "Idraulico", + Reparto: "Servizi igienici", + Luogo: "Bagni personale — piano terra", + Descrizione: "Perdita dal rubinetto del lavandino nel bagno del personale. Sostituzione guarnizioni.", + Allegati: + [ + new("A13", "foto_rubinetto.jpg", "https://picsum.photos/seed/rubinetto/400/300", TipoAllegato.Immagine) + ], + Ordine: 1 + ) + ]; + + public List GetOperatoriDefault() => + [ + new("OP-001", "Marco Esposito"), + new("OP-002", "Luca Ferretti") + ]; + + public List GetAziendeTerme() => + [ + "Arneg", + "Desich", + "Idracol", + "Carrier", + "Danfoss", + "Alfa Laval", + "Altro" + ]; +} diff --git a/Fixiy.Shared/_Imports.razor b/Fixiy.Shared/_Imports.razor index a805c70..379519a 100644 --- a/Fixiy.Shared/_Imports.razor +++ b/Fixiy.Shared/_Imports.razor @@ -5,5 +5,9 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop +@using MudBlazor @using Fixiy.Shared.Components +@using Fixiy.Shared.Components.SingleElements +@using Fixiy.Shared.Models +@using Fixiy.Shared.Services @using static InteractiveRenderSettings diff --git a/Fixiy.Shared/wwwroot/css/app.css b/Fixiy.Shared/wwwroot/css/app.css index 4423743..61b5f46 100644 --- a/Fixiy.Shared/wwwroot/css/app.css +++ b/Fixiy.Shared/wwwroot/css/app.css @@ -7,7 +7,6 @@ } a, .btn-link { - /*color: #006bb7;*/ text-decoration: none; color: inherit; } @@ -23,7 +22,8 @@ a, .btn-link { } .content { - padding-top: 1.1rem; + padding-top: 1.5rem; + padding-bottom: calc(6.5rem + env(safe-area-inset-bottom, 0px)); } h1:focus { @@ -76,9 +76,11 @@ h1:focus { } .page-title { - /*text-align: center;*/ - font-size: x-large; + font-size: 1.4rem; + font-weight: 700; color: var(--darker-color); + margin: 0; + line-height: 1.2; } @supports (-webkit-touch-callout: none) { @@ -92,7 +94,6 @@ h1:focus { z-index: 1; } - #app { padding-top: env(safe-area-inset-top); height: 100vh; @@ -102,3 +103,75 @@ h1:focus { padding-left: env(safe-area-inset-left); } } + +/* ─── Bottom sheet — custom MudDialog (ChiusuraModal, AllegatiModal) ─── */ + +.mud-dialog.bottom-sheet { + position: fixed; + left: 0; + right: 0; + bottom: 0; + margin: 0 auto; + width: 100%; + max-width: 600px; + max-height: 92vh; + border-radius: 24px 24px 0 0; + display: flex; + flex-direction: column; + overflow: hidden; + padding-bottom: env(safe-area-inset-bottom, 0px); + box-shadow: 0 -8px 40px rgba(31, 30, 90, 0.22); + animation: bottom-sheet-up 0.28s cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.mud-dialog.bottom-sheet .mud-dialog-title { + padding: 0; + margin: 0; + flex-shrink: 0; +} + +.mud-dialog.bottom-sheet .mud-dialog-content { + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; +} + +@keyframes bottom-sheet-up { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + +.sheet-grabber { + width: 40px; + height: 4px; + border-radius: 100px; + background: #d8d8e4; + margin: 0.6rem auto 0.1rem; +} + +.sheet-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1.2rem 0.7rem; + border-bottom: 1px solid #f0f0f0; +} + +.sheet-title { + display: flex; + align-items: center; + gap: 0.45rem; + font-size: 0.98rem; + font-weight: 700; + color: var(--darker-color); +} + +.sheet-close { + background: #f0f0f5 !important; + color: #666 !important; +} + + diff --git a/Fixiy.Shared/wwwroot/js/signaturePad.js b/Fixiy.Shared/wwwroot/js/signaturePad.js new file mode 100644 index 0000000..1f6ede1 --- /dev/null +++ b/Fixiy.Shared/wwwroot/js/signaturePad.js @@ -0,0 +1,78 @@ +window.signaturePad = { + instances: {}, + + init(canvasId, dotNetRef) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + ctx.strokeStyle = '#1a1a2e'; + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + let drawing = false; + + function getPos(e) { + const rect = canvas.getBoundingClientRect(); + const src = e.touches ? e.touches[0] : e; + return { + x: (src.clientX - rect.left) * (canvas.width / rect.width), + y: (src.clientY - rect.top) * (canvas.height / rect.height) + }; + } + + function start(e) { + e.preventDefault(); + drawing = true; + const pos = getPos(e); + ctx.beginPath(); + ctx.moveTo(pos.x, pos.y); + } + + function move(e) { + if (!drawing) return; + e.preventDefault(); + const pos = getPos(e); + ctx.lineTo(pos.x, pos.y); + ctx.stroke(); + } + + function end(e) { + if (!drawing) return; + drawing = false; + ctx.closePath(); + if (dotNetRef) { + dotNetRef.invokeMethodAsync('OnSignatureChanged', canvas.toDataURL('image/png')); + } + } + + canvas.addEventListener('mousedown', start); + canvas.addEventListener('mousemove', move); + canvas.addEventListener('mouseup', end); + canvas.addEventListener('mouseleave', end); + canvas.addEventListener('touchstart', start, { passive: false }); + canvas.addEventListener('touchmove', move, { passive: false }); + canvas.addEventListener('touchend', end); + + this.instances[canvasId] = { canvas, ctx }; + }, + + clear(canvasId) { + const instance = this.instances[canvasId]; + if (!instance) return; + instance.ctx.clearRect(0, 0, instance.canvas.width, instance.canvas.height); + }, + + getDataUrl(canvasId) { + const instance = this.instances[canvasId]; + return instance ? instance.canvas.toDataURL('image/png') : null; + }, + + isEmpty(canvasId) { + const instance = this.instances[canvasId]; + if (!instance) return true; + const data = instance.ctx.getImageData(0, 0, instance.canvas.width, instance.canvas.height).data; + return !data.some(v => v !== 0); + } +}; diff --git a/Fixiy.Web/Components/App.razor b/Fixiy.Web/Components/App.razor index 90941b7..c960428 100644 --- a/Fixiy.Web/Components/App.razor +++ b/Fixiy.Web/Components/App.razor @@ -15,6 +15,7 @@ + @@ -26,6 +27,8 @@ + + diff --git a/Fixiy.Web/Fixiy.Web.csproj b/Fixiy.Web/Fixiy.Web.csproj index 32b675d..5f6397d 100644 --- a/Fixiy.Web/Fixiy.Web.csproj +++ b/Fixiy.Web/Fixiy.Web.csproj @@ -8,6 +8,7 @@ + diff --git a/Fixiy.Web/Program.cs b/Fixiy.Web/Program.cs index 5877211..7d2e277 100644 --- a/Fixiy.Web/Program.cs +++ b/Fixiy.Web/Program.cs @@ -1,7 +1,9 @@ using IntegryApiClient.Blazor; using Fixiy.Web.Components; using Fixiy.Shared.Interfaces; +using Fixiy.Shared.Services; using Fixiy.Web.Services; +using MudBlazor.Services; const string appToken = "3e7e7147-1391-48e7-86bd-b70e7418d40d"; @@ -14,6 +16,8 @@ builder.Services.AddRazorComponents() builder.Services.UseIntegry(appToken: appToken, useLoginAzienda: true); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddMudServices(); var app = builder.Build();