Prima implementazione pagina "Attività"

This commit is contained in:
2026-06-08 09:25:29 +02:00
parent 4661922633
commit b7955f0e1f
34 changed files with 1582 additions and 266 deletions
@@ -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; }