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

+
+
+ +
+
+
+
+ + + + + + + + +
+
+
Elenco Piani
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeNomePrezzoIntervalloTrialStripe ProductStripe Price *StatoAzioni
+
+ +
+ +
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
+
+ +
+
+
+ + +
+ + +
+ + + +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Reset +
+
+
+
+ + +
+
+
+
+
Schools
+
rows
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SchoolOwnerSchool statusSubscriptionPlanPeriodTrialCancel at period endStripe IDsUpdatedActions
+
+
+ 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) ?> + + + + +
    + + + +
    +
    +
    +
    +
    Profilo Insegnante
    +
    + +
    + +
    + + +
    + + + +
    + + +
    + + +
    +
    +
    + Foto Profilo + +
    + + + Max 2MB – JPG, PNG, GIF +
    + + +
    + + +
    + +
    +
    + QR Code +
    + +
    + +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    + +
    + +
    + + + Es: Hatha Yoga, Vinyasa, Yin, Restorative... +
    + +
    + +
    + > + +
    +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + + +
    + + + + + + + + \ No newline at end of file