diff --git a/public/phototeachers/qrcodes/2-354894dfb52e966e.png b/public/phototeachers/qrcodes/2-354894dfb52e966e.png
new file mode 100644
index 0000000..0989777
Binary files /dev/null and b/public/phototeachers/qrcodes/2-354894dfb52e966e.png differ
diff --git a/public/userarea/admin_subscription_plans.php b/public/userarea/admin_subscription_plans.php
new file mode 100644
index 0000000..7210b49
--- /dev/null
+++ b/public/userarea/admin_subscription_plans.php
@@ -0,0 +1,557 @@
+getConnection();
+
+// Check login
+if (!isset($iduserlogin)) {
+ die("Errore: ID utente non definito.");
+}
+
+/**
+ * Check if the current user is admin.
+ * IMPORTANT: set your real admin role_id(s) here.
+ */
+function isAdmin(PDO $pdo, int $userId): bool
+{
+ // TODO: adjust these role ids according to your system
+ $adminRoleIds = [1]; // e.g. 1 = superadmin
+
+ $stmt = $pdo->prepare("SELECT role_id FROM auth_users WHERE id = ?");
+ $stmt->execute([$userId]);
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$row) return false;
+ return in_array((int)$row['role_id'], $adminRoleIds, true);
+}
+
+if (!isAdmin($pdo, (int)$iduserlogin)) {
+ die("Accesso negato: pagina riservata all'amministratore.");
+}
+
+function formatMoneyFromCents(int $cents, string $currency): string
+{
+ $amount = number_format($cents / 100, 2, ',', '.');
+ return $amount . ' ' . strtoupper($currency);
+}
+
+// Handle POST actions
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $action = $_POST['action'] ?? '';
+
+ // Common fields
+ $code = trim($_POST['code'] ?? '');
+ $name = trim($_POST['name'] ?? '');
+ $description = trim($_POST['description'] ?? '');
+
+ $stripe_product_id = trim($_POST['stripe_product_id'] ?? '');
+ $stripe_price_id = trim($_POST['stripe_price_id'] ?? '');
+
+ $currency = strtoupper(trim($_POST['currency'] ?? 'EUR'));
+ $unit_amount = (int)($_POST['unit_amount'] ?? 0); // cents
+ $interval = in_array(($_POST['interval'] ?? ''), ['day', 'week', 'month', 'year'], true) ? $_POST['interval'] : 'month';
+ $interval_count = max(1, (int)($_POST['interval_count'] ?? 1));
+ $trial_days = max(0, (int)($_POST['trial_days'] ?? 0));
+ $is_active = isset($_POST['is_active']) ? 1 : 0;
+
+ // ADD
+ if ($action === 'add_plan') {
+ if ($code === '' || $name === '') {
+ $error = "Code e Nome sono obbligatori.";
+ } elseif ($stripe_price_id === '') {
+ $error = "Stripe Price ID è obbligatorio (campo NOT NULL in tabella).";
+ } elseif (strlen($currency) !== 3) {
+ $error = "Currency deve essere nel formato ISO (es. EUR).";
+ } elseif ($unit_amount < 0) {
+ $error = "Unit amount non può essere negativo.";
+ } else {
+ try {
+ $stmt = $pdo->prepare("
+ INSERT INTO billing_plans
+ (code, name, description, stripe_product_id, stripe_price_id, currency, unit_amount, `interval`, interval_count, trial_days, is_active, created_at, updated_at)
+ VALUES
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
+ ");
+ $stmt->execute([
+ $code,
+ $name,
+ $description ?: null,
+ $stripe_product_id ?: null,
+ $stripe_price_id,
+ $currency,
+ $unit_amount,
+ $interval,
+ $interval_count,
+ $trial_days,
+ $is_active
+ ]);
+ $success_message = "Piano creato con successo.";
+ } catch (PDOException $e) {
+ $error = "Errore durante la creazione del piano: " . $e->getMessage();
+ }
+ }
+ }
+
+ // EDIT
+ if ($action === 'edit_plan') {
+ $id = (int)($_POST['id'] ?? 0);
+
+ if ($id <= 0) {
+ $error = "ID piano non valido.";
+ } elseif ($code === '' || $name === '') {
+ $error = "Code e Nome sono obbligatori.";
+ } elseif ($stripe_price_id === '') {
+ $error = "Stripe Price ID è obbligatorio (campo NOT NULL in tabella).";
+ } elseif (strlen($currency) !== 3) {
+ $error = "Currency deve essere nel formato ISO (es. EUR).";
+ } elseif ($unit_amount < 0) {
+ $error = "Unit amount non può essere negativo.";
+ } else {
+ try {
+ $stmt = $pdo->prepare("
+ UPDATE billing_plans
+ SET
+ code = ?,
+ name = ?,
+ description = ?,
+ stripe_product_id = ?,
+ stripe_price_id = ?,
+ currency = ?,
+ unit_amount = ?,
+ `interval` = ?,
+ interval_count = ?,
+ trial_days = ?,
+ is_active = ?,
+ updated_at = NOW()
+ WHERE id = ?
+ ");
+ $stmt->execute([
+ $code,
+ $name,
+ $description ?: null,
+ $stripe_product_id ?: null,
+ $stripe_price_id,
+ $currency,
+ $unit_amount,
+ $interval,
+ $interval_count,
+ $trial_days,
+ $is_active,
+ $id
+ ]);
+ $success_message = "Piano aggiornato con successo.";
+ } catch (PDOException $e) {
+ $error = "Errore durante l'aggiornamento del piano: " . $e->getMessage();
+ }
+ }
+ }
+
+ // DISABLE
+ if ($action === 'disable_plan') {
+ $id = (int)($_POST['id'] ?? 0);
+ if ($id <= 0) {
+ $error = "ID piano non valido.";
+ } else {
+ $stmt = $pdo->prepare("UPDATE billing_plans SET is_active = 0, updated_at = NOW() WHERE id = ?");
+ $stmt->execute([$id]);
+ $success_message = "Piano disattivato.";
+ }
+ }
+
+ // ENABLE
+ if ($action === 'enable_plan') {
+ $id = (int)($_POST['id'] ?? 0);
+ if ($id <= 0) {
+ $error = "ID piano non valido.";
+ } else {
+ $stmt = $pdo->prepare("UPDATE billing_plans SET is_active = 1, updated_at = NOW() WHERE id = ?");
+ $stmt->execute([$id]);
+ $success_message = "Piano riattivato.";
+ }
+ }
+}
+
+// Fetch plans
+$stmt = $pdo->prepare("SELECT * FROM billing_plans ORDER BY is_active DESC, name ASC");
+$stmt->execute();
+$plans = $stmt->fetchAll(PDO::FETCH_ASSOC);
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Subscription Plans (Admin)
+
Gestione piani abbonamento delle scuole
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Code |
+ Nome |
+ Prezzo |
+ Intervallo |
+ Trial |
+ Stripe Product |
+ Stripe Price * |
+ Stato |
+ Azioni |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+ |
+ |
+ 0 ? ((int)$p['trial_days'] . ' gg') : '—'; ?> |
+
+
+
+
+ —
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ Nota: `unit_amount` è in centesimi (es. 2990 = 29,90 EUR)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/userarea/admin_subscriptions.php b/public/userarea/admin_subscriptions.php
new file mode 100644
index 0000000..ae9c70a
--- /dev/null
+++ b/public/userarea/admin_subscriptions.php
@@ -0,0 +1,544 @@
+getConnection();
+
+// ---- Auth check ----
+if (!isset($iduserlogin)) {
+ die("Access denied.");
+}
+
+// ---- Admin check (Vanguard usually uses role_id=1 for admin) ----
+// Adjust role_id list if needed.
+$stmt = $pdo->prepare("SELECT role_id, email FROM auth_users WHERE id = ? LIMIT 1");
+$stmt->execute([$iduserlogin]);
+$me = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$me || !in_array((int)$me['role_id'], [1])) {
+ die("Access denied: admin only.");
+}
+
+// ---- Handle POST actions ----
+$success_message = null;
+$error = null;
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
+ $action = $_POST['action'];
+
+ // Update school status (local app status)
+ if ($action === 'update_school_status') {
+ $school_id = (int)($_POST['school_id'] ?? 0);
+ $new_status = $_POST['status'] ?? 'active';
+ $allowed = ['active', 'inactive', 'suspended'];
+
+ if ($school_id <= 0 || !in_array($new_status, $allowed, true)) {
+ $error = "Invalid request.";
+ } else {
+ $stmt = $pdo->prepare("UPDATE schools SET status = ? WHERE id = ?");
+ if ($stmt->execute([$new_status, $school_id])) {
+ $success_message = "School status updated.";
+ } else {
+ $error = "Failed updating school status.";
+ }
+ }
+ }
+
+ // NOTE: Stripe actions (cancel, resume, change plan) should call Stripe API.
+ // Here we provide placeholders so UI is ready.
+ if ($action === 'flag_cancel_at_period_end') {
+ $sub_id = (int)($_POST['subscription_row_id'] ?? 0);
+ $flag = (int)($_POST['flag'] ?? 0);
+ $school_id = (int)($_POST['school_id'] ?? 0);
+
+ if ($sub_id <= 0 || $school_id <= 0 || !in_array($flag, [0, 1], true)) {
+ $error = "Invalid request.";
+ } else {
+ $stmt = $pdo->prepare("
+ UPDATE school_subscriptions
+ SET cancel_at_period_end = ?
+ WHERE id = ? AND school_id = ?
+ ");
+ $ok = $stmt->execute([$flag, $sub_id, $school_id]);
+
+ if ($ok) {
+ $success_message = $flag ? "Marked cancel at period end (LOCAL)." : "Unmarked cancel at period end (LOCAL).";
+ } else {
+ $error = "Failed updating subscription flag.";
+ }
+ }
+ }
+}
+
+// ---- Filters (GET) ----
+$q = trim($_GET['q'] ?? '');
+$plan_id = (int)($_GET['plan_id'] ?? 0);
+$sub_status = trim($_GET['sub_status'] ?? ''); // e.g. active, trialing, past_due, canceled, incomplete, unpaid
+$school_status = trim($_GET['school_status'] ?? ''); // active, inactive, suspended
+$has_sub = $_GET['has_sub'] ?? ''; // '1' or '0' or ''
+
+$where = [];
+$params = [];
+
+// Search by school name/email/owner email
+if ($q !== '') {
+ $where[] = "(s.name LIKE ? OR s.email LIKE ? OR ou.email LIKE ? OR CONCAT(ou.first_name,' ',ou.last_name) LIKE ?)";
+ $like = '%' . $q . '%';
+ $params[] = $like;
+ $params[] = $like;
+ $params[] = $like;
+ $params[] = $like;
+}
+
+if ($plan_id > 0) {
+ $where[] = "ss.plan_id = ?";
+ $params[] = $plan_id;
+}
+
+if ($sub_status !== '') {
+ $where[] = "ss.status = ?";
+ $params[] = $sub_status;
+}
+
+if ($school_status !== '') {
+ $where[] = "s.status = ?";
+ $params[] = $school_status;
+}
+
+if ($has_sub === '1') {
+ $where[] = "ss.id IS NOT NULL";
+} elseif ($has_sub === '0') {
+ $where[] = "ss.id IS NULL";
+}
+
+$sqlWhere = '';
+if (!empty($where)) {
+ $sqlWhere = "WHERE " . implode(" AND ", $where);
+}
+
+// ---- Load plans for filter dropdown ----
+$stmt = $pdo->prepare("SELECT id, code, name, currency, unit_amount, `interval`, interval_count, is_active FROM billing_plans ORDER BY is_active DESC, unit_amount ASC, name ASC");
+$stmt->execute();
+$plans = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// ---- Main query: schools + subscription + owner + plan ----
+$stmt = $pdo->prepare("
+ SELECT
+ s.id AS school_id,
+ s.name AS school_name,
+ s.email AS school_email,
+ s.status AS school_status,
+ s.created_at AS school_created_at,
+
+ ou.id AS owner_id,
+ ou.first_name AS owner_first_name,
+ ou.last_name AS owner_last_name,
+ ou.email AS owner_email,
+
+ ss.id AS subscription_row_id,
+ ss.stripe_customer_id,
+ ss.stripe_subscription_id,
+ COALESCE(ss.status, 'none') AS subscription_status,
+ ss.current_period_start,
+ ss.current_period_end,
+ ss.trial_start,
+ ss.trial_end,
+ ss.cancel_at_period_end,
+ ss.updated_at AS subscription_updated_at,
+
+ bp.id AS plan_id,
+ bp.code AS plan_code,
+ bp.name AS plan_name,
+ bp.currency,
+ bp.unit_amount,
+ bp.`interval`,
+ bp.interval_count
+
+ FROM schools s
+ JOIN auth_users ou ON ou.id = s.owner_id
+ LEFT JOIN school_subscriptions ss ON ss.school_id = s.id
+ LEFT JOIN billing_plans bp ON bp.id = ss.plan_id
+
+ $sqlWhere
+ ORDER BY
+ -- Put schools without subscription at the bottom
+ CASE WHEN ss.id IS NULL THEN 1 ELSE 0 END ASC,
+
+ -- Show problematic subscriptions first
+ FIELD(ss.status, 'past_due','unpaid','incomplete','incomplete_expired','canceled','paused','trialing','active') ASC,
+
+ s.created_at DESC
+
+");
+$stmt->execute($params);
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// ---- Helpers ----
+function h($v)
+{
+ return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
+}
+
+function moneyFmt($unit_amount, $currency)
+{
+ if ($unit_amount === null || $currency === null) return '—';
+ $amount = ((int)$unit_amount) / 100;
+ return number_format($amount, 2, ',', '.') . ' ' . strtoupper($currency);
+}
+
+function dateFmt($dt)
+{
+ if (!$dt) return '—';
+ // Accept both timestamp and datetime strings
+ $ts = is_numeric($dt) ? (int)$dt : strtotime($dt);
+ if (!$ts) return '—';
+ return date('d/m/Y', $ts);
+}
+
+function badgeClassForSub($status)
+{
+ $map = [
+ 'active' => 'bg-success',
+ 'trialing' => 'bg-info',
+ 'past_due' => 'bg-warning',
+ 'unpaid' => 'bg-danger',
+ 'incomplete' => 'bg-warning',
+ 'incomplete_expired' => 'bg-danger',
+ 'canceled' => 'bg-secondary',
+ 'paused' => 'bg-secondary',
+ 'none' => 'bg-secondary',
+ ];
+ return $map[$status] ?? 'bg-dark';
+}
+
+function badgeClassForSchool($status)
+{
+ $map = [
+ 'active' => 'bg-success',
+ 'inactive' => 'bg-secondary',
+ 'suspended' => 'bg-danger',
+ ];
+ return $map[$status] ?? 'bg-dark';
+}
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Admin Subscriptions
+
Schools + Stripe subscription status overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | School |
+ Owner |
+ School status |
+ Subscription |
+ Plan |
+ Period |
+ Trial |
+ Cancel at period end |
+ Stripe IDs |
+ Updated |
+ Actions |
+
+
+
+
+
+
+ |
+
+
+ ID: ·
+
+
+ Created:
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ Qty:
+
+
+ |
+
+
+
+
+
+ ·
+
+
+ Every
+
+
+ —
+
+ |
+
+
+
+
+ Start:
+
+
+ End:
+
+
+ —
+
+ |
+
+
+
+
+ Start:
+
+
+ End:
+
+
+ —
+
+ |
+
+
+
+
+ Yes
+
+ No
+
+
+ —
+
+ |
+
+
+
+
+ cust:
+
+
+ sub:
+
+
+ —
+
+ |
+
+
+
+ |
+
+
+
+
+
+ —
+
+ |
+
+
+
+
+
+ | No results |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/userarea/include/navbar.php b/public/userarea/include/navbar.php
index 3d389c4..ec3ca73 100644
--- a/public/userarea/include/navbar.php
+++ b/public/userarea/include/navbar.php
@@ -145,11 +145,12 @@ if (!empty($_SESSION['school_id'])) {
-
+
hasRole('school_owner')) || (Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('teacher'))) : ?>
-
+
+ if ((Auth::user()->hasRole('school_owner')) || (Auth::user()->hasRole('Admin'))) : ?>
+
@@ -159,12 +160,38 @@ if (!empty($_SESSION['school_id'])) {
+ hasRole('school_owner')) || (Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('teacher'))) : ?>
+
+
+
+
+
+
+
+
+
+
+
hasRole('Admin'))) : ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/public/userarea/photoschool/2-1769006694-logo.jpg b/public/userarea/photoschool/2-1769006694-logo.jpg
new file mode 100644
index 0000000..5cf062c
Binary files /dev/null and b/public/userarea/photoschool/2-1769006694-logo.jpg differ
diff --git a/public/userarea/phototeachers/2-1768987735-profile.jpg b/public/userarea/phototeachers/2-1769005619-profile.jpg
similarity index 100%
rename from public/userarea/phototeachers/2-1768987735-profile.jpg
rename to public/userarea/phototeachers/2-1769005619-profile.jpg
diff --git a/public/userarea/phototeachers/2-1769005707-profile.jpg b/public/userarea/phototeachers/2-1769005707-profile.jpg
new file mode 100644
index 0000000..c3a33c4
Binary files /dev/null and b/public/userarea/phototeachers/2-1769005707-profile.jpg differ
diff --git a/public/userarea/school_dashboard.php b/public/userarea/school_dashboard.php
index 8a82024..95d6edb 100644
--- a/public/userarea/school_dashboard.php
+++ b/public/userarea/school_dashboard.php
@@ -35,9 +35,22 @@ $stmt->execute();
$categories = $stmt->fetchAll();
// Recupera tutti gli insegnanti della scuola
-$stmt = $pdo->prepare("SELECT id, first_name, last_name FROM teachers WHERE user_id = ? AND status = 'active' ORDER BY first_name, last_name");
-$stmt->execute([$iduserlogin]);
-$teachers = $stmt->fetchAll();
+// Teachers linked to this school (active/pending)
+$stmt = $pdo->prepare("
+ SELECT
+ t.id,
+ u.first_name,
+ u.last_name
+ FROM teacher_schools ts
+ JOIN teachers t ON ts.teacher_id = t.id
+ JOIN auth_users u ON t.user_id = u.id
+ WHERE ts.school_id = ?
+ AND ts.status IN ('active','pending')
+ ORDER BY u.first_name, u.last_name
+");
+$stmt->execute([$school_id]);
+$teachers = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
// Funzione per ridimensionare l'immagine
function resizeImage($source_path, $dest_path, $max_width = 800)
diff --git a/public/userarea/teacher_list.php b/public/userarea/teacher_list.php
index 956f914..40a6a39 100644
--- a/public/userarea/teacher_list.php
+++ b/public/userarea/teacher_list.php
@@ -120,6 +120,7 @@ $teachers = $stmt->fetchAll(PDO::FETCH_ASSOC);
Insegnanti - = htmlspecialchars($school_name) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+