Gestione turni in PHP: da foglio Excel a web app con notifiche email
Come ho progettato e costruito GestoreTurni: un'app web per la pianificazione turni, gestione ferie e scambi tra dipendenti con notifiche email automatiche.
Il problema: 47 dipendenti gestiti con un foglio Excel condiviso
Il cliente aveva un foglio Excel su Google Drive per gestire i turni di 47 dipendenti. Il manager compilava le celle a mano, i dipendenti controllavano "quando potevano", le richieste di ferie arrivavano via WhatsApp, e gli scambi turno erano gestiti a voce. Risultato: errori costanti, turni scoperti, conflitti tra dipendenti.
La richiesta: un sistema web dove il manager assegna i turni, i dipendenti vedono il loro calendario, e le richieste di ferie/scambi passano per un flusso di approvazione con notifiche email.
Architettura: 3 ruoli, 1 codebase
Il sistema ha 3 livelli di accesso con un unico punto di autenticazione:
- Admin — Gestisce dipendenti, crea pattern turni, approva ferie/scambi, vede report ore lavorate
- Manager — Assegna turni al calendario, visualizza copertura settimanale, approva scambi del suo reparto
- Dipendente — Vede i propri turni, richiede ferie, propone scambi, consulta il monte ore
// Middleware di autorizzazione per ruolo
function requireRole($minRole) {
$roles = ['dipendente' => 1, 'manager' => 2, 'admin' => 3];
$userRole = $_SESSION['user_role'] ?? 'dipendente';
if (($roles[$userRole] ?? 0) < ($roles[$minRole] ?? 0)) {
http_response_code(403);
include __DIR__ . '/views/403.php';
exit;
}
}
// Utilizzo nelle pagine
requireRole('manager'); // solo manager e admin
requireRole('admin'); // solo admin
Il calendario: CSS Grid, non librerie
Il calendario turni è costruito con CSS Grid puro — nessun FullCalendar.js, nessuna libreria. 7 colonne per i giorni, righe dinamiche per i dipendenti. Ogni cella è un div cliccabile che apre un modale di assegnazione.
/* Griglia calendario turni settimanale */
.shift-grid {
display: grid;
grid-template-columns: 180px repeat(7, 1fr);
gap: 1px;
background: rgba(27,86,89,0.06);
border-radius: 12px;
overflow: hidden;
}
.shift-cell {
background: white;
padding: 12px;
min-height: 60px;
cursor: pointer;
transition: background 0.15s;
}
.shift-cell:hover { background: rgba(255,92,0,0.04); }
.shift-cell.assigned { border-left: 3px solid var(--orange); }
.shift-cell.day-off { background: rgba(0,229,200,0.04); }
La scelta di non usare librerie per il calendario è stata intenzionale. FullCalendar.js pesa 200KB+ minificato. Il mio calendario CSS Grid pesa 2KB di CSS e 4KB di JS. Su mobile la differenza di performance è enorme.
Il flusso richieste ferie: stato-macchina semplice
Ogni richiesta di ferie passa per 4 stati: pending → approved/rejected → completed. Il passaggio di stato trigga una email automatica al dipendente.
// Stato-macchina per le richieste ferie
function updateLeaveRequest($requestId, $newStatus, $adminNote = '') {
global $conn;
$validTransitions = [
'pending' => ['approved', 'rejected'],
'approved' => ['completed', 'cancelled'],
'rejected' => [], // stato finale
];
// Verifica transizione valida
$current = getRequestStatus($requestId);
if (!in_array($newStatus, $validTransitions[$current] ?? [])) {
return ['error' => "Transizione non valida: $current → $newStatus"];
}
$stmt = $conn->prepare("UPDATE leave_requests
SET status = ?, admin_note = ?, updated_at = NOW()
WHERE id = ?");
$stmt->bind_param("ssi", $newStatus, $adminNote, $requestId);
$stmt->execute();
// Notifica email al dipendente
$request = getRequestById($requestId);
sendLeaveStatusEmail($request, $newStatus, $adminNote);
return ['success' => true];
}
Scambi turno: il protocollo a 3 step
Lo scambio turno tra dipendenti è il flusso più complesso. Richiede 3 conferme:
- Dipendente A propone lo scambio e seleziona il turno che vuole cedere e quello che vuole ricevere
- Dipendente B accetta o rifiuta la proposta
- Manager approva o rifiuta lo scambio (verifica copertura minima)
Solo al terzo step il calendario viene effettivamente aggiornato. Se uno qualsiasi dei 3 rifiuta, lo scambio si annulla.
Il bug più insidioso: quando il Dipendente A proponeva uno scambio e poi il Manager approvava una richiesta di ferie per lo stesso giorno del Dipendente B, lo scambio andava in conflitto. Fix: prima di approvare ferie, il sistema controlla se ci sono scambi pendenti per gli stessi giorni e li segnala.
Demo isolation: sessioni separate per utenti demo
Il progetto è pubblicato come demo live su dgdesigned.it. Il problema: non posso permettere che i visitatori demo modifichino dati reali o vedano dati di altri visitatori demo. La soluzione: sessioni demo isolate.
// autologin.php — crea sessione demo isolata
session_start();
// Genera ID demo unico
$demoId = 'demo_' . bin2hex(random_bytes(8));
$_SESSION['user_id'] = $demoId;
$_SESSION['user_role'] = 'admin'; // l'utente demo vede tutto
$_SESSION['is_demo'] = true;
$_SESSION['demo_start'] = time();
// Seed dati demo per questa sessione
seedDemoData($demoId);
header('Location: dashboard.php');
exit;
Ogni sessione demo riceve un set di dati fittizi (dipendenti, turni, richieste ferie) generati al volo. Quando la sessione scade o l'utente esce, i dati vengono cancellati. Zero interferenze tra visitatori.
Il risultato
GestoreTurni gestisce turni settimanali e mensili, ferie con approvazione, scambi tra dipendenti, calcolo automatico ore lavorate e report mensili. Tutto in PHP nativo, con un calendario CSS Grid da 2KB e zero dipendenze JavaScript esterne. Il manager che prima passava 3 ore a settimana su Excel ora fa tutto in 20 minuti.