Identità visiva e design system
Il marchio è rigorosamente monocromo: nero e bianco puri, nessun colore d'accento. È una scelta forte e vincola l'interfaccia — la gerarchia si costruisce con peso, scala e spazio, non con il colore.
Il principio
Il vincolo monocromo è del logo, non dell'interfaccia. #000000 su #FFFFFF a tutta pagina, per otto ore di uso continuato, affatica: nero e bianco puri restano al logo e a pochissimi elementi di enfasi massima. Tutto il resto vive su una scala di grigi a 12 gradini con semantica fissa, e il colore compare solo come stato.
Scala grigi — 12 step
Lo step 07 è il bordo dei campi, non un grigio chiaro decorativo: quando il bordo è l'unico modo di riconoscere un campo, WCAG 1.4.11 impone 3:1. È la violazione più frequente nei design monocromi, e la evitiamo per costruzione.
Stati — l'unico posto dove entra il colore
Ogni stato porta sempre un'etichetta oltre al colore: in bianco e nero deve restare riconoscibile (WCAG 1.4.1). Valori funzionali presi dal GOV.UK Design System, già collaudati: errore #CA3535, successo #0F7A52, focus #FFDD00 su testo #0B0C0C.
Il focus da tastiera è trattato come colore funzionale a sé, giallo pieno: un anello grigio su fondo grigio è invisibile, e questa è un'app che si usa tutto il giorno anche da tastiera.
Tipografia — le font del marchio
Maven Pro e Mulish non sono una scelta mia: sono le font già in uso su gentiluomoestetica.it, lette dal CSS del sito in produzione. Entrambe su Google Fonts, nessuna licenza da comprare. Aggiungo IBM Plex Mono per i dati, perché importi e orari incolonnati richiedono cifre a larghezza fissa: senza tabular-nums una colonna di euro non si legge.
Componenti
Base shadcn/ui su Base UI + Tailwind. shadcn non è una libreria ma un modello di distribuzione: il sorgente dei componenti vive nel nostro repo e si riscrive liberamente. Con Mantine o Ant Design passeremmo il tempo a combattere contro un tema di default che non è il nostro.
Pattern presi da prodotti reali
| Prodotto | Cosa gli rubiamo |
|---|---|
| Mangomint | I requisiti di risorsa stanno sul servizio, non sull'appuntamento: il sistema alloca da sé. Scaldacera 1 e Scaldacera 2 sono due entità distinte in un gruppo, non una risorsa con quantità 2. |
| Phorest | Il conflitto di risorsa è un avviso bloccante morbido: si può forzare, ma chi forza e quando resta tracciato. |
| Fresha | Blocco orario inseribile in pochi secondi (pausa, formazione, manutenzione) e tempo extra di servizio oltre la durata — decisivo per cera e laser, quasi sempre dimenticato. |
| Nielsen Norman | Scheda cliente in pannello laterale non modale: il modale nasconde i dati di riferimento proprio mentre servono. Quantità con barre, mai torte o tachimetri. |
| Linear | Densità dalla tipografia invece che dalla cornice, navigazione minima, palette comandi e scorciatoie per le tre azioni ad altissima frequenza: nuovo appuntamento, cerca cliente, incassa. |
Stack definitivo
Verificato sul server reale, non assunto: PHP 8.4, Redis 6.2 attivo, Supervisor 4.2.5, 4 core e 15 GB di RAM. Horizon è praticabile.
| Ambito | Scelta | Perché |
|---|---|---|
| Backend | Laravel 13 · PHP 8.4 | Major stabile con supporto sicurezza fino a marzo 2028. PHP 8.4 già disponibile nel pool Plesk |
| Database | MariaDB 10.5 | Quella installata. Fuori supporto da giugno 2025: aggiornamento da mettere in conto |
| Code | Redis + Horizon + Supervisor | Tutto già presente sul server. Code separate: default, notifiche, sync, report |
| Auth | Sanctum 4 | Cookie stateful per la SPA, token bearer per le app. La doc di Passport rimanda esplicitamente qui |
| Multi-tenant | trait BelongsToCentro | Global scope su centro_id. Con 5-9 tenant e un solo database, i pacchetti dedicati aggiungono peso senza vantaggio |
| Permessi | spatie/laravel-permission | Con teams=true su centro_id: la stessa persona può avere ruoli diversi in centri diversi |
| Audit | spatie/laravel-activitylog | Copre anche eventi non-Eloquent: login, export, invio notifica |
| Contabilità | ledger append-only | Incassi, prima nota, Gentil Card e sedute come righe immutabili; gli storni sono righe negative. Nessun log di audit è fonte di verità contabile |
| Frontend | React 19 + Vite | SPA statica dietro autenticazione: zero SEO, Next.js aggiungerebbe un runtime Node senza ritorno |
| App mobili | Expo SDK 57 + Expo Router | Due app, operatrici e cliente. EAS Build compila iOS anche senza Mac |
| Codice condiviso | monorepo pnpm + Turborepo | Si condivide logica, non interfaccia: schemi Zod, tipi, client API, regole di dominio |
| Stato server | TanStack Query | Cache, deduplica e coda mutazioni offline già risolte. Zustand solo per lo stato locale |
| Form | React Hook Form + Zod | Uno schema solo: valida il web, valida il mobile, tipizza il payload, si rispecchia nelle FormRequest |
| Calendario | DayPilot Lite | Apache 2.0, gratuito, include sia lo scheduler a righe sia la vista a colonne per risorsa. Attribuzione obbligatoria; la validazione durante il trascinamento la fa il server, che è comunque l'unico arbitro valido |
| Test backend | Pest + MySQL reale | Mai SQLite: non riproduce il locking InnoDB e i test di concorrenza passerebbero falsamente |
| Test frontend | Vitest + Playwright | Unitari e percorsi end-to-end reali sul browser |
Sanctum in modalità cookie richiede che SPA e API stiano sotto lo stesso dominio di secondo livello. Con gentiluomo.nubelab.it e backend.gentiluomo.nubelab.it la condizione è soddisfatta. Se in produzione si passa a gentiluomoestetica.it, vanno spostati entrambi insieme: separarli obbliga a riscrivere l'autenticazione.
Struttura del repository
Monorepo unico: un solo posto dove le regole di dominio esistono, tre applicazioni che le consumano.
gentiluomo/ ├── apps/ │ ├── api/ Laravel 13 — backend, unica fonte di verità │ ├── gestionale/ React + Vite — dashboard desktop del centro │ ├── operatrici/ Expo — app staff: agenda, timbratura, incassi │ └── cliente/ Expo — app cliente: prenota, storico, card ├── packages/ │ ├── core/ schemi Zod, tipi, regole di dominio condivise │ ├── api-client/ client HTTP tipizzato, generato dal contratto │ └── ui/ token di design e componenti web condivisi ├── mocks/ fake server dei servizi esterni ├── assets/brand/ loghi, icone, manifest d'uso ├── docs/ specifiche v2.0, piano, contratto API └── infra/ deploy, systemd, nginx, backup
| Sottodominio | Cosa serve |
|---|---|
| gentiluomo.nubelab.it | Gestionale desktop (SPA) |
| backend.gentiluomo.nubelab.it | API Laravel |
| docs.gentiluomo.nubelab.it | Documentazione — già online |
| mock.gentiluomo.nubelab.it | Fake server dei servizi esterni |
Seed e fake server
Nessuna attesa sul cliente: si sviluppa su dati finti realistici e su un server che imita i servizi esterni. Il passaggio alla realtà sarà solo una variabile d'ambiente e una chiave.
Il seed
Dati di prova che riproducono la realtà del centro di Aosta: due centri (Aosta reale e un secondo fittizio, per provare l'isolamento), personale con turni e ferie, il listino completo dalle specifiche, clienti con storici diversi, Gentil Card a saldi vari, percorsi a stadi diversi di avanzamento.
La regola che rende il seed utile è che non deve contenere solo casi felici. Genera deliberatamente le situazioni che rompono il software:
- due trattamenti a cera parzialmente sovrapposti, al limite dei due scaldacera;
- una Gentil Card a saldo zero e una con residuo insufficiente per il trattamento prenotato;
- un percorso all'ultima seduta e uno scaduto da sollecitare;
- un'operatrice in ferie con appuntamenti già presi prima della richiesta;
- un centro con una sola cabina, che satura subito;
- lo stesso nome e cognome cliente in due centri diversi;
- un cliente con no-show non regolarizzato che prova a riprenotare.
Faker in italiano con seme fisso, date ancorate a una data base costante e non a oggi: due esecuzioni del seed devono produrre lo stesso database, altrimenti i test diventano capricciosi.
Il fake server
Un solo processo che monta tutti i provider su prefissi distinti, mantenendo i path reali dopo il prefisso. Il passaggio mock → sandbox → produzione è solo SUMUP_BASE_URL e simili: zero if (env) nel codice di business.
| Servizio | Cosa mocchiamo | Sandbox ufficiale |
|---|---|---|
| SumUp | Checkout, storico transazioni, payout, rimborsi, lettore Solo, webhook di stato | Sì, stesso host. Mock per lavoro offline e CI |
| WhatsApp Cloud API | Invio template, stati sent/delivered/read/failed, webhook firmati | Solo numeri di test, non riflette limiti né costi |
| Skebby SMS | Login con sessione che scade, invio, polling stato | Non documentata: mock indispensabile |
| Resend email | Invio e webhook firmati Svix | Non verificata |
| Satispay | Firma RSA, pagamenti, lista per riconciliazione, report | Sì ma non self-service: si richiede via form |
| Scalapay · Klarna | Ordini, cattura, rimborsi, pagina di checkout finta, webhook | Sì, entrambi |
Deve firmare davvero i webhook. Se il mock manda payload non firmati, la verifica della firma non viene mai esercitata e va in produzione rotta.
Deve saper fallire. Importi che danno sempre errore, template non approvati, eventi fuori ordine, firme sbagliate, il nostro endpoint che risponde 500. Un mock che risponde sempre bene serve solo a farci credere che funzioni.
Checklist di chiusura, uguale per ogni SAL
Nessun SAL è chiuso finché non passa questi cinque punti. Sono l'ultimo task di ogni SAL, sempre gli stessi.
I sei SAL
Quaranta giorni lavorativi, dal lunedì 7 settembre al 31 ottobre. Ogni SAL chiude con qualcosa che puoi aprire e provare, e con la stessa checklist di verifica.
Il settimo SAL non è stato tagliato: è stato ridistribuito. La migrazione da Treatwell entra nel SAL 1, dove l'import da CSV era già previsto. Le statistiche entrano nel SAL 3, dove i dati economici nascono. Accesso casa madre, provisioning e messa in produzione entrano nel SAL 5. Backup, monitoraggio e irrobustimento diventano parte della checklist di chiusura di ogni SAL invece di una fase finale.
Il margine è zero. Ogni imprevisto — la questione del macchinario NIR, l'export Treatwell, l'iter art. 4 — si mangia direttamente giorni di consegna. Lo scrivo una volta e poi lavoro.
Fondamenta e identità
7 → 11 set- Monorepo pnpm + Turborepo, convenzioni, linting, formattazione, hook pre-commit.
- Design system in codice: token dei 12 grigi, stati semantici, tipografia, componenti base shadcn ritematizzati.
- Laravel 13 su PHP 8.4, database dedicato, Redis, Horizon sotto Supervisor.
- Sottodomini
gentiluomo,backend.gentiluomo,mock.gentiluomocon certificati. - Autenticazione Sanctum, login, logout, sessione, ruoli di base.
- Script di deploy ripetibile e primo rilascio automatico.
Una guida di stile navigabile online con tutti i componenti nei loro stati, e il gestionale che ti fa entrare con utente e password. L'API risponde al controllo di salute.
Anagrafiche, dati e migrazione
14 → 18 set- Schema completo secondo le specifiche v2.0, con
centro_idovunque e indici unici composti. - Isolamento per centro, ruoli con ambito, registro di audit.
- Seed completo con tutti i casi difficili, non solo quelli felici.
- Anagrafica clienti: elenco, ricerca, scheda in pannello laterale, note, consensi.
- Personale, listino trattamenti, risorse fisiche con i requisiti sul servizio.
- Migrazione Treatwell: import CSV con anteprima, correzione errori e quadratura prima di scrivere.
Carichi il CSV vero dei clienti e li vedi comparire. Cerchi un cliente, apri la scheda, la modifichi. Verifichi che un utente di Aosta non riesca a vedere i clienti dell'altro centro.
Agenda — il cuore del sistema
21 set → 2 ott- Orari di apertura, turni mensili, ferie e assenze con causale che bloccano gli slot.
- Motore di disponibilità con occupazione risorse a segmenti: copre lo SkinGent che libera il macchinario al minuto 30 e lo scrub che occupa la doccia solo per la sua fase.
- Assegnazione del personale a intervalli: Combo Full Body con rilascio della seconda operatrice.
- Blocco transazionale contro la doppia prenotazione, con test di concorrenza reali.
- Vista a colonne per operatrice e vista a righe per macchinario su DayPilot Lite, spostamento con trascinamento validato dal server.
- Appuntamento a testata e righe, buffer di 10 minuti in coda al blocco, lista d'attesa con precedenza di 4 ore.
Prenoti davvero. Provi a mettere due laser in contemporanea e il sistema te lo impedisce spiegando perché. Sposti un appuntamento trascinandolo. Metti un'operatrice in ferie e gli slot spariscono.
Denaro, cassa e statistiche
5 → 16 ott- Gentil Card: fasce bonus, saldo unico, scalo automatico, storico movimenti.
- Percorsi con composizione a zone, avanzamento, stato «da sollecitare», bonus percorso separato.
- Consulenza con sconto a termine e avviso 24 ore prima della scadenza.
- Preventivo in PDF con il lockup del marchio, inviabile via email o WhatsApp.
- Incassi con i sei metodi, prima nota con uscite e versamenti, chiusura mensile.
- Fake server dei pagamenti e adattatori pronti per le chiavi reali.
- Statistiche del centro: venduto ed erogato separati, scontrino medio, upsell, fasce d'età, flussi tra card e percorsi.
Vendi una Gentil Card da 800 € e vedi comparire i 200 € di bonus. Fai una seduta e guardi il saldo scalare. Chiudi la prima nota del mese ed esporti il PDF per la commercialista.
Personale, CRM e app operatrici
19 → 23 ott- App operatrici in Expo: agenda del giorno, scheda cliente, incassi.
- Timbratura con QR a token rotante, dispositivo abbinato, prossimità e rete WiFi del centro.
- Riepilogo mensile ore e straordinari sul monte ore, esportabile per il consulente del lavoro.
- No-show con finestra di 48 ore, penale da confermare, blocco e sblocco.
- Omaggi, alert di inattività a 45 giorni, compleanni, sotto-scorta.
- Notifiche con consensi per canale e finalità, su fake server.
Installi l'app su un telefono, inquadri il QR del centro e timbri. Vedi la timbratura nel gestionale e le ore accumularsi. Fai scattare un alert e ne guardi partire il messaggio.
App cliente, casa madre, produzione
26 → 31 ott- App cliente in Expo: registrazione, verifica telefono, accettazione condizioni.
- Prenotazione autonoma sulla lista chiusa, il resto come richiesta da confermare.
- Storico, sedute residue, movimenti della card, disdetta con la regola delle 48 ore.
- Modalità offline degradata e notifiche push.
- Accesso casa madre in sola lettura, aggregati per centro, nominativi dietro interruttore tracciato.
- Provisioning di un nuovo centro per clonazione e messa in produzione con ripristino provato.
Dal tuo telefono prenoti un massaggio e lo vedi comparire nell'agenda. Entri come casa madre e confronti i due centri. Cloni un centro nuovo in pochi minuti.
Decisioni prese
Prese il 4 settembre 2026. Da qui non si torna indietro senza un motivo nuovo.
| Questione | Decisione | Conseguenza operativa |
|---|---|---|
| Scadenza | Ambito pieno entro ottobre. Niente MVP ridotto, niente proroga Treatwell | Sei SAL su quaranta giorni lavorativi, margine zero. Il settimo SAL ridistribuito negli altri, non tagliato |
| Calendario | DayPilot Lite, Apache 2.0, gratuito | Nessuna spesa e nessuna attesa di licenza. L'attribuzione a daypilot.org va tenuta. Il passaggio a FullCalendar resta possibile in SAL 2 se la vista a risorse non regge |
| Domini | Staging su nubelab.it, produzione dopo | gentiluomo.nubelab.it e backend.gentiluomo.nubelab.it: stesso dominio di secondo livello, Sanctum in modalità cookie funziona. Lo spostamento su gentiluomoestetica.it a fine progetto muove entrambi insieme |
Tre pratiche hanno tempi burocratici che non si comprimono e vanno aperte questa settimana, altrimenti scadono fuori da ottobre a prescindere dal codice.
Iter art. 4 dello Statuto dei Lavoratori per le timbrature geolocalizzate: accordo sindacale o istanza all'Ispettorato. Senza, i dati raccolti sono inutilizzabili anche se il software funziona. Account sviluppatore Apple e Google come organizzazione: Apple richiede codice D-U-N-S ed entità legale; Google impone a certi account 14 giorni continuativi di test chiuso con 12 tester prima della pubblicazione — da solo questo mette la pubblicazione sugli store oltre ottobre. Le app saranno pronte e installabili per il collaudo, ma la presenza sugli store non è una scadenza che posso rispettare io. Verifica aziendale Meta per i messaggi WhatsApp.
Rischi tecnici e come li disinnesco
| Rischio | Mitigazione |
|---|---|
| Doppia prenotazione sulla risorsa condivisa. «Cerca conflitti, poi inserisci» è una corsa critica: due richieste leggono zero conflitti e inseriscono entrambe | Transazione con blocco delle righe risorsa in ordine di id, controllo delle sovrapposizioni dentro la transazione, poi inserimento. Testato con connessioni reali concorrenti |
| Confini di intervallo sbagliati, che rendono l'agenda inutilizzabile | Intervalli semiaperti: le 10-11 e le 11-12 non confliggono. Nessun BETWEEN |
| Contesto del centro perso nei lavori in coda: è il punto esatto dove i sistemi multi-centro si rompono in produzione | centro_id trasportato nel job e reimpostato all'esecuzione, più una guardia che fa fallire subito ogni salvataggio senza centro |
| Isolamento aggirato da query diverse da Eloquent, e vincoli unici non protetti | Tutti gli indici unici composti con centro_id. Query diverse da Eloquent vietate sui modelli con isolamento |
| Timbrature duplicate al recupero della coda offline: ore sbagliate e contenzioso sul cedolino | Identificativo generato sul dispositivo con vincolo di unicità: il rinvio restituisce la timbratura esistente invece di crearne una seconda. Ora del server autorevole |
| Prenotazione creata offline: due operatrici prendono lo stesso laser e il conflitto si scopre col cliente già in negozio | Prenotazioni, incassi e ricariche non vanno offline. Offline solo timbrature, note, consumo seduta e consultazione |
| Nessuna API SumUp per lo scontrino fiscale italiano | Il gestionale non emette il documento commerciale: lo emette la cassa. Registra e riconcilia a posteriori sul codice transazione |
| Limite di messaggi WhatsApp condiviso fra tutti i centri: una campagna di uno brucia la quota e fa fallire i promemoria degli altri | Quota per centro applicata prima della coda, priorità assoluta ai messaggi di servizio sui promozionali |
| Webhook duplicati dai provider di pagamento, che generano doppi movimenti in prima nota | Chiave di idempotenza sull'identificativo del provider, e stato sempre riletto con una chiamata diretta invece di fidarsi del payload |
| Seed che nasconde i bug di isolamento, perché ogni relazione crea un centro nuovo | Riuso esplicito del centro nelle factory, seme fisso, date ancorate |
Cosa serve da te
Il piano non si ferma in attesa di queste risposte — si sviluppa su dati finti. Ma prima del collaudo con dati veri servono.
Da avviare tu, questa settimana
- Iter art. 4 presso l'Ispettorato o accordo sindacale, per le timbrature geolocalizzate.
- Account sviluppatore Apple e Google intestati all'organizzazione: hanno lead time che non si comprime.
- Verifica aziendale Meta per l'invio dei messaggi WhatsApp.
Dati dal cliente
Rispetto ai nove di prima ne restano sette: il sito ufficiale ne ha già chiusi due.
| # | Cosa manca | Blocca |
|---|---|---|
| 1 | SkinGent: durata della seduta in minuti e prezzo. Dal sito so già che è antiage viso con tecnologia NIR, 5 sedute settimanali, su viso, collo, mento e contorno occhi | Listino, agenda |
| 2 | Conferma sul macchinario: il sito dice che Body Strategy usa il manipolo NIR e che SkinGent usa il NIR. Se è lo stesso apparecchio del laser, allora Laser, SkinGent e Body Strategy non possono mai convivere — e la capacità del centro è molto più bassa del previsto | Motore agenda |
| 3 | Inventario risorse per centro: quante cabine in tutto, quante con doccia, quanti scaldacera, quante frese, quanti lettini | Motore agenda |
| 4 | «Cera brasiliana»: a quale voce del listino corrisponde | Regole di parallelismo |
| 5 | Treatwell: lo storico appuntamenti è esportabile? Serve un CSV di esempio reale e il tracciato dell'Excel dei pacchetti residui | Migrazione |
| 6 | Testi legali: informativa privacy, condizioni con la penale del 50 %, regolamento Gentil Card. Chi li redige | App cliente |
| 7 | Conferma del commercialista che la Gentil Card è un buono monouso, con IVA alla ricarica e scontrino a zero sulle sedute | Modello fiscale |
Il sito pubblicizza uno sconto del 10 % scegliendo tre o più servizi. Non compare nelle specifiche v1.0 né nelle risposte. Va confermato e aggiunto alle regole di sconto, altrimenti il gestionale calcolerà prezzi diversi da quelli promessi ai clienti.