Poltrona
Gestionale in cloud per parrucchieri e barbieri.
Codice disponibile su richiesta

Il problema
I saloni prenotano attorno ai tempi di posa: mentre un colore è in posa, l’operatore può prendere un altro cliente, e i gestionali generici non sanno rappresentarlo. I saloni custodiscono anche dati sanitari dei clienti, come allergie e test di sensibilità, e non cambiano software se non possono portarsi dietro migliaia di clienti.
- Stato
- Prima del lancio, beta chiusa in arrivo
- Stack
- Next.js 16, React 19, TypeScript, Tailwind 4, Supabase, PostgreSQL, Edge Functions, Resend, Vercel
- Facts
- 117 pagine
- 77 migrazioni, circa 67 tabelle
- Circa 630 controlli di isolamento tra saloni
Le scelte che vale la pena spiegare
È il database a rifiutare le doppie prenotazioni
Ogni appuntamento è diviso in una riga per fase, ognuna con il suo intervallo di tempo. Un vincolo di esclusione gist vale solo per le fasi in cui l’operatore è davvero occupato: si può prenotare un cliente durante la posa di un altro, e due tablet alla reception non possono mai prenotare due volte lo stesso operatore.
Le note sanitarie stanno in una tabella chiusa
La tabella delle note sensibili ha la row level security attiva e nessuna policy, quindi nessuno può leggerla direttamente. L’unico accesso è una funzione che controlla il consenso e scrive ogni lettura in un registro. Nemmeno un admin con accesso di supporto vede quelle note.
Ogni scrittura è una funzione Postgres, e un test dimostra che i muri reggono
Le tabelle non hanno policy di scrittura. Ogni azione è una funzione security definer che controlla da sé chi la chiama, e la creazione di un salone blocca la riga del profilo, così un doppio invio non crea due saloni. Una suite SQL di 4.000 righe gira in una transazione sempre annullata e verifica che nessuna tabella sia visibile da un altro salone.
Un pezzo del codice
La regola delle sovrapposizioni è un vincolo, quindi nessun percorso nel codice può saltarla.
create table public.appointment_segments (
id uuid primary key default gen_random_uuid(),
location_id uuid not null references public.locations (id) on delete cascade,
appointment_id uuid not null references public.appointments (id) on delete cascade,
item_id uuid not null references public.appointment_items (id) on delete cascade,
staff_member_id uuid not null references public.staff_members (id),
time_range tstzrange not null check (not isempty(time_range)),
operator_busy boolean not null,
is_active boolean not null default true,
-- No double booking of an operator's work (PRD "Vincoli chiave").
constraint appointment_segments_no_overlap exclude using gist (
staff_member_id with =,
time_range with &&
) where (operator_busy and is_active)
);