v1.0 Aprile 2026
1

Panoramica progetto

Nome progetto
PrenoTaglio
Tipo
PWA Multi-Tenant
Hosting
Hostinger Shared
Stack Backend
PHP 8.x + MySQL
Stack Frontend
Vue 3 CDN + Tailwind CDN
Database
u568594947_prenotataglio
ℹ️

Descrizione

PrenoTaglio Γ¨ una Progressive Web App multi-tenant per la gestione delle prenotazioni di saloni parrucchieri. Ogni salone (tenant) ha un proprio slug, una propria pagina di prenotazione pubblica per i clienti e un pannello di amministrazione dedicato. L'intera applicazione gira su hosting condiviso Hostinger senza necessitΓ  di Node.js o WebSocket.

Architettura multi-tenant

Il sistema supporta piΓΉ saloni sulla stessa installazione. Ogni tenant Γ¨ identificato dal proprio slug univoco nella URL. La risoluzione del tenant avviene lato PHP tramite il file tenant.php prima di ogni request.

2

Struttura file sul server

I file sono organizzati in due posizioni distinte: gli include (file PHP sensibili fuori dalla webroot) e il public_html (file accessibili via web).

Albero directory
/domains/mydigitaltools.it/
β”œβ”€β”€ include/
β”‚   └── prenotataglio/
β”‚       β”œβ”€β”€ config.php          ← credenziali DB, JWT secret, costanti app
β”‚       β”œβ”€β”€ db.php              ← PDO singleton connection
β”‚       β”œβ”€β”€ jwt.php             ← implementazione JWT HS256
β”‚       β”œβ”€β”€ auth.php            ← middleware autenticazione
β”‚       β”œβ”€β”€ functions.php       ← helper (uuid, jsonResponse, generateSlots, ecc.)
β”‚       └── tenant.php          ← risoluzione tenant da slug
└── public_html/
    └── prenotataglio/          ← document root sottodominio
        β”œβ”€β”€ .htaccess           ← URL rewriting + sicurezza
        β”œβ”€β”€ bootstrap.php       ← carica tutti gli include
        β”œβ”€β”€ index.php           ← entry point cliente SPA
        β”œβ”€β”€ manifest.php        ← PWA manifest dinamico (cliente)
        β”œβ”€β”€ sw.js               ← Service Worker
        β”œβ”€β”€ offline.html        ← fallback offline
        β”œβ”€β”€ install.sql         ← schema DB (DA ELIMINARE dopo import)
        β”œβ”€β”€ api/
        β”‚   β”œβ”€β”€ .htaccess
        β”‚   β”œβ”€β”€ auth/
        β”‚   β”‚   β”œβ”€β”€ register.php
        β”‚   β”‚   β”œβ”€β”€ login.php
        β”‚   β”‚   β”œβ”€β”€ logout.php
        β”‚   β”‚   β”œβ”€β”€ refresh.php
        β”‚   β”‚   β”œβ”€β”€ admin-login.php
        β”‚   β”‚   β”œβ”€β”€ admin-logout.php
        β”‚   β”‚   └── admin-refresh.php
        β”‚   β”œβ”€β”€ servizi.php
        β”‚   β”œβ”€β”€ disponibilita.php
        β”‚   β”œβ”€β”€ prenotazioni.php
        β”‚   └── admin/
        β”‚       β”œβ”€β”€ dashboard.php
        β”‚       β”œβ”€β”€ servizi.php
        β”‚       β”œβ”€β”€ orari.php
        β”‚       β”œβ”€β”€ chiusure.php
        β”‚       β”œβ”€β”€ prenotazioni.php
        β”‚       β”œβ”€β”€ operatori.php
        β”‚       β”œβ”€β”€ clienti.php
        β”‚       └── tenants.php
        β”œβ”€β”€ admin/
        β”‚   β”œβ”€β”€ index.php       ← admin SPA
        β”‚   └── manifest.php    ← PWA manifest dinamico (admin)
        β”œβ”€β”€ setup/
        β”‚   └── index.php       ← wizard primo setup (DA ELIMINARE dopo uso)
        └── assets/
            β”œβ”€β”€ css/
            β”‚   β”œβ”€β”€ app.css
            β”‚   └── admin.css
            β”œβ”€β”€ js/
            β”‚   β”œβ”€β”€ app.js
            β”‚   β”œβ”€β”€ booking.js
            β”‚   └── admin.js
            β”œβ”€β”€ img/
            β”‚   β”œβ”€β”€ generate-icons.php  ← DA ELIMINARE dopo uso
            β”‚   β”œβ”€β”€ icon-192.png
            β”‚   └── icon-512.png
            └── uploads/
                └── servizi/    ← foto servizi caricati
⚠️
File da eliminare dopo il deploy I file evidenziati in arancione (setup/, install.sql, generate-icons.php) devono essere eliminati dal server dopo l'uso per ragioni di sicurezza.
3

Schema Database

Database: u568594947_prenotataglio β€” Utente MySQL: u568594947_prenotataglio

Tabella: tenants

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
slugVARCHAR(50)UNIQUE β€” usato nelle URL (es. elite-parrucchieri)
nomeVARCHAR(150)Nome visualizzato del salone
emailVARCHAR(150)Email di contatto
telefonoVARCHAR(20)Numero di telefono
indirizzoTEXTIndirizzo fisico
colore_primarioVARCHAR(7)Hex colore tema (es. #1a1a2e)
colore_secondarioVARCHAR(7)Hex colore secondario
logo_urlVARCHAR(255)Percorso logo caricato
pianoENUM'base', 'pro', 'enterprise'
attivoTINYINT(1)1=attivo, 0=disabilitato
created_atDATETIMEData creazione

Tabella: admin_users

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
tenant_idCHAR(36)FK β†’ tenants.id
nomeVARCHAR(100)
cognomeVARCHAR(100)
emailVARCHAR(150)UNIQUE per tenant
password_hashVARCHAR(255)Argon2ID hash
roleENUM'superadmin', 'admin', 'operatore'
attivoTINYINT(1)
created_atDATETIME

Tabella: clienti

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
tenant_idCHAR(36)FK β†’ tenants.id
nomeVARCHAR(100)
cognomeVARCHAR(100)
emailVARCHAR(150)UNIQUE per tenant
telefonoVARCHAR(20)
password_hashVARCHAR(255)Argon2ID hash
attivoTINYINT(1)
created_atDATETIME

Tabella: operatori

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
tenant_idCHAR(36)FK β†’ tenants.id
nomeVARCHAR(100)
cognomeVARCHAR(100)
coloreVARCHAR(7)Colore hex per calendario
attivoTINYINT(1)
created_atDATETIME

Tabella: servizi

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
tenant_idCHAR(36)FK β†’ tenants.id
nomeVARCHAR(150)Nome del servizio
descrizioneTEXT
durata_minutiINTDurata in minuti (es. 30, 45, 60)
prezzoDECIMAL(8,2)Prezzo in euro
foto_urlVARCHAR(255)Percorso foto in uploads/servizi/
attivoTINYINT(1)
ordinamentoINTOrdine visualizzazione
created_atDATETIME

Tabella: config_orari

Orari di apertura per giorno della settimana. 0 = Domenica, 1 = Lunedì, ..., 6 = Sabato (convenzione PHP/MySQL DAYOFWEEK).

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
tenant_idCHAR(36)FK β†’ tenants.id
giorno_settimanaTINYINT0=Dom, 1=Lun, 2=Mar, 3=Mer, 4=Gio, 5=Ven, 6=Sab
apertoTINYINT(1)1=aperto, 0=chiuso
ora_aperturaTIMEEs. 09:00:00
ora_chiusuraTIMEEs. 18:00:00
durata_slotINTMinuti per slot (es. 20, 30)
created_atDATETIME

Tabella: chiusure

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
tenant_idCHAR(36)FK β†’ tenants.id
data_inizioDATEInizio periodo chiusura
data_fineDATEFine periodo chiusura
motivoVARCHAR(255)Descrizione (es. "Ferie agosto")
created_atDATETIME

Tabella: prenotazioni

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
tenant_idCHAR(36)FK β†’ tenants.id
cliente_idCHAR(36)FK β†’ clienti.id
servizio_idCHAR(36)FK β†’ servizi.id
operatore_idCHAR(36)FK β†’ operatori.id (nullable)
data_oraDATETIMEData e ora appuntamento
durata_minutiINTSnapshot durata al momento prenotazione
prezzoDECIMAL(8,2)Snapshot prezzo al momento prenotazione
statoENUM'in_attesa', 'confermata', 'annullata', 'completata'
noteTEXTNote del cliente
created_atDATETIME

Tabella: sessioni e sessioni_admin

Refresh token per clienti e admin rispettivamente. Stessa struttura:

ColonnaTipoNote
idCHAR(36)UUID, PRIMARY KEY
utente_idCHAR(36)FK β†’ clienti.id oppure admin_users.id
tenant_idCHAR(36)FK β†’ tenants.id
refresh_tokenVARCHAR(255)Token SHA256 opaco
scadenzaDATETIMEScadenza refresh token (default 30 giorni)
ipVARCHAR(45)IP client al momento del login
user_agentTEXTBrowser/device
created_atDATETIME
4

Setup iniziale passo per passo

Creare il sottodominio in hPanel

Accedi a hPanel β†’ Domains β†’ Subdomains e crea:

CampoValore
Sottodominioprenotataglio
Dominio basemydigitaltools.it
Document Rootdomains/mydigitaltools.it/public_html/prenotataglio
ℹ️
La propagazione DNS puΓ² richiedere fino a 24 ore, ma di solito Γ¨ questione di minuti su Hostinger.

Configurare config.php

Modifica il file /domains/mydigitaltools.it/include/prenotataglio/config.php:

PHP
<?php
// Database
define('DB_HOST', 'localhost');
define('DB_NAME', 'u568594947_prenotataglio');
define('DB_USER', 'u568594947_prenotataglio');
define('DB_PASS', 'LA_TUA_PASSWORD');

// Sicurezza
define('JWT_SECRET', 'stringa-random-64-chars-cambia-con-valore-sicuro');

// App
define('APP_URL', 'https://prenotataglio.mydigitaltools.it');
define('APP_ENV', 'production');

// JWT durata
define('JWT_EXPIRE', 900);        // 15 minuti access token
define('REFRESH_EXPIRE', 2592000); // 30 giorni refresh token
πŸ”΄
Non committare mai questo file! Contiene credenziali sensibili. Il file Γ¨ fuori dalla webroot proprio per sicurezza.

Caricare i file sul server

Usa il File Manager di hPanel o un client FTP (FileZilla, WinSCP):

Cartella localeDestinazione server
include/prenotataglio//domains/mydigitaltools.it/include/prenotataglio/
public_html/prenotataglio//domains/mydigitaltools.it/public_html/prenotataglio/
ℹ️
Assicurati che la cartella assets/uploads/servizi/ abbia permessi 755 per consentire l'upload delle foto.

Importare il database

  1. Vai su hPanel β†’ Databases β†’ phpMyAdmin
  2. Seleziona il database u568594947_prenotataglio
  3. Clicca su Importa nella barra superiore
  4. Scegli il file install.sql
  5. Clicca Esegui
⚠️
Dopo l'import, elimina o rendi inaccessibile install.sql. L'.htaccess lo blocca giΓ , ma Γ¨ preferibile rimuoverlo fisicamente.

Generare le icone PWA

Visita questo URL nel browser per generare automaticamente le icone icon-192.png e icon-512.png:

URL
https://prenotataglio.mydigitaltools.it/assets/img/generate-icons.php
πŸ”΄
Elimina subito dopo! Rimuovi assets/img/generate-icons.php dal server non appena le icone sono state generate.

Setup primo tenant (wizard)

Visita l'URL del wizard di setup per creare il primo salone:

URL
https://prenotataglio.mydigitaltools.it/setup/

Il wizard creerΓ  automaticamente il tenant, l'admin e gli orari di default.

πŸ”΄
Elimina la cartella setup/ dopo l'uso! Lasciarla accessibile Γ¨ un grave rischio di sicurezza.

Verificare il file .htaccess

Il file .htaccess nella root del sottodominio deve contenere:

Apache .htaccess
Options -Indexes
RewriteEngine On
RewriteBase /

# Blocca accesso a file sensibili
RewriteRule ^bootstrap\.php$ - [F,L]
RewriteRule ^install\.sql$ - [F,L]

# Servi file e directory esistenti direttamente
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# Cartelle di sistema β€” non trattare come slug tenant
RewriteRule ^(api|assets|setup|offline\.html|manifest\.json|manifest\.php|sw\.js)(.*)?$ - [L]

# Admin tenant: /{slug}/admin
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9_-]+)/admin(/.*)?$ admin/index.php?tenant=$1 [QSA,L]

# Homepage tenant: /{slug}/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9_-]+)/?$ index.php?tenant=$1 [QSA,L]

# Sottopagine tenant: /{slug}/{path}
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9_-]+)/(.+)$ index.php?tenant=$1&path=$2 [QSA,L]
5

URL e Routing

URLDestinazione PHPDescrizione
prenotataglio.mydigitaltools.it/index.php (no tenant)Landing page β€” lista saloni attivi
prenotataglio.mydigitaltools.it/{slug}/index.php?tenant={slug}SPA prenotazione cliente
prenotataglio.mydigitaltools.it/{slug}/adminadmin/index.php?tenant={slug}Pannello admin del salone
prenotataglio.mydigitaltools.it/api/...api/... (diretto)REST API β€” non rewrittata
prenotataglio.mydigitaltools.it/sw.jssw.js (diretto)Service Worker
prenotataglio.mydigitaltools.it/{slug}/manifest.jsonmanifest.php?tenant={slug}PWA Manifest cliente dinamico
prenotataglio.mydigitaltools.it/{slug}/admin/manifest.jsonadmin/manifest.php?tenant={slug}PWA Manifest admin dinamico
ℹ️
Esempio con il tenant "elite-parrucchieri" Cliente: prenotataglio.mydigitaltools.it/elite-parrucchieri/ β€” Admin: prenotataglio.mydigitaltools.it/elite-parrucchieri/admin
6

API Endpoints

Auth Cliente

POST/api/auth/register.phpRegistrazione nuovo cliente
POST/api/auth/login.phpLogin β€” ritorna JWT access + refresh cookie
POST/api/auth/logout.phpLogout β€” invalida sessione
POST/api/auth/refresh.phpRinnova JWT access token tramite refresh cookie

Auth Admin

POST/api/auth/admin-login.phpLogin admin β€” ritorna JWT admin
POST/api/auth/admin-logout.phpLogout admin
POST/api/auth/admin-refresh.phpRinnova JWT admin

Endpoint Pubblici Cliente

GET/api/servizi.php?tenant_slug={slug}Lista servizi attivi del salone
GET/api/disponibilita.php?tenant_slug={slug}&data=YYYY-MM-DD&servizio_id={id}Slot disponibili per una data e servizio

Prenotazioni Cliente (autenticato)

GET/api/prenotazioni.phpLista prenotazioni del cliente loggato
POST/api/prenotazioni.phpCrea nuova prenotazione
DEL/api/prenotazioni.php?id={uuid}Annulla prenotazione

Admin β€” Dashboard

GET/api/admin/dashboard.php?tenant_slug={slug}Statistiche: prenotazioni oggi, settimana, stati

Admin β€” Servizi

GET/api/admin/servizi.phpLista tutti i servizi
POST/api/admin/servizi.phpCrea nuovo servizio
PUT/api/admin/servizi.php?id={uuid}Modifica servizio
DEL/api/admin/servizi.php?id={uuid}Elimina servizio

Admin β€” Orari

GET/api/admin/orari.phpLeggi orari settimanali
PUT/api/admin/orari.phpSalva orari settimanali (body: array 7 giorni)

Admin β€” Chiusure

GET/api/admin/chiusure.phpLista periodi di chiusura
POST/api/admin/chiusure.phpAggiungi periodo chiusura
DEL/api/admin/chiusure.php?id={uuid}Rimuovi chiusura

Admin β€” Prenotazioni

GET/api/admin/prenotazioni.phpLista prenotazioni con filtri (data, stato, operatore)
PATCH/api/admin/prenotazioni.php?id={uuid}Cambia stato prenotazione
DEL/api/admin/prenotazioni.php?id={uuid}Elimina prenotazione

Admin β€” Operatori

GET/api/admin/operatori.phpLista operatori
POST/api/admin/operatori.phpCrea operatore
PUT/api/admin/operatori.php?id={uuid}Modifica operatore
DEL/api/admin/operatori.php?id={uuid}Elimina operatore

Admin β€” Clienti

GET/api/admin/clienti.phpLista clienti registrati del tenant
7

PWA β€” Dettagli implementazione

Due PWA installabili separatamente

La stessa origine serve due app installabili: una per il cliente e una per l'admin di ogni salone. Questo Γ¨ possibile grazie ai campi id e scope nel manifest.

PWAidscopestart_url
Cliente/pwa/client/{slug}/{slug}//{slug}/
Admin/pwa/admin/{slug}/{slug}/admin/{slug}/admin
ℹ️
PerchΓ© ID separati? I browser usano il campo id del manifest per identificare univocamente una PWA. Senza ID diversi, lo stesso browser non permetterebbe di installare sia l'app cliente che quella admin dallo stesso dominio. Con scope diversi e ID univoci, entrambe possono essere installate e apparse come app separate nel launcher del dispositivo.

Manifest dinamico (manifest.php)

I manifest sono generati dinamicamente da PHP perchΓ© devono includere il nome del salone, i colori del tema e gli ID specifici per ogni tenant. Vengono serviti con header Content-Type: application/manifest+json.

Service Worker (sw.js) β€” Strategie di cache

RisorsaStrategiaNote
HTML/CSS/JS app shellCache FirstAggiornato a ogni nuovo deploy
ImmaginiCache FirstTTL 30 giorni
Chiamate API (/api/...)Network OnlyDati sempre freschi
Offline fallbackβ€”Serve offline.html se rete non disponibile
βœ…
PWA installabile su iOS e Android Grazie a manifest valido, Service Worker registrato e HTTPS, l'app puΓ² essere installata come app nativa su qualsiasi dispositivo moderno.
8

Real-time polling admin

ℹ️
PerchΓ© polling invece di WebSocket? Hostinger shared hosting non supporta connessioni WebSocket persistenti. Il polling ogni 20 secondi offre un buon compromesso tra reattivitΓ  e carico server.

Implementazione

Il pannello admin usa setInterval da 20 secondi per aggiornare i dati silenziosamente senza ricaricare la pagina.

Componenti con polling attivo

Componente VueAzione al poll
AdminDashboardRileva nuove prenotazioni, mostra toast notifica
AdminCalendarioChiama calendar.refetchEvents()
AdminPrenotazioniAggiorna lista silenziosamente (senza loader)

Change detection via signature

JavaScript
// Genera una firma stringa dalla lista prenotazioni
// Se la firma cambia β†’ ci sono nuove prenotazioni o cambi di stato
_signature(list) {
  return list.map(p => p.id + '|' + p.stato).join(',');
}

Toast notification system

Il sistema di notifiche usa un array adminStore.toasts[] reattivo in Pinia/Vue.

JavaScript
// Mostra un toast β€” tipo: 'success' | 'error' | 'info' | 'warning'
adminStore.showToast(message, type, duration)

// Esempio β€” nuova prenotazione rilevata dal polling
if (currentSig !== this.lastSignature) {
  adminStore.showToast('Nuova prenotazione ricevuta!', 'success', 4000);
  this.lastSignature = currentSig;
}

Live indicator

Nel topbar del pannello admin Γ¨ presente un pallino verde pulsante con la scritta LIVE che indica che il polling Γ¨ attivo. Si colora di rosso e mostra OFFLINE in caso di errore di rete.

9

Bug risolti durante lo sviluppo

Bug 1: .htaccess catturava assets/ come slug tenant

Sintomo: Le richieste a /assets/css/app.css venivano reindirizzate a index.php?tenant=assets invece di servire il file CSS.

Causa: In Apache, le direttive RewriteCond si applicano solo alla RewriteRule immediatamente successiva. Le condizioni !-f e !-d scritte una volta prima di piΓΉ RewriteRule non proteggevano tutte le regole seguenti.

Soluzione: Aggiunta una regola esplicita per tutte le cartelle di sistema prima delle regole tenant, e le condizioni RewriteCond sono state ripetute prima di ogni RewriteRule che ne ha bisogno.

Apache β€” Fix
# Questa regola esplicita protegge le cartelle di sistema
RewriteRule ^(api|assets|setup|offline\.html|manifest\.json|manifest\.php|sw\.js)(.*)?$ - [L]

# Le RewriteCond devono essere ripetute per OGNI RewriteRule che ne ha bisogno
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9_-]+)/admin(/.*)?$ admin/index.php?tenant=$1 [QSA,L]
βœ“ Risolto

Bug 2: Calendario tutto occupato (nessuno slot disponibile)

Sintomo: L'API /api/disponibilita.php restituiva sempre zero slot liberi, anche per giorni senza prenotazioni.

Causa: SQLSTATE[HY093] Invalid parameter number β€” con PDO e ATTR_EMULATE_PREPARES = false, lo stesso named parameter (es. :data) non puΓ² essere usato piΓΉ di una volta nella stessa query preparata.

PHP β€” Query errata
-- ERRATO: :data usato due volte
WHERE data_inizio <= :data AND data_fine >= :data
PHP β€” Query corretta
-- CORRETTO: parametri con nomi distinti
WHERE data_inizio <= :data1 AND data_fine >= :data2

// Binding PHP
$stmt->execute([':data1' => $data, ':data2' => $data]);
βœ“ Risolto

Bug 3: PWA admin e cliente in conflitto sullo stesso device

Sintomo: Impossibile installare sia la PWA cliente che quella admin sullo stesso dispositivo. Il browser mostrava un solo prompt di installazione, o sovrascriveva quella giΓ  installata.

Causa: Entrambi i manifest usavano "scope": "/" e non avevano il campo id esplicito. Il browser le considerava la stessa app.

Soluzione: Scope diversi per le due PWA e campo id univoco per ogni combinazione tenant/tipo:

PHP β€” manifest.php cliente
"id": "/pwa/client/",
"scope": "//",
"start_url": "//"
PHP β€” manifest.php admin
"id": "/pwa/admin/",
"scope": "//admin",
"start_url": "//admin"
βœ“ Risolto
10

Aggiungere un nuovo tenant

Per aggiungere un nuovo salone al sistema, esegui queste query in phpMyAdmin sul database u568594947_prenotataglio.

⚠️
Prima genera l'hash della password! Vedi la sezione 11 per generare l'hash Argon2ID. Non inserire mai la password in chiaro nel database.
SQL β€” Nuovo tenant completo
-- 1. Inserisci il tenant
INSERT INTO tenants (id, slug, nome, email, colore_primario, colore_secondario, piano, attivo, created_at)
VALUES (UUID(), 'nuovo-salone', 'Nome Salone', 'email@salone.it', '#1a1a2e', '#16213e', 'base', 1, NOW());

-- 2. Recupera l'ID appena creato
SELECT id FROM tenants WHERE slug = 'nuovo-salone';

-- 3. Crea l'admin del tenant (sostituisci ID-DEL-TENANT con il valore del passo 2)
INSERT INTO admin_users (id, tenant_id, nome, cognome, email, password_hash, role, attivo, created_at)
VALUES (UUID(), 'ID-DEL-TENANT', 'Nome', 'Cognome', 'admin@salone.it', '$2y$10$...', 'admin', 1, NOW());

-- 4. Inserisci orari default (0=Dom, 1=Lun, 2=Mar, 3=Mer, 4=Gio, 5=Ven, 6=Sab)
INSERT INTO config_orari (id, tenant_id, giorno_settimana, aperto, ora_apertura, ora_chiusura, durata_slot, created_at)
VALUES
(UUID(), 'ID-DEL-TENANT', 0, 0, '09:00:00', '18:00:00', 20, NOW()),  -- Dom: chiuso
(UUID(), 'ID-DEL-TENANT', 1, 1, '09:00:00', '18:00:00', 20, NOW()),  -- Lun: aperto
(UUID(), 'ID-DEL-TENANT', 2, 1, '09:00:00', '18:00:00', 20, NOW()),  -- Mar: aperto
(UUID(), 'ID-DEL-TENANT', 3, 1, '09:00:00', '18:00:00', 20, NOW()),  -- Mer: aperto
(UUID(), 'ID-DEL-TENANT', 4, 1, '09:00:00', '18:00:00', 20, NOW()),  -- Gio: aperto
(UUID(), 'ID-DEL-TENANT', 5, 1, '09:00:00', '18:00:00', 20, NOW()),  -- Ven: aperto
(UUID(), 'ID-DEL-TENANT', 6, 0, '09:00:00', '13:00:00', 20, NOW());  -- Sab: chiuso
11

Generare una password hash PHP

Per creare la password_hash da inserire nel database, crea un file PHP temporaneo sul server:

PHP β€” hashgen.php (temporaneo)
<?php
// 1. Carica questo file sul server come hashgen.php
// 2. Visitalo nel browser
// 3. Copia l'hash generato
// 4. ELIMINA subito questo file!

echo password_hash('LA_PASSWORD_DESIDERATA', PASSWORD_ARGON2ID);
πŸ”΄
Elimina immediatamente questo file dopo l'uso! Non lasciarlo accessibile sul server, anche se non contiene la password in chiaro β€” potrebbe essere usato per generare hash arbitrari.
ℹ️
Perché Argon2ID? Argon2ID è l'algoritmo di hashing password raccomandato da OWASP. È memory-hard e resistente ad attacchi con GPU e ASIC. PHP lo supporta nativamente dalla versione 7.3.
12

Manutenzione

Pulizia sessioni scadute

Da eseguire periodicamente in phpMyAdmin per evitare che la tabella sessioni cresca indefinitamente:

SQL β€” Pulizia sessioni
DELETE FROM sessioni WHERE scadenza < NOW();
DELETE FROM sessioni_admin WHERE scadenza < NOW();

Backup database

  1. Vai su hPanel β†’ Databases β†’ phpMyAdmin
  2. Seleziona il database u568594947_prenotataglio
  3. Clicca Esporta nella barra superiore
  4. Formato: SQL, Metodo: Veloce
  5. Clicca Esegui β€” scarica il file .sql
ℹ️
Hostinger offre anche backup automatici nel piano. Verifica che siano attivi in hPanel β†’ Backups.

Cambio password admin

Prima genera il nuovo hash (sezione 11), poi esegui:

SQL β€” Cambio password
UPDATE admin_users
SET password_hash = '$2y$10$...'  -- incolla qui il nuovo hash
WHERE email = 'admin@salone.it'
  AND tenant_id = 'ID-TENANT';  -- sicurezza: limita al tenant corretto

Verificare le prenotazioni del giorno

SQL β€” Prenotazioni odierne
SELECT
  p.data_ora,
  p.stato,
  p.prezzo,
  CONCAT(c.nome, ' ', c.cognome) AS cliente,
  c.telefono,
  s.nome AS servizio,
  s.durata_minuti,
  CONCAT(o.nome, ' ', o.cognome) AS operatore
FROM prenotazioni p
JOIN clienti c ON p.cliente_id = c.id
JOIN servizi s ON p.servizio_id = s.id
LEFT JOIN operatori o ON p.operatore_id = o.id
WHERE DATE(p.data_ora) = CURDATE()
  AND p.tenant_id = 'ID-TENANT'
  AND p.stato = 'confermata'
ORDER BY p.data_ora ASC;
13

Checklist sicurezza post-deploy

Dopo aver completato il deploy, verifica tutti i punti seguenti. Clicca sulle caselle per segnarle come completate.

πŸ”΄
Non trascurare questa checklist! I punti non completati rappresentano vulnerabilitΓ  di sicurezza attive.
14

Link utili

RisorsaURL
App cliente (Elite Parrucchieri)https://prenotataglio.mydigitaltools.it/elite-parrucchieri/
Pannello admin (Elite Parrucchieri)https://prenotataglio.mydigitaltools.it/elite-parrucchieri/admin
hPanel Hostingerhttps://hpanel.hostinger.com
phpMyAdminAccessibile da hPanel β†’ Databases β†’ phpMyAdmin
File ManagerAccessibile da hPanel β†’ Files β†’ File Manager
βœ…
Documentazione completa! Tutti i componenti del progetto PrenoTaglio sono documentati. Per aggiornamenti o modifiche future, aggiorna questo file di conseguenza.