commit merge

This commit is contained in:
2026-02-02 15:37:00 +01:00
16 changed files with 394 additions and 131 deletions

View File

@ -0,0 +1,13 @@
using OAService.Service.Servizi.Implementazioni;
using TecniStamp.Domain;
using TecniStamp.Service.Interfaces;
using TecniStamp.Service.Repository;
namespace TecniStamp.Service;
public class FeatureService : TService<Feature>, IFeatureService
{
public FeatureService(ITecniStampUnitOfWork unitOfWork) : base(unitOfWork)
{
}
}

View File

@ -0,0 +1,8 @@
using OAService.Service.Servizi.Interfacce;
using TecniStamp.Domain;
namespace TecniStamp.Service.Interfaces;
public interface IFeatureService : ITService<Feature>
{
}

View File

@ -2,6 +2,7 @@
public interface IManagerService
{
IFeatureService FeatureService { get; set; }
IPermissionService PermissionService { get; set; }
IRuoloService RuoloService{ get; set; }
ISezioneService SezioneService { get; set; }

View File

@ -4,14 +4,16 @@ namespace TecniStamp.Service;
public class ManagerService : IManagerService
{
public ManagerService(IUserService userService, ISezioneService sezioneService, IPermissionService permissionService, IRuoloService ruoloService)
public ManagerService(IUserService userService, ISezioneService sezioneService, IPermissionService permissionService, IRuoloService ruoloService, IFeatureService featureService)
{
UtenteService = userService;
SezioneService = sezioneService;
PermissionService = permissionService;
RuoloService = ruoloService;
FeatureService = featureService;
}
public IFeatureService FeatureService { get; set; }
public IPermissionService PermissionService { get; set; }
public IRuoloService RuoloService { get; set; }
public ISezioneService SezioneService { get; set; }

View File

@ -7,6 +7,7 @@
<base href="/"/>
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>
<link rel="stylesheet" href="app.css"/>
<link rel="stylesheet" href="css/space.css"/>
<link rel="stylesheet" href="TecniStamp.styles.css"/>
<link rel="icon" type="image/png" href="favicon.png"/>

View File

@ -1,10 +1,10 @@
@page "/anagrafiche"
@using Microsoft.EntityFrameworkCore
@using TecniStamp.Components.Widget
@using TecniStamp.Model.Common
@using TecniStamp.Utils
<PageTitle>Anagrafiche</PageTitle>
<Breadcrumb Items="BreadcrumbList" />
<main role="main">

View File

@ -1,23 +1,19 @@
@page "/Anagrafiche/Operatori"
@using Microsoft.EntityFrameworkCore
@using TecniStamp.Domain
@using TecniStamp.Model
@using TecniStamp.Model.Common
@using TecniStamp.Utils
@rendermode InteractiveServer
@inject AuthenticationStateProvider auth
<PageTitle>Operatori</PageTitle>
<Breadcrumb Items="BreadcrumbList" />
<div class="page-wrapper">
<!-- BEGIN PAGE BODY -->
<div class="page-body">
<div class="container-xl">
<main role="main">
<div class="container-fluid h-100 mt-5">
<div class="row justify-content-start">
<div class="row row-cards">
<div class="col">
<!-- Page pre-title -->
<h2 class="page-title">Operatori</h2>
</div>
<div class="col-auto ms-auto">
<div class="btn-list">
<a href="/Anagrafiche/Operatori/Modifica" class="btn btn-primary btn-5 d-none d-sm-inline-block">
@ -51,11 +47,12 @@
</div>
</div>
</div>
</div>
</main>
@code {
List<UserViewModel> utenti;
RadzenDataGrid<UserViewModel> userGrid;
public List<BreadcrumbViewModel> BreadcrumbList { get; set; } = new();
/// <summary>
/// Carica la lista degli utenti non eliminati, ordinandoli per cognome e nome.
@ -69,6 +66,8 @@
includi: x => x.Include(y => y.Ruolo),
ordinamento: x => x.OrderBy(y => y.Cognome).ThenBy(z => z.Nome)))
.Select(x => (UserViewModel)x).ToList();
BreadcrumbList = await BreadcrumbUtils.BuildBreadcrumbByFeature(_managerService, "Operatori_Insert");
}
/// <summary>

View File

@ -2,28 +2,26 @@
@page "/Anagrafiche/Operatori/Modifica/{UserId:guid}"
@using Microsoft.AspNetCore.Identity
@using Microsoft.EntityFrameworkCore
@using TecniStamp.Domain
@using TecniStamp.Model
@using TecniStamp.Utils
@using TecniStamp.Model.Common
@rendermode InteractiveServer
<PageTitle>@pageTitle</PageTitle>
<Breadcrumb Items="BreadcrumbList" />
<div class="page-wrapper">
<!-- BEGIN PAGE HEADER -->
<div class="page-header d-print-none" aria-label="Page header">
<div class="container-xl">
<div class="row g-2 align-items-center">
<main role="main">
<div class="container-fluid h-100 mt-5">
<div class="row justify-content-start">
<div class="row row-cards">
<div class="col">
<h2 class="page-title">@pageTitle</h2>
</div>
</div>
</div>
</div>
<!-- END PAGE HEADER -->
<!-- BEGIN PAGE BODY -->
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-lg-12">
<div class="card">
@ -87,8 +85,7 @@
</div>
</div>
</div>
</div>
</main>
@code {
[Parameter] public Guid? UserId { get; set; }
@ -98,6 +95,7 @@
private string pageTitle => Model?.Id == Guid.Empty ? "Nuovo operatore" : $"Modifica operatore {Model}";
private List<RuoloViewModel> ruoli { get; set; } = new();
public List<BreadcrumbViewModel> BreadcrumbList { get; set; } = new();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
@ -109,6 +107,8 @@
ruoli = (await _managerService.RuoloService.RicercaQueryable(x => x.Eliminato == false))
.Select(x => (RuoloViewModel)x).ToList();
BreadcrumbList = await BreadcrumbUtils.BuildBreadcrumbByFeature(_managerService, "Operatori_Insert", "Modifica", "/Anagrafiche/Operatori");
}
/// <summary>

View File

@ -1,7 +1,5 @@
@page "/Anagrafiche/ruoli/Modifica"
@page "/Anagrafiche/ruoli/Modifica/{RuoloId:guid}"
@using Microsoft.AspNetCore.Identity
@using Microsoft.EntityFrameworkCore
@using TecniStamp.Domain
@using TecniStamp.Model
@using TecniStamp.Utils

View File

@ -1,130 +1,223 @@
@page "/Carico"
@page "/carico"
@using System.Globalization
@using Microsoft.EntityFrameworkCore
@using TecniStamp.Model.Carico
@using TecniStamp.Model.Common
@using TecniStamp.Utils
<RadzenDataGrid Data="@righe" TItem="CalendarioRigaViewModel"
ColumnWidth="120px"
AllowFiltering="false"
AllowSorting="false">
<PageTitle>Carico Macchinari</PageTitle>
<Breadcrumb Items="BreadcrumbList" />
<Columns>
<!-- Colonna fissa -->
<RadzenDataGridColumn TItem="CalendarioRigaViewModel"
Property="Lavorazione"
Title="Lavorazione"
Frozen="true"
Width="100px" />
<main role="main">
<div class="container-fluid h-100 mt-10">
<div class="row justify-content-start">
<div class="calendar-wrapper">
<div class="calendar-scroll">
<table class="calendar-table">
<thead>
<!-- Riga MESI -->
<tr class="month-row">
<th class="sticky-col month-spacer"></th>
<!-- Colonne dinamiche: settimane -->
@foreach (var settimana in settimane)
{
<RadzenDataGridColumn TItem="CalendarioRigaViewModel"
Title="@($"SETTIMANA {settimana.Numero}")"
Width="175px">
<Template Context="riga">
@{
riga.Settimane.TryGetValue(settimana.Numero, out var cella);
}
@foreach (var m in mesiHeader)
{
<th class="month-cell" colspan="@m.ColSpan">
@m.Nome
</th>
}
</tr>
@if (cella != null)
{
<div class="calendar-card">
<div class="card-header">
<span class="status">@cella.Stato</span>
<RadzenButton Icon="more_horiz"
Size="ButtonSize.Small"
ButtonStyle="ButtonStyle.Light" />
</div>
<!-- Riga SETTIMANE -->
<tr class="week-row">
<th class="sticky-col week-spacer">Lavorazione</th>
<div class="card-body">
<small>Commesse: @cella.Stato</small>
</div>
@foreach (var s in settimane)
{
<th class="week-cell">
SETTIMANA @s.Numero
</th>
}
</tr>
</thead>
<RadzenProgressBar Value="@cella.Percentuale"
ShowValue="false"
Unit="%"
Style="height:12px"
Color="@GetColor(cella.Percentuale)" />
<tbody>
@foreach (var riga in righe)
{
<tr>
<td class="sticky-col row-title">
@riga.Lavorazione
</td>
</div>
}
</Template>
</RadzenDataGridColumn>
}
@foreach (var s in settimane)
{
riga.Settimane.TryGetValue(s.Numero, out var cella);
</Columns>
</RadzenDataGrid>
<td class="data-cell">
@if (cella is not null)
{
<div class="calendar-card">
<div class="card-top">
<span class="status">@cella.Stato</span>
<div class="card-actions">
<RadzenButton Text="Vedi dettaglio"
Size="ButtonSize.Small"
ButtonStyle="ButtonStyle.Light"
Click="@(() => ApriDettaglio(riga, s, cella))"/>
<RadzenButton Icon="more_horiz"
Size="ButtonSize.Small"
ButtonStyle="ButtonStyle.Light"/>
</div>
</div>
@code {
<div class="card-body">
<div class="sub">Commesse: @cella.Commesse</div>
<div class="sub small">@cella.Ore/@cella.Capacita h</div>
</div>
<RadzenProgressBar Value="@cella.Percentuale"
ShowValue="true"
Unit="%"
ProgressBarStyle="@GetProgressStyle(cella.Percentuale)"
Style="height:8px"/>
</div>
}
</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
@code
{
// ====== DATA ======
private List<SettimanaViewModel> settimane = new();
private List<MeseHeaderViewModel> mesiHeader = new();
private List<CalendarioRigaViewModel> righe = new();
protected override async Task OnInitializedAsync()
public List<BreadcrumbViewModel> BreadcrumbList { get; set; } = new();
protected override void OnInitialized()
{
await base.OnInitializedAsync();
settimane = GeneraSettimaneAnno(2026);
var settimanePerMese = settimane
.GroupBy(s => new { s.Anno, Mese = s.Inizio.Month })
.Select(g => new
settimane = GeneraSettimaneDaPrimoGennaio(2026);
// Header mesi: raggruppa le settimane per mese (in base alla data di inizio settimana)
mesiHeader = settimane
.GroupBy(s => s.Inizio.Month)
.Select(g => new MeseHeaderViewModel
{
Mese = g.Key.Mese,
Nome = new DateTime(2026, g.Key.Mese, 1).ToString("MMMM").ToUpper(),
Count = g.Count()
Mese = g.Key,
Nome = new DateTime(2026, g.Key, 1).ToString("MMMM", CultureInfo.GetCultureInfo("it-IT")).ToUpperInvariant(),
ColSpan = g.Count()
})
.ToList();
// Mock righe/celle
righe = new List<CalendarioRigaViewModel>
{
new()
{
Id = 1,
Lavorazione = "Taglio laser Commessa A",
Settimane = new Dictionary<int, CalendarioCellaViewModel>
Lavorazione = "TAGLIO",
Settimane =
{
[6] = new() { Ore = 16, Stato = "InCorso", Percentuale = 50 },
[7] = new() { Ore = 24, Stato = "InCorso", Percentuale = 75 }
[3] = new() { Ore = 90, Capacita = 100, Stato = "Estremamente Saturo", Commesse = "24-24, 25-25, ..." },
[4] = new() { Ore = 100, Capacita = 100, Stato = "Saturo", Commesse = "16-24, 28-24, ..." }
}
},
new()
{
Id = 2,
Lavorazione = "Saldatura Commessa A",
Settimane = new Dictionary<int, CalendarioCellaViewModel>
Lavorazione = "TORNITURA",
Settimane =
{
[7] = new() { Ore = 32, Stato = "Pianificato", Percentuale = 40},
[8] = new() { Ore = 16, Stato = "Pianificato", Percentuale = 60}
[1] = new() { Ore = 100, Capacita = 100, Stato = "Saturo", Commesse = "15-24, 25-24, ..." },
[5] = new() { Ore = 60, Capacita = 100, Stato = "Non Saturo", Commesse = "35-25, 65-25, ..." }
}
},
new()
{
Id = 3,
Lavorazione = "Verniciatura Commessa B",
Settimane = new Dictionary<int, CalendarioCellaViewModel>
Lavorazione = "FRESATURA",
Settimane =
{
[8] = new() { Ore = 40, Stato = "Pianificato", Percentuale = 90}
[2] = new() { Ore = 40, Capacita = 100, Stato = "Non Saturo", Commesse = "12-11, 44-11, ..." },
[3] = new() { Ore = 100, Capacita = 100, Stato = "Saturo", Commesse = "17-22, 19-22, ..." }
}
}
};
}
public static List<SettimanaViewModel> GeneraSettimaneAnno(int anno)
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbList = await BreadcrumbUtils.BuildBreadcrumbByFeature(_managerService, "Carico_Info");
}
private void ApriDettaglio(CalendarioRigaViewModel riga, SettimanaViewModel settimana, CalendarioCellaViewModel cella)
{
// qui ci attacchi un RadzenDialogService se vuoi
// es: DialogService.Open(...)
Console.WriteLine($"Dettaglio: {riga.Lavorazione} - W{settimana.Numero} - {cella.Ore}/{cella.Capacita}");
}
private static ProgressBarStyle GetProgressStyle(int percentuale)
{
// taralo come vuoi
if (percentuale >= 100) return ProgressBarStyle.Success;
if (percentuale >= 80) return ProgressBarStyle.Warning;
return ProgressBarStyle.Info;
}
// ====== ISO WEEKS (corretto) ======
private static List<SettimanaViewModel> GeneraSettimaneISO(int anno)
{
var list = new List<SettimanaViewModel>();
// ISOWeek è disponibile su .NET 6+ (quindi ok per Blazor moderno)
int weeksInYear = ISOWeek.GetWeeksInYear(anno);
for (int w = 1; w <= weeksInYear; w++)
{
// Lunedì della settimana ISO
DateTime start = ISOWeek.ToDateTime(anno, w, DayOfWeek.Monday);
DateTime end = start.AddDays(6);
list.Add(new SettimanaViewModel
{
Anno = anno,
Numero = w,
Inizio = start,
Fine = end
});
}
return list;
}
private static List<SettimanaViewModel> GeneraSettimaneDaPrimoGennaio(int anno)
{
var settimane = new List<SettimanaViewModel>();
// ISO: settimana 1 = quella che contiene il 4 gennaio
var jan4 = new DateTime(anno, 1, 4);
var start = new DateTime(anno, 1, 1);
var endOfYear = new DateTime(anno, 12, 31);
// lunedì della settimana 1
var startOfWeek1 = jan4.AddDays(-(int)(jan4.DayOfWeek == DayOfWeek.Sunday
? 6
: jan4.DayOfWeek - DayOfWeek.Monday));
int weekNumber = 1;
var currentStart = start;
var currentStart = startOfWeek1;
var weekNumber = 1;
while (currentStart.Year <= anno)
while (currentStart <= endOfYear)
{
var currentEnd = currentStart.AddDays(6);
if (currentEnd > endOfYear)
currentEnd = endOfYear;
settimane.Add(new SettimanaViewModel
{
@ -140,9 +233,4 @@
return settimane;
}
object GetColor(int valuePercentuale)
{
return "Red";
}
}

View File

@ -1,35 +1,118 @@
.calendar-month-header {
display: grid;
grid-template-columns: 250px repeat(53, 120px);
border-bottom: 1px solid #ddd;
.calendar-wrapper {
border: 1px solid #ddd;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.calendar-scroll {
overflow-x: auto;
overflow-y: auto;
max-height: 70vh;
}
.calendar-table {
border-collapse: collapse;
table-layout: fixed;
width: max-content; /* importantissimo per far lavorare lo scroll orizzontale */
min-width: 100%;
}
/* larghezze: devono combaciare con il tuo mock */
.calendar-table th,
.calendar-table td {
border: 1px solid #e3e3e3;
width: 120px;
min-width: 120px;
padding: 8px;
vertical-align: top;
background: #fff;
}
/* Colonna sticky (lavorazioni) */
.sticky-col {
position: sticky;
left: 0;
z-index: 3;
width: 250px !important;
min-width: 250px !important;
background: #fff;
}
/* header sticky in alto */
thead th {
position: sticky;
top: 0;
z-index: 4;
background: #f5f5f5;
font-weight: 600;
}
/* incrocio sticky (top + left) */
thead .sticky-col {
z-index: 6;
background: #f5f5f5;
}
.month-header {
.month-row th {
text-align: center;
font-weight: 600;
padding: 8px 0;
border-left: 1px solid #ddd;
font-size: 12px;
letter-spacing: .5px;
}
.month-spacer {
border-right: 1px solid #ddd;
.week-row th {
text-align: center;
font-size: 12px;
font-weight: 500;
color: #666;
}
.row-title {
font-weight: 600;
color: #666;
text-transform: uppercase;
padding-top: 18px;
}
.data-cell {
background: #fff;
}
.calendar-card {
background: #f7f7f7;
border-radius: 6px;
padding: 6px;
box-shadow: inset 0 0 0 1px #ddd;
padding: 8px;
box-shadow: inset 0 0 0 1px #dedede;
}
.card-header {
.card-top {
display: flex;
align-items: start;
justify-content: space-between;
font-size: 12px;
margin-bottom: 4px;
gap: 8px;
margin-bottom: 8px;
}
.status {
font-size: 12px;
font-weight: 600;
color: #444;
line-height: 1.1;
}
.card-actions {
display: flex;
gap: 6px;
align-items: center;
}
.card-body .sub {
font-size: 11px;
color: #666;
margin-bottom: 6px;
}
.card-body .small {
font-size: 10px;
color: #888;
}

View File

@ -0,0 +1,18 @@
@page "/commesse"
@using Microsoft.EntityFrameworkCore
@using TecniStamp.Model.Common
@using TecniStamp.Utils
<PageTitle>Commesse</PageTitle>
<Breadcrumb Items="BreadcrumbList" />
@code {
public List<BreadcrumbViewModel> BreadcrumbList { get; set; } = new();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbList = await BreadcrumbUtils.BuildBreadcrumbByFeature(_managerService, "Commesse_Info");
}
}

View File

@ -3,10 +3,10 @@
@using Microsoft.AspNetCore.Authorization
@using Microsoft.EntityFrameworkCore
@using TecniStamp.Components.Widget
@using TecniStamp.Model.Common
@using TecniStamp.Utils
<PageTitle>Home</PageTitle>
<Breadcrumb Items="BreadcrumbList" />
<main role="main">

View File

@ -9,6 +9,7 @@
@using Microsoft.JSInterop
@using TecniStamp
@using TecniStamp.Components
@using TecniStamp.Components.Widget
@using Radzen
@using Radzen.Blazor

View File

@ -3,6 +3,22 @@
public class CalendarioCellaViewModel
{
public int Ore { get; set; }
public int Capacita { get; set; } = 100;
public string Stato { get; set; }
public string Commesse { get; set; } = "";
public int Percentuale { get; set; }
}
public class MeseHeaderViewModel
{
public int Mese { get; set; }
public string Nome { get; set; } = "";
public int ColSpan { get; set; }
}
public class CalendarioHeaderViewModel
{
public int Mese { get; set; }
public string Nome { get; set; }
public int Count { get; set; }
}

View File

@ -1,11 +1,25 @@
using TecniStamp.Domain;
using Microsoft.EntityFrameworkCore;
using TecniStamp.Domain;
using TecniStamp.Model.Common;
using TecniStamp.Service.Interfaces;
namespace TecniStamp.Utils;
public static class BreadcrumbUtils
{
public static List<BreadcrumbViewModel> BuildBreadcrumb(Sezione sezione)
public static async Task<List<BreadcrumbViewModel>> BuildBreadcrumbByFeature(IManagerService _managerService, string featureName,
string? extraText = null,
string? parentUrlOverride = null)
{
var section = await _managerService.FeatureService.RicercaPer(x => x.Nome == featureName,
includi:x => x.Include(y => y.Sezione).ThenInclude(x => x.Parent));
return BuildBreadcrumb(section.Sezione, extraText, parentUrlOverride);
}
public static List<BreadcrumbViewModel> BuildBreadcrumb(
Sezione sezione,
string? extraText = null,
string? parentUrlOverride = null)
{
var stack = new Stack<Sezione>();
var current = sezione;
@ -37,9 +51,30 @@ public static class BreadcrumbUtils
});
}
breadcrumb.Last().IsActive = true;
breadcrumb.Last().Url = null;
if (!string.IsNullOrWhiteSpace(extraText))
{
// 🔹 rendo cliccabile il penultimo (la sezione)
var parent = breadcrumb.Last();
parent.IsActive = false;
parent.Url = parentUrlOverride;
// 🔹 aggiungo la voce finale custom
breadcrumb.Add(new BreadcrumbViewModel
{
Text = extraText,
Url = null,
IsActive = true
});
}
else
{
// comportamento originale (index)
var last = breadcrumb.Last();
last.IsActive = true;
last.Url = null;
}
return breadcrumb;
}
}