Prima implementazione pagina "Attività"
This commit is contained in:
@@ -101,6 +101,7 @@
|
||||
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.70" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.70" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.8" />
|
||||
<PackageReference Include="MudBlazor" Version="9.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<IFormFactor, FormFactor>();
|
||||
builder.Services.AddSingleton<MockAttivitaService>();
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/remixicon/remixicon.css" />
|
||||
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/app.css" />
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/default-theme.css" />
|
||||
<link rel="stylesheet" href="Fixiy.Maui.styles.css" />
|
||||
@@ -35,6 +36,8 @@
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webview.js" autostart="false"></script>
|
||||
<script src="_content/Fixiy.Shared/js/signaturePad.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="_content/Fixiy.Shared/js/bootstrap/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMnet107bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
<!-- Add chart.js reference if chart components are used in your application. -->
|
||||
<!--<script src="_content/Fixiy.Shared/js/bootstrap/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>-->
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
@*<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>*@
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<div class="page">
|
||||
<NavMenu/>
|
||||
|
||||
<main>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<div class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span>Home</span>
|
||||
<i class="ri-home-5-line"/>
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" id="workout">
|
||||
<span>Workout</span>
|
||||
<i class="ri-empathize-line"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" id="logbook">
|
||||
<span>Logbook</span>
|
||||
<i class="ri-health-book-line"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" id="settings">
|
||||
<span>Impostazioni</span>
|
||||
<i class="ri-settings-5-line"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,38 +1,22 @@
|
||||
<nav class="navbar navbar-expand justify-content-center">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav nav-justified w-100 text-center">
|
||||
<nav class="bottom-nav">
|
||||
<NavLink class="nav-item" href="" Match="NavLinkMatch.All">
|
||||
<div class="nav-icon-wrap">
|
||||
<i class="ri-home-5-line"></i>
|
||||
</div>
|
||||
<span class="nav-label">Home</span>
|
||||
</NavLink>
|
||||
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link " href="workout" Match="NavLinkMatch.All">
|
||||
<div class="d-flex flex-column">
|
||||
<i class="ri-empathize-line"/>
|
||||
@* <span>Workout</span> *@
|
||||
</div>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<div class="d-flex flex-column">
|
||||
<i class="ri-home-5-line"/>
|
||||
@* <span>Home</span> *@
|
||||
</div>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link" href="logbook" Match="NavLinkMatch.All">
|
||||
<div class="d-flex flex-column">
|
||||
<i class="ri-health-book-line"></i>
|
||||
@* <span>Log Book</span> *@
|
||||
</div>
|
||||
</NavLink>
|
||||
</li>
|
||||
@* <li class="nav-item"> *@
|
||||
@* <NavLink class="nav-link d-flex flex-column" href="settings" Match="NavLinkMatch.All"> *@
|
||||
@* <i class="ri-settings-5-line"/> *@
|
||||
@* <span>Impostazioni</span> *@
|
||||
@* </NavLink> *@
|
||||
@* </li> *@
|
||||
</ul>
|
||||
</div>
|
||||
<NavLink class="nav-item" href="scheda-viaggi" Match="NavLinkMatch.Prefix">
|
||||
<div class="nav-icon-wrap">
|
||||
<i class="ri-route-line"></i>
|
||||
</div>
|
||||
<span class="nav-label">Viaggi</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="nav-item" href="attivita" Match="NavLinkMatch.Prefix">
|
||||
<div class="nav-icon-wrap">
|
||||
<i class="ri-task-line"></i>
|
||||
</div>
|
||||
<span class="nav-label">Attività</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
@@ -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 <a> 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
@page "/attivita"
|
||||
@rendermode @(InteractiveServer)
|
||||
@inject MockAttivitaService MockService
|
||||
|
||||
<div class="attivita-page">
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Attività</h1>
|
||||
<span class="page-date">@DateTime.Today.ToString("dddd d MMMM", new System.Globalization.CultureInfo("it-IT"))</span>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<span class="counter-badge">@_attivita.Count(a => a.Stato == StatoAttivita.Aperta) aperte</span>
|
||||
<button type="button" class="btn-fine-viaggio" @onclick="FineViaggio">
|
||||
<i class="ri-flag-2-line"></i>
|
||||
Fine Viaggio
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!_attivita.Any())
|
||||
{
|
||||
<NoDataAvailable
|
||||
ImageSource="_content/Fixiy.Shared/images/empty-state.svg"
|
||||
Text="Nessuna attività per oggi" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="attivita-list">
|
||||
@foreach (var item in AttivitaOrdinata)
|
||||
{
|
||||
<AttivitaCard
|
||||
Attivita="item"
|
||||
OnChiudi="ApriChiusura"
|
||||
OnVisualizzaAllegati="ApriAllegati"
|
||||
OnDragStart="OnDragStart"
|
||||
OnDrop="OnDrop" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ChiusuraModal
|
||||
Attivita="_attivitaSelezionata"
|
||||
Visible="_chiusuraVisible"
|
||||
OnClosed="() => _chiusuraVisible = false"
|
||||
OnConferma="OnConfermaChiusura" />
|
||||
|
||||
<AllegatiModal
|
||||
Attivita="_attivitaSelezionata"
|
||||
Visible="_allegatiVisible"
|
||||
OnClosed="() => _allegatiVisible = false" />
|
||||
|
||||
@code {
|
||||
List<AttivitaItem> _attivita = [];
|
||||
AttivitaItem? _attivitaSelezionata;
|
||||
bool _chiusuraVisible;
|
||||
bool _allegatiVisible;
|
||||
AttivitaItem? _dragSource;
|
||||
|
||||
IEnumerable<AttivitaItem> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@page "/scheda-viaggi"
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Scheda Viaggi</h1>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<NoDataAvailable
|
||||
ImageSource="_content/Fixiy.Shared/images/empty-state.svg"
|
||||
Text="Nessun viaggio disponibile" />
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
<MudDialog Visible="@(Attivita is not null && Visible)" VisibleChanged="OnVisibleChanged"
|
||||
Options="BottomSheetOptions" Class="bottom-sheet">
|
||||
<TitleContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="sheet-grabber"></div>
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-title">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Attachment" Size="Size.Small" />
|
||||
Allegati — @Attivita.PuntoVendita
|
||||
</span>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Size="Size.Small"
|
||||
Class="sheet-close" OnClick="Chiudi" />
|
||||
</div>
|
||||
}
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="allegati-list">
|
||||
@foreach (var allegato in Attivita.Allegati)
|
||||
{
|
||||
<div class="allegato-item">
|
||||
@if (allegato.Tipo is TipoAllegato.Immagine or TipoAllegato.Piantina)
|
||||
{
|
||||
<div class="allegato-preview">
|
||||
<img src="@allegato.Url" alt="@allegato.Nome" loading="lazy" />
|
||||
<span class="allegato-type-badge @(allegato.Tipo == TipoAllegato.Piantina ? "badge-piantina" : "badge-foto")">
|
||||
@(allegato.Tipo == TipoAllegato.Piantina ? "Piantina" : "Foto")
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="allegato-icon-box @(allegato.Tipo == TipoAllegato.Email ? "icon-email" : "icon-doc")">
|
||||
<MudIcon Icon="@(allegato.Tipo == TipoAllegato.Email ? Icons.Material.Outlined.Email : Icons.Material.Outlined.Article)"
|
||||
Size="Size.Large" />
|
||||
</div>
|
||||
}
|
||||
<div class="allegato-info">
|
||||
<span class="allegato-nome">@allegato.Nome</span>
|
||||
<a href="@allegato.Url" class="allegato-download" target="_blank" title="Apri allegato">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.OpenInNew" Size="Size.Small" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<article class="@CardClass"
|
||||
draggable="@(IsDraggable ? "true" : "false")"
|
||||
@ondragstart="@(() => OnDragStart.InvokeAsync(Attivita))"
|
||||
@ondragover:preventDefault
|
||||
@ondrop="@(() => OnDrop.InvokeAsync(Attivita))">
|
||||
|
||||
<span class="accent-bar"></span>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<header class="card-top">
|
||||
<span class="@($"priorita-pill {PrioritaClass}")">
|
||||
<MudIcon Icon="@PrioritaIcon" Size="Size.Small" />
|
||||
<span>@Attivita.Priorita.ToString().ToUpper()</span>
|
||||
</span>
|
||||
|
||||
@if (Attivita.IsLocked)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small"
|
||||
Class="handle-icon lock-icon" Title="Emergenza: non riordinabile" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.DragIndicator" Size="Size.Small"
|
||||
Class="handle-icon drag-icon" Title="Trascina per riordinare" />
|
||||
}
|
||||
</header>
|
||||
|
||||
<h3 class="card-store">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Store" Size="Size.Small" />
|
||||
<span>@Attivita.PuntoVendita</span>
|
||||
</h3>
|
||||
|
||||
<nav class="card-breadcrumb">
|
||||
<span>@Attivita.Categoria</span>
|
||||
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Size="Size.Small" />
|
||||
<span>@Attivita.Sottocategoria</span>
|
||||
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Size="Size.Small" />
|
||||
<span>@Attivita.Reparto</span>
|
||||
</nav>
|
||||
|
||||
<div class="card-location">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.LocationOn" Size="Size.Small" />
|
||||
<span>@Attivita.Luogo</span>
|
||||
</div>
|
||||
|
||||
<p class="card-desc">@Attivita.Descrizione</p>
|
||||
|
||||
@if (Attivita.Allegati.Count > 0)
|
||||
{
|
||||
<span class="card-allegati-pill">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.AttachFile" Size="Size.Small" />
|
||||
<span>@Attivita.Allegati.Count allegat@(Attivita.Allegati.Count == 1 ? "o" : "i")</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<footer class="card-actions">
|
||||
@if (Attivita.Allegati.Count > 0)
|
||||
{
|
||||
<button type="button" class="btn-card btn-card-ghost"
|
||||
@onclick="@(() => OnVisualizzaAllegati.InvokeAsync(Attivita))">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.FolderOpen" Size="Size.Small" />
|
||||
<span>Allegati</span>
|
||||
</button>
|
||||
}
|
||||
@if (Attivita.Stato == StatoAttivita.Aperta)
|
||||
{
|
||||
<button type="button" class="btn-card btn-card-primary"
|
||||
@onclick="@(() => OnChiudi.InvokeAsync(Attivita))">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" />
|
||||
<span>Chiudi Attività</span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="@($"stato-pill {StatoClass}")">
|
||||
<MudIcon Icon="@StatoIcon" Size="Size.Small" />
|
||||
<span>@Attivita.Stato</span>
|
||||
</span>
|
||||
}
|
||||
</footer>
|
||||
</article>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public AttivitaItem? Attivita { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> OnChiudi { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> OnVisualizzaAllegati { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> OnDragStart { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> 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
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -0,0 +1,259 @@
|
||||
<MudDialog Visible="@(Attivita is not null && Visible)" VisibleChanged="OnVisibleChanged"
|
||||
Options="BottomSheetOptions" Class="bottom-sheet">
|
||||
<TitleContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="sheet-grabber"></div>
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-title">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Build" Size="Size.Small" />
|
||||
@Attivita.PuntoVendita
|
||||
</span>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Size="Size.Small"
|
||||
Class="sheet-close" OnClick="Annulla" />
|
||||
</div>
|
||||
}
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn @(_tab == "chiudi" ? "active" : "")" @onclick='() => _tab = "chiudi"'>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" /> Chiudi
|
||||
</button>
|
||||
<button class="tab-btn @(_tab == "rimanda" ? "active" : "")" @onclick='() => _tab = "rimanda"'>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Small" /> Rimanda
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-body">
|
||||
|
||||
@if (_tab == "chiudi")
|
||||
{
|
||||
<div class="form-row">
|
||||
<MudTimePicker Label="Ora ingresso" @bind-Time="_oraIngresso"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
<MudTimePicker Label="Ora fine lavori" @bind-Time="_oraFine"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">Operatori</MudText>
|
||||
<div class="operatori-list">
|
||||
@for (int i = 0; i < _operatori.Count; i++)
|
||||
{
|
||||
int idx = i;
|
||||
<div class="operatore-row">
|
||||
<MudTextField T="string" Value="_operatori[idx]"
|
||||
ValueChanged="v => _operatori[idx] = v"
|
||||
Placeholder="Nome operatore"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
@if (_operatori.Count > 1)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Remove"
|
||||
Color="Color.Error" Size="Size.Small"
|
||||
OnClick="() => _operatori.RemoveAt(idx)" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Text"
|
||||
Color="Color.Primary" Size="Size.Small"
|
||||
OnClick="() => _operatori.Add(string.Empty)">
|
||||
Aggiungi operatore
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MudTextField T="string" Label="Descrizione intervento"
|
||||
@bind-Value="_descrizioneIntervento"
|
||||
Lines="3" Placeholder="Descrivi l intervento eseguito..."
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||
Required="true"
|
||||
Error="@(_validato && string.IsNullOrWhiteSpace(_descrizioneIntervento))"
|
||||
ErrorText="Campo obbligatorio" />
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">
|
||||
Firma Capo Negozio <span class="required">*</span>
|
||||
</MudText>
|
||||
<SignaturePad OnFirmaCambiata="f => _firma = f" />
|
||||
@if (_validato && string.IsNullOrWhiteSpace(_firma))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Error">Firma obbligatoria</MudText>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">Foto lavoro svolto</MudText>
|
||||
<InputFile class="form-input-file" accept="image/*" multiple
|
||||
OnChange="OnFotoChiusura" />
|
||||
@if (_fotoNomi.Any())
|
||||
{
|
||||
<div class="foto-preview-list">
|
||||
@foreach (var nome in _fotoNomi)
|
||||
{
|
||||
<MudChip T="string" Icon="@Icons.Material.Outlined.Image"
|
||||
Size="Size.Small" Color="Color.Primary">@nome</MudChip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<MudTextField T="string" Label="Commento foto (opzionale)"
|
||||
@bind-Value="_commentoFoto" Lines="2"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||
Class="mt-2" />
|
||||
</div>
|
||||
|
||||
<MudButton FullWidth="true" Variant="Variant.Filled" Color="Color.Primary"
|
||||
Size="Size.Large" StartIcon="@Icons.Material.Filled.CheckCircleOutline"
|
||||
OnClick="ConfermaChiusura">
|
||||
Chiudi Attivita
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField T="string" Label="Motivo del rimando"
|
||||
@bind-Value="_motivoRimando" Lines="3"
|
||||
Placeholder="Es. Non di competenza ufficio tecnico..."
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||
Required="true"
|
||||
Error="@(_validato && string.IsNullOrWhiteSpace(_motivoRimando))"
|
||||
ErrorText="Campo obbligatorio" />
|
||||
|
||||
<MudSelect T="string" Label="Azienda terza (se di competenza)"
|
||||
@bind-Value="_aziendaTerza"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
<MudSelectItem T="string" Value='""'>Nessuna</MudSelectItem>
|
||||
@foreach (var az in _aziendeTerme)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@az">@az</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (_aziendaTerza == "Altro")
|
||||
{
|
||||
<MudTextField T="string" Label="Specifica azienda"
|
||||
@bind-Value="_aziendaAltra"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">
|
||||
Foto KO <span class="required">*</span>
|
||||
</MudText>
|
||||
<InputFile class="form-input-file" accept="image/*" multiple
|
||||
OnChange="OnFotoRimando" />
|
||||
@if (_fotoRimandoNomi.Any())
|
||||
{
|
||||
<div class="foto-preview-list">
|
||||
@foreach (var nome in _fotoRimandoNomi)
|
||||
{
|
||||
<MudChip T="string" Icon="@Icons.Material.Outlined.Image"
|
||||
Size="Size.Small" Color="Color.Warning">@nome</MudChip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (_validato && !_fotoRimandoNomi.Any())
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Error">Almeno una foto e obbligatoria</MudText>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudButton FullWidth="true" Variant="Variant.Filled" Color="Color.Warning"
|
||||
Size="Size.Large" StartIcon="@Icons.Material.Filled.ArrowForward"
|
||||
OnClick="ConfermaRimando">
|
||||
Rimanda Attivita
|
||||
</MudButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
@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<string> _operatori = ["Marco Esposito"];
|
||||
string _descrizioneIntervento = string.Empty;
|
||||
string _firma = string.Empty;
|
||||
string _commentoFoto = string.Empty;
|
||||
List<string> _fotoNomi = [];
|
||||
|
||||
string _motivoRimando = string.Empty;
|
||||
string _aziendaTerza = string.Empty;
|
||||
string _aziendaAltra = string.Empty;
|
||||
List<string> _fotoRimandoNomi = [];
|
||||
|
||||
readonly List<string> _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"];
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -0,0 +1,43 @@
|
||||
@inject IJSRuntime JS
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="signature-container">
|
||||
<canvas id="@_canvasId" width="600" height="200" class="signature-canvas"></canvas>
|
||||
<div class="signature-actions">
|
||||
<button type="button" class="btn-clear-sig" @onclick="CancellaFirma">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
Cancella firma
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public EventCallback<string> OnFirmaCambiata { get; set; }
|
||||
|
||||
readonly string _canvasId = $"sig-{Guid.NewGuid():N}";
|
||||
DotNetObjectReference<SignaturePad>? _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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -13,6 +13,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IntegryApiClient.Core" Version="2.2.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.8" />
|
||||
<PackageReference Include="MudBlazor" Version="9.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public record Allegato(
|
||||
string Id,
|
||||
string Nome,
|
||||
string Url,
|
||||
TipoAllegato Tipo
|
||||
);
|
||||
@@ -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<Allegato> Allegati,
|
||||
StatoAttivita Stato = StatoAttivita.Aperta,
|
||||
int Ordine = 0
|
||||
)
|
||||
{
|
||||
public bool IsLocked => Priorita == PrioritaAttivita.Emergenza;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public record OperatoreItem(
|
||||
string Id,
|
||||
string Nome
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public enum PrioritaAttivita
|
||||
{
|
||||
Normale,
|
||||
Alta,
|
||||
Emergenza
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public enum StatoAttivita
|
||||
{
|
||||
Aperta,
|
||||
Chiusa,
|
||||
Rimandata
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public enum TipoAllegato
|
||||
{
|
||||
Immagine,
|
||||
Piantina,
|
||||
Documento,
|
||||
Email
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using Fixiy.Shared.Models;
|
||||
|
||||
namespace Fixiy.Shared.Services;
|
||||
|
||||
public class MockAttivitaService
|
||||
{
|
||||
public List<AttivitaItem> 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<OperatoreItem> GetOperatoriDefault() =>
|
||||
[
|
||||
new("OP-001", "Marco Esposito"),
|
||||
new("OP-002", "Luca Ferretti")
|
||||
];
|
||||
|
||||
public List<string> GetAziendeTerme() =>
|
||||
[
|
||||
"Arneg",
|
||||
"Desich",
|
||||
"Idracol",
|
||||
"Carrier",
|
||||
"Danfoss",
|
||||
"Alfa Laval",
|
||||
"Altro"
|
||||
];
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/remixicon/remixicon.css" />
|
||||
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/app.css" />
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/default-theme.css" />
|
||||
<link rel="stylesheet" href="Fixiy.Web.styles.css" />
|
||||
@@ -26,6 +27,8 @@
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="_content/Fixiy.Shared/js/signaturePad.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="_content/Fixiy.Shared/js/bootstrap/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMnet107bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
<!-- Add chart.js reference if chart components are used in your application. -->
|
||||
<!--<script src="_content/Fixiy.Shared/js/bootstrap/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>-->
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IntegryApiClient.Blazor" Version="2.2.4" />
|
||||
<PackageReference Include="MudBlazor" Version="9.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<IFormFactor, FormFactor>();
|
||||
builder.Services.AddScoped<MockAttivitaService>();
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user