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.

+ + +
+ + +
+ + + +
+ +
+ +
+

1 Il tuo account

+

Con questi dati accederai alla tua area titolare.

+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+

2 Il tuo salone

+

Il nome con cui i clienti ti troveranno.

+
+ + +
+
+ + +
+

3 Dati di fatturazione

+

Necessari per emettere la fattura dell'abbonamento.

+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+

Per la fattura elettronica è obbligatorio almeno uno tra PEC e Codice SDI.

+
+
+ + +
+
+

Riepilogo ordine

+
+
+ +
Prezzo mensile€
+ +
Durata12 mesi
+
Sconto−€
+ +
Durata1 mese
+ + +
+ Totale + € +
+ +
+ Pagamento simulato: nessun addebito reale. L'integrazione con Stripe arriverà a breve. +
+ + +

Dati protetti · disdici quando vuoi

+
+
+
+
+
+
+
+ + + + + + \ 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 @@ -