Allineamento da prod

This commit is contained in:
2025-12-11 08:36:48 +01:00
parent 0cac2ec202
commit 0a5b30fa0a
13 changed files with 340 additions and 42 deletions

View File

@ -0,0 +1,18 @@
using StandManager.Domain.Entita.Base;
using System.ComponentModel.DataAnnotations.Schema;
namespace StandManager.Domain.Entita;
public class InvitoEvento : EntitaBase
{
[ForeignKey(nameof(Evento))]
public Guid? EventoId { get; set; }
public Evento Evento{ get; set; }
[ForeignKey(nameof(Cliente))]
public Guid? ClienteId { get; set; }
public Cliente Cliente { get; set; }
[InverseProperty(nameof(IscrizioneEvento.InvitoEvento))]
public List<IscrizioneEvento> IscrizioniEvento { get; set; }
}

View File

@ -3,20 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace StandManager.Domain.Entita;
public class InvitoEvento : EntitaBase
{
[ForeignKey(nameof(Evento))]
public Guid? EventoId { get; set; }
public Evento Evento{ get; set; }
[ForeignKey(nameof(Cliente))]
public Guid? ClienteId { get; set; }
public Cliente Cliente { get; set; }
[InverseProperty(nameof(IscrizioneEvento.InvitoEvento))]
public List<IscrizioneEvento> IscrizioniEvento { get; set; }
}
public class IscrizioneEvento : EntitaBase
{
[ForeignKey(nameof(Evento))]

View File

@ -3,7 +3,6 @@ using StandManager.Domain.Entita;
namespace StandManager.Service.Interfaces;
public interface IIscrizioneEventoService : ITService<IscrizioneEvento>
{
public interface IIscrizioneEventoService : ITService<IscrizioneEvento>{
Task<IscrizioneEvento> Salva(IscrizioneEvento model);
}

View File

@ -6,7 +6,7 @@
<div class="page">
<!-- NAV MENU MANAGEMENT -->
<header class="navbar navbar-expand-md d-print-none" >
<header class="navbar navbar-expand-md d-print-none">
<div class="container-xl">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>

View File

@ -1,6 +1,7 @@
@inherits LayoutComponentBase
@using StandManager.Components.Pages.Management
<PublicHeader />
@Body
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
<script src="/js/qrScanner.js"></script>

View File

@ -1,4 +1,5 @@
@page "/"
@page "/Home"
@page "/{invitationId:guid}"
@layout PublicLayout
@ -12,6 +13,40 @@
<PageTitle>Iscrizioni Evento</PageTitle>
<AuthorizeView>
<Authorized>
<header class="navbar navbar-expand-lg navbar-transparent py-3">
<div class="container">
<div class="collapse navbar-collapse">
<nav class="navbar-nav ms-auto">
<div class="nav-item">
<a class="nav-link" href="/management"><span class="nav-link-title">Management</span></a>
</div>
<div class="nav-item">
<a class="nav-link" href="/scan"><span class="nav-link-title">Scansione</span></a>
</div>
</nav>
</div>
</div>
</header>
</Authorized>
<NotAuthorized>
<header class="navbar navbar-expand-lg navbar-transparent py-3">
<div class="container">
<div class="collapse navbar-collapse">
<nav class="navbar-nav ms-auto">
<div class="nav-item">
<a class="nav-link" href="/management"><span class="nav-link-title">Management</span></a>
</div>
</nav>
</div>
</div>
</header>
</NotAuthorized>
</AuthorizeView>
<header class="hero pb-0">
<div class="container">
@if (invitationId.HasValue && invito != null)
@ -128,13 +163,12 @@
protected override async Task OnInitializedAsync()
{
base.OnInitializedAsync();
invito = invitationId.GetValueOrDefault() != Guid.Empty
? await _managerService.InvitoEventoService.RicercaPer(x => x.Id == invitationId && x.Eliminato == false,
includi: x => x.Include(y => y.Evento).Include(y => y.Cliente).ThenInclude(y => y.Destinazioni).Include(y => y.IscrizioniEvento))
: new();
if(invito == null)
if (invito == null)
invalidCode = "Il codice inserito non risulta corretto";
else
{
@ -177,7 +211,6 @@
await _managerService.IscrizioneEventoService.Salva(model);
_navManager.NavigateTo($"/");
}
}

View File

@ -112,7 +112,7 @@
model = destinazione.Map(model);
if (destinazione.AgenteId.GetValueOrDefault() != Guid.Empty)
model.AgenteId = destinazione.AgenteId;
model.Agente = await _managerService.UtenteService.RicercaPer(x => x.Id == destinazione.AgenteId);
await _managerService.DestinazioneService.Salva(model, idClaim);
_dialogService.Close(true);

View File

@ -1,9 +1,12 @@
@attribute [Authorize]
@page "/management/Eventi"
@using Microsoft.EntityFrameworkCore
@using StandManager.Model
@rendermode InteractiveServer
@inject TooltipService tooltipService
<PageTitle>Eventi</PageTitle>
<div class="page-wrapper">
@ -37,8 +40,9 @@
<RadzenDataGridColumn Context="evento" Filterable="false" Sortable="false" TextAlign="TextAlign.Right" Width="250px">
<Template Context="evento">
<RadzenButton Icon="edit" ButtonStyle="ButtonStyle.Light" Variant="Variant.Flat" Size="ButtonSize.Medium" class="rz-my-1 rz-ms-1" Click="@(args => EditRow(evento))" @onclick:stopPropagation="true" />
<RadzenButton Icon="delete" ButtonStyle="ButtonStyle.Danger" Variant="Variant.Flat" Size="ButtonSize.Medium" Shade="Shade.Lighter" class="rz-my-1 rz-ms-1" Click="@(args => DeleteRow(evento))" @onclick:stopPropagation="true" />
<RadzenButton Icon="calendar_clock" ButtonStyle="ButtonStyle.Light" Variant="Variant.Flat" Size="ButtonSize.Medium" class="rz-my-1 rz-ms-1" Click="@(args => SendInvitation(evento))" @onclick:stopPropagation="true" MouseEnter="@(args => ShowTooltip(args, new TooltipOptions() { Text = "Invia invito" }))" />
<RadzenButton Icon="edit" ButtonStyle="ButtonStyle.Light" Variant="Variant.Flat" Size="ButtonSize.Medium" class="rz-my-1 rz-ms-1" Click="@(args => EditRow(evento))" @onclick:stopPropagation="true" MouseEnter="@(args => ShowTooltip(args, new TooltipOptions() { Text = "Modifica evento" }))" />
<RadzenButton Icon="delete" ButtonStyle="ButtonStyle.Danger" Variant="Variant.Flat" Size="ButtonSize.Medium" Shade="Shade.Lighter" class="rz-my-1 rz-ms-1" Click="@(args => DeleteRow(evento))" @onclick:stopPropagation="true" MouseEnter="@(args => ShowTooltip(args, new TooltipOptions() { Text = "Rimuovi evento" }))" />
</Template>
</RadzenDataGridColumn>
</Columns>
@ -77,4 +81,18 @@
eventi = (await _managerService.EventoService.RicercaQueryable(x => x.Eliminato == false)).Select(x => (EventoViewModel)x).ToList();
}
}
private async Task SendInvitation(EventoViewModel evento)
{
var ok = await _dialogService.Confirm($"Vuoi davvero invitare i clienti all'evento {evento.Titolo}?", "Conferma invito", new ConfirmOptions { OkButtonText = "Sì", CancelButtonText = "No", Width = "400px" });
if (ok == true)
{
_dialogService.Close();
//Invito
await _dialogService.OpenAsync<Eventi_Inviti>("Inviti", new Dictionary<string, object>() { { "eventoId", evento.Id } });
}
}
void ShowTooltip(ElementReference elementReference, TooltipOptions options = null) => tooltipService.Open(elementReference, options.Text, options);
}

View File

@ -0,0 +1,46 @@
@using Microsoft.EntityFrameworkCore
<RadzenStack Gap="1rem" class="rz-m-12">
<RadzenProgressBar Value="@counter" Max="@clientiTotali" Unit="@counterLabel" AriaLabel="" />
</RadzenStack>
@code {
[CascadingParameter] private Dialog _dialog { get; set; }
[Parameter] public Guid eventoId { get; set; } = Guid.Empty;
private int clientiTotali { get; set; } = 0;
private int counter { get; set; } = 0;
private string counterLabel{ get; set; } = string.Empty;
protected override async Task OnInitializedAsync()
{
base.OnInitializedAsync();
var evento = await _managerService.EventoService.RicercaPer(x => x.Eliminato == false && x.Id == eventoId);
var clienti = await _managerService.ClienteService.RicercaQueryable(x => x.Eliminato == false);
var invitati = await _managerService.InvitoEventoService.RicercaQueryable(x => x.Eliminato == false && x.EventoId == eventoId, includi: y => y.Include(z => z.Cliente));
var clientiInvitatiIds = invitati.Select(x => x.ClienteId).ToList();
var userId = await MembershipUtils.GetUserId(_auth);
counter = 0;
clientiTotali = clienti.Count(c => !clientiInvitatiIds.Contains(c.Id));
foreach (var c in clienti.Where(c => !clientiInvitatiIds.Contains(c.Id)))
{
var invito = new InvitoEvento
{
ClienteId = c.Id,
EventoId = evento.Id,
Eliminato = false
};
await _managerService.InvitoEventoService.Salva(invito, userId);
counter += 1;
counterLabel = " di " + clientiTotali;
StateHasChanged();
await Task.Delay(10); // Simula un ritardo per l'invio dell'email
}
_dialogService.Close();
}
}

View File

@ -27,13 +27,13 @@
<div class="col-lg-12">
<div class="card">
<div class="table-responsive">
<RadzenDataGrid @ref="userGrid" AllowFiltering="true" AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.CheckBoxList" AllowSorting="true" PageSize="5"
<RadzenDataGrid @ref="userGrid" AllowFiltering="true" AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.CheckBoxList" AllowSorting="true" PageSize="15"
AllowPaging="true" PagerHorizontalAlign="HorizontalAlign.Left" ShowPagingSummary="true"
Data="@utenti" ColumnWidth="300px" LogicalFilterOperator="LogicalFilterOperator.Or" SelectionMode="DataGridSelectionMode.Single">
<Columns>
<RadzenDataGridColumn Property="@nameof(Utente.Id)" Filterable="false" Title="ID" Width="80px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn Property="@nameof(Utente.Nome)" Title="First Name" Width="160px" />
<RadzenDataGridColumn Property="@nameof(Utente.Cognome)" Title="Last Name" Width="160px" />
<RadzenDataGridColumn Property="@nameof(Utente.Nome)" Title="Nome" Width="160px" />
<RadzenDataGridColumn Property="@nameof(Utente.Cognome)" Title="Cognome" Width="160px" />
<RadzenDataGridColumn Property="@nameof(Utente.Email)" Title="Mail" Width="200px" />
<RadzenDataGridColumn Context="order" Filterable="false" Sortable="false" TextAlign="TextAlign.Right">

View File

@ -0,0 +1,110 @@
@page "/scan"
@using StandManager.Components.Layout
@layout PublicLayout
@inject IJSRuntime JS
@inject BodyClassService BodyClass
@rendermode InteractiveServer
<PageTitle>Scan</PageTitle>
<header class="hero pb-0">
<div class="container">
<div class="mb-3">
<video @ref="videoRef" autoplay playsinline style="width:100%;max-width:400px;border:1px solid #ccc;border-radius:8px;"></video>
</div>
<button class="btn btn-primary" @onclick="StartScan" disabled="@isScanning">
Avvia scansione
</button>
<button class="btn btn-secondary" @onclick="StopScan" disabled="@(!isScanning)">
Ferma
</button>
</div>
</header>
@if (!string.IsNullOrWhiteSpace(lastResult))
{
<div class="alert alert-success">
<strong>QR letto:</strong> @lastResult
</div>
}
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
<div class="alert alert-danger">
<strong>Errore:</strong> @errorMessage
</div>
}
@code {
private ElementReference videoRef;
private DotNetObjectReference<Scan>? objRef;
private bool isScanning;
private string? lastResult;
private string? errorMessage;
protected override void OnInitialized()
{
objRef = DotNetObjectReference.Create(this);
}
private async Task StartScan()
{
errorMessage = null;
lastResult = null;
if (isScanning)
return;
isScanning = true;
await JS.InvokeVoidAsync("qrScanner.start", videoRef, objRef);
}
private async Task StopScan()
{
if (!isScanning)
return;
isScanning = false;
await JS.InvokeVoidAsync("qrScanner.stop", videoRef);
}
[JSInvokable]
public Task OnQrDecoded(string text)
{
lastResult = text;
isScanning = false;
StateHasChanged();
return Task.CompletedTask;
}
[JSInvokable]
public Task OnQrError(string message)
{
errorMessage = message;
isScanning = false;
StateHasChanged();
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
if (isScanning)
{
await StopScan();
}
objRef?.Dispose();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await BodyClass.SetBodyClass("body-marketing body-gradient");
}
}

View File

@ -1,12 +1,21 @@
@using Microsoft.AspNetCore.Components.Authorization
<Router AppAssembly="@typeof(Program).Assembly">
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
<NotFound>
<LayoutView Layout="@typeof(Layout.MainLayout)">
<p>Pagina non trovata.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

View File

@ -0,0 +1,78 @@
window.qrScanner = (function () {
let animationFrameId = null;
let currentStream = null;
async function start(videoElement, dotNetRef) {
try {
// chiede accesso alla camera (di solito quella posteriore su mobile)
const constraints = {
video: {
facingMode: "environment"
}
};
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
videoElement.srcObject = currentStream;
await videoElement.play();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const tick = async () => {
if (!videoElement || videoElement.readyState !== videoElement.HAVE_ENOUGH_DATA) {
animationFrameId = requestAnimationFrame(tick);
return;
}
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, canvas.width, canvas.height, {
inversionAttempts: "dontInvert"
});
if (code) {
console.log("QR trovato:", code.data);
// Chiama il metodo C# marcato [JSInvokable]
await dotNetRef.invokeMethodAsync("OnQrDecoded", code.data);
stop(videoElement); // ferma appena trovato
return;
}
animationFrameId = requestAnimationFrame(tick);
};
tick();
} catch (err) {
console.error("Errore avvio camera:", err);
if (dotNetRef) {
await dotNetRef.invokeMethodAsync("OnQrError", err.message ?? "Errore sconosciuto");
}
}
}
function stop(videoElement) {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (currentStream) {
currentStream.getTracks().forEach(t => t.stop());
currentStream = null;
}
if (videoElement) {
videoElement.srcObject = null;
}
}
return {
start: start,
stop: stop
};
})();