Passa al contenuto principale

Calcola Turno

Screen 1Screen 1
Screen 2Screen 2
Screen 3Screen 3
Lancia App

Calcola Turno

Apri Web App ↗

Funziona offline su tutti i dispositivi

Il Progetto

Calcola Turno è una Progressive Web App (PWA) che risolve un problema concreto per gli operatori di fabbrica: sapere in anticipo quale turno faranno (mattino, pomeriggio o notte) in qualsiasi giorno futuro.
Pensata per essere installata come app nativa, funziona completamente offline e calcola i turni in modo automatico senza richiedere aggiornamenti manuali o connessione.

Codice Sorgente

<!-- DESIGN 
------
* This HTML is characterized by:
* - A Single Page Application (SPA) structure managed via visibility states
* (hidden class) rather than multi-page navigation, to ensure instant
* transitions typical of native apps
* - Native System Fonts stack (Segoe UI, San Francisco) instead of external
* imports to maximize load performance and blend in with the OS UI (Metro/iOS)
* - A strict PWA (Progressive Web App) configuration in the <head>,
* specifically targeting iOS quirks (meta apple-mobile-web-app) which
* often ignores standard manifest declarations.
-->

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">

<!--
* I set maximum-scale=1.0 and user-scalable=no to prevent the browser
* from zooming in when double-tapping buttons, mimicking the
* non-scalable interface of a native mobile application
-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<title>Calcola Turno</title>

<link rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.json">

<!--
* Dynamic theme-color: allows the browser status bar to adapt
* automatically to the user's system preference (Light/Dark mode),
* maintaining visual consistency
-->
<meta name="theme-color" content="#0078D4" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">

<!-- --- iOS SPECIFIC CONFIGURATION --- -->

<!--
* iOS Safari requires specific link tags for the home screen icon
* and meta tags to hide the URL bar (standalone mode), as it does
* not fully support the web app manifest for these features yet
-->
<link rel="apple-touch-icon" href="icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Turni">

<!-- * Fallback icon for desktop browser tabs -->
<link rel="icon" type="image/png" href="icon.png">

</head>
<body>

<!--
* This wrapper simulates the device frame during development/desktop view,
* but acts as a transparent container in mobile view thanks to
* CSS media queries
-->
<div class="device-frame">
<div class="device-screen">
<div class="container">

<!-- VIEW 1: ONBOARDING -->
<div id="onboarding-view">
<h1>Calcola turno</h1>
<div class="section">
<div class="section-title">che turno fai questa settimana?</div>

<!--
* I used a flex/grid container for buttons to handle
* responsive alignment. The buttons utilize data-attributes
* for logic to keep the DOM clean from styling classes
-->
<div class="onboarding-buttons">

<!--
* Grid Stack Technique: inside the button, span and loader
* occupy the same grid area to allow seamless transition
* between text and loading spinner without layout shifts
-->
<button class="group-btn" data-select-shift="Mattino">
<span class="btn-text">Mattino</span>
<div class="loader"></div>
</button>

<button class="group-btn" data-select-shift="Pomeriggio">
<span class="btn-text">Pomeriggio</span>
<div class="loader"></div>
</button>

<button class="group-btn" data-select-shift="Notte">
<span class="btn-text">Notte</span>
<div class="loader"></div>
</button>

</div>
</div>
</div>

<!-- VIEW 2: MAIN APPLICATION
Hidden by default, toggled via JavaScript upon initialization
-->
<div id="main-view" class="hidden">
<h1>Calcola turno</h1>

<!-- Information Card Component -->
<div class="section">
<div class="current-week-card">
<div class="card-label">Questa settimana sei di</div>
<div class="card-value" id="week-shift-display">-</div>
</div>
</div>

<!--
* Calendar Component
* Divided into Controls (Nav), Header (Weekdays) and Grid (Days)
-->
<div class="section">
<div class="calendar-controls">
<div class="calendar-header">
<button class="nav-btn" id="prev-month" aria-label="Mese precedente"></button>

<!--
* Hybrid Input Wrapper: overlays a native transparent
* date input over a custom text div. This forces mobile
* browsers to open their native date scrollers (wheel),
* improving UX over a custom-built JS picker
-->
<div class="month-picker-wrapper">
<div class="month-year" id="month-year"></div>
<input type="month" id="native-month-picker" aria-label="Seleziona mese">
</div>

<button class="nav-btn" id="next-month" aria-label="Mese successivo"></button>
</div>

<button id="today-btn" class="today-btn">Torna a oggi</button>
</div>

<div class="weekdays">
<div class="weekday">lun</div>
<div class="weekday">mar</div>
<div class="weekday">mer</div>
<div class="weekday">gio</div>
<div class="weekday">ven</div>
<div class="weekday">sab</div>
<div class="weekday">dom</div>
</div>

<!-- Grid container populated dynamically by JavaScript -->
<div class="days" id="calendar-days"></div>
</div>

<!-- Result Feedback Area -->
<div class="result" id="result">
<div class="result-date" id="result-date">oggi</div>
<div class="result-shift" id="result-shift">-</div>
</div>

<div class="footer-actions">
<button id="reset-btn" class="text-btn">Non è corretto? Reimposta</button>
</div>
</div>

</div>
</div>
</div>

<!--
* Install Banner Component
* Positioned fixed at the bottom, separate from the main flow
* Visibility is managed by localStorage and device type detection in JS
-->
<div id="install-banner" class="install-banner hidden">
<div class="install-content">
<img src="icon.png" alt="Icona" class="install-icon">
<div class="install-text">
<span class="install-title">Installa l'App</span>
<span class="install-desc" id="install-instructions">Per un'esperienza migliore</span>
</div>
<button id="install-btn-action" class="install-action-btn">Installa</button>
<button id="close-install" class="install-close" aria-label="Chiudi banner"></button>
</div>
</div>

<div id="update-toast" class="update-toast hidden">
<div class="update-content">
<span class="update-text">È disponibile un aggiornamento</span>
<button id="refresh-btn" class="update-action-btn">AGGIORNA</button>
</div>
</div>

<script src="script.js"></script>
</body>
</html>

Seconda versione

Questa è la seconda versione di Calcola Turno. La prima l'avevo sviluppata un anno e mezzo fa, molto prima di iniziare questo percorso, usando esclusivamente l'AI in modalità "Vibe Coding" (termine che "all'epoca" non esisteva ancora) senza capire realmente cosa stesse accadendo sotto il cofano. Impiegai diversi giorni, non ricordo con esattezza quanti, ma non avevo alcuna competenza, solo la voglia di realizzarla a qualunque costo.
Aveva funzionato: l'app si è diffusa in fabbrica e ha risolto un problema concreto per chi lavora sui 3 turni.
Questa volta è diverso. Ho ricostruito l'app da zero in 5 ore totali (2 ore per il core funzionante + 3 ore per rifinitura e responsive), applicando tutto ciò che ho imparato in questi mesi.

Calcola Turno Versione 1.0

La versione 1.0 (2024)

Perché Rifarla?

La sera prima di rientrare al lavoro ero in ansia. Non sapevo se sarei rimasto nello stesso reparto, e il solo pensiero di non poter continuare a imparare e dedicarmi ai progetti mi "uccideva". Ho placato quell'ansia facendo l'unica cosa che amo fare: costruendo qualcosa. Ho ripreso l'idea di Calcola Turno, ma questa volta con una consapevolezza completamente diversa. Non stavo solo "facendo funzionare" un'app con l'aiuto dell'AI. Stavo orchestrando l'AI, dicendole esattamente cosa serviva, chiedendo conferma su ogni scelta, verificando che non mi stessi perdendo qualcosa di importante.

Il Design System: Windows Phone 8 Rivisitato

Ho usato il design system creato per Telephone Number Validator, fortemente ispirato a Windows Phone 8, ma con una scelta particolare: nessun primary color nel senso tradizionale.
Dato che è un'app destinata agli operatori della fabbrica in cui lavoro, sapevo che l'estetica Windows sarebbe stata familiare: tutti i computer aziendali usano Windows, quindi il linguaggio visivo Metro/Modern UI risulta immediato e riconoscibile per loro.

Volevo che l'utente avesse solo 3 colori in testa quando usava l'app:

  • Azzurro = Mattino
  • Giallo = Pomeriggio
  • Viola = Notte

È un'applicazione colorata, ma con colori primari neutri. Paradossale, ma funziona: i colori qui non fungono da decorazione, bensì da informazione diretta.

Il Design dell'Icona: Dal Calendario ai Tre Turni

Ero ancorato mentalmente all'idea del calendario con all'interno un orologio che caratterizzava la primissima versione.
Volevo qualcosa di diverso e guardando le altre applicazioni sul mio telefono, mi sono reso conto di una cosa: le icone migliori sono quelle con semplicità assoluta. Niente sovrapposizioni complesse, niente dettagli che si perdono a 60×60 pixel.

Ho realizzato diverse versioni su Figma, iterando continuamente: un calendario realizzato con 3 pezzi di puzzle che rappresentavano i 3 turni, poi un orologio con una freccia che ruotava attorno ad esso. Nessuna mi convinceva del tutto.
Ho riaperto l'app e ho trovato la risposta proprio davanti a me: i 3 rettangoli dell'onboarding, quelli che rappresentano i 3 turni (Mattino, Pomeriggio, Notte).

Così ho garantito tre cose fondamentali:

  • Coerenza visiva con l'interfaccia interna dell'app
  • Riconoscibilità immediata da parte degli utenti
  • Pulizia visiva assoluta, senza elementi ridondanti

L'icona della primissima versione era completamente diversa, era infatti più dettagliata, ma (forse) meno efficace. Questa volta ho scelto l'essenziale. D'altronde l'app promette di fare una cosa sola: dirti quale dei 3 turni farai.

Vecchia Icona 2024

2024

Nuova Icona 2025

2025

Architettura: Offline-First e Gestione della Cache

Fin dall'inizio ho ragionato sull'architettura e su cosa avrei avuto bisogno per il backend, in particolare mi riferisco al localStorage e alla gestione della cache. Volevo che funzionasse offline e ricordasse il turno scelto dall'utente nell'onboarding. Volevo che fosse funzionante la sera stessa, quindi ho orchestrato l'AI dicendole le mie idee e chiedendo conferma se mi stessi perdendo qualcosa o se ci fossero modi migliori per giungere alla soluzione.

Il problema della mezzanotte
Un punto interessante è stato l'algoritmo per cambiare automaticamente il giorno del calendario a mezzanotte esatta.
Inizialmente pensavo a una soluzione che credevo elegante: un Timer che calcolasse esattamente quanto tempo mancava alla mezzanotte. All'apertura dell'app, JavaScript avrebbe controllato quanto tempo sarebbe mancato prima della mezzanotte, regolandosi di conseguenza per il cambio del giorno.

Il problema? Mi sarei scontrato con la gestione di iOS e Android che congelano i browser in background per risparmiare batteria. Il timer si sarebbe fermato.
Ho optato per una soluzione veramente semplice e banale: un Interval di 60 secondi. Ogni 60 secondi JavaScript controlla, per dirla semplice, se oggi != ieri (oggi è diverso da ieri).

Mi premeva l'impatto sulla batteria, che ho scoperto essere bassissimo. Questo perché la CPU si sveglia per un micro-istante, controlla un numero, vede che non è cambiato nulla e torna "a dormire". È molto più costoso per la batteria gestire il rendering grafico o la connessione alla rete.
L'idea del "Timer preciso alla mezzanotte" avrebbe quindi rischiato di creare bug in cui l'app non si aggiorna se il telefono va in standby, mentre il risparmio energetico sarebbe talmente piccolo da non essere misurabile.

Ebbene, in 2 ore avevo l'app funzionante. Ho dedicato altre 3 ore il giorno successivo per rifinire i dettagli e disegnare l'esperienza desktop e landscape negli smartphone.

Layout Responsive: Da Mobile a Dashboard

L'esperienza landscape trasforma l'app in una dashboard: i turni vengono disposti orizzontalmente, sfruttando al meglio lo spazio disponibile e offrendo una visione più ampia del calendario.
È stato un progetto divertentissimo. Mi sono fermato perché mi sarei perso nei dettagli.

Riflessioni: Empatia, Processo e Controllo

Non sono cinico, altrimenti non mirerei a fare questo lavoro, ma credo che alle persone non interessi sentire che sai fare questo o quell'altro, bensì sapere cosa sai fare nel concreto e, per essere ancora più specifici, cosa sai fare di concreto da cui loro possano trarre vantaggio.
Credo sia il medesimo tema del public speaking. Carlo Loiudice diceva (non ricordo se in uno dei suoi corsi oppure nei suoi podcast) che il pubblico è egoista: non gli interessa chi sei, cosa hai fatto e perché sei lì a parlare, bensì cosa si porta a casa di quello che dici.
Proprio per questo motivo sostengo che sia di vitale importanza che quello che fai ti piaccia non solo per i risultati che ottieni e i feedback altrui, bensì per il processo stesso.

La sensazione più bella infatti non è stata da parte dei colleghi che mi hanno fatto i complimenti e hanno aggiunto l'app alla home, bensì la sensazione che seppur usando l'AI molto più del solito, sapevo esattamente cosa stavamo facendo e perché.
È stato tutto ciò che mi ha permesso di fare un lavoro migliore, molto più divertente e didattico della versione precedente in 1/10 del tempo.

Cosa Ho Imparato

Architettura SPA senza framework:

  • Implementato pattern Single Page Application pura con switching di visibilità (.hidden) tra onboarding e vista principale, eliminando navigazione e ricariche pagina per un'esperienza "app nativa".
  • Gestione dello stato applicativo minimalista con localStorage per persistenza del gruppo utente e variabili globali (selectedDate, currentMonth) per lo stato UI corrente.

Algoritmo deterministico per turni ciclici:

  • Creato sistema di calcolo turni basato su epoch (5 gennaio 2025) e ciclo modulare ogni 3 settimane, evitando completamente database o API esterne.
  • Implementato getWeekNumber() con normalizzazione europea (lunedì come inizio settimana) e gestione edge case domenica.
  • Risolto bug DST (Daylight Saving Time) usando Math.round invece di Math.floor per compensare settimane di 167.9 o 168.1 ore durante cambio ora legale.
  • Pattern reverse lookup: deduzione automatica gruppo A/B/C dal turno corrente dell'utente, rendendo l'onboarding più intuitivo ("che turno fai oggi?" invece di "sei del gruppo A?").

PWA e offline-first architecture:

  • Service Worker con strategia "cache-first, network fallback" e precaching aggressivo di tutti gli asset critici.
  • Lifecycle gestito con skipWaiting() e clients.claim() per aggiornamenti immediati, ma con controllo UI lato utente (toast + reload manuale).
  • Manifest biforcato: gestione nativa beforeinstallprompt per Android/desktop e fallback con istruzioni manuali per iOS (che ignora parte del manifest standard).
  • Versioning manuale cache (turni-app-v10) con pulizia automatica vecchie versioni in fase di activate.

UX mobile "app-like":

  • Meta viewport con maximum-scale=1.0 e user-scalable=no per eliminare zoom involontario da doppio tap.
  • iOS-specific: apple-mobile-web-app-capable, apple-touch-icon, theme-color dinamico via media query per status bar coerente con tema.
  • Feedback tattile via classe .clicked con ritardi intenzionali (setTimeout) per non troncare animazioni durante interazioni veloci.
  • Input type="month" nativo invisibile sovrapposto a label custom: su mobile apre picker nativo (rotella iOS, spinner Android) invece di datepicker HTML custom meno performanti.

CSS utility-first con variabili responsive:

  • Design system centralizzato in :root per colori, spaziature, tipografia e dimensioni componenti, con override via media query (dark mode, desktop, tablet, landscape).
  • Layout Grid "stack technique" per sovrapporre testo e loader nei bottoni senza salti di layout durante stato loading.
  • Responsive avanzato: desktop (min-width: 950px) trasforma app in dashboard a due colonne usando display: contents per neutralizzare wrapper semantici e grid-template-areas per riorganizzare completamente il layout.
  • Animazioni CSS (@keyframes softPivot) retriggerate via JavaScript (trick: animation='none' → reflow → ripristino) per restart sincronizzato con cambio stato.

Navigazione da tastiera accessibile:

  • Modalità "day navigation" attivabile con ArrowDown: frecce navigano giorno per giorno (±1 o ±7), con auto-scorrimento mese quando si esce dai bordi.
  • Separazione chiara tra "navigazione mese" (ArrowLeft/Right cambiano mese) e "navigazione giorno" per evitare conflitti di input.
  • Gestione Enter, Tab, Escape per entrare/uscire da modalità giorno e tornare a oggi.

Gestione tempo real-time e lifecycle:

  • Auto-refresh a mezzanotte con controllo ogni 60s (setInterval) confrontando lastKnownDay con data corrente.
  • Listener visibilitychange per ricontrollare stato quando app torna in foreground dopo standby prolungato (previene bug "app rimasta aperta tutta notte").
  • Controllo DST-safe per evitare sfasamenti di un giorno durante cambio ora.

Performance e thermal management:

  • Zero font esterni, scrollbar nascosta, touch-action: manipulation, -webkit-tap-highlight-color: transparent per ridurre overhead rendering e migliorare sensazione tattile.
  • Uso strategico di pointer-events: none durante stati loading per prevenire doppi click e race condition.
  • Approccio "Silent Mac": interval a 60s ha impatto in termini di consumi difficilmente misurabile perché la CPU si sveglia solo per confrontare un numero, non per rendering o network.

Pattern UI avanzati:

  • Calendario con padding smart: giorni mese precedente/successivo (.other-month) disabilitati ma visibili per completare griglia 7 colonne senza buchi visivi.
  • Classi dinamiche basate su turno (.mattino, .pomeriggio, .notte) applicate sia a .today che a .selected per colorazione semantica immediata.
  • Animazione click con ritardo prima di cambio stato (setTimeout 150ms) per dare feedback visivo completo prima di triggerare rerender pesante.

Next:
La precedenza ora è imparare React. Ho iniziato oggi, grazie al fatto che mi sono portato parecchio avanti con l'università nell'ultimo mese. Ma la sfida ora non è imparare una nuova libreria, perché sono certo di riuscirci come sono riuscito ad imparare JavaScript, bensì gestire lavoro, università e questo percorso che vorrei fare a tempo pieno.