implementato modulo HR
This commit is contained in:
@@ -20,3 +20,5 @@ File riassuntivo dello stato di sviluppo di Zentral.
|
||||
- Implementazione modulo Gestione Eventi: strutturazione frontend, integrazione funzionalità e attivazione store.
|
||||
- [Event Module Development](./devlog/event-module.md) - Implementazione modulo eventi
|
||||
- [Menu Refactoring](./devlog/menu-refactoring.md) - Riorganizzazione menu e moduli (Dashboard, Clienti, Articoli, Risorse)
|
||||
- [2025-12-03 Implementazione Modulo Personale](./devlog/2025-12-03_implementazione_modulo_personale.md) - **In Corso**
|
||||
- Implementazione entità, API e Frontend per gestione Personale (Dipendenti, Contratti, Assenze, Pagamenti).
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Implementazione Modulo Personale
|
||||
|
||||
## Obiettivo
|
||||
Implementare il modulo "Personale" per la gestione delle risorse umane, come richiesto nelle specifiche di posizionamento di mercato.
|
||||
|
||||
## Funzionalità Richieste
|
||||
- **Gestione Personale (Dipendenti)**: Anagrafica dipendenti.
|
||||
- **Contratti**: Gestione dei contratti di lavoro (tipo, date, livello, retribuzione).
|
||||
- **Assenze**: Tracciamento ferie, malattie, permessi.
|
||||
- **Pagamenti**: Registro dei pagamenti stipendi.
|
||||
- **Rimborsi**: Gestione note spese e rimborsi.
|
||||
- **Analisi**: Dashboard statistiche (da implementare successivamente).
|
||||
|
||||
## Piano di Lavoro
|
||||
|
||||
### Backend (.NET)
|
||||
1. [ ] Creare cartella `src/backend/Zentral.Domain/Entities/Personale`.
|
||||
2. [ ] Definire le entità:
|
||||
* `Dipendente`
|
||||
* `Contratto`
|
||||
* `Assenza`
|
||||
* `Pagamento`
|
||||
* `Rimborso`
|
||||
3. [ ] Aggiornare `ZentralDbContext` aggiungendo i `DbSet`.
|
||||
4. [ ] Creare la migrazione EF Core.
|
||||
5. [ ] Creare i Controller API in `src/backend/Zentral.API/Controllers/Personale`.
|
||||
|
||||
### Frontend (React)
|
||||
1. [ ] Strutturare `src/frontend/src/modules/personale`.
|
||||
2. [ ] Implementare le pagine CRUD:
|
||||
* `DipendentiPage`
|
||||
* `ContrattiPage`
|
||||
* `AssenzePage`
|
||||
* `PagamentiPage` (include Rimborsi per ora o separato).
|
||||
3. [ ] Configurare il routing del modulo.
|
||||
4. [ ] Aggiungere il modulo alla configurazione `AppModule` (se non presente) e verificare l'attivazione.
|
||||
|
||||
### Integrazione
|
||||
1. [ ] Verificare che il modulo appaia nel menu solo se attivo.
|
||||
2. [ ] Testare il flusso completo (creazione dipendente -> contratto -> assenza).
|
||||
@@ -0,0 +1,94 @@
|
||||
using Zentral.Domain.Entities.HR;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.API.Modules.HR.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/hr/[controller]")]
|
||||
public class AssenzeController : ControllerBase
|
||||
{
|
||||
private readonly ZentralDbContext _context;
|
||||
|
||||
public AssenzeController(ZentralDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Assenza>>> GetAssenze([FromQuery] int? dipendenteId, [FromQuery] DateTime? from, [FromQuery] DateTime? to)
|
||||
{
|
||||
var query = _context.Assenze.Include(a => a.Dipendente).AsQueryable();
|
||||
|
||||
if (dipendenteId.HasValue)
|
||||
query = query.Where(a => a.DipendenteId == dipendenteId.Value);
|
||||
|
||||
if (from.HasValue)
|
||||
query = query.Where(a => a.DataInizio >= from.Value);
|
||||
|
||||
if (to.HasValue)
|
||||
query = query.Where(a => a.DataFine <= to.Value);
|
||||
|
||||
return await query.OrderByDescending(a => a.DataInizio).ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Assenza>> GetAssenza(int id)
|
||||
{
|
||||
var assenza = await _context.Assenze
|
||||
.Include(a => a.Dipendente)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
if (assenza == null)
|
||||
return NotFound();
|
||||
|
||||
return assenza;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Assenza>> CreateAssenza(Assenza assenza)
|
||||
{
|
||||
assenza.CreatedAt = DateTime.UtcNow;
|
||||
_context.Assenze.Add(assenza);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetAssenza), new { id = assenza.Id }, assenza);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateAssenza(int id, Assenza assenza)
|
||||
{
|
||||
if (id != assenza.Id)
|
||||
return BadRequest();
|
||||
|
||||
assenza.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Entry(assenza).State = EntityState.Modified;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!await _context.Assenze.AnyAsync(a => a.Id == id))
|
||||
return NotFound();
|
||||
throw;
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteAssenza(int id)
|
||||
{
|
||||
var assenza = await _context.Assenze.FindAsync(id);
|
||||
if (assenza == null)
|
||||
return NotFound();
|
||||
|
||||
_context.Assenze.Remove(assenza);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Zentral.Domain.Entities.HR;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.API.Modules.HR.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/hr/[controller]")]
|
||||
public class ContrattiController : ControllerBase
|
||||
{
|
||||
private readonly ZentralDbContext _context;
|
||||
|
||||
public ContrattiController(ZentralDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Contratto>>> GetContratti([FromQuery] int? dipendenteId)
|
||||
{
|
||||
var query = _context.Contratti.Include(c => c.Dipendente).AsQueryable();
|
||||
|
||||
if (dipendenteId.HasValue)
|
||||
query = query.Where(c => c.DipendenteId == dipendenteId.Value);
|
||||
|
||||
return await query.OrderByDescending(c => c.DataInizio).ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Contratto>> GetContratto(int id)
|
||||
{
|
||||
var contratto = await _context.Contratti
|
||||
.Include(c => c.Dipendente)
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
if (contratto == null)
|
||||
return NotFound();
|
||||
|
||||
return contratto;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Contratto>> CreateContratto(Contratto contratto)
|
||||
{
|
||||
contratto.CreatedAt = DateTime.UtcNow;
|
||||
_context.Contratti.Add(contratto);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetContratto), new { id = contratto.Id }, contratto);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateContratto(int id, Contratto contratto)
|
||||
{
|
||||
if (id != contratto.Id)
|
||||
return BadRequest();
|
||||
|
||||
contratto.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Entry(contratto).State = EntityState.Modified;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!await _context.Contratti.AnyAsync(c => c.Id == id))
|
||||
return NotFound();
|
||||
throw;
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteContratto(int id)
|
||||
{
|
||||
var contratto = await _context.Contratti.FindAsync(id);
|
||||
if (contratto == null)
|
||||
return NotFound();
|
||||
|
||||
_context.Contratti.Remove(contratto);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Zentral.Domain.Entities.HR;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.API.Modules.HR.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/hr/[controller]")]
|
||||
public class DipendentiController : ControllerBase
|
||||
{
|
||||
private readonly ZentralDbContext _context;
|
||||
|
||||
public DipendentiController(ZentralDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Dipendente>>> GetDipendenti([FromQuery] string? search)
|
||||
{
|
||||
var query = _context.Dipendenti.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
query = query.Where(d => d.Nome.Contains(search) ||
|
||||
d.Cognome.Contains(search) ||
|
||||
(d.CodiceFiscale != null && d.CodiceFiscale.Contains(search)));
|
||||
|
||||
return await query.OrderBy(d => d.Cognome).ThenBy(d => d.Nome).ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Dipendente>> GetDipendente(int id)
|
||||
{
|
||||
var dipendente = await _context.Dipendenti
|
||||
.Include(d => d.Contratti)
|
||||
.Include(d => d.Assenze)
|
||||
.Include(d => d.Pagamenti)
|
||||
.Include(d => d.Rimborsi)
|
||||
.FirstOrDefaultAsync(d => d.Id == id);
|
||||
|
||||
if (dipendente == null)
|
||||
return NotFound();
|
||||
|
||||
return dipendente;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Dipendente>> CreateDipendente(Dipendente dipendente)
|
||||
{
|
||||
dipendente.CreatedAt = DateTime.UtcNow;
|
||||
_context.Dipendenti.Add(dipendente);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetDipendente), new { id = dipendente.Id }, dipendente);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateDipendente(int id, Dipendente dipendente)
|
||||
{
|
||||
if (id != dipendente.Id)
|
||||
return BadRequest();
|
||||
|
||||
dipendente.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Entry(dipendente).State = EntityState.Modified;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!await _context.Dipendenti.AnyAsync(d => d.Id == id))
|
||||
return NotFound();
|
||||
throw;
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteDipendente(int id)
|
||||
{
|
||||
var dipendente = await _context.Dipendenti.FindAsync(id);
|
||||
if (dipendente == null)
|
||||
return NotFound();
|
||||
|
||||
_context.Dipendenti.Remove(dipendente);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Zentral.Domain.Entities.HR;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.API.Modules.HR.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/hr/[controller]")]
|
||||
public class PagamentiController : ControllerBase
|
||||
{
|
||||
private readonly ZentralDbContext _context;
|
||||
|
||||
public PagamentiController(ZentralDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Pagamento>>> GetPagamenti([FromQuery] int? dipendenteId, [FromQuery] DateTime? from, [FromQuery] DateTime? to)
|
||||
{
|
||||
var query = _context.Pagamenti.Include(p => p.Dipendente).AsQueryable();
|
||||
|
||||
if (dipendenteId.HasValue)
|
||||
query = query.Where(p => p.DipendenteId == dipendenteId.Value);
|
||||
|
||||
if (from.HasValue)
|
||||
query = query.Where(p => p.DataPagamento >= from.Value);
|
||||
|
||||
if (to.HasValue)
|
||||
query = query.Where(p => p.DataPagamento <= to.Value);
|
||||
|
||||
return await query.OrderByDescending(p => p.DataPagamento).ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Pagamento>> GetPagamento(int id)
|
||||
{
|
||||
var pagamento = await _context.Pagamenti
|
||||
.Include(p => p.Dipendente)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (pagamento == null)
|
||||
return NotFound();
|
||||
|
||||
return pagamento;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Pagamento>> CreatePagamento(Pagamento pagamento)
|
||||
{
|
||||
pagamento.CreatedAt = DateTime.UtcNow;
|
||||
_context.Pagamenti.Add(pagamento);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetPagamento), new { id = pagamento.Id }, pagamento);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdatePagamento(int id, Pagamento pagamento)
|
||||
{
|
||||
if (id != pagamento.Id)
|
||||
return BadRequest();
|
||||
|
||||
pagamento.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Entry(pagamento).State = EntityState.Modified;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!await _context.Pagamenti.AnyAsync(p => p.Id == id))
|
||||
return NotFound();
|
||||
throw;
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeletePagamento(int id)
|
||||
{
|
||||
var pagamento = await _context.Pagamenti.FindAsync(id);
|
||||
if (pagamento == null)
|
||||
return NotFound();
|
||||
|
||||
_context.Pagamenti.Remove(pagamento);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Zentral.Domain.Entities.HR;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.API.Modules.HR.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/hr/[controller]")]
|
||||
public class RimborsiController : ControllerBase
|
||||
{
|
||||
private readonly ZentralDbContext _context;
|
||||
|
||||
public RimborsiController(ZentralDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Rimborso>>> GetRimborsi([FromQuery] int? dipendenteId, [FromQuery] string? stato)
|
||||
{
|
||||
var query = _context.Rimborsi.Include(r => r.Dipendente).AsQueryable();
|
||||
|
||||
if (dipendenteId.HasValue)
|
||||
query = query.Where(r => r.DipendenteId == dipendenteId.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(stato))
|
||||
query = query.Where(r => r.Stato == stato);
|
||||
|
||||
return await query.OrderByDescending(r => r.DataSpesa).ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Rimborso>> GetRimborso(int id)
|
||||
{
|
||||
var rimborso = await _context.Rimborsi
|
||||
.Include(r => r.Dipendente)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (rimborso == null)
|
||||
return NotFound();
|
||||
|
||||
return rimborso;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Rimborso>> CreateRimborso(Rimborso rimborso)
|
||||
{
|
||||
rimborso.CreatedAt = DateTime.UtcNow;
|
||||
_context.Rimborsi.Add(rimborso);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetRimborso), new { id = rimborso.Id }, rimborso);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateRimborso(int id, Rimborso rimborso)
|
||||
{
|
||||
if (id != rimborso.Id)
|
||||
return BadRequest();
|
||||
|
||||
rimborso.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Entry(rimborso).State = EntityState.Modified;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!await _context.Rimborsi.AnyAsync(r => r.Id == id))
|
||||
return NotFound();
|
||||
throw;
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteRimborso(int id)
|
||||
{
|
||||
var rimborso = await _context.Rimborsi.FindAsync(id);
|
||||
if (rimborso == null)
|
||||
return NotFound();
|
||||
|
||||
_context.Rimborsi.Remove(rimborso);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -405,9 +405,6 @@ public class ModuleService
|
||||
/// </summary>
|
||||
public async Task SeedDefaultModulesAsync()
|
||||
{
|
||||
if (await _context.AppModules.AnyAsync())
|
||||
return;
|
||||
|
||||
var defaultModules = new List<AppModule>
|
||||
{
|
||||
new AppModule
|
||||
@@ -482,12 +479,46 @@ public class ModuleService
|
||||
RoutePath = "/quality",
|
||||
IsAvailable = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
},
|
||||
new AppModule
|
||||
{
|
||||
Code = "events",
|
||||
Name = "Gestione Eventi",
|
||||
Description = "Gestione eventi, pianificazione e controllo avanzamento",
|
||||
Icon = "Event",
|
||||
BasePrice = 2000m,
|
||||
MonthlyMultiplier = 1.2m,
|
||||
SortOrder = 60,
|
||||
IsCore = false,
|
||||
RoutePath = "/events",
|
||||
IsAvailable = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
},
|
||||
new AppModule
|
||||
{
|
||||
Code = "hr",
|
||||
Name = "Gestione Personale",
|
||||
Description = "Gestione personale, contratti, pagamenti, assenze, rimborsi e analisi personale",
|
||||
Icon = "People",
|
||||
BasePrice = 1600m,
|
||||
MonthlyMultiplier = 1.2m,
|
||||
SortOrder = 70,
|
||||
IsCore = false,
|
||||
RoutePath = "/hr",
|
||||
IsAvailable = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
_context.AppModules.AddRange(defaultModules);
|
||||
await _context.SaveChangesAsync();
|
||||
var existingCodes = await _context.AppModules.Select(m => m.Code).ToListAsync();
|
||||
var newModules = defaultModules.Where(m => !existingCodes.Contains(m.Code)).ToList();
|
||||
|
||||
_logger.LogInformation("Seed {Count} moduli di default completato", defaultModules.Count);
|
||||
if (newModules.Any())
|
||||
{
|
||||
_context.AppModules.AddRange(newModules);
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInformation("Added {Count} new default modules: {Modules}",
|
||||
newModules.Count, string.Join(", ", newModules.Select(m => m.Code)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/backend/Zentral.Domain/Entities/HR/Assenza.cs
Normal file
15
src/backend/Zentral.Domain/Entities/HR/Assenza.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Zentral.Domain.Entities.HR;
|
||||
|
||||
public class Assenza : BaseEntity
|
||||
{
|
||||
public int DipendenteId { get; set; }
|
||||
public Dipendente Dipendente { get; set; } = null!;
|
||||
|
||||
public DateTime DataInizio { get; set; }
|
||||
public DateTime DataFine { get; set; }
|
||||
public string TipoAssenza { get; set; } = string.Empty;
|
||||
public string Stato { get; set; } = "Richiesta";
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
17
src/backend/Zentral.Domain/Entities/HR/Contratto.cs
Normal file
17
src/backend/Zentral.Domain/Entities/HR/Contratto.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Zentral.Domain.Entities.HR;
|
||||
|
||||
public class Contratto : BaseEntity
|
||||
{
|
||||
public int DipendenteId { get; set; }
|
||||
public Dipendente Dipendente { get; set; } = null!;
|
||||
|
||||
public DateTime DataInizio { get; set; }
|
||||
public DateTime? DataFine { get; set; }
|
||||
public string TipoContratto { get; set; } = string.Empty;
|
||||
public string? Livello { get; set; }
|
||||
public decimal RetribuzioneLorda { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool Attivo { get; set; } = true;
|
||||
}
|
||||
21
src/backend/Zentral.Domain/Entities/HR/Dipendente.cs
Normal file
21
src/backend/Zentral.Domain/Entities/HR/Dipendente.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Zentral.Domain.Entities.HR;
|
||||
|
||||
public class Dipendente : BaseEntity
|
||||
{
|
||||
public string Nome { get; set; } = string.Empty;
|
||||
public string Cognome { get; set; } = string.Empty;
|
||||
public string? CodiceFiscale { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Telefono { get; set; }
|
||||
public string? Indirizzo { get; set; }
|
||||
public DateTime? DataNascita { get; set; }
|
||||
public string? Ruolo { get; set; }
|
||||
|
||||
public List<Contratto> Contratti { get; set; } = new();
|
||||
public List<Assenza> Assenze { get; set; } = new();
|
||||
public List<Pagamento> Pagamenti { get; set; } = new();
|
||||
public List<Rimborso> Rimborsi { get; set; } = new();
|
||||
}
|
||||
14
src/backend/Zentral.Domain/Entities/HR/Pagamento.cs
Normal file
14
src/backend/Zentral.Domain/Entities/HR/Pagamento.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Zentral.Domain.Entities.HR;
|
||||
|
||||
public class Pagamento : BaseEntity
|
||||
{
|
||||
public int DipendenteId { get; set; }
|
||||
public Dipendente Dipendente { get; set; } = null!;
|
||||
|
||||
public DateTime DataPagamento { get; set; }
|
||||
public decimal ImportoNetto { get; set; }
|
||||
public string Descrizione { get; set; } = string.Empty;
|
||||
public bool Pagato { get; set; } = false;
|
||||
}
|
||||
14
src/backend/Zentral.Domain/Entities/HR/Rimborso.cs
Normal file
14
src/backend/Zentral.Domain/Entities/HR/Rimborso.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Zentral.Domain.Entities.HR;
|
||||
|
||||
public class Rimborso : BaseEntity
|
||||
{
|
||||
public int DipendenteId { get; set; }
|
||||
public Dipendente Dipendente { get; set; } = null!;
|
||||
|
||||
public DateTime DataSpesa { get; set; }
|
||||
public decimal Importo { get; set; }
|
||||
public string Descrizione { get; set; } = string.Empty;
|
||||
public string Stato { get; set; } = "Richiesto";
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Zentral.Domain.Entities.Warehouse;
|
||||
using Zentral.Domain.Entities.Purchases;
|
||||
using Zentral.Domain.Entities.Sales;
|
||||
using Zentral.Domain.Entities.Production;
|
||||
using Zentral.Domain.Entities.HR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.Infrastructure.Data;
|
||||
@@ -86,10 +87,70 @@ public class ZentralDbContext : DbContext
|
||||
public DbSet<ProductionOrderPhase> ProductionOrderPhases => Set<ProductionOrderPhase>();
|
||||
public DbSet<MrpSuggestion> MrpSuggestions => Set<MrpSuggestion>();
|
||||
|
||||
// Personale module entities
|
||||
public DbSet<Dipendente> Dipendenti => Set<Dipendente>();
|
||||
public DbSet<Contratto> Contratti => Set<Contratto>();
|
||||
public DbSet<Assenza> Assenze => Set<Assenza>();
|
||||
public DbSet<Pagamento> Pagamenti => Set<Pagamento>();
|
||||
public DbSet<Rimborso> Rimborsi => Set<Rimborso>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// ===============================================
|
||||
// PERSONALE MODULE ENTITIES
|
||||
// ===============================================
|
||||
|
||||
modelBuilder.Entity<Dipendente>(entity =>
|
||||
{
|
||||
entity.ToTable("Dipendenti");
|
||||
entity.HasIndex(e => e.CodiceFiscale).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Contratto>(entity =>
|
||||
{
|
||||
entity.ToTable("Contratti");
|
||||
entity.Property(e => e.RetribuzioneLorda).HasPrecision(18, 2);
|
||||
|
||||
entity.HasOne(e => e.Dipendente)
|
||||
.WithMany(d => d.Contratti)
|
||||
.HasForeignKey(e => e.DipendenteId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Assenza>(entity =>
|
||||
{
|
||||
entity.ToTable("Assenze");
|
||||
|
||||
entity.HasOne(e => e.Dipendente)
|
||||
.WithMany(d => d.Assenze)
|
||||
.HasForeignKey(e => e.DipendenteId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Pagamento>(entity =>
|
||||
{
|
||||
entity.ToTable("Pagamenti");
|
||||
entity.Property(e => e.ImportoNetto).HasPrecision(18, 2);
|
||||
|
||||
entity.HasOne(e => e.Dipendente)
|
||||
.WithMany(d => d.Pagamenti)
|
||||
.HasForeignKey(e => e.DipendenteId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Rimborso>(entity =>
|
||||
{
|
||||
entity.ToTable("Rimborsi");
|
||||
entity.Property(e => e.Importo).HasPrecision(18, 2);
|
||||
|
||||
entity.HasOne(e => e.Dipendente)
|
||||
.WithMany(d => d.Rimborsi)
|
||||
.HasForeignKey(e => e.DipendenteId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// Cliente
|
||||
modelBuilder.Entity<Cliente>(entity =>
|
||||
{
|
||||
|
||||
4657
src/backend/Zentral.Infrastructure/Migrations/20251203190021_AddPersonaleModule.Designer.cs
generated
Normal file
4657
src/backend/Zentral.Infrastructure/Migrations/20251203190021_AddPersonaleModule.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Zentral.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPersonaleModule : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Dipendenti",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Nome = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Cognome = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CodiceFiscale = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Email = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Telefono = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Indirizzo = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DataNascita = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Ruolo = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Dipendenti", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Assenze",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DipendenteId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataInizio = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
DataFine = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
TipoAssenza = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Stato = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Note = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Assenze", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Assenze_Dipendenti_DipendenteId",
|
||||
column: x => x.DipendenteId,
|
||||
principalTable: "Dipendenti",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contratti",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DipendenteId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataInizio = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
DataFine = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
TipoContratto = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Livello = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RetribuzioneLorda = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
|
||||
Note = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Attivo = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contratti", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Contratti_Dipendenti_DipendenteId",
|
||||
column: x => x.DipendenteId,
|
||||
principalTable: "Dipendenti",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Pagamenti",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DipendenteId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataPagamento = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
ImportoNetto = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
|
||||
Descrizione = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Pagato = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Pagamenti", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Pagamenti_Dipendenti_DipendenteId",
|
||||
column: x => x.DipendenteId,
|
||||
principalTable: "Dipendenti",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Rimborsi",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DipendenteId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataSpesa = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Importo = table.Column<decimal>(type: "TEXT", precision: 18, scale: 2, nullable: false),
|
||||
Descrizione = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Stato = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Rimborsi", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Rimborsi_Dipendenti_DipendenteId",
|
||||
column: x => x.DipendenteId,
|
||||
principalTable: "Dipendenti",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Assenze_DipendenteId",
|
||||
table: "Assenze",
|
||||
column: "DipendenteId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contratti_DipendenteId",
|
||||
table: "Contratti",
|
||||
column: "DipendenteId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Dipendenti_CodiceFiscale",
|
||||
table: "Dipendenti",
|
||||
column: "CodiceFiscale",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Pagamenti_DipendenteId",
|
||||
table: "Pagamenti",
|
||||
column: "DipendenteId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Rimborsi_DipendenteId",
|
||||
table: "Rimborsi",
|
||||
column: "DipendenteId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Assenze");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contratti");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Pagamenti");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Rimborsi");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Dipendenti");
|
||||
}
|
||||
}
|
||||
}
|
||||
4657
src/backend/Zentral.Infrastructure/Migrations/20251203190418_RenamePersonaleToHR.Designer.cs
generated
Normal file
4657
src/backend/Zentral.Infrastructure/Migrations/20251203190418_RenamePersonaleToHR.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Zentral.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenamePersonaleToHR : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -940,6 +940,254 @@ namespace Zentral.Infrastructure.Migrations
|
||||
b.ToTable("EventiDettaglioRisorse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Assenza", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomFieldsJson")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DataFine")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DataInizio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DipendenteId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Stato")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TipoAssenza")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DipendenteId");
|
||||
|
||||
b.ToTable("Assenze", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Contratto", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Attivo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomFieldsJson")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DataFine")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DataInizio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DipendenteId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Livello")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("RetribuzioneLorda")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TipoContratto")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DipendenteId");
|
||||
|
||||
b.ToTable("Contratti", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Dipendente", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CodiceFiscale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Cognome")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomFieldsJson")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DataNascita")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Indirizzo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Nome")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Ruolo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Telefono")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CodiceFiscale")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Dipendenti", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Pagamento", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomFieldsJson")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DataPagamento")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Descrizione")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DipendenteId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<decimal>("ImportoNetto")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Pagato")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DipendenteId");
|
||||
|
||||
b.ToTable("Pagamenti", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Rimborso", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomFieldsJson")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DataSpesa")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Descrizione")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DipendenteId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<decimal>("Importo")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Stato")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DipendenteId");
|
||||
|
||||
b.ToTable("Rimborsi", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.Location", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -3697,6 +3945,50 @@ namespace Zentral.Infrastructure.Migrations
|
||||
b.Navigation("Risorsa");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Assenza", b =>
|
||||
{
|
||||
b.HasOne("Zentral.Domain.Entities.HR.Dipendente", "Dipendente")
|
||||
.WithMany("Assenze")
|
||||
.HasForeignKey("DipendenteId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Dipendente");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Contratto", b =>
|
||||
{
|
||||
b.HasOne("Zentral.Domain.Entities.HR.Dipendente", "Dipendente")
|
||||
.WithMany("Contratti")
|
||||
.HasForeignKey("DipendenteId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Dipendente");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Pagamento", b =>
|
||||
{
|
||||
b.HasOne("Zentral.Domain.Entities.HR.Dipendente", "Dipendente")
|
||||
.WithMany("Pagamenti")
|
||||
.HasForeignKey("DipendenteId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Dipendente");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Rimborso", b =>
|
||||
{
|
||||
b.HasOne("Zentral.Domain.Entities.HR.Dipendente", "Dipendente")
|
||||
.WithMany("Rimborsi")
|
||||
.HasForeignKey("DipendenteId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Dipendente");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.ModuleSubscription", b =>
|
||||
{
|
||||
b.HasOne("Zentral.Domain.Entities.AppModule", "Module")
|
||||
@@ -4219,6 +4511,17 @@ namespace Zentral.Infrastructure.Migrations
|
||||
b.Navigation("DettagliRisorse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.HR.Dipendente", b =>
|
||||
{
|
||||
b.Navigation("Assenze");
|
||||
|
||||
b.Navigation("Contratti");
|
||||
|
||||
b.Navigation("Pagamenti");
|
||||
|
||||
b.Navigation("Rimborsi");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.Location", b =>
|
||||
{
|
||||
b.Navigation("Eventi");
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"purchases": "Purchases",
|
||||
"sales": "Sales",
|
||||
"production": "Production",
|
||||
"hr": "Human Resources",
|
||||
"reports": "Reports",
|
||||
"modules": "Modules",
|
||||
"autoCodes": "Auto Codes",
|
||||
@@ -264,6 +265,14 @@
|
||||
"stock": "Stock",
|
||||
"categories": "Categories"
|
||||
},
|
||||
"hr": {
|
||||
"title": "Human Resources",
|
||||
"dipendenti": "Employees",
|
||||
"contratti": "Contracts",
|
||||
"assenze": "Absences",
|
||||
"pagamenti": "Payments",
|
||||
"rimborsi": "Reimbursements"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Module Management",
|
||||
"subtitle": "Configure active modules and manage subscriptions",
|
||||
@@ -356,6 +365,14 @@
|
||||
"4": "Certifications and audits",
|
||||
"5": "Quality statistics"
|
||||
},
|
||||
"hr": {
|
||||
"0": "Employee management",
|
||||
"1": "Contract management",
|
||||
"2": "Absence and leave register",
|
||||
"3": "Payroll and salary management",
|
||||
"4": "Expense reports and reimbursements",
|
||||
"5": "Personnel cost analysis"
|
||||
},
|
||||
"default": "Complete module features"
|
||||
}
|
||||
},
|
||||
@@ -1337,5 +1354,45 @@
|
||||
"process": "Process / Create Order"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personale": {
|
||||
"dipendentiTitle": "Employee Management",
|
||||
"newDipendente": "New Employee",
|
||||
"editDipendente": "Edit Employee",
|
||||
"nome": "First Name",
|
||||
"cognome": "Last Name",
|
||||
"codiceFiscale": "Tax Code",
|
||||
"email": "Email",
|
||||
"telefono": "Phone",
|
||||
"ruolo": "Role",
|
||||
"indirizzo": "Address",
|
||||
"citta": "City",
|
||||
"cap": "ZIP Code",
|
||||
"dataAssunzione": "Hiring Date",
|
||||
"dipartimento": "Department",
|
||||
"contrattiTitle": "Contract Management",
|
||||
"newContratto": "New Contract",
|
||||
"editContratto": "Edit Contract",
|
||||
"dipendente": "Employee",
|
||||
"tipoContratto": "Contract Type",
|
||||
"dataInizio": "Start Date",
|
||||
"dataFine": "End Date",
|
||||
"retribuzione": "Salary",
|
||||
"attivo": "Active",
|
||||
"assenzeTitle": "Absence Management",
|
||||
"newAssenza": "New Absence",
|
||||
"editAssenza": "Edit Absence",
|
||||
"tipoAssenza": "Absence Type",
|
||||
"stato": "Status",
|
||||
"pagamentiTitle": "Payment Management",
|
||||
"newPagamento": "New Payment",
|
||||
"editPagamento": "Edit Payment",
|
||||
"data": "Date",
|
||||
"importo": "Amount",
|
||||
"causale": "Reason",
|
||||
"rimborsiTitle": "Reimbursement Management",
|
||||
"newRimborso": "New Reimbursement",
|
||||
"editRimborso": "Edit Reimbursement",
|
||||
"descrizione": "Description"
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@
|
||||
"purchases": "Acquisti",
|
||||
"sales": "Vendite",
|
||||
"production": "Produzione",
|
||||
"hr": "Gestione Personale",
|
||||
"reports": "Report",
|
||||
"modules": "Moduli",
|
||||
"autoCodes": "Codici Auto",
|
||||
@@ -260,6 +261,14 @@
|
||||
"stock": "Giacenze",
|
||||
"categories": "Categorie"
|
||||
},
|
||||
"hr": {
|
||||
"title": "Gestione Personale",
|
||||
"dipendenti": "Dipendenti",
|
||||
"contratti": "Contratti",
|
||||
"assenze": "Assenze",
|
||||
"pagamenti": "Pagamenti",
|
||||
"rimborsi": "Rimborsi"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Gestione Moduli",
|
||||
"subtitle": "Configura i moduli attivi e gestisci le subscription",
|
||||
@@ -353,6 +362,14 @@
|
||||
"4": "Certificazioni e audit",
|
||||
"5": "Statistiche qualità"
|
||||
},
|
||||
"hr": {
|
||||
"0": "Gestione anagrafica dipendenti",
|
||||
"1": "Gestione contratti",
|
||||
"2": "Registro assenze e ferie",
|
||||
"3": "Gestione pagamenti e stipendi",
|
||||
"4": "Note spese e rimborsi",
|
||||
"5": "Analisi costi personale"
|
||||
},
|
||||
"default": "Funzionalità complete del modulo"
|
||||
}
|
||||
},
|
||||
@@ -1390,5 +1407,45 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"personale": {
|
||||
"dipendentiTitle": "Gestione Dipendenti",
|
||||
"newDipendente": "Nuovo Dipendente",
|
||||
"editDipendente": "Modifica Dipendente",
|
||||
"nome": "Nome",
|
||||
"cognome": "Cognome",
|
||||
"codiceFiscale": "Codice Fiscale",
|
||||
"email": "Email",
|
||||
"telefono": "Telefono",
|
||||
"ruolo": "Ruolo",
|
||||
"indirizzo": "Indirizzo",
|
||||
"citta": "Città",
|
||||
"cap": "CAP",
|
||||
"dataAssunzione": "Data Assunzione",
|
||||
"dipartimento": "Dipartimento",
|
||||
"contrattiTitle": "Gestione Contratti",
|
||||
"newContratto": "Nuovo Contratto",
|
||||
"editContratto": "Modifica Contratto",
|
||||
"dipendente": "Dipendente",
|
||||
"tipoContratto": "Tipo Contratto",
|
||||
"dataInizio": "Data Inizio",
|
||||
"dataFine": "Data Fine",
|
||||
"retribuzione": "Retribuzione",
|
||||
"attivo": "Attivo",
|
||||
"assenzeTitle": "Gestione Assenze",
|
||||
"newAssenza": "Nuova Assenza",
|
||||
"editAssenza": "Modifica Assenza",
|
||||
"tipoAssenza": "Tipo Assenza",
|
||||
"stato": "Stato",
|
||||
"pagamentiTitle": "Gestione Pagamenti",
|
||||
"newPagamento": "Nuovo Pagamento",
|
||||
"editPagamento": "Modifica Pagamento",
|
||||
"data": "Data",
|
||||
"importo": "Importo",
|
||||
"causale": "Causale",
|
||||
"rimborsiTitle": "Gestione Rimborsi",
|
||||
"newRimborso": "Nuovo Rimborso",
|
||||
"editRimborso": "Modifica Rimborso",
|
||||
"descrizione": "Descrizione"
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,7 @@ import { AppLanguageProvider } from "./contexts/LanguageContext";
|
||||
|
||||
import Layout from "./components/Layout";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import EventiPage from "./pages/EventiPage";
|
||||
import EventoDetailPage from "./pages/EventoDetailPage";
|
||||
import ClientiPage from "./pages/ClientiPage";
|
||||
import LocationPage from "./pages/LocationPage";
|
||||
import ArticoliPage from "./pages/ArticoliPage";
|
||||
import RisorsePage from "./pages/RisorsePage";
|
||||
import CalendarioPage from "./pages/CalendarioPage";
|
||||
|
||||
import ReportTemplatesPage from "./pages/ReportTemplatesPage";
|
||||
import ReportEditorPage from "./pages/ReportEditorPage";
|
||||
import ModulesAdminPage from "./pages/ModulesAdminPage";
|
||||
@@ -24,6 +18,8 @@ import WarehouseRoutes from "./modules/warehouse/routes";
|
||||
import PurchasesRoutes from "./modules/purchases/routes";
|
||||
import SalesRoutes from "./modules/sales/routes";
|
||||
import ProductionRoutes from "./modules/production/routes";
|
||||
import EventsRoutes from "./modules/events/routes";
|
||||
import HRRoutes from "./modules/hr/routes";
|
||||
import { ModuleGuard } from "./components/ModuleGuard";
|
||||
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
||||
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
||||
@@ -62,13 +58,7 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="calendario" element={<CalendarioPage />} />
|
||||
<Route path="eventi" element={<EventiPage />} />
|
||||
<Route path="eventi/:id" element={<EventoDetailPage />} />
|
||||
<Route path="clienti" element={<ClientiPage />} />
|
||||
<Route path="location" element={<LocationPage />} />
|
||||
<Route path="articoli" element={<ArticoliPage />} />
|
||||
<Route path="risorse" element={<RisorsePage />} />
|
||||
|
||||
<Route
|
||||
path="report-templates"
|
||||
element={<ReportTemplatesPage />}
|
||||
@@ -131,6 +121,24 @@ function App() {
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* Events Module */}
|
||||
<Route
|
||||
path="events/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="events">
|
||||
<EventsRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* HR Module */}
|
||||
<Route
|
||||
path="hr/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="hr">
|
||||
<HRRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</TabProvider>
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
Timeline as TimelineIcon,
|
||||
PrecisionManufacturing as ManufacturingIcon,
|
||||
Category as CategoryIcon,
|
||||
AttachMoney as AttachMoneyIcon,
|
||||
Receipt as ReceiptIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
@@ -62,6 +64,8 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
||||
purchases: false,
|
||||
sales: false,
|
||||
production: false,
|
||||
events: false,
|
||||
hr: false,
|
||||
admin: false,
|
||||
});
|
||||
|
||||
@@ -85,10 +89,7 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
||||
icon: <DashboardIcon />,
|
||||
children: [
|
||||
{ id: 'dashboard', label: t('menu.dashboard'), icon: <DashboardIcon />, path: '/' },
|
||||
{ id: 'calendar', label: t('menu.calendar'), icon: <CalendarIcon />, path: '/calendario' },
|
||||
{ id: 'events', label: t('menu.events'), icon: <EventIcon />, path: '/eventi' },
|
||||
{ id: 'clients', label: t('menu.clients'), icon: <PeopleIcon />, path: '/clienti' },
|
||||
{ id: 'location', label: t('menu.location'), icon: <PlaceIcon />, path: '/location' },
|
||||
{ id: 'articles', label: t('menu.articles'), icon: <InventoryIcon />, path: '/articoli' },
|
||||
{ id: 'resources', label: t('menu.resources'), icon: <PersonIcon />, path: '/risorse' },
|
||||
],
|
||||
@@ -140,6 +141,30 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
||||
{ id: 'prod-mrp', label: 'MRP', icon: <ManufacturingIcon />, path: '/production/mrp' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
label: t('menu.events'),
|
||||
icon: <EventIcon />,
|
||||
moduleCode: 'events',
|
||||
children: [
|
||||
{ id: 'ev-list', label: t('menu.events'), icon: <EventIcon />, path: '/events/list' },
|
||||
{ id: 'ev-calendar', label: t('menu.calendar'), icon: <CalendarIcon />, path: '/events/calendar' },
|
||||
{ id: 'ev-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/events/locations' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'hr',
|
||||
label: t('modules.hr.title'),
|
||||
icon: <PeopleIcon />,
|
||||
moduleCode: 'hr',
|
||||
children: [
|
||||
{ id: 'hr-dipendenti', label: t('modules.hr.dipendenti'), icon: <PeopleIcon />, path: '/hr/dipendenti' },
|
||||
{ id: 'hr-contratti', label: t('modules.hr.contratti'), icon: <AssignmentIcon />, path: '/hr/contratti' },
|
||||
{ id: 'hr-assenze', label: t('modules.hr.assenze'), icon: <EventIcon />, path: '/hr/assenze' },
|
||||
{ id: 'hr-pagamenti', label: t('modules.hr.pagamenti'), icon: <AttachMoneyIcon />, path: '/hr/pagamenti' },
|
||||
{ id: 'hr-rimborsi', label: t('modules.hr.rimborsi'), icon: <ReceiptIcon />, path: '/hr/rimborsi' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
label: 'Amministrazione',
|
||||
|
||||
@@ -18,7 +18,7 @@ import interactionPlugin from "@fullcalendar/interaction";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { eventiService } from "../services/eventiService";
|
||||
import { eventiService } from "../../../services/eventiService";
|
||||
|
||||
export default function CalendarioPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -53,7 +53,7 @@ export default function CalendarioPage() {
|
||||
|
||||
const handleCreateEvent = () => {
|
||||
// Naviga alla pagina nuovo evento con la data preselezionata
|
||||
navigate("/eventi/0", { state: { dataEvento: newEventDialog.date } });
|
||||
navigate("/events/list/0", { state: { dataEvento: newEventDialog.date } });
|
||||
setNewEventDialog({ open: false, date: "", formattedDate: "" });
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function CalendarioPage() {
|
||||
};
|
||||
|
||||
const handleEventClick = (info: any) => {
|
||||
navigate(`/eventi/${info.event.id}`);
|
||||
navigate(`/events/list/${info.event.id}`);
|
||||
};
|
||||
|
||||
const handleDatesSet = (info: any) => {
|
||||
430
src/frontend/src/modules/events/pages/DashboardPage.tsx
Normal file
430
src/frontend/src/modules/events/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,430 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Chip,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Event as EventIcon,
|
||||
People as PeopleIcon,
|
||||
CheckCircle as ConfirmedIcon,
|
||||
PendingActions as PendingIcon,
|
||||
PlayArrow as GenerateIcon,
|
||||
DeleteSweep as ClearIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { eventiService } from "../services/eventiService";
|
||||
import { demoService, DemoDataResult } from "../services/demoService";
|
||||
import { StatoEvento } from "../types";
|
||||
|
||||
const StatCard = ({
|
||||
title,
|
||||
value,
|
||||
icon,
|
||||
color,
|
||||
}: {
|
||||
title: string;
|
||||
value: number;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
}) => (
|
||||
<Card sx={{ height: "100%" }}>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography color="textSecondary" gutterBottom variant="body2">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h4">{value}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ color, opacity: 0.7 }}>{icon}</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const getStatoLabel = (stato: StatoEvento, t: any) => {
|
||||
switch (stato) {
|
||||
case StatoEvento.Scheda:
|
||||
return t("status.scheda");
|
||||
case StatoEvento.Preventivo:
|
||||
return t("status.preventivo");
|
||||
case StatoEvento.Confermato:
|
||||
return t("status.confermato");
|
||||
default:
|
||||
return t("common.unknown");
|
||||
}
|
||||
};
|
||||
|
||||
const getStatoColor = (stato: StatoEvento) => {
|
||||
switch (stato) {
|
||||
case StatoEvento.Scheda:
|
||||
return "default";
|
||||
case StatoEvento.Preventivo:
|
||||
return "warning";
|
||||
case StatoEvento.Confermato:
|
||||
return "success";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [demoDialog, setDemoDialog] = useState<"generate" | "clear" | null>(
|
||||
null,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<DemoDataResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { data: eventi = [] } = useQuery({
|
||||
queryKey: ["eventi"],
|
||||
queryFn: () => eventiService.getAll(),
|
||||
});
|
||||
|
||||
const handleGenerateDemo = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await demoService.generateDemoData();
|
||||
setResult(res);
|
||||
queryClient.invalidateQueries();
|
||||
} catch (err: any) {
|
||||
setError(
|
||||
err.response?.data?.error || t("dashboard.generateError"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearDemo = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await demoService.clearDemoData();
|
||||
setResult(res);
|
||||
queryClient.invalidateQueries();
|
||||
} catch (err: any) {
|
||||
setError(
|
||||
err.response?.data?.error || t("dashboard.clearError"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setDemoDialog(null);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const oggi = dayjs().startOf("day");
|
||||
const prossimi30Giorni = oggi.add(30, "day");
|
||||
|
||||
const eventiProssimi = eventi
|
||||
.filter(
|
||||
(e) =>
|
||||
dayjs(e.dataEvento).isAfter(oggi) &&
|
||||
dayjs(e.dataEvento).isBefore(prossimi30Giorni),
|
||||
)
|
||||
.sort((a, b) => dayjs(a.dataEvento).diff(dayjs(b.dataEvento)));
|
||||
|
||||
const eventiConfermati = eventi.filter(
|
||||
(e) => e.stato === StatoEvento.Confermato,
|
||||
).length;
|
||||
const eventiPreventivo = eventi.filter(
|
||||
(e) => e.stato === StatoEvento.Preventivo,
|
||||
).length;
|
||||
const eventiOggi = eventi.filter((e) =>
|
||||
dayjs(e.dataEvento).isSame(oggi, "day"),
|
||||
).length;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">{t("dashboard.title")}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<GenerateIcon />}
|
||||
onClick={() => setDemoDialog("generate")}
|
||||
>
|
||||
{t("dashboard.generateDemoData")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<ClearIcon />}
|
||||
onClick={() => setDemoDialog("clear")}
|
||||
>
|
||||
{t("dashboard.clearDatabase")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.totalEvents")}
|
||||
value={eventi.length}
|
||||
icon={<EventIcon sx={{ fontSize: 48 }} />}
|
||||
color="#1976d2"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.confirmed")}
|
||||
value={eventiConfermati}
|
||||
icon={<ConfirmedIcon sx={{ fontSize: 48 }} />}
|
||||
color="#4caf50"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.inQuote")}
|
||||
value={eventiPreventivo}
|
||||
icon={<PendingIcon sx={{ fontSize: 48 }} />}
|
||||
color="#ff9800"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.eventsToday")}
|
||||
value={eventiOggi}
|
||||
icon={<PeopleIcon sx={{ fontSize: 48 }} />}
|
||||
color="#9c27b0"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("dashboard.upcomingEvents")}
|
||||
</Typography>
|
||||
<List>
|
||||
{eventiProssimi.slice(0, 10).map((evento) => (
|
||||
<ListItem
|
||||
key={evento.id}
|
||||
component="div"
|
||||
onClick={() => navigate(`/eventi/${evento.id}`)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={evento.descrizione || evento.codice}
|
||||
secondary={
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ display: "flex", gap: 1, alignItems: "center" }}
|
||||
>
|
||||
<span>
|
||||
{dayjs(evento.dataEvento).format("DD/MM/YYYY")}
|
||||
</span>
|
||||
<span>-</span>
|
||||
<span>{evento.cliente?.ragioneSociale}</span>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label={getStatoLabel(evento.stato, t)}
|
||||
color={getStatoColor(evento.stato) as any}
|
||||
size="small"
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{eventiProssimi.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText primary={t("dashboard.noEvents")} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("dashboard.expiringQuotes")}
|
||||
</Typography>
|
||||
<List>
|
||||
{eventi
|
||||
.filter(
|
||||
(e) =>
|
||||
e.stato === StatoEvento.Preventivo &&
|
||||
e.dataScadenzaPreventivo,
|
||||
)
|
||||
.sort((a, b) =>
|
||||
dayjs(a.dataScadenzaPreventivo).diff(
|
||||
dayjs(b.dataScadenzaPreventivo),
|
||||
),
|
||||
)
|
||||
.slice(0, 5)
|
||||
.map((evento) => (
|
||||
<ListItem
|
||||
key={evento.id}
|
||||
component="div"
|
||||
onClick={() => navigate(`/eventi/${evento.id}`)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={evento.descrizione || evento.codice}
|
||||
secondary={t("dashboard.expires", { date: dayjs(evento.dataScadenzaPreventivo).format("DD/MM/YYYY") })}
|
||||
/>
|
||||
<Chip
|
||||
label={t("dashboard.guests", { count: evento.numeroOspiti || 0 })}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{eventi.filter((e) => e.stato === StatoEvento.Preventivo)
|
||||
.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText primary={t("dashboard.noQuotes")} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Dialog Genera Dati Demo */}
|
||||
<Dialog open={demoDialog === "generate"} onClose={handleCloseDialog}>
|
||||
<DialogTitle>{t("dashboard.generateDialogTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{!result && !error && (
|
||||
<DialogContentText>
|
||||
<Trans i18nKey="dashboard.generateDialogText" components={{ br: <br /> }} />
|
||||
</DialogContentText>
|
||||
)}
|
||||
{loading && (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{result && (
|
||||
<Alert severity="success" sx={{ mt: 1 }}>
|
||||
{result.message}
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
{result ? t("common.close") : t("common.cancel")}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
onClick={handleGenerateDemo}
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("common.generate")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog Pulisci Database */}
|
||||
<Dialog open={demoDialog === "clear"} onClose={handleCloseDialog}>
|
||||
<DialogTitle>{t("dashboard.clearDialogTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{!result && !error && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
{t("dashboard.clearDialogWarning")}
|
||||
</Alert>
|
||||
)}
|
||||
{!result && !error && (
|
||||
<DialogContentText>
|
||||
<Trans i18nKey="dashboard.clearDialogText" components={{ br: <br /> }} />
|
||||
</DialogContentText>
|
||||
)}
|
||||
{loading && (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{result && (
|
||||
<Alert severity="success" sx={{ mt: 1 }}>
|
||||
{t("dashboard.clearSuccess", {
|
||||
events: result.eventiCreati,
|
||||
clients: result.clientiCreati,
|
||||
locations: result.locationCreate,
|
||||
resources: result.risorseCreate,
|
||||
articles: result.articoliCreati
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
{result ? t("common.close") : t("common.cancel")}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
onClick={handleClearDemo}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("common.deleteAll")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -29,9 +29,9 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventiService } from '../services/eventiService';
|
||||
import { lookupService } from '../services/lookupService';
|
||||
import { Evento, StatoEvento } from '../types';
|
||||
import { eventiService } from '../../../services/eventiService';
|
||||
import { lookupService } from '../../../services/lookupService';
|
||||
import { Evento, StatoEvento } from '../../../types';
|
||||
|
||||
const getStatoLabel = (stato: StatoEvento, t: any) => {
|
||||
switch (stato) {
|
||||
@@ -150,10 +150,10 @@ export default function EventiPage() {
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => navigate(`/eventi/${params.row.id}`)}>
|
||||
<IconButton size="small" onClick={() => navigate(`/events/list/${params.row.id}`)}>
|
||||
<ViewIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => navigate(`/eventi/${params.row.id}`)}>
|
||||
<IconButton size="small" onClick={() => navigate(`/events/list/${params.row.id}`)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => duplicaMutation.mutate(params.row.id)}>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { signalRService } from "../services/signalr";
|
||||
import { signalRService } from "../../../services/signalr";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -42,16 +42,16 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import dayjs from "dayjs";
|
||||
import { eventiService } from "../services/eventiService";
|
||||
import { lookupService } from "../services/lookupService";
|
||||
import EventoCostiPanel from "../components/EventoCostiPanel";
|
||||
import { eventiService } from "../../../services/eventiService";
|
||||
import { lookupService } from "../../../services/lookupService";
|
||||
import EventoCostiPanel from "../../../components/EventoCostiPanel";
|
||||
import {
|
||||
Evento,
|
||||
StatoEvento,
|
||||
EventoDettaglioOspiti,
|
||||
EventoDettaglioPrelievo,
|
||||
EventoDettaglioRisorsa,
|
||||
} from "../types";
|
||||
} from "../../../types";
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -147,7 +147,7 @@ export default function EventoDetailPage() {
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Evento>) => eventiService.create(data),
|
||||
onSuccess: (newEvento) => {
|
||||
navigate(`/eventi/${newEvento.id}`);
|
||||
navigate(`/events/list/${newEvento.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -206,7 +206,7 @@ export default function EventoDetailPage() {
|
||||
const duplicaMutation = useMutation({
|
||||
mutationFn: () => eventiService.duplica(eventoId),
|
||||
onSuccess: (newEvento) => {
|
||||
navigate(`/eventi/${newEvento.id}`);
|
||||
navigate(`/events/list/${newEvento.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -362,7 +362,7 @@ export default function EventoDetailPage() {
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
onClick={() => navigate("/eventi")}
|
||||
onClick={() => navigate("/events/list")}
|
||||
sx={{ color: statoInfo.textColor }}
|
||||
>
|
||||
<BackIcon />
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { locationService } from '../services/lookupService';
|
||||
import { Location } from '../types';
|
||||
import { locationService } from '../../../services/lookupService';
|
||||
import { Location } from '../../../types';
|
||||
|
||||
export default function LocationPage() {
|
||||
const queryClient = useQueryClient();
|
||||
17
src/frontend/src/modules/events/routes.tsx
Normal file
17
src/frontend/src/modules/events/routes.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import EventiPage from "./pages/EventiPage";
|
||||
import EventoDetailPage from "./pages/EventoDetailPage";
|
||||
import CalendarioPage from "./pages/CalendarioPage";
|
||||
import LocationPage from "./pages/LocationPage";
|
||||
|
||||
export default function EventsRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="list" replace />} />
|
||||
<Route path="list" element={<EventiPage />} />
|
||||
<Route path="list/:id" element={<EventoDetailPage />} />
|
||||
<Route path="calendar" element={<CalendarioPage />} />
|
||||
<Route path="locations" element={<LocationPage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
281
src/frontend/src/modules/hr/pages/AssenzePage.tsx
Normal file
281
src/frontend/src/modules/hr/pages/AssenzePage.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Grid,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import axios from 'axios';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Interfaces
|
||||
interface Dipendente {
|
||||
id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
}
|
||||
|
||||
interface Assenza {
|
||||
id: number;
|
||||
dipendenteId: number;
|
||||
dipendente?: Dipendente;
|
||||
dataInizio: string;
|
||||
dataFine: string;
|
||||
tipoAssenza: string;
|
||||
stato: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
const assenzeService = {
|
||||
getAll: async (dipendenteId?: number) => {
|
||||
const params = dipendenteId ? { dipendenteId } : {};
|
||||
const response = await axios.get<Assenza[]>('/api/hr/assenze', { params });
|
||||
return response.data;
|
||||
},
|
||||
create: async (data: Partial<Assenza>) => {
|
||||
const response = await axios.post<Assenza>('/api/hr/assenze', data);
|
||||
return response.data;
|
||||
},
|
||||
update: async (id: number, data: Partial<Assenza>) => {
|
||||
const response = await axios.put(`/api/hr/assenze/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
delete: async (id: number) => {
|
||||
await axios.delete(`/api/hr/assenze/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
const dipendentiService = {
|
||||
getAll: async () => {
|
||||
const response = await axios.get<Dipendente[]>('/api/hr/dipendenti');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default function AssenzePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Assenza>>({ stato: 'Richiesta' });
|
||||
|
||||
const { data: assenze = [], isLoading } = useQuery({
|
||||
queryKey: ['assenze'],
|
||||
queryFn: () => assenzeService.getAll(),
|
||||
});
|
||||
|
||||
const { data: dipendenti = [] } = useQuery({
|
||||
queryKey: ['dipendenti'],
|
||||
queryFn: () => dipendentiService.getAll(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Assenza>) => assenzeService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['assenze'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Assenza> }) => assenzeService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['assenze'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => assenzeService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['assenze'] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenDialog(false);
|
||||
setEditingId(null);
|
||||
setFormData({ stato: 'Richiesta' });
|
||||
};
|
||||
|
||||
const handleEdit = (assenza: Assenza) => {
|
||||
setFormData(assenza);
|
||||
setEditingId(assenza.id);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'dipendente',
|
||||
headerName: t('personale.dipendente'),
|
||||
width: 200,
|
||||
valueGetter: (params: any) => params?.nome && params?.cognome ? `${params.cognome} ${params.nome}` : '',
|
||||
},
|
||||
{ field: 'tipoAssenza', headerName: t('personale.tipoAssenza'), width: 150 },
|
||||
{
|
||||
field: 'dataInizio',
|
||||
headerName: t('personale.dataInizio'),
|
||||
width: 120,
|
||||
valueFormatter: (params: any) => params?.value ? dayjs(params.value).format('DD/MM/YYYY') : '',
|
||||
},
|
||||
{
|
||||
field: 'dataFine',
|
||||
headerName: t('personale.dataFine'),
|
||||
width: 120,
|
||||
valueFormatter: (params: any) => params?.value ? dayjs(params.value).format('DD/MM/YYYY') : '',
|
||||
},
|
||||
{ field: 'stato', headerName: t('personale.stato'), width: 120 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: t('common.actions'),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => handleEdit(params.row)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm(t('common.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">{t('personale.assenzeTitle')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
{t('personale.newAssenza')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={assenze}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? t('personale.editAssenza') : t('personale.newAssenza')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label={t('personale.dipendente')}
|
||||
value={formData.dipendenteId || ''}
|
||||
onChange={(e) => setFormData({ ...formData, dipendenteId: Number(e.target.value) })}
|
||||
required
|
||||
>
|
||||
{dipendenti?.map((d) => (
|
||||
<MenuItem key={d.id} value={d.id}>
|
||||
{d.nome} {d.cognome}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label={t('personale.tipoAssenza')}
|
||||
value={formData.tipoAssenza || ''}
|
||||
onChange={(e) => setFormData({ ...formData, tipoAssenza: e.target.value })}
|
||||
required
|
||||
>
|
||||
<MenuItem value="Ferie">Ferie</MenuItem>
|
||||
<MenuItem value="Malattia">Malattia</MenuItem>
|
||||
<MenuItem value="Permesso">Permesso</MenuItem>
|
||||
<MenuItem value="Altro">Altro</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label={t('personale.stato')}
|
||||
value={formData.stato || 'Richiesta'}
|
||||
onChange={(e) => setFormData({ ...formData, stato: e.target.value })}
|
||||
required
|
||||
>
|
||||
<MenuItem value="Richiesta">Richiesta</MenuItem>
|
||||
<MenuItem value="Approvata">Approvata</MenuItem>
|
||||
<MenuItem value="Rifiutata">Rifiutata</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<DatePicker
|
||||
label={t('personale.dataInizio')}
|
||||
value={formData.dataInizio ? dayjs(formData.dataInizio) : null}
|
||||
onChange={(newValue) => setFormData({ ...formData, dataInizio: newValue ? newValue.toISOString() : undefined })}
|
||||
slotProps={{ textField: { fullWidth: true, required: true } }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<DatePicker
|
||||
label={t('personale.dataFine')}
|
||||
value={formData.dataFine ? dayjs(formData.dataFine) : null}
|
||||
onChange={(newValue) => setFormData({ ...formData, dataFine: newValue ? newValue.toISOString() : undefined })}
|
||||
slotProps={{ textField: { fullWidth: true, required: true } }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
label={t('common.notes')}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={formData.note || ''}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
292
src/frontend/src/modules/hr/pages/ContrattiPage.tsx
Normal file
292
src/frontend/src/modules/hr/pages/ContrattiPage.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Grid,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import axios from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Interfaces
|
||||
interface Dipendente {
|
||||
id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
}
|
||||
|
||||
interface Contratto {
|
||||
id: number;
|
||||
dipendenteId: number;
|
||||
dipendente?: Dipendente;
|
||||
dataInizio: string;
|
||||
dataFine?: string;
|
||||
tipoContratto: string;
|
||||
livello?: string;
|
||||
retribuzioneLorda: number;
|
||||
oreSettimanali?: number;
|
||||
note?: string;
|
||||
attivo: boolean;
|
||||
}
|
||||
|
||||
const contrattiService = {
|
||||
getAll: async (dipendenteId?: number) => {
|
||||
const params = dipendenteId ? { dipendenteId } : {};
|
||||
const response = await axios.get<Contratto[]>('/api/hr/contratti', { params });
|
||||
return response.data;
|
||||
},
|
||||
create: async (data: Partial<Contratto>) => {
|
||||
const response = await axios.post<Contratto>('/api/hr/contratti', data);
|
||||
return response.data;
|
||||
},
|
||||
update: async (id: number, data: Partial<Contratto>) => {
|
||||
const response = await axios.put(`/api/hr/contratti/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
delete: async (id: number) => {
|
||||
await axios.delete(`/api/hr/contratti/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
const dipendentiService = {
|
||||
getAll: async () => {
|
||||
const response = await axios.get<Dipendente[]>('/api/hr/dipendenti');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default function ContrattiPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Contratto>>({ attivo: true });
|
||||
|
||||
const { data: contratti = [], isLoading } = useQuery({
|
||||
queryKey: ['contratti'],
|
||||
queryFn: () => contrattiService.getAll(),
|
||||
});
|
||||
|
||||
const { data: dipendenti = [] } = useQuery({
|
||||
queryKey: ['dipendenti'],
|
||||
queryFn: () => dipendentiService.getAll(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Contratto>) => contrattiService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contratti'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Contratto> }) => contrattiService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contratti'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => contrattiService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['contratti'] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenDialog(false);
|
||||
setEditingId(null);
|
||||
setFormData({ attivo: true });
|
||||
};
|
||||
|
||||
const handleEdit = (contratto: Contratto) => {
|
||||
setFormData(contratto);
|
||||
setEditingId(contratto.id);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'dipendente',
|
||||
headerName: t('personale.dipendente'),
|
||||
width: 200,
|
||||
valueGetter: (params: any) => params?.nome && params?.cognome ? `${params.cognome} ${params.nome}` : '',
|
||||
},
|
||||
{ field: 'tipoContratto', headerName: t('personale.tipoContratto'), width: 150 },
|
||||
{
|
||||
field: 'dataInizio',
|
||||
headerName: t('personale.dataInizio'),
|
||||
width: 120,
|
||||
valueFormatter: (params: any) => params?.value ? dayjs(params.value).format('DD/MM/YYYY') : '',
|
||||
},
|
||||
{
|
||||
field: 'dataFine',
|
||||
headerName: t('personale.dataFine'),
|
||||
width: 120,
|
||||
valueFormatter: (params: any) => params?.value ? dayjs(params.value).format('DD/MM/YYYY') : '',
|
||||
},
|
||||
{ field: 'retribuzioneLorda', headerName: t('personale.retribuzione'), width: 120, type: 'number' },
|
||||
{ field: 'attivo', headerName: t('personale.attivo'), width: 100, type: 'boolean' },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: t('common.actions'),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => handleEdit(params.row)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm(t('common.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">{t('personale.contrattiTitle')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
{t('personale.newContratto')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={contratti}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? t('personale.editContratto') : t('personale.newContratto')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label="Dipendente"
|
||||
value={formData.dipendenteId || ''}
|
||||
onChange={(e) => setFormData({ ...formData, dipendenteId: Number(e.target.value) })}
|
||||
>
|
||||
{dipendenti?.map((d) => (
|
||||
<MenuItem key={d.id} value={d.id}>
|
||||
{d.nome} {d.cognome}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Tipo Contratto"
|
||||
value={formData.tipoContratto || ''}
|
||||
onChange={(e) => setFormData({ ...formData, tipoContratto: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Livello"
|
||||
value={formData.livello || ''}
|
||||
onChange={(e) => setFormData({ ...formData, livello: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="date"
|
||||
label="Data Inizio"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={formData.dataInizio ? new Date(formData.dataInizio).toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => setFormData({ ...formData, dataInizio: new Date(e.target.value).toISOString() })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="date"
|
||||
label="Data Fine"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={formData.dataFine ? new Date(formData.dataFine).toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => setFormData({ ...formData, dataFine: e.target.value ? new Date(e.target.value).toISOString() : undefined })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label="RAL"
|
||||
value={formData.retribuzioneLorda || ''}
|
||||
onChange={(e) => setFormData({ ...formData, retribuzioneLorda: Number(e.target.value) })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label="Ore Settimanali"
|
||||
value={formData.oreSettimanali || ''}
|
||||
onChange={(e) => setFormData({ ...formData, oreSettimanali: Number(e.target.value) })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="Note"
|
||||
value={formData.note || ''}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
277
src/frontend/src/modules/hr/pages/DipendentiPage.tsx
Normal file
277
src/frontend/src/modules/hr/pages/DipendentiPage.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Grid,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaces
|
||||
interface Dipendente {
|
||||
id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
codiceFiscale?: string;
|
||||
email?: string;
|
||||
telefono?: string;
|
||||
ruolo?: string;
|
||||
dataNascita?: string;
|
||||
indirizzo?: string;
|
||||
citta?: string;
|
||||
cap?: string;
|
||||
dataAssunzione?: string;
|
||||
dipartimento?: string;
|
||||
}
|
||||
|
||||
const dipendentiService = {
|
||||
getAll: async (search?: string) => {
|
||||
const params = search ? { search } : {};
|
||||
const response = await axios.get<Dipendente[]>('/api/hr/dipendenti', { params });
|
||||
return response.data;
|
||||
},
|
||||
get: async (id: number) => {
|
||||
const response = await axios.get<Dipendente>(`/api/hr/dipendenti/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
create: async (data: Partial<Dipendente>) => {
|
||||
const response = await axios.post<Dipendente>('/api/hr/dipendenti', data);
|
||||
return response.data;
|
||||
},
|
||||
update: async (id: number, data: Partial<Dipendente>) => {
|
||||
const response = await axios.put(`/api/hr/dipendenti/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
delete: async (id: number) => {
|
||||
await axios.delete(`/api/hr/dipendenti/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
export default function DipendentiPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Dipendente>>({});
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const { data: dipendenti = [], isLoading } = useQuery({
|
||||
queryKey: ['dipendenti', search],
|
||||
queryFn: () => dipendentiService.getAll(search),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Dipendente>) => dipendentiService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['dipendenti'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Dipendente> }) => dipendentiService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['dipendenti'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => dipendentiService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dipendenti'] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenDialog(false);
|
||||
setEditingId(null);
|
||||
setFormData({});
|
||||
};
|
||||
|
||||
const handleEdit = (dipendente: Dipendente) => {
|
||||
setFormData(dipendente);
|
||||
setEditingId(dipendente.id);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'nome', headerName: t('personale.nome'), width: 150 },
|
||||
{ field: 'cognome', headerName: t('personale.cognome'), width: 150 },
|
||||
{ field: 'ruolo', headerName: t('personale.ruolo'), width: 150 },
|
||||
{ field: 'email', headerName: t('personale.email'), width: 200 },
|
||||
{ field: 'telefono', headerName: t('personale.telefono'), width: 150 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: t('common.actions'),
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => handleEdit(params.row)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm(t('common.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">{t('personale.dipendentiTitle')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
{t('personale.newDipendente')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={dipendenti}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? t('personale.editDipendente') : t('personale.newDipendente')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.nome')}
|
||||
required
|
||||
value={formData.nome || ''}
|
||||
onChange={(e) => setFormData({ ...formData, nome: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.cognome')}
|
||||
required
|
||||
value={formData.cognome || ''}
|
||||
onChange={(e) => setFormData({ ...formData, cognome: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.codiceFiscale')}
|
||||
value={formData.codiceFiscale || ''}
|
||||
onChange={(e) => setFormData({ ...formData, codiceFiscale: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.email')}
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.telefono')}
|
||||
value={formData.telefono || ''}
|
||||
onChange={(e) => setFormData({ ...formData, telefono: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.indirizzo')}
|
||||
value={formData.indirizzo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, indirizzo: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.citta')}
|
||||
value={formData.citta || ''}
|
||||
onChange={(e) => setFormData({ ...formData, citta: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.cap')}
|
||||
value={formData.cap || ''}
|
||||
onChange={(e) => setFormData({ ...formData, cap: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="date"
|
||||
label={t('personale.dataAssunzione')}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={formData.dataAssunzione ? new Date(formData.dataAssunzione).toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => setFormData({ ...formData, dataAssunzione: new Date(e.target.value).toISOString() })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.ruolo')}
|
||||
value={formData.ruolo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, ruolo: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.dipartimento')}
|
||||
value={formData.dipartimento || ''}
|
||||
onChange={(e) => setFormData({ ...formData, dipartimento: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
256
src/frontend/src/modules/hr/pages/PagamentiPage.tsx
Normal file
256
src/frontend/src/modules/hr/pages/PagamentiPage.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Grid,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import axios from 'axios';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Interfaces
|
||||
interface Dipendente {
|
||||
id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
}
|
||||
|
||||
interface Pagamento {
|
||||
id: number;
|
||||
dipendenteId: number;
|
||||
dipendente?: Dipendente;
|
||||
data: string;
|
||||
importo: number;
|
||||
causale: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
const pagamentiService = {
|
||||
getAll: async (dipendenteId?: number) => {
|
||||
const params = dipendenteId ? { dipendenteId } : {};
|
||||
const response = await axios.get<Pagamento[]>('/api/hr/pagamenti', { params });
|
||||
return response.data;
|
||||
},
|
||||
create: async (data: Partial<Pagamento>) => {
|
||||
const response = await axios.post<Pagamento>('/api/hr/pagamenti', data);
|
||||
return response.data;
|
||||
},
|
||||
update: async (id: number, data: Partial<Pagamento>) => {
|
||||
const response = await axios.put(`/api/hr/pagamenti/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
delete: async (id: number) => {
|
||||
await axios.delete(`/api/hr/pagamenti/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
const dipendentiService = {
|
||||
getAll: async () => {
|
||||
const response = await axios.get<Dipendente[]>('/api/hr/dipendenti');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default function PagamentiPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Pagamento>>({});
|
||||
|
||||
const { data: pagamenti = [], isLoading } = useQuery({
|
||||
queryKey: ['pagamenti'],
|
||||
queryFn: () => pagamentiService.getAll(),
|
||||
});
|
||||
|
||||
const { data: dipendenti = [] } = useQuery({
|
||||
queryKey: ['dipendenti'],
|
||||
queryFn: () => dipendentiService.getAll(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Pagamento>) => pagamentiService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pagamenti'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Pagamento> }) => pagamentiService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pagamenti'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => pagamentiService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['pagamenti'] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenDialog(false);
|
||||
setEditingId(null);
|
||||
setFormData({});
|
||||
};
|
||||
|
||||
const handleEdit = (pagamento: Pagamento) => {
|
||||
setFormData(pagamento);
|
||||
setEditingId(pagamento.id);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'dipendente',
|
||||
headerName: t('personale.dipendente'),
|
||||
width: 200,
|
||||
valueGetter: (params: any) => params?.nome && params?.cognome ? `${params.cognome} ${params.nome}` : '',
|
||||
},
|
||||
{
|
||||
field: 'data',
|
||||
headerName: t('personale.data'),
|
||||
width: 120,
|
||||
valueFormatter: (params: any) => params?.value ? dayjs(params.value).format('DD/MM/YYYY') : '',
|
||||
},
|
||||
{ field: 'importo', headerName: t('personale.importo'), width: 120, type: 'number' },
|
||||
{ field: 'causale', headerName: t('personale.causale'), width: 200 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: t('common.actions'),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => handleEdit(params.row)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm(t('common.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">{t('personale.pagamentiTitle')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
{t('personale.newPagamento')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={pagamenti}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? t('personale.editPagamento') : t('personale.newPagamento')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label={t('personale.dipendente')}
|
||||
value={formData.dipendenteId || ''}
|
||||
onChange={(e) => setFormData({ ...formData, dipendenteId: Number(e.target.value) })}
|
||||
required
|
||||
>
|
||||
{dipendenti?.map((d) => (
|
||||
<MenuItem key={d.id} value={d.id}>
|
||||
{d.nome} {d.cognome}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<DatePicker
|
||||
label={t('personale.data')}
|
||||
value={formData.data ? dayjs(formData.data) : null}
|
||||
onChange={(newValue) => setFormData({ ...formData, data: newValue ? newValue.toISOString() : undefined })}
|
||||
slotProps={{ textField: { fullWidth: true, required: true } }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label={t('personale.importo')}
|
||||
value={formData.importo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, importo: Number(e.target.value) })}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.causale')}
|
||||
value={formData.causale || ''}
|
||||
onChange={(e) => setFormData({ ...formData, causale: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label={t('common.notes')}
|
||||
value={formData.note || ''}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
273
src/frontend/src/modules/hr/pages/RimborsiPage.tsx
Normal file
273
src/frontend/src/modules/hr/pages/RimborsiPage.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Grid,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import axios from 'axios';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Interfaces
|
||||
interface Dipendente {
|
||||
id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
}
|
||||
|
||||
interface Rimborso {
|
||||
id: number;
|
||||
dipendenteId: number;
|
||||
dipendente?: Dipendente;
|
||||
data: string;
|
||||
importo: number;
|
||||
descrizione: string;
|
||||
stato: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
const rimborsiService = {
|
||||
getAll: async (dipendenteId?: number) => {
|
||||
const params = dipendenteId ? { dipendenteId } : {};
|
||||
const response = await axios.get<Rimborso[]>('/api/hr/rimborsi', { params });
|
||||
return response.data;
|
||||
},
|
||||
create: async (data: Partial<Rimborso>) => {
|
||||
const response = await axios.post<Rimborso>('/api/hr/rimborsi', data);
|
||||
return response.data;
|
||||
},
|
||||
update: async (id: number, data: Partial<Rimborso>) => {
|
||||
const response = await axios.put(`/api/hr/rimborsi/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
delete: async (id: number) => {
|
||||
await axios.delete(`/api/hr/rimborsi/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
const dipendentiService = {
|
||||
getAll: async () => {
|
||||
const response = await axios.get<Dipendente[]>('/api/hr/dipendenti');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default function RimborsiPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Rimborso>>({ stato: 'Richiesto' });
|
||||
|
||||
const { data: rimborsi = [], isLoading } = useQuery({
|
||||
queryKey: ['rimborsi'],
|
||||
queryFn: () => rimborsiService.getAll(),
|
||||
});
|
||||
|
||||
const { data: dipendenti = [] } = useQuery({
|
||||
queryKey: ['dipendenti'],
|
||||
queryFn: () => dipendentiService.getAll(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Rimborso>) => rimborsiService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['rimborsi'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Rimborso> }) => rimborsiService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['rimborsi'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => rimborsiService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['rimborsi'] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenDialog(false);
|
||||
setEditingId(null);
|
||||
setFormData({ stato: 'Richiesto' });
|
||||
};
|
||||
|
||||
const handleEdit = (rimborso: Rimborso) => {
|
||||
setFormData(rimborso);
|
||||
setEditingId(rimborso.id);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'dipendente',
|
||||
headerName: t('personale.dipendente'),
|
||||
width: 200,
|
||||
valueGetter: (params: any) => params?.nome && params?.cognome ? `${params.cognome} ${params.nome}` : '',
|
||||
},
|
||||
{
|
||||
field: 'data',
|
||||
headerName: t('personale.data'),
|
||||
width: 120,
|
||||
valueFormatter: (params: any) => params?.value ? dayjs(params.value).format('DD/MM/YYYY') : '',
|
||||
},
|
||||
{ field: 'importo', headerName: t('personale.importo'), width: 120, type: 'number' },
|
||||
{ field: 'descrizione', headerName: t('personale.descrizione'), width: 200 },
|
||||
{ field: 'stato', headerName: t('personale.stato'), width: 120 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: t('common.actions'),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => handleEdit(params.row)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm(t('common.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">{t('personale.rimborsiTitle')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
{t('personale.newRimborso')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={rimborsi}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? t('personale.editRimborso') : t('personale.newRimborso')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label={t('personale.dipendente')}
|
||||
value={formData.dipendenteId || ''}
|
||||
onChange={(e) => setFormData({ ...formData, dipendenteId: Number(e.target.value) })}
|
||||
required
|
||||
>
|
||||
{dipendenti?.map((d) => (
|
||||
<MenuItem key={d.id} value={d.id}>
|
||||
{d.nome} {d.cognome}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<DatePicker
|
||||
label={t('personale.data')}
|
||||
value={formData.data ? dayjs(formData.data) : null}
|
||||
onChange={(newValue) => setFormData({ ...formData, data: newValue ? newValue.toISOString() : undefined })}
|
||||
slotProps={{ textField: { fullWidth: true, required: true } }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label={t('personale.importo')}
|
||||
value={formData.importo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, importo: Number(e.target.value) })}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('personale.descrizione')}
|
||||
value={formData.descrizione || ''}
|
||||
onChange={(e) => setFormData({ ...formData, descrizione: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label={t('personale.stato')}
|
||||
value={formData.stato || 'Richiesto'}
|
||||
onChange={(e) => setFormData({ ...formData, stato: e.target.value })}
|
||||
required
|
||||
>
|
||||
<MenuItem value="Richiesto">Richiesto</MenuItem>
|
||||
<MenuItem value="Approvato">Approvato</MenuItem>
|
||||
<MenuItem value="Rimborsato">Rimborsato</MenuItem>
|
||||
<MenuItem value="Rifiutato">Rifiutato</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label={t('common.notes')}
|
||||
value={formData.note || ''}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
19
src/frontend/src/modules/hr/routes.tsx
Normal file
19
src/frontend/src/modules/hr/routes.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import DipendentiPage from "./pages/DipendentiPage";
|
||||
import ContrattiPage from "./pages/ContrattiPage";
|
||||
import AssenzePage from "./pages/AssenzePage";
|
||||
import PagamentiPage from "./pages/PagamentiPage";
|
||||
import RimborsiPage from "./pages/RimborsiPage";
|
||||
|
||||
export default function HRRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="dipendenti" replace />} />
|
||||
<Route path="dipendenti" element={<DipendentiPage />} />
|
||||
<Route path="contratti" element={<ContrattiPage />} />
|
||||
<Route path="assenze" element={<AssenzePage />} />
|
||||
<Route path="pagamenti" element={<PagamentiPage />} />
|
||||
<Route path="rimborsi" element={<RimborsiPage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@@ -1,430 +1,21 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Chip,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Event as EventIcon,
|
||||
People as PeopleIcon,
|
||||
CheckCircle as ConfirmedIcon,
|
||||
PendingActions as PendingIcon,
|
||||
PlayArrow as GenerateIcon,
|
||||
DeleteSweep as ClearIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { eventiService } from "../services/eventiService";
|
||||
import { demoService, DemoDataResult } from "../services/demoService";
|
||||
import { StatoEvento } from "../types";
|
||||
|
||||
const StatCard = ({
|
||||
title,
|
||||
value,
|
||||
icon,
|
||||
color,
|
||||
}: {
|
||||
title: string;
|
||||
value: number;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
}) => (
|
||||
<Card sx={{ height: "100%" }}>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography color="textSecondary" gutterBottom variant="body2">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h4">{value}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ color, opacity: 0.7 }}>{icon}</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const getStatoLabel = (stato: StatoEvento, t: any) => {
|
||||
switch (stato) {
|
||||
case StatoEvento.Scheda:
|
||||
return t("status.scheda");
|
||||
case StatoEvento.Preventivo:
|
||||
return t("status.preventivo");
|
||||
case StatoEvento.Confermato:
|
||||
return t("status.confermato");
|
||||
default:
|
||||
return t("common.unknown");
|
||||
}
|
||||
};
|
||||
|
||||
const getStatoColor = (stato: StatoEvento) => {
|
||||
switch (stato) {
|
||||
case StatoEvento.Scheda:
|
||||
return "default";
|
||||
case StatoEvento.Preventivo:
|
||||
return "warning";
|
||||
case StatoEvento.Confermato:
|
||||
return "success";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
import { Box, Typography, Grid, Paper } from '@mui/material';
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [demoDialog, setDemoDialog] = useState<"generate" | "clear" | null>(
|
||||
null,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<DemoDataResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { data: eventi = [] } = useQuery({
|
||||
queryKey: ["eventi"],
|
||||
queryFn: () => eventiService.getAll(),
|
||||
});
|
||||
|
||||
const handleGenerateDemo = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await demoService.generateDemoData();
|
||||
setResult(res);
|
||||
queryClient.invalidateQueries();
|
||||
} catch (err: any) {
|
||||
setError(
|
||||
err.response?.data?.error || t("dashboard.generateError"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearDemo = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await demoService.clearDemoData();
|
||||
setResult(res);
|
||||
queryClient.invalidateQueries();
|
||||
} catch (err: any) {
|
||||
setError(
|
||||
err.response?.data?.error || t("dashboard.clearError"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setDemoDialog(null);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const oggi = dayjs().startOf("day");
|
||||
const prossimi30Giorni = oggi.add(30, "day");
|
||||
|
||||
const eventiProssimi = eventi
|
||||
.filter(
|
||||
(e) =>
|
||||
dayjs(e.dataEvento).isAfter(oggi) &&
|
||||
dayjs(e.dataEvento).isBefore(prossimi30Giorni),
|
||||
)
|
||||
.sort((a, b) => dayjs(a.dataEvento).diff(dayjs(b.dataEvento)));
|
||||
|
||||
const eventiConfermati = eventi.filter(
|
||||
(e) => e.stato === StatoEvento.Confermato,
|
||||
).length;
|
||||
const eventiPreventivo = eventi.filter(
|
||||
(e) => e.stato === StatoEvento.Preventivo,
|
||||
).length;
|
||||
const eventiOggi = eventi.filter((e) =>
|
||||
dayjs(e.dataEvento).isSame(oggi, "day"),
|
||||
).length;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">{t("dashboard.title")}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<GenerateIcon />}
|
||||
onClick={() => setDemoDialog("generate")}
|
||||
>
|
||||
{t("dashboard.generateDemoData")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<ClearIcon />}
|
||||
onClick={() => setDemoDialog("clear")}
|
||||
>
|
||||
{t("dashboard.clearDatabase")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.totalEvents")}
|
||||
value={eventi.length}
|
||||
icon={<EventIcon sx={{ fontSize: 48 }} />}
|
||||
color="#1976d2"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.confirmed")}
|
||||
value={eventiConfermati}
|
||||
icon={<ConfirmedIcon sx={{ fontSize: 48 }} />}
|
||||
color="#4caf50"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.inQuote")}
|
||||
value={eventiPreventivo}
|
||||
icon={<PendingIcon sx={{ fontSize: 48 }} />}
|
||||
color="#ff9800"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title={t("dashboard.eventsToday")}
|
||||
value={eventiOggi}
|
||||
icon={<PeopleIcon sx={{ fontSize: 48 }} />}
|
||||
color="#9c27b0"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Dashboard
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Grid size={12}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("dashboard.upcomingEvents")}
|
||||
<Typography variant="h6">Welcome to Zentral</Typography>
|
||||
<Typography variant="body1">
|
||||
Select a module from the menu to get started.
|
||||
</Typography>
|
||||
<List>
|
||||
{eventiProssimi.slice(0, 10).map((evento) => (
|
||||
<ListItem
|
||||
key={evento.id}
|
||||
component="div"
|
||||
onClick={() => navigate(`/eventi/${evento.id}`)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={evento.descrizione || evento.codice}
|
||||
secondary={
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ display: "flex", gap: 1, alignItems: "center" }}
|
||||
>
|
||||
<span>
|
||||
{dayjs(evento.dataEvento).format("DD/MM/YYYY")}
|
||||
</span>
|
||||
<span>-</span>
|
||||
<span>{evento.cliente?.ragioneSociale}</span>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label={getStatoLabel(evento.stato, t)}
|
||||
color={getStatoColor(evento.stato) as any}
|
||||
size="small"
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{eventiProssimi.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText primary={t("dashboard.noEvents")} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("dashboard.expiringQuotes")}
|
||||
</Typography>
|
||||
<List>
|
||||
{eventi
|
||||
.filter(
|
||||
(e) =>
|
||||
e.stato === StatoEvento.Preventivo &&
|
||||
e.dataScadenzaPreventivo,
|
||||
)
|
||||
.sort((a, b) =>
|
||||
dayjs(a.dataScadenzaPreventivo).diff(
|
||||
dayjs(b.dataScadenzaPreventivo),
|
||||
),
|
||||
)
|
||||
.slice(0, 5)
|
||||
.map((evento) => (
|
||||
<ListItem
|
||||
key={evento.id}
|
||||
component="div"
|
||||
onClick={() => navigate(`/eventi/${evento.id}`)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={evento.descrizione || evento.codice}
|
||||
secondary={t("dashboard.expires", { date: dayjs(evento.dataScadenzaPreventivo).format("DD/MM/YYYY") })}
|
||||
/>
|
||||
<Chip
|
||||
label={t("dashboard.guests", { count: evento.numeroOspiti || 0 })}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{eventi.filter((e) => e.stato === StatoEvento.Preventivo)
|
||||
.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText primary={t("dashboard.noQuotes")} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Dialog Genera Dati Demo */}
|
||||
<Dialog open={demoDialog === "generate"} onClose={handleCloseDialog}>
|
||||
<DialogTitle>{t("dashboard.generateDialogTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{!result && !error && (
|
||||
<DialogContentText>
|
||||
<Trans i18nKey="dashboard.generateDialogText" components={{ br: <br /> }} />
|
||||
</DialogContentText>
|
||||
)}
|
||||
{loading && (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{result && (
|
||||
<Alert severity="success" sx={{ mt: 1 }}>
|
||||
{result.message}
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
{result ? t("common.close") : t("common.cancel")}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
onClick={handleGenerateDemo}
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("common.generate")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog Pulisci Database */}
|
||||
<Dialog open={demoDialog === "clear"} onClose={handleCloseDialog}>
|
||||
<DialogTitle>{t("dashboard.clearDialogTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{!result && !error && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
{t("dashboard.clearDialogWarning")}
|
||||
</Alert>
|
||||
)}
|
||||
{!result && !error && (
|
||||
<DialogContentText>
|
||||
<Trans i18nKey="dashboard.clearDialogText" components={{ br: <br /> }} />
|
||||
</DialogContentText>
|
||||
)}
|
||||
{loading && (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{result && (
|
||||
<Alert severity="success" sx={{ mt: 1 }}>
|
||||
{t("dashboard.clearSuccess", {
|
||||
events: result.eventiCreati,
|
||||
clients: result.clientiCreati,
|
||||
locations: result.locationCreate,
|
||||
resources: result.risorseCreate,
|
||||
articles: result.articoliCreati
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
{result ? t("common.close") : t("common.cancel")}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
onClick={handleClearDemo}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("common.deleteAll")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Grid,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { risorseService, lookupService } from '../services/lookupService';
|
||||
import { Risorsa } from '../types';
|
||||
|
||||
export default function RisorsePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Risorsa>>({ attivo: true });
|
||||
|
||||
const { data: risorse = [], isLoading } = useQuery({
|
||||
queryKey: ['risorse'],
|
||||
queryFn: () => risorseService.getAll(),
|
||||
});
|
||||
|
||||
const { data: tipiRisorsa = [] } = useQuery({
|
||||
queryKey: ['lookup', 'tipi-risorsa'],
|
||||
queryFn: () => lookupService.getTipiRisorsa(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Risorsa>) => risorseService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['risorse'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Risorsa> }) => risorseService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['risorse'] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => risorseService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['risorse'] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenDialog(false);
|
||||
setEditingId(null);
|
||||
setFormData({ attivo: true });
|
||||
};
|
||||
|
||||
const handleEdit = (risorsa: Risorsa) => {
|
||||
setFormData(risorsa);
|
||||
setEditingId(risorsa.id);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'nome', headerName: t('resources.name'), width: 150 },
|
||||
{ field: 'cognome', headerName: t('resources.surname'), width: 150 },
|
||||
{
|
||||
field: 'tipoRisorsa',
|
||||
headerName: t('resources.type'),
|
||||
width: 150,
|
||||
valueGetter: (value: any) => value?.descrizione || '',
|
||||
},
|
||||
{ field: 'telefono', headerName: t('resources.phone'), width: 130 },
|
||||
{ field: 'email', headerName: t('resources.email'), flex: 1, minWidth: 200 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: t('common.actions'),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => handleEdit(params.row)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm(t('resources.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">{t('resources.title')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
{t('resources.newResource')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={risorse}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{editingId ? t('resources.editResource') : t('resources.newResource')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label={t('resources.name')}
|
||||
fullWidth
|
||||
required
|
||||
value={formData.nome || ''}
|
||||
onChange={(e) => setFormData({ ...formData, nome: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label={t('resources.surname')}
|
||||
fullWidth
|
||||
value={formData.cognome || ''}
|
||||
onChange={(e) => setFormData({ ...formData, cognome: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('resources.resourceType')}</InputLabel>
|
||||
<Select
|
||||
value={formData.tipoRisorsaId || ''}
|
||||
label={t('resources.resourceType')}
|
||||
onChange={(e) => setFormData({ ...formData, tipoRisorsaId: e.target.value as number })}
|
||||
>
|
||||
{tipiRisorsa.map((t) => (
|
||||
<MenuItem key={t.id} value={t.id}>{t.descrizione}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label={t('resources.phone')}
|
||||
fullWidth
|
||||
value={formData.telefono || ''}
|
||||
onChange={(e) => setFormData({ ...formData, telefono: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label={t('resources.email')}
|
||||
fullWidth
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label={t('common.notes')}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={formData.note || ''}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user