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.
|
- Implementazione modulo Gestione Eventi: strutturazione frontend, integrazione funzionalità e attivazione store.
|
||||||
- [Event Module Development](./devlog/event-module.md) - Implementazione modulo eventi
|
- [Event Module Development](./devlog/event-module.md) - Implementazione modulo eventi
|
||||||
- [Menu Refactoring](./devlog/menu-refactoring.md) - Riorganizzazione menu e moduli (Dashboard, Clienti, Articoli, Risorse)
|
- [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>
|
/// </summary>
|
||||||
public async Task SeedDefaultModulesAsync()
|
public async Task SeedDefaultModulesAsync()
|
||||||
{
|
{
|
||||||
if (await _context.AppModules.AnyAsync())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var defaultModules = new List<AppModule>
|
var defaultModules = new List<AppModule>
|
||||||
{
|
{
|
||||||
new AppModule
|
new AppModule
|
||||||
@@ -482,12 +479,46 @@ public class ModuleService
|
|||||||
RoutePath = "/quality",
|
RoutePath = "/quality",
|
||||||
IsAvailable = true,
|
IsAvailable = true,
|
||||||
CreatedAt = DateTime.UtcNow
|
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);
|
var existingCodes = await _context.AppModules.Select(m => m.Code).ToListAsync();
|
||||||
await _context.SaveChangesAsync();
|
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.Purchases;
|
||||||
using Zentral.Domain.Entities.Sales;
|
using Zentral.Domain.Entities.Sales;
|
||||||
using Zentral.Domain.Entities.Production;
|
using Zentral.Domain.Entities.Production;
|
||||||
|
using Zentral.Domain.Entities.HR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Zentral.Infrastructure.Data;
|
namespace Zentral.Infrastructure.Data;
|
||||||
@@ -86,10 +87,70 @@ public class ZentralDbContext : DbContext
|
|||||||
public DbSet<ProductionOrderPhase> ProductionOrderPhases => Set<ProductionOrderPhase>();
|
public DbSet<ProductionOrderPhase> ProductionOrderPhases => Set<ProductionOrderPhase>();
|
||||||
public DbSet<MrpSuggestion> MrpSuggestions => Set<MrpSuggestion>();
|
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)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(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
|
// Cliente
|
||||||
modelBuilder.Entity<Cliente>(entity =>
|
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");
|
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 =>
|
modelBuilder.Entity("Zentral.Domain.Entities.Location", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -3697,6 +3945,50 @@ namespace Zentral.Infrastructure.Migrations
|
|||||||
b.Navigation("Risorsa");
|
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 =>
|
modelBuilder.Entity("Zentral.Domain.Entities.ModuleSubscription", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Zentral.Domain.Entities.AppModule", "Module")
|
b.HasOne("Zentral.Domain.Entities.AppModule", "Module")
|
||||||
@@ -4219,6 +4511,17 @@ namespace Zentral.Infrastructure.Migrations
|
|||||||
b.Navigation("DettagliRisorse");
|
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 =>
|
modelBuilder.Entity("Zentral.Domain.Entities.Location", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Eventi");
|
b.Navigation("Eventi");
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
"purchases": "Purchases",
|
"purchases": "Purchases",
|
||||||
"sales": "Sales",
|
"sales": "Sales",
|
||||||
"production": "Production",
|
"production": "Production",
|
||||||
|
"hr": "Human Resources",
|
||||||
"reports": "Reports",
|
"reports": "Reports",
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"autoCodes": "Auto Codes",
|
"autoCodes": "Auto Codes",
|
||||||
@@ -264,6 +265,14 @@
|
|||||||
"stock": "Stock",
|
"stock": "Stock",
|
||||||
"categories": "Categories"
|
"categories": "Categories"
|
||||||
},
|
},
|
||||||
|
"hr": {
|
||||||
|
"title": "Human Resources",
|
||||||
|
"dipendenti": "Employees",
|
||||||
|
"contratti": "Contracts",
|
||||||
|
"assenze": "Absences",
|
||||||
|
"pagamenti": "Payments",
|
||||||
|
"rimborsi": "Reimbursements"
|
||||||
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "Module Management",
|
"title": "Module Management",
|
||||||
"subtitle": "Configure active modules and manage subscriptions",
|
"subtitle": "Configure active modules and manage subscriptions",
|
||||||
@@ -356,6 +365,14 @@
|
|||||||
"4": "Certifications and audits",
|
"4": "Certifications and audits",
|
||||||
"5": "Quality statistics"
|
"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"
|
"default": "Complete module features"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1337,5 +1354,45 @@
|
|||||||
"process": "Process / Create Order"
|
"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",
|
"purchases": "Acquisti",
|
||||||
"sales": "Vendite",
|
"sales": "Vendite",
|
||||||
"production": "Produzione",
|
"production": "Produzione",
|
||||||
|
"hr": "Gestione Personale",
|
||||||
"reports": "Report",
|
"reports": "Report",
|
||||||
"modules": "Moduli",
|
"modules": "Moduli",
|
||||||
"autoCodes": "Codici Auto",
|
"autoCodes": "Codici Auto",
|
||||||
@@ -260,6 +261,14 @@
|
|||||||
"stock": "Giacenze",
|
"stock": "Giacenze",
|
||||||
"categories": "Categorie"
|
"categories": "Categorie"
|
||||||
},
|
},
|
||||||
|
"hr": {
|
||||||
|
"title": "Gestione Personale",
|
||||||
|
"dipendenti": "Dipendenti",
|
||||||
|
"contratti": "Contratti",
|
||||||
|
"assenze": "Assenze",
|
||||||
|
"pagamenti": "Pagamenti",
|
||||||
|
"rimborsi": "Rimborsi"
|
||||||
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "Gestione Moduli",
|
"title": "Gestione Moduli",
|
||||||
"subtitle": "Configura i moduli attivi e gestisci le subscription",
|
"subtitle": "Configura i moduli attivi e gestisci le subscription",
|
||||||
@@ -353,6 +362,14 @@
|
|||||||
"4": "Certificazioni e audit",
|
"4": "Certificazioni e audit",
|
||||||
"5": "Statistiche qualità"
|
"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"
|
"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 Layout from "./components/Layout";
|
||||||
import Dashboard from "./pages/Dashboard";
|
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 ReportTemplatesPage from "./pages/ReportTemplatesPage";
|
||||||
import ReportEditorPage from "./pages/ReportEditorPage";
|
import ReportEditorPage from "./pages/ReportEditorPage";
|
||||||
import ModulesAdminPage from "./pages/ModulesAdminPage";
|
import ModulesAdminPage from "./pages/ModulesAdminPage";
|
||||||
@@ -24,6 +18,8 @@ import WarehouseRoutes from "./modules/warehouse/routes";
|
|||||||
import PurchasesRoutes from "./modules/purchases/routes";
|
import PurchasesRoutes from "./modules/purchases/routes";
|
||||||
import SalesRoutes from "./modules/sales/routes";
|
import SalesRoutes from "./modules/sales/routes";
|
||||||
import ProductionRoutes from "./modules/production/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 { ModuleGuard } from "./components/ModuleGuard";
|
||||||
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
||||||
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
||||||
@@ -62,13 +58,7 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Layout />}>
|
<Route path="/" element={<Layout />}>
|
||||||
<Route index element={<Dashboard />} />
|
<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
|
<Route
|
||||||
path="report-templates"
|
path="report-templates"
|
||||||
element={<ReportTemplatesPage />}
|
element={<ReportTemplatesPage />}
|
||||||
@@ -131,6 +121,24 @@ function App() {
|
|||||||
</ModuleGuard>
|
</ModuleGuard>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* Events Module */}
|
||||||
|
<Route
|
||||||
|
path="events/*"
|
||||||
|
element={
|
||||||
|
<ModuleGuard moduleCode="events">
|
||||||
|
<EventsRoutes />
|
||||||
|
</ModuleGuard>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* HR Module */}
|
||||||
|
<Route
|
||||||
|
path="hr/*"
|
||||||
|
element={
|
||||||
|
<ModuleGuard moduleCode="hr">
|
||||||
|
<HRRoutes />
|
||||||
|
</ModuleGuard>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</TabProvider>
|
</TabProvider>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import {
|
|||||||
Timeline as TimelineIcon,
|
Timeline as TimelineIcon,
|
||||||
PrecisionManufacturing as ManufacturingIcon,
|
PrecisionManufacturing as ManufacturingIcon,
|
||||||
Category as CategoryIcon,
|
Category as CategoryIcon,
|
||||||
|
AttachMoney as AttachMoneyIcon,
|
||||||
|
Receipt as ReceiptIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
@@ -62,6 +64,8 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
|||||||
purchases: false,
|
purchases: false,
|
||||||
sales: false,
|
sales: false,
|
||||||
production: false,
|
production: false,
|
||||||
|
events: false,
|
||||||
|
hr: false,
|
||||||
admin: false,
|
admin: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,10 +89,7 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
|||||||
icon: <DashboardIcon />,
|
icon: <DashboardIcon />,
|
||||||
children: [
|
children: [
|
||||||
{ id: 'dashboard', label: t('menu.dashboard'), icon: <DashboardIcon />, path: '/' },
|
{ 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: '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: 'articles', label: t('menu.articles'), icon: <InventoryIcon />, path: '/articoli' },
|
||||||
{ id: 'resources', label: t('menu.resources'), icon: <PersonIcon />, path: '/risorse' },
|
{ 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: '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',
|
id: 'admin',
|
||||||
label: 'Amministrazione',
|
label: 'Amministrazione',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import interactionPlugin from "@fullcalendar/interaction";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { eventiService } from "../services/eventiService";
|
import { eventiService } from "../../../services/eventiService";
|
||||||
|
|
||||||
export default function CalendarioPage() {
|
export default function CalendarioPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -53,7 +53,7 @@ export default function CalendarioPage() {
|
|||||||
|
|
||||||
const handleCreateEvent = () => {
|
const handleCreateEvent = () => {
|
||||||
// Naviga alla pagina nuovo evento con la data preselezionata
|
// 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: "" });
|
setNewEventDialog({ open: false, date: "", formattedDate: "" });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export default function CalendarioPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEventClick = (info: any) => {
|
const handleEventClick = (info: any) => {
|
||||||
navigate(`/eventi/${info.event.id}`);
|
navigate(`/events/list/${info.event.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDatesSet = (info: any) => {
|
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';
|
} from '@mui/icons-material';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { eventiService } from '../services/eventiService';
|
import { eventiService } from '../../../services/eventiService';
|
||||||
import { lookupService } from '../services/lookupService';
|
import { lookupService } from '../../../services/lookupService';
|
||||||
import { Evento, StatoEvento } from '../types';
|
import { Evento, StatoEvento } from '../../../types';
|
||||||
|
|
||||||
const getStatoLabel = (stato: StatoEvento, t: any) => {
|
const getStatoLabel = (stato: StatoEvento, t: any) => {
|
||||||
switch (stato) {
|
switch (stato) {
|
||||||
@@ -150,10 +150,10 @@ export default function EventiPage() {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params) => (
|
renderCell: (params) => (
|
||||||
<Box>
|
<Box>
|
||||||
<IconButton size="small" onClick={() => navigate(`/eventi/${params.row.id}`)}>
|
<IconButton size="small" onClick={() => navigate(`/events/list/${params.row.id}`)}>
|
||||||
<ViewIcon />
|
<ViewIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" onClick={() => navigate(`/eventi/${params.row.id}`)}>
|
<IconButton size="small" onClick={() => navigate(`/events/list/${params.row.id}`)}>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" onClick={() => duplicaMutation.mutate(params.row.id)}>
|
<IconButton size="small" onClick={() => duplicaMutation.mutate(params.row.id)}>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { signalRService } from "../services/signalr";
|
import { signalRService } from "../../../services/signalr";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -42,16 +42,16 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { eventiService } from "../services/eventiService";
|
import { eventiService } from "../../../services/eventiService";
|
||||||
import { lookupService } from "../services/lookupService";
|
import { lookupService } from "../../../services/lookupService";
|
||||||
import EventoCostiPanel from "../components/EventoCostiPanel";
|
import EventoCostiPanel from "../../../components/EventoCostiPanel";
|
||||||
import {
|
import {
|
||||||
Evento,
|
Evento,
|
||||||
StatoEvento,
|
StatoEvento,
|
||||||
EventoDettaglioOspiti,
|
EventoDettaglioOspiti,
|
||||||
EventoDettaglioPrelievo,
|
EventoDettaglioPrelievo,
|
||||||
EventoDettaglioRisorsa,
|
EventoDettaglioRisorsa,
|
||||||
} from "../types";
|
} from "../../../types";
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -147,7 +147,7 @@ export default function EventoDetailPage() {
|
|||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: Partial<Evento>) => eventiService.create(data),
|
mutationFn: (data: Partial<Evento>) => eventiService.create(data),
|
||||||
onSuccess: (newEvento) => {
|
onSuccess: (newEvento) => {
|
||||||
navigate(`/eventi/${newEvento.id}`);
|
navigate(`/events/list/${newEvento.id}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ export default function EventoDetailPage() {
|
|||||||
const duplicaMutation = useMutation({
|
const duplicaMutation = useMutation({
|
||||||
mutationFn: () => eventiService.duplica(eventoId),
|
mutationFn: () => eventiService.duplica(eventoId),
|
||||||
onSuccess: (newEvento) => {
|
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 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => navigate("/eventi")}
|
onClick={() => navigate("/events/list")}
|
||||||
sx={{ color: statoInfo.textColor }}
|
sx={{ color: statoInfo.textColor }}
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { locationService } from '../services/lookupService';
|
import { locationService } from '../../../services/lookupService';
|
||||||
import { Location } from '../types';
|
import { Location } from '../../../types';
|
||||||
|
|
||||||
export default function LocationPage() {
|
export default function LocationPage() {
|
||||||
const queryClient = useQueryClient();
|
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 { Box, Typography, Grid, Paper } from '@mui/material';
|
||||||
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() {
|
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 (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box
|
<Typography variant="h4" gutterBottom>
|
||||||
sx={{
|
Dashboard
|
||||||
display: "flex",
|
</Typography>
|
||||||
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 container spacing={3}>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={12}>
|
||||||
<Paper sx={{ p: 2 }}>
|
<Paper sx={{ p: 2 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6">Welcome to Zentral</Typography>
|
||||||
{t("dashboard.upcomingEvents")}
|
<Typography variant="body1">
|
||||||
|
Select a module from the menu to get started.
|
||||||
</Typography>
|
</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>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
</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>
|
</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