diff --git a/app/Http/Controllers/Web/Auth/LoginController.php b/app/Http/Controllers/Web/Auth/LoginController.php
index e35c4e9..7e086c9 100644
--- a/app/Http/Controllers/Web/Auth/LoginController.php
+++ b/app/Http/Controllers/Web/Auth/LoginController.php
@@ -123,10 +123,10 @@ class LoginController extends Controller
// Redirezione basata sul ruolo
if ($user->hasRole('Admin')) {
return redirect()->to('userarea/admin.php');
+ } elseif ($user->hasRole('Owner')) {
+ return redirect()->to('userarea/appointments.php');
} elseif ($user->hasRole('User')) {
- return redirect()->to('userarea/select_school.php');
- } elseif ($user->hasRole('teacher')) {
- return redirect()->to('userarea/teacher.php');
+ return redirect()->to('userarea/my_appointments.php');
} elseif ($user->hasRole('school_owner')) {
return redirect()->to('userarea/school_dashboard.php');
}
@@ -134,6 +134,10 @@ class LoginController extends Controller
return redirect()->intended('userarea/default.php');
}
+
+
+
+
protected function logoutAndRedirectToTokenPage(Request $request, $user, ?string $redirectPage): RedirectResponse
{
Auth::logout();
diff --git a/db/migrations/20260901120958_create_subscriptions.php b/db/migrations/20260901120958_create_subscriptions.php
new file mode 100644
index 0000000..1484cab
--- /dev/null
+++ b/db/migrations/20260901120958_create_subscriptions.php
@@ -0,0 +1,69 @@
+table('subscription_plans', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+
+ $plans
+ ->addColumn('id', 'integer', ['identity' => true, 'signed' => false])
+ ->addColumn('name', 'string', ['limit' => 80])
+ ->addColumn('description', 'string', ['limit' => 255, 'null' => true])
+ ->addColumn('price', 'decimal', ['precision' => 8, 'scale' => 2, 'default' => '0.00'])
+ ->addColumn('billing_period', 'enum', ['values' => ['monthly', 'yearly'], 'default' => 'monthly'])
+ ->addColumn('stripe_price_id', 'string', ['limit' => 120, 'null' => true])
+ ->addColumn('is_active', 'boolean', ['default' => true])
+ ->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
+ ->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
+ ->create();
+
+ // ---- shop_subscriptions ----
+ $subs = $this->table('shop_subscriptions', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+
+ $subs
+ ->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
+ ->addColumn('shop_id', 'biginteger', ['signed' => false])
+ ->addColumn('plan_id', 'integer', ['signed' => false, 'null' => true])
+ ->addColumn('status', 'enum', [
+ 'values' => ['active', 'expired', 'blocked', 'trialing', 'pending'],
+ 'default' => 'pending',
+ ])
+ ->addColumn('started_at', 'date', ['null' => true])
+ ->addColumn('expires_at', 'date', ['null' => true])
+ ->addColumn('block_reason', 'string', ['limit' => 255, 'null' => true])
+ ->addColumn('stripe_customer_id', 'string', ['limit' => 120, 'null' => true])
+ ->addColumn('stripe_subscription_id', 'string', ['limit' => 120, 'null' => true])
+ ->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
+ ->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
+ ->addIndex(['shop_id'], ['unique' => true, 'name' => 'uniq_shop'])
+ ->addIndex(['status'], ['name' => 'idx_status'])
+ ->addIndex(['expires_at'], ['name' => 'idx_expires'])
+ ->addForeignKey('shop_id', 'shops', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION', 'constraint' => 'fk_sub_shop'])
+ ->addForeignKey('plan_id', 'subscription_plans', 'id', ['delete' => 'SET_NULL', 'update' => 'NO_ACTION', 'constraint' => 'fk_sub_plan'])
+ ->create();
+ }
+}
diff --git a/db/migrations/20260902145605_create_billing_profiles.php b/db/migrations/20260902145605_create_billing_profiles.php
new file mode 100644
index 0000000..dd83390
--- /dev/null
+++ b/db/migrations/20260902145605_create_billing_profiles.php
@@ -0,0 +1,43 @@
+table('billing_profiles', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+
+ $t
+ ->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
+ ->addColumn('user_id', 'integer', ['signed' => false])
+ ->addColumn('shop_id', 'biginteger', ['signed' => false, 'null' => true])
+ ->addColumn('business_name', 'string', ['limit' => 191])
+ ->addColumn('vat_number', 'string', ['limit' => 20])
+ ->addColumn('tax_code', 'string', ['limit' => 20, 'null' => true])
+ ->addColumn('address', 'string', ['limit' => 255])
+ ->addColumn('zip_code', 'string', ['limit' => 10])
+ ->addColumn('city', 'string', ['limit' => 100])
+ ->addColumn('province', 'string', ['limit' => 50, 'null' => true])
+ ->addColumn('pec', 'string', ['limit' => 191, 'null' => true])
+ ->addColumn('sdi_code', 'string', ['limit' => 7, 'null' => true])
+ ->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
+ ->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
+ ->addIndex(['user_id'], ['name' => 'idx_bp_user'])
+ ->addIndex(['shop_id'], ['name' => 'idx_bp_shop'])
+ ->addForeignKey('user_id', 'auth_users', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION', 'constraint' => 'fk_bp_user'])
+ ->addForeignKey('shop_id', 'shops', 'id', ['delete' => 'SET_NULL', 'update' => 'NO_ACTION', 'constraint' => 'fk_bp_shop'])
+ ->create();
+ }
+}
diff --git a/db/migrations/20260903062920_add_target_audience.php b/db/migrations/20260903062920_add_target_audience.php
new file mode 100644
index 0000000..230c3b9
--- /dev/null
+++ b/db/migrations/20260903062920_add_target_audience.php
@@ -0,0 +1,42 @@
+execute("
+ ALTER TABLE `shops`
+ ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
+ AFTER `description`
+ ");
+
+ // staff: chi tratta cosa. Default unisex.
+ $this->execute("
+ ALTER TABLE `staff`
+ ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
+ AFTER `role`
+ ");
+
+ // services: a chi è rivolto il servizio. Default unisex.
+ $this->execute("
+ ALTER TABLE `services`
+ ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
+ AFTER `category`
+ ");
+ }
+}
diff --git a/db/seeds/SubscriptionPlansSeeder.php b/db/seeds/SubscriptionPlansSeeder.php
new file mode 100644
index 0000000..9c52e55
--- /dev/null
+++ b/db/seeds/SubscriptionPlansSeeder.php
@@ -0,0 +1,39 @@
+fetchRow("SELECT COUNT(*) AS c FROM subscription_plans")['c'];
+ if ($count > 0) {
+ return;
+ }
+
+ $data = [
+ [
+ 'name' => 'Base',
+ 'description' => 'Prenotazioni online, agenda e gestione clienti.',
+ 'price' => 19.90,
+ 'billing_period' => 'monthly',
+ 'is_active' => 1,
+ ],
+ [
+ 'name' => 'Pro',
+ 'description' => 'Tutto del piano Base + più staff, statistiche e priorità in ricerca.',
+ 'price' => 39.90,
+ 'billing_period' => 'monthly',
+ 'is_active' => 1,
+ ],
+ ];
+
+ $this->table('subscription_plans')->insert($data)->saveData();
+ }
+}
diff --git a/public/assets/img/logo/alogo.png b/public/assets/img/logo/alogo.png
new file mode 100644
index 0000000..84d0965
Binary files /dev/null and b/public/assets/img/logo/alogo.png differ
diff --git a/public/assets/img/logo/logo.png b/public/assets/img/logo/logo.png
index 84d0965..a319aee 100644
Binary files a/public/assets/img/logo/logo.png and b/public/assets/img/logo/logo.png differ
diff --git a/public/business.php b/public/business.php
new file mode 100644
index 0000000..ff8e011
--- /dev/null
+++ b/public/business.php
@@ -0,0 +1,540 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
★ La piattaforma per il tuo salone
+
Il tuo salone, sempre prenotabile online
+
Basta telefonate e agende cartacee. Con Come Li Facciamo i tuoi clienti prenotano da soli, a qualsiasi ora, e tu riempi la giornata senza pensieri.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
24/7
+
Prenotazioni sempre aperte, anche a salone chiuso
+
+
+
0€
+
Nessun costo di installazione o hardware
+
+
+
5 min
+
Per attivare il salone e iniziare
+
+
+
+
+
+
+
+
Come funziona
+
Tre passi e sei operativo. Nessuna competenza tecnica richiesta.
+
+
+
1
+
Crea il tuo salone
+
Ti registri, inserisci i servizi, gli orari e il tuo staff. In pochi minuti la tua pagina è online.
+
+
+
2
+
Ricevi prenotazioni
+
I clienti ti trovano nella ricerca, scelgono servizio, operatore e orario. Tu li vedi subito in agenda.
+
+
+
3
+
Gestisci tutto
+
Confermi, sposti o annulli con un tocco. Agenda per operatore, storico clienti e tutto sotto controllo.
+
+
+
+
+
+
+
+
+
Perché sceglierci
+
Tutto quello che serve per gestire e far crescere il salone, in un unico posto.
+
+
+
+
+
Agenda intelligente
+
Vista per operatore, slot automatici in base a orari e durata dei servizi. Niente più doppie prenotazioni.
+
+
+
+
+
+
Nuovi clienti
+
Comparì nella ricerca dei clienti della tua zona e riempi gli spazi vuoti in agenda.
+
+
+
+
+
+
Prenotazioni da mobile
+
I tuoi clienti prenotano dallo smartphone in pochi tap, senza chiamare né aspettare.
+
+
+
+
+
+
Pagina salone + QR
+
Una pagina professionale con i tuoi servizi e un QR code da mettere in vetrina o sui social.
+
+
+
+
+
+
Gestione staff
+
Ogni operatore ha la sua agenda e i suoi orari. Il cliente sceglie da chi farsi servire.
+
+
+
+
+
+
Clienti fidelizzati
+
Storico appuntamenti e preferiti: i clienti ti ritrovano e tornano con un tap.
+
+
+
+
+
+
+
+
+
+
+
Prezzi trasparenti, senza sorprese
+
Un canone chiaro, tutto incluso. Nessun costo nascosto, nessuna commissione sulle prenotazioni, nessun vincolo a lungo termine.
+
+
Nessuna commissione
+
Nessun vincolo
+
Disdici quando vuoi
+
+
Vedi i piani e i prezzi
+
+
+
+
+
+
+ Pronto a far risplendere il tuo salone?
+ Attivalo oggi e inizia a ricevere prenotazioni da subito.
+ Inizia ora
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/checkout.php b/public/checkout.php
new file mode 100644
index 0000000..67c080c
--- /dev/null
+++ b/public/checkout.php
@@ -0,0 +1,438 @@
+getConnection();
+
+function getFlash(): ?array
+{
+ if (!isset($_SESSION['flash'])) return null;
+ $f = $_SESSION['flash'];
+ unset($_SESSION['flash']);
+ return $f;
+}
+function euro($v): string
+{
+ return number_format((float)$v, 2, ',', '.');
+}
+
+$plan_id = (int)($_GET['plan'] ?? 0);
+$cycle = ($_GET['cycle'] ?? 'monthly') === 'yearly' ? 'yearly' : 'monthly';
+
+if ($plan_id <= 0) {
+ header('Location: pricing.php');
+ exit;
+}
+
+// Piano scelto
+$st = $pdo->prepare("SELECT id, name, description, price FROM subscription_plans WHERE id = ? AND is_active = 1");
+$st->execute([$plan_id]);
+$plan = $st->fetch(PDO::FETCH_ASSOC);
+if (!$plan) {
+ header('Location: pricing.php');
+ exit;
+}
+
+$monthly = (float)$plan['price'];
+$total = $cycle === 'yearly' ? $monthly * 11 : $monthly; // annuale = 11 mesi
+$cycleLabel = $cycle === 'yearly' ? 'Annuale (1 mese gratis)' : 'Mensile';
+
+// Ripopola i campi in caso di errore (tornano da checkout_process)
+$old = $_SESSION['checkout_old'] ?? [];
+unset($_SESSION['checkout_old']);
+function old(string $k, array $old): string
+{
+ return htmlspecialchars($old[$k] ?? '', ENT_QUOTES, 'UTF-8');
+}
+
+$flash = getFlash();
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Completa il tuo abbonamento
+
Crea il tuo account e attiva il salone in un solo passaggio.
+
+
+
= htmlspecialchars($flash['text']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/checkout_process.php b/public/checkout_process.php
new file mode 100644
index 0000000..84572fe
--- /dev/null
+++ b/public/checkout_process.php
@@ -0,0 +1,193 @@
+getConnection();
+
+// Ruolo Owner (da headscript: ROLE_OWNER = 2)
+const ROLE_OWNER = 2;
+
+function setFlash(string $type, string $text): void
+{
+ $_SESSION['flash'] = ['type' => $type, 'text' => $text];
+}
+
+function backToCheckout(array $post, string $msg): void
+{
+ // Ripopola il form e torna al checkout con l'errore
+ $_SESSION['checkout_old'] = $post;
+ setFlash('danger', $msg);
+ $plan = (int)($post['plan_id'] ?? 0);
+ $cycle = ($post['cycle'] ?? 'monthly') === 'yearly' ? 'yearly' : 'monthly';
+ header("Location: checkout.php?plan={$plan}&cycle={$cycle}");
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ header('Location: pricing.php');
+ exit;
+}
+
+$post = $_POST;
+
+// --- Raccolta dati ---
+$plan_id = (int)($post['plan_id'] ?? 0);
+$cycle = ($post['cycle'] ?? 'monthly') === 'yearly' ? 'yearly' : 'monthly';
+
+$first_name = trim($post['first_name'] ?? '');
+$last_name = trim($post['last_name'] ?? '');
+$email = trim($post['email'] ?? '');
+$password = (string)($post['password'] ?? '');
+$password2 = (string)($post['password_confirm'] ?? '');
+
+$shop_name = trim($post['shop_name'] ?? '');
+
+$business_name = trim($post['business_name'] ?? '');
+$vat_number = trim($post['vat_number'] ?? '');
+$tax_code = trim($post['tax_code'] ?? '');
+$address = trim($post['address'] ?? '');
+$city = trim($post['city'] ?? '');
+$province = trim($post['province'] ?? '');
+$zip_code = trim($post['zip_code'] ?? '');
+$pec = trim($post['pec'] ?? '');
+$sdi_code = trim($post['sdi_code'] ?? '');
+
+// --- Validazioni ---
+if ($plan_id <= 0) {
+ header('Location: pricing.php');
+ exit;
+}
+
+if ($first_name === '' || $last_name === '' || $email === '' || $password === '') {
+ backToCheckout($post, 'Compila tutti i campi obbligatori dell\'account.');
+}
+if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ backToCheckout($post, 'Inserisci un indirizzo email valido.');
+}
+if (strlen($password) < 8) {
+ backToCheckout($post, 'La password deve avere almeno 8 caratteri.');
+}
+if ($password !== $password2) {
+ backToCheckout($post, 'Le due password non coincidono.');
+}
+if ($shop_name === '') {
+ backToCheckout($post, 'Inserisci il nome del salone.');
+}
+if ($business_name === '' || $vat_number === '' || $address === '' || $city === '' || $zip_code === '') {
+ backToCheckout($post, 'Compila tutti i dati di fatturazione obbligatori.');
+}
+// Normativa IT: almeno uno tra PEC e SDI
+if ($pec === '' && $sdi_code === '') {
+ backToCheckout($post, 'Per la fattura elettronica inserisci almeno la PEC o il Codice SDI.');
+}
+if ($pec !== '' && !filter_var($pec, FILTER_VALIDATE_EMAIL)) {
+ backToCheckout($post, 'La PEC non è un indirizzo valido.');
+}
+
+try {
+ // Email già registrata?
+ $chk = $pdo->prepare("SELECT id FROM auth_users WHERE email = ?");
+ $chk->execute([$email]);
+ if ($chk->fetch()) {
+ backToCheckout($post, 'Esiste già un account con questa email. Prova ad accedere.');
+ }
+
+ // Piano valido + prezzo
+ $ps = $pdo->prepare("SELECT id, price FROM subscription_plans WHERE id = ? AND is_active = 1");
+ $ps->execute([$plan_id]);
+ $plan = $ps->fetch(PDO::FETCH_ASSOC);
+ if (!$plan) {
+ backToCheckout($post, 'Il piano selezionato non è più disponibile.');
+ }
+
+ // Slug salone univoco (generazione al volo, finché non c'è il sistema slug dedicato)
+ $baseSlug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $shop_name), '-'));
+ if ($baseSlug === '') $baseSlug = 'salone';
+ $slug = $baseSlug;
+ $i = 1;
+ $slugChk = $pdo->prepare("SELECT id FROM shops WHERE slug = ?");
+ while (true) {
+ $slugChk->execute([$slug]);
+ if (!$slugChk->fetch()) break;
+ $slug = $baseSlug . '-' . (++$i);
+ }
+
+ // Durata abbonamento
+ $expires = $cycle === 'yearly'
+ ? date('Y-m-d', strtotime('+1 year'))
+ : date('Y-m-d', strtotime('+1 month'));
+
+ // ---- TRANSAZIONE: tutto o niente ----
+ $pdo->beginTransaction();
+
+ // 1. Utente Owner
+ $hash = password_hash($password, PASSWORD_BCRYPT);
+ $insU = $pdo->prepare("
+ INSERT INTO auth_users (email, password, role_id, status, first_name, last_name, created_at)
+ VALUES (?, ?, ?, 'Active', ?, ?, NOW())
+ ");
+ $insU->execute([$email, $hash, ROLE_OWNER, $first_name, $last_name]);
+ $user_id = (int)$pdo->lastInsertId();
+
+ // 2. Salone (indirizzo iniziale = indirizzo fiscale; modificabile poi dal profilo)
+ $insS = $pdo->prepare("
+ INSERT INTO shops (owner_id, name, slug, address, city, province, zip_code, email, active, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())
+ ");
+ $insS->execute([$user_id, $shop_name, $slug, $address, $city, ($province ?: null), $zip_code, $email]);
+ $shop_id = (int)$pdo->lastInsertId();
+
+ // 3. Profilo fiscale
+ $insB = $pdo->prepare("
+ INSERT INTO billing_profiles
+ (user_id, shop_id, business_name, vat_number, tax_code, address, zip_code, city, province, pec, sdi_code)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ");
+ $insB->execute([
+ $user_id,
+ $shop_id,
+ $business_name,
+ $vat_number,
+ ($tax_code ?: null),
+ $address,
+ $zip_code,
+ $city,
+ ($province ?: null),
+ ($pec ?: null),
+ ($sdi_code ?: null)
+ ]);
+
+ // 4. Abbonamento attivo (pagamento SIMULATO riuscito)
+ $insSub = $pdo->prepare("
+ INSERT INTO shop_subscriptions (shop_id, plan_id, status, started_at, expires_at)
+ VALUES (?, ?, 'active', CURDATE(), ?)
+ ");
+ $insSub->execute([$shop_id, $plan_id, $expires]);
+
+ $pdo->commit();
+
+ // --- Login: impostiamo la sessione userarea come fa il resto del sito ---
+ // NB: headscript usa Auth::user() (Laravel). Qui creiamo comunque le chiavi
+ // di sessione PHP native usate nelle pagine userarea, così l'utente è "dentro".
+ $_SESSION['iduserlogin'] = $user_id;
+ $_SESSION['nameuser'] = $first_name;
+ $_SESSION['surnameuser'] = $last_name;
+ $_SESSION['emailuser'] = $email;
+
+ setFlash('success', 'Benvenuto! Il tuo salone è attivo. Completa il profilo e inizia a ricevere prenotazioni.');
+ // Va in area titolare (l'agenda). Se la sessione Laravel non è attiva,
+ // la pagina rimanderà al login: in tal caso l'utente accede con le sue credenziali.
+ header('Location: userarea/appointments.php');
+ exit;
+} catch (Throwable $e) {
+ if ($pdo->inTransaction()) $pdo->rollBack();
+ error_log('checkout_process: ' . $e->getMessage());
+ backToCheckout($post, 'Si è verificato un errore durante l\'attivazione. Riprova.');
+}
diff --git a/public/cron_subscriptions.php b/public/cron_subscriptions.php
new file mode 100644
index 0000000..d27743b
--- /dev/null
+++ b/public/cron_subscriptions.php
@@ -0,0 +1,85 @@
+getConnection();
+
+$today = date('Y-m-d');
+$log = [];
+
+try {
+ $pdo->beginTransaction();
+
+ // 1. Scaduti: erano active, la data è passata → expired + salone spento
+ $exp = $pdo->prepare("
+ SELECT shop_id FROM shop_subscriptions
+ WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at < ?
+ ");
+ $exp->execute([$today]);
+ $expiredShops = $exp->fetchAll(PDO::FETCH_COLUMN);
+
+ if ($expiredShops) {
+ $updSub = $pdo->prepare("
+ UPDATE shop_subscriptions SET status = 'expired'
+ WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at < ?
+ ");
+ $updSub->execute([$today]);
+
+ // Spegni i saloni relativi
+ $in = implode(',', array_fill(0, count($expiredShops), '?'));
+ $updShop = $pdo->prepare("UPDATE shops SET active = 0 WHERE id IN ($in)");
+ $updShop->execute($expiredShops);
+
+ $log[] = count($expiredShops) . " salone/i scaduto/i e disattivato/i.";
+ }
+
+ // 2. Riattivazione: abbonamento tornato valido ma salone ancora spento.
+ // (utile se un rinnovo ha rimesso status=active ma active era 0)
+ $rea = $pdo->prepare("
+ SELECT s.id
+ FROM shops s
+ JOIN shop_subscriptions sub ON sub.shop_id = s.id
+ WHERE s.active = 0
+ AND sub.status = 'active'
+ AND (sub.expires_at IS NULL OR sub.expires_at >= ?)
+ ");
+ $rea->execute([$today]);
+ $reactivate = $rea->fetchAll(PDO::FETCH_COLUMN);
+
+ if ($reactivate) {
+ $in = implode(',', array_fill(0, count($reactivate), '?'));
+ $updOn = $pdo->prepare("UPDATE shops SET active = 1 WHERE id IN ($in)");
+ $updOn->execute($reactivate);
+ $log[] = count($reactivate) . " salone/i riattivato/i.";
+ }
+
+ $pdo->commit();
+
+ if (empty($log)) $log[] = "Nessuna modifica.";
+ $line = '[' . date('Y-m-d H:i:s') . '] ' . implode(' ', $log) . PHP_EOL;
+ echo $line;
+ // Log su file accanto allo script
+ @file_put_contents(__DIR__ . '/cron_subscriptions.log', $line, FILE_APPEND);
+} catch (Throwable $e) {
+ if ($pdo->inTransaction()) $pdo->rollBack();
+ $err = '[' . date('Y-m-d H:i:s') . '] ERRORE: ' . $e->getMessage() . PHP_EOL;
+ echo $err;
+ @file_put_contents(__DIR__ . '/cron_subscriptions.log', $err, FILE_APPEND);
+}
diff --git a/public/cssinclude.php b/public/cssinclude.php
index 59bc1bf..8df15e3 100644
--- a/public/cssinclude.php
+++ b/public/cssinclude.php
@@ -8,18 +8,101 @@
\ No newline at end of file
diff --git a/public/footer.php b/public/footer.php
index 6340a75..e790282 100644
--- a/public/footer.php
+++ b/public/footer.php
@@ -1,194 +1,189 @@
-