◆ GestoreTurni

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.

Fabio De Giovine · 10 min lettura ·
PHP 8.2 MySQL Vanilla JS PHPMailer CSS Grid SMTP Aruba
▶ Demo live
3
Ruoli Utente
7
Viste Calendario
Dipendenti
0
Dipendenze NPM

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: pendingapproved/rejectedcompleted. 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:

  1. Dipendente A propone lo scambio e seleziona il turno che vuole cedere e quello che vuole ricevere
  2. Dipendente B accetta o rifiuta la proposta
  3. 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.

Lezione appresa

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.