Tipologia cliente

This commit is contained in:
2025-12-15 11:01:14 +01:00
parent 69d28744f6
commit 664cee9656
22 changed files with 1118 additions and 491 deletions

View File

@ -0,0 +1,24 @@
<footer class="footer bg-red text-white">
<div class="container-xl">
<div class="row align-items-start">
<h3><b>DAC S.p.A.</b></h3>
<div class="col-4 text-left">
C.F. / P. IVA: IT03038290171<br />
Reg. Imp. di città n. 03038290171<br />
R.E.A. n. BS-313463<br />
Capitale Sociale € 3.000.000,00 i.v.<br />
</div>
<div class="col-4 text-left">
<i class="fa-regular fa-map"></i><b> Sede legale e amministrativa</b><br />
Via G.Marconi, n.15 - 25020 Flero (BS)<br />
<i class="fa-solid fa-phone"></i><b> Tel. +39 030 256 8211</b><br />
<i class="fa-regular fa-envelope"></i><b> info@gruppodac.eu</b>
</div>
<div class="col-4 text-center">
<a href="#" class="text-white mx-2 text-decoration-none" aria-label="Button"><i class="fa-brands fa-facebook"></i></a>
<a href="#" class="text-white mx-2 text-decoration-none" aria-label="Button"><i class="fa-brands fa-instagram"></i></a>
<a href="#" class="text-white mx-2 text-decoration-none" aria-label="Button"><i class="fa-brands fa-youtube"></i></a>
</div>
</div>
</div>
</footer>

View File

@ -116,9 +116,9 @@
</div>
<div class="col-6 mb-3">
<RadzenText TextStyle="TextStyle.Subtitle2" TagName="TagName.H3">Tipologia</RadzenText>
<RadzenDropDown @bind-Value="@iscrizione.TipologiaInt" TValue="int?" Style="width: 100%" TextProperty="Description" ValueProperty="Key" Placeholder="Seleziona la tipologia"
<RadzenDropDown @bind-Value="@iscrizione.TipologiaClienteId" TValue="Guid" Style="width: 100%" TextProperty="Description" ValueProperty="Key" Placeholder="Seleziona la tipologia"
Data="@tipologiaList" Size="ButtonSize.Small" />
<ValidationMessage For="@(() => iscrizione.TipologiaInt)" />
<ValidationMessage For="@(() => iscrizione.TipologiaClienteId)" />
</div>
</div>
@ -140,9 +140,9 @@
<div class="row">
<div class="col-6 mb-3">
<RadzenText TextStyle="TextStyle.Subtitle2" TagName="TagName.H3">Destinazione</RadzenText>
<RadzenDropDown @bind-Value="@iscrizione.Destinazione" Style="width: 100%" TextProperty="RagioneSociale" Placeholder="Seleziona la destinazione"
<RadzenDropDown @bind-Value="@iscrizione.DestinazioneId" Style="width: 100%" ValueProperty="Id" TextProperty="RagioneSociale" Placeholder="Seleziona la destinazione"
Data="@destinazioniList" Size="ButtonSize.Small" />
<ValidationMessage For="@(() => iscrizione.Destinazione)" />
<ValidationMessage For="@(() => iscrizione.DestinazioneId)" />
</div>
<div class="col-6 mb-3">
<RadzenText TextStyle="TextStyle.Subtitle2" TagName="TagName.H3">Numero di partecipanti</RadzenText>
@ -221,31 +221,6 @@
}
</div>
<footer class="footer bg-red text-white">
<div class="container-xl">
<div class="row align-items-start">
<h3><b>DAC S.p.A.</b></h3>
<div class="col-4 text-left">
C.F. / P. IVA: IT03038290171<br />
Reg. Imp. di città n. 03038290171<br />
R.E.A. n. BS-313463<br />
Capitale Sociale € 3.000.000,00 i.v.<br />
</div>
<div class="col-4 text-left">
<b><i class="fa-regular fa-map"></i> Sede legale e amministrativa</b><br />
Via G.Marconi, n.15 - 25020 Flero (BS)<br />
<b><i class="fa-solid fa-phone"></i> Tel. +39 030 256 8211</b><br />
<b><i class="fa-regular fa-envelope"></i> info@gruppodac.eu</b>
</div>
<div class="col-4 text-center">
<a href="#" class="text-white mx-2 text-decoration-none" aria-label="Button"><i class="fa-brands fa-facebook"></i></a>
<a href="#" class="text-white mx-2 text-decoration-none" aria-label="Button"><i class="fa-brands fa-instagram"></i></a>
<a href="#" class="text-white mx-2 text-decoration-none" aria-label="Button"><i class="fa-brands fa-youtube"></i></a>
</div>
</div>
</div>
</footer>
@code {
[Parameter]
public Guid? invitationId { get; set; }
@ -257,7 +232,7 @@
private IEnumerable<string> comuneList { get; set; }
private IEnumerable<string> provinciaList { get; set; }
private IEnumerable<DestinazioneViewModel> destinazioniList { get; set; }
private IEnumerable<LookupViewModel<int>> tipologiaList { get; set; }
private IEnumerable<LookupViewModel<Guid>> tipologiaList { get; set; }
private IEnumerable<string> esperienzaList { get; set; }
private IEnumerable<LookupViewModel<int>> ruoloList { get; set; }
@ -283,7 +258,9 @@
provinciaList = new List<string>() { "BS" };
esperienzaList = new List<string>() { "Si", "No" };
var eUtils = new EnumUtils();
tipologiaList = eUtils.GetEnumList<ClienteTipo>();
tipologiaList = (await _managerService.TipologiaClienteService.RicercaQueryable(x => x.Eliminato == false))
.Select(x => new LookupViewModel<Guid>(x.Id, x.Nome)).ToList();
ruoloList = eUtils.GetEnumList<RuoloTipo>();
}
}
@ -313,11 +290,13 @@
private async Task onIscrizioneSave()
{
var model = new IscrizioneEvento() { EventoId = invito.EventoId, InvitoEventoId = invito.Id, ClienteId = invito.ClienteId, DestinazioneId = iscrizione.Destinazione.Id };
var model = new IscrizioneEvento() {
EventoId = invito.EventoId,
InvitoEventoId = invito.Id,
ClienteId = invito.ClienteId,
DestinazioneId = iscrizione.DestinazioneId };
model = iscrizione.Map(model);
// TODO: Implementazione generazione QR Code
model.QrCodeCode = Guid.NewGuid().ToString();
model.TipologiaCliente = await _managerService.TipologiaClienteService.RicercaPer(x => x.Id == iscrizione.TipologiaClienteId && x.Eliminato == false);
await _managerService.IscrizioneEventoService.Salva(model);

View File

@ -1,5 +1,6 @@
@attribute [Authorize]
@page "/management/Clienti"
@using ClosedXML.Excel
@using Microsoft.EntityFrameworkCore
@using StandManager.Model
@ -21,6 +22,8 @@
</div>
<div class="col-auto ms-auto">
<div class="btn-list">
<RadzenUpload class="btn-5 d-none d-sm-inline-block" Change=@onUpload ChooseText="Importa da excel" />
<a href="/management/Clienti/Modifica" class="btn btn-primary btn-5 d-none d-sm-inline-block">
Nuovo cliente
</a>
@ -29,7 +32,7 @@
<div class="col-lg-12">
<div class="card">
<div class="table-responsive">
<RadzenDataGrid @ref="clientiGrid" AllowFiltering="true" AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.CheckBoxList" AllowSorting="true" PageSize="5"
<RadzenDataGrid @ref="clientiGrid" AllowFiltering="true" AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.CheckBoxList" AllowSorting="true" PageSize="25"
AllowPaging="true" PagerHorizontalAlign="HorizontalAlign.Left" ShowPagingSummary="true"
Data="@clienti" LogicalFilterOperator="LogicalFilterOperator.Or" SelectionMode="DataGridSelectionMode.Single">
<Columns>
@ -93,4 +96,55 @@
.Select(x => (ClienteViewModel)x).ToList();
}
}
private async Task onUpload(UploadChangeEventArgs args)
{
var file = args.Files.FirstOrDefault();
if (file == null && (!file.Name.EndsWith(".xlsx") || !file.Name.EndsWith(".xls")))
{
await _dialogService.Alert("Il file selezionato non è nel formato corretto.", "Errore");
return;
}
try
{
await using var uploadStream = file.OpenReadStream(10_000_000);
await using var ms = new MemoryStream();
await uploadStream.CopyToAsync(ms);
ms.Position = 0;
using var workbook = new XLWorkbook(ms);
var ws = workbook.Worksheet(1);
var usedRange = ws.RangeUsed();
var ragioniSociali = ws.RangeUsed().RowsUsed().Skip(1).Select(r => new { Rid = r.Cell(1).GetString(), RagioneSociale = r.Cell(4).GetString() }).Distinct().ToList();
foreach (var cliente in ragioniSociali)
{
var righeCliente = usedRange.RowsUsed().Where(r => r.Cell(1).GetString() == cliente.Rid).ToList();
var clienteDb = await _managerService.ClienteService.RicercaPer(
x => x.Rid == cliente.Rid && x.Eliminato == false,
includi: x => x.Include(i => i.Destinazioni),
solaLettura: false) ?? new Cliente();
clienteDb = mapCliente(clienteDb, righeCliente);
}
ms.Close();
}
catch (Exception ex)
{
var er = ex.Message;
await _dialogService.Alert("Si è verificato un errore durante l'importazione del file.", "Errore");
}
}
private Cliente mapCliente(Cliente model, List<IXLRangeRow> rows)
{
var firstRow = rows.First();
model.Rid = firstRow.Cell(1).GetString();
model.RagioneSociale = firstRow.Cell(4).GetString();
return model;
}
}

View File

@ -88,11 +88,11 @@
<div class="row">
<div class="col-3 mb-3">
<RadzenText TextStyle="TextStyle.Subtitle2" TagName="TagName.H3">Agente</RadzenText>
<RadzenDropDown Style="width: 100%" TValue="Guid?" @bind-Value=@cliente.AgenteId Data=@agenti TextProperty="Info" ValueProperty="Id" Name="agenteDrop" />
<RadzenDropDown Style="width: 100%" TValue="Guid ?" @bind-Value=@cliente.AgenteId Data=@agenti TextProperty="Info" ValueProperty="Id" Name="agenteDrop" />
</div>
<div class="col-3 mb-3">
<RadzenText TextStyle="TextStyle.Subtitle2" TagName="TagName.H3">Tipologia</RadzenText>
<RadzenDropDown Style="width: 100%" TValue="int" @bind-Value=@cliente.TipologiaClienteInt Data=@tipologiaCliente TextProperty="Description" ValueProperty="Key" Name="tipologiaClienteDrop" />
<RadzenDropDown Style="width: 100%" TValue="Guid ?" @bind-Value=@cliente.TipologiaClienteId Data=@tipologiaCliente TextProperty="Nome" ValueProperty="Id" Name="tipologiaClienteDrop" />
</div>
<div class="col-3 mb-3">
<RadzenText TextStyle="TextStyle.Subtitle2" TagName="TagName.H3">Stato</RadzenText>
@ -174,7 +174,7 @@
private ClienteViewModel cliente { get; set; } = new();
private IEnumerable<UtenteViewModel> agenti { get; set; } = new List<UtenteViewModel>();
private List<LookupViewModel<int>> tipologiaCliente { get; set; } = new List<LookupViewModel<int>>();
private List<TipologiaClienteViewModel> tipologiaCliente { get; set; } = new();
private List<LookupViewModel<int>> statoCliente { get; set; } = new List<LookupViewModel<int>>();
RadzenDataGrid<DestinazioneViewModel> destinazioniGrid;
@ -197,11 +197,11 @@
agenti = (await _managerService.UtenteService.RicercaQueryable(x => x.Eliminato == false, ordinamento: x => x.OrderBy(y => y.Cognome).ThenBy(z => z.Nome)))
.Select(x => (UtenteViewModel)x).ToList();
var eUtils = new EnumUtils();
tipologiaCliente = eUtils.GetEnumList<ClienteTipo>();
tipologiaCliente = (await _managerService.TipologiaClienteService.RicercaQueryable(ordinamento: x => x.OrderBy(y => y.Nome))).Select(x => (TipologiaClienteViewModel)x).ToList();
statoCliente = eUtils.GetEnumList<ClienteStato>();
if (ClienteId.GetValueOrDefault() != Guid.Empty)
cliente = await _managerService.ClienteService.RicercaPer(x => x.Id == ClienteId, includi: x => x.Include(y => y.Agente).Include(y => y.Destinazioni).ThenInclude(z => z.Agente));
cliente = await _managerService.ClienteService.RicercaPer(x => x.Id == ClienteId, includi: x => x.Include(y => y.Agente).Include(y => y.TipologiaCliente).Include(y => y.Destinazioni).ThenInclude(z => z.Agente));
else
cliente = new ClienteViewModel();
@ -234,6 +234,12 @@
if (cliente.AgenteId.GetValueOrDefault() != Guid.Empty)
model.Agente = await _managerService.UtenteService.RicercaPer(x => x.Id == cliente.AgenteId);
if (cliente.TipologiaClienteId.GetValueOrDefault() != Guid.Empty)
{
model.TipologiaClienteId = cliente.TipologiaClienteId;
model.TipologiaCliente = await _managerService.TipologiaClienteService.RicercaPer(x => x.Id == cliente.TipologiaClienteId);
}
if (cliente.Id == Guid.Empty)
{
var destinazione = new Destinazione() { Cliente = model };
@ -260,7 +266,7 @@
{
await _dialogService.OpenAsync<Cliente_Destinazione>($"Destinazione {destinazione.RagioneSociale}", new Dictionary<string, object>() { { "destinazioneId", destinazione.Id }, { "clienteId", cliente.Id } }, editNewDialogOption);
}
/// <summary>
/// Apre il dialog per creare una nuova destinazione collegata al cliente.
/// </summary>

View File

@ -29,7 +29,7 @@
<div class="col-lg-12">
<div class="card">
<div class="table-responsive">
<RadzenDataGrid @ref="eventiGrid" AllowFiltering="true" AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.CheckBoxList" AllowSorting="true" PageSize="5"
<RadzenDataGrid @ref="eventiGrid" AllowFiltering="true" AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.CheckBoxList" AllowSorting="true" PageSize="25"
AllowPaging="true" PagerHorizontalAlign="HorizontalAlign.Left" ShowPagingSummary="true"
Data="@eventi" LogicalFilterOperator="LogicalFilterOperator.Or" SelectionMode="DataGridSelectionMode.Single">
<Columns>

View File

@ -27,7 +27,7 @@
<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="15"
<RadzenDataGrid @ref="userGrid" AllowFiltering="true" AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.CheckBoxList" AllowSorting="true" PageSize="25"
AllowPaging="true" PagerHorizontalAlign="HorizontalAlign.Left" ShowPagingSummary="true"
Data="@utenti" ColumnWidth="300px" LogicalFilterOperator="LogicalFilterOperator.Or" SelectionMode="DataGridSelectionMode.Single">
<Columns>

View File

@ -2,10 +2,11 @@
using System.ComponentModel.DataAnnotations;
namespace StandManager.Model;
public class ClienteViewModel
{
public Guid Id { get; set; }
[Required(ErrorMessage ="La ragione sociale è obbligatoria")]
[Required(ErrorMessage = "La ragione sociale è obbligatoria")]
public string RagioneSociale { get; set; }
[Required(ErrorMessage = "La Partita IVA")]
public string PartitaIva { get; set; }
@ -21,8 +22,8 @@ public class ClienteViewModel
public List<DestinazioneViewModel> Destinazioni { get; set; }
public string Rid { get; set; }
public ClienteTipo TipologiaCliente { get; set; }
public int TipologiaClienteInt { get; set; }
public TipologiaClienteViewModel TipologiaCliente { get; set; }
public Guid? TipologiaClienteId { get; set; }
public ClienteStato StatoCliente { get; set; }
public int StatoClienteInt { get; set; }
@ -47,7 +48,7 @@ public class ClienteViewModel
RagioneSociale = model.RagioneSociale,
Rid = model.Rid,
TipologiaCliente = model.TipologiaCliente,
TipologiaClienteInt = (int)model.TipologiaCliente,
TipologiaClienteId = model.TipologiaCliente?.Id ?? Guid.Empty,
StatoCliente = model.StatoCliente,
StatoClienteInt = (int)model.StatoCliente
};
@ -64,7 +65,6 @@ public class ClienteViewModel
model.EmailInvito = EmailInvito;
model.NumeroTelefono = NumeroTelefono;
model.Rid = Rid;
model.TipologiaCliente = (ClienteTipo)TipologiaClienteInt;
model.StatoCliente = (ClienteStato)StatoClienteInt;
return model;
@ -84,3 +84,20 @@ public class ClienteViewModel
return model;
}
}
public class TipologiaClienteViewModel
{
public Guid Id { get; set; }
public string Nome { get; set; }
public static implicit operator TipologiaClienteViewModel(TipologiaCliente model)
{
return model == null
? null
: new TipologiaClienteViewModel()
{
Id = model.Id,
Nome = model.Nome
};
}
}

View File

@ -11,13 +11,12 @@ public class IscrizioneEventoViewModel
public ClienteViewModel Cliente { get; set; }
[Required(ErrorMessage = "La destinazione è obbligatoria")]
public DestinazioneViewModel Destinazione { get; set; }
public Guid DestinazioneId { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Inserire un numero di partecipanti validi")]
public int Partecipanti { get; set; }
[Required(ErrorMessage = "Le note sono obbligatorie")]
public string Note { get; set; }
public string QrCodeCode { get; set; }
public bool ScanCompleto { get; set; }
public DateTime? DataScan { get; set; }
[Required(ErrorMessage = "Il nome è obbigatorio")]
@ -38,10 +37,8 @@ public class IscrizioneEventoViewModel
public string RagioneSociale { get; set; }
[Required(ErrorMessage = "Il campo è obbigatorio")]
public string EsperienzaConDAC { get; set; }
public ClienteTipo Tipologia { get; set; }
[Required(ErrorMessage = "La tipologia è obbigatoria")]
public int? TipologiaInt { get; set; }
public Guid TipologiaClienteId { get; set; }
public RuoloTipo Ruolo { get; set; }
[Required(ErrorMessage = "Il ruolo è obbigatorio")]
public int? RuoloInt { get; set; }
@ -49,43 +46,10 @@ public class IscrizioneEventoViewModel
public bool PresaVisioneDatiPersonali { get; set; }
public static implicit operator IscrizioneEventoViewModel(IscrizioneEvento model)
{
return model == null
? null
: new IscrizioneEventoViewModel()
{
Id = model.Id,
Evento = model.Evento,
InvitoEvento = model.InvitoEvento,
Cliente = model.Cliente,
Destinazione = model.Destinazione,
Partecipanti = model.Partecipanti,
Note = model.Note,
QrCodeCode = model.QrCodeCode,
ScanCompleto = model.ScanCompleto,
DataScan = model.DataScan,
Nome = model.Nome,
Cognome = model.Cognome,
Email = model.Email,
NumeroTelefono = model.NumeroTelefono,
Provincia = model.Provincia,
Comune = model.Comune,
Cap = model.Cap,
RagioneSociale = model.RagioneSociale,
EsperienzaConDAC = model.EsperienzaConDAC,
Tipologia = model.Tipologia,
TipologiaInt = (int)model.Tipologia,
Ruolo = model.Ruolo,
RuoloInt = (int)model.Ruolo,
};
}
public IscrizioneEvento Map(IscrizioneEvento model)
{
model.Partecipanti = Partecipanti;
model.Note = Note;
model.QrCodeCode = QrCodeCode;
model.ScanCompleto = ScanCompleto;
model.DataScan = DataScan;
model.Nome = Nome;
@ -97,8 +61,6 @@ public class IscrizioneEventoViewModel
model.Cap = Cap;
model.RagioneSociale = RagioneSociale;
model.EsperienzaConDAC = EsperienzaConDAC;
model.Tipologia = (ClienteTipo)TipologiaInt;
model.Ruolo = (RuoloTipo)RuoloInt;
return model;
}

View File

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.22" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.22" />

View File

@ -1,376 +1,8 @@
html {
font-size: 14px;
.footer {
font-size: 0.875rem; /* stesso size Tabler */
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px;
}
:root {
--tblr-primary: #90bd22 !important;
}
.card-selectable {
min-height: 240px;
display: flex;
flex-direction: column;
justify-content: flex-end; /* permette di vedere prima l'immagine poi il titolo in fondo */
margin-bottom: 20px; /* spazio tra le righe */
overflow: visible;
}
.card-selectable-opt {
display: flex;
flex-direction: column;
justify-content: flex-end; /* permette di vedere prima l'immagine poi il titolo in fondo */
margin-bottom: 20px; /* spazio tra le righe */
overflow: visible;
}
.card-widget-image-vert-s {
background-size: contain; /* Cambiato da auto 100% a cover */
background-position: center;
background-repeat: no-repeat;
height: 50px;
min-height: 50px;
max-height: 50px;
border-radius: 8px 8px 0 0;
overflow: hidden; /* Nasconde tutto ciò che esce dal box */
}
.card-widget-image-vert-m {
background-size: contain; /* Cambiato da auto 100% a cover */
background-position: center;
background-repeat: no-repeat;
height: 100px;
min-height: 100px;
max-height: 100px;
border-radius: 8px 8px 0 0;
overflow: hidden; /* Nasconde tutto ciò che esce dal box */
}
.card-widget-image-vert-l {
background-size: contain; /* Cambiato da auto 100% a cover */
background-position: center;
background-repeat: no-repeat;
height: 150px;
min-height: 150px;
max-height: 150px;
border-radius: 8px 8px 0 0;
overflow: hidden; /* Nasconde tutto ciò che esce dal box */
}
.card-widget-image-vert-xl {
background-size: contain; /* Cambiato da auto 100% a cover */
background-position: center;
background-repeat: no-repeat;
height: 200px;
min-height: 200px;
max-height: 200px;
border-radius: 8px 8px 0 0;
overflow: hidden; /* Nasconde tutto ciò che esce dal box */
}
.card-widget-title {
width: 100%;
background: #f5f5f5 !important;
color: #222 !important;
display: block;
padding: 8px 16px;
border-radius: 0 0 8px 8px;
margin: 0;
min-height: 44px;
border-top: 1px solid #ececec;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
transition: background 0.2s;
/* Aggiungi:
margin-bottom per separare la card dalla riga sotto
*/
margin-bottom: 2px;
}
.card-widget-image {
background-size:cover;
background-position:center;
height:75px;
min-height:75px;
max-height:75px;
}
.card-widget {
height:75px;
min-height:75px;
max-height:75px;
}
.card-color-preview-s {
height: 50px;
min-height: 50px;
max-height: 50px;
border-radius: 8px 8px 0 0;
width: 100%;
/* Così come l'immagine: parte alta della card */
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.card-color-preview-m {
height: 100px;
min-height: 100px;
max-height: 100px;
border-radius: 8px 8px 0 0;
width: 100%;
/* Così come l'immagine: parte alta della card */
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.card-color-preview-l {
height: 150px;
min-height: 150px;
max-height: 150px;
border-radius: 8px 8px 0 0;
width: 100%;
/* Così come l'immagine: parte alta della card */
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.card-color-preview-xl {
height: 200px;
min-height: 200px;
max-height: 200px;
border-radius: 8px 8px 0 0;
width: 100%;
/* Così come l'immagine: parte alta della card */
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.optSelected{
border: 3px solid green !important;
}
.timeline{
position:relative;
padding-left: 32px;
}
.timeline-step {
position:relative;
margin-bottom:24px;
min-height:36px;
}
.timeline-step:last-child {
margin-bottom:0;
}
.timeline-flag {
position:absolute;
left:-28px;
top:3px;
width:20px;
height:20px;
border-radius:50%;
background:#e0e0e0;
border:2px solid #bbb;
display:inline-block;
transition:background 0.2s, border 0.2s;
text-align:center;
font-size:14px;
line-height:18px;
}
.timeline-step.completed{
cursor: pointer;
}
.timeline-step.completed .timeline-flag {
background:#28a745;
border-color:#1e7e34;
color:#fff;
}
.timeline-step .timeline-flag:after{
content: '\2713';
opacity:0;
transition:opacity 0.2s;
font-weight:bold;
display:block;
}
.timeline-step.completed .timeline-flag:after{
opacity:1;
}
/*Allargo la dimensione della pagina*/
@media (min-width: 1400px) {
.container, .container-lg, .container-md, .container-sm, .container-xl, .container-xxl {
max-width: 90%;
.footer i[class^="fa-"],
.footer i[class*=" fa-"] {
font-size: 1em;
}
}
/*Mirino*/
/* === Contenitore principale === */
.trim-stage.compact{
position:relative;
width:min(560px, 100%);
margin:0 auto;
height:240px; /* Altezza compatta */
background:#fff;
border:1px solid #e5e7eb;
border-radius:16px;
box-shadow:0 4px 12px rgba(0,0,0,.06);
}
/* === Immagine centrale === */
.trim-image{
position:absolute;
inset:20% 22%; /* margini interni */
max-width:56%;
max-height:60%;
object-fit:contain;
background:#fff;
border:1px dashed #cbd5e1;
border-radius:12px;
}
/* === Controlli (pillole) === */
.trim-ctrl{
position:absolute;
display:flex;
align-items:center;
gap:.5rem;
padding:.25rem .5rem;
background:#ffffffd9;
border:1px solid #e5e7eb;
border-radius:999px;
box-shadow:0 2px 6px rgba(0,0,0,.06);
}
/* Etichette "Sopra/Sotto/Sinistra/Destra" */
.trim-label{
font-size:.75rem;
font-weight:600;
color:#334155;
margin:0;
}
/* Wrapper per select + unità */
.trim-input{
display:flex;
align-items:center;
gap:.25rem;
}
/* Dropdown compatto */
.trim-input .form-control{
min-width: 120px;
}
/* Etichetta "mm" */
.unit-label{
font-size:.75rem;
font-weight:500;
color:#475569;
}
/* === Posizionamenti attorno allimmagine === */
.trim-ctrl-top { top:0; left:50%; transform:translate(-50%,-40%); }
.trim-ctrl-bottom{ bottom:0; left:50%; transform:translate(-50%,40%); }
.trim-ctrl-left { left:0; top:50%; transform:translate(-40%,-50%); }
.trim-ctrl-right { right:0; top:50%; transform:translate(40%,-50%); }
/* === Responsività (mobile) === */
@media (max-width: 576px){
.trim-stage.compact{
height:auto;
padding-bottom: 260px; /* spazio per i controlli sotto */
}
.trim-ctrl{
position:static;
transform:none;
margin:.35rem auto 0;
width:min(420px, 92%);
justify-content:space-between;
}
}
.trim-stage.compact .trim-image{
position:absolute;
top:50%;
left:50%;
transform:translate(-50%, -50%); /* centra in entrambi gli assi */
max-width:56%;
max-height:60%;
width:auto;
height:auto;
object-fit:contain;
background:#fff;
border:1px dashed #cbd5e1;
border-radius:12px;
}
/* (facoltativo) se vuoi leggermente più grande o più piccolo */
@media (min-width: 768px){
.trim-stage.compact .trim-image{
max-width:60%;
max-height:70%;
}
}
.step-no-border{
padding-left: 0 !important;
border-left: 0 !important;
}
.modal-config{
margin-left: 20px !important;
margin-right: 20px !important;
}
@media (min-width: 576px) {
.modal-config{
max-width: 97% !important;
}
}
.reset-flash {
--flash-color: var(--tblr-info, rgb(var(--bs-info-rgb, 58,167,255)));
outline: 2px solid var(--flash-color);
border-radius: .6rem;
will-change: box-shadow, transform, opacity;
animation: resetFlashSmooth 1.6s cubic-bezier(.22,.61,.36,.50) both;
}
@keyframes resetFlashSmooth {
0% {
box-shadow: 0 0 0 .75rem color-mix(in srgb, var(--flash-color) 45%, transparent);
transform: translateZ(0) scale(1.01);
opacity: 1;
}
35% {
box-shadow: 0 0 0 .55rem color-mix(in srgb, var(--flash-color) 32%, transparent);
transform: scale(1.005);
}
70% {
box-shadow: 0 0 0 .25rem color-mix(in srgb, var(--flash-color) 18%, transparent);
transform: scale(1);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--flash-color) 0%, transparent);
opacity: 1;
}
}