浏览代码

Hockey Shop

root 1 月之前
父节点
当前提交
6553799d06
共有 100 个文件被更改,包括 16815 次插入 和 0 次删除
  1. 24 0
      admin/admin_auth.php
  2. 389 0
      admin/attributes.php
  3. 400 0
      admin/categories.php
  4. 226 0
      admin/edit_order.php
  5. 177 0
      admin/email_templates.php
  6. 68 0
      admin/google_login.php
  7. 179 0
      admin/index.php
  8. 150 0
      admin/login.php
  9. 8 0
      admin/logout.php
  10. 265 0
      admin/mail_settings.php
  11. 227 0
      admin/orders.php
  12. 150 0
      admin/product_attributes.php
  13. 846 0
      admin/products.php
  14. 80 0
      admin/sidebar.php
  15. 229 0
      admin/sms_settings.php
  16. 235 0
      admin/texts.php
  17. 262 0
      admin/themes.php
  18. 453 0
      admin/users.php
  19. 221 0
      admin/whatsapp_settings.php
  20. 108 0
      cart.php
  21. 68 0
      category.php
  22. 108 0
      checkout.php
  23. 7 0
      composer.json
  24. 1536 0
      composer.lock
  25. 28 0
      config/database.php
  26. 56 0
      config/functions.php
  27. 259 0
      google_auth.php
  28. 62 0
      includes/mailer.php
  29. 218 0
      index.php
  30. 142 0
      login.php
  31. 5 0
      logout.php
  32. 207 0
      privacy.php
  33. 115 0
      product.php
  34. 98 0
      profile.php
  35. 158 0
      register.php
  36. 119 0
      search.php
  37. 334 0
      seller_dashboard.php
  38. 175 0
      seller_orders.php
  39. 533 0
      sqlSchema/hockey_cms.sql
  40. 201 0
      terms.php
  41. 294 0
      themes/default/base.html.twig
  42. 148 0
      themes/default/cart.html.twig
  43. 338 0
      themes/default/index.html.twig
  44. 76 0
      themes/default/login.html.twig
  45. 130 0
      themes/default/product.html.twig
  46. 84 0
      themes/default/register.html.twig
  47. 335 0
      themes/default/seller_dashboard.html.twig
  48. 146 0
      themes/default/seller_orders.html.twig
  49. 二进制
      uploads/equipment/prod_2_1790588393.jpg
  50. 二进制
      uploads/equipment/prod_3_1790588897.jpg
  51. 二进制
      uploads/equipment/prod_4_1790589083.jpg
  52. 二进制
      uploads/equipment/prod_5_1790589860.jpg
  53. 二进制
      uploads/equipment/prod_6_1790590114.jpg
  54. 二进制
      uploads/logo_1790538884.png
  55. 二进制
      uploads/logo_1790539766.jpg
  56. 二进制
      uploads/logo_1790539773.jpg
  57. 二进制
      uploads/logo_1790540300.jpg
  58. 二进制
      uploads/logo_1790540314.jpg
  59. 二进制
      uploads/logo_1790540471.jpg
  60. 二进制
      uploads/logo_1790540487.jpg
  61. 二进制
      uploads/logo_1790540851.jpg
  62. 二进制
      uploads/logo_1790540911.png
  63. 二进制
      uploads/logo_1790669112.jpg
  64. 二进制
      uploads/logo_1790669218.jpg
  65. 二进制
      uploads/logo_1790669312.jpg
  66. 二进制
      uploads/logo_1790669499.jpg
  67. 22 0
      vendor/autoload.php
  68. 579 0
      vendor/composer/ClassLoader.php
  69. 396 0
      vendor/composer/InstalledVersions.php
  70. 21 0
      vendor/composer/LICENSE
  71. 44 0
      vendor/composer/autoload_classmap.php
  72. 20 0
      vendor/composer/autoload_files.php
  73. 9 0
      vendor/composer/autoload_namespaces.php
  74. 27 0
      vendor/composer/autoload_psr4.php
  75. 50 0
      vendor/composer/autoload_real.php
  76. 185 0
      vendor/composer/autoload_static.php
  77. 1583 0
      vendor/composer/installed.json
  78. 227 0
      vendor/composer/installed.php
  79. 25 0
      vendor/composer/platform_check.php
  80. 272 0
      vendor/firebase/php-jwt/CHANGELOG.md
  81. 30 0
      vendor/firebase/php-jwt/LICENSE
  82. 457 0
      vendor/firebase/php-jwt/README.md
  83. 1 0
      vendor/firebase/php-jwt/VERSION
  84. 53 0
      vendor/firebase/php-jwt/composer.json
  85. 18 0
      vendor/firebase/php-jwt/src/BeforeValidException.php
  86. 275 0
      vendor/firebase/php-jwt/src/CachedKeySet.php
  87. 30 0
      vendor/firebase/php-jwt/src/ExpiredException.php
  88. 376 0
      vendor/firebase/php-jwt/src/JWK.php
  89. 835 0
      vendor/firebase/php-jwt/src/JWT.php
  90. 20 0
      vendor/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
  91. 53 0
      vendor/firebase/php-jwt/src/Key.php
  92. 7 0
      vendor/firebase/php-jwt/src/SignatureInvalidException.php
  93. 203 0
      vendor/google/apiclient-services/LICENSE
  94. 19 0
      vendor/google/apiclient-services/README.md
  95. 7 0
      vendor/google/apiclient-services/SECURITY.md
  96. 50 0
      vendor/google/apiclient-services/autoload.php
  97. 27 0
      vendor/google/apiclient-services/composer.json
  98. 94 0
      vendor/google/apiclient-services/src/ACMEDNS.php
  99. 43 0
      vendor/google/apiclient-services/src/ACMEDNS/AcmeChallengeSet.php
  100. 80 0
      vendor/google/apiclient-services/src/ACMEDNS/AcmeTxtRecord.php

+ 24 - 0
admin/admin_auth.php

@@ -0,0 +1,24 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/admin_auth.php
+if (session_status() === PHP_SESSION_NONE) { 
+    session_start(); 
+}
+
+if (!isset($_SESSION['user_id'])) {
+    header("Location: ../login.php");
+    exit;
+}
+
+// Έλεγχος αν ο χρήστης ΔΕΝ είναι admin (role_id 1)
+if ($_SESSION['role_id'] != 1) {
+    // Αν είναι seller (role_id 2) ή έχει το flag is_seller
+    if ($_SESSION['role_id'] == 2 || !empty($_SESSION['is_seller'])) {
+        header("Location: ../seller_dashboard.php");
+        exit;
+    } else {
+        // Αλλιώς τον στέλνουμε στην αρχική σελίδα
+        header("Location: /index.php");
+        exit;
+    }
+}
+?>

+ 389 - 0
admin/attributes.php

@@ -0,0 +1,389 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/attributes.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Χαρακτηριστικά - Hockey CMS',
+        'header_title' => 'Διαχείριση Χαρακτηριστικών Εξοπλισμού',
+        'edit_group' => '✏️ Επεξεργασία Ομάδας',
+        'new_group' => '📁 Νέα Ομάδα (Group)',
+        'name_gr' => 'Όνομα 🇬🇷 (GR) *',
+        'name_en' => 'Όνομα 🇬🇧 (EN)',
+        'btn_save_group' => 'Αποθήκευση Ομάδας',
+        'btn_add_group' => 'Προσθήκη Ομάδας',
+        'edit_attr' => '✏️ Επεξεργασία Χαρακτηριστικού',
+        'new_attr' => '⚡ Νέο Χαρακτηριστικό',
+        'belongs_group' => 'Ανήκει στην Ομάδα *',
+        'select_group' => '-- Επίλεξε Ομάδα --',
+        'unit' => 'Μονάδα Μέτρησης (Προαιρετικό)',
+        'btn_save_attr' => 'Αποθήκευση Χαρακ.',
+        'btn_add_attr' => 'Προσθήκη Χαρακτηριστικού',
+        'existing_groups' => '📁 Υπάρχουσες Ομάδες',
+        'group_name' => 'Όνομα Ομάδας',
+        'actions' => 'Ενέργειες',
+        'no_groups' => 'Δεν υπάρχουν ομάδες.',
+        'existing_attrs' => '⚡ Υπάρχοντα Χαρακτηριστικά',
+        'attr_name' => 'Χαρακτηριστικό',
+        'no_attrs' => 'Δεν υπάρχουν χαρακτηριστικά.',
+        'btn_cancel' => 'Ακύρωση',
+        'msg_group_ok' => 'Η ομάδα δημιουργήθηκε!',
+        'msg_group_upd' => 'Η ομάδα ενημερώθηκε!',
+        'msg_attr_ok' => 'Το χαρακτηριστικό προστέθηκε!',
+        'msg_attr_upd' => 'Το χαρακτηριστικό ενημερώθηκε!',
+        'msg_group_del' => 'Η ομάδα διαγράφηκε επιτυχώς!',
+        'msg_attr_del' => 'Το χαρακτηριστικό διαγράφηκε επιτυχώς!',
+        'confirm_del_grp' => 'Σίγουρα διαγραφή της ομάδας;',
+        'confirm_del_attr' => 'Σίγουρα διαγραφή του χαρακτηριστικού;'
+    ],
+    'en' => [
+        'page_title' => 'Attributes - Hockey CMS',
+        'header_title' => 'Equipment Attributes Management',
+        'edit_group' => '✏️ Edit Group',
+        'new_group' => '📁 New Group',
+        'name_gr' => 'Name 🇬🇷 (GR) *',
+        'name_en' => 'Name 🇬🇧 (EN)',
+        'btn_save_group' => 'Save Group',
+        'btn_add_group' => 'Add Group',
+        'edit_attr' => '✏️ Edit Attribute',
+        'new_attr' => '⚡ New Attribute',
+        'belongs_group' => 'Belongs to Group *',
+        'select_group' => '-- Select Group --',
+        'unit' => 'Unit of Measurement (Optional)',
+        'btn_save_attr' => 'Save Attr.',
+        'btn_add_attr' => 'Add Attribute',
+        'existing_groups' => '📁 Existing Groups',
+        'group_name' => 'Group Name',
+        'actions' => 'Actions',
+        'no_groups' => 'No groups found.',
+        'existing_attrs' => '⚡ Existing Attributes',
+        'attr_name' => 'Attribute',
+        'no_attrs' => 'No attributes found.',
+        'btn_cancel' => 'Cancel',
+        'msg_group_ok' => 'Group created!',
+        'msg_group_upd' => 'Group updated!',
+        'msg_attr_ok' => 'Attribute added!',
+        'msg_attr_upd' => 'Attribute updated!',
+        'msg_group_del' => 'Group deleted successfully!',
+        'msg_attr_del' => 'Attribute deleted successfully!',
+        'confirm_del_grp' => 'Are you sure you want to delete this group?',
+        'confirm_del_attr' => 'Are you sure you want to delete this attribute?'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+// ==========================================
+// 1. ΧΕΙΡΙΣΜΟΣ POST (Προσθήκη / Επεξεργασία)
+// ==========================================
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
+    $action = $_POST['action'];
+
+    if ($action === 'add_group' || $action === 'edit_group') {
+        $name = trim($_POST['name']);
+        $name_en = trim($_POST['name_en']);
+        
+        if (!empty($name)) {
+            try {
+                if ($action === 'add_group') {
+                    $stmt = $pdo->prepare("INSERT INTO attribute_groups (name, name_en) VALUES (?, ?)");
+                    $stmt->execute([$name, $name_en]);
+                    $message = "<div class='alert success'>{$t['msg_group_ok']}</div>";
+                } else {
+                    $group_id = (int)$_POST['group_id'];
+                    $stmt = $pdo->prepare("UPDATE attribute_groups SET name = ?, name_en = ? WHERE id = ?");
+                    $stmt->execute([$name, $name_en, $group_id]);
+                    $message = "<div class='alert success'>{$t['msg_group_upd']}</div>";
+                }
+            } catch (Exception $e) {
+                $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+            }
+        }
+    }
+
+    if ($action === 'add_attr' || $action === 'edit_attr') {
+        $group_id = (int)$_POST['group_id'];
+        $name = trim($_POST['name']);
+        $name_en = trim($_POST['name_en']);
+        $unit = trim($_POST['unit']);
+        
+        if (!empty($group_id) && !empty($name)) {
+            try {
+                if ($action === 'add_attr') {
+                    $stmt = $pdo->prepare("INSERT INTO attributes (group_id, name, name_en, unit) VALUES (?, ?, ?, ?)");
+                    $stmt->execute([$group_id, $name, $name_en, $unit]);
+                    $message = "<div class='alert success'>{$t['msg_attr_ok']}</div>";
+                } else {
+                    $attr_id = (int)$_POST['attr_id'];
+                    $stmt = $pdo->prepare("UPDATE attributes SET group_id = ?, name = ?, name_en = ?, unit = ? WHERE id = ?");
+                    $stmt->execute([$group_id, $name, $name_en, $unit, $attr_id]);
+                    $message = "<div class='alert success'>{$t['msg_attr_upd']}</div>";
+                }
+            } catch (Exception $e) {
+                $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+            }
+        }
+    }
+}
+
+// ==========================================
+// 2. ΧΕΙΡΙΣΜΟΣ GET (Διαγραφές)
+// ==========================================
+if (isset($_GET['delete_group'])) {
+    $del_id = (int)$_GET['delete_group'];
+    $checkAttrs = $pdo->prepare("SELECT COUNT(*) FROM attributes WHERE group_id = ?");
+    $checkAttrs->execute([$del_id]);
+    $checkCats = $pdo->prepare("SELECT COUNT(*) FROM categories WHERE attribute_group_id = ?");
+    $checkCats->execute([$del_id]);
+    
+    if ($checkAttrs->fetchColumn() > 0) {
+        $message = "<div class='alert error'>Αδυναμία διαγραφής! Υπάρχουν χαρακτηριστικά μέσα σε αυτή την ομάδα.</div>";
+    } elseif ($checkCats->fetchColumn() > 0) {
+        $message = "<div class='alert error'>Αδυναμία διαγραφής! Η ομάδα συνδέεται με Κατηγορία.</div>";
+    } else {
+        $pdo->prepare("DELETE FROM attribute_groups WHERE id = ?")->execute([$del_id]);
+        $message = "<div class='alert success'>{$t['msg_group_del']}</div>";
+    }
+}
+
+if (isset($_GET['delete_attr'])) {
+    $del_id = (int)$_GET['delete_attr'];
+    $checkVals = $pdo->prepare("SELECT COUNT(*) FROM product_attribute_values WHERE attribute_id = ?");
+    $checkVals->execute([$del_id]);
+    
+    if ($checkVals->fetchColumn() > 0) {
+        $message = "<div class='alert error'>Αδυναμία διαγραφής! Το χαρακτηριστικό χρησιμοποιείται σε προϊόντα.</div>";
+    } else {
+        $pdo->prepare("DELETE FROM attributes WHERE id = ?")->execute([$del_id]);
+        $message = "<div class='alert success'>{$t['msg_attr_del']}</div>";
+    }
+}
+
+$edit_group = null;
+if (isset($_GET['edit_group'])) {
+    $stmt = $pdo->prepare("SELECT * FROM attribute_groups WHERE id = ?");
+    $stmt->execute([(int)$_GET['edit_group']]);
+    $edit_group = $stmt->fetch(PDO::FETCH_ASSOC);
+}
+
+$edit_attr = null;
+if (isset($_GET['edit_attr'])) {
+    $stmt = $pdo->prepare("SELECT * FROM attributes WHERE id = ?");
+    $stmt->execute([(int)$_GET['edit_attr']]);
+    $edit_attr = $stmt->fetch(PDO::FETCH_ASSOC);
+}
+
+$groups = $pdo->query("SELECT * FROM attribute_groups ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
+$attributes = $pdo->query("
+    SELECT a.id, a.name, a.name_en, a.unit, ag.name as group_name 
+    FROM attributes a 
+    JOIN attribute_groups ag ON a.group_id = ag.id 
+    ORDER BY ag.name, a.name
+")->fetchAll(PDO::FETCH_ASSOC);
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input, .form-group select { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus, .form-group select:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-cancel { padding: 12px 20px; background: rgba(255,255,255,0.05); color: var(--text-main); text-decoration: none; border-radius: 6px; display: inline-block; font-weight: 700; border: 1px solid var(--border-color); text-transform: uppercase; transition: 0.2s;}
+        .btn-cancel:hover { background: rgba(255,255,255,0.1); }
+        
+        .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; }
+        
+        table { width: 100%; border-collapse: collapse; margin-top: 10px; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; vertical-align: middle; }
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; }
+        table td { color: var(--text-main); font-size: 14px; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .action-link { padding: 6px 12px; text-decoration: none; border-radius: 4px; color: #fff; font-size: 12px; margin-right: 5px; display: inline-block; font-weight: 700; text-transform: uppercase; transition: 0.2s;}
+        .action-edit { background-color: rgba(249, 115, 22, 0.1); color: var(--primary); border: 1px solid rgba(249, 115, 22, 0.2); }
+        .action-edit:hover { background-color: var(--primary); color: white; }
+        .action-delete { background-color: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        .action-delete:hover { background-color: #ef4444; color: white; }
+        .lang-hint { font-size: 11px; color: var(--text-muted); }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div class="grid-2">
+            <div class="card">
+                <h3><?php echo $edit_group ? $t['edit_group'] : $t['new_group']; ?></h3>
+                <form method="POST" action="attributes.php">
+                    <input type="hidden" name="action" value="<?php echo $edit_group ? 'edit_group' : 'add_group'; ?>">
+                    <?php if ($edit_group): ?>
+                        <input type="hidden" name="group_id" value="<?php echo $edit_group['id']; ?>">
+                    <?php endif; ?>
+                    
+                    <div class="form-group">
+                        <label><?php echo $t['name_gr']; ?></label>
+                        <input type="text" name="name" value="<?php echo $edit_group ? htmlspecialchars($edit_group['name']) : ''; ?>" required>
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['name_en']; ?></label>
+                        <input type="text" name="name_en" value="<?php echo $edit_group ? htmlspecialchars($edit_group['name_en'] ?? '') : ''; ?>">
+                    </div>
+                    
+                    <button type="submit" class="btn"><?php echo $edit_group ? $t['btn_save_group'] : $t['btn_add_group']; ?></button>
+                    <?php if ($edit_group): ?>
+                        <a href="attributes.php" class="btn-cancel"><?php echo $t['btn_cancel']; ?></a>
+                    <?php endif; ?>
+                </form>
+            </div>
+
+            <div class="card">
+                <h3><?php echo $edit_attr ? $t['edit_attr'] : $t['new_attr']; ?></h3>
+                <form method="POST" action="attributes.php">
+                    <input type="hidden" name="action" value="<?php echo $edit_attr ? 'edit_attr' : 'add_attr'; ?>">
+                    <?php if ($edit_attr): ?>
+                        <input type="hidden" name="attr_id" value="<?php echo $edit_attr['id']; ?>">
+                    <?php endif; ?>
+                    
+                    <div class="form-group">
+                        <label><?php echo $t['belongs_group']; ?></label>
+                        <select name="group_id" required>
+                            <option value=""><?php echo $t['select_group']; ?></option>
+                            <?php foreach($groups as $group): ?>
+                                <option value="<?php echo $group['id']; ?>" <?php echo ($edit_attr && $edit_attr['group_id'] == $group['id']) ? 'selected' : ''; ?>>
+                                    <?php echo htmlspecialchars($group['name']); ?>
+                                </option>
+                            <?php endforeach; ?>
+                        </select>
+                    </div>
+                    
+                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
+                        <div class="form-group">
+                            <label><?php echo $t['name_gr']; ?></label>
+                            <input type="text" name="name" value="<?php echo $edit_attr ? htmlspecialchars($edit_attr['name']) : ''; ?>" required>
+                        </div>
+                        <div class="form-group">
+                            <label><?php echo $t['name_en']; ?></label>
+                            <input type="text" name="name_en" value="<?php echo $edit_attr ? htmlspecialchars($edit_attr['name_en'] ?? '') : ''; ?>">
+                        </div>
+                    </div>
+                    
+                    <div class="form-group">
+                        <label><?php echo $t['unit']; ?></label>
+                        <input type="text" name="unit" value="<?php echo $edit_attr ? htmlspecialchars($edit_attr['unit'] ?? '') : ''; ?>" placeholder="cm, kg, mm">
+                    </div>
+                    
+                    <button type="submit" class="btn" style="background: #10b981;"><?php echo $edit_attr ? $t['btn_save_attr'] : $t['btn_add_attr']; ?></button>
+                    <?php if ($edit_attr): ?>
+                        <a href="attributes.php" class="btn-cancel"><?php echo $t['btn_cancel']; ?></a>
+                    <?php endif; ?>
+                </form>
+            </div>
+        </div>
+
+        <div class="grid-2">
+            <div class="card">
+                <h3><?php echo $t['existing_groups']; ?></h3>
+                <table>
+                    <thead>
+                        <tr>
+                            <th><?php echo $t['group_name']; ?></th>
+                            <th style="width: 120px;"><?php echo $t['actions']; ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php if (count($groups) > 0): ?>
+                            <?php foreach($groups as $grp): ?>
+                            <tr>
+                                <td>
+                                    <strong><?php echo htmlspecialchars($grp['name']); ?></strong><br>
+                                    <span class="lang-hint"><?php echo htmlspecialchars($grp['name_en'] ?? ''); ?></span>
+                                </td>
+                                <td>
+                                    <a href="attributes.php?edit_group=<?php echo $grp['id']; ?>" class="action-link action-edit">Edit</a>
+                                    <a href="attributes.php?delete_group=<?php echo $grp['id']; ?>" class="action-link action-delete" onclick="return confirm('<?php echo $t['confirm_del_grp']; ?>');">Del</a>
+                                </td>
+                            </tr>
+                            <?php endforeach; ?>
+                        <?php else: ?>
+                            <tr><td colspan="2" style="text-align:center; color:var(--text-muted);"><?php echo $t['no_groups']; ?></td></tr>
+                        <?php endif; ?>
+                    </tbody>
+                </table>
+            </div>
+
+            <div class="card">
+                <h3><?php echo $t['existing_attrs']; ?></h3>
+                <table>
+                    <thead>
+                        <tr>
+                            <th>Group</th>
+                            <th><?php echo $t['attr_name']; ?></th>
+                            <th>Unit</th>
+                            <th style="width: 120px;"><?php echo $t['actions']; ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php if (count($attributes) > 0): ?>
+                            <?php foreach($attributes as $attr): ?>
+                            <tr>
+                                <td><?php echo htmlspecialchars($attr['group_name']); ?></td>
+                                <td>
+                                    <strong><?php echo htmlspecialchars($attr['name']); ?></strong><br>
+                                    <span class="lang-hint"><?php echo htmlspecialchars($attr['name_en'] ?? ''); ?></span>
+                                </td>
+                                <td><?php echo htmlspecialchars($attr['unit'] ? $attr['unit'] : '-'); ?></td>
+                                <td>
+                                    <a href="attributes.php?edit_attr=<?php echo $attr['id']; ?>" class="action-link action-edit">Edit</a>
+                                    <a href="attributes.php?delete_attr=<?php echo $attr['id']; ?>" class="action-link action-delete" onclick="return confirm('<?php echo $t['confirm_del_attr']; ?>');">Del</a>
+                                </td>
+                            </tr>
+                            <?php endforeach; ?>
+                        <?php else: ?>
+                            <tr><td colspan="4" style="text-align:center; color:var(--text-muted);"><?php echo $t['no_attrs']; ?></td></tr>
+                        <?php endif; ?>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</body>
+</html>

+ 400 - 0
admin/categories.php

@@ -0,0 +1,400 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/categories.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'manage_cats' => 'Διαχείριση Κατηγοριών',
+        'edit_cat' => '✏️ Επεξεργασία Κατηγορίας',
+        'add_cat' => '📁 Προσθήκη Νέας Κατηγορίας',
+        'name_el' => 'Όνομα 🇬🇷 (Ελληνικά) *',
+        'name_en' => 'Όνομα 🇬🇧 (Αγγλικά)',
+        'parent_cat' => 'Ανήκει σε... (Προαιρετικό)',
+        'is_main' => '-- Είναι Κεντρική Κατηγορία --',
+        'attr_group' => 'Ομάδα Χαρακτηριστικών',
+        'no_attr' => '-- Χωρίς Ειδικά Χαρακτηριστικά --',
+        'btn_save' => 'Αποθήκευση Αλλαγών',
+        'btn_add' => 'Προσθήκη Κατηγορίας',
+        'btn_cancel' => 'Ακύρωση',
+        'tree_title' => 'Δέντρο Κατηγοριών (Treeview)',
+        'no_cats' => 'Δεν υπάρχουν κατηγορίες ακόμα.',
+        'msg_add_ok' => 'Η κατηγορία προστέθηκε επιτυχώς!',
+        'msg_edit_ok' => 'Η κατηγορία ενημερώθηκε επιτυχώς!',
+        'msg_err_name' => 'Το Ελληνικό όνομα είναι υποχρεωτικό.',
+        'msg_err_slug' => 'Σφάλμα: (Μήπως το URL/Slug υπάρχει ήδη;)',
+        'msg_del_ok' => 'Η κατηγορία διεγράφη επιτυχώς!',
+        'msg_err_prod' => 'Αδυναμία διαγραφής! Υπάρχουν προϊόντα σε αυτή την κατηγορία.',
+        'msg_err_child' => 'Αδυναμία διαγραφής! Υπάρχουν υποκατηγορίες μέσα σε αυτή την κατηγορία.',
+        'confirm_del' => 'Είστε σίγουρος ότι θέλετε να διαγράψετε αυτή την κατηγορία;'
+    ],
+    'en' => [
+        'manage_cats' => 'Manage Categories',
+        'edit_cat' => '✏️ Edit Category',
+        'add_cat' => '📁 Add New Category',
+        'name_el' => 'Name 🇬🇷 (Greek) *',
+        'name_en' => 'Name 🇬🇧 (English)',
+        'parent_cat' => 'Belongs to... (Optional)',
+        'is_main' => '-- Is Main Category --',
+        'attr_group' => 'Attribute Group',
+        'no_attr' => '-- No Special Attributes --',
+        'btn_save' => 'Save Changes',
+        'btn_add' => 'Add Category',
+        'btn_cancel' => 'Cancel',
+        'tree_title' => 'Category Tree (Treeview)',
+        'no_cats' => 'No categories found.',
+        'msg_add_ok' => 'Category added successfully!',
+        'msg_edit_ok' => 'Category updated successfully!',
+        'msg_err_name' => 'The Greek name is required.',
+        'msg_err_slug' => 'Error: (Maybe the URL/Slug already exists?)',
+        'msg_del_ok' => 'Category deleted successfully!',
+        'msg_err_prod' => 'Cannot delete! There are products in this category.',
+        'msg_err_child' => 'Cannot delete! There are subcategories inside this category.',
+        'confirm_del' => 'Are you sure you want to delete this category?'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+
+$message = '';
+
+function createSlug($string) {
+    $greek = ['α','ά','Α','Ά','β','Β','γ','Γ','δ','Δ','ε','έ','Ε','Έ','ζ','Ζ','η','ή','Η','Ή','θ','Θ','ι','ί','ϊ','ΐ','Ι','Ί','Ϊ','κ','Κ','λ','Λ','μ','Μ','ν','Ν','ξ','Ξ','ο','ό','Ο','Ό','π','Π','ρ','Ρ','σ','ς','Σ','τ','Τ','υ','ύ','ϋ','ΰ','Υ','Ύ','Ϋ','φ','Φ','χ','Χ','ψ','Ψ','ω','ώ','Ω','Ώ'];
+    $latin = ['a','a','a','a','b','b','g','g','d','d','e','e','e','e','z','z','i','i','i','i','th','th','i','i','i','i','i','i','i','k','k','l','l','m','m','n','n','x','x','o','o','o','o','p','p','r','r','s','s','s','t','t','y','y','y','y','y','y','y','f','f','ch','ch','ps','ps','o','o','o','o'];
+    $string = str_replace($greek, $latin, $string);
+    $string = strtolower(trim($string));
+    $string = preg_replace('/[^a-z0-9-]/', '-', $string);
+    $string = preg_replace('/-+/', "-", $string);
+    return trim($string, '-');
+}
+
+// Χειρισμός Μετακίνησης
+if (isset($_GET['move']) && isset($_GET['id'])) {
+    $move_id = (int)$_GET['id'];
+    $direction = $_GET['move'];
+
+    $all = $pdo->query("
+        SELECT c1.id 
+        FROM categories c1 
+        LEFT JOIN categories c2 ON c1.parent_id = c2.id 
+        ORDER BY c1.sort_order ASC, c2.name_el ASC, c1.name_el ASC
+    ")->fetchAll(PDO::FETCH_ASSOC);
+    
+    $current_index = -1;
+    $seq = 1;
+    foreach ($all as $idx => $row) {
+        $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$seq, $row['id']]);
+        $all[$idx]['sort_order'] = $seq;
+        if ($row['id'] == $move_id) { $current_index = $idx; }
+        $seq++;
+    }
+
+    if ($current_index !== -1) {
+        if ($direction === 'up' && $current_index > 0) {
+            $prev_id = $all[$current_index - 1]['id'];
+            $current_order = $all[$current_index]['sort_order'];
+            $prev_order = $all[$current_index - 1]['sort_order'];
+            
+            $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$prev_order, $move_id]);
+            $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$current_order, $prev_id]);
+            
+        } elseif ($direction === 'down' && $current_index < count($all) - 1) {
+            $next_id = $all[$current_index + 1]['id'];
+            $current_order = $all[$current_index]['sort_order'];
+            $next_order = $all[$current_index + 1]['sort_order'];
+            
+            $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$next_order, $move_id]);
+            $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$current_order, $next_id]);
+        }
+    }
+    header("Location: categories.php");
+    exit;
+}
+
+// Χειρισμός POST
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
+    $action = $_POST['action'];
+    $name_el = trim($_POST['name_el']);
+    $name_en = trim($_POST['name_en']);
+    $parent_id = !empty($_POST['parent_id']) ? $_POST['parent_id'] : null;
+    $attribute_group_id = !empty($_POST['attribute_group_id']) ? (int)$_POST['attribute_group_id'] : null;
+
+    if (!empty($name_el)) {
+        $slug_source = !empty($name_en) ? $name_en : $name_el;
+        $slug = createSlug($slug_source);
+        
+        try {
+            if ($action == 'add') {
+                $stmt = $pdo->prepare("INSERT INTO categories (parent_id, name_el, name_en, slug, attribute_group_id, sort_order) VALUES (?, ?, ?, ?, ?, 999)");
+                $stmt->execute([$parent_id, $name_el, $name_en, $slug, $attribute_group_id]);
+                $message = "<div class='alert success'>{$t['msg_add_ok']}</div>";
+            } elseif ($action == 'edit') {
+                $category_id = (int)$_POST['category_id'];
+                $stmt = $pdo->prepare("UPDATE categories SET parent_id = ?, name_el = ?, name_en = ?, slug = ?, attribute_group_id = ? WHERE id = ?");
+                $stmt->execute([$parent_id, $name_el, $name_en, $slug, $attribute_group_id, $category_id]);
+                $message = "<div class='alert success'>{$t['msg_edit_ok']}</div>";
+            }
+        } catch (PDOException $e) {
+            $message = "<div class='alert error'>{$t['msg_err_slug']} " . $e->getMessage() . "</div>";
+        }
+    } else {
+        $message = "<div class='alert error'>{$t['msg_err_name']}</div>";
+    }
+}
+
+// Χειρισμός Διαγραφής
+if (isset($_GET['delete'])) {
+    $delete_id = (int)$_GET['delete'];
+    
+    $checkStmt = $pdo->prepare("SELECT COUNT(*) FROM products WHERE category_id = ?");
+    $checkStmt->execute([$delete_id]);
+    $hasProducts = $checkStmt->fetchColumn();
+
+    $checkChild = $pdo->prepare("SELECT COUNT(*) FROM categories WHERE parent_id = ?");
+    $checkChild->execute([$delete_id]);
+    $hasChildren = $checkChild->fetchColumn();
+
+    if ($hasProducts > 0) {
+        $message = "<div class='alert error'>{$t['msg_err_prod']}</div>";
+    } elseif ($hasChildren > 0) {
+        $message = "<div class='alert error'>{$t['msg_err_child']}</div>";
+    } else {
+        try {
+            $stmt = $pdo->prepare("DELETE FROM categories WHERE id = ?");
+            $stmt->execute([$delete_id]);
+            $message = "<div class='alert success'>{$t['msg_del_ok']}</div>";
+        } catch (PDOException $e) {
+            $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    }
+}
+
+$edit_category = null;
+if (isset($_GET['edit'])) {
+    $edit_id = (int)$_GET['edit'];
+    $editStmt = $pdo->prepare("SELECT * FROM categories WHERE id = ?");
+    $editStmt->execute([$edit_id]);
+    $edit_category = $editStmt->fetch();
+}
+
+$stmt = $pdo->query("
+    SELECT c1.id, c1.parent_id, c1.name_el, c1.name_en, c1.slug, c1.sort_order, ag.name as group_name
+    FROM categories c1 
+    LEFT JOIN attribute_groups ag ON c1.attribute_group_id = ag.id
+    ORDER BY c1.sort_order ASC, c1.name_el ASC
+");
+$all_categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+function buildCategoryTree($categories, $parentId = null) {
+    $branch = [];
+    foreach ($categories as $cat) {
+        if ($cat['parent_id'] == $parentId) {
+            $children = buildCategoryTree($categories, $cat['id']);
+            if ($children) {
+                $cat['children'] = $children;
+            }
+            $branch[] = $cat;
+        }
+    }
+    return $branch;
+}
+
+$categoryTree = buildCategoryTree($all_categories);
+
+function renderTreeHTML($tree, $level = 0, $t) {
+    $html = '<ul class="' . ($level === 0 ? 'tree-root' : 'tree-branch') . '">';
+    foreach ($tree as $node) {
+        $padding = $level * 35; 
+        $html .= '<li class="tree-node">';
+        $html .= '<div class="tree-row" style="padding-left: ' . ($padding + 15) . 'px;">';
+        
+        if ($level > 0) {
+            $html .= '<span class="tree-line"></span>';
+        }
+
+        $html .= '<div class="tree-arrows">';
+        $html .= '<a href="categories.php?move=up&id='.$node['id'].'">▲</a>';
+        $html .= '<a href="categories.php?move=down&id='.$node['id'].'">▼</a>';
+        $html .= '</div>';
+
+        $html .= '<div class="tree-info">';
+        $html .= '<strong>' . htmlspecialchars($node['name_el']) . '</strong>';
+        if (!empty($node['name_en'])) {
+            $html .= ' <span class="tree-en">(' . htmlspecialchars($node['name_en']) . ')</span>';
+        }
+        $html .= ' <span class="tree-slug">/' . htmlspecialchars($node['slug']) . '</span>';
+        if (!empty($node['group_name'])) {
+            $html .= ' <span class="tree-badge">' . htmlspecialchars($node['group_name']) . '</span>';
+        }
+        $html .= '</div>';
+
+        $html .= '<div class="tree-actions">';
+        $html .= '<a href="categories.php?edit='.$node['id'].'" class="action-link action-edit">Edit</a>';
+        $html .= '<a href="categories.php?delete='.$node['id'].'" class="action-link action-delete" onclick="return confirm(\''.$t['confirm_del'].'\');">Delete</a>';
+        $html .= '</div>';
+        $html .= '</div>';
+
+        if (!empty($node['children'])) {
+            $html .= renderTreeHTML($node['children'], $level + 1, $t);
+        }
+
+        $html .= '</li>';
+    }
+    $html .= '</ul>';
+    return $html;
+}
+
+$stmtParents = $pdo->query("SELECT id, name_el FROM categories WHERE parent_id IS NULL ORDER BY sort_order ASC, name_el ASC");
+$parentCategories = $stmtParents->fetchAll();
+
+$attrGroups = $pdo->query("SELECT id, name FROM attribute_groups ORDER BY name")->fetchAll();
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['manage_cats']; ?> - Hockey CMS</title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input, .form-group select { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus, .form-group select:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-cancel { background: rgba(255,255,255,0.05); color: var(--text-main); padding: 12px 20px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: 700; border: 1px solid var(--border-color); text-transform: uppercase; transition: 0.2s;}
+        .btn-cancel:hover { background: rgba(255,255,255,0.1); }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        /* --- DARK TREEVIEW STYLES --- */
+        .tree-root { list-style: none; padding: 0; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; }
+        .tree-branch { list-style: none; padding: 0; margin: 0; }
+        .tree-node { border-bottom: 1px solid rgba(255,255,255,0.05); }
+        .tree-node:last-child { border-bottom: none; }
+        
+        .tree-row { display: flex; align-items: center; padding: 15px; background: var(--panel-bg); transition: 0.2s; position: relative; }
+        .tree-row:hover { background: rgba(255,255,255,0.02); }
+        
+        .tree-line { display: inline-block; width: 15px; height: 15px; border-left: 2px solid var(--border-color); border-bottom: 2px solid var(--border-color); margin-right: 15px; margin-top: -10px; }
+        
+        .tree-arrows { display: flex; flex-direction: column; margin-right: 15px; gap: 3px; }
+        .tree-arrows a { text-decoration: none; color: var(--text-muted); font-size: 10px; background: #0f172a; padding: 3px 6px; border-radius: 4px; line-height: 1; border: 1px solid var(--border-color); transition: 0.2s;}
+        .tree-arrows a:hover { background: var(--primary); color: white; border-color: var(--primary); }
+        
+        .tree-info { flex-grow: 1; }
+        .tree-info strong { color: var(--text-main); font-size: 15px; }
+        .tree-en { color: var(--text-muted); font-size: 13px; font-weight: 500;}
+        .tree-slug { color: #3b82f6; font-size: 12px; margin-left: 10px; font-family: monospace;}
+        .tree-badge { background: rgba(0,0,0,0.3); color: var(--text-muted); padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 700; margin-left: 10px; border: 1px solid var(--border-color); text-transform: uppercase;}
+        
+        .tree-actions { flex-shrink: 0; }
+        .action-link { padding: 6px 12px; text-decoration: none; border-radius: 4px; font-size: 12px; margin-left: 5px; display: inline-block; font-weight: 700; text-transform: uppercase; transition: 0.2s;}
+        .action-edit { background-color: rgba(249, 115, 22, 0.1); color: var(--primary); border: 1px solid rgba(249, 115, 22, 0.2); }
+        .action-edit:hover { background-color: var(--primary); color: white; }
+        .action-delete { background-color: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        .action-delete:hover { background-color: #ef4444; color: white; }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['manage_cats']; ?></h2>
+        <?php echo $message; ?>
+
+        <div class="card">
+            <h3><?php echo $edit_category ? $t['edit_cat'] : $t['add_cat']; ?></h3>
+            <form method="POST" action="categories.php">
+                <input type="hidden" name="action" value="<?php echo $edit_category ? 'edit' : 'add'; ?>">
+                <?php if ($edit_category): ?>
+                    <input type="hidden" name="category_id" value="<?php echo $edit_category['id']; ?>">
+                <?php endif; ?>
+                
+                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
+                    <div class="form-group">
+                        <label><?php echo $t['name_el']; ?></label>
+                        <input type="text" name="name_el" value="<?php echo $edit_category ? htmlspecialchars($edit_category['name_el']) : ''; ?>" required>
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['name_en']; ?></label>
+                        <input type="text" name="name_en" value="<?php echo $edit_category ? htmlspecialchars($edit_category['name_en'] ?? '') : ''; ?>">
+                    </div>
+                </div>
+                
+                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
+                    <div class="form-group">
+                        <label><?php echo $t['parent_cat']; ?></label>
+                        <select name="parent_id">
+                            <option value=""><?php echo $t['is_main']; ?></option>
+                            <?php foreach($parentCategories as $parent): ?>
+                                <?php if (!$edit_category || $edit_category['id'] != $parent['id']): ?>
+                                    <option value="<?php echo $parent['id']; ?>" <?php echo ($edit_category && $edit_category['parent_id'] == $parent['id']) ? 'selected' : ''; ?>>
+                                        <?php echo htmlspecialchars($parent['name_el']); ?>
+                                    </option>
+                                <?php endif; ?>
+                            <?php endforeach; ?>
+                        </select>
+                    </div>
+
+                    <div class="form-group">
+                        <label><?php echo $t['attr_group']; ?></label>
+                        <select name="attribute_group_id">
+                            <option value=""><?php echo $t['no_attr']; ?></option>
+                            <?php foreach($attrGroups as $group): ?>
+                                <option value="<?php echo $group['id']; ?>" <?php echo ($edit_category && $edit_category['attribute_group_id'] == $group['id']) ? 'selected' : ''; ?>>
+                                    <?php echo htmlspecialchars($group['name']); ?>
+                                </option>
+                            <?php endforeach; ?>
+                        </select>
+                    </div>
+                </div>
+                
+                <button type="submit" class="btn"><?php echo $edit_category ? $t['btn_save'] : $t['btn_add']; ?></button>
+                <?php if ($edit_category): ?>
+                    <a href="categories.php" class="btn-cancel"><?php echo $t['btn_cancel']; ?></a>
+                <?php endif; ?>
+            </form>
+        </div>
+
+        <div class="card">
+            <h3>📂 <?php echo $t['tree_title']; ?></h3>
+            <?php 
+                if (!empty($categoryTree)) {
+                    echo renderTreeHTML($categoryTree, 0, $t); 
+                } else {
+                    echo "<p style='color: var(--text-muted); padding: 20px; text-align: center;'>{$t['no_cats']}</p>";
+                }
+            ?>
+        </div>
+    </div>
+
+</body>
+</html>

+ 226 - 0
admin/edit_order.php

@@ -0,0 +1,226 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/edit_order.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+session_start();
+
+if (!isset($_SESSION['user_id'])) {
+    header("Location: login.php");
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Επεξεργασία Παραγγελίας',
+        'customer_name' => 'Ονοματεπώνυμο Πελάτη *',
+        'email' => 'Email *',
+        'phone' => 'Τηλέφωνο *',
+        'status' => 'Κατάσταση Παραγγελίας',
+        'status_pending' => 'Εκκρεμεί',
+        'status_completed' => 'Ολοκληρώθηκε',
+        'status_cancelled' => 'Απορρίφθηκε',
+        'address' => 'Διεύθυνση / Πόλη',
+        'products_title' => 'Προϊόντα Παραγγελίας',
+        'prod_col' => 'Προϊόν',
+        'price_col' => 'Τιμή Μονάδας',
+        'qty_col' => 'Ποσότητα',
+        'total_col' => 'Σύνολο',
+        'grand_total' => 'Συνολικό Ποσό:',
+        'btn_save' => 'Αποθήκευση Αλλαγών',
+        'btn_back' => 'Επιστροφή στη Λίστα',
+        'msg_ok' => 'Η παραγγελία ενημερώθηκε επιτυχώς!',
+        'msg_err' => 'Παρακαλώ συμπληρώστε τα υποχρεωτικά πεδία.'
+    ],
+    'en' => [
+        'page_title' => 'Edit Order',
+        'customer_name' => 'Customer Name *',
+        'email' => 'Email *',
+        'phone' => 'Phone *',
+        'status' => 'Order Status',
+        'status_pending' => 'Pending',
+        'status_completed' => 'Completed',
+        'status_cancelled' => 'Cancelled',
+        'address' => 'Address / City',
+        'products_title' => 'Order Products',
+        'prod_col' => 'Product',
+        'price_col' => 'Unit Price',
+        'qty_col' => 'Quantity',
+        'total_col' => 'Total',
+        'grand_total' => 'Grand Total:',
+        'btn_save' => 'Save Changes',
+        'btn_back' => 'Back to List',
+        'msg_ok' => 'Order updated successfully!',
+        'msg_err' => 'Please fill in required fields.'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+
+$message = '';
+$order_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+
+if ($order_id === 0) {
+    header("Location: orders.php");
+    exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $customer_name = trim($_POST['customer_name']);
+    $customer_email = trim($_POST['customer_email']);
+    $customer_phone = trim($_POST['customer_phone']);
+    $shipping_address = trim($_POST['shipping_address']);
+    $status = $_POST['status'];
+
+    if (!empty($customer_name) && !empty($customer_email) && !empty($customer_phone)) {
+        try {
+            $stmt = $pdo->prepare("UPDATE orders SET customer_name = ?, customer_email = ?, customer_phone = ?, shipping_address = ?, status = ? WHERE id = ?");
+            $stmt->execute([$customer_name, $customer_email, $customer_phone, $shipping_address, $status, $order_id]);
+            $message = "<div class='alert success'>{$t['msg_ok']}</div>";
+        } catch (Exception $e) {
+            $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    } else {
+        $message = "<div class='alert error'>{$t['msg_err']}</div>";
+    }
+}
+
+$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ?");
+$stmt->execute([$order_id]);
+$order = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$order) {
+    die("Order not found.");
+}
+
+$itemsStmt = $pdo->prepare("
+    SELECT oi.*, p.title 
+    FROM order_items oi 
+    JOIN products p ON oi.product_id = p.id 
+    WHERE oi.order_id = ?
+");
+$itemsStmt->execute([$order_id]);
+$items = $itemsStmt->fetchAll(PDO::FETCH_ASSOC);
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?> #<?php echo $order['id']; ?> - Hockey CMS</title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 30px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 20px; }
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 25px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 800; text-transform: uppercase; transition: 0.2s;}
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-secondary { background: rgba(255,255,255,0.05); text-decoration: none; display: inline-block; padding: 12px 25px; color: var(--text-main); border-radius: 6px; font-weight: 700; border: 1px solid var(--border-color); text-transform: uppercase; transition: 0.2s;}
+        .btn-secondary:hover { background: rgba(255,255,255,0.1); }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        table { width: 100%; border-collapse: collapse; margin-top: 10px; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; }
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; }
+        table td { color: var(--text-main); font-size: 14px; }
+    </style>
+</head>
+<body>
+
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['page_title']; ?> #<?php echo $order['id']; ?></h2>
+        <?php echo $message; ?>
+
+        <div class="card">
+            <form method="POST">
+                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
+                    <div class="form-group">
+                        <label><?php echo $t['customer_name']; ?></label>
+                        <input type="text" name="customer_name" value="<?php echo htmlspecialchars($order['customer_name']); ?>" required>
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['email']; ?></label>
+                        <input type="email" name="customer_email" value="<?php echo htmlspecialchars($order['customer_email']); ?>" required>
+                    </div>
+                </div>
+
+                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
+                    <div class="form-group">
+                        <label><?php echo $t['phone']; ?></label>
+                        <input type="text" name="customer_phone" value="<?php echo htmlspecialchars($order['customer_phone']); ?>" required>
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['status']; ?></label>
+                        <select name="status">
+                            <option value="pending" <?php echo $order['status'] == 'pending' ? 'selected' : ''; ?>><?php echo $t['status_pending']; ?></option>
+                            <option value="completed" <?php echo $order['status'] == 'completed' ? 'selected' : ''; ?>><?php echo $t['status_completed']; ?></option>
+                            <option value="cancelled" <?php echo $order['status'] == 'cancelled' ? 'selected' : ''; ?>><?php echo $t['status_cancelled']; ?></option>
+                        </select>
+                    </div>
+                </div>
+
+                <div class="form-group">
+                    <label><?php echo $t['address']; ?></label>
+                    <textarea name="shipping_address" rows="3"><?php echo htmlspecialchars($order['shipping_address']); ?></textarea>
+                </div>
+
+                <h3 style="margin-top: 30px; color: var(--text-main); font-size: 18px; text-transform: uppercase;"><?php echo $t['products_title']; ?></h3>
+                <table>
+                    <thead>
+                        <tr>
+                            <th><?php echo $t['prod_col']; ?></th>
+                            <th><?php echo $t['price_col']; ?></th>
+                            <th><?php echo $t['qty_col']; ?></th>
+                            <th><?php echo $t['total_col']; ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach ($items as $item): ?>
+                        <tr>
+                            <td><strong><?php echo htmlspecialchars($item['title']); ?></strong></td>
+                            <td><?php echo number_format($item['price'], 2, ',', '.'); ?> €</td>
+                            <td><?php echo $item['quantity']; ?></td>
+                            <td><strong><?php echo number_format($item['price'] * $item['quantity'], 2, ',', '.'); ?> €</strong></td>
+                        </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+                <p style="text-align: right; font-size: 1.2em; margin-top: 20px; color: var(--text-muted);">
+                    <?php echo $t['grand_total']; ?> <strong style="color: #10b981;"><?php echo number_format($order['total_amount'], 2, ',', '.'); ?> €</strong>
+                </p>
+
+                <div style="margin-top: 25px; display: flex; gap: 10px;">
+                    <button type="submit" class="btn"><?php echo $t['btn_save']; ?></button>
+                    <a href="orders.php" class="btn-secondary"><?php echo $t['btn_back']; ?></a>
+                </div>
+            </form>
+        </div>
+    </div>
+</body>
+</html>

+ 177 - 0
admin/email_templates.php

@@ -0,0 +1,177 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/email_templates.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+
+if (!isset($_SESSION['user_id']) or $_SESSION['role_id'] != 1) {
+    header("Location: ../index.php");
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Πρότυπα Email - Hockey CMS',
+        'header_title' => 'Διαχείριση Προτύπων Email',
+        'info_vars' => '<strong>Διαθέσιμες μεταβλητές (Placeholders):</strong> Μπορείτε να χρησιμοποιήσετε τα παρακάτω μέσα στα θέματα και στα κείμενα, τα οποία θα αντικαθίστανται αυτόματα:<br>',
+        'order_id_hint' => '(Κωδικός Παραγγελίας)',
+        'customer_name_hint' => '(Όνομα Πελάτη)',
+        'total_amount_hint' => '(Συνολικό Ποσό)',
+        'status_hint' => '(Κατάσταση Παραγγελίας)',
+        'subj_el' => 'Θέμα Email (Ελληνικά - EL)',
+        'subj_en' => 'Θέμα Email (English - EN)',
+        'body_el' => 'Κείμενο / Σώμα Email (Ελληνικά - EL)',
+        'body_en' => 'Κείμενο / Σώμα Email (English - EN)',
+        'btn_save' => '💾 Αποθήκευση Όλων των Προτύπων',
+        'msg_ok' => 'Τα πρότυπα email αποθηκεύτηκαν επιτυχώς!',
+        'msg_err' => 'Σφάλμα αποθήκευσης: '
+    ],
+    'en' => [
+        'page_title' => 'Email Templates - Hockey CMS',
+        'header_title' => 'Email Templates Management',
+        'info_vars' => '<strong>Available variables (Placeholders):</strong> You can use the following in subjects and bodies, which will be replaced automatically:<br>',
+        'order_id_hint' => '(Order ID)',
+        'customer_name_hint' => '(Customer Name)',
+        'total_amount_hint' => '(Total Amount)',
+        'status_hint' => '(Order Status)',
+        'subj_el' => 'Email Subject (Greek - EL)',
+        'subj_en' => 'Email Subject (English - EN)',
+        'body_el' => 'Email Body (Greek - EL)',
+        'body_en' => 'Email Body (English - EN)',
+        'btn_save' => '💾 Save All Templates',
+        'msg_ok' => 'Email templates saved successfully!',
+        'msg_err' => 'Save Error: '
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+// --- ΑΠΟΘΗΚΕΥΣΗ ΠΡΟΤΥΠΩΝ ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action']) and $_POST['action'] == 'save_templates') {
+    try {
+        $pdo->beginTransaction();
+        foreach ($_POST['templates'] as $id => $data) {
+            $stmt = $pdo->prepare("UPDATE email_templates SET subject_el = ?, subject_en = ?, body_el = ?, body_en = ? WHERE id = ?");
+            $stmt->execute([
+                trim($data['subject_el']),
+                trim($data['subject_en']),
+                trim($data['body_el']),
+                trim($data['body_en']),
+                (int)$id
+            ]);
+        }
+        $pdo->commit();
+        $message = "<div class='alert success'>{$t['msg_ok']}</div>";
+    } catch (Exception $e) {
+        $pdo->rollBack();
+        $message = "<div class='alert error'>{$t['msg_err']}" . $e->getMessage() . "</div>";
+    }
+}
+
+// Ανάγνωση προτύπων από τη βάση
+$templates = $pdo->query("SELECT * FROM email_templates ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; display: flex; justify-content: space-between; align-items: center;}
+        
+        .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input, .form-group textarea { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; font-size: 14px;}
+        .form-group input:focus, .form-group textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 25px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .info-box { background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 15px; border-radius: 4px; font-size: 13px; color: #60a5fa; margin-bottom: 20px; line-height: 1.5; }
+        .info-box code { background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; color: #f8fafc; font-family: monospace; border: 1px solid var(--border-color);}
+        
+        .badge-key { background: rgba(0,0,0,0.3); color: var(--text-muted); padding: 4px 10px; border-radius: 12px; font-size: 11px; font-family: monospace; border: 1px solid var(--border-color); text-transform: none; letter-spacing: 0;}
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div class="info-box">
+            <?php echo $t['info_vars']; ?>
+            <code>{order_id}</code> <?php echo $t['order_id_hint']; ?>, 
+            <code>{customer_name}</code> <?php echo $t['customer_name_hint']; ?>, 
+            <code>{total_amount}</code> <?php echo $t['total_amount_hint']; ?>, 
+            <code>{status}</code> <?php echo $t['status_hint']; ?>
+        </div>
+
+        <form method="POST" action="email_templates.php">
+            <input type="hidden" name="action" value="save_templates">
+
+            <?php foreach ($templates as $tpl): ?>
+                <div class="card">
+                    <h3>
+                        <span><?php echo htmlspecialchars($tpl['name']); ?></span>
+                        <span class="badge-key"><?php echo htmlspecialchars($tpl['template_key']); ?></span>
+                    </h3>
+
+                    <div class="form-grid">
+                        <div class="form-group">
+                            <label><?php echo $t['subj_el']; ?></label>
+                            <input type="text" name="templates[<?php echo $tpl['id']; ?>][subject_el]" value="<?php echo htmlspecialchars($tpl['subject_el']); ?>" required>
+                        </div>
+                        <div class="form-group">
+                            <label><?php echo $t['subj_en']; ?></label>
+                            <input type="text" name="templates[<?php echo $tpl['id']; ?>][subject_en]" value="<?php echo htmlspecialchars($tpl['subject_en']); ?>" required>
+                        </div>
+                    </div>
+
+                    <div class="form-grid">
+                        <div class="form-group">
+                            <label><?php echo $t['body_el']; ?></label>
+                            <textarea name="templates[<?php echo $tpl['id']; ?>][body_el]" rows="6" required><?php echo htmlspecialchars($tpl['body_el']); ?></textarea>
+                        </div>
+                        <div class="form-group">
+                            <label><?php echo $t['body_en']; ?></label>
+                            <textarea name="templates[<?php echo $tpl['id']; ?>][body_en]" rows="6" required><?php echo htmlspecialchars($tpl['body_en']); ?></textarea>
+                        </div>
+                    </div>
+                </div>
+            <?php endforeach; ?>
+
+            <button type="submit" class="btn"><?php echo $t['btn_save']; ?></button>
+        </form>
+    </div>
+</body>
+</html>

+ 68 - 0
admin/google_login.php

@@ -0,0 +1,68 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/google_login.php
+session_start();
+require_once '../config/database.php';
+require_once '../vendor/autoload.php';
+
+// --- ΡΥΘΜΙΣΕΙΣ ΝΕΟΥ ADMIN GOOGLE API ---
+$clientId     = '1062187052171-ijgd341h6mdko1k05e78srbl9otmlisl.apps.googleusercontent.com';
+$clientSecret = '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';
+$redirectUri  = 'https://hockeycms.eparmak.eu/admin/google_login.php'; // ⚠️ Πρόσεξε να ταιριάζει ακριβώς με το Redirect URI στο Google Console σου
+
+$client = new Google_Client();
+$client->setClientId($clientId);
+$client->setClientSecret($clientSecret);
+$client->setRedirectUri($redirectUri);
+$client->addScope('email');
+$client->addScope('profile');
+
+// Αν δεν έχει έρθει το ?code= από την Google, τον στέλνουμε στη σελίδα login της Google
+if (!isset($_GET['code'])) {
+    $authUrl = $client->createAuthUrl();
+    header('Location: ' . filter_var($authUrl, FILTER_SANITIZE_URL));
+    exit;
+}
+
+// Αν επέστρεψε με κωδικό, επεξεργαζόμαστε τα στοιχεία
+try {
+    $token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
+    if (isset($token['error'])) {
+        throw new Exception("Σφάλμα σύνδεσης: " . $token['error']);
+    }
+    
+    $client->setAccessToken($token['access_token']);
+    
+    // Παίρνουμε τα στοιχεία του χρήστη από την Google
+    $google_oauth = new Google_Service_Oauth2($client);
+    $google_account_info = $google_oauth->userinfo->get();
+    
+    $email = $google_account_info->email;
+
+    // Ελέγχουμε αποκλειστικά αν το email υπάρχει στη βάση και είναι Admin (role_id = 1)
+    $stmt = $pdo->prepare("SELECT id, role_id, first_name, username, is_seller FROM users WHERE email = ? AND role_id = 1");
+    $stmt->execute([$email]);
+    $user = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    if ($user) {
+        // Επιτυχής σύνδεση Admin
+        session_regenerate_id(true);
+        $_SESSION['user_id'] = $user['id'];
+        $_SESSION['role_id'] = $user['role_id'];
+        $_SESSION['first_name'] = $user['first_name'] ?? '';
+        $_SESSION['username'] = $user['username'];
+        $_SESSION['is_seller'] = $user['is_seller'] ?? 0;
+        
+        header("Location: index.php");
+        exit;
+    } else {
+        // Ο χρήστης δεν βρέθηκε ή δεν έχει δικαιώματα διαχειριστή
+        $_SESSION['login_error'] = "Ο λογαριασμός Google δεν έχει δικαιώματα διαχειριστή (Admin).";
+        header("Location: login.php");
+        exit;
+    }
+
+} catch (Exception $e) {
+    $_SESSION['login_error'] = "Σφάλμα Google Login: " . $e->getMessage();
+    header("Location: login.php");
+    exit;
+}

+ 179 - 0
admin/index.php

@@ -0,0 +1,179 @@
+<?php
+session_start();
+
+// 1. Αν δεν είναι καν συνδεδεμένος -> Πάει στο Login
+if (!isset($_SESSION['user_id'])) {
+    header("Location: ../login.php");
+    exit;
+}
+
+// 2. Αν ΕΙΝΑΙ συνδεδεμένος αλλά ΔΕΝ είναι Admin -> Ακαριαίο redirect
+if ($_SESSION['role_id'] != 1) {
+    if ($_SESSION['role_id'] == 2 || !empty($_SESSION['is_seller'])) {
+        header("Location: ../seller_dashboard.php");
+    } else {
+        header("Location: ../index.php");
+    }
+    exit;
+}
+
+require_once '../config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'dashboard' => 'Dashboard',
+        'welcome' => 'Καλώς ήρθες,',
+        'welcome_sub' => 'Αυτή είναι η συνοπτική εικόνα του καταστήματός σου.',
+        'products' => 'Προϊόντα',
+        'categories' => 'Κατηγορίες',
+        'attributes' => 'Χαρακτηριστικά',
+        'users' => 'Εγγεγραμμένοι Χρήστες',
+        'latest_adds' => 'Τελευταίες Προσθήκες Εξοπλισμού',
+        'title' => 'Τίτλος',
+        'price' => 'Τιμή',
+        'date' => 'Ημερομηνία',
+        'no_products' => 'Δεν υπάρχουν ακόμα προϊόντα.'
+    ],
+    'en' => [
+        'dashboard' => 'Dashboard',
+        'welcome' => 'Welcome,',
+        'welcome_sub' => 'Here is the overview of your store.',
+        'products' => 'Products',
+        'categories' => 'Categories',
+        'attributes' => 'Attributes',
+        'users' => 'Registered Users',
+        'latest_adds' => 'Latest Equipment Additions',
+        'title' => 'Title',
+        'price' => 'Price',
+        'date' => 'Date',
+        'no_products' => 'No products available yet.'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+
+// Συγκεντρώνουμε μερικά γρήγορα στατιστικά για το Dashboard
+try {
+    $total_products = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
+    $total_categories = $pdo->query("SELECT COUNT(*) FROM categories")->fetchColumn();
+    $total_users = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
+    $total_groups = $pdo->query("SELECT COUNT(*) FROM attribute_groups")->fetchColumn();
+    
+    // Φέρνουμε τα 5 πιο πρόσφατα προϊόντα
+    $recent_products = $pdo->query("
+        SELECT title, price, created_at 
+        FROM products 
+        ORDER BY id DESC LIMIT 5
+    ")->fetchAll();
+
+} catch (Exception $e) {
+    die("Σφάλμα κατά την ανάγνωση των στατιστικών: " . $e->getMessage());
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['dashboard']; ?> - Hockey CMS</title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); margin-bottom: 20px; border: 1px solid var(--border-color); }
+        .card h3 { margin-top: 0; color: var(--primary); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 10px; font-size: 18px; text-transform: uppercase; }
+        
+        .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
+        .stat-card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); text-align: center; border: 1px solid var(--border-color); border-bottom: 4px solid var(--primary); transition: 0.2s; }
+        .stat-card:hover { transform: translateY(-5px); }
+        .stat-card h3 { margin: 0; font-size: 36px; color: var(--text-main); font-weight: 900; border: none; padding: 0;}
+        .stat-card p { margin: 10px 0 0; color: var(--text-muted); font-size: 13px; text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px; }
+        
+        .stat-card:nth-child(2) { border-bottom-color: #10b981; }
+        .stat-card:nth-child(3) { border-bottom-color: #3b82f6; }
+        .stat-card:nth-child(4) { border-bottom-color: #ef4444; }
+
+        table { width: 100%; border-collapse: collapse; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; }
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; letter-spacing: 0.5px; }
+        table td { color: var(--text-main); font-size: 15px; }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['dashboard']; ?></h2>
+        <p style="color: var(--text-muted); margin-bottom: 30px; font-size: 15px;">
+            <?php echo $t['welcome']; ?> <strong style="color: var(--text-main);"><?php echo htmlspecialchars($_SESSION['first_name'] ?? 'Admin'); ?></strong>! 
+            <?php echo $t['welcome_sub']; ?>
+        </p>
+
+        <!-- Στατιστικά -->
+        <div class="stats-grid">
+            <div class="stat-card">
+                <h3><?php echo $total_products; ?></h3>
+                <p><?php echo $t['products']; ?></p>
+            </div>
+            <div class="stat-card">
+                <h3><?php echo $total_categories; ?></h3>
+                <p><?php echo $t['categories']; ?></p>
+            </div>
+            <div class="stat-card">
+                <h3><?php echo $total_groups; ?></h3>
+                <p><?php echo $t['attributes']; ?></p>
+            </div>
+            <div class="stat-card">
+                <h3><?php echo $total_users; ?></h3>
+                <p><?php echo $t['users']; ?></p>
+            </div>
+        </div>
+
+        <!-- Τελευταίες προσθήκες -->
+        <div class="card" style="max-width: 900px;">
+            <h3>📦 <?php echo $t['latest_adds']; ?></h3>
+            <?php if (count($recent_products) > 0): ?>
+                <table>
+                    <thead>
+                        <tr>
+                            <th><?php echo $t['title']; ?></th>
+                            <th><?php echo $t['price']; ?></th>
+                            <th><?php echo $t['date']; ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach($recent_products as $p): ?>
+                        <tr>
+                            <td><strong><?php echo htmlspecialchars($p['title']); ?></strong></td>
+                            <td style="color: #10b981; font-weight: 700;"><?php echo htmlspecialchars($p['price']); ?> €</td>
+                            <td style="color: var(--text-muted);"><?php echo date('d/m/Y H:i', strtotime($p['created_at'])); ?></td>
+                        </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+            <?php else: ?>
+                <p style="color: var(--text-muted); text-align: center; padding: 20px;"><?php echo $t['no_products']; ?></p>
+            <?php endif; ?>
+        </div>
+    </div>
+
+</body>
+</html>

+ 150 - 0
admin/login.php

@@ -0,0 +1,150 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/login.php
+session_start();
+require_once '../config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'login_title' => 'Σύνδεση Διαχειριστή - Hockey CMS',
+        'app_name' => 'Hockey CMS - Admin',
+        'username' => 'Όνομα Χρήστη',
+        'password' => 'Κωδικός Πρόσβασης',
+        'btn_login' => 'Σύνδεση',
+        'or' => 'Ή',
+        'btn_google' => 'Σύνδεση Διαχειριστή με Google',
+        'err_empty' => 'Παρακαλώ συμπληρώστε όλα τα πεδία.',
+        'err_invalid' => 'Λάθος όνομα χρήστη ή κωδικός.'
+    ],
+    'en' => [
+        'login_title' => 'Admin Login - Hockey CMS',
+        'app_name' => 'Hockey CMS - Admin',
+        'username' => 'Username',
+        'password' => 'Password',
+        'btn_login' => 'Login',
+        'or' => 'OR',
+        'btn_google' => 'Admin Sign in with Google',
+        'err_empty' => 'Please fill in all fields.',
+        'err_invalid' => 'Invalid username or password.'
+    ]
+];
+$t = $translations[$current_lang];
+
+// Αν ο χρήστης είναι ήδη συνδεδεμένος ως Admin
+if (isset($_SESSION['user_id']) && isset($_SESSION['role_id']) && $_SESSION['role_id'] == 1) {
+    header("Location: index.php");
+    exit;
+}
+
+$error = '';
+if (isset($_SESSION['login_error'])) {
+    $error = $_SESSION['login_error'];
+    unset($_SESSION['login_error']);
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $username = trim($_POST['username']);
+    $password = $_POST['password'];
+
+    if (empty($username) || empty($password)) {
+        $error = $t['err_empty'];
+    } else {
+        $stmt = $pdo->prepare("SELECT id, role_id, password, first_name, username, is_seller FROM users WHERE username = ?");
+        $stmt->execute([$username]);
+        $user = $stmt->fetch();
+
+        // Έλεγχος ότι είναι όντως Admin (role_id == 1)
+        if ($user && password_verify($password, $user['password']) && $user['role_id'] == 1) {
+            session_regenerate_id(true);
+            $_SESSION['user_id'] = $user['id'];
+            $_SESSION['role_id'] = $user['role_id'];
+            $_SESSION['first_name'] = $user['first_name'] ?? '';
+            $_SESSION['username'] = $user['username'];
+            $_SESSION['is_seller'] = $user['is_seller'] ?? 0;
+            
+            header("Location: index.php");
+            exit;
+        } else {
+            $error = $t['err_invalid'];
+        }
+    }
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['login_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; flex-direction: column; }
+        .login-box { background: var(--panel-bg); padding: 40px; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); width: 100%; max-width: 350px; border: 1px solid var(--border-color); }
+        .login-box h2 { text-align: center; margin-top: 0; margin-bottom: 25px; color: var(--primary); font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase; }
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; color: var(--text-muted); font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
+        .form-group input { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        .btn { width: 100%; padding: 14px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; margin-top: 10px; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        
+        .btn-google { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 12px; background: #ffffff; color: #1e293b; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 700; text-decoration: none; margin-top: 15px; transition: 0.2s; box-sizing: border-box; }
+        .btn-google:hover { background: #f1f5f9; }
+        
+        .divider { text-align: center; border-bottom: 1px solid var(--border-color); line-height: 0.1em; margin: 25px 0 20px 0; }
+        .divider span { background: var(--panel-bg); padding: 0 10px; color: var(--text-muted); font-size: 12px; font-weight: 600; text-transform: uppercase; }
+
+        .error { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); color: #fca5a5; padding: 10px; border-radius: 6px; margin-bottom: 20px; text-align: center; font-size: 14px; font-weight: 600; }
+        
+        .lang-switch { margin-bottom: 20px; display: flex; gap: 10px; justify-content: center; }
+        .lang-switch a { color: var(--text-muted); text-decoration: none; font-size: 13px; font-weight: 700; padding: 4px 10px; border-radius: 12px; transition: 0.2s; }
+        .lang-switch a.active { background: var(--border-color); color: var(--text-main); }
+        .lang-switch a:hover { color: var(--text-main); }
+    </style>
+</head>
+<body>
+    <div class="lang-switch">
+        <a href="?lang=el" class="<?php echo $current_lang == 'el' ? 'active' : ''; ?>">GR</a>
+        <a href="?lang=en" class="<?php echo $current_lang == 'en' ? 'active' : ''; ?>">EN</a>
+    </div>
+    
+    <div class="login-box">
+        <h2><?php echo $t['app_name']; ?></h2>
+        <?php if ($error): ?>
+            <div class="error"><?php echo htmlspecialchars($error); ?></div>
+        <?php endif; ?>
+        <form method="POST" action="login.php">
+            <div class="form-group">
+                <label><?php echo $t['username']; ?></label>
+                <input type="text" name="username" required>
+            </div>
+            <div class="form-group">
+                <label><?php echo $t['password']; ?></label>
+                <input type="password" name="password" required>
+            </div>
+            <button type="submit" class="btn"><?php echo $t['btn_login']; ?></button>
+        </form>
+
+        <div class="divider"><span><?php echo $t['or']; ?></span></div>
+
+        <!-- Κουμπί Google Login για Admin -->
+        <a href="google_login.php" class="btn-google">
+            <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.7 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.46-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>
+            <?php echo $t['btn_google']; ?>
+        </a>
+    </div>
+</body>
+</html>

+ 8 - 0
admin/logout.php

@@ -0,0 +1,8 @@
+<?php
+session_start();
+session_unset();    // Καθαρίζει τα δεδομένα του session
+session_destroy();  // Καταστρέφει το session
+
+// Ανακατεύθυνση στη σελίδα σύνδεσης
+header("Location: login.php");
+exit;

+ 265 - 0
admin/mail_settings.php

@@ -0,0 +1,265 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/mail_settings.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+require_once '../vendor/autoload.php';
+
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception;
+
+if (!isset($_SESSION['user_id']) or $_SESSION['role_id'] != 1) {
+    header("Location: ../index.php");
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Ρυθμίσεις Email (SMTP)',
+        'header_title' => 'Ρυθμίσεις Διακομιστή Email (SMTP)',
+        'provider_settings' => 'Ρυθμίσεις Παρόχου',
+        'gmail_instructions' => 'Οδηγίες για GMAIL: Αν χρησιμοποιείτε Gmail, βάλτε Host: smtp.gmail.com, Port: 587, Ασφάλεια: TLS. Πρέπει να ενεργοποιήσετε το 2-Factor Authentication και να δημιουργήσετε ένα App Password.',
+        'smtp_host' => 'Διακομιστής (SMTP Host) *',
+        'smtp_port' => 'Θύρα (SMTP Port) *',
+        'smtp_user' => 'Όνομα Χρήστη / Email (Username) *',
+        'smtp_pass' => 'Κωδικός Πρόσβασης (App Password) *',
+        'encryption' => 'Κρυπτογράφηση *',
+        'enc_tls' => 'STARTTLS (Συνιστάται - Port 587)',
+        'enc_ssl' => 'SSL (Port 465)',
+        'sender_details' => 'Στοιχεία Αποστολέα (Αυτό που βλέπει ο πελάτης)',
+        'sender_email' => 'Email Αποστολέα *',
+        'sender_name' => 'Όνομα Αποστολέα *',
+        'btn_save' => '💾 Αποθήκευση Ρυθμίσεων',
+        'test_title' => 'Δοκιμή Αποστολής (Test Email)',
+        'test_desc' => 'Αφού αποθηκεύσετε τις ρυθμίσεις σας, στείλτε ένα δοκιμαστικό email στον εαυτό σας.',
+        'test_email_addr' => 'Διεύθυνση Email παραλήπτη',
+        'btn_test' => '✉️ Αποστολή Δοκιμαστικού',
+        'msg_save_ok' => 'Οι ρυθμίσεις SMTP αποθηκεύτηκαν επιτυχώς!',
+        'msg_err_email' => 'Παρακαλώ εισάγετε ένα έγκυρο Email!',
+        'msg_test_ok' => 'Το δοκιμαστικό Email στάλθηκε επιτυχώς στο',
+        'msg_err_smtp' => 'Αποτυχία Αποστολής (SMTP Error).'
+    ],
+    'en' => [
+        'page_title' => 'Email Settings (SMTP)',
+        'header_title' => 'Email Server Settings (SMTP)',
+        'provider_settings' => 'Provider Settings',
+        'gmail_instructions' => 'GMAIL Instructions: If using Gmail, set Host: smtp.gmail.com, Port: 587, Security: TLS. You must enable 2-Factor Auth and create an App Password.',
+        'smtp_host' => 'Server (SMTP Host) *',
+        'smtp_port' => 'Port (SMTP Port) *',
+        'smtp_user' => 'Username / Email *',
+        'smtp_pass' => 'Password (App Password) *',
+        'encryption' => 'Encryption *',
+        'enc_tls' => 'STARTTLS (Recommended - Port 587)',
+        'enc_ssl' => 'SSL (Port 465)',
+        'sender_details' => 'Sender Details (Seen by customer)',
+        'sender_email' => 'Sender Email *',
+        'sender_name' => 'Sender Name *',
+        'btn_save' => '💾 Save Settings',
+        'test_title' => 'Test Email Delivery',
+        'test_desc' => 'After saving your settings, send a test email to yourself.',
+        'test_email_addr' => 'Recipient Email Address',
+        'btn_test' => '✉️ Send Test Email',
+        'msg_save_ok' => 'SMTP settings saved successfully!',
+        'msg_err_email' => 'Please enter a valid Email!',
+        'msg_test_ok' => 'Test Email sent successfully to',
+        'msg_err_smtp' => 'Delivery Failed (SMTP Error).'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+$setting_keys = ['smtp_host', 'smtp_port', 'smtp_username', 'smtp_password', 'smtp_encryption', 'mail_from_address', 'mail_from_name'];
+
+// --- 1. ΑΠΟΘΗΚΕΥΣΗ ΡΥΘΜΙΣΕΩΝ ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action']) and $_POST['action'] == 'save_settings') {
+    try {
+        $pdo->beginTransaction();
+        foreach ($setting_keys as $key) {
+            $value = trim($_POST[$key] ?? '');
+            $check = $pdo->prepare("SELECT COUNT(*) FROM settings WHERE `key` = ?");
+            $check->execute([$key]);
+            if ($check->fetchColumn() > 0) {
+                $stmt =$pdo->prepare("UPDATE settings SET `value` = ? WHERE `key` = ?");
+                $stmt->execute([$value,$key]);
+            } else {
+                $stmt =$pdo->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?)");
+                $stmt->execute([$key,$value]);
+            }
+        }
+        $pdo->commit();$message = "<div class='alert success'>{$t['msg_save_ok']}</div>";
+    } catch (PDOException $e) {
+        $pdo->rollBack();$message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+    }
+}
+
+// --- Ανάγνωση υφιστάμενων ρυθμίσεων ---
+$current_settings = [];
+$stmt =$pdo->query("SELECT `key`, `value` FROM settings WHERE `key` IN ('" . implode("','", $setting_keys) . "')");
+while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {$current_settings[$row['key']] =$row['value'];
+}
+
+// --- 2. ΑΠΟΣΤΟΛΗ ΔΟΚΙΜΑΣΤΙΚΟΥ EMAIL ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action']) and$_POST['action'] == 'test_email') {
+    $test_email = trim($_POST['test_email']);
+    
+    if (empty($test_email) or !filter_var($test_email, FILTER_VALIDATE_EMAIL)) {$message = "<div class='alert error'>{$t['msg_err_email']}</div>";
+    } else {
+        $mail = new PHPMailer(true);
+        try {
+            $mail->isSMTP();$mail->Host       = $current_settings['smtp_host'] ?? '';$mail->SMTPAuth   = true;
+            $mail->Username   =$current_settings['smtp_username'] ?? '';
+            $mail->Password   =$current_settings['smtp_password'] ?? '';
+            
+            $encryption =$current_settings['smtp_encryption'] ?? 'tls';
+            if ($encryption === 'ssl') {$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
+            } else {
+                $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+            }
+            
+            $mail->Port       = (int)($current_settings['smtp_port'] ?? 587);$mail->CharSet    = 'UTF-8';
+
+            $from_email = !empty($current_settings['mail_from_address']) ?$current_settings['mail_from_address'] : 'info@yourshop.com';
+            $from_name  = !empty($current_settings['mail_from_name']) ? $current_settings['mail_from_name'] : 'Hockey CMS';$mail->setFrom($from_email,$from_name);
+            $mail->addAddress($test_email);
+
+            $mail->isHTML(true);$mail->Subject = 'Επιβεβαίωση SMTP - Hockey CMS';
+            $mail->Body    = '<div style="font-family: sans-serif; padding: 20px; background: #f8fafc; border-radius: 8px;"><h2 style="color: #2563eb;">Συγχαρητήρια! 🎉</h2><p>Αν διαβάζετε αυτό το μήνυμα, οι ρυθμίσεις του διακομιστή SMTP έχουν γίνει σωστά στο <strong>Hockey CMS</strong>.</p></div>';
+
+            $mail->send();$message = "<div class='alert success'>{$t['msg_test_ok']} <strong>{$test_email}</strong>!</div>";
+        } catch (Exception $e) {$message = "<div class='alert error'>{$t['msg_err_smtp']} <br><small>{$mail->ErrorInfo}</small></div>";
+        }
+    }
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?> - Hockey CMS</title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input, .form-group select { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus, .form-group select:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-test { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .btn-test:hover { background: #10b981; color: white; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .info-box { background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 15px; border-radius: 4px; font-size: 13px; color: #60a5fa; margin-bottom: 20px; line-height: 1.5; }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div style="display: grid; grid-template-columns: 2fr 1fr; gap: 25px;">
+            
+            <div class="card">
+                <h3><?php echo $t['provider_settings']; ?></h3>
+                
+                <div class="info-box">
+                    <?php echo $t['gmail_instructions']; ?>
+                </div>
+
+                <form method="POST" action="mail_settings.php">
+                    <input type="hidden" name="action" value="save_settings">
+                    
+                    <div class="form-grid">
+                        <div class="form-group">
+                            <label><?php echo $t['smtp_host']; ?></label>
+                            <input type="text" name="smtp_host" value="<?php echo htmlspecialchars($current_settings['smtp_host'] ?? ''); ?>" placeholder="smtp.gmail.com" required>
+                        </div>
+                        
+                        <div class="form-group">
+                            <label><?php echo $t['smtp_port']; ?></label>
+                            <input type="number" name="smtp_port" value="<?php echo htmlspecialchars($current_settings['smtp_port'] ?? '587'); ?>" placeholder="587" required>
+                        </div>
+
+                        <div class="form-group">
+                            <label><?php echo $t['smtp_user']; ?></label>
+                            <input type="text" name="smtp_username" value="<?php echo htmlspecialchars($current_settings['smtp_username'] ?? ''); ?>" placeholder="email@gmail.com" required>
+                        </div>
+
+                        <div class="form-group">
+                            <label><?php echo $t['smtp_pass']; ?></label>
+                            <input type="password" name="smtp_password" value="<?php echo htmlspecialchars($current_settings['smtp_password'] ?? ''); ?>" placeholder="••••••••••••" required>
+                        </div>
+
+                        <div class="form-group">
+                            <label><?php echo $t['encryption']; ?></label>
+                            <select name="smtp_encryption" required>
+                                <option value="tls" <?php echo (isset($current_settings['smtp_encryption']) and $current_settings['smtp_encryption'] == 'tls') ? 'selected' : ''; ?>><?php echo$t['enc_tls']; ?></option>
+                                <option value="ssl" <?php echo (isset($current_settings['smtp_encryption']) and $current_settings['smtp_encryption'] == 'ssl') ? 'selected' : ''; ?>><?php echo$t['enc_ssl']; ?></option>
+                            </select>
+                        </div>
+                    </div>
+
+                    <h3 style="margin-top: 20px;"><?php echo $t['sender_details']; ?></h3>
+                    <div class="form-grid">
+                        <div class="form-group">
+                            <label><?php echo $t['sender_email']; ?></label>
+                            <input type="email" name="mail_from_address" value="<?php echo htmlspecialchars($current_settings['mail_from_address'] ?? ''); ?>" placeholder="no-reply@domain.com" required>
+                        </div>
+                        <div class="form-group">
+                            <label><?php echo $t['sender_name']; ?></label>
+                            <input type="text" name="mail_from_name" value="<?php echo htmlspecialchars($current_settings['mail_from_name'] ?? ''); ?>" placeholder="Store Name" required>
+                        </div>
+                    </div>
+                    
+                    <button type="submit" class="btn"><?php echo $t['btn_save']; ?></button>
+                </form>
+            </div>
+
+            <div class="card" style="height: fit-content;">
+                <h3><?php echo $t['test_title']; ?></h3>
+                <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;"><?php echo $t['test_desc']; ?></p>
+                
+                <form method="POST" action="mail_settings.php">
+                    <input type="hidden" name="action" value="test_email">
+                    <div class="form-group">
+                        <label><?php echo $t['test_email_addr']; ?></label>
+                        <input type="email" name="test_email" placeholder="example@email.com" required>
+                    </div>
+                    <button type="submit" class="btn btn-test"><?php echo $t['btn_test']; ?></button>
+                </form>
+            </div>
+            
+        </div>
+    </div>
+</body>
+</html>

+ 227 - 0
admin/orders.php

@@ -0,0 +1,227 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/orders.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+require_once __DIR__ . '/../config/functions.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Παραγγελίες - Hockey CMS',
+        'header_title' => 'Διαχείριση Παραγγελιών',
+        'th_id' => 'ID',
+        'th_customer' => 'Πελάτης',
+        'th_products' => 'Προϊόντα',
+        'th_total' => 'Σύνολο',
+        'th_actions' => 'Κατάσταση & Διαχείριση',
+        'status_pending' => 'Εκκρεμεί',
+        'status_completed' => 'Ολοκληρώθηκε',
+        'status_cancelled' => 'Ακύρωση & Επιστροφή Αποθ.',
+        'btn_update' => 'Ενημέρωση',
+        'btn_delete' => 'Διαγραφή',
+        'no_orders' => 'Καμία παραγγελία.',
+        'msg_ok' => 'Επιτυχής ενημέρωση της παραγγελίας και του αποθέματος!',
+        'msg_del' => 'Η παραγγελία διαγράφηκε οριστικά από το σύστημα.',
+        'confirm_del' => 'Είστε απόλυτα σίγουρος ότι θέλετε να ΔΙΑΓΡΑΨΕΤΕ αυτή την παραγγελία;'
+    ],
+    'en' => [
+        'page_title' => 'Orders - Hockey CMS',
+        'header_title' => 'Orders Management',
+        'th_id' => 'ID',
+        'th_customer' => 'Customer',
+        'th_products' => 'Products',
+        'th_total' => 'Total',
+        'th_actions' => 'Status & Management',
+        'status_pending' => 'Pending',
+        'status_completed' => 'Completed',
+        'status_cancelled' => 'Cancelled & Restock',
+        'btn_update' => 'Update',
+        'btn_delete' => 'Delete',
+        'no_orders' => 'No orders found.',
+        'msg_ok' => 'Order and inventory updated successfully!',
+        'msg_del' => 'Order deleted permanently.',
+        'confirm_del' => 'Are you sure you want to DELETE this order?'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+if (isset($_POST['update_status']) && isset($_POST['order_id'])) {
+    $order_id = (int)$_POST['order_id'];
+    $new_status = $_POST['status'];
+
+    try {
+        $pdo->beginTransaction();
+
+        $oldStatusStmt = $pdo->prepare("SELECT status FROM orders WHERE id = ?");
+        $oldStatusStmt->execute([$order_id]);
+        $old_status = $oldStatusStmt->fetchColumn();
+
+        $stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
+        $stmt->execute([$new_status, $order_id]);
+
+        if ($new_status === 'cancelled' && $old_status !== 'cancelled') {
+            $itemsStmt = $pdo->prepare("SELECT product_id, quantity FROM order_items WHERE order_id = ?");
+            $itemsStmt->execute([$order_id]);
+            $items = $itemsStmt->fetchAll(PDO::FETCH_ASSOC);
+
+            $restockStmt = $pdo->prepare("UPDATE products SET quantity = quantity + ?, status = 'available' WHERE id = ?");
+            foreach ($items as $item) {
+                $restockStmt->execute([$item['quantity'], $item['product_id']]);
+            }
+        }
+        
+        if ($old_status === 'cancelled' && $new_status !== 'cancelled') {
+            $itemsStmt = $pdo->prepare("SELECT product_id, quantity FROM order_items WHERE order_id = ?");
+            $itemsStmt->execute([$order_id]);
+            $items = $itemsStmt->fetchAll(PDO::FETCH_ASSOC);
+
+            $deductStmt = $pdo->prepare("UPDATE products SET status = CASE WHEN (quantity - ?) <= 0 THEN 'sold' ELSE status END, quantity = quantity - ? WHERE id = ?");
+            foreach ($items as $item) {
+                $deductStmt->execute([$item['quantity'], $item['quantity'], $item['product_id']]);
+            }
+        }
+
+        $pdo->commit();
+        $message = "<div class='alert success'>{$t['msg_ok']}</div>";
+    } catch (Exception $e) {
+        $pdo->rollBack();
+        $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+    }
+}
+
+if (isset($_GET['delete_order'])) {
+    $delete_id = (int)$_GET['delete_order'];
+    try {
+        $pdo->beginTransaction();
+        $pdo->prepare("DELETE FROM order_items WHERE order_id = ?")->execute([$delete_id]);
+        $pdo->prepare("DELETE FROM orders WHERE id = ?")->execute([$delete_id]);
+        $pdo->commit();
+        $message = "<div class='alert success'>{$t['msg_del']}</div>";
+    } catch (Exception $e) {
+        $pdo->rollBack();
+        $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+    }
+}
+
+$orders = $pdo->query("SELECT * FROM orders ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
+$order_items = [];
+$itemsStmt = $pdo->query("SELECT oi.order_id, oi.quantity, oi.price, p.title FROM order_items oi JOIN products p ON oi.product_id = p.id");
+while ($row = $itemsStmt->fetch(PDO::FETCH_ASSOC)) { 
+    $order_items[$row['order_id']][] = $row; 
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        table { width: 100%; border-collapse: collapse; margin-top: 10px; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; vertical-align: middle;}
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; }
+        table td { color: var(--text-main); font-size: 14px; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .btn-sm { padding: 8px 15px; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); color: #3b82f6; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 700; text-transform: uppercase; transition: 0.2s;}
+        .btn-sm:hover { background: #3b82f6; color: white; }
+        
+        .btn-del { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); color: #ef4444; padding: 8px 15px; text-decoration: none; border-radius: 6px; font-size: 13px; font-weight: 700; text-transform: uppercase; transition: 0.2s;}
+        .btn-del:hover { background: #ef4444; color: white; }
+        
+        .actions-wrap { display: flex; gap: 10px; align-items: center; }
+        select { padding: 8px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; outline: none; font-family: 'Inter', sans-serif;}
+    </style>
+</head>
+<body>
+    <?php include 'sidebar.php'; ?>
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+        <div class="card">
+            <?php if (count($orders) > 0): ?>
+                <table>
+                    <thead>
+                        <tr>
+                            <th><?php echo $t['th_id']; ?></th>
+                            <th><?php echo $t['th_customer']; ?></th>
+                            <th><?php echo $t['th_products']; ?></th>
+                            <th><?php echo $t['th_total']; ?></th>
+                            <th><?php echo $t['th_actions']; ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach ($orders as $order): ?>
+                        <tr>
+                            <td><strong>#<?php echo $order['id']; ?></strong></td>
+                            <td>
+                                <strong><?php echo htmlspecialchars($order['customer_name']); ?></strong><br>
+                                <small style="color: var(--text-muted);"><?php echo htmlspecialchars($order['customer_email']); ?></small>
+                            </td>
+                            <td>
+                                <ul style="margin: 0; padding-left: 20px; font-size: 14px;">
+                                    <?php if (isset($order_items[$order['id']])): ?>
+                                        <?php foreach ($order_items[$order['id']] as $item): ?>
+                                            <li><?php echo htmlspecialchars($item['title']); ?> <strong style="color: #3b82f6;">(x<?php echo $item['quantity']; ?>)</strong></li>
+                                        <?php endforeach; ?>
+                                    <?php endif; ?>
+                                </ul>
+                            </td>
+                            <td><strong style="color: #10b981; font-size: 15px;"><?php echo number_format($order['total_amount'], 2, ',', '.'); ?> €</strong></td>
+                            <td>
+                                <div class="actions-wrap">
+                                    <form method="POST" style="margin: 0; display: flex; gap: 8px;">
+                                        <input type="hidden" name="order_id" value="<?php echo $order['id']; ?>">
+                                        <select name="status">
+                                            <option value="pending" <?php echo $order['status'] == 'pending' ? 'selected' : ''; ?>><?php echo $t['status_pending']; ?></option>
+                                            <option value="completed" <?php echo $order['status'] == 'completed' ? 'selected' : ''; ?>><?php echo $t['status_completed']; ?></option>
+                                            <option value="cancelled" <?php echo $order['status'] == 'cancelled' ? 'selected' : ''; ?>><?php echo $t['status_cancelled']; ?></option>
+                                        </select>
+                                        <button type="submit" name="update_status" class="btn-sm"><?php echo $t['btn_update']; ?></button>
+                                    </form>
+                                    
+                                    <a href="orders.php?delete_order=<?php echo $order['id']; ?>" 
+                                       class="btn-del" 
+                                       onclick="return confirm('<?php echo $t['confirm_del']; ?>');">
+                                       <?php echo $t['btn_delete']; ?>
+                                    </a>
+                                </div>
+                            </td>
+                        </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+            <?php else: ?>
+                <div style="text-align: center; padding: 40px; color: var(--text-muted);">
+                    <?php echo $t['no_orders']; ?>
+                </div>
+            <?php endif; ?>
+        </div>
+    </div>
+</body>
+</html>

+ 150 - 0
admin/product_attributes.php

@@ -0,0 +1,150 @@
+<?php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+session_start();
+if (!isset($_SESSION['user_id'])) {
+    header("Location: login.php");
+    exit;
+}
+
+require_once '../config/database.php';
+header('Content-Type: text/html; charset=utf-8');
+
+$message = '';
+$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+
+if ($product_id === 0) {
+    die("Δεν επιλέχθηκε προϊόν.");
+}
+
+// 1. Φέρνουμε τα στοιχεία του προϊόντος για να δούμε σε ποιο Group ανήκει
+$stmt = $pdo->prepare("SELECT title, attribute_group_id FROM products WHERE id = ?");
+$stmt->execute([$product_id]);
+$product = $stmt->fetch();
+
+if (!$product) {
+    die("Το προϊόν δεν βρέθηκε.");
+}
+
+$group_id = $product['attribute_group_id'];
+
+// Αν το προϊόν δεν έχει Attribute Group, δεν μπορούμε να βάλουμε χαρακτηριστικά
+if (!$group_id) {
+    $no_group_msg = true;
+} else {
+    $no_group_msg = false;
+    
+    // Χειρισμός Αποθήκευσης Φόρμας
+    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['attributes'])) {
+        try {
+            $pdo->beginTransaction();
+            
+            // Διαγράφουμε τις παλιές τιμές για αυτό το προϊόν (ώστε να κάνουμε καθαρό update)
+            $delStmt = $pdo->prepare("DELETE FROM product_attribute_values WHERE product_id = ?");
+            $delStmt->execute([$product_id]);
+            
+            // Εισάγουμε τις νέες τιμές
+            $insStmt = $pdo->prepare("INSERT INTO product_attribute_values (product_id, attribute_id, value) VALUES (?, ?, ?)");
+            
+            foreach ($_POST['attributes'] as $attr_id => $value) {
+                $value = trim($value);
+                if ($value !== '') { // Αποθηκεύουμε μόνο όσα δεν είναι κενά
+                    $insStmt->execute([$product_id, $attr_id, $value]);
+                }
+            }
+            
+            $pdo->commit();
+            $message = "<div class='alert success'>Τα χαρακτηριστικά αποθηκεύτηκαν επιτυχώς!</div>";
+        } catch (Exception $e) {
+            $pdo->rollBack();
+            $message = "<div class='alert error'>Σφάλμα αποθήκευσης: " . $e->getMessage() . "</div>";
+        }
+    }
+
+    // 2. Φέρνουμε τα Attributes της ομάδας + Τις τρέχουσες τιμές (αν υπάρχουν ήδη καταχωρημένες)
+    // Χρησιμοποιούμε LEFT JOIN για να ενώσουμε τα χαρακτηριστικά με τις τιμές που έχει βάλει ο χρήστης
+    $attrStmt = $pdo->prepare("
+        SELECT a.id as attr_id, a.name as attr_name, a.unit, pav.value 
+        FROM attributes a
+        LEFT JOIN product_attribute_values pav ON a.id = pav.attribute_id AND pav.product_id = ?
+        WHERE a.group_id = ?
+        ORDER BY a.name
+    ");
+    $attrStmt->execute([$product_id, $group_id]);
+    $product_attributes = $attrStmt->fetchAll();
+}
+?>
+<!DOCTYPE html>
+<html lang="el">
+<head>
+    <meta charset="UTF-8">
+    <title>Χαρακτηριστικά: <?php echo htmlspecialchars($product['title']); ?></title>
+    <style>
+        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f6f9; margin: 0; }
+        .sidebar { width: 250px; background: #343a40; color: white; position: fixed; height: 100vh; padding-top: 20px; }
+        .sidebar a { display: block; color: white; padding: 15px; text-decoration: none; }
+        .sidebar a:hover { background: #495057; }
+        .content { margin-left: 250px; padding: 20px; }
+        .card { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 600px; margin-top: 20px; }
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #333; }
+        .input-group { display: flex; align-items: center; }
+        .input-group input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px 0 0 4px; box-sizing: border-box; }
+        .input-group .unit { background: #e9ecef; padding: 10px 15px; border: 1px solid #ccc; border-left: none; border-radius: 0 4px 4px 0; color: #555; }
+        .btn { padding: 10px 20px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
+        .btn:hover { background: #218838; }
+        .btn-back { padding: 10px 20px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; margin-right: 10px; }
+        .alert { padding: 15px; border-radius: 4px; margin-bottom: 20px; }
+        .success { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
+        .error { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
+        .info { background: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2>Συμπλήρωση Χαρακτηριστικών</h2>
+        <h4 style="color: #666; margin-top: -15px;">Προϊόν: <?php echo htmlspecialchars($product['title']); ?></h4>
+        
+        <?php echo $message; ?>
+
+        <div class="card">
+            <?php if ($no_group_msg): ?>
+                <div class="alert info">
+                    Αυτό το προϊόν δημιουργήθηκε χωρίς "Ομάδα Χαρακτηριστικών". 
+                    Μπορείτε να του αναθέσετε μια ομάδα επεξεργάζοντας το προϊόν.
+                </div>
+                <a href="products.php" class="btn-back">Επιστροφή στα Προϊόντα</a>
+            <?php else: ?>
+                
+                <form method="POST" action="product_attributes.php?id=<?php echo $product_id; ?>">
+                    
+                    <?php foreach($product_attributes as $attr): ?>
+                        <div class="form-group">
+                            <label><?php echo htmlspecialchars($attr['attr_name']); ?></label>
+                            
+                            <?php if (!empty($attr['unit'])): ?>
+                                <div class="input-group">
+                                    <input type="text" name="attributes[<?php echo $attr['attr_id']; ?>]" value="<?php echo htmlspecialchars($attr['value'] ?? ''); ?>">
+                                    <span class="unit"><?php echo htmlspecialchars($attr['unit']); ?></span>
+                                </div>
+                            <?php else: ?>
+                                <input type="text" name="attributes[<?php echo $attr['attr_id']; ?>]" value="<?php echo htmlspecialchars($attr['value'] ?? ''); ?>" style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
+                            <?php endif; ?>
+                        </div>
+                    <?php endforeach; ?>
+                    
+                    <div style="margin-top: 30px;">
+                        <a href="products.php" class="btn-back">Επιστροφή</a>
+                        <button type="submit" class="btn">Αποθήκευση Χαρακτηριστικών</button>
+                    </div>
+                </form>
+
+            <?php endif; ?>
+        </div>
+    </div>
+
+</body>
+</html>

+ 846 - 0
admin/products.php

@@ -0,0 +1,846 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/products.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Διαχείριση Προϊόντων - Hockey CMS',
+        'header_title' => 'Διαχείριση Προϊόντων',
+        'attr_cat_title' => 'Χαρακτηριστικά Κατηγορίας',
+        'attr_placeholder' => 'π.χ. συμπληρώστε τιμή...',
+        'edit_prod_title' => 'Επεξεργασία Προϊόντος #',
+        'add_prod_title' => 'Προσθήκη Νέου Προϊόντος',
+        'prod_title' => 'Τίτλος Προϊόντος *',
+        'category' => 'Κατηγορία *',
+        'select_cat' => '-- Επίλεξε --',
+        'sku' => 'Κωδικός / Αύξων Αριθμός (SKU)',
+        'price' => 'Τιμή (€) *',
+        'stock' => 'Απόθεμα',
+        'status' => 'Κατάσταση',
+        'status_avail' => 'Διαθέσιμο (Available)',
+        'status_sold' => 'Πουλήθηκε (Sold)',
+        'status_hidden' => 'Απόκρυψη',
+        'seller_assign' => 'Ιδιοκτήτης / Πωλητής *',
+        'tags' => 'Ετικέτες Αναζήτησης (Tags)',
+        'tags_placeholder' => 'Πληκτρολογήστε λέξη και πατήστε Enter...',
+        'tags_hint' => 'Πατήστε Enter ή Κόμμα (,) για να δημιουργήσετε tag.',
+        'desc' => 'Περιγραφή',
+        'photo' => 'Κεντρική Φωτογραφία',
+        'photo_hint_edit' => '(Αφήστε το κενό αν δεν θέλετε να την αλλάξετε)',
+        'photo_hint_add' => '(Αυτόματο resize)',
+        'btn_update' => 'Ενημέρωση',
+        'btn_add' => 'Προσθήκη',
+        'btn_cancel' => 'Ακύρωση',
+        'list_title' => 'Λίστα Προϊόντων',
+        'th_id_sku' => 'ID / SKU',
+        'th_photo' => 'Φωτογραφία',
+        'th_title' => 'Τίτλος / Tags',
+        'th_cat' => 'Κατηγορία',
+        'th_price' => 'Τιμή',
+        'th_stock' => 'Απόθεμα',
+        'th_status' => 'Κατάσταση',
+        'th_actions' => 'Ενέργειες',
+        'no_img' => 'No IMG',
+        'pcs' => 'τεμ.',
+        'btn_view' => 'Προβολή',
+        'btn_del' => 'Διαγραφή',
+        'confirm_del' => 'Είστε σίγουρος ότι θέλετε να διαγράψετε αυτό το προϊόν;',
+        'modal_id' => 'Κωδικός (ID):',
+        'modal_sku' => 'SKU:',
+        'modal_cat' => 'Κατηγορία:',
+        'modal_price' => 'Τιμή:',
+        'modal_stock' => 'Απόθεμα:',
+        'modal_status' => 'Κατάσταση:',
+        'modal_seller' => 'Πωλητής:',
+        'modal_tags' => 'Tags:',
+        'modal_desc' => 'Περιγραφή Αγγελίας:',
+        'modal_no_attrs' => 'Δεν υπάρχουν επιπλέον χαρακτηριστικά',
+        'msg_add_ok' => 'Το προϊόν προστέθηκε επιτυχώς!',
+        'msg_edit_ok' => 'Το προϊόν ενημερώθηκε επιτυχώς!',
+        'msg_err_req' => 'Παρακαλώ συμπληρώστε τα υποχρεωτικά πεδία.',
+        'msg_err_del_orders' => 'Δεν μπορείτε να διαγράψετε αυτό το προϊόν διότι υπάρχει στο ιστορικό παραγγελιών. Αν δεν θέλετε να φαίνεται, αλλάξτε την κατάστασή του σε "Απόκρυψη".',
+        'msg_del_ok' => 'Το προϊόν διαγράφηκε επιτυχώς!'
+    ],
+    'en' => [
+        'page_title' => 'Products Management - Hockey CMS',
+        'header_title' => 'Products Management',
+        'attr_cat_title' => 'Category Attributes',
+        'attr_placeholder' => 'e.g. fill in value...',
+        'edit_prod_title' => 'Edit Product #',
+        'add_prod_title' => 'Add New Product',
+        'prod_title' => 'Product Title *',
+        'category' => 'Category *',
+        'select_cat' => '-- Select --',
+        'sku' => 'Code / Sequential (SKU)',
+        'price' => 'Price (€) *',
+        'stock' => 'Stock',
+        'status' => 'Status',
+        'status_avail' => 'Available',
+        'status_sold' => 'Sold',
+        'status_hidden' => 'Hidden',
+        'seller_assign' => 'Owner / Seller *',
+        'tags' => 'Search Tags',
+        'tags_placeholder' => 'Type a word and press Enter...',
+        'tags_hint' => 'Press Enter or Comma (,) to create a tag.',
+        'desc' => 'Description',
+        'photo' => 'Main Photo',
+        'photo_hint_edit' => '(Leave empty to keep current)',
+        'photo_hint_add' => '(Auto resize)',
+        'btn_update' => 'Update',
+        'btn_add' => 'Add',
+        'btn_cancel' => 'Cancel',
+        'list_title' => 'Products List',
+        'th_id_sku' => 'ID / SKU',
+        'th_photo' => 'Photo',
+        'th_title' => 'Title / Tags',
+        'th_cat' => 'Category',
+        'th_price' => 'Price',
+        'th_stock' => 'Stock',
+        'th_status' => 'Status',
+        'th_actions' => 'Actions',
+        'no_img' => 'No IMG',
+        'pcs' => 'pcs.',
+        'btn_view' => 'View',
+        'btn_del' => 'Delete',
+        'confirm_del' => 'Are you sure you want to delete this product?',
+        'modal_id' => 'ID:',
+        'modal_sku' => 'SKU:',
+        'modal_cat' => 'Category:',
+        'modal_price' => 'Price:',
+        'modal_stock' => 'Stock:',
+        'modal_status' => 'Status:',
+        'modal_seller' => 'Seller:',
+        'modal_tags' => 'Tags:',
+        'modal_desc' => 'Listing Description:',
+        'modal_no_attrs' => 'No additional attributes',
+        'msg_add_ok' => 'Product added successfully!',
+        'msg_edit_ok' => 'Product updated successfully!',
+        'msg_err_req' => 'Please fill in the required fields.',
+        'msg_err_del_orders' => 'Cannot delete product because it exists in order history. If you want to hide it, change status to "Hidden".',
+        'msg_del_ok' => 'Product deleted successfully!'
+    ]
+];
+$t = $translations[$current_lang];
+
+// --- 1. AJAX API: Φέρνει δυναμικά τα πεδία όταν αλλάζει η Κατηγορία ---
+if (isset($_GET['ajax_get_attributes']) && isset($_GET['category_id'])) {
+    $cat_id = (int)$_GET['category_id'];
+    $product_id = isset($_GET['product_id']) ? (int)$_GET['product_id'] : 0;
+    
+    $stmt = $pdo->prepare("SELECT attribute_group_id FROM categories WHERE id = ?");
+    $stmt->execute([$cat_id]);
+    $cat = $stmt->fetch();
+    
+    if ($cat && $cat['attribute_group_id']) {
+        $attrStmt = $pdo->prepare("SELECT id, name, unit FROM attributes WHERE group_id = ? ORDER BY name");
+        $attrStmt->execute([$cat['attribute_group_id']]);
+        $attributes = $attrStmt->fetchAll();
+        
+        $existing_values = [];
+        if ($product_id > 0) {
+            $valStmt = $pdo->prepare("SELECT attribute_id, value FROM product_attribute_values WHERE product_id = ?");
+            $valStmt->execute([$product_id]);
+            while ($vRow = $valStmt->fetch(PDO::FETCH_ASSOC)) {
+                $existing_values[$vRow['attribute_id']] = $vRow['value'];
+            }
+        }
+        
+        if ($attributes) {
+            echo "<h4 style='margin-bottom: 12px; margin-top: 20px; color: var(--primary); text-transform: uppercase; font-size: 14px; letter-spacing: 0.5px;'>{$t['attr_cat_title']}</h4>";
+            echo "<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px;'>";
+            foreach ($attributes as $attr) {
+                $val = isset($existing_values[$attr['id']]) ? htmlspecialchars($existing_values[$attr['id']]) : '';
+                echo "<div class='form-group' style='margin-bottom:0;'>";
+                echo "<label style='display:block; margin-bottom:8px; font-weight:700; font-size:13px; color:var(--text-muted); text-transform:uppercase;'>" . htmlspecialchars($attr['name']) . ($attr['unit'] ? " ({$attr['unit']})" : "") . "</label>";
+                echo "<input type='text' name='attributes[" . $attr['id'] . "]' value='" . $val . "' placeholder='{$t['attr_placeholder']}' style='width:100%; padding:12px; background:#0f172a; border:1px solid var(--border-color); color:var(--text-main); border-radius:6px; box-sizing:border-box; font-family:Inter,sans-serif; outline:none;'>";
+                echo "</div>";
+            }
+            echo "</div>";
+        }
+    }
+    exit;
+}
+
+// --- 2. AJAX API: Δημιουργία προτεινόμενου SKU (Πρόθεμα 3 Γράμματα + Αύξων) ---
+if (isset($_GET['ajax_generate_sku']) && isset($_GET['category_id'])) {
+    $cat_id = (int)$_GET['category_id'];
+    $stmt = $pdo->prepare("SELECT name_en FROM categories WHERE id = ?");
+    $stmt->execute([$cat_id]);
+    $name_en = $stmt->fetchColumn();
+
+    if ($name_en) {
+        // Κρατάμε μόνο τα γράμματα από τα Αγγλικά
+        $clean_name = preg_replace('/[^a-zA-Z]/', '', $name_en);
+        $prefix = strtoupper(substr($clean_name, 0, 3));
+        if (empty($prefix)) { $prefix = 'CAT'; }
+
+        // Βρίσκουμε το μεγαλύτερο νούμερο με αυτό το πρόθεμα
+        $stmtSku = $pdo->prepare("SELECT sku FROM products WHERE sku LIKE ?");
+        $stmtSku->execute([$prefix . '-%']);
+        $skus = $stmtSku->fetchAll(PDO::FETCH_COLUMN);
+
+        $max = 0;
+        foreach ($skus as $s) {
+            $parts = explode('-', $s);
+            if (count($parts) > 1) {
+                $num = (int)end($parts);
+                if ($num > $max) { $max = $num; }
+            }
+        }
+        // Γεννάμε το νέο νούμερο με μηδενικά μπροστά (π.χ. 001, 002)
+        $next_num = str_pad($max + 1, 3, '0', STR_PAD_LEFT);
+        echo $prefix . '-' . $next_num;
+    }
+    exit;
+}
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+function resizeAndSaveImage($source_file, $target_file, $max_resolution = 800) {
+    $info = getimagesize($source_file);
+    if (!$info) return false;
+    $mime = $info['mime'];
+    switch ($mime) {
+        case 'image/jpeg': $image = imagecreatefromjpeg($source_file); break;
+        case 'image/png':  $image = imagecreatefrompng($source_file); break;
+        case 'image/gif':  $image = imagecreatefromgif($source_file); break;
+        default: return false;
+    }
+    $original_width = imagesx($image);
+    $original_height = imagesy($image);
+    $ratio = $max_resolution / max($original_width, $original_height);
+    $new_width = $original_width > $max_resolution ? round($original_width * $ratio) : $original_width;
+    $new_height = $original_height > $max_resolution ? round($original_height * $ratio) : $original_height;
+    $new_image = imagecreatetruecolor($new_width, $new_height);
+    if ($mime == 'image/png') {
+        imagealphablending($new_image, false);
+        imagesavealpha($new_image, true);
+    }
+    imagecopyresampled($new_image, $image, 0, 0, 0, 0, $new_width, $new_height, $original_width, $original_height);
+    imagejpeg($new_image, $target_file, 85);
+    imagedestroy($image);
+    imagedestroy($new_image);
+    return true;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
+    $action = $_POST['action'];
+    $title = trim($_POST['title']);
+    $sku = trim($_POST['sku']); 
+    $category_id = (int)$_POST['category_id'];
+    $price = (float)$_POST['price'];
+    $quantity = (int)$_POST['quantity'];
+    $status = $_POST['status'] ?? 'available';
+    $description = trim($_POST['description']);
+    $search_tags = trim($_POST['search_tags']); 
+    
+    $seller_id = isset($_POST['seller_id']) ? (int)$_POST['seller_id'] : $_SESSION['user_id'];
+
+    if (!empty($title) && !empty($category_id) && !empty($price)) {
+        try {
+            $pdo->beginTransaction();
+
+            $stmtGrp = $pdo->prepare("SELECT attribute_group_id FROM categories WHERE id = ?");
+            $stmtGrp->execute([$category_id]);
+            $group_id = $stmtGrp->fetchColumn();
+
+            if ($action == 'add_product') {
+                $stmt = $pdo->prepare("INSERT INTO products (seller_id, category_id, attribute_group_id, title, sku, description, search_tags, price, quantity, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+                $stmt->execute([$seller_id, $category_id, $group_id, $title, $sku, $description, $search_tags, $price, $quantity, $status]);
+                $product_id = $pdo->lastInsertId();
+                $message_text = $t['msg_add_ok'];
+
+            } elseif ($action == 'edit_product') {
+                $product_id = (int)$_POST['product_id'];
+                $stmt = $pdo->prepare("UPDATE products SET seller_id = ?, category_id = ?, attribute_group_id = ?, title = ?, sku = ?, description = ?, search_tags = ?, price = ?, quantity = ?, status = ? WHERE id = ?");
+                $stmt->execute([$seller_id, $category_id, $group_id, $title, $sku, $description, $search_tags, $price, $quantity, $status, $product_id]);
+                
+                $delAttr = $pdo->prepare("DELETE FROM product_attribute_values WHERE product_id = ?");
+                $delAttr->execute([$product_id]);
+                $message_text = $t['msg_edit_ok'];
+            }
+
+            if (isset($_POST['attributes']) && is_array($_POST['attributes'])) {
+                $insAttr = $pdo->prepare("INSERT INTO product_attribute_values (product_id, attribute_id, value) VALUES (?, ?, ?)");
+                foreach ($_POST['attributes'] as $attr_id => $val) {
+                    $val = trim($val);
+                    if ($val !== '') {
+                        $insAttr->execute([$product_id, $attr_id, $val]);
+                    }
+                }
+            }
+
+            if (isset($_FILES['image']) && $_FILES['image']['error'] == 0) {
+                $upload_dir = '../uploads/equipment/';
+                if (!is_dir($upload_dir)) { mkdir($upload_dir, 0755, true); }
+                $filename = 'prod_' . $product_id . '_' . time() . '.jpg';
+                $target_path = $upload_dir . $filename;
+                if (resizeAndSaveImage($_FILES['image']['tmp_name'], $target_path)) {
+                    $checkImg = $pdo->prepare("SELECT id FROM product_images WHERE product_id = ? AND is_primary = 1");
+                    $checkImg->execute([$product_id]);
+                    if ($checkImg->fetch()) {
+                        $updImg = $pdo->prepare("UPDATE product_images SET image_path = ? WHERE product_id = ? AND is_primary = 1");
+                        $updImg->execute(['uploads/equipment/' . $filename, $product_id]);
+                    } else {
+                        $insImg = $pdo->prepare("INSERT INTO product_images (product_id, image_path, is_primary) VALUES (?, ?, 1)");
+                        $insImg->execute([$product_id, 'uploads/equipment/' . $filename]);
+                    }
+                }
+            }
+
+            $pdo->commit();
+            $message = "<div class='alert success'>{$message_text}</div>";
+        } catch (Exception $e) {
+            $pdo->rollBack();
+            $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    } else {
+        $message = "<div class='alert error'>{$t['msg_err_req']}</div>";
+    }
+}
+
+if (isset($_GET['delete'])) {
+    $del_id = (int)$_GET['delete'];
+    
+    $checkOrders = $pdo->prepare("SELECT COUNT(*) FROM order_items WHERE product_id = ?");
+    $checkOrders->execute([$del_id]);
+    
+    if ($checkOrders->fetchColumn() > 0) {
+        $message = "<div class='alert error'>{$t['msg_err_del_orders']}</div>";
+    } else {
+        try {
+            $pdo->beginTransaction();
+            
+            $imgStmt = $pdo->prepare("SELECT image_path FROM product_images WHERE product_id = ?");
+            $imgStmt->execute([$del_id]);
+            $images = $imgStmt->fetchAll();
+            foreach ($images as $img) {
+                $imgPath = '../' . $img['image_path'];
+                if (file_exists($imgPath) && is_file($imgPath)) { @unlink($imgPath); }
+            }
+            
+            $pdo->prepare("DELETE FROM product_attribute_values WHERE product_id = ?")->execute([$del_id]);
+            $pdo->prepare("DELETE FROM product_images WHERE product_id = ?")->execute([$del_id]);
+            $pdo->prepare("DELETE FROM products WHERE id = ?")->execute([$del_id]);
+            
+            $pdo->commit();
+            $message = "<div class='alert success'>{$t['msg_del_ok']}</div>";
+        } catch (Exception $e) {
+            $pdo->rollBack();
+            $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    }
+}
+
+$edit_product = null;
+if (isset($_GET['edit'])) {
+    $edit_id = (int)$_GET['edit'];
+    $editStmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
+    $editStmt->execute([$edit_id]);
+    $edit_product = $editStmt->fetch(PDO::FETCH_ASSOC);
+}
+
+// Φόρτωση Κατηγοριών
+$cat_col = ($current_lang === 'en') ? 'name_en' : 'name_el';
+$categories = $pdo->query("SELECT id, IFNULL(NULLIF(name_en, ''), name_el) as name FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll();
+if ($current_lang === 'el') {
+    $categories = $pdo->query("SELECT id, name_el as name FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll();
+}
+
+// Φόρτωση όλων των χρηστών & Έξυπνη Εύρεση Ονόματος/Επιθέτου
+$all_users = $pdo->query("SELECT * FROM users ORDER BY username ASC")->fetchAll(PDO::FETCH_ASSOC);
+$user_map = [];
+
+foreach ($all_users as $u) {
+    $fName = $u['first_name'] ?? $u['name'] ?? $u['firstname'] ?? '';
+    $lName = $u['last_name'] ?? $u['surname'] ?? $u['lastname'] ?? '';
+    $fullName = trim($fName . ' ' . $lName);
+    
+    $user_map[$u['id']] = $fullName ? $fullName . ' (' . $u['username'] . ')' : $u['username'];
+}
+
+// Λίστα προϊόντων 
+$products = $pdo->query("SELECT p.id, p.sku, p.title, p.description, p.search_tags, p.price, p.quantity, p.status, p.category_id, p.seller_id, p.created_at, 
+                         IFNULL(NULLIF(c.name_en, ''), c.name_el) as cat_name_en, c.name_el as cat_name_el, u.username, pi.image_path 
+                         FROM products p 
+                         LEFT JOIN categories c ON p.category_id = c.id 
+                         LEFT JOIN users u ON p.seller_id = u.id
+                         LEFT JOIN product_images pi ON (p.id = pi.product_id AND pi.is_primary = 1)
+                         ORDER BY p.id DESC")->fetchAll(PDO::FETCH_ASSOC);
+
+$all_attrs = $pdo->query("SELECT pav.product_id, a.name, a.name_en, a.unit, pav.value 
+                          FROM product_attribute_values pav
+                          JOIN attributes a ON pav.attribute_id = a.id")->fetchAll(PDO::FETCH_ASSOC);
+
+$attrs_by_product = [];
+foreach($all_attrs as $attr) {
+    $attr['display_name'] = ($current_lang === 'en' && !empty($attr['name_en'])) ? $attr['name_en'] : $attr['name'];
+    $attrs_by_product[$attr['product_id']][] = $attr;
+}
+
+foreach ($products as &$prod) {
+    $prod['cat_name'] = ($current_lang === 'en' && !empty($prod['cat_name_en'])) ? $prod['cat_name_en'] : $prod['cat_name_el'];
+    $prod['attributes'] = isset($attrs_by_product[$prod['id']]) ? $attrs_by_product[$prod['id']] : [];
+    
+    $prod['seller_display'] = isset($user_map[$prod['seller_id']]) ? $user_map[$prod['seller_id']] : $prod['username'];
+}
+unset($prod);
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input:not(#tag_input_field), .form-group select, .form-group textarea { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        input[type="file"] { color: var(--text-main); font-family: 'Inter', sans-serif;}
+
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-secondary { background: rgba(255,255,255,0.05); color: var(--text-main); padding: 12px 20px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: 700; border: 1px solid var(--border-color); text-transform: uppercase; transition: 0.2s;}
+        .btn-secondary:hover { background: rgba(255,255,255,0.1); }
+        
+        table { width: 100%; border-collapse: collapse; margin-top: 10px; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; vertical-align: middle; }
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; }
+        table td { color: var(--text-main); font-size: 14px; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .thumb { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border-color);}
+        
+        .badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 800; color: #fff; text-transform: uppercase; }
+        .badge-available { background: #10b981; }
+        .badge-sold { background: #ef4444; }
+        .badge-hidden { background: var(--text-muted); color: #0b1120; }
+
+        .action-btn { background: rgba(59, 130, 246, 0.1); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.2); padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; text-decoration: none; display: inline-block; font-weight: 700; text-transform: uppercase; transition: 0.2s;}
+        .action-btn:hover { background: #3b82f6; color: white; }
+        .btn-edit { background: rgba(249, 115, 22, 0.1); color: var(--primary); border: 1px solid rgba(249, 115, 22, 0.2); }
+        .btn-edit:hover { background: var(--primary); color: white; }
+        .btn-delete { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        .btn-delete:hover { background: #ef4444; color: white; }
+
+        /* Tags Input Styles */
+        .tags-input-container { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px 10px; border: 1px solid var(--border-color); border-radius: 6px; background: #0f172a; align-items: center; cursor: text; }
+        .tags-input-container input { border: none; outline: none; flex-grow: 1; padding: 4px; min-width: 150px; font-family: inherit; font-size: 14px; background: transparent; color: var(--text-main);}
+        .tag-chip { background: rgba(255,255,255,0.1); color: var(--text-main); padding: 4px 10px; border-radius: 16px; font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 6px; border: 1px solid rgba(255,255,255,0.05);}
+        .tag-chip span { cursor: pointer; color: var(--text-muted); font-weight: bold; font-size: 15px; line-height: 1; transition: 0.2s;}
+        .tag-chip span:hover { color: #ef4444; }
+
+        .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.8); }
+        .modal-content { background-color: var(--panel-bg); margin: 5% auto; padding: 30px; border-radius: 8px; width: 80%; max-width: 700px; position: relative; box-shadow: 0 10px 25px rgba(0,0,0,0.5); border: 1px solid var(--border-color);}
+        .close-btn { position: absolute; right: 20px; top: 15px; font-size: 28px; font-weight: bold; cursor: pointer; color: var(--text-muted); }
+        .close-btn:hover { color: var(--text-main); }
+        .modal-body { display: flex; gap: 30px; margin-top: 20px; }
+        .modal-body img { max-width: 100%; border-radius: 8px; border: 1px solid var(--border-color); }
+        .modal-info p { margin: 8px 0; font-size: 14px; color: var(--text-muted);}
+        .modal-info p strong { color: var(--text-main); font-weight: 600;}
+        .modal-desc { background: rgba(0,0,0,0.2); padding: 15px; border-radius: 6px; border: 1px solid var(--border-color); margin-top: 15px; white-space: pre-wrap; font-size: 14px; color: var(--text-main); line-height: 1.5;}
+        .modal-attr-list { list-style: none; padding: 0; margin: 15px 0; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 15px; }
+        .modal-attr-list li { margin-bottom: 8px; font-size: 14px; color: var(--text-muted);}
+        .modal-attr-list li strong { color: var(--text-main); font-weight: 600;}
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div class="card">
+            <h3><?php echo $edit_product ? $t['edit_prod_title'] . $edit_product['id'] : $t['add_prod_title']; ?></h3>
+            <form method="POST" action="products.php" enctype="multipart/form-data" id="product_form">
+                <input type="hidden" name="action" value="<?php echo $edit_product ? 'edit_product' : 'add_product'; ?>">
+                <?php if ($edit_product): ?>
+                    <input type="hidden" name="product_id" value="<?php echo $edit_product['id']; ?>">
+                <?php endif; ?>
+                
+                <div style="display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 20px;">
+                    <div class="form-group">
+                        <label><?php echo $t['prod_title']; ?></label>
+                        <input type="text" name="title" value="<?php echo $edit_product ? htmlspecialchars($edit_product['title']) : ''; ?>" required>
+                    </div>
+
+                    <div class="form-group">
+                        <label><?php echo $t['category']; ?></label>
+                        <select name="category_id" id="category_select" required onchange="loadAttributes(this.value); generateSku(this.value);">
+                            <option value=""><?php echo $t['select_cat']; ?></option>
+                            <?php foreach($categories as $cat): ?>
+                                <option value="<?php echo $cat['id']; ?>" <?php echo ($edit_product && $edit_product['category_id'] == $cat['id']) ? 'selected' : ''; ?>>
+                                    <?php echo htmlspecialchars($cat['name']); ?>
+                                </option>
+                            <?php endforeach; ?>
+                        </select>
+                    </div>
+                    
+                    <div class="form-group">
+                        <label><?php echo $t['sku']; ?></label>
+                        <input type="text" name="sku" id="sku_input_field" value="<?php echo $edit_product ? htmlspecialchars($edit_product['sku']) : ''; ?>" placeholder="π.χ. 001, 002...">
+                    </div>
+                </div>
+
+                <div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 20px;">
+                    <div class="form-group">
+                        <label><?php echo $t['price']; ?></label>
+                        <input type="number" step="0.01" name="price" value="<?php echo $edit_product ? htmlspecialchars($edit_product['price']) : ''; ?>" required>
+                    </div>
+                    
+                    <div class="form-group">
+                        <label><?php echo $t['stock']; ?></label>
+                        <input type="number" name="quantity" value="<?php echo $edit_product ? (int)$edit_product['quantity'] : 1; ?>" min="0" required>
+                    </div>
+
+                    <div class="form-group">
+                        <label><?php echo $t['status']; ?></label>
+                        <select name="status">
+                            <option value="available" <?php echo ($edit_product && $edit_product['status'] == 'available') ? 'selected' : ''; ?>><?php echo $t['status_avail']; ?></option>
+                            <option value="sold" <?php echo ($edit_product && $edit_product['status'] == 'sold') ? 'selected' : ''; ?>><?php echo $t['status_sold']; ?></option>
+                            <option value="hidden" <?php echo ($edit_product && $edit_product['status'] == 'hidden') ? 'selected' : ''; ?>><?php echo $t['status_hidden']; ?></option>
+                        </select>
+                    </div>
+
+                    <div class="form-group">
+                        <label><?php echo $t['seller_assign']; ?></label>
+                        <select name="seller_id" required>
+                            <?php foreach($all_users as $usr): ?>
+                                <option value="<?php echo $usr['id']; ?>" <?php echo ($edit_product && $edit_product['seller_id'] == $usr['id']) ? 'selected' : ((!$edit_product && $_SESSION['user_id'] == $usr['id']) ? 'selected' : ''); ?>>
+                                    <?php echo htmlspecialchars($user_map[$usr['id']]); ?>
+                                </option>
+                            <?php endforeach; ?>
+                        </select>
+                    </div>
+                </div>
+                
+                <div class="form-group">
+                    <label><?php echo $t['tags']; ?></label>
+                    <div class="tags-input-container" id="tags_container">
+                        <input type="text" id="tag_input_field" placeholder="<?php echo $t['tags_placeholder']; ?>">
+                    </div>
+                    <input type="hidden" name="search_tags" id="hidden_search_tags" value="<?php echo $edit_product ? htmlspecialchars($edit_product['search_tags']) : ''; ?>">
+                    <small style="color: var(--text-muted); font-size: 12px; margin-top:6px; display:block;"><?php echo $t['tags_hint']; ?></small>
+                </div>
+
+                <div id="dynamic-attributes-container" style="background: rgba(0,0,0,0.1); padding: 5px 20px; border-radius: 8px; border: 1px solid var(--border-color); margin-bottom: 20px; display: none;"></div>
+
+                <div class="form-group">
+                    <label><?php echo $t['desc']; ?></label>
+                    <textarea name="description" rows="4"><?php echo $edit_product ? htmlspecialchars($edit_product['description']) : ''; ?></textarea>
+                </div>
+
+                <div class="form-group">
+                    <label><?php echo $t['photo']; ?> <span style="color: var(--text-muted); font-size: 11px; text-transform: none;"><?php echo $edit_product ? $t['photo_hint_edit'] : $t['photo_hint_add']; ?></span></label>
+                    <input type="file" name="image" accept="image/jpeg, image/png, image/gif" style="background: transparent; border: 1px dashed var(--border-color); padding: 15px;">
+                </div>
+                
+                <div style="display: flex; gap: 10px; margin-top: 10px;">
+                    <button type="submit" class="btn"><?php echo $edit_product ? $t['btn_update'] : $t['btn_add']; ?></button>
+                    <?php if ($edit_product): ?>
+                        <a href="products.php" class="btn-cancel"><?php echo $t['btn_cancel']; ?></a>
+                    <?php endif; ?>
+                </div>
+            </form>
+        </div>
+
+        <div class="card">
+            <h3><?php echo $t['list_title']; ?></h3>
+            <table>
+                <thead>
+                    <tr>
+                        <th><?php echo $t['th_id_sku']; ?></th>
+                        <th><?php echo $t['th_photo']; ?></th>
+                        <th><?php echo $t['th_title']; ?></th>
+                        <th><?php echo $t['th_cat']; ?></th>
+                        <th><?php echo $t['th_price']; ?></th>
+                        <th><?php echo $t['th_stock']; ?></th>
+                        <th><?php echo $t['th_status']; ?></th>
+                        <th><?php echo $t['th_actions']; ?></th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <?php foreach($products as $prod): ?>
+                    <tr>
+                        <td>
+                            <strong>#<?php echo $prod['id']; ?></strong>
+                            <?php if(!empty($prod['sku'])): ?>
+                                <br><small style="color:var(--text-muted); font-weight:600;"><?php echo htmlspecialchars($prod['sku']); ?></small>
+                            <?php endif; ?>
+                        </td>
+                        <td>
+                            <?php if($prod['image_path']): ?>
+                                <img src="../<?php echo htmlspecialchars($prod['image_path']); ?>" class="thumb">
+                            <?php else: ?>
+                                <div style="width: 60px; height: 60px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); display:flex; align-items:center; justify-content:center; border-radius:6px; font-size:10px; font-weight: bold; color: var(--text-muted);"><?php echo $t['no_img']; ?></div>
+                            <?php endif; ?>
+                        </td>
+                        <td>
+                            <strong style="font-size: 15px;"><?php echo htmlspecialchars($prod['title']); ?></strong>
+                            <?php if(!empty($prod['search_tags'])): ?>
+                                <div style="margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px;">
+                                <?php 
+                                    $tagsArr = explode(',', $prod['search_tags']);
+                                    foreach($tagsArr as $tg) {
+                                        echo '<span style="background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:12px; padding:2px 8px; font-size:10px; color: var(--text-muted); font-weight:600;">'.htmlspecialchars(trim($tg)).'</span>';
+                                    }
+                                ?>
+                                </div>
+                            <?php endif; ?>
+                        </td>
+                        <td><?php echo htmlspecialchars($prod['cat_name']); ?></td>
+                        <td style="color: var(--primary); font-weight: 700;"><?php echo htmlspecialchars($prod['price']); ?> €</td>
+                        <td><?php echo (int)$prod['quantity']; ?> <?php echo $t['pcs']; ?></td>
+                        <td>
+                            <?php 
+                                $status_class = 'badge-available';
+                                $status_label = $t['status_avail'];
+                                if ($prod['status'] == 'sold') { $status_class = 'badge-sold'; $status_label = $t['status_sold']; }
+                                if ($prod['status'] == 'hidden') { $status_class = 'badge-hidden'; $status_label = $t['status_hidden']; }
+                            ?>
+                            <span class="badge <?php echo $status_class; ?>">
+                                <?php echo $status_label; ?>
+                            </span>
+                        </td>
+                        <td>
+                            <div style="display: flex; gap: 6px;">
+                                <button type="button" class="action-btn" 
+                                    onclick="openModal(this)" 
+                                    data-product="<?php echo htmlspecialchars(json_encode($prod), ENT_QUOTES, 'UTF-8'); ?>">
+                                    <?php echo $t['btn_view']; ?>
+                                </button>
+                                <a href="products.php?edit=<?php echo $prod['id']; ?>" class="action-btn btn-edit">Edit</a>
+                                <a href="products.php?delete=<?php echo $prod['id']; ?>" class="action-btn btn-delete" onclick="return confirm('<?php echo $t['confirm_del']; ?>');">Del</a>
+                            </div>
+                        </td>
+                    </tr>
+                    <?php endforeach; ?>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
+    <!-- Modal Προβολής -->
+    <div id="productModal" class="modal">
+        <div class="modal-content">
+            <span class="close-btn" onclick="closeModal()">&times;</span>
+            <h2 id="modal-title" style="margin-top: 0; color: var(--text-main); font-size: 22px;"></h2>
+            
+            <div class="modal-body">
+                <div style="flex: 1;">
+                    <img id="modal-img" src="" alt="Photo" style="display: none;">
+                </div>
+                <div class="modal-info" style="flex: 1.5;">
+                    <p><strong><?php echo $t['modal_id']; ?></strong> #<span id="modal-id"></span></p>
+                    <p><strong><?php echo $t['modal_sku']; ?></strong> <span id="modal-sku" style="color: var(--primary);"></span></p>
+                    <p><strong><?php echo $t['modal_cat']; ?></strong> <span id="modal-cat"></span></p>
+                    <p><strong><?php echo $t['modal_price']; ?></strong> <span id="modal-price" style="color: #10b981; font-weight: bold; font-size: 18px;"></span> €</p>
+                    <p><strong><?php echo $t['modal_stock']; ?></strong> <span id="modal-qty"></span> <?php echo $t['pcs']; ?></p>
+                    <p><strong><?php echo $t['modal_status']; ?></strong> <span id="modal-status" style="text-transform: uppercase; font-size: 12px; background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;"></span></p>
+                    <p><strong><?php echo $t['modal_seller']; ?></strong> <span id="modal-seller" style="color: var(--primary);"></span></p>
+                    <p><strong><?php echo $t['modal_tags']; ?></strong> <span id="modal-tags"></span></p>
+                    
+                    <ul id="modal-attributes" class="modal-attr-list"></ul>
+                </div>
+            </div>
+            
+            <h4 style="margin-bottom: 8px; color: var(--text-main); font-size: 16px; text-transform: uppercase;"><?php echo $t['modal_desc']; ?></h4>
+            <div id="modal-desc" class="modal-desc"></div>
+        </div>
+    </div>
+
+    <script>
+        document.addEventListener('DOMContentLoaded', function() {
+            const tagContainer = document.getElementById('tags_container');
+            const tagInput = document.getElementById('tag_input_field');
+            const hiddenTagsInput = document.getElementById('hidden_search_tags');
+            
+            let tags = [];
+            
+            if (hiddenTagsInput.value.trim() !== '') {
+                tags = hiddenTagsInput.value.split(',').map(t => t.trim()).filter(t => t !== '');
+                renderTags();
+            }
+
+            tagContainer.addEventListener('click', () => tagInput.focus());
+
+            tagInput.addEventListener('keydown', function(e) {
+                if (e.key === 'Enter' || e.key === ',') {
+                    e.preventDefault();
+                    addTag(tagInput.value);
+                } else if (e.key === 'Backspace' && tagInput.value === '' && tags.length > 0) {
+                    removeTag(tags.length - 1);
+                }
+            });
+
+            function addTag(tag) {
+                tag = tag.replace(/,/g, '').trim();
+                if (tag !== '' && !tags.includes(tag)) {
+                    tags.push(tag);
+                    updateHiddenAndRender();
+                }
+                tagInput.value = '';
+            }
+
+            window.removeTagEl = function(index) {
+                tags.splice(index, 1);
+                updateHiddenAndRender();
+            };
+
+            function removeTag(index) {
+                window.removeTagEl(index);
+            }
+
+            function updateHiddenAndRender() {
+                hiddenTagsInput.value = tags.join(', ');
+                renderTags();
+            }
+
+            function renderTags() {
+                tagContainer.querySelectorAll('.tag-chip').forEach(chip => chip.remove());
+                tags.forEach((tag, index) => {
+                    const chip = document.createElement('div');
+                    chip.className = 'tag-chip';
+                    chip.innerHTML = `${tag} <span onclick="window.removeTagEl(${index})">&times;</span>`;
+                    tagContainer.insertBefore(chip, tagInput);
+                });
+            }
+        });
+
+        // ΝΕΑ ΣΥΝΑΡΤΗΣΗ: Υπολογισμός SKU
+        function generateSku(categoryId) {
+            const skuInput = document.getElementById('sku_input_field');
+            if (!categoryId || skuInput.value.trim() !== '') return; // Παράγει μόνο αν το SKU είναι άδειο
+            
+            fetch('products.php?ajax_generate_sku=1&category_id=' + categoryId)
+                .then(res => res.text())
+                .then(sku => {
+                    if (sku.trim() !== '') {
+                        skuInput.value = sku.trim();
+                    }
+                })
+                .catch(err => console.error('SKU Fetch Error:', err));
+        }
+
+        function loadAttributes(categoryId, productId = 0) {
+            const container = document.getElementById('dynamic-attributes-container');
+            if (!categoryId) {
+                container.style.display = 'none';
+                container.innerHTML = '';
+                return;
+            }
+            
+            let url = 'products.php?ajax_get_attributes=1&category_id=' + categoryId;
+            if (productId > 0) {
+                url += '&product_id=' + productId;
+            }
+            
+            fetch(url)
+                .then(response => response.text())
+                .then(html => {
+                    if (html.trim() !== '') {
+                        container.innerHTML = html;
+                        container.style.display = 'block';
+                    } else {
+                        container.style.display = 'none';
+                        container.innerHTML = '';
+                    }
+                })
+                .catch(error => console.error('Error:', error));
+        }
+
+        <?php if ($edit_product): ?>
+            window.addEventListener('DOMContentLoaded', () => {
+                loadAttributes(<?php echo $edit_product['category_id']; ?>, <?php echo $edit_product['id']; ?>);
+            });
+        <?php endif; ?>
+
+        function openModal(button) {
+            const product = JSON.parse(button.getAttribute('data-product'));
+            
+            document.getElementById('modal-id').innerText = product.id;
+            document.getElementById('modal-sku').innerText = product.sku || '-';
+            document.getElementById('modal-title').innerText = product.title;
+            document.getElementById('modal-cat').innerText = product.cat_name;
+            document.getElementById('modal-price').innerText = product.price;
+            document.getElementById('modal-qty').innerText = product.quantity;
+            
+            let stLabel = '<?php echo $t['status_avail']; ?>';
+            if (product.status === 'sold') stLabel = '<?php echo $t['status_sold']; ?>';
+            if (product.status === 'hidden') stLabel = '<?php echo $t['status_hidden']; ?>';
+            document.getElementById('modal-status').innerText = stLabel;
+            
+            document.getElementById('modal-seller').innerText = product.seller_display;
+            
+            document.getElementById('modal-desc').innerText = product.description || '-';
+            document.getElementById('modal-tags').innerText = product.search_tags || '-';
+            
+            const attrList = document.getElementById('modal-attributes');
+            attrList.innerHTML = '';
+            
+            if (product.attributes && product.attributes.length > 0) {
+                product.attributes.forEach(attr => {
+                    let unit = attr.unit ? ' ' + attr.unit : '';
+                    let li = document.createElement('li');
+                    li.innerHTML = `<strong>${attr.display_name}:</strong> ${attr.value}${unit}`;
+                    attrList.appendChild(li);
+                });
+            } else {
+                attrList.innerHTML = '<li><em><?php echo $t['modal_no_attrs']; ?></em></li>';
+            }
+            
+            const imgEl = document.getElementById('modal-img');
+            if (product.image_path) {
+                imgEl.src = '../' + product.image_path;
+                imgEl.style.display = 'block';
+            } else {
+                imgEl.style.display = 'none';
+            }
+            
+            document.getElementById('productModal').style.display = 'block';
+        }
+
+        function closeModal() {
+            document.getElementById('productModal').style.display = 'none';
+        }
+
+        window.onclick = function(event) {
+            const modal = document.getElementById('productModal');
+            if (event.target == modal) {
+                closeModal();
+            }
+        }
+    </script>
+</body>
+</html>

+ 80 - 0
admin/sidebar.php

@@ -0,0 +1,80 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/sidebar.php
+
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$sidebar_t = [
+    'el' => [
+        'panel_title' => 'Admin Panel',
+        'dashboard' => 'Dashboard',
+        'categories' => 'Κατηγορίες',
+        'attributes' => 'Χαρακτηριστικά',
+        'products' => 'Προϊόντα',
+        'orders' => 'Παραγγελίες',
+        'users' => 'Χρήστες & Sellers',
+        'texts' => 'Κείμενα (Δίγλωσσα)',
+        'email_settings' => 'Ρυθμίσεις Email',
+        'whatsapp_settings' => 'Ρυθμίσεις WhatsApp',
+        'sms_settings' => 'Ρυθμίσεις SMS (Brevo)',
+        'email_templates' => 'Πρότυπα Email',
+        'themes' => 'Εμφάνιση (Themes)',
+        'back_eshop' => 'Επιστροφή E-shop',
+        'logout' => 'Αποσύνδεση'
+    ],
+    'en' => [
+        'panel_title' => 'Admin Panel',
+        'dashboard' => 'Dashboard',
+        'categories' => 'Categories',
+        'attributes' => 'Attributes',
+        'products' => 'Products',
+        'orders' => 'Orders',
+        'users' => 'Users & Sellers',
+        'texts' => 'Texts (Bilingual)',
+        'email_settings' => 'Email Settings',
+        'whatsapp_settings' => 'WhatsApp Settings',
+        'sms_settings' => 'SMS Settings (Brevo)',
+        'email_templates' => 'Email Templates',
+        'themes' => 'Appearance (Themes)',
+        'back_eshop' => 'Back to E-shop',
+        'logout' => 'Logout'
+    ]
+];
+$sb_t = $sidebar_t[$current_lang];
+
+$current_page = basename($_SERVER['PHP_SELF']);
+?>
+<div class="sidebar">
+    <h3 style="text-align: center; color: var(--primary); margin-bottom: 25px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase;">⚙️ <?php echo $sb_t['panel_title']; ?></h3>
+    
+    <a href="index.php" class="<?php echo $current_page == 'index.php' ? 'active' : ''; ?>">📊 <?php echo $sb_t['dashboard']; ?></a>
+    <a href="categories.php" class="<?php echo $current_page == 'categories.php' ? 'active' : ''; ?>">📁 <?php echo $sb_t['categories']; ?></a>
+    <a href="attributes.php" class="<?php echo $current_page == 'attributes.php' ? 'active' : ''; ?>">⚡ <?php echo $sb_t['attributes']; ?></a>
+    <a href="products.php" class="<?php echo $current_page == 'products.php' ? 'active' : ''; ?>">🏒 <?php echo $sb_t['products']; ?></a>
+    <a href="orders.php" class="<?php echo $current_page == 'orders.php' ? 'active' : ''; ?>">🛒 <?php echo $sb_t['orders']; ?></a>
+    <a href="users.php" class="<?php echo $current_page == 'users.php' ? 'active' : ''; ?>">👥 <?php echo $sb_t['users']; ?></a>
+    <a href="texts.php" class="<?php echo $current_page == 'texts.php' ? 'active' : ''; ?>">📝 <?php echo $sb_t['texts']; ?></a>
+    <a href="mail_settings.php" class="<?php echo $current_page == 'mail_settings.php' ? 'active' : ''; ?>">📧 <?php echo $sb_t['email_settings']; ?></a>
+    <a href="whatsapp_settings.php" class="<?php echo $current_page == 'whatsapp_settings.php' ? 'active' : ''; ?>">📱 <?php echo $sb_t['whatsapp_settings']; ?></a>
+    <a href="sms_settings.php" class="<?php echo $current_page == 'sms_settings.php' ? 'active' : ''; ?>">💬 <?php echo $sb_t['sms_settings']; ?></a>
+    <a href="email_templates.php" class="<?php echo $current_page == 'email_templates.php' ? 'active' : ''; ?>">✉️ <?php echo $sb_t['email_templates']; ?></a>
+    <a href="themes.php" class="<?php echo $current_page == 'themes.php' ? 'active' : ''; ?>">🎨 <?php echo $sb_t['themes']; ?></a>
+    
+    <div style="position: absolute; bottom: 20px; width: 100%;">
+        <div style="display: flex; justify-content: center; gap: 10px; margin-bottom: 15px;">
+            <a href="?lang=el" style="padding: 2px 8px; border-radius: 12px; font-size: 12px; margin:0; border: none; background: <?php echo $current_lang == 'el' ? 'var(--primary)' : 'rgba(255,255,255,0.1)'; ?>; color: #fff;">GR</a>
+            <a href="?lang=en" style="padding: 2px 8px; border-radius: 12px; font-size: 12px; margin:0; border: none; background: <?php echo $current_lang == 'en' ? 'var(--primary)' : 'rgba(255,255,255,0.1)'; ?>; color: #fff;">EN</a>
+        </div>
+        <a href="../index.php" style="background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-align: center; margin: 0 15px; border-radius: 6px; border: 1px solid rgba(59, 130, 246, 0.2);"><?php echo $sb_t['back_eshop']; ?></a>
+        <a href="../logout.php" style="color: #ef4444; text-align: center; margin-top: 10px; background: transparent; border:none;"><?php echo $sb_t['logout']; ?></a>
+    </div>
+</div>
+
+<style>
+    .sidebar { width: 250px; background: var(--panel-bg); color: var(--text-main); position: fixed; height: 100vh; padding-top: 20px; box-shadow: 2px 0 10px rgba(0,0,0,0.5); border-right: 1px solid var(--border-color); z-index: 100;}
+    .sidebar a { display: block; color: var(--text-muted); padding: 12px 20px; text-decoration: none; font-size: 14px; font-weight: 600; transition: 0.2s; border-left: 4px solid transparent; text-transform: uppercase; letter-spacing: 0.5px;}
+    .sidebar a:hover { background: rgba(255,255,255,0.02); color: var(--text-main); }
+    .sidebar a.active { background: rgba(249, 115, 22, 0.05); color: var(--primary); border-left: 4px solid var(--primary); }
+</style>

+ 229 - 0
admin/sms_settings.php

@@ -0,0 +1,229 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/sms_settings.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+
+if (!isset($_SESSION['user_id']) or$_SESSION['role_id'] != 1) {
+    header("Location: ../index.php");
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Ρυθμίσεις SMS (Brevo) - Hockey CMS',
+        'header_title' => 'Ρυθμίσεις SMS (Brevo API)',
+        'api_details' => 'Στοιχεία Σύνδεσης Brevo',
+        'api_instructions' => 'Οδηγίες: Κάντε δωρεάν εγγραφή στο <a href="https://www.brevo.com/" target="_blank" style="color: #60a5fa;">Brevo</a>, πηγαίνετε στις ρυθμίσεις του λογαριασμού σας (SMTP & API) και δημιουργήστε ένα νέο <strong>API v3 Key</strong>.',
+        'api_key' => 'Brevo API Key *',
+        'sender_name' => 'Όνομα Αποστολέα SMS (Sender Name) *',
+        'sender_hint' => 'Το όνομα που θα φαίνεται στον παραλήπτη (μόνο λατινικοί χαρακτήρες, έως 11 γράμματα).',
+        'btn_save' => '💾 Αποθήκευση Ρυθμίσεων',
+        'test_title' => 'Δοκιμή Αποστολής SMS',
+        'test_desc' => 'Στείλτε ένα δοκιμαστικό SMS στο κινητό σας (με κωδικό χώρας, π.χ. +3069...).',
+        'test_phone' => 'Αριθμός Κινητού Παραλήπτη',
+        'test_text' => 'Κείμενο Μηνύματος',
+        'btn_test' => '💬 Αποστολή SMS',
+        'msg_save_ok' => 'Οι ρυθμίσεις SMS αποθηκεύτηκαν επιτυχώς!',
+        'msg_err_empty' => 'Συμπληρώστε το Brevo API Key και τον αριθμό παραλήπτη!',
+        'msg_test_ok' => 'Το δοκιμαστικό SMS στάλθηκε επιτυχώς στο ',
+        'msg_err_api' => 'Αποτυχία αποστολής SMS'
+    ],
+    'en' => [
+        'page_title' => 'SMS Settings (Brevo) - Hockey CMS',
+        'header_title' => 'SMS Settings (Brevo API)',
+        'api_details' => 'Brevo Connection Details',
+        'api_instructions' => 'Instructions: Register for free on <a href="https://www.brevo.com/" target="_blank" style="color: #60a5fa;">Brevo</a>, go to your account settings (SMTP & API) and create a new <strong>API v3 Key</strong>.',
+        'api_key' => 'Brevo API Key *',
+        'sender_name' => 'SMS Sender Name *',
+        'sender_hint' => 'The name the recipient will see (Latin characters only, up to 11 letters).',
+        'btn_save' => '💾 Save Settings',
+        'test_title' => 'Test SMS Delivery',
+        'test_desc' => 'Send a test SMS to your mobile phone (with country code, e.g. +3069...).',
+        'test_phone' => 'Recipient Mobile Number',
+        'test_text' => 'Message Text',
+        'btn_test' => '💬 Send SMS',
+        'msg_save_ok' => 'SMS settings saved successfully!',
+        'msg_err_empty' => 'Please fill in the Brevo API Key and the recipient number!',
+        'msg_test_ok' => 'Test SMS sent successfully to ',
+        'msg_err_api' => 'SMS Delivery Failed'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+$setting_keys = ['brevo_api_key', 'brevo_sender_name'];
+
+// --- 1. ΑΠΟΘΗΚΕΥΣΗ ΡΥΘΜΙΣΕΩΝ ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action']) and$_POST['action'] == 'save_settings') {
+    try {
+        $pdo->beginTransaction();
+        foreach ($setting_keys as $key) {$value = trim($_POST[$key] ?? '');
+            
+            $check =$pdo->prepare("SELECT COUNT(*) FROM settings WHERE `key` = ?");
+            $check->execute([$key]);
+            
+            if ($check->fetchColumn() > 0) {
+                $stmt =$pdo->prepare("UPDATE settings SET `value` = ? WHERE `key` = ?");
+                $stmt->execute([$value,$key]);
+            } else {
+                $stmt =$pdo->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?)");
+                $stmt->execute([$key,$value]);
+            }
+        }
+        $pdo->commit();$message = "<div class='alert success'>{$t['msg_save_ok']}</div>";
+    } catch (PDOException $e) {
+        $pdo->rollBack();$message = "<div class='alert error'>Σφάλμα αποθήκευσης: " . $e->getMessage() . "</div>";
+    }
+}
+
+// --- Ανάγνωση υφιστάμενων ρυθμίσεων ---
+$current_settings = [];
+$stmt =$pdo->query("SELECT `key`, `value` FROM settings WHERE `key` IN ('" . implode("','", $setting_keys) . "')");
+while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {$current_settings[$row['key']] =$row['value'];
+}
+
+// --- 2. ΑΠΟΣΤΟΛΗ ΔΟΚΙΜΑΣΤΙΚΟΥ SMS ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action']) and$_POST['action'] == 'test_sms') {
+    $test_phone = trim($_POST['test_phone']);
+    $test_text  = trim($_POST['test_text']);
+    
+    $api_key =$current_settings['brevo_api_key'] ?? '';
+    $sender  =$current_settings['brevo_sender_name'] ?? 'HockeyShop';
+
+    if (empty($test_phone) or empty($api_key)) {$message = "<div class='alert error'>{$t['msg_err_empty']}</div>";
+    } else {
+        $url = "https://api.brevo.com/v3/transactionalSMS/sms";
+        
+        $data = [
+            "sender" => $sender,
+            "recipient" => $test_phone,
+            "content" => $test_text,
+            "type" => "transactional"
+        ];
+
+        $ch = curl_init($url);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+        curl_setopt($ch, CURLOPT_HTTPHEADER, [
+            "accept: application/json",
+            "api-key: " . $api_key,
+            "content-type: application/json"
+        ]);
+
+        $response = curl_exec($ch);
+        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+
+        if ($http_code === 201 or $http_code === 200) {$message = "<div class='alert success'>{$t['msg_test_ok']} <strong>{$test_phone}</strong>!</div>";
+        } else {
+            $message = "<div class='alert error'>{$t['msg_err_api']} (HTTP Code: {$http_code}). <br><small>{$response}</small></div>";
+        }
+    }
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input, .form-group textarea { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus, .form-group textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-test { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .btn-test:hover { background: #10b981; color: white; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .info-box { background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 15px; border-radius: 4px; font-size: 13px; color: #60a5fa; margin-bottom: 20px; line-height: 1.5; }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div style="display: grid; grid-template-columns: 2fr 1fr; gap: 25px;">
+            
+            <div class="card">
+                <h3><?php echo $t['api_details']; ?></h3>
+                
+                <div class="info-box">
+                    <?php echo $t['api_instructions']; ?>
+                </div>
+
+                <form method="POST" action="sms_settings.php">
+                    <input type="hidden" name="action" value="save_settings">
+                    
+                    <div class="form-group">
+                        <label><?php echo $t['api_key']; ?></label>
+                        <input type="password" name="brevo_api_key" value="<?php echo htmlspecialchars($current_settings['brevo_api_key'] ?? ''); ?>" placeholder="xkeysib-..." required>
+                    </div>
+
+                    <div class="form-group">
+                        <label><?php echo $t['sender_name']; ?></label>
+                        <input type="text" name="brevo_sender_name" value="<?php echo htmlspecialchars($current_settings['brevo_sender_name'] ?? 'HockeyShop'); ?>" maxlength="11" placeholder="HockeyShop" required>
+                        <small style="color: var(--text-muted);"><?php echo $t['sender_hint']; ?></small>
+                    </div>
+                    
+                    <button type="submit" class="btn"><?php echo $t['btn_save']; ?></button>
+                </form>
+            </div>
+
+            <div class="card" style="height: fit-content;">
+                <h3><?php echo $t['test_title']; ?></h3>
+                <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;"><?php echo $t['test_desc']; ?></p>
+                
+                <form method="POST" action="sms_settings.php">
+                    <input type="hidden" name="action" value="test_sms">
+                    <div class="form-group">
+                        <label><?php echo $t['test_phone']; ?></label>
+                        <input type="text" name="test_phone" value="+30" placeholder="+306912345678" required>
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['test_text']; ?></label>
+                        <textarea name="test_text" rows="3" required>Γεια σας! Αυτό είναι ένα δοκιμαστικό SMS από το Hockey CMS.</textarea>
+                    </div>
+                    <button type="submit" class="btn btn-test"><?php echo $t['btn_test']; ?></button>
+                </form>
+            </div>
+            
+        </div>
+    </div>
+</body>
+</html>

+ 235 - 0
admin/texts.php

@@ -0,0 +1,235 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/texts.php
+session_start();
+if (!isset($_SESSION['user_id']) || $_SESSION['role_id'] != 1) {
+    header("Location: ../login.php");
+    exit;
+}
+
+require_once '../config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Διαχείριση Κειμένων & Logo - Hockey CMS',
+        'header_title' => '✏️ Διαχείριση Στατικών Κειμένων & Logo',
+        'logo_title' => '🖼️ Λογότυπο Site (Logo)',
+        'btn_logo' => '📤 Ανέβασμα & Αποθήκευση Logo',
+        'texts_title' => '📝 Δίγλωσσα Στατικά Λεκτικά',
+        'texts_desc' => 'Συμπληρώστε τα κείμενα και για τις δύο γλώσσες.',
+        'th_key' => 'Κλειδί / Περιγραφή',
+        'th_gr' => '🇬🇷 Ελληνικά (Greek)',
+        'th_en' => '🇬🇧 Αγγλικά (English)',
+        'btn_save' => '💾 Αποθήκευση Γλωσσών',
+        'msg_texts_ok' => 'Όλα τα δίγλωσσα κείμενα ενημερώθηκαν επιτυχώς!',
+        'msg_logo_ok' => 'Το λογότυπο ανέβηκε επιτυχώς!',
+        'msg_err_type' => 'Μη επιτρεπτός τύπος αρχείου.',
+        'msg_err_sql' => 'Προσοχή: Δεν βρέθηκαν τα πεδία. Τρέξατε το SQL query;',
+        'no_texts' => 'Δεν βρέθηκαν κείμενα στη βάση.'
+    ],
+    'en' => [
+        'page_title' => 'Texts & Logo Management - Hockey CMS',
+        'header_title' => '✏️ Static Texts & Logo Management',
+        'logo_title' => '🖼️ Site Logo',
+        'btn_logo' => '📤 Upload & Save Logo',
+        'texts_title' => '📝 Bilingual Static Texts',
+        'texts_desc' => 'Fill in the texts for both languages.',
+        'th_key' => 'Key / Description',
+        'th_gr' => '🇬🇷 Greek',
+        'th_en' => '🇬🇧 English',
+        'btn_save' => '💾 Save Languages',
+        'msg_texts_ok' => 'All bilingual texts updated successfully!',
+        'msg_logo_ok' => 'Logo uploaded successfully!',
+        'msg_err_type' => 'Invalid file type.',
+        'msg_err_sql' => 'Warning: Fields not found. Did you run the SQL query?',
+        'no_texts' => 'No texts found in database.'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_texts'])) {
+    try {
+        $pdo->beginTransaction();
+        $stmt = $pdo->prepare("UPDATE site_texts SET text_value_el = ?, text_value_en = ? WHERE text_key = ?");
+        
+        if (isset($_POST['texts_el']) && is_array($_POST['texts_el'])) {
+            foreach ($_POST['texts_el'] as $key => $val_el) {
+                $val_en = isset($_POST['texts_en'][$key]) ? $_POST['texts_en'][$key] : '';
+                $stmt->execute([trim($val_el), trim($val_en), $key]);
+            }
+        }
+        $pdo->commit();
+        $message = "<div class='alert success'>{$t['msg_texts_ok']}</div>";
+    } catch (Exception $e) {
+        $pdo->rollBack();
+        $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload_logo'])) {
+    if (isset($_FILES['logo_file']) && $_FILES['logo_file']['error'] === UPLOAD_ERR_OK) {
+        $upload_dir = __DIR__ . '/../uploads/';
+        if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0755, true); }
+        $file_extension = strtolower(pathinfo($_FILES['logo_file']['name'], PATHINFO_EXTENSION));
+        $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
+        
+        if (in_array($file_extension, $allowed_extensions)) {
+            $filename = 'logo_' . time() . '.' . $file_extension;
+            $target_path = $upload_dir . $filename;
+            
+            if (move_uploaded_file($_FILES['logo_file']['tmp_name'], $target_path)) {
+                $logo_url = 'uploads/' . $filename;
+                try {
+                    $stmtCheck = $pdo->prepare("SELECT COUNT(*) FROM settings WHERE `key` = 'site_logo'");
+                    $stmtCheck->execute();
+                    if ($stmtCheck->fetchColumn() > 0) {
+                        $stmtUpd = $pdo->prepare("UPDATE settings SET value = ? WHERE `key` = 'site_logo'");
+                        $stmtUpd->execute([$logo_url]);
+                    } else {
+                        $stmtIns = $pdo->prepare("INSERT INTO settings (`key`, value) VALUES ('site_logo', ?)");
+                        $stmtIns->execute([$logo_url]);
+                    }
+                    $message = "<div class='alert success'>{$t['msg_logo_ok']}</div>";
+                } catch (Exception $e) {
+                    $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+                }
+            }
+        } else {
+            $message = "<div class='alert error'>{$t['msg_err_type']}</div>";
+        }
+    }
+}
+
+$current_logo = '';
+try {
+    $logoStmt = $pdo->prepare("SELECT value FROM settings WHERE `key` = 'site_logo'");
+    $logoStmt->execute();
+    $current_logo = $logoStmt->fetchColumn();
+} catch (Exception $e) {}
+
+$texts = [];
+try {
+    $texts = $pdo->query("SELECT * FROM site_texts ORDER BY description ASC")->fetchAll(PDO::FETCH_ASSOC);
+} catch (Exception $e) {
+    $message = "<div class='alert error'>{$t['msg_err_sql']}</div>";
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        table { width: 100%; border-collapse: collapse; margin-top: 15px; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; vertical-align: top; }
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; }
+        
+        input[type="file"] { color: var(--text-main); font-family: 'Inter', sans-serif;}
+        textarea { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; font-family: 'Inter', sans-serif; font-size: 14px; box-sizing: border-box; resize: vertical; outline: none; transition: 0.2s; }
+        textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 25px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .btn-success:hover { background: #10b981; color: white; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .desc-text { font-size: 12px; color: var(--text-muted); display: block; margin-top: 6px; line-height: 1.4; }
+        .lang-label { font-size: 11px; font-weight: 800; color: #3b82f6; display: block; margin-bottom: 6px; letter-spacing: 0.5px;}
+    </style>
+</head>
+<body>
+
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div class="card">
+            <h3><?php echo $t['logo_title']; ?></h3>
+            <?php if ($current_logo): ?>
+                <div style="margin-bottom: 20px; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; display: inline-block; border: 1px solid var(--border-color);">
+                    <img src="../<?php echo htmlspecialchars($current_logo); ?>" style="max-height: 45px; display: block;">
+                </div>
+            <?php endif; ?>
+            <form method="POST" enctype="multipart/form-data">
+                <input type="file" name="logo_file" accept="image/*" required style="margin-bottom: 20px; display: block;">
+                <button type="submit" name="upload_logo" value="1" class="btn btn-success"><?php echo $t['btn_logo']; ?></button>
+            </form>
+        </div>
+
+        <div class="card">
+            <h3><?php echo $t['texts_title']; ?></h3>
+            <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;"><?php echo $t['texts_desc']; ?></p>
+            
+            <form method="POST">
+                <table>
+                    <thead>
+                        <tr>
+                            <th style="width: 25%;"><?php echo $t['th_key']; ?></th>
+                            <th style="width: 37.5%;"><?php echo $t['th_gr']; ?></th>
+                            <th style="width: 37.5%;"><?php echo $t['th_en']; ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php 
+                        if (!empty($texts)) {
+                            foreach($texts as $text_row) {
+                                echo "<tr>";
+                                echo "<td><strong style='color: var(--text-main); font-size: 14px;'>" . htmlspecialchars($text_row['text_key']) . "</strong><span class='desc-text'>" . htmlspecialchars($text_row['description']) . "</span></td>";
+                                
+                                echo "<td>";
+                                echo "<span class='lang-label'>GR</span>";
+                                echo "<textarea name='texts_el[" . htmlspecialchars($text_row['text_key']) . "]' rows='3'>" . htmlspecialchars($text_row['text_value_el'] ?? '', ENT_QUOTES) . "</textarea>";
+                                echo "</td>";
+
+                                echo "<td>";
+                                echo "<span class='lang-label'>EN</span>";
+                                echo "<textarea name='texts_en[" . htmlspecialchars($text_row['text_key']) . "]' rows='3'>" . htmlspecialchars($text_row['text_value_en'] ?? '', ENT_QUOTES) . "</textarea>";
+                                echo "</td>";
+
+                                echo "</tr>";
+                            }
+                        } else {
+                            echo "<tr><td colspan='3' style='text-align:center; padding:30px; color:var(--text-muted);'>{$t['no_texts']}</td></tr>";
+                        }
+                        ?>
+                    </tbody>
+                </table>
+                <br>
+                <button type="submit" name="update_texts" value="1" class="btn"><?php echo $t['btn_save']; ?></button>
+            </form>
+        </div>
+    </div>
+
+</body>
+</html>

+ 262 - 0
admin/themes.php

@@ -0,0 +1,262 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/themes.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+session_start();
+
+if (!isset($_SESSION['user_id']) or$_SESSION['role_id'] != 1) {
+    header("Location: ../login.php");
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Διαχείριση Themes - Hockey CMS',
+        'header_title' => '🎨 Διαχείριση Εμφάνισης (Themes)',
+        'upload_title' => 'Μεταφόρτωση Νέου Theme (.zip)',
+        'choose_zip' => 'Επιλέξτε αρχείο Zip Θέματος',
+        'btn_upload' => 'Ανέβασμα & Εγκατάσταση',
+        'list_title' => 'Εγκατεστημένα Themes',
+        'th_name' => 'Όνομα Φακέλου Theme',
+        'th_status' => 'Κατάσταση',
+        'th_action' => 'Ενέργεια',
+        'badge_active' => 'Ενεργό',
+        'badge_inactive' => 'Ανενεργό',
+        'btn_activate' => 'Ενεργοποίηση',
+        'btn_delete' => 'Διαγραφή',
+        'in_use' => 'Σε χρήση',
+        'msg_upload_ok' => 'Το θέμα ανέβηκε και αποσυμπιέστηκε επιτυχώς!',
+        'msg_err_read' => 'Αποτυχία: Δεν ήταν δυνατή η ανάγνωση του αρχείου ZIP.',
+        'msg_err_ext' => 'Επιτρέπονται μόνο αρχεία .zip',
+        'msg_err_upload' => 'Σφάλμα κατά το ανέβασμα του αρχείου.',
+        'msg_act_ok' => 'Το ενεργό θέμα άλλαξε επιτυχώς!',
+        'msg_err_del_def' => 'Δεν μπορείτε να διαγράψετε το προεπιλεγμένο θέμα (default).',
+        'msg_err_del_act' => 'Δεν μπορείτε να διαγράψετε το θέμα που είναι αυτή τη στιγμή ενεργό. Ενεργοποιήστε πρώτα ένα άλλο.',
+        'msg_del_ok' => 'Το θέμα διαγράφηκε επιτυχώς!',
+        'msg_err_perms' => 'Σφάλμα κατά τη διαγραφή. Ελέγξτε τα δικαιώματα (permissions).'
+    ],
+    'en' => [
+        'page_title' => 'Theme Management - Hockey CMS',
+        'header_title' => '🎨 Appearance Management (Themes)',
+        'upload_title' => 'Upload New Theme (.zip)',
+        'choose_zip' => 'Select Theme Zip File',
+        'btn_upload' => 'Upload & Install',
+        'list_title' => 'Installed Themes',
+        'th_name' => 'Theme Folder Name',
+        'th_status' => 'Status',
+        'th_action' => 'Action',
+        'badge_active' => 'Active',
+        'badge_inactive' => 'Inactive',
+        'btn_activate' => 'Activate',
+        'btn_delete' => 'Delete',
+        'in_use' => 'In Use',
+        'msg_upload_ok' => 'Theme uploaded and extracted successfully!',
+        'msg_err_read' => 'Failure: Could not read ZIP file.',
+        'msg_err_ext' => 'Only .zip files are allowed.',
+        'msg_err_upload' => 'Error uploading file.',
+        'msg_act_ok' => 'Active theme changed successfully!',
+        'msg_err_del_def' => 'You cannot delete the default theme.',
+        'msg_err_del_act' => 'You cannot delete the currently active theme. Activate another one first.',
+        'msg_del_ok' => 'Theme deleted successfully!',
+        'msg_err_perms' => 'Error deleting files. Check permissions.'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+
+$message = '';$themes_dir = __DIR__ . '/../themes/';
+
+function deleteDirectory($dir) {
+    if (!file_exists($dir)) { return true; }
+    if (!is_dir($dir)) { return unlink($dir); }
+    foreach (scandir($dir) as$item) {
+        if ($item == '.' or$item == '..') { continue; }
+        if (!deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) { return false; }
+    }
+    return rmdir($dir);
+}
+
+try {
+    $pdo->exec("CREATE TABLE IF NOT EXISTS settings (`key` VARCHAR(50) PRIMARY KEY, `value` TEXT NOT NULL)");
+} catch (Exception $e) {}
+
+$stmt =$pdo->prepare("SELECT value FROM settings WHERE `key` = 'active_theme'");
+$stmt->execute();
+$active_theme =$stmt->fetchColumn();
+if (!$active_theme) {$active_theme = 'default'; }
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_FILES['theme_zip'])) {
+    if ($_FILES['theme_zip']['error'] == 0) {
+        $file_name =$_FILES['theme_zip']['name'];
+        $file_tmp =$_FILES['theme_zip']['tmp_name'];
+        $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
+
+        if ($ext === 'zip') {$zip = new ZipArchive;
+            if ($zip->open($file_tmp) === TRUE) {$theme_name = pathinfo($file_name, PATHINFO_FILENAME);$target_path = $themes_dir .$theme_name;
+
+                if (!is_dir($target_path)) { mkdir($target_path, 0755, true); }
+                $zip->extractTo($target_path);
+                $zip->close();$message = "<div class='alert success'>{$t['msg_upload_ok']}</div>";
+            } else {
+                $message = "<div class='alert error'>{$t['msg_err_read']}</div>";
+            }
+        } else {
+            $message = "<div class='alert error'>{$t['msg_err_ext']}</div>";
+        }
+    } else {
+        $message = "<div class='alert error'>{$t['msg_err_upload']}</div>";
+    }
+}
+
+if (isset($_GET['activate'])) {
+    $new_theme = basename($_GET['activate']);
+    if (is_dir($themes_dir .$new_theme)) {
+        $stmt =$pdo->prepare("REPLACE INTO settings (`key`, `value`) VALUES ('active_theme', ?)");
+        $stmt->execute([$new_theme]);
+        $active_theme =$new_theme;
+        $message = "<div class='alert success'>{$t['msg_act_ok']}</div>";
+    }
+}
+
+if (isset($_GET['delete'])) {
+    $theme_to_delete = basename($_GET['delete']);
+    if ($theme_to_delete === 'default') {$message = "<div class='alert error'>{$t['msg_err_del_def']}</div>";
+    } elseif ($theme_to_delete === $active_theme) {$message = "<div class='alert error'>{$t['msg_err_del_act']}</div>";
+    } else {
+        $delete_path = $themes_dir .$theme_to_delete;
+        if (is_dir($delete_path)) {
+            if (deleteDirectory($delete_path)) {$message = "<div class='alert success'>{$t['msg_del_ok']}</div>";
+            } else {
+                $message = "<div class='alert error'>{$t['msg_err_perms']}</div>";
+            }
+        }
+    }
+}
+
+$available_themes = [];
+if (is_dir($themes_dir)) {
+    $scan = scandir($themes_dir);
+    foreach ($scan as$folder) {
+        if ($folder !== '.' and$folder !== '..' and is_dir($themes_dir .$folder)) {
+            $available_themes[] =$folder;
+        }
+    }
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 30px;}
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        input[type="file"] { color: var(--text-main); font-family: 'Inter', sans-serif;}
+        
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        
+        table { width: 100%; border-collapse: collapse; margin-top: 10px; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; vertical-align: middle; }
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; }
+        table td { color: var(--text-main); font-size: 15px; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .badge-active { background: #10b981; color: white; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 800; text-transform: uppercase; }
+        
+        .btn-activate { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); padding: 8px 15px; font-size: 12px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: 700; text-transform: uppercase; transition: 0.2s;}
+        .btn-activate:hover { background: #10b981; color: white; }
+        .btn-delete { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); padding: 8px 15px; font-size: 12px; text-decoration: none; border-radius: 6px; display: inline-block; margin-left: 5px; font-weight: 700; text-transform: uppercase; transition: 0.2s;}
+        .btn-delete:hover { background: #ef4444; color: white; }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 30px;">
+            <div class="card">
+                <h3><?php echo $t['upload_title']; ?></h3>
+                <form method="POST" action="themes.php" enctype="multipart/form-data">
+                    <div class="form-group">
+                        <label><?php echo $t['choose_zip']; ?></label>
+                        <input type="file" name="theme_zip" accept=".zip" required style="margin-bottom: 15px; display: block;">
+                    </div>
+                    <button type="submit" class="btn" style="width: 100%;"><?php echo $t['btn_upload']; ?></button>
+                </form>
+            </div>
+
+            <div class="card">
+                <h3><?php echo $t['list_title']; ?></h3>
+                <table>
+                    <thead>
+                        <tr>
+                            <th><?php echo $t['th_name']; ?></th>
+                            <th><?php echo $t['th_status']; ?></th>
+                            <th><?php echo $t['th_action']; ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach($available_themes as$th): ?>
+                        <tr>
+                            <td><strong style="font-family: monospace; font-size: 16px; color: var(--primary);"><?php echo htmlspecialchars($th); ?></strong></td>
+                            <td>
+                                <?php if ($th ===$active_theme): ?>
+                                    <span class="badge-active"><?php echo $t['badge_active']; ?></span>
+                                <?php else: ?>
+                                    <span style="color: var(--text-muted); font-size: 13px; font-weight: 600; text-transform: uppercase;"><?php echo $t['badge_inactive']; ?></span>
+                                <?php endif; ?>
+                            </td>
+                            <td>
+                                <?php if ($th !==$active_theme): ?>
+                                    <a href="themes.php?activate=<?php echo urlencode($th); ?>" class="btn-activate"><?php echo $t['btn_activate']; ?></a>
+                                    
+                                    <?php if ($th !== 'default'): ?>
+                                        <a href="themes.php?delete=<?php echo urlencode($th); ?>" class="btn-delete" onclick="return confirm('Σίγουρα διαγραφή;');"><?php echo $t['btn_delete']; ?></a>
+                                    <?php endif; ?>
+                                    
+                                <?php else: ?>
+                                    <em style="color: var(--text-muted); font-size: 13px; font-weight: 600;"><?php echo $t['in_use']; ?></em>
+                                <?php endif; ?>
+                            </td>
+                        </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+
+</body>
+</html>

+ 453 - 0
admin/users.php

@@ -0,0 +1,453 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/users.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+session_start();
+
+// Αυστηρός έλεγχος: Μόνο ο Administrator (role_id = 1) έχει πρόσβαση
+if (!isset($_SESSION['user_id']) || $_SESSION['role_id'] != 1) {
+    header("Location: index.php");
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Διαχείριση Χρηστών',
+        'header_title' => 'Διαχείριση Χρηστών & Sellers',
+        'allow_sellers_q' => 'Επιτρέπεται οι χρήστες (Πελάτες) να γίνονται από μόνοι τους Πωλητές (Sellers) μέσα από το προφίλ τους;',
+        'yes_allow' => 'ΝΑΙ, ΕΠΙΤΡΕΠΕΤΑΙ',
+        'new_account' => 'Δημιουργία Νέου Λογαριασμού',
+        'username' => 'Όνομα Χρήστη (Username) *',
+        'password' => 'Κωδικός Πρόσβασης (Password) *',
+        'first_name' => 'Όνομα',
+        'last_name' => 'Επώνυμο',
+        'email' => 'Email',
+        'phone' => 'Τηλέφωνο',
+        'role' => 'Ρόλος (Δικαιώματα) *',
+        'select_role' => '-- Επίλεξε Ρόλο --',
+        'btn_create' => 'Δημιουργία Χρήστη',
+        'user_list' => 'Λίστα Εγγεγραμμένων Χρηστών',
+        'th_id' => 'ID',
+        'th_user' => 'Χρήστης',
+        'th_name' => 'Ονοματεπώνυμο',
+        'th_contact' => 'Επικοινωνία',
+        'th_role' => 'Ρόλος',
+        'th_actions' => 'Ενέργειες',
+        'btn_pass' => 'Κωδικός',
+        'btn_del' => 'Διαγραφή',
+        'your_account' => 'Ο λογαριασμός σας',
+        'change_pass_title' => 'Αλλαγή Κωδικού',
+        'for_user' => 'Για τον χρήστη:',
+        'new_pass_label' => 'Νέος Κωδικός Πρόσβασης',
+        'btn_save_pass' => 'Αποθήκευση νέου κωδικού',
+        'msg_setting_ok' => 'Η ρύθμιση για τους πωλητές ενημερώθηκε!',
+        'msg_user_added' => 'Ο χρήστης δημιουργήθηκε επιτυχώς!',
+        'msg_err_empty' => 'Παρακαλώ συμπληρώστε τα υποχρεωτικά πεδία.',
+        'msg_err_role_self' => 'Δεν μπορείτε να αλλάξετε τον δικό σας ρόλο!',
+        'msg_role_upd' => 'Ο ρόλος του χρήστη ενημερώθηκε επιτυχώς!',
+        'msg_err_del_self' => 'Απαγορεύεται η διαγραφή του δικού σας λογαριασμού!',
+        'msg_del_ok' => 'Ο χρήστης διαγράφηκε επιτυχώς!',
+        'msg_err_del' => 'Δεν μπορεί να διαγραφεί αυτός ο χρήστης γιατί έχει καταχωρημένα προϊόντα ή ιστορικό παραγγελιών.',
+        'msg_pass_self_ok' => 'Ο προσωπικός σας κωδικός άλλαξε επιτυχώς! Στην επόμενη σύνδεση θα χρησιμοποιήσετε τον νέο.',
+        'msg_pass_ok' => 'Ο κωδικός του χρήστη άλλαξε επιτυχώς!',
+        'confirm_del' => 'ΠΡΟΣΟΧΗ: Θέλετε σίγουρα να διαγράψετε τον χρήστη: '
+    ],
+    'en' => [
+        'page_title' => 'User Management',
+        'header_title' => 'Users & Sellers Management',
+        'allow_sellers_q' => 'Are users (Customers) allowed to become Sellers on their own through their profile?',
+        'yes_allow' => 'YES, ALLOWED',
+        'new_account' => 'Create New Account',
+        'username' => 'Username *',
+        'password' => 'Password *',
+        'first_name' => 'First Name',
+        'last_name' => 'Last Name',
+        'email' => 'Email',
+        'phone' => 'Phone',
+        'role' => 'Role (Permissions) *',
+        'select_role' => '-- Select Role --',
+        'btn_create' => 'Create User',
+        'user_list' => 'Registered Users List',
+        'th_id' => 'ID',
+        'th_user' => 'User',
+        'th_name' => 'Full Name',
+        'th_contact' => 'Contact',
+        'th_role' => 'Role',
+        'th_actions' => 'Actions',
+        'btn_pass' => 'Password',
+        'btn_del' => 'Delete',
+        'your_account' => 'Your account',
+        'change_pass_title' => 'Change Password',
+        'for_user' => 'For user:',
+        'new_pass_label' => 'New Password',
+        'btn_save_pass' => 'Save new password',
+        'msg_setting_ok' => 'Seller setting updated!',
+        'msg_user_added' => 'User created successfully!',
+        'msg_err_empty' => 'Please fill in the required fields.',
+        'msg_err_role_self' => 'You cannot change your own role!',
+        'msg_role_upd' => 'User role updated successfully!',
+        'msg_err_del_self' => 'Deleting your own account is forbidden!',
+        'msg_del_ok' => 'User deleted successfully!',
+        'msg_err_del' => 'Cannot delete this user because they have registered products or order history.',
+        'msg_pass_self_ok' => 'Your personal password changed successfully! Use the new one on next login.',
+        'msg_pass_ok' => 'User password changed successfully!',
+        'confirm_del' => 'WARNING: Are you sure you want to delete user: '
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+try {
+    $pdo->exec("CREATE TABLE IF NOT EXISTS settings (`key` VARCHAR(50) PRIMARY KEY, `value` TEXT NOT NULL)");
+    $stmt = $pdo->query("SELECT value FROM settings WHERE `key` = 'allow_user_to_seller'");
+    if ($stmt->rowCount() == 0) {
+        $pdo->exec("INSERT INTO settings (`key`, `value`) VALUES ('allow_user_to_seller', '0')");
+    }
+} catch (Exception $e) {}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] == 'update_seller_setting') {
+    $allow_seller = isset($_POST['allow_seller']) ? '1' : '0';
+    $stmt = $pdo->prepare("REPLACE INTO settings (`key`, `value`) VALUES ('allow_user_to_seller', ?)");
+    $stmt->execute([$allow_seller]);
+    $message = "<div class='alert success'>{$t['msg_setting_ok']}</div>";
+}
+
+$stmt = $pdo->query("SELECT value FROM settings WHERE `key` = 'allow_user_to_seller'");
+$allow_user_to_seller = (int)$stmt->fetchColumn();
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] == 'add_user') {
+    $username = trim($_POST['username']);
+    $password = $_POST['password'];
+    $first_name = trim($_POST['first_name']);
+    $last_name = trim($_POST['last_name']);
+    $email = trim($_POST['email']);
+    $phone = trim($_POST['phone']);
+    $role_id = (int)$_POST['role_id'];
+    
+    if (!empty($username) && !empty($password) && !empty($role_id)) {
+        try {
+            $hashed_password = password_hash($password, PASSWORD_DEFAULT);
+            $stmt = $pdo->prepare("INSERT INTO users (role_id, username, password, first_name, last_name, email, phone) VALUES (?, ?, ?, ?, ?, ?, ?)");
+            $stmt->execute([$role_id, $username, $hashed_password, $first_name, $last_name, $email, $phone]);
+            $message = "<div class='alert success'>{$t['msg_user_added']}</div>";
+        } catch (PDOException $e) {
+            $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    } else {
+        $message = "<div class='alert error'>{$t['msg_err_empty']}</div>";
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] == 'change_role') {
+    $target_user_id = (int)$_POST['user_id'];
+    $new_role_id = (int)$_POST['new_role_id'];
+    
+    if ($target_user_id === (int)$_SESSION['user_id']) {
+        $message = "<div class='alert error'>{$t['msg_err_role_self']}</div>";
+    } else {
+        try {
+            $stmt = $pdo->prepare("UPDATE users SET role_id = ? WHERE id = ?");
+            $stmt->execute([$new_role_id, $target_user_id]);
+            $message = "<div class='alert success'>{$t['msg_role_upd']}</div>";
+        } catch (Exception $e) {
+            $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] == 'delete_user') {
+    $del_user_id = (int)$_POST['user_id'];
+    
+    if ($del_user_id === (int)$_SESSION['user_id']) {
+        $message = "<div class='alert error'>{$t['msg_err_del_self']}</div>";
+    } else {
+        try {
+            $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
+            $stmt->execute([$del_user_id]);
+            $message = "<div class='alert success'>{$t['msg_del_ok']}</div>";
+        } catch (PDOException $e) {
+            $message = "<div class='alert error'>{$t['msg_err_del']}</div>";
+        }
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] == 'change_password') {
+    $cp_user_id = (int)$_POST['user_id'];
+    $new_password = $_POST['new_password'];
+    
+    if (!empty($new_password)) {
+        try {
+            $hashed = password_hash($new_password, PASSWORD_DEFAULT);
+            $stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
+            $stmt->execute([$hashed, $cp_user_id]);
+            
+            if ($cp_user_id === (int)$_SESSION['user_id']) {
+                $message = "<div class='alert success'>{$t['msg_pass_self_ok']}</div>";
+            } else {
+                $message = "<div class='alert success'>{$t['msg_pass_ok']}</div>";
+            }
+        } catch (Exception $e) {
+            $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    }
+}
+
+$roles = $pdo->query("SELECT id, name FROM roles ORDER BY id")->fetchAll();
+$users = $pdo->query("
+    SELECT u.id, u.username, u.first_name, u.last_name, u.email, u.phone, u.created_at, u.role_id, r.name as role_name 
+    FROM users u 
+    JOIN roles r ON u.role_id = r.id 
+    ORDER BY u.id DESC
+")->fetchAll();
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?> - Hockey CMS</title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input, .form-group select { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus, .form-group select:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        
+        table { width: 100%; border-collapse: collapse; margin-top: 10px; }
+        table th, table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; vertical-align: middle; }
+        table th { background: rgba(0,0,0,0.2); color: var(--text-muted); text-transform: uppercase; font-size: 13px; font-weight: 700; }
+        table td { color: var(--text-main); font-size: 14px; }
+        
+        .badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 800; color: #fff; text-transform: uppercase; }
+        .badge-admin { background: #ef4444; }
+        .badge-seller { background: #10b981; }
+        .badge-user { background: var(--text-muted); color: #0b1120; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .btn-small { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 700; color: #fff; margin-right: 5px; text-transform: uppercase; transition: 0.2s;}
+        .btn-pass { background: rgba(59, 130, 246, 0.1); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.2); }
+        .btn-pass:hover { background: #3b82f6; color: white;}
+        .btn-del { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        .btn-del:hover { background: #ef4444; color: white;}
+        
+        .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.8); }
+        .modal-content { background-color: var(--panel-bg); margin: 10% auto; padding: 30px; border-radius: 8px; width: 400px; position: relative; box-shadow: 0 10px 25px rgba(0,0,0,0.5); border: 1px solid var(--border-color); }
+        .close-btn { position: absolute; right: 20px; top: 15px; font-size: 24px; font-weight: bold; cursor: pointer; color: var(--text-muted); }
+        .close-btn:hover { color: var(--text-main); }
+        
+        .switch-container { background: rgba(249, 115, 22, 0.1); border: 1px solid rgba(249, 115, 22, 0.2); padding: 15px 25px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px;}
+        .switch-container p { margin: 0; color: var(--primary); font-weight: 600; font-size: 14px;}
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        
+        <?php echo $message; ?>
+
+        <!-- Ρύθμιση: Να γίνονται μόνοι τους Sellers; -->
+        <div class="switch-container">
+            <p><?php echo $t['allow_sellers_q']; ?></p>
+            <form method="POST" action="users.php" style="margin: 0; display: flex; align-items: center; gap: 10px;">
+                <input type="hidden" name="action" value="update_seller_setting">
+                <label style="cursor: pointer; display: flex; align-items: center; gap: 8px; font-weight: 800; color: var(--text-main); font-size: 13px;">
+                    <input type="checkbox" name="allow_seller" value="1" <?php echo $allow_user_to_seller ? 'checked' : ''; ?> style="width: 18px; height: 18px; accent-color: var(--primary);" onchange="this.form.submit()">
+                    <?php echo $t['yes_allow']; ?>
+                </label>
+            </form>
+        </div>
+
+        <!-- Φόρμα Δημιουργίας Χρήστη -->
+        <div class="card">
+            <h3><?php echo $t['new_account']; ?></h3>
+            <form method="POST" action="users.php">
+                <input type="hidden" name="action" value="add_user">
+                
+                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
+                    <div class="form-group">
+                        <label><?php echo $t['username']; ?></label>
+                        <input type="text" name="username" required>
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['password']; ?></label>
+                        <input type="password" name="password" required>
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['first_name']; ?></label>
+                        <input type="text" name="first_name">
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['last_name']; ?></label>
+                        <input type="text" name="last_name">
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['email']; ?></label>
+                        <input type="email" name="email">
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['phone']; ?></label>
+                        <input type="text" name="phone">
+                    </div>
+                    <div class="form-group">
+                        <label><?php echo $t['role']; ?></label>
+                        <select name="role_id" required>
+                            <option value=""><?php echo $t['select_role']; ?></option>
+                            <?php foreach($roles as $role): ?>
+                                <option value="<?php echo $role['id']; ?>"><?php echo strtoupper($role['name']); ?></option>
+                            <?php endforeach; ?>
+                        </select>
+                    </div>
+                </div>
+                <button type="submit" class="btn"><?php echo $t['btn_create']; ?></button>
+            </form>
+        </div>
+
+        <!-- Λίστα Χρηστών -->
+        <div class="card">
+            <h3><?php echo $t['user_list']; ?></h3>
+            <table>
+                <thead>
+                    <tr>
+                        <th><?php echo $t['th_id']; ?></th>
+                        <th><?php echo $t['th_user']; ?></th>
+                        <th><?php echo $t['th_name']; ?></th>
+                        <th><?php echo $t['th_contact']; ?></th>
+                        <th><?php echo $t['th_role']; ?></th>
+                        <th><?php echo $t['th_actions']; ?></th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <?php foreach($users as $user): ?>
+                    <tr>
+                        <td><strong>#<?php echo $user['id']; ?></strong></td>
+                        <td><?php echo htmlspecialchars($user['username']); ?></td>
+                        <td><?php echo htmlspecialchars($user['first_name'] . ' ' . $user['last_name']); ?></td>
+                        <td>
+                            <small style="color: var(--text-muted);">
+                                ✉️ <?php echo htmlspecialchars($user['email']); ?><br>
+                                📞 <?php echo htmlspecialchars($user['phone']); ?>
+                            </small>
+                        </td>
+                        <td>
+                            <?php 
+                                $badge_class = 'badge-user';
+                                if ($user['role_id'] == 1) $badge_class = 'badge-admin';
+                                if ($user['role_id'] == 2) $badge_class = 'badge-seller';
+                            ?>
+                            <span class="badge <?php echo $badge_class; ?>">
+                                <?php echo htmlspecialchars($user['role_name']); ?>
+                            </span>
+                        </td>
+                        <td>
+                            <?php if ($user['id'] != $_SESSION['user_id']): ?>
+                                
+                                <form method="POST" action="users.php" style="display: inline-block; margin-right: 5px;">
+                                    <input type="hidden" name="action" value="change_role">
+                                    <input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
+                                    <select name="new_role_id" style="padding: 6px; border-radius: 4px; font-size: 12px; width: 100px;" onchange="this.form.submit()">
+                                        <?php foreach($roles as $role): ?>
+                                            <option value="<?php echo $role['id']; ?>" <?php echo ($role['id'] == $user['role_id']) ? 'selected' : ''; ?>>
+                                                <?php echo ucfirst($role['name']); ?>
+                                            </option>
+                                        <?php endforeach; ?>
+                                    </select>
+                                </form>
+
+                                <button type="button" class="btn-small btn-pass" 
+                                    onclick="openPasswordModal(<?php echo $user['id']; ?>, '<?php echo addslashes(htmlspecialchars($user['username'])); ?>')">
+                                    <?php echo $t['btn_pass']; ?>
+                                </button>
+                                
+                                <form method="POST" action="users.php" style="display: inline-block;" onsubmit="return confirm('<?php echo $t['confirm_del'] . addslashes(htmlspecialchars($user['username'])); ?>;');">
+                                    <input type="hidden" name="action" value="delete_user">
+                                    <input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
+                                    <button type="submit" class="btn-small btn-del"><?php echo $t['btn_del']; ?></button>
+                                </form>
+                                
+                            <?php else: ?>
+                                <span style="color: var(--text-muted); font-size: 12px; margin-right: 15px; font-style: italic;"><?php echo $t['your_account']; ?></span>
+                                
+                                <button type="button" class="btn-small btn-pass" 
+                                    onclick="openPasswordModal(<?php echo $user['id']; ?>, '<?php echo addslashes(htmlspecialchars($user['username'])); ?>')">
+                                    <?php echo $t['btn_pass']; ?>
+                                </button>
+                            <?php endif; ?>
+                        </td>
+                    </tr>
+                    <?php endforeach; ?>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
+    <!-- Το Αναδυόμενο Παράθυρο (Modal) για την αλλαγή κωδικού -->
+    <div id="passwordModal" class="modal">
+        <div class="modal-content">
+            <span class="close-btn" onclick="closePasswordModal()">&times;</span>
+            <h3 style="margin-top: 0; color: var(--text-main); font-size: 18px; text-transform: uppercase;"><?php echo $t['change_pass_title']; ?></h3>
+            <p style="color: var(--text-muted); font-size: 14px;"><?php echo $t['for_user']; ?> <strong id="modal-username-display" style="color: var(--primary);"></strong></p>
+            
+            <form method="POST" action="users.php">
+                <input type="hidden" name="action" value="change_password">
+                <input type="hidden" name="user_id" id="modal-user-id" value="">
+                
+                <div class="form-group" style="margin-top: 25px;">
+                    <label><?php echo $t['new_pass_label']; ?></label>
+                    <input type="password" name="new_password" required placeholder="••••••••">
+                </div>
+                
+                <button type="submit" class="btn" style="width: 100%; margin-top: 10px;"><?php echo $t['btn_save_pass']; ?></button>
+            </form>
+        </div>
+    </div>
+
+    <script>
+        function openPasswordModal(userId, username) {
+            document.getElementById('modal-user-id').value = userId;
+            document.getElementById('modal-username-display').innerText = username;
+            document.getElementById('passwordModal').style.display = 'block';
+        }
+        function closePasswordModal() {
+            document.getElementById('passwordModal').style.display = 'none';
+        }
+        window.onclick = function(event) {
+            const modal = document.getElementById('passwordModal');
+            if (event.target == modal) {
+                closePasswordModal();
+            }
+        }
+    </script>
+</body>
+</html>

+ 221 - 0
admin/whatsapp_settings.php

@@ -0,0 +1,221 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/admin/whatsapp_settings.php
+require_once 'admin_auth.php';
+require_once '../config/database.php';
+
+if (!isset($_SESSION['user_id']) or$_SESSION['role_id'] != 1) {
+    header("Location: ../index.php");
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+$translations = [
+    'el' => [
+        'page_title' => 'Ρυθμίσεις WhatsApp',
+        'header_title' => 'Ρυθμίσεις WhatsApp Cloud API',
+        'api_details' => 'Στοιχεία Σύνδεσης Meta for Developers',
+        'api_instructions' => 'Οδηγίες: Για να συνδέσετε το WhatsApp, δημιουργήστε λογαριασμό στο Meta for Developers, προσθέστε προϊόν WhatsApp σε μια εφαρμογή (App) και αντιγράψτε τα στοιχεία.',
+        'phone_id' => 'Phone Number ID *',
+        'access_token' => 'Temporary / Permanent Access Token *',
+        'business_id' => 'WhatsApp Business Account ID (Προαιρετικό)',
+        'btn_save' => '💾 Αποθήκευση Ρυθμίσεων',
+        'test_title' => 'Δοκιμή Μηνύματος (Test Message)',
+        'test_desc' => 'Στείλτε ένα δοκιμαστικό μήνυμα σε έναν αριθμό κινητού (π.χ. ο δικός σας αριθμός).',
+        'test_phone' => 'Αριθμός Κινητού (με κωδικό χώρας)',
+        'btn_test' => '💬 Αποστολή WhatsApp',
+        'msg_save_ok' => 'Οι ρυθμίσεις WhatsApp αποθηκεύτηκαν επιτυχώς!',
+        'msg_err_empty' => 'Παρακαλώ συμπληρώστε το τηλέφωνο παραλήπτη και τις ρυθμίσεις API παραπάνω!',
+        'msg_test_ok' => 'Το δοκιμαστικό μήνυμα WhatsApp στάλθηκε επιτυχώς στον αριθμό',
+        'msg_err_api' => 'Αποτυχία αποστολής WhatsApp.'
+    ],
+    'en' => [
+        'page_title' => 'WhatsApp Settings',
+        'header_title' => 'WhatsApp Cloud API Settings',
+        'api_details' => 'Meta for Developers Credentials',
+        'api_instructions' => 'Instructions: To connect WhatsApp, create an account on Meta for Developers, add WhatsApp product to an App and copy the credentials.',
+        'phone_id' => 'Phone Number ID *',
+        'access_token' => 'Temporary / Permanent Access Token *',
+        'business_id' => 'WhatsApp Business Account ID (Optional)',
+        'btn_save' => '💾 Save Settings',
+        'test_title' => 'Test Message',
+        'test_desc' => 'Send a test message to a mobile number (e.g. your own number).',
+        'test_phone' => 'Mobile Number (with country code)',
+        'btn_test' => '💬 Send WhatsApp',
+        'msg_save_ok' => 'WhatsApp settings saved successfully!',
+        'msg_err_empty' => 'Please fill in the recipient phone and API settings above!',
+        'msg_test_ok' => 'Test WhatsApp message sent successfully to',
+        'msg_err_api' => 'WhatsApp Delivery Failed.'
+    ]
+];
+$t = $translations[$current_lang];
+
+header('Content-Type: text/html; charset=utf-8');
+$message = '';
+
+$setting_keys = ['whatsapp_phone_number_id', 'whatsapp_access_token', 'whatsapp_business_account_id'];
+
+// --- 1. ΑΠΟΘΗΚΕΥΣΗ ΡΥΘΜΙΣΕΩΝ ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action']) and$_POST['action'] == 'save_settings') {
+    try {
+        $pdo->beginTransaction();
+        foreach ($setting_keys as $key) {$value = trim($_POST[$key] ?? '');
+            $check =$pdo->prepare("SELECT COUNT(*) FROM settings WHERE `key` = ?");
+            $check->execute([$key]);
+            if ($check->fetchColumn() > 0) {
+                $stmt =$pdo->prepare("UPDATE settings SET `value` = ? WHERE `key` = ?");
+                $stmt->execute([$value,$key]);
+            } else {
+                $stmt =$pdo->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?)");
+                $stmt->execute([$key,$value]);
+            }
+        }
+        $pdo->commit();$message = "<div class='alert success'>{$t['msg_save_ok']}</div>";
+    } catch (PDOException $e) {
+        $pdo->rollBack();$message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+    }
+}
+
+// --- Ανάγνωση υφιστάμενων ρυθμίσεων ---
+$current_settings = [];
+$stmt =$pdo->query("SELECT `key`, `value` FROM settings WHERE `key` IN ('" . implode("','", $setting_keys) . "')");
+while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {$current_settings[$row['key']] =$row['value'];
+}
+
+// --- 2. ΑΠΟΣΤΟΛΗ ΔΟΚΙΜΑΣΤΙΚΟΥ ΜΗΝΥΜΑΤΟΣ ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action']) and$_POST['action'] == 'test_whatsapp') {
+    $test_phone = trim($_POST['test_phone']);
+    $phone_number_id =$current_settings['whatsapp_phone_number_id'] ?? '';
+    $access_token =$current_settings['whatsapp_access_token'] ?? '';
+
+    if (empty($test_phone) or empty($phone_number_id) or empty($access_token)) {$message = "<div class='alert error'>{$t['msg_err_empty']}</div>";
+    } else {
+        $url = "https://graph.facebook.com/v17.0/{$phone_number_id}/messages";
+        $data = [
+            "messaging_product" => "whatsapp",
+            "to" => $test_phone,
+            "type" => "text",
+            "text" => ["body" => "🎉 Γεια σας! Αυτό είναι ένα δοκιμαστικό μήνυμα επιβεβαίωσης από το Hockey CMS."]
+        ];
+
+        $ch = curl_init($url);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+        curl_setopt($ch, CURLOPT_HTTPHEADER, [
+            "Authorization: Bearer " . $access_token,
+            "Content-Type: application/json"
+        ]);
+
+        $response = curl_exec($ch);
+        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+
+        if ($http_code === 200) {$message = "<div class='alert success'>{$t['msg_test_ok']} <strong>{$test_phone}</strong>!</div>";
+        } else {
+            $message = "<div class='alert error'>{$t['msg_err_api']} (HTTP Code: {$http_code}). <br><small>{$response}</small></div>";
+        }
+    }
+}
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo $t['page_title']; ?> - Hockey CMS</title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120;
+            --panel-bg: #1e293b;
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+        }
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
+        .content { margin-left: 250px; padding: 30px; }
+        .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .card { background: var(--panel-bg); padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid var(--border-color); margin-bottom: 25px; }
+        .card h3 { margin-top: 0; color: var(--text-main); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; font-size: 18px; text-transform: uppercase; letter-spacing: 0.5px; }
+        
+        .form-group { margin-bottom: 20px; }
+        .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+        .form-group input { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; outline: none; transition: 0.2s; }
+        .form-group input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        
+        .btn { padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; }
+        .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
+        .btn-test { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .btn-test:hover { background: #10b981; color: white; }
+        
+        .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+        .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
+        .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+        
+        .info-box { background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 15px; border-radius: 4px; font-size: 13px; color: #60a5fa; margin-bottom: 20px; line-height: 1.5; }
+    </style>
+</head>
+<body>
+    
+    <?php include 'sidebar.php'; ?>
+
+    <div class="content">
+        <h2><?php echo $t['header_title']; ?></h2>
+        <?php echo $message; ?>
+
+        <div style="display: grid; grid-template-columns: 2fr 1fr; gap: 25px;">
+            
+            <div class="card">
+                <h3><?php echo $t['api_details']; ?></h3>
+                
+                <div class="info-box">
+                    <?php echo $t['api_instructions']; ?>
+                </div>
+
+                <form method="POST" action="whatsapp_settings.php">
+                    <input type="hidden" name="action" value="save_settings">
+                    
+                    <div class="form-group">
+                        <label><?php echo $t['phone_id']; ?></label>
+                        <input type="text" name="whatsapp_phone_number_id" value="<?php echo htmlspecialchars($current_settings['whatsapp_phone_number_id'] ?? ''); ?>" placeholder="e.g. 1059384820..." required>
+                    </div>
+
+                    <div class="form-group">
+                        <label><?php echo $t['access_token']; ?></label>
+                        <input type="password" name="whatsapp_access_token" value="<?php echo htmlspecialchars($current_settings['whatsapp_access_token'] ?? ''); ?>" placeholder="EAAG..." required>
+                    </div>
+
+                    <div class="form-group">
+                        <label><?php echo $t['business_id']; ?></label>
+                        <input type="text" name="whatsapp_business_account_id" value="<?php echo htmlspecialchars($current_settings['whatsapp_business_account_id'] ?? ''); ?>" placeholder="e.g. 104928374...">
+                    </div>
+                    
+                    <button type="submit" class="btn"><?php echo $t['btn_save']; ?></button>
+                </form>
+            </div>
+
+            <div class="card" style="height: fit-content;">
+                <h3><?php echo $t['test_title']; ?></h3>
+                <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;"><?php echo $t['test_desc']; ?></p>
+                
+                <form method="POST" action="whatsapp_settings.php">
+                    <input type="hidden" name="action" value="test_whatsapp">
+                    <div class="form-group">
+                        <label><?php echo $t['test_phone']; ?></label>
+                        <input type="text" name="test_phone" placeholder="306912345678" required>
+                    </div>
+                    <button type="submit" class="btn btn-test"><?php echo $t['btn_test']; ?></button>
+                </form>
+            </div>
+            
+        </div>
+    </div>
+</body>
+</html>

+ 108 - 0
cart.php

@@ -0,0 +1,108 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/cart.php
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+require_once __DIR__ . '/config/functions.php';
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+// Αρχικοποίηση καλαθιού αν δεν υπάρχει
+if (!isset($_SESSION['cart'])) {$_SESSION['cart'] = [];
+}
+
+// Χειρισμός Ενεργειών (Προσθήκη, Διαγραφή)
+$action = isset($_GET['action']) ?$_GET['action'] : '';
+
+if ($action === 'add') {$product_id = isset($_POST['product_id']) ? (int)$_POST['product_id'] : 0;
+    $qty = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 1;
+    if ($product_id > 0) {
+        if (isset($_SESSION['cart'][$product_id])) {$_SESSION['cart'][$product_id] +=$qty;
+        } else {
+            $_SESSION['cart'][$product_id] =$qty;
+        }
+    }
+    header("Location: cart.php");
+    exit;
+}
+
+if ($action === 'remove') {$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+    if (isset($_SESSION['cart'][$product_id])) {
+        unset($_SESSION['cart'][$product_id]);
+    }
+    header("Location: cart.php");
+    exit;
+}
+
+// Δυναμική ανάγνωση ενεργού theme
+$active_theme = 'default';
+try {
+    $stmt =$pdo->query("SELECT value FROM settings WHERE `key` = 'active_theme'");
+    $res =$stmt->fetchColumn();
+    if ($res) { $active_theme =$res; }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' .$active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';$theme_dir = __DIR__ . '/themes/default';
+}
+
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, ['cache' => false]);
+$twig->addGlobal('session',$_SESSION);
+$twig->addGlobal('current_lang',$current_lang);
+
+// Διόρθωση: Ανάγνωση κατηγοριών με τα δίγλωσσα πεδία (name_el / name_en)
+try {
+    $categoriesRaw =$pdo->query("SELECT * FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll(PDO::FETCH_ASSOC);
+    $categories = [];
+    foreach ($categoriesRaw as $cat) {$cat['name'] = ($current_lang === 'en' && !empty($cat['name_en'])) ? $cat['name_en'] :$cat['name_el'];
+        $categories[] =$cat;
+    }
+} catch (Exception $e) {$categories = [];
+}
+
+// Φέρνουμε τα στοιχεία των προϊόντων που βρίσκονται στο καλάθι από τη βάση
+$cart_items = [];$total_price = 0;
+
+if (!empty($_SESSION['cart'])) {
+    $ids = array_keys($_SESSION['cart']);
+    $placeholders = implode(',', array_fill(0, count($ids), '?'));
+    
+    $stmt =$pdo->prepare("
+        SELECT p.id, p.title, p.price, pi.image_path 
+        FROM products p 
+        LEFT JOIN product_images pi ON (p.id = pi.product_id AND pi.is_primary = 1)
+        WHERE p.id IN ($placeholders)
+    ");
+    $stmt->execute($ids);
+    $products_in_db =$stmt->fetchAll(PDO::FETCH_ASSOC);
+
+    foreach ($products_in_db as$prod) {
+        $prod_id =$prod['id'];
+        $qty =$_SESSION['cart'][$prod_id];$subtotal = $prod['price'] *$qty;
+        $total_price +=$subtotal;
+
+        $cart_items[] = [
+            'id' => $prod_id,
+            'title' => $prod['title'],
+            'price' => $prod['price'],
+            'image_path' => $prod['image_path'],
+            'quantity' => $qty,
+            'subtotal' => $subtotal
+        ];
+    }
+}
+
+echo $twig->render('cart.html.twig', [
+    'site_title' => 'Hockey Gear CMS',
+    'categories' => $categories,
+    'cart_items' => $cart_items,
+    'total_price' => $total_price
+]);

+ 68 - 0
category.php

@@ -0,0 +1,68 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/category.php
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// Δυναμική ανάγνωση ενεργού theme
+$active_theme = 'default';
+try {
+    $stmt =$pdo->query("SELECT value FROM settings WHERE `key` = 'active_theme'");
+    $res =$stmt->fetchColumn();
+    if ($res) { $active_theme =$res; }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' .$active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';$theme_dir = __DIR__ . '/themes/default';
+}
+
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, [
+    'cache' => false,
+]);
+
+// 1. Παίρνουμε το ID της κατηγορίας από το URL (π.χ. category.php?id=2)
+$cat_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+
+if ($cat_id === 0) {
+    header("Location: index.php");
+    exit;
+}
+
+try {
+    // 2. Φέρνουμε τα στοιχεία της συγκεκριμένης κατηγορίας
+    $catStmt =$pdo->prepare("SELECT * FROM categories WHERE id = ?");
+    $catStmt->execute([$cat_id]);
+    $current_category =$catStmt->fetch();
+
+    if (!$current_category) {
+        die("Η κατηγορία δεν βρέθηκε.");
+    }
+
+    // 3. Φέρνουμε όλες τις κατηγορίες για το μενού του header
+    $categories =$pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
+
+    // 4. Φέρνουμε ΜΟΝΟ τα προϊόντα που ανήκουν σε αυτή την κατηγορία
+    $stmt =$pdo->prepare("
+        SELECT p.id, p.title, p.price, p.created_at, pi.image_path 
+        FROM products p 
+        LEFT JOIN product_images pi ON (p.id = pi.product_id AND pi.is_primary = 1)
+        WHERE p.category_id = ? AND p.status = 'available'
+        ORDER BY p.id DESC
+    ");
+    $stmt->execute([$cat_id]);
+    $products =$stmt->fetchAll();
+
+    // 5. Render μέσω Twig (μπορούμε να ξαναχρησιμοποιήσουμε το index.html.twig ή να φτιάξουμε ξεχωριστό category.html.twig)
+    // Εδώ περνάμε και το όνομα της τρέχουσας κατηγορίας για τίτλο
+    echo $twig->render('index.html.twig', [
+        'site_title' => 'Hockey Gear CMS',
+        'categories' => $categories,
+        'products' => $products,
+        'current_category' => $current_category
+    ]);
+
+} catch (Exception $e) {
+    die("Σφάλμα συστήματος: " . $e->getMessage());
+}

+ 108 - 0
checkout.php

@@ -0,0 +1,108 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/checkout.php
+session_start();
+require_once __DIR__ . '/config/database.php';
+require_once __DIR__ . '/includes/mailer.php';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_SESSION['cart'])) {
+    $name = trim($_POST['customer_name']);
+    $email = trim($_POST['customer_email']);
+    $phone = trim($_POST['customer_phone']);
+    $address = trim($_POST['shipping_address']);
+
+    if (!empty($name) && !empty($email) && !empty($phone) && !empty($address)) {
+        try {
+            $pdo->beginTransaction();
+
+            $ids = array_keys($_SESSION['cart']);
+            $placeholders = implode(',', array_fill(0, count($ids), '?'));
+            
+            $stmt = $pdo->prepare("SELECT id, price, quantity FROM products WHERE id IN ($placeholders)");
+            $stmt->execute($ids);
+            $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+            $total_amount = 0;
+            $items_data = [];
+            
+            foreach ($products as $p) {
+                $prod_id = $p['id'];
+                $cart_qty = $_SESSION['cart'][$prod_id];
+                
+                if ($p['quantity'] < $cart_qty) {
+                    throw new Exception("Το προϊόν με ID #{$prod_id} δεν έχει επαρκές απόθεμα.");
+                }
+
+                $total_amount += ($p['price'] * $cart_qty);
+                $items_data[] = [
+                    'product_id' => $prod_id,
+                    'quantity' => $cart_qty,
+                    'price' => $p['price']
+                ];
+            }
+
+            // Εισαγωγή Παραγγελίας
+            $orderStmt = $pdo->prepare("INSERT INTO orders (customer_name, customer_email, customer_phone, shipping_address, total_amount) VALUES (?, ?, ?, ?, ?)");
+            $orderStmt->execute([$name, $email, $phone, $address, $total_amount]);
+            $order_id = $pdo->lastInsertId();
+
+            $itemStmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)");
+            
+            $stockStmt = $pdo->prepare("
+                UPDATE products 
+                SET status = CASE WHEN (quantity - ?) <= 0 THEN 'sold' ELSE status END,
+                    quantity = quantity - ?
+                WHERE id = ?
+            ");
+
+            foreach ($items_data as $item) {
+                $itemStmt->execute([$order_id, $item['product_id'], $item['quantity'], $item['price']]);
+                // Αφαίρεση από το απόθεμα
+                $stockStmt->execute([$item['quantity'], $item['quantity'], $item['product_id']]);
+            }
+
+            $pdo->commit();
+            
+            // --- ΑΠΟΣΤΟΛΗ EMAIL ΜΕΤΑ ΤΗΝ ΕΠΙΤΥΧΗ ΚΑΤΑΧΩΡΗΣΗ ---
+            $placeholders = [
+                'order_id' => $order_id,
+                'customer_name' => $name,
+                'total_amount' => number_format($total_amount, 2, ',', '.')
+            ];
+
+            // 1. Αποστολή Email στον Πελάτη (με το βασικό template)
+            sendTemplateEmail($pdo, 'new_order', $email, $placeholders);
+
+            // 2. Αποστολή Email στους Πωλητές (με το ξεχωριστό seller template)
+            $sellersStmt = $pdo->prepare("
+                SELECT DISTINCT u.email 
+                FROM order_items oi 
+                JOIN products p ON oi.product_id = p.id 
+                JOIN users u ON p.seller_id = u.id 
+                WHERE oi.order_id = ? AND u.email IS NOT NULL AND u.email != ''
+            ");
+            $sellersStmt->execute([$order_id]);
+            $sellers = $sellersStmt->fetchAll(PDO::FETCH_ASSOC);
+
+            foreach ($sellers as $seller) {
+                sendTemplateEmail($pdo, 'seller_new_order', $seller['email'], $placeholders);
+            }
+            // ----------------------------------------------------
+
+            unset($_SESSION['cart']);
+
+            die("<div style='font-family:sans-serif; text-align:center; margin-top:50px;'>
+                    <h1 style='color: #198754;'>Η παραγγελία σου καταχωρήθηκε!</h1>
+                    <p>Ο κωδικός είναι: <strong>#{$order_id}</strong></p>
+                    <p>Το απόθεμα ενημερώθηκε και στάλθηκαν τα αντίστοιχα email. <a href='index.php'>Επιστροφή στην αρχική</a></p>
+                 </div>");
+
+        } catch (Exception $e) {
+            $pdo->rollBack();
+            die("<div style='font-family:sans-serif; text-align:center; margin-top:50px; color: #dc3545;'>
+                    <h2>Σφάλμα: " . $e->getMessage() . "</h2><a href='cart.php'>Πίσω</a>
+                 </div>");
+        }
+    }
+}
+header("Location: index.php");
+exit;

+ 7 - 0
composer.json

@@ -0,0 +1,7 @@
+{
+    "require": {
+        "twig/twig": "^3.0",
+        "phpmailer/phpmailer": "^7.1",
+        "google/apiclient": "^2.20"
+    }
+}

+ 1536 - 0
composer.lock

@@ -0,0 +1,1536 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "142f497976190af6add4332ff879a643",
+    "packages": [
+        {
+            "name": "firebase/php-jwt",
+            "version": "v7.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/php-jwt.git",
+                "reference": "0501f5729b9e2448516674aff07c7d1727869b8e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/0501f5729b9e2448516674aff07c7d1727869b8e",
+                "reference": "0501f5729b9e2448516674aff07c7d1727869b8e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^8.0"
+            },
+            "require-dev": {
+                "guzzlehttp/guzzle": "^7.4||^8.0",
+                "phpfastcache/phpfastcache": "^9.2",
+                "phpseclib/phpseclib": "~3.0",
+                "phpspec/prophecy-phpunit": "^2.2",
+                "phpunit/phpunit": "^9.5",
+                "psr/cache": "^2.0||^3.0",
+                "psr/http-client": "^1.0",
+                "psr/http-factory": "^1.0"
+            },
+            "suggest": {
+                "ext-sodium": "Support EdDSA (Ed25519) signatures",
+                "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
+                "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures"
+            },
+            "type": "library",
+            "extra": {
+                "component": {
+                    "id": "jwt",
+                    "path": "Jwt",
+                    "entry": "README.md",
+                    "target": "googleapis/php-jwt.git"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Firebase\\JWT\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Neuman Vong",
+                    "email": "neuman+pear@twilio.com",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Anant Narayanan",
+                    "email": "anant@php.net",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+            "homepage": "https://github.com/googleapis/php-jwt",
+            "keywords": [
+                "jwt",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/googleapis/php-jwt/issues",
+                "source": "https://github.com/googleapis/php-jwt/tree/v7.2.1"
+            },
+            "time": "2026-09-28T17:51:46+00:00"
+        },
+        {
+            "name": "google/apiclient",
+            "version": "v2.20.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/google-api-php-client.git",
+                "reference": "31b577e7012c4e68d5289d63aaf7e6d471ec1ff1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/31b577e7012c4e68d5289d63aaf7e6d471ec1ff1",
+                "reference": "31b577e7012c4e68d5289d63aaf7e6d471ec1ff1",
+                "shasum": ""
+            },
+            "require": {
+                "firebase/php-jwt": "^6.0||^7.0",
+                "google/apiclient-services": "~0.350",
+                "google/auth": "^1.53",
+                "guzzlehttp/guzzle": "^7.8.2||^8.0",
+                "guzzlehttp/psr7": "^2.6.3||^3.0",
+                "monolog/monolog": "^2.9||^3.0",
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "cache/filesystem-adapter": "^1.1",
+                "composer/composer": "^2.9",
+                "phpcompatibility/php-compatibility": "^9.2",
+                "phpspec/prophecy-phpunit": "^2.1",
+                "phpunit/phpunit": "^9.6",
+                "squizlabs/php_codesniffer": "^3.8",
+                "symfony/css-selector": "^5.4",
+                "symfony/dom-crawler": "^5.4"
+            },
+            "suggest": {
+                "cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)"
+            },
+            "type": "library",
+            "extra": {
+                "component": {
+                    "entry": "src/Client.php"
+                },
+                "branch-alias": {
+                    "dev-main": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/aliases.php"
+                ],
+                "psr-4": {
+                    "Google\\": "src/"
+                },
+                "classmap": [
+                    "src/aliases.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "Client library for Google APIs",
+            "homepage": "http://developers.google.com/api-client-library/php",
+            "keywords": [
+                "google"
+            ],
+            "support": {
+                "issues": "https://github.com/googleapis/google-api-php-client/issues",
+                "source": "https://github.com/googleapis/google-api-php-client/tree/v2.20.1"
+            },
+            "time": "2026-09-25T18:54:19+00:00"
+        },
+        {
+            "name": "google/apiclient-services",
+            "version": "v0.461.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/google-api-php-client-services.git",
+                "reference": "653eec9b7767d34f0cbe2a702cf2c2b825a4857d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/653eec9b7767d34f0cbe2a702cf2c2b825a4857d",
+                "reference": "653eec9b7767d34f0cbe2a702cf2c2b825a4857d",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.6"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "autoload.php"
+                ],
+                "psr-4": {
+                    "Google\\Service\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "Client library for Google APIs",
+            "homepage": "http://developers.google.com/api-client-library/php",
+            "keywords": [
+                "google"
+            ],
+            "support": {
+                "issues": "https://github.com/googleapis/google-api-php-client-services/issues",
+                "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.461.0"
+            },
+            "time": "2026-09-25T22:00:27+00:00"
+        },
+        {
+            "name": "google/auth",
+            "version": "v1.55.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/google-auth-library-php.git",
+                "reference": "ef1b92cb42470b09b3caaf23bb9b4bb9a2dc787e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/ef1b92cb42470b09b3caaf23bb9b4bb9a2dc787e",
+                "reference": "ef1b92cb42470b09b3caaf23bb9b4bb9a2dc787e",
+                "shasum": ""
+            },
+            "require": {
+                "firebase/php-jwt": "^6.0||^7.0",
+                "guzzlehttp/guzzle": "^7.8.2||^8.0",
+                "guzzlehttp/psr7": "^2.6.3||^3.0",
+                "php": "^8.1",
+                "psr/cache": "^2.0||^3.0",
+                "psr/http-client": "^1.0",
+                "psr/http-message": "^1.1||^2.0",
+                "psr/log": "^2.0||^3.0"
+            },
+            "require-dev": {
+                "guzzlehttp/promises": "^2.0.3||^3.0",
+                "kelvinmo/simplejwt": "^1.1.0",
+                "phpseclib/phpseclib": "^3.0.35",
+                "phpspec/prophecy-phpunit": "^2.1",
+                "phpunit/phpunit": "^9.6.34",
+                "sebastian/comparator": ">=1.2.3",
+                "squizlabs/php_codesniffer": "^4.0",
+                "symfony/filesystem": "^6.4.39||^7.3",
+                "symfony/process": "^6.4.41||^7.0",
+                "webmozart/assert": "^1.11||^2.0"
+            },
+            "suggest": {
+                "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
+            },
+            "type": "library",
+            "extra": {
+                "component": {
+                    "id": "auth",
+                    "path": "Auth",
+                    "entry": "README.md",
+                    "target": "googleapis/google-auth-library-php.git"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Google\\Auth\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "Google Auth Library for PHP",
+            "homepage": "https://github.com/googleapis/google-auth-library-php",
+            "keywords": [
+                "Authentication",
+                "google",
+                "oauth2"
+            ],
+            "support": {
+                "docs": "https://cloud.google.com/php/docs/reference/auth/latest",
+                "issues": "https://github.com/googleapis/google-auth-library-php/issues",
+                "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.55.1"
+            },
+            "time": "2026-09-28T17:51:46+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "8.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "93939470950a9b11e2e84204166ef5e048c55fe4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/93939470950a9b11e2e84204166ef5e048c55fe4",
+                "reference": "93939470950a9b11e2e84204166ef5e048c55fe4",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^3.0.2",
+                "guzzlehttp/psr7": "^3.1",
+                "php": "^7.4 || ^8.0",
+                "psr/http-client": "^1.0",
+                "psr/http-factory": "^1.0",
+                "symfony/polyfill-php80": "^1.25",
+                "symfony/polyfill-php82": "^1.27"
+            },
+            "provide": {
+                "psr/http-client-implementation": "1.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "ext-curl": "*",
+                "guzzle/client-integration-tests": "4.0.1",
+                "guzzlehttp/test-server": "^1.0.1",
+                "php-http/message-factory": "^1.1",
+                "phpunit/phpunit": "^9.6.34",
+                "psr/log": "^1.1 || ^2.0 || ^3.0"
+            },
+            "suggest": {
+                "ext-curl": "Required for CURL handler support",
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "psr-18",
+                "psr-7",
+                "rest",
+                "web service"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/8.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-09-06T13:55:09+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "42118e66a53c492effaf92bc357e931985d5c6f9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/42118e66a53c492effaf92bc357e931985d5c6f9",
+                "reference": "42118e66a53c492effaf92bc357e931985d5c6f9",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "phpunit/phpunit": "^9.6.34"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/3.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-08-24T10:00:26+00:00"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "3.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/a3059ba1a84c9139c4ae03cf0f45bea276c97c74",
+                "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0",
+                "psr/http-factory": "^1.1",
+                "psr/http-message": "^2.0",
+                "symfony/polyfill-php80": "^1.25",
+                "symfony/polyfill-php82": "^1.27"
+            },
+            "provide": {
+                "psr/http-factory-implementation": "1.1",
+                "psr/http-message-implementation": "2.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "http-interop/http-factory-tests": "1.1.0",
+                "jshttp/mime-db": "1.54.0.1",
+                "php-http/psr7-integration-tests": "^1.5.1",
+                "phpunit/phpunit": "^9.6.34"
+            },
+            "suggest": {
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://sagikazarmark.hu"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/psr7/issues",
+                "source": "https://github.com/guzzle/psr7/tree/3.1.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-08-24T11:02:13+00:00"
+        },
+        {
+            "name": "monolog/monolog",
+            "version": "3.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/monolog.git",
+                "reference": "72c534fc0ab181ef52d92a68382318631e301608"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/72c534fc0ab181ef52d92a68382318631e301608",
+                "reference": "72c534fc0ab181ef52d92a68382318631e301608",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "psr/log": "^2.0 || ^3.0"
+            },
+            "provide": {
+                "psr/log-implementation": "3.0.0"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^3.0",
+                "doctrine/couchdb": "~1.0@dev",
+                "elasticsearch/elasticsearch": "^7 || ^8",
+                "ext-json": "*",
+                "graylog2/gelf-php": "^1.4.2 || ^2.0",
+                "guzzlehttp/guzzle": "^7.4.5",
+                "guzzlehttp/psr7": "^2.2",
+                "mongodb/mongodb": "^1.8 || ^2.0",
+                "php-amqplib/php-amqplib": "~2.4 || ^3",
+                "php-console/php-console": "^3.1.8",
+                "phpstan/phpstan": "^2",
+                "phpstan/phpstan-deprecation-rules": "^2",
+                "phpstan/phpstan-strict-rules": "^2",
+                "phpunit/phpunit": "^10.5.17 || ^11.0.7",
+                "predis/predis": "^1.1 || ^2",
+                "psr/clock": "^1.0",
+                "rollbar/rollbar": "^4.0",
+                "ruflin/elastica": "^7 || ^8",
+                "symfony/mailer": "^5.4 || ^6",
+                "symfony/mime": "^5.4 || ^6"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+                "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
+                "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+                "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
+                "ext-mbstring": "Allow to work properly with unicode symbols",
+                "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
+                "ext-openssl": "Required to send log messages using SSL",
+                "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
+                "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+                "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
+                "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+                "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records",
+                "rollbar/rollbar": "Allow sending log messages to Rollbar",
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Monolog\\": "src/Monolog"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "https://seld.be"
+                }
+            ],
+            "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+            "homepage": "https://github.com/Seldaek/monolog",
+            "keywords": [
+                "log",
+                "logging",
+                "psr-3"
+            ],
+            "support": {
+                "issues": "https://github.com/Seldaek/monolog/issues",
+                "source": "https://github.com/Seldaek/monolog/tree/3.12.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Seldaek",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-09-09T08:34:20+00:00"
+        },
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v7.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
+                "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "ext-hash": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+                "doctrine/annotations": "^1.2.6 || ^1.13.3",
+                "php-parallel-lint/php-console-highlighter": "^1.0.0",
+                "php-parallel-lint/php-parallel-lint": "^1.3.2",
+                "phpcompatibility/php-compatibility": "^10.0.0@dev",
+                "squizlabs/php_codesniffer": "^3.13.5",
+                "yoast/phpunit-polyfills": "^1.0.4"
+            },
+            "suggest": {
+                "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+                "directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
+                "ext-imap": "Needed to support advanced email address parsing according to RFC822",
+                "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+                "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+                "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+                "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-only"
+            ],
+            "authors": [
+                {
+                    "name": "Marcus Bointon",
+                    "email": "phpmailer@synchromedia.co.uk"
+                },
+                {
+                    "name": "Jim Jagielski",
+                    "email": "jimjag@gmail.com"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "codeworxtech@users.sourceforge.net"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "support": {
+                "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+                "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Synchro",
+                    "type": "github"
+                }
+            ],
+            "time": "2026-05-18T08:06:14+00:00"
+        },
+        {
+            "name": "psr/cache",
+            "version": "3.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+                "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/cache/tree/3.0.0"
+            },
+            "time": "2021-02-03T23:26:27+00:00"
+        },
+        {
+            "name": "psr/http-client",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-client.git",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Client\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP clients",
+            "homepage": "https://github.com/php-fig/http-client",
+            "keywords": [
+                "http",
+                "http-client",
+                "psr",
+                "psr-18"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-client"
+            },
+            "time": "2023-09-23T14:17:50+00:00"
+        },
+        {
+            "name": "psr/http-factory",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-factory.git",
+                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+            "keywords": [
+                "factory",
+                "http",
+                "message",
+                "psr",
+                "psr-17",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-factory"
+            },
+            "time": "2024-04-15T12:06:14+00:00"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/2.0"
+            },
+            "time": "2023-04-04T09:54:51+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+                "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/3.0.2"
+            },
+            "time": "2024-09-11T13:17:53+00:00"
+        },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v3.7.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
+                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/contracts",
+                    "name": "symfony/contracts"
+                },
+                "branch-alias": {
+                    "dev-main": "3.7-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "function.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-06-05T06:23:12+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.37.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
+                "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "provide": {
+                "ext-ctype": "*"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-04-10T16:19:22+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.38.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+                "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+                "shasum": ""
+            },
+            "require": {
+                "ext-iconv": "*",
+                "php": ">=7.2"
+            },
+            "provide": {
+                "ext-mbstring": "*"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-05-27T06:59:30+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.37.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+                "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-04-10T16:19:22+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php82",
+            "version": "v1.38.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php82.git",
+                "reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
+                "reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php82\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php82/tree/v1.38.1"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-05-26T12:45:58+00:00"
+        },
+        {
+            "name": "twig/twig",
+            "version": "v3.30.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/twigphp/Twig.git",
+                "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef",
+                "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1.0",
+                "symfony/deprecation-contracts": "^2.5|^3",
+                "symfony/polyfill-ctype": "^1.8",
+                "symfony/polyfill-mbstring": "^1.3"
+            },
+            "require-dev": {
+                "php-cs-fixer/shim": "^3.0@stable",
+                "phpstan/phpstan": "^2.0@stable",
+                "psr/container": "^1.0|^2.0",
+                "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/Resources/core.php",
+                    "src/Resources/debug.php",
+                    "src/Resources/escaper.php",
+                    "src/Resources/string_loader.php"
+                ],
+                "psr-4": {
+                    "Twig\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com",
+                    "homepage": "http://fabien.potencier.org",
+                    "role": "Lead Developer"
+                },
+                {
+                    "name": "Twig Team",
+                    "role": "Contributors"
+                },
+                {
+                    "name": "Armin Ronacher",
+                    "email": "armin.ronacher@active-4.com",
+                    "role": "Project Founder"
+                }
+            ],
+            "description": "Twig, the flexible, fast, and secure template language for PHP",
+            "homepage": "https://twig.symfony.com",
+            "keywords": [
+                "templating"
+            ],
+            "support": {
+                "issues": "https://github.com/twigphp/Twig/issues",
+                "source": "https://github.com/twigphp/Twig/tree/v3.30.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2026-09-25T13:20:01+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {},
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {},
+    "platform-dev": {},
+    "plugin-api-version": "2.9.0"
+}

+ 28 - 0
config/database.php

@@ -0,0 +1,28 @@
+<?php
+// Αρχείο: config/database.php
+
+// Βάλε εδώ τα δικά σου στοιχεία από το Webmin / MariaDB
+$host = '127.0.0.1'; 
+$db   = 'hockey_cms'; // Το όνομα της βάσης που έφτιαξες
+$user = 'hockeycms';      // Το username (καλό είναι να φτιάξεις έναν ξεχωριστό user, όχι τον root)
+$pass = 'P@ssw0rd'; // Ο κωδικός της βάσης
+$charset = 'utf8mb4'; // Υποστηρίζει τέλεια Ελληνικά και Emojis
+
+// Στήσιμο του Data Source Name (DSN)
+$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
+
+// Ρυθμίσεις του PDO για μέγιστη ασφάλεια και ευκολία
+$options = [
+    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // Πετάει errors αν κάτι πάει λάθος στην SQL
+    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,       // Φέρνει τα αποτελέσματα σαν καθαρά Arrays
+    PDO::ATTR_EMULATE_PREPARES   => false,                  // Αφήνει τη MariaDB να κάνει το escaping (έξτρα ασφάλεια)
+];
+
+try {
+    // Προσπάθεια σύνδεσης
+    $pdo = new PDO($dsn, $user, $pass, $options);
+} catch (\PDOException $e) {
+    // Αν σκάσει η σύνδεση, σταματάει το site και μας δείχνει το λάθος
+    die("Σφάλμα σύνδεσης με τη βάση δεδομένων: " . $e->getMessage());
+}
+?>

+ 56 - 0
config/functions.php

@@ -0,0 +1,56 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/config/functions.php
+
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception;
+
+function sendTemplateEmail($pdo, $template_key,$to_email, $placeholders,$lang = 'el') {
+    if (empty($to_email)) return false;
+
+    // 1. Φόρτωση SMTP ρυθμίσεων από τη βάση
+    $settingsStmt =$pdo->query("SELECT `key`, `value` FROM settings WHERE `key` LIKE 'smtp_%' OR `key` LIKE 'mail_from_%'");
+    $smtp_settings =$settingsStmt->fetchAll(PDO::FETCH_KEY_PAIR);
+
+    // 2. Φόρτωση Template από τη βάση
+    $tplStmt =$pdo->prepare("SELECT * FROM email_templates WHERE template_key = ?");
+    $tplStmt->execute([$template_key]);
+    $tpl =$tplStmt->fetch(PDO::FETCH_ASSOC);
+
+    if (!$tpl) return false;
+
+    $subject = ($lang === 'en') ? $tpl['subject_en'] :$tpl['subject_el'];
+    $body    = ($lang === 'en') ? $tpl['body_en'] :$tpl['body_el'];
+
+    // 3. Αντικατάσταση μεταβλητών (placeholders)
+    foreach ($placeholders as $key =>$val) {
+        $subject = str_replace('{' .$key . '}', $val,$subject);
+        $body    = str_replace('{' .$key . '}', $val,$body);
+    }
+
+    // 4. Αποστολή με PHPMailer
+    require_once __DIR__ . '/../vendor/autoload.php';
+    $mail = new PHPMailer(true);
+
+    try {
+        $mail->isSMTP();$mail->Host       = $smtp_settings['smtp_host'] ?? '';$mail->SMTPAuth   = true;
+        $mail->Username   =$smtp_settings['smtp_username'] ?? '';
+        $mail->Password   =$smtp_settings['smtp_password'] ?? '';
+        
+        $encryption =$smtp_settings['smtp_encryption'] ?? 'tls';
+        $mail->SMTPSecure = ($encryption === 'ssl') ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
+        
+        $mail->Port       = (int)($smtp_settings['smtp_port'] ?? 587);$mail->CharSet    = 'UTF-8';
+
+        $from_email =$smtp_settings['mail_from_address'] ?? 'info@hockeycms.com';
+        $from_name  = $smtp_settings['mail_from_name'] ?? 'Hockey CMS';$mail->setFrom($from_email,$from_name);
+        $mail->addAddress($to_email);
+
+        $mail->isHTML(true);
+        $mail->Subject =$subject;
+        $mail->Body    = nl2br($body);
+
+        return $mail->send();
+    } catch (Exception $e) {
+        return false;
+    }
+}

+ 259 - 0
google_auth.php

@@ -0,0 +1,259 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/google_auth.php
+
+session_start();
+
+require_once __DIR__ . '/config/database.php';
+require_once __DIR__ . '/vendor/autoload.php';
+
+// ============================================================
+// GOOGLE OAUTH SETTINGS
+// ============================================================
+
+$clientID = '1062187052171-is354osfmism8chdok6bqe25i0iccmdt.apps.googleusercontent.com';
+
+// ΒΑΛΕ ΕΔΩ ΤΟ ΝΕΟ CLIENT SECRET ΠΟΥ ΘΑ ΚΑΝΕΙΣ ROTATE
+$clientSecret = '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';
+
+$redirectUri = 'https://hockeycms.eparmak.eu/google_auth.php';
+
+// ============================================================
+// GOOGLE CLIENT
+// ============================================================
+
+$client = new Google_Client();
+
+$client->setClientId($clientID);
+$client->setClientSecret($clientSecret);
+$client->setRedirectUri($redirectUri);
+
+// OAuth scopes
+$client->addScope('openid');
+$client->addScope('email');
+$client->addScope('profile');
+
+// Ζητάμε offline access όπου χρειάζεται
+$client->setAccessType('online');
+
+// Για να παίρνουμε πάντα consent όταν χρειάζεται
+$client->setPrompt('select_account');
+
+// ============================================================
+// GOOGLE CALLBACK
+// ============================================================
+
+if (isset($_GET['code'])) {
+
+    // --------------------------------------------------------
+    // Έλεγχος OAuth state
+    // --------------------------------------------------------
+
+    if (
+        !isset($_GET['state']) ||
+        !isset($_SESSION['google_oauth_state']) ||
+        !hash_equals($_SESSION['google_oauth_state'], $_GET['state'])
+    ) {
+        die('Μη έγκυρο OAuth state.');
+    }
+
+    unset($_SESSION['google_oauth_state']);
+
+    // --------------------------------------------------------
+    // Ανταλλαγή authorization code με access token
+    // --------------------------------------------------------
+
+    try {
+        $token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
+    } catch (Exception $e) {
+        die(
+            'Σφάλμα κατά τη σύνδεση με Google: ' .
+            htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')
+        );
+    }
+
+    if (isset($token['error'])) {
+        $errorDescription = $token['error_description'] ?? $token['error'];
+        die(
+            'Google OAuth Error: ' .
+            htmlspecialchars($errorDescription, ENT_QUOTES, 'UTF-8')
+        );
+    }
+
+    if (!isset($token['access_token'])) {
+        die('Δεν ελήφθη access token από τη Google.');
+    }
+
+    $client->setAccessToken($token['access_token']);
+
+    // --------------------------------------------------------
+    // Λήψη στοιχείων χρήστη από Google
+    // --------------------------------------------------------
+
+    try {
+        $google_oauth = new Google_Service_Oauth2($client);
+        $google_account_info = $google_oauth->userinfo->get();
+    } catch (Exception $e) {
+        die(
+            'Αδυναμία λήψης στοιχείων από Google: ' .
+            htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')
+        );
+    }
+
+    // --------------------------------------------------------
+    // User information
+    // --------------------------------------------------------
+
+    $email = trim((string) $google_account_info->email);
+    $first_name = trim((string) ($google_account_info->givenName ?? ''));
+    $last_name = trim((string) ($google_account_info->familyName ?? ''));
+
+    if ($email === '') {
+        die('Η Google δεν επέστρεψε email για τον χρήστη.');
+    }
+
+    // ========================================================
+    // SEARCH USER IN DATABASE
+    // ========================================================
+
+    try {
+        $stmt = $pdo->prepare(
+            "SELECT *
+             FROM users
+             WHERE username = ? OR email = ?
+             LIMIT 1"
+        );
+        $stmt->execute([$email, $email]);
+        $user = $stmt->fetch(PDO::FETCH_ASSOC);
+    } catch (PDOException $e) {
+        die(
+            'Σφάλμα βάσης δεδομένων: ' .
+            htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')
+        );
+    }
+
+    // ========================================================
+    // EXISTING USER
+    // ========================================================
+
+    if ($user) {
+
+        // Προαιρετικά ενημερώνουμε όνομα/επώνυμο
+        // αν υπάρχουν διαθέσιμα από Google.
+        if (empty($user['first_name']) && $first_name !== '') {
+            $update = $pdo->prepare(
+                "UPDATE users
+                 SET first_name = ?
+                 WHERE id = ?"
+            );
+            $update->execute([$first_name, $user['id']]);
+            $user['first_name'] = $first_name;
+        }
+
+        if (empty($user['last_name']) && $last_name !== '') {
+            $update = $pdo->prepare(
+                "UPDATE users
+                 SET last_name = ?
+                 WHERE id = ?"
+            );
+            $update->execute([$last_name, $user['id']]);
+            $user['last_name'] = $last_name;
+        }
+
+        // ----------------------------------------------------
+        // Login
+        // ----------------------------------------------------
+        session_regenerate_id(true);
+
+        $_SESSION['user_id'] = $user['id'];
+        $_SESSION['username'] = $user['username'];
+        $_SESSION['role_id'] = $user['role_id'];
+        $_SESSION['first_name'] = $user['first_name'] ?? '';
+        $_SESSION['is_seller'] = $user['is_seller'] ?? 0;
+
+    }
+
+    // ========================================================
+    // NEW USER
+    // ========================================================
+
+    else {
+
+        // Τυχαίος password επειδή ο χρήστης θα κάνει login μέσω Google.
+        $random_password = password_hash(
+            bin2hex(random_bytes(32)),
+            PASSWORD_DEFAULT
+        );
+
+        // 3 = Απλός χρήστης / Πελάτης
+        // 2 = Seller
+        // 1 = Admin
+        $role_id = 3;
+
+        try {
+            $insert = $pdo->prepare(
+                "INSERT INTO users
+                (
+                    username,
+                    email,
+                    password,
+                    first_name,
+                    last_name,
+                    role_id
+                )
+                VALUES (?, ?, ?, ?, ?, ?)"
+            );
+
+            $insert->execute([
+                $email,
+                $email,
+                $random_password,
+                $first_name,
+                $last_name,
+                $role_id
+            ]);
+
+            $new_user_id = $pdo->lastInsertId();
+
+        } catch (PDOException $e) {
+            die(
+                'Σφάλμα κατά την αυτόματη εγγραφή του χρήστη: ' .
+                htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')
+            );
+        }
+
+        // ----------------------------------------------------
+        // Login νέου χρήστη
+        // ----------------------------------------------------
+        session_regenerate_id(true);
+
+        $_SESSION['user_id'] = $new_user_id;
+        $_SESSION['username'] = $email;
+        $_SESSION['role_id'] = $role_id;
+        $_SESSION['first_name'] = $first_name;
+        $_SESSION['is_seller'] = 0;
+    }
+
+    // ========================================================
+    // SUCCESS
+    // ========================================================
+    header('Location: index.php');
+    exit;
+}
+
+// ============================================================
+// INITIAL REQUEST - REDIRECT TO GOOGLE
+// ============================================================
+
+else {
+    // Δημιουργία τυχαίου state για προστασία από CSRF
+    $state = bin2hex(random_bytes(32));
+    $_SESSION['google_oauth_state'] = $state;
+    $client->setState($state);
+
+    // Δημιουργία Google authorization URL
+    $authUrl = $client->createAuthUrl();
+
+
+    header('Location: ' . filter_var($authUrl, FILTER_SANITIZE_URL));
+    exit;
+}

+ 62 - 0
includes/mailer.php

@@ -0,0 +1,62 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/includes/mailer.php
+
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception;
+
+function sendTemplateEmail($pdo, $template_key,$to_email, $placeholders,$lang = 'el') {
+    if (empty($to_email)) return false;
+
+    // 1. Φόρτωση SMTP ρυθμίσεων από τη βάση (πίνακας settings)
+    try {
+        $settingsStmt =$pdo->query("SELECT `key`, `value` FROM settings WHERE `key` LIKE 'smtp_%' OR `key` LIKE 'mail_from_%'");
+        $smtp_settings =$settingsStmt->fetchAll(PDO::FETCH_KEY_PAIR);
+    } catch (Exception $e) {$smtp_settings = [];
+    }
+
+    // 2. Φόρτωση Template από τη βάση (πίνακας email_templates)
+    try {
+        $tplStmt =$pdo->prepare("SELECT * FROM email_templates WHERE template_key = ?");
+        $tplStmt->execute([$template_key]);
+        $tpl =$tplStmt->fetch(PDO::FETCH_ASSOC);
+    } catch (Exception $e) {$tpl = false;
+    }
+
+    if (!$tpl) return false;
+
+    $subject = ($lang === 'en') ? $tpl['subject_en'] :$tpl['subject_el'];
+    $body    = ($lang === 'en') ? $tpl['body_en'] :$tpl['body_el'];
+
+    // 3. Αντικατάσταση μεταβλητών (placeholders) π.χ. {order_id}, {customer_name}
+    foreach ($placeholders as $key =>$val) {
+        $subject = str_replace('{' .$key . '}', $val,$subject);
+        $body    = str_replace('{' .$key . '}', $val,$body);
+    }
+
+    // 4. Αποστολή με PHPMailer
+    require_once __DIR__ . '/../vendor/autoload.php';
+    $mail = new PHPMailer(true);
+
+    try {
+        $mail->isSMTP();$mail->Host       = $smtp_settings['smtp_host'] ?? '';$mail->SMTPAuth   = true;
+        $mail->Username   =$smtp_settings['smtp_username'] ?? '';
+        $mail->Password   =$smtp_settings['smtp_password'] ?? '';
+        
+        $encryption =$smtp_settings['smtp_encryption'] ?? 'tls';
+        $mail->SMTPSecure = ($encryption === 'ssl') ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
+        
+        $mail->Port       = (int)($smtp_settings['smtp_port'] ?? 587);$mail->CharSet    = 'UTF-8';
+
+        $from_email =$smtp_settings['mail_from_address'] ?? 'info@hockeycms.com';
+        $from_name  = $smtp_settings['mail_from_name'] ?? 'Hockey CMS';$mail->setFrom($from_email,$from_name);
+        $mail->addAddress($to_email);
+
+        $mail->isHTML(true);
+        $mail->Subject =$subject;
+        $mail->Body    = nl2br($body);
+
+        return $mail->send();
+    } catch (Exception $e) {
+        return false;
+    }
+}

+ 218 - 0
index.php

@@ -0,0 +1,218 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/index.php
+
+if (session_status() === PHP_SESSION_NONE) { session_start(); }
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας (Language Switcher) ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+// --- Εντοπισμός Ενεργού Theme ---
+$active_theme = 'default';
+try {
+    $stmt =$pdo->query("SELECT value FROM settings WHERE `key` = 'active_theme'");
+    $res =$stmt->fetchColumn();
+    if ($res) { $active_theme =$res; }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' .$active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';$theme_dir = __DIR__ . '/themes/default';
+}
+
+// --- Αρχικοποίηση Twig ---
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, ['cache' => false]);
+
+$twig->addGlobal('session',$_SESSION);
+$twig->addGlobal('current_lang',$current_lang);
+
+// --- Φόρτωση Δυναμικών Κειμένων Site ---
+$siteTexts = [];
+try {
+    $text_column = ($current_lang === 'en') ? 'text_value_en' : 'text_value_el';
+    $textsStmt =$pdo->query("SELECT text_key, {$text_column} as text_value FROM site_texts");
+    
+    while ($row = $textsStmt->fetch(PDO::FETCH_ASSOC)) {$siteTexts[$row['text_key']] =$row['text_value'];
+    }
+} catch (Exception $e) {}
+$twig->addGlobal('text',$siteTexts);
+
+// --- Φόρτωση Logo Site ---
+$site_logo = '';
+try {
+    $logoStmt =$pdo->prepare("SELECT value FROM settings WHERE `key` = 'site_logo'");
+    $logoStmt->execute();
+    $site_logo =$logoStmt->fetchColumn();
+} catch (Exception $e) {}
+$twig->addGlobal('site_logo',$site_logo);
+
+try {
+    // --- Φόρτωση Κατηγοριών ---
+    $categoriesRaw =$pdo->query("SELECT * FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll(PDO::FETCH_ASSOC);
+    
+    $categories = [];
+    foreach ($categoriesRaw as $cat) {$cat['display_name'] = ($current_lang === 'en' && !empty($cat['name_en'])) ? $cat['name_en'] :$cat['name_el'];
+        $categories[] =$cat;
+    }
+    
+    // --- Λήψη Παραμέτρων Φίλτρων & ΣΕΛΙΔΟΠΟΙΗΣΗΣ ---
+    $category_id = isset($_GET['category_id']) ? (int)$_GET['category_id'] : 0;
+    $sort = isset($_GET['sort']) ?$_GET['sort'] : 'newest';
+    $min_price = (isset($_GET['min_price']) && is_numeric($_GET['min_price'])) ? (float)$_GET['min_price'] : null;
+    $max_price = (isset($_GET['max_price']) && is_numeric($_GET['max_price'])) ? (float)$_GET['max_price'] : null;
+    $search_query = isset($_GET['q']) ? trim($_GET['q']) : '';
+
+    // Παράμετροι Σελιδοποίησης
+    $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 30;
+    if (!in_array($limit, [30, 50, 100])) { $limit = 30; } // Default & Safety$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
+    if ($page < 1) {$page = 1; }
+
+    $current_category_name = null;
+    $category_attributes = [];
+
+    // --- ΔΙΑΣΠΑΣΗ ΤΟΥ SQL QUERY (Για να μετράμε και το σύνολο) ---
+    $sql_select = "SELECT p.id, p.sku, p.title, p.price, p.created_at, pi.image_path";
+    $sql_from = " FROM products p LEFT JOIN product_images pi ON (p.id = pi.product_id AND pi.is_primary = 1)";
+    $sql_where = " WHERE p.status = 'available'";
+    $params = [];
+
+    // 1. Φίλτρο Κατηγορίας
+    if ($category_id > 0) {
+        $catStmt =$pdo->prepare("SELECT name_el, name_en, attribute_group_id FROM categories WHERE id = ?");
+        $catStmt->execute([$category_id]);
+        $catData =$catStmt->fetch(PDO::FETCH_ASSOC);
+        
+        if ($catData) {
+            $current_category_name = ($current_lang === 'en' && !empty($catData['name_en'])) ?$catData['name_en'] : $catData['name_el'];$sql_where .= " AND (p.category_id = ? OR p.category_id IN (SELECT id FROM categories WHERE parent_id = ?))";
+            $params[] =$category_id;
+            $params[] =$category_id;
+
+            if ($catData['attribute_group_id']) {
+                $attrStmt =$pdo->prepare("
+                    SELECT a.id, a.name as name_el, a.name_en, a.unit, 
+                           (SELECT GROUP_CONCAT(DISTINCT value SEPARATOR '||') FROM product_attribute_values WHERE attribute_id = a.id) as distinct_values
+                    FROM attributes a 
+                    WHERE a.group_id = ?
+                ");
+                $attrStmt->execute([$catData['attribute_group_id']]);
+                $catAttrsRaw =$attrStmt->fetchAll(PDO::FETCH_ASSOC);
+                
+                foreach ($catAttrsRaw as $a) {$a['display_name'] = ($current_lang === 'en' && !empty($a['name_en'])) ? $a['name_en'] :$a['name_el'];
+                    $category_attributes[] =$a;
+                }
+            }
+        }
+    }
+
+    // 2. Έξυπνο Φίλτρο Αναζήτησης (Βελτιωμένο)
+    if ($search_query !== '') {
+        $clean_query = preg_replace('/[^\p{L}\p{N}\s\-]/u', ' ',$search_query);
+        $words = preg_split('/\s+/', mb_strtolower($clean_query, 'UTF-8'));
+        $words = array_filter($words, function($w) { return mb_strlen(trim($w)) > 1; });
+
+        if (!empty($words)) {$whereClauses = [];
+            foreach ($words as $word) {$whereClauses[] = "(LOWER(COALESCE(p.sku, '')) LIKE CONCAT('%', ?, '%') OR LOWER(COALESCE(p.title, '')) LIKE CONCAT('%', ?, '%') OR LOWER(COALESCE(p.description, '')) LIKE CONCAT('%', ?, '%') OR LOWER(COALESCE(p.search_tags, '')) LIKE CONCAT('%', ?, '%'))";
+                array_push($params, $word,$word, $word,$word);
+            }
+            $sql_where .= " AND (" . implode(" AND ", $whereClauses) . ")";
+        }
+    }
+
+    // 3. Φίλτρο Τιμής
+    if ($min_price !== null) {$sql_where .= " AND p.price >= ?"; $params[] =$min_price; }
+    if ($max_price !== null) {$sql_where .= " AND p.price <= ?"; $params[] =$max_price; }
+
+    // 4. Φίλτρο Δυναμικών Χαρακτηριστικών
+    if (isset($_GET['attr']) && is_array($_GET['attr'])) {
+        foreach ($_GET['attr'] as $attr_id =>$attr_val) {
+            if (!empty($attr_val)) {$sql_where .= " AND EXISTS (SELECT 1 FROM product_attribute_values pav WHERE pav.product_id = p.id AND pav.attribute_id = ? AND pav.value = ?)";
+                $params[] = (int)$attr_id;
+                $params[] =$attr_val;
+            }
+        }
+    }
+
+    // --- ΜΕΤΡΗΣΗ ΣΥΝΟΛΙΚΩΝ ΠΡΟΪΟΝΤΩΝ ΓΙΑ ΣΕΛΙΔΟΠΟΙΗΣΗ ---
+    $countSql = "SELECT COUNT(p.id)" . $sql_from .$sql_where;
+    $stmtCount = $pdo->prepare($countSql);
+    $stmtCount->execute($params);
+    $total_products = (int)$stmtCount->fetchColumn();
+
+    $total_pages = ceil($total_products / $limit);
+    if ($page >$total_pages && $total_pages > 0) {$page = $total_pages; }$offset = ($page - 1) *$limit;
+
+    // --- ΤΕΛΙΚΟ QUERY ΠΡΟΪΟΝΤΩΝ ΜΕ ΟΡΙΑ ---
+    $sql =$sql_select . $sql_from .$sql_where;
+
+    // 5. Ταξινόμηση
+    if ($sort === 'price_asc') {$sql .= " ORDER BY p.price ASC"; }
+    elseif ($sort === 'price_desc') {$sql .= " ORDER BY p.price DESC"; }
+    else { $sql .= " ORDER BY p.id DESC"; }
+
+    // 6. Προσθήκη Ορίων (Pagination)
+    $sql .= " LIMIT " . $limit . " OFFSET " . $offset;
+
+    // Εκτέλεση
+    $stmt = $pdo->prepare($sql);
+    $stmt->execute($params);
+    $products =$stmt->fetchAll(PDO::FETCH_ASSOC);
+
+    // --- ΕΝΣΩΜΑΤΩΣΗ ΧΑΡΑΚΤΗΡΙΣΤΙΚΩΝ ΣΤΑ ΠΡΟΪΟΝΤΑ ---
+    if (!empty($products)) {
+        $product_ids = array_column($products, 'id');
+        $inQuery = implode(',', array_fill(0, count($product_ids), '?'));
+        
+        $attrValsStmt =$pdo->prepare("
+            SELECT pav.product_id, a.name as name_el, a.name_en, a.unit, pav.value 
+            FROM product_attribute_values pav
+            JOIN attributes a ON pav.attribute_id = a.id
+            WHERE pav.product_id IN ($inQuery)
+        ");
+        $attrValsStmt->execute($product_ids);
+        $all_attrs =$attrValsStmt->fetchAll(PDO::FETCH_ASSOC);
+
+        $attrs_by_pid = [];
+        foreach ($all_attrs as $attr) {$attr_name = ($current_lang === 'en' && !empty($attr['name_en'])) ? $attr['name_en'] :$attr['name_el'];
+            $attrs_by_pid[$attr['product_id']][] = [
+                'name' => $attr_name,
+                'value' => $attr['value'],
+                'unit' => $attr['unit']
+            ];
+        }
+
+        foreach ($products as &$prod) {$prod['attributes'] = $attrs_by_pid[$prod['id']] ?? [];
+        }
+        unset($prod);
+    }
+
+    // --- ΔΗΜΙΟΥΡΓΙΑ BASE URL ΓΙΑ ΤΑ ΚΟΥΜΠΙΑ ΣΕΛΙΔΟΠΟΙΗΣΗΣ ---
+    $qs =$_GET;
+    unset($qs['page']); // Αφαιρούμε το page για να το βάλει δυναμικά το Twig
+    $base_qs = http_build_query($qs);$pagination_url = 'index.php?' . ($base_qs ? $base_qs . '&' : '') . 'page=';
+
+} catch (Exception $e) {
+    die("Σφάλμα ανάγνωσης δεδομένων: " . $e->getMessage());
+}
+
+echo $twig->render('index.html.twig', [
+    'site_title' => 'Hockey Gear CMS',
+    'categories' => $categories,
+    'products' => $products,
+    'current_category_name' => $current_category_name,
+    'category_id' => $category_id,
+    'category_attributes' => $category_attributes,
+    'search_query' => $search_query,
+    'filters' => $_GET,
+    // Νέες μεταβλητές Σελιδοποίησης
+    'current_page' => $page,
+    'total_pages' => $total_pages,
+    'limit' => $limit,
+    'total_products' => $total_products,
+    'pagination_url' => $pagination_url
+]);

+ 142 - 0
login.php

@@ -0,0 +1,142 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/login.php
+
+if (session_status() === PHP_SESSION_NONE) { session_start(); }
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// Αν ο χρήστης είναι ήδη συνδεδεμένος, τον στέλνουμε ανάλογα με τον ρόλο του
+if (isset($_SESSION['user_id'])) {
+    if (isset($_SESSION['role_id']) && ($_SESSION['role_id'] == 1 || ($_SESSION['is_seller'] ?? 0) == 1)) {
+        header("Location: admin/index.php");
+    } else {
+        header("Location: index.php");
+    }
+    exit;
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+// --- Εύρεση Ενεργού Theme (Χωρίς backticks για αποφυγή σφαλμάτων) ---
+$active_theme = 'default';
+try {
+    $stmt = $pdo->query("SELECT * FROM settings");
+    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+        if (isset($row['key']) && $row['key'] === 'active_theme' && !empty($row['value'])) {
+            $active_theme = trim($row['value']);
+        }
+    }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' . $active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';
+    $theme_dir = __DIR__ . '/themes/default';
+}
+
+// --- Αρχικοποίηση Twig ---
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, ['cache' => false]);
+
+$twig->addGlobal('session', $_SESSION);
+$twig->addGlobal('current_lang', $current_lang);
+
+// --- Φόρτωση Δυναμικών Κειμένων Site ---
+$siteTexts = [];
+try {
+    $text_column = ($current_lang === 'en') ? 'text_value_en' : 'text_value_el';
+    $textsStmt = $pdo->query("SELECT text_key, {$text_column} as text_value FROM site_texts");
+    while ($row = $textsStmt->fetch(PDO::FETCH_ASSOC)) {
+        $siteTexts[$row['text_key']] = $row['text_value'];
+    }
+} catch (Exception $e) {}
+$twig->addGlobal('text', $siteTexts);
+
+// --- Φόρτωση Logo Site ---
+$site_logo = '';
+try {
+    $stmtSettings = $pdo->query("SELECT * FROM settings");
+    while ($row = $stmtSettings->fetch(PDO::FETCH_ASSOC)) {
+        if (isset($row['key']) && $row['key'] === 'site_logo') {
+            $site_logo = $row['value'];
+        }
+    }
+} catch (Exception $e) {}
+$twig->addGlobal('site_logo', $site_logo);
+
+// --- Μεταφράσεις για τη φόρμα σύνδεσης ---
+$translations = [
+    'el' => [
+        'page_title' => 'Σύνδεση - Hockey CMS',
+        'heading' => 'Καλώς ήρθατε',
+        'subheading' => 'Συνδεθείτε στον λογαριασμό σας',
+        'username' => 'Email ή Username',
+        'password' => 'Κωδικός Πρόσβασης',
+        'btn_login' => 'Σύνδεση',
+        'btn_google' => 'Σύνδεση με Google',
+        'register_text' => 'Δεν έχετε λογαριασμό;',
+        'register_link' => 'Εγγραφείτε εδώ',
+        'err_empty' => 'Παρακαλώ συμπληρώστε όλα τα πεδία.',
+        'err_invalid' => 'Λάθος στοιχεία σύνδεσης.'
+    ],
+    'en' => [
+        'page_title' => 'Login - Hockey CMS',
+        'heading' => 'Welcome Back',
+        'subheading' => 'Sign in to your account',
+        'username' => 'Email or Username',
+        'password' => 'Password',
+        'btn_login' => 'Sign In',
+        'btn_google' => 'Sign in with Google',
+        'register_text' => "Don't have an account?",
+        'register_link' => 'Register here',
+        'err_empty' => 'Please fill in all fields.',
+        'err_invalid' => 'Invalid login credentials.'
+    ]
+];
+$t = $translations[$current_lang];
+$error = '';
+
+// --- Διαχείριση POST (Κλασικό Login) ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $username = trim($_POST['username'] ?? '');
+    $password = $_POST['password'] ?? '';
+
+    if (empty($username) || empty($password)) {
+        $error = $t['err_empty'];
+    } else {
+        $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
+        $stmt->execute([$username, $username]);
+        $user = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        if ($user && password_verify($password, $user['password'])) {
+            session_regenerate_id(true);
+            $_SESSION['user_id'] = $user['id'];
+            $_SESSION['username'] = $user['username'];
+            $_SESSION['role_id'] = $user['role_id'];
+            $_SESSION['first_name'] = $user['first_name'] ?? '';
+            $_SESSION['is_seller'] = $user['is_seller'] ?? 0;
+
+            if ($user['role_id'] == 1 || $user['is_seller'] == 1) {
+                header("Location: admin/index.php");
+            } else {
+                header("Location: index.php");
+            }
+            exit;
+        } else {
+            $error = $t['err_invalid'];
+        }
+    }
+}
+
+// Render μέσω Twig από τον φάκελο του ενεργού theme
+echo $twig->render('login.html.twig', [
+    't' => $t,
+    'current_lang' => $current_lang,
+    'error' => $error,
+    'site_title' => 'Hockey Gear CMS'
+]);

+ 5 - 0
logout.php

@@ -0,0 +1,5 @@
+<?php
+session_start();
+session_destroy();
+header("Location: index.php");
+exit;

+ 207 - 0
privacy.php

@@ -0,0 +1,207 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/privacy.php
+session_start();
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+// --- Λεξικό Μεταφράσεων ---
+$translations = [
+    'el' => [
+        'title' => 'Πολιτική Απορρήτου - Hockey CMS',
+        'h1' => 'Πολιτική Προστασίας Προσωπικών Δεδομένων',
+        'last_updated' => 'Τελευταία ενημέρωση: ' . date('d/m/Y'),
+        'p1' => 'Στο <strong>Hockey CMS</strong> σεβόμαστε απόλυτα το απόρρητό σας και δεσμευόμαστε να προστατεύουμε τα προσωπικά σας δεδομένα. Αυτή η πολιτική εξηγεί πώς συλλέγουμε, χρησιμοποιούμε και προστατεύουμε τις πληροφορίες σας όταν χρησιμοποιείτε το ηλεκτρονικό μας κατάστημα.',
+        'h2_1' => '1. Ποια δεδομένα συλλέγουμε',
+        'p2_1' => 'Κατά την εγγραφή σας, την υποβολή φόρμας επικοινωνίας ή την ολοκλήρωση μιας παραγγελίας, συλλέγουμε βασικές πληροφορίες που μας παρέχετε, όπως: <strong>Ονοματεπώνυμο, Διεύθυνση Email, Αριθμό Τηλεφώνου και Ταχυδρομική Διεύθυνση</strong>.',
+        'h2_2' => '2. Πώς χρησιμοποιούμε τα δεδομένα σας',
+        'p2_2' => 'Τα δεδομένα σας χρησιμοποιούνται αποκλειστικά για την εξυπηρέτησή σας και συγκεκριμένα για:<br>
+                   &bull; Την ορθή επεξεργασία και αποστολή των παραγγελιών σας.<br>
+                   &bull; Την αποστολή αυτοματοποιημένων ενημερώσεων (μέσω Email, SMS ή WhatsApp) σχετικά με την εξέλιξη της παραγγελίας σας.<br>
+                   &bull; Την παροχή υποστήριξης πελατών σε περίπτωση προβλήματος.',
+        'h2_3' => '3. Προστασία & Κοινοποίηση Δεδομένων',
+        'p2_3' => 'Τα προσωπικά σας δεδομένα <strong>δεν πωλούνται, δεν ενοικιάζονται και δεν κοινοποιούνται</strong> σε τρίτους για διαφημιστικούς ή εμπορικούς σκοπούς. Μοιράζονται μόνο με απολύτως απαραίτητους συνεργάτες (όπως οι εταιρείες ταχυμεταφορών - courier) αποκλειστικά και μόνο για την παράδοση των προϊόντων σας.',
+        'h2_4' => '4. Τα δικαιώματά σας',
+        'p2_4' => 'Έχετε το δικαίωμα να ζητήσετε πρόσβαση, διόρθωση, τροποποίηση ή οριστική διαγραφή των προσωπικών σας δεδομένων ανά πάσα στιγμή. Μπορείτε να διαχειριστείτε τα στοιχεία σας μέσα από το προφίλ σας ή επικοινωνώντας απευθείας μαζί μας.',
+        'btn_back' => '&larr; Επιστροφή στο Κατάστημα'
+    ],
+    'en' => [
+        'title' => 'Privacy Policy - Hockey CMS',
+        'h1' => 'Privacy Policy & Data Protection',
+        'last_updated' => 'Last updated: ' . date('d/m/Y'),
+        'p1' => 'At <strong>Hockey CMS</strong>, we deeply respect your privacy and are committed to protecting your personal data. This policy explains how we collect, use, and safeguard your information when you use our online store.',
+        'h2_1' => '1. What data we collect',
+        'p2_1' => 'When you register, submit a contact form, or complete an order, we collect basic information you provide to us, such as: <strong>Full Name, Email Address, Phone Number, and Shipping Address</strong>.',
+        'h2_2' => '2. How we use your data',
+        'p2_2' => 'Your data is used exclusively to serve you, specifically for:<br>
+                   &bull; Proper processing and shipping of your orders.<br>
+                   &bull; Sending automated updates (via Email, SMS, or WhatsApp) regarding your order status.<br>
+                   &bull; Providing customer support in case of issues.',
+        'h2_3' => '3. Data Protection & Sharing',
+        'p2_3' => 'Your personal data is <strong>never sold, rented, or shared</strong> with third parties for marketing or commercial purposes. It is only shared with strictly necessary partners (such as courier companies) solely for the purpose of delivering your products.',
+        'h2_4' => '4. Your rights',
+        'p2_4' => 'You have the right to request access to, correction, modification, or permanent deletion of your personal data at any time. You can manage your details through your profile or by contacting us directly.',
+        'btn_back' => '&larr; Back to Store'
+    ]
+];
+
+$t = $translations[$current_lang];
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title><?php echo $t['title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --bg-color: #f4f6f9;
+            --card-bg: #ffffff;
+            --text-main: #1e293b;
+            --text-muted: #64748b;
+            --border-color: #e2e8f0;
+        }
+
+        /* Υποστήριξη Dark Mode ανάλογα με τις ρυθμίσεις του χρήστη (προαιρετικό) */
+        @media (prefers-color-scheme: dark) {
+            :root {
+                --bg-color: #0b1120;
+                --card-bg: #1e293b;
+                --text-main: #f8fafc;
+                --text-muted: #94a3b8;
+                --border-color: #334155;
+            }
+        }
+
+        body {
+            font-family: 'Inter', sans-serif;
+            background-color: var(--bg-color);
+            color: var(--text-main);
+            line-height: 1.6;
+            margin: 0;
+            padding: 40px 20px;
+        }
+
+        .container {
+            max-width: 800px;
+            margin: 0 auto;
+        }
+
+        .card {
+            background-color: var(--card-bg);
+            padding: 40px;
+            border-radius: 12px;
+            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+            border: 1px solid var(--border-color);
+        }
+
+        h1 {
+            color: var(--primary);
+            font-size: 28px;
+            margin-top: 0;
+            margin-bottom: 5px;
+        }
+
+        .last-updated {
+            font-size: 13px;
+            color: var(--text-muted);
+            margin-bottom: 30px;
+            display: block;
+        }
+
+        h2 {
+            font-size: 18px;
+            margin-top: 30px;
+            margin-bottom: 10px;
+            color: var(--text-main);
+            border-bottom: 2px solid var(--border-color);
+            padding-bottom: 5px;
+        }
+
+        p {
+            margin-bottom: 15px;
+            font-size: 15px;
+            color: var(--text-muted);
+        }
+
+        strong {
+            color: var(--text-main);
+        }
+
+        .lang-switch {
+            text-align: right;
+            margin-bottom: 20px;
+        }
+
+        .lang-switch a {
+            text-decoration: none;
+            font-size: 13px;
+            font-weight: 700;
+            padding: 6px 12px;
+            border-radius: 20px;
+            background: var(--card-bg);
+            color: var(--text-muted);
+            border: 1px solid var(--border-color);
+            transition: 0.2s;
+            margin-left: 5px;
+        }
+
+        .lang-switch a.active {
+            background: var(--primary);
+            color: white;
+            border-color: var(--primary);
+        }
+
+        .back-btn {
+            display: inline-block;
+            margin-top: 30px;
+            text-decoration: none;
+            color: var(--primary);
+            font-weight: 600;
+            font-size: 15px;
+            transition: 0.2s;
+        }
+
+        .back-btn:hover {
+            color: #ea580c;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="container">
+        <!-- Κουμπιά εναλλαγής γλώσσας -->
+        <div class="lang-switch">
+            <a href="?lang=el" class="<?php echo $current_lang == 'el' ? 'active' : ''; ?>">GR</a>
+            <a href="?lang=en" class="<?php echo $current_lang == 'en' ? 'active' : ''; ?>">EN</a>
+        </div>
+
+        <div class="card">
+            <h1><?php echo $t['h1']; ?></h1>
+            <span class="last-updated"><?php echo $t['last_updated']; ?></span>
+
+            <p><?php echo $t['p1']; ?></p>
+
+            <h2><?php echo $t['h2_1']; ?></h2>
+            <p><?php echo $t['p2_1']; ?></p>
+
+            <h2><?php echo $t['h2_2']; ?></h2>
+            <p><?php echo $t['p2_2']; ?></p>
+
+            <h2><?php echo $t['h2_3']; ?></h2>
+            <p><?php echo $t['p2_3']; ?></p>
+
+            <h2><?php echo $t['h2_4']; ?></h2>
+            <p><?php echo $t['p2_4']; ?></p>
+
+            <a href="index.php" class="back-btn"><?php echo $t['btn_back']; ?></a>
+        </div>
+    </div>
+
+</body>
+</html>

+ 115 - 0
product.php

@@ -0,0 +1,115 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/product.php
+if (session_status() === PHP_SESSION_NONE) { session_start(); }
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας (Language Switcher) ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+// Δυναμική ανάγνωση ενεργού theme από τη βάση
+$active_theme = 'default';
+try {
+    $stmt =$pdo->query("SELECT value FROM settings WHERE `key` = 'active_theme'");
+    $res =$stmt->fetchColumn();
+    if ($res) { $active_theme =$res; }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' .$active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';$theme_dir = __DIR__ . '/themes/default';
+}
+
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, [
+    'cache' => false,
+]);
+
+// Περνάμε Session και Γλώσσα στο Twig (για το header)
+$twig->addGlobal('session',$_SESSION);
+$twig->addGlobal('current_lang',$current_lang);
+
+// --- Φόρτωση Δυναμικών Κειμένων ανάλογα τη γλώσσα ---
+$siteTexts = [];
+try {
+    $text_column = ($current_lang === 'en') ? 'text_value_en' : 'text_value_el';
+    $textsStmt =$pdo->query("SELECT text_key, {$text_column} as text_value FROM site_texts");
+    while ($row = $textsStmt->fetch(PDO::FETCH_ASSOC)) {$siteTexts[$row['text_key']] =$row['text_value'];
+    }
+} catch (Exception $e) {}
+$twig->addGlobal('text',$siteTexts);
+
+// --- Φόρτωση Logo Site ---
+$site_logo = '';
+try {
+    $logoStmt =$pdo->prepare("SELECT value FROM settings WHERE `key` = 'site_logo'");
+    $logoStmt->execute();
+    $site_logo =$logoStmt->fetchColumn();
+} catch (Exception $e) {}
+$twig->addGlobal('site_logo',$site_logo);
+
+// 1. Παίρνουμε το ID του προϊόντος από το URL
+$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+
+if ($product_id === 0) {
+    die("Δεν επιλέχθηκε προϊόν.");
+}
+
+try {
+    // 2. Φέρνουμε τα στοιχεία του προϊόντος, την κατηγορία (Δίγλωσσα) και τον πωλητή
+    $stmt =$pdo->prepare("
+        SELECT p.*, c.name_el as cat_name_el, c.name_en as cat_name_en, u.username as seller_name, pi.image_path 
+        FROM products p 
+        LEFT JOIN categories c ON p.category_id = c.id 
+        LEFT JOIN users u ON p.seller_id = u.id 
+        LEFT JOIN product_images pi ON (p.id = pi.product_id AND pi.is_primary = 1)
+        WHERE p.id = ?
+    ");
+    $stmt->execute([$product_id]);
+    $product =$stmt->fetch(PDO::FETCH_ASSOC);
+
+    if (!$product) {
+        die("Το προϊόν δεν βρέθηκε.");
+    }
+
+    // Ορισμός της σωστής γλώσσας για το όνομα της κατηγορίας του προϊόντος
+    $product['cat_name'] = ($current_lang === 'en' && !empty($product['cat_name_en'])) ? $product['cat_name_en'] :$product['cat_name_el'];
+
+    // 3. Φέρνουμε τα Δυναμικά Χαρακτηριστικά (Attributes) - Δίγλωσσα
+    $attrStmt =$pdo->prepare("
+        SELECT a.name as name_el, a.name_en, a.unit, pav.value 
+        FROM product_attribute_values pav
+        JOIN attributes a ON pav.attribute_id = a.id
+        WHERE pav.product_id = ?
+        ORDER BY a.name
+    ");
+    $attrStmt->execute([$product_id]);
+    $attributesRaw =$attrStmt->fetchAll(PDO::FETCH_ASSOC);
+    
+    // Μεταφράζουμε τα χαρακτηριστικά δυναμικά
+    $attributes = [];
+    foreach ($attributesRaw as $attr) {$attr['name'] = ($current_lang === 'en' && !empty($attr['name_en'])) ? $attr['name_en'] :$attr['name_el'];
+        $attributes[] =$attr;
+    }
+
+    // 4. Φέρνουμε τις Κατηγορίες για το Sidebar μενού - Δίγλωσσα
+    $categoriesRaw =$pdo->query("SELECT * FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll(PDO::FETCH_ASSOC);
+    $categories = [];
+    foreach ($categoriesRaw as $cat) {$cat['display_name'] = ($current_lang === 'en' && !empty($cat['name_en'])) ? $cat['name_en'] :$cat['name_el'];
+        $categories[] =$cat;
+    }
+
+    // 5. Render μέσω Twig του template
+    echo $twig->render('product.html.twig', [
+        'site_title' => 'Hockey Gear CMS',
+        'categories' => $categories,
+        'product' => $product,
+        'attributes' => $attributes
+    ]);
+
+} catch (Exception $e) {
+    die("Σφάλμα συστήματος: " . $e->getMessage());
+}

+ 98 - 0
profile.php

@@ -0,0 +1,98 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/profile.php
+session_start();
+require_once __DIR__ . '/config/database.php';
+
+// Πρέπει να είναι συνδεδεμένος
+if (!isset($_SESSION['user_id'])) { header("Location: login.php"); exit; }
+
+$user_id = $_SESSION['user_id'];$message = '';
+
+// Διαβάζουμε αν επιτρέπεται στους χρήστες να γίνονται sellers (από το admin)
+$stmt =$pdo->query("SELECT value FROM settings WHERE `key` = 'allow_user_to_seller'");
+$allow_user_to_seller = (int)$stmt->fetchColumn();
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+    $email = trim($_POST['email']);
+    $phone = trim($_POST['phone']);
+    
+    // Αν επιτρέπεται η αλλαγή σε seller, διαβάζουμε το checkbox. Αλλιώς, κρατάμε την τρέχουσα κατάσταση (ή 0)
+    if ($allow_user_to_seller) {
+        $is_seller = isset($_POST['is_seller']) ? 1 : 0;
+    } else {
+        // Αν δεν επιτρέπεται, δεν αφήνουμε την αλλαγή μέσω της φόρμας
+        $is_seller =$_SESSION['is_seller'] ?? 0;
+    }
+    
+    // Ενημέρωση βάσης
+    $stmt =$pdo->prepare("UPDATE users SET email = ?, phone = ?, is_seller = ? WHERE id = ?");
+    $stmt->execute([$email,$phone, $is_seller,$user_id]);
+    
+    // Ενημέρωση του session αμέσως!
+    $_SESSION['is_seller'] =$is_seller;
+    
+    $message = "<div style='background:#d1e7dd; color:#0f5132; padding:10px; border-radius:6px; margin-bottom:20px;'>Το προφίλ σου ενημερώθηκε επιτυχώς!</div>";
+}
+
+// Φέρνουμε τα τρέχοντα στοιχεία
+$stmt =$pdo->prepare("SELECT * FROM users WHERE id = ?");
+$stmt->execute([$user_id]);
+$user =$stmt->fetch(PDO::FETCH_ASSOC);
+?>
+<!DOCTYPE html>
+<html lang="el">
+<head>
+    <meta charset="UTF-8">
+    <title>Το Προφίλ μου</title>
+    <style>
+        body { font-family: 'Segoe UI', sans-serif; background: #0f172a; padding: 40px; color: #f8fafc;}
+        .profile-container { background: #1e293b; padding: 30px; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); max-width: 500px; margin: 0 auto; border: 1px solid #334155; }
+        input[type="text"], input[type="email"] { width: 100%; padding: 12px; margin: 8px 0 20px; border: 1px solid #334155; border-radius: 6px; box-sizing: border-box; background: #0f172a; color: #fff; }
+        input[type="text"]:focus, input[type="email"]:focus { outline: none; border-color: #f97316; }
+        button { padding: 12px 20px; background: #f97316; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; width: 100%; text-transform: uppercase; font-size: 15px;}
+        button:hover { background: #ea580c; }
+        .seller-box { background: rgba(249, 115, 22, 0.1); border: 1px solid rgba(249, 115, 22, 0.2); padding: 15px; border-radius: 6px; margin-bottom: 20px; }
+        label { font-weight: 600; color: #cbd5e1; font-size: 14px;}
+    </style>
+</head>
+<body>
+    <div class="profile-container">
+        <h2 style="color: #f8fafc; margin-top: 0;">👤 Το Προφίλ μου</h2>
+        <?php echo $message; ?>
+        
+        <form method="POST">
+            <label>Όνομα Χρήστη (Δεν αλλάζει)</label>
+            <input type="text" value="<?php echo htmlspecialchars($user['username']); ?>" disabled style="background: rgba(255,255,255,0.05); color: #94a3b8;">
+
+            <label>Email</label>
+            <input type="email" name="email" value="<?php echo htmlspecialchars($user['email'] ?? ''); ?>" required>
+            
+            <label>Τηλέφωνο</label>
+            <input type="text" name="phone" value="<?php echo htmlspecialchars($user['phone'] ?? ''); ?>">
+
+            <?php if ($allow_user_to_seller): ?>
+                <div class="seller-box">
+                    <label style="display:flex; align-items:center; gap:10px; font-weight:bold; cursor:pointer; color: #f97316;">
+                        <input type="checkbox" name="is_seller" value="1" <?php echo $user['is_seller'] ? 'checked' : ''; ?> style="width:20px; height:20px; accent-color: #f97316;">
+                        Θέλω να γίνω Πωλητής (Ανέβασμα Αγγελιών)
+                    </label>
+                    <p style="font-size:12px; color:#94a3b8; margin:8px 0 0 30px;">Ενεργοποίησέ το για να εμφανιστεί το εικονίδιο 'Seller' στο μενού και να μπορείς να πουλάς τον δικό σου εξοπλισμό!</p>
+                </div>
+            <?php else: ?>
+                <!-- Αν είναι ήδη πωλητής από παλιά, ή του το έδωσε ο admin, τον ενημερώνουμε απλά -->
+                <?php if ($user['is_seller']): ?>
+                    <div style="background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2); padding: 15px; border-radius: 6px; margin-bottom: 20px;">
+                        <p style="margin:0; color: #10b981; font-weight: bold;">✔️ Ο λογαριασμός σου έχει δικαιώματα Πωλητή.</p>
+                    </div>
+                <?php endif; ?>
+            <?php endif; ?>
+
+            <button type="submit">Αποθήκευση Αλλαγών</button>
+        </form>
+        
+        <div style="text-align:center; margin-top:20px;">
+            <a href="index.php" style="color: #94a3b8; text-decoration:none;">Επιστροφή στο E-shop</a>
+        </div>
+    </div>
+</body>
+</html>

+ 158 - 0
register.php

@@ -0,0 +1,158 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/register.php
+
+if (session_status() === PHP_SESSION_NONE) { session_start(); }
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// Αν είναι ήδη συνδεδεμένος, πάει αρχική
+if (isset($_SESSION['user_id'])) { 
+    header("Location: index.php"); 
+    exit; 
+}
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+// --- Εύρεση Ενεργού Theme ---
+$active_theme = 'default';
+try {
+    $stmt = $pdo->query("SELECT * FROM settings");
+    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+        if (isset($row['key']) && $row['key'] === 'active_theme' && !empty($row['value'])) {
+            $active_theme = trim($row['value']);
+        }
+    }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' . $active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';
+    $theme_dir = __DIR__ . '/themes/default';
+}
+
+// --- Αρχικοποίηση Twig ---
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, ['cache' => false]);
+
+$twig->addGlobal('session', $_SESSION);
+$twig->addGlobal('current_lang', $current_lang);
+
+// --- Φόρτωση Δυναμικών Κειμένων Site ---
+$siteTexts = [];
+try {
+    $text_column = ($current_lang === 'en') ? 'text_value_en' : 'text_value_el';
+    $textsStmt = $pdo->query("SELECT text_key, {$text_column} as text_value FROM site_texts");
+    while ($row = $textsStmt->fetch(PDO::FETCH_ASSOC)) {
+        $siteTexts[$row['text_key']] = $row['text_value'];
+    }
+} catch (Exception $e) {}
+$twig->addGlobal('text', $siteTexts);
+
+// --- Φόρτωση Logo Site ---
+$site_logo = '';
+try {
+    $stmtSettings = $pdo->query("SELECT * FROM settings");
+    while ($row = $stmtSettings->fetch(PDO::FETCH_ASSOC)) {
+        if (isset($row['key']) && $row['key'] === 'site_logo') {
+            $site_logo = $row['value'];
+        }
+    }
+} catch (Exception $e) {}
+$twig->addGlobal('site_logo', $site_logo);
+
+// --- Μεταφράσεις ---
+$translations = [
+    'el' => [
+        'title' => 'Νέα Εγγραφή',
+        'fname' => 'Όνομα *',
+        'lname' => 'Επίθετο *',
+        'username' => 'Διάλεξε Username *',
+        'email' => 'Το Email σου *',
+        'phone' => 'Τηλέφωνο (Προαιρετικό)',
+        'password' => 'Κωδικός *',
+        'btn_register' => 'Δημιουργία Λογαριασμού',
+        'btn_google' => 'Εγγραφή με Google',
+        'already_have' => 'Έχετε ήδη λογαριασμό;',
+        'login' => 'Σύνδεση',
+        'err_exists' => 'Το Όνομα Χρήστη ή το Email υπάρχει ήδη!',
+        'err_general' => 'Σφάλμα κατά την εγγραφή. Δοκιμάστε ξανά.',
+        'err_req_fields' => 'Παρακαλώ συμπληρώστε όλα τα υποχρεωτικά πεδία!',
+        'err_captcha' => 'Λάθος αποτέλεσμα στην επαλήθευση (CAPTCHA)!',
+        'success' => 'Η εγγραφή ολοκληρώθηκε! Μπορείτε να συνδεθείτε.',
+        'captcha_label' => 'Επαλήθευση: Πόσο κάνει ',
+        'captcha_ph' => 'Αποτέλεσμα...'
+    ],
+    'en' => [
+        'title' => 'Create Account',
+        'fname' => 'First Name *',
+        'lname' => 'Last Name *',
+        'username' => 'Choose Username *',
+        'email' => 'Email Address *',
+        'phone' => 'Phone (Optional)',
+        'password' => 'Password *',
+        'btn_register' => 'Register',
+        'btn_google' => 'Sign up with Google',
+        'already_have' => 'Already have an account?',
+        'login' => 'Login',
+        'err_exists' => 'Username or Email already exists!',
+        'err_general' => 'Registration error. Please try again.',
+        'err_req_fields' => 'Please fill in all required fields!',
+        'err_captcha' => 'Incorrect verification result (CAPTCHA)!',
+        'success' => 'Registration complete! You can now login.',
+        'captcha_label' => 'Verification: How much is ',
+        'captcha_ph' => 'Result...'
+    ]
+];
+$t = $translations[$current_lang];
+$message = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $first_name = trim($_POST['reg_first_name'] ?? '');
+    $last_name  = trim($_POST['reg_last_name'] ?? '');
+    $username   = trim($_POST['reg_username'] ?? '');
+    $email      = trim($_POST['reg_email'] ?? '');
+    $phone      = trim($_POST['reg_phone'] ?? '');
+    $password   = $_POST['reg_password'] ?? '';
+    $captcha_answer = isset($_POST['reg_captcha']) ? (int)$_POST['reg_captcha'] : -1;
+    $expected_captcha = $_SESSION['captcha_result'] ?? -2;
+    
+    if (empty($first_name) || empty($last_name) || empty($username) || empty($email) || empty($password)) {
+        $message = "<div style='padding: 12px; border-radius: 6px; margin-bottom: 15px; font-size: 13px; text-align: center; font-weight: 600; background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2);'>" . $t['err_req_fields'] . "</div>";
+    } elseif ($captcha_answer !== $expected_captcha) {
+        $message = "<div style='padding: 12px; border-radius: 6px; margin-bottom: 15px; font-size: 13px; text-align: center; font-weight: 600; background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2);'>" . $t['err_captcha'] . "</div>";
+    } else {
+        $check = $pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
+        $check->execute([$username, $email]);
+        
+        if ($check->fetch()) {
+            $message = "<div style='padding: 12px; border-radius: 6px; margin-bottom: 15px; font-size: 13px; text-align: center; font-weight: 600; background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2);'>" . $t['err_exists'] . "</div>";
+        } else {
+            $hashed = password_hash($password, PASSWORD_DEFAULT);
+            $stmt = $pdo->prepare("INSERT INTO users (first_name, last_name, username, email, phone, password, role_id, is_seller) VALUES (?, ?, ?, ?, ?, ?, 3, 0)");
+            
+            if ($stmt->execute([$first_name, $last_name, $username, $email, $phone, $hashed])) {
+                $message = "<div style='padding: 12px; border-radius: 6px; margin-bottom: 15px; font-size: 13px; text-align: center; font-weight: 600; background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2);'>" . $t['success'] . "</div>";
+            } else {
+                $message = "<div style='padding: 12px; border-radius: 6px; margin-bottom: 15px; font-size: 13px; text-align: center; font-weight: 600; background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2);'>" . $t['err_general'] . "</div>";
+            }
+        }
+    }
+}
+
+// CAPTCHA
+$num1 = rand(1, 10);
+$num2 = rand(1, 10);
+$_SESSION['captcha_result'] = $num1 + $num2;
+$captcha_text = $num1 . " + " . $num2;
+
+echo $twig->render('register.html.twig', [
+    't' => $t,
+    'current_lang' => $current_lang,
+    'message' => $message,
+    'captcha_text' => $captcha_text
+]);

+ 119 - 0
search.php

@@ -0,0 +1,119 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/search.php
+if (session_status() === PHP_SESSION_NONE) { session_start(); }
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+// --- Φόρτωση Settings (Theme, Logo, Title) ---
+$active_theme = 'default';
+$site_logo = '';$site_title = 'Hockey CMS';
+
+try {
+    $stmtSettings =$pdo->query("SELECT `key`, value FROM settings WHERE `key` IN ('active_theme', 'site_logo', 'site_title')");
+    while ($row =$stmtSettings->fetch(PDO::FETCH_ASSOC)) {
+        if ($row['key'] === 'active_theme' && !empty($row['value'])) {
+            $active_theme = trim($row['value']);
+        } elseif ($row['key'] === 'site_logo') {
+            $site_logo =$row['value'];
+        } elseif ($row['key'] === 'site_title' && !empty($row['value'])) {
+            $site_title = trim($row['value']);
+        }
+    }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' .$active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';$theme_dir = __DIR__ . '/themes/default';
+}
+
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, [
+    'cache' => false,
+]);
+
+$twig->addGlobal('session',$_SESSION);
+$twig->addGlobal('current_lang',$current_lang);
+$twig->addGlobal('site_logo',$site_logo);
+
+// --- Φόρτωση Δυναμικών Κειμένων Site ---
+$siteTexts = [];
+try {
+    $text_column = ($current_lang === 'en') ? 'text_value_en' : 'text_value_el';
+    $textsStmt =$pdo->query("SELECT text_key, {$text_column} as text_value FROM site_texts");
+    while ($row = $textsStmt->fetch(PDO::FETCH_ASSOC)) {$siteTexts[$row['text_key']] =$row['text_value'];
+    }
+} catch (Exception $e) {}
+$twig->addGlobal('text',$siteTexts);
+
+// 1. Παίρνουμε τον όρο αναζήτησης
+$search_query = isset($_GET['q']) ? trim($_GET['q']) : '';
+
+try {
+    // 2. Φόρτωση κατηγοριών για το sidebar
+    $categories = [];
+    try {
+        $categoriesRaw =$pdo->query("SELECT id, parent_id, name_el, name_en, sort_order FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll(PDO::FETCH_ASSOC);
+        foreach ($categoriesRaw as $cat) {$cat['display_name'] = ($current_lang === 'en' && !empty($cat['name_en'])) ? $cat['name_en'] :$cat['name_el'];
+            $categories[] =$cat;
+        }
+    } catch (Exception $e) {}
+
+    $products = [];
+    if (!empty($search_query)) {
+        $words = preg_split('/\s+/', mb_strtolower($search_query, 'UTF-8'));
+        $words = array_filter($words, function($w) { return mb_strlen(trim($w)) > 0; });
+
+        if (!empty($words)) {
+            $whereClauses = [];$params = [];
+
+            foreach ($words as $word) {$like = '%' . $word . '\%';$whereClauses[] = "(LOWER(p.title) LIKE ? OR LOWER(p.description) LIKE ? OR LOWER(p.search_tags) LIKE ?)";
+                array_push($params,$like, $like,$like);
+            }
+
+            $searchCondition = "(" . implode(" OR ", $whereClauses) . ")";
+
+            $sql = "
+                SELECT p.id, p.title, p.price, p.created_at, pi.image_path 
+                FROM products p 
+                LEFT JOIN product_images pi ON (p.id = pi.product_id AND pi.is_primary = 1)
+                WHERE p.status = 'available' AND " . $searchCondition . "
+                ORDER BY p.id DESC
+            ";
+
+            // --- DEBUG SECTION ---
+            echo "<div style='background:#111; color:#0f0; padding:15px; font-family:monospace; z-index:99999; position:relative;'>";
+            echo "<strong>DEBUG INFO:</strong><br>";
+            echo "Search Query: " . htmlspecialchars($search_query) . "<br>";
+            echo "SQL: " . htmlspecialchars($sql) . "<br>";
+            echo "Params: "; print_r($params);
+            echo "</div>";
+            // ---------------------
+
+            $stmt = $pdo->prepare($sql);
+            $stmt->execute($params);
+            $products =$stmt->fetchAll(PDO::FETCH_ASSOC);
+
+            echo "<div style='background:#111; color:#ff0; padding:15px; font-family:monospace; z-index:99999; position:relative;'>";
+            echo "Products Found Count: " . count($products) . "<br>";
+            echo "</div>";
+        }
+    }
+
+    // 4. Αποστολή δεδομένων στο Twig
+    echo $twig->render('index.html.twig', [
+        'site_title' => $site_title,
+        'categories' => $categories,
+        'products' => $products,
+        'search_query' => $search_query
+    ]);
+
+} catch (Exception $e) {
+    die("Σφάλμα αναζήτησης: " . $e->getMessage());
+}

+ 334 - 0
seller_dashboard.php

@@ -0,0 +1,334 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/seller_dashboard.php
+session_start();
+
+if (!isset($_SESSION['user_id']) or empty($_SESSION['is_seller'])) {
+    header("Location: index.php");
+    exit;
+}
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) and in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+// --- Λεξικό Μεταφράσεων ---
+$translations = [
+    'el' => [
+        'seller_panel' => 'Πίνακας Πωλητή',
+        'my_listings' => 'Οι Αγγελίες Μου',
+        'customer_orders' => 'Παραγγελίες Πελατών',
+        'edit_listing' => 'Επεξεργασία Αγγελίας',
+        'add_listing' => 'Ανέβασμα Αγγελίας',
+        'title' => 'Τίτλος Προϊόντος *',
+        'category' => 'Κατηγορία *',
+        'select_cat' => '-- Επίλεξε Κατηγορία --',
+        'sku' => 'Κωδικός (SKU)',
+        'price' => 'Τιμή (€) *',
+        'stock' => 'Απόθεμα',
+        'tags' => 'Ετικέτες Αναζήτησης (Tags)',
+        'tags_placeholder' => 'Γράψτε λέξη και πατήστε Enter...',
+        'description' => 'Περιγραφή',
+        'listing_status' => 'Κατάσταση Αγγελίας',
+        'status_opt_available' => 'Ενεργή (Εμφανίζεται)',
+        'status_opt_hidden' => 'Προσωρινή Απόκρυψη',
+        'status_opt_sold' => 'Πουλήθηκε',
+        'status_active' => 'Ενεργή', 
+        'status_hidden' => 'Κρυφή',   
+        'status_sold' => 'Πουλήθηκε', 
+        'photo' => 'Φωτογραφία',
+        'photo_req' => '(Απαραίτητη)',
+        'photo_opt' => '(Αφήστε κενό αν δεν αλλάξει)',
+        'btn_save' => 'Αποθήκευση',
+        'btn_upload' => 'Ανέβασμα Προϊόντος',
+        'btn_cancel' => 'Ακύρωση',
+        'stock_short' => 'Αποθ:',
+        'no_img' => 'No IMG',
+        'confirm_delete' => 'Σίγουρα θέλετε να διαγράψετε την αγγελία;',
+        'no_listings' => 'Δεν έχεις ανεβάσει ακόμα καμία αγγελία.',
+        'use_form' => 'Χρησιμοποίησε τη φόρμα για να ξεκινήσεις!',
+        'features' => 'Χαρακτηριστικά',
+        'features_placeholder' => 'π.χ. τιμή...'
+    ],
+    'en' => [
+        'seller_panel' => 'Seller Dashboard',
+        'my_listings' => 'My Listings',
+        'customer_orders' => 'Customer Orders',
+        'edit_listing' => 'Edit Listing',
+        'add_listing' => 'Upload Listing',
+        'title' => 'Product Title *',
+        'category' => 'Category *',
+        'select_cat' => '-- Select Category --',
+        'sku' => 'SKU',
+        'price' => 'Price (€) *',
+        'stock' => 'Stock',
+        'tags' => 'Search Tags',
+        'tags_placeholder' => 'Type a word and press Enter...',
+        'description' => 'Description',
+        'listing_status' => 'Listing Status',
+        'status_opt_available' => 'Active (Visible)',
+        'status_opt_hidden' => 'Temporarily Hidden',
+        'status_opt_sold' => 'Sold',
+        'status_active' => 'Active', 
+        'status_hidden' => 'Hidden', 
+        'status_sold' => 'Sold',     
+        'photo' => 'Photo',
+        'photo_req' => '(Required)',
+        'photo_opt' => '(Leave blank if unchanged)',
+        'btn_save' => 'Save Changes',
+        'btn_upload' => 'Upload Product',
+        'btn_cancel' => 'Cancel',
+        'stock_short' => 'Qty:',
+        'no_img' => 'No IMG',
+        'confirm_delete' => 'Are you sure you want to delete this listing?',
+        'no_listings' => 'You haven\'t uploaded any listings yet.',
+        'use_form' => 'Use the form to get started!',
+        'features' => 'Features',
+        'features_placeholder' => 'e.g. value...'
+    ]
+];
+$t =$translations[$current_lang];$seller_id = $_SESSION['user_id'];$message = '';
+
+// AJAX GET ATTRIBUTES
+if (isset($_GET['ajax_get_attributes']) and isset($_GET['category_id'])) {$cat_id = (int)$_GET['category_id'];$product_id = isset($_GET['product_id']) ? (int)$_GET['product_id'] : 0;
+    
+    $stmt =$pdo->prepare("SELECT attribute_group_id FROM categories WHERE id = ?");
+    $stmt->execute([$cat_id]);
+    $cat =$stmt->fetch();
+    
+    if ($cat &&$cat['attribute_group_id']) {
+        $attrStmt =$pdo->prepare("SELECT id, name, unit FROM attributes WHERE group_id = ? ORDER BY name");
+        $attrStmt->execute([$cat['attribute_group_id']]);$attributes = $attrStmt->fetchAll();$existing_values = [];
+        if ($product_id > 0) {
+            $valStmt =$pdo->prepare("SELECT attribute_id, value FROM product_attribute_values WHERE product_id = ?");
+            $valStmt->execute([$product_id]);
+            while ($vRow = $valStmt->fetch(PDO::FETCH_ASSOC)) {$existing_values[$vRow['attribute_id']] =$vRow['value'];
+            }
+        }
+        
+        if ($attributes) {
+            echo "<h4 style='margin-bottom: 12px; margin-top: 20px; color: var(--primary); text-transform: uppercase; letter-spacing: 1px; font-size: 14px;'>".$t['features']."</h4>";
+            echo "<div class='form-cols' style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px;'>";
+            foreach ($attributes as $attr) {$val = isset($existing_values[$attr['id']]) ? htmlspecialchars($existing_values[$attr['id']]) : '';
+                echo "<div class='form-group'>";
+                echo "<label>" . htmlspecialchars($attr['name']) . ($attr['unit'] ? " ({$attr['unit']})" : "") . "</label>";
+                echo "<input type='text' name='attributes[" . $attr['id'] . "]' value='" . $val . "' placeholder='".$t['features_placeholder']."'>";
+                echo "</div>";
+            }
+            echo "</div>";
+        }
+    }
+    exit;
+}
+
+// --- 2. AJAX API: Δημιουργία προτεινόμενου SKU (Πρόθεμα 3 Γράμματα + Αύξων) ---
+if (isset($_GET['ajax_generate_sku']) && isset($_GET['category_id'])) {
+    $cat_id = (int)$_GET['category_id'];
+    $stmt =$pdo->prepare("SELECT name_en FROM categories WHERE id = ?");
+    $stmt->execute([$cat_id]);
+    $name_en =$stmt->fetchColumn();
+
+    if ($name_en) {
+        // Κρατάμε μόνο τα γράμματα από τα Αγγλικά
+        $clean_name = preg_replace('/[^a-zA-Z]/', '',$name_en);
+        $prefix = strtoupper(substr($clean_name, 0, 3));
+        if (empty($prefix)) {$prefix = 'CAT'; }
+
+        // Βρίσκουμε το μεγαλύτερο νούμερο με αυτό το πρόθεμα που ανήκει στον seller (ή γενικά)
+        // Θα το κάνουμε γενικά (όπως στο Admin) για να είναι το SKU απολύτως μοναδικό σε όλο το σύστημα
+        $stmtSku =$pdo->prepare("SELECT sku FROM products WHERE sku LIKE ?");
+        $stmtSku->execute([$prefix . '-%']);
+        $skus =$stmtSku->fetchAll(PDO::FETCH_COLUMN);
+
+        $max = 0;
+        foreach ($skus as$s) {
+            $parts = explode('-',$s);
+            if (count($parts) > 1) {
+                $num = (int)end($parts);
+                if ($num >$max) { $max =$num; }
+            }
+        }
+        $next_num = str_pad($max + 1, 3, '0', STR_PAD_LEFT);
+        echo $prefix . '-' .$next_num;
+    }
+    exit;
+}
+
+// Βοηθητική Resize
+function resizeAndSaveImage($source_file, $target_file,$max_resolution = 800) {
+    $info = getimagesize($source_file);
+    if (!$info) return false;
+    $mime =$info['mime'];
+    switch ($mime) {
+        case 'image/jpeg': $image = imagecreatefromjpeg($source_file); break;
+        case 'image/png':  $image = imagecreatefrompng($source_file); break;
+        case 'image/gif':  $image = imagecreatefromgif($source_file); break;
+        default: return false;
+    }
+    $original_width = imagesx($image);
+    $original_height = imagesy($image);
+    $ratio =$max_resolution / max($original_width,$original_height);
+    $new_width =$original_width > $max_resolution ? round($original_width * $ratio) :$original_width;
+    $new_height =$original_height > $max_resolution ? round($original_height * $ratio) :$original_height;
+    $new_image = imagecreatetruecolor($new_width,$new_height);
+    if ($mime == 'image/png') {
+        imagealphablending($new_image, false);
+        imagesavealpha($new_image, true);
+    }
+    imagecopyresampled($new_image,$image, 0, 0, 0, 0, $new_width,$new_height, $original_width,$original_height);
+    imagejpeg($new_image,$target_file, 85);
+    imagedestroy($image);
+    imagedestroy($new_image);
+    return true;
+}
+
+// POST Actions
+if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['action'])) {
+    $action =$_POST['action'];
+    
+    if ($action == 'delete_product') {
+        $product_id = (int)$_POST['product_id'];
+        try {
+            $delStmt =$pdo->prepare("DELETE FROM products WHERE id = ? AND seller_id = ?");
+            $delStmt->execute([$product_id,$seller_id]);
+            if ($delStmt->rowCount() > 0) {$message = "<div class='alert success'>Η αγγελία διαγράφηκε επιτυχώς!</div>";
+            } else {
+                $message = "<div class='alert error'>Αδυναμία διαγραφής.</div>";
+            }
+        } catch (Exception $e) {$message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+        }
+    } 
+    else {
+        $title = trim($_POST['title']);
+        $sku = trim($_POST['sku']); 
+        $category_id = (int)$_POST['category_id'];
+        $price = (float)$_POST['price'];
+        $quantity = (int)$_POST['quantity'];
+        $status =$_POST['status'] ?? 'available';
+        $description = trim($_POST['description']);
+        $search_tags = trim($_POST['search_tags']);
+
+        if (!empty($title) and !empty($category_id) and !empty($price)) {
+            try {
+                $pdo->beginTransaction();
+                $stmtGrp =$pdo->prepare("SELECT attribute_group_id FROM categories WHERE id = ?");
+                $stmtGrp->execute([$category_id]);
+                $group_id =$stmtGrp->fetchColumn();
+
+                if ($action == 'add_product') {
+                    $stmt =$pdo->prepare("INSERT INTO products (seller_id, category_id, attribute_group_id, title, sku, description, search_tags, price, quantity, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+                    $stmt->execute([$seller_id, $category_id,$group_id, $title,$sku, $description,$search_tags, $price,$quantity, $status]);$product_id = $pdo->lastInsertId();$message = "<div class='alert success'>Η αγγελία ανέβηκε επιτυχώς!</div>";
+                } elseif ($action == 'edit_product') {
+                    $product_id = (int)$_POST['product_id'];
+                    $stmt =$pdo->prepare("UPDATE products SET category_id = ?, attribute_group_id = ?, title = ?, sku = ?, description = ?, search_tags = ?, price = ?, quantity = ?, status = ? WHERE id = ? AND seller_id = ?");
+                    $stmt->execute([$category_id, $group_id,$title, $sku,$description, $search_tags,$price, $quantity,$status, $product_id,$seller_id]);
+                    
+                    $delAttr =$pdo->prepare("DELETE FROM product_attribute_values WHERE product_id = ?");
+                    $delAttr->execute([$product_id]);$message = "<div class='alert success'>Η αγγελία ενημερώθηκε επιτυχώς!</div>";
+                }
+
+                if (isset($_POST['attributes']) and is_array($_POST['attributes'])) {
+                    $insAttr =$pdo->prepare("INSERT INTO product_attribute_values (product_id, attribute_id, value) VALUES (?, ?, ?)");
+                    foreach ($_POST['attributes'] as $attr_id =>$val) {
+                        $val = trim($val);
+                        if ($val !== '') {
+                            $insAttr->execute([$product_id, $attr_id,$val]);
+                        }
+                    }
+                }
+
+                if (isset($_FILES['image']) and $_FILES['image']['error'] == 0) {$upload_dir = __DIR__ . '/uploads/equipment/';
+                    if (!is_dir($upload_dir)) { mkdir($upload_dir, 0755, true); }
+                    
+                    $filename = 'prod_' .$product_id . '_' . time() . '.jpg';
+                    $target_path = $upload_dir .$filename;
+                    
+                    if (resizeAndSaveImage($_FILES['image']['tmp_name'],$target_path)) {
+                        $checkImg =$pdo->prepare("SELECT id FROM product_images WHERE product_id = ? AND is_primary = 1");
+                        $checkImg->execute([$product_id]);
+                        if ($checkImg->fetch()) {
+                            $updImg =$pdo->prepare("UPDATE product_images SET image_path = ? WHERE product_id = ? AND is_primary = 1");
+                            $updImg->execute(['uploads/equipment/' . $filename,$product_id]);
+                        } else {
+                            $insImg =$pdo->prepare("INSERT INTO product_images (product_id, image_path, is_primary) VALUES (?, ?, 1)");
+                            $insImg->execute([$product_id, 'uploads/equipment/' .$filename]);
+                        }
+                    }
+                }
+                $pdo->commit();
+            } catch (Exception $e) {
+                $pdo->rollBack();$message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
+            }
+        } else {
+            $message = "<div class='alert error'>Συμπληρώστε τα υποχρεωτικά πεδία!</div>";
+        }
+    }
+}
+
+// Ανάκτηση δεδομένων για το View
+$edit_product = null;
+if (isset($_GET['edit'])) {
+    $edit_id = (int)$_GET['edit'];
+    $editStmt =$pdo->prepare("SELECT * FROM products WHERE id = ? AND seller_id = ?");
+    $editStmt->execute([$edit_id,$seller_id]);
+    $edit_product =$editStmt->fetch(PDO::FETCH_ASSOC);
+}
+
+$categories =$pdo->query("SELECT id, parent_id, IFNULL(NULLIF(name_en, ''), name_el) as display_name FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll();
+if ($current_lang === 'el') {
+    $categories =$pdo->query("SELECT id, parent_id, name_el as display_name FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll();
+}
+
+$stmtProds =$pdo->prepare("
+    SELECT p.id, p.sku, p.title, p.search_tags, p.price, p.quantity, p.status, 
+           IFNULL(NULLIF(c.name_en, ''), c.name_el) as cat_name_en, c.name_el as cat_name_el, 
+           pi.image_path 
+    FROM products p 
+    LEFT JOIN categories c ON p.category_id = c.id 
+    LEFT JOIN product_images pi ON (p.id = pi.product_id AND pi.is_primary = 1)
+    WHERE p.seller_id = ?
+    ORDER BY p.id DESC
+");
+$stmtProds->execute([$seller_id]);
+$my_products =$stmtProds->fetchAll(PDO::FETCH_ASSOC);
+
+// --- ΦΟΡΤΩΣΗ TWIG ---
+$active_theme = 'default';
+$site_logo = '';$site_title = 'Hockey CMS';
+
+try {
+    $stmtSettings =$pdo->query("SELECT `key`, value FROM settings WHERE `key` IN ('active_theme', 'site_logo', 'site_title')");
+    while ($row =$stmtSettings->fetch(PDO::FETCH_ASSOC)) {
+        if ($row['key'] === 'active_theme' && !empty($row['value'])) {
+            $active_theme =$row['value'];
+        } elseif ($row['key'] === 'site_logo') {
+            $site_logo =$row['value'];
+        } elseif ($row['key'] === 'site_title' && !empty($row['value'])) {
+            $site_title =$row['value'];
+        }
+    }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' .$active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';$theme_dir = __DIR__ . '/themes/default';
+}
+
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, ['cache' => false]);
+$twig->addGlobal('session',$_SESSION);
+$twig->addGlobal('current_lang',$current_lang);
+$twig->addGlobal('site_logo',$site_logo);
+
+echo $twig->render('seller_dashboard.html.twig', [
+    'site_title' => $site_title,
+    't' => $t,
+    'message' => $message,
+    'edit_product' => $edit_product,
+    'categories' => $categories,
+    'my_products' => $my_products
+]);

+ 175 - 0
seller_orders.php

@@ -0,0 +1,175 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/seller_orders.php
+session_start();
+
+if (!isset($_SESSION['user_id']) or empty($_SESSION['is_seller'])) {
+    header("Location: index.php");
+    exit;
+}
+
+require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/config/database.php';
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) and in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] =$_GET['lang'];
+}
+$current_lang =$_SESSION['lang'] ?? 'el';
+
+// --- Λεξικό Μεταφράσεων ---
+$translations = [
+    'el' => [
+        'seller_panel' => 'Πίνακας Πωλητή',
+        'my_listings' => 'Οι Αγγελίες Μου',
+        'customer_orders' => 'Παραγγελίες Πελατών',
+        'order_id' => 'Κωδ. Παραγγελίας',
+        'customer' => 'Πελάτης',
+        'contact' => 'Επικοινωνία / Αποστολή',
+        'date' => 'Ημερομηνία',
+        'my_products' => 'Τα Προϊόντα μου στην παραγγελία',
+        'status' => 'Κατάσταση',
+        'no_orders' => 'Δεν υπάρχουν ακόμα παραγγελίες για τα προϊόντα σου.',
+        'status_pending' => 'Εκκρεμεί',
+        'status_completed' => 'Ολοκληρώθηκε',
+        'status_cancelled' => 'Ακυρώθηκε',
+        'btn_update' => 'Ενημέρωση',
+        'btn_delete' => 'Διαγραφή',
+        'confirm_delete' => 'Σίγουρα θέλετε να διαγράψετε αυτή την παραγγελία;',
+        'msg_status_ok' => 'Η κατάσταση της παραγγελίας ενημερώθηκε!',
+        'msg_delete_ok' => 'Η παραγγελία διαγράφηκε επιτυχώς!',
+        'error' => 'Σφάλμα:'
+    ],
+    'en' => [
+        'seller_panel' => 'Seller Dashboard',
+        'my_listings' => 'My Listings',
+        'customer_orders' => 'Customer Orders',
+        'order_id' => 'Order ID',
+        'customer' => 'Customer',
+        'contact' => 'Contact / Shipping',
+        'date' => 'Date',
+        'my_products' => 'My Products in this order',
+        'status' => 'Status',
+        'no_orders' => 'No orders found for your products yet.',
+        'status_pending' => 'Pending',
+        'status_completed' => 'Completed',
+        'status_cancelled' => 'Cancelled',
+        'btn_update' => 'Update',
+        'btn_delete' => 'Delete',
+        'confirm_delete' => 'Are you sure you want to delete this order?',
+        'msg_status_ok' => 'Order status updated successfully!',
+        'msg_delete_ok' => 'Order deleted successfully!',
+        'error' => 'Error:'
+    ]
+];
+$t =$translations[$current_lang];$seller_id = $_SESSION['user_id'];$message = '';
+
+// --- Χειρισμός Φόρμας (Αλλαγή Status ή Διαγραφή) ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    if (isset($_POST['action']) &&$_POST['action'] === 'update_status') {
+        $order_id = (int)$_POST['order_id'];
+        $new_status =$_POST['status'];
+        
+        try {
+            $stmt =$pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
+            $stmt->execute([$new_status, $order_id]);$message = "<div class='alert success'>{$t['msg_status_ok']}</div>";
+        } catch (Exception $e) {$message = "<div class='alert error'>{$t['error']} " . $e->getMessage() . "</div>";
+        }
+    } 
+    elseif (isset($_POST['action']) &&$_POST['action'] === 'delete_order') {
+        $order_id = (int)$_POST['order_id'];
+        
+        try {
+            // Διαγράφουμε πρώτα τα είδη από το order_items και μετά την παραγγελία
+            $pdo->beginTransaction();
+            $pdo->prepare("DELETE FROM order_items WHERE order_id = ?")->execute([$order_id]);
+            $pdo->prepare("DELETE FROM orders WHERE id = ?")->execute([$order_id]);
+            $pdo->commit();$message = "<div class='alert success'>{$t['msg_delete_ok']}</div>";
+        } catch (Exception $e) {
+            $pdo->rollBack();$message = "<div class='alert error'>{$t['error']} " . $e->getMessage() . "</div>";
+        }
+    }
+}
+
+// --- Ανάκτηση Κατηγοριών (ΓΙΑ ΤΟ ΜΕΝΟΥ / SIDEBAR) ---
+try {
+    $categoriesRaw =$pdo->query("SELECT * FROM categories ORDER BY sort_order ASC, name_el ASC")->fetchAll(PDO::FETCH_ASSOC);
+    $categories = [];
+    foreach ($categoriesRaw as $cat) {$cat['display_name'] = ($current_lang === 'en' && !empty($cat['name_en'])) ? $cat['name_en'] :$cat['name_el'];
+        $categories[] =$cat;
+    }
+} catch (Exception $e) {$categories = [];
+}
+
+// Ανάκτηση Παραγγελιών που περιέχουν προϊόντα αυτού του Πωλητή
+$stmtOrders =$pdo->prepare("
+    SELECT o.id as order_id, o.customer_name, o.customer_email, o.customer_phone, o.shipping_address, o.status as order_status, o.created_at,
+           oi.quantity, oi.price, p.title as product_title, p.id as product_id
+    FROM orders o
+    JOIN order_items oi ON o.id = oi.order_id
+    JOIN products p ON oi.product_id = p.id
+    WHERE p.seller_id = ?
+    ORDER BY o.created_at DESC
+");
+$stmtOrders->execute([$seller_id]);
+$raw_orders =$stmtOrders->fetchAll(PDO::FETCH_ASSOC);
+
+// Ομαδοποίηση παραγγελιών ανά order_id
+$orders = [];
+foreach ($raw_orders as$row) {
+    $oid =$row['order_id'];
+    if (!isset($orders[$oid])) {
+        $orders[$oid] = [
+            'id' => $oid,
+            'customer_name' => $row['customer_name'],
+            'customer_email' => $row['customer_email'],
+            'customer_phone' => $row['customer_phone'],
+            'shipping_address' => $row['shipping_address'],
+            'status' => $row['order_status'],
+            'created_at' => $row['created_at'],
+            'items' => []
+        ];
+    }
+    $orders[$oid]['items'][] = [
+        'product_id' => $row['product_id'],
+        'title' => $row['product_title'],
+        'quantity' => $row['quantity'],
+        'price' => $row['price']
+    ];
+}
+
+// --- ΦΟΡΤΩΣΗ TWIG THEME ---
+// --- ΦΟΡΤΩΣΗ SETTINGS (Theme, Logo, Title) ---
+$active_theme = 'default';
+$site_logo = '';$site_title = 'Hockey CMS';
+
+try {
+    $stmtSettings =$pdo->query("SELECT `key`, value FROM settings WHERE `key` IN ('active_theme', 'site_logo', 'site_title')");
+    while ($row =$stmtSettings->fetch(PDO::FETCH_ASSOC)) {
+        if ($row['key'] === 'active_theme' && !empty($row['value'])) {
+            $active_theme =$row['value'];
+        } elseif ($row['key'] === 'site_logo') {
+            $site_logo =$row['value'];
+        } elseif ($row['key'] === 'site_title' && !empty($row['value'])) {
+            $site_title =$row['value'];
+        }
+    }
+} catch (Exception $e) {}
+
+$theme_dir = __DIR__ . '/themes/' .$active_theme;
+if (!is_dir($theme_dir)) {
+    $active_theme = 'default';$theme_dir = __DIR__ . '/themes/default';
+}
+
+$loader = new \Twig\Loader\FilesystemLoader($theme_dir);
+$twig = new \Twig\Environment($loader, ['cache' => false]);
+$twig->addGlobal('session',$_SESSION);
+$twig->addGlobal('current_lang',$current_lang);
+$twig->addGlobal('site_logo',$site_logo); // Ορισμός global για να εμφανίζεται σωστά το λογότυπο
+
+echo $twig->render('seller_orders.html.twig', [
+    'site_title' => $site_title,
+    't' => $t,
+    'message' => $message,
+    'orders' => $orders,
+    'categories' => $categories
+]);

+ 533 - 0
sqlSchema/hockey_cms.sql

@@ -0,0 +1,533 @@
+/*M!999999\- enable the sandbox mode */ 
+-- MariaDB dump 10.19-11.8.6-MariaDB, for debian-linux-gnu (aarch64)
+--
+-- Host: localhost    Database: hockey_cms
+-- ------------------------------------------------------
+-- Server version	11.8.6-MariaDB-0+deb13u1 from Debian
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
+
+--
+-- Table structure for table `attribute_groups`
+--
+
+DROP TABLE IF EXISTS `attribute_groups`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `attribute_groups` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  `name_en` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `attribute_groups`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `attribute_groups` WRITE;
+/*!40000 ALTER TABLE `attribute_groups` DISABLE KEYS */;
+INSERT INTO `attribute_groups` VALUES
+(1,'ICE SKATES','ΠΑΓΟΠΕΔΙΛΑ'),
+(2,'ΡΟΥΧΙΣΜΟΣ','APPAREL'),
+(10,'ΜΠΑΣΤΟΥΝΙΑ','STICKS');
+/*!40000 ALTER TABLE `attribute_groups` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `attributes`
+--
+
+DROP TABLE IF EXISTS `attributes`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `attributes` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `group_id` int(11) NOT NULL,
+  `name` varchar(100) NOT NULL,
+  `name_en` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
+  `unit` varchar(50) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `group_id` (`group_id`),
+  CONSTRAINT `attributes_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `attribute_groups` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `attributes`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `attributes` WRITE;
+/*!40000 ALTER TABLE `attributes` DISABLE KEYS */;
+INSERT INTO `attributes` VALUES
+(1,1,'ΜΕΓΕΘΟΣ','SIZE','US'),
+(3,1,'ΜΕΓΕΘΟΣ','SIZE','EU'),
+(4,1,'ΚΑΤΑΣΤΑΣΗ','CONDITION',''),
+(6,2,'ΜΕΓΕΘΟΣ','SIZE',''),
+(7,2,'ΗΛΙΚΙΑ','AGE','YO'),
+(8,2,'ΚΑΤΑΣΤΑΣΗ','CONDITION',''),
+(12,10,'ΚΑΤΑΣΤΑΣΗ','CONDITION',''),
+(13,10,'ΥΨΟΣ','HEIGHT','CM'),
+(14,10,'ΧΕΡΙ','HAND','Left/Right');
+/*!40000 ALTER TABLE `attributes` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `categories`
+--
+
+DROP TABLE IF EXISTS `categories`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `parent_id` int(11) DEFAULT NULL,
+  `name_el` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `name_en` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
+  `sort_order` int(11) NOT NULL DEFAULT 0,
+  `slug` varchar(100) NOT NULL,
+  `attribute_group_id` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `slug` (`slug`),
+  KEY `parent_id` (`parent_id`),
+  KEY `attribute_group_id` (`attribute_group_id`),
+  CONSTRAINT `categories_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `categories_ibfk_2` FOREIGN KEY (`attribute_group_id`) REFERENCES `attribute_groups` (`id`) ON DELETE SET NULL,
+  CONSTRAINT `categories_ibfk_3` FOREIGN KEY (`attribute_group_id`) REFERENCES `attribute_groups` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `categories`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `categories` WRITE;
+/*!40000 ALTER TABLE `categories` DISABLE KEYS */;
+INSERT INTO `categories` VALUES
+(1,NULL,'ΠΑΓΟΠΕΔΙΛΑ','ICE SKATES',1,'ice-skates',1),
+(9,NULL,'ΕΞΟΠΛΗΣΜΟΣ ΤΕΡΜΑΤΟΦΥΛΑΚΑ','GOALIE GEAR',12,'goalie-gear',2),
+(10,NULL,'ΜΠΑΣΤΟΥΝΙΑ','STICKS',2,'sticks',10),
+(13,NULL,'ΡΟΥΧΙΣΜΟΣ','APPAREL',3,'apparel',2),
+(15,16,'ΓΑΝΤΙΑ','GLOVES',5,'gloves',2),
+(16,NULL,'ΠΡΟΣΤΑΣΙΑ','PROTECTIVE GEAR',4,'protective-gear',2),
+(17,16,'ΕΠΙΑΓΚΩΝΙΔΕΣ','PADS ELBOW',6,'pads-elbow',2),
+(18,16,'ΕΠΙΚΑΛΑΜΙΔΕΣ','SHIN GUARDS',7,'shin-guards',2),
+(19,13,'ΘΩΡΑΚΕΣ & ΕΠΩΜΙΔΕΣ','CHEST & SOULDER PADS',9,'chest-soulder-pads',2),
+(20,16,'ΚΡΑΝΟΣ','HELMET',8,'helmet',2),
+(21,13,'ΠΑΝΤΕΛΟΝΙ','PANTS',10,'pants',2),
+(22,13,'ΣΠΑΣΟΥΑΡ','JOCK',11,'jock',2);
+/*!40000 ALTER TABLE `categories` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `email_templates`
+--
+
+DROP TABLE IF EXISTS `email_templates`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `email_templates` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `template_key` varchar(50) NOT NULL,
+  `name` varchar(100) NOT NULL,
+  `subject_el` varchar(255) NOT NULL,
+  `subject_en` varchar(255) NOT NULL,
+  `body_el` text NOT NULL,
+  `body_en` text NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `template_key` (`template_key`)
+) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `email_templates`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `email_templates` WRITE;
+/*!40000 ALTER TABLE `email_templates` DISABLE KEYS */;
+INSERT INTO `email_templates` VALUES
+(1,'new_order','  (  / )','HockeyGear - Επιβεβαίωση καταχώρισης παραγγελίας #{order_id}','HockeyGear - Order Confirmation #{order_id}','Αγαπητέ/ή {customer_name},\r\n\r\nΚαταχωρίθηκε επιτυχώς η παραγγελία σας με τα παρακάτω στοιχεία:\r\n\r\nΚωδικός Παραγγελίας: #{order_id}\r\n\r\nΣυνολική Τιμή: {total_amount}\r\n\r\nΠαρακαλούμε όπως επικοινωνήσετε με τον πωλητή για τη διαδικασία παραλαβής.','Dear {customer_name},\r\n\r\nYour order has been successfully placed with the following details:\r\n\r\nOrder ID: #{order_id}\r\n\r\nTotal Amount: {total_amount}\r\n\r\nPlease contact the seller to arrange your order collection/pickup.'),
+(2,'status_change','  ','HockeyGear - Ενημέρωση κατάστασης παραγγελίας #{order_id}','HockeyGear - Order Status Update #{order_id}','Αγαπητέ/ή {customer_name},\r\n\r\nΣας ενημερώνουμε ότι η κατάσταση της παραγγελίας σας άλλαξε.\r\n\r\nΚωδικός Παραγγελίας: #{order_id}\r\n\r\nΝέα Κατάσταση: {status}\r\n\r\nΠαρακαλούμε επικοινωνήστε μαζί μας ή με τον πωλητή για περισσότερες πληροφορίες.','Dear {customer_name},\r\n\r\nWe would like to inform you that the status of your order has been updated.\r\n\r\nOrder ID: #{order_id}\r\n\r\nNew Status: {status}\r\n\r\nPlease contact us or the seller for further details.'),
+(3,'seller_new_order','  ( )','HockeyGear - Νέα Παραγγελία #{order_id} - Πάνελ Διαχείρισης','New Sale! Order #{order_id} - Hockey CMS','Αγαπητέ συνεργάτη,\r\n\r\nΚαταχωρίθηκε νέα παραγγελία στο σύστημα με τα παρακάτω στοιχεία:\r\n\r\nΚωδικός Παραγγελίας: #{order_id}\r\n\r\nΠελάτης: {customer_name}\r\n\r\nΣυνολική Τιμή: {total_amount}\r\n\r\nΠαρακαλούμε συνδεθείτε στο πάνελ διαχείρισης για να δείτε τις λεπτομέρειες και να προχωρήσετε στις απαραίτητες ενέργειες.','Dear Partner,\r\n\r\nA new order has been placed in the system with the following details:\r\n\r\nOrder ID: #{order_id}\r\n\r\nCustomer: {customer_name}\r\n\r\nTotal Amount: {total_amount}\r\n\r\nPlease log in to your admin panel to view the details and proceed with the order fulfillment.');
+/*!40000 ALTER TABLE `email_templates` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `order_items`
+--
+
+DROP TABLE IF EXISTS `order_items`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `order_items` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `order_id` int(11) NOT NULL,
+  `product_id` int(11) NOT NULL,
+  `quantity` int(11) NOT NULL,
+  `price` decimal(10,2) NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `order_id` (`order_id`),
+  KEY `product_id` (`product_id`),
+  CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `order_items`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `order_items` WRITE;
+/*!40000 ALTER TABLE `order_items` DISABLE KEYS */;
+/*!40000 ALTER TABLE `order_items` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `orders`
+--
+
+DROP TABLE IF EXISTS `orders`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `orders` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `customer_name` varchar(100) NOT NULL,
+  `customer_email` varchar(100) NOT NULL,
+  `customer_phone` varchar(50) NOT NULL,
+  `shipping_address` text NOT NULL,
+  `total_amount` decimal(10,2) NOT NULL,
+  `status` varchar(20) DEFAULT 'pending',
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `orders`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `orders` WRITE;
+/*!40000 ALTER TABLE `orders` DISABLE KEYS */;
+/*!40000 ALTER TABLE `orders` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `product_attribute_values`
+--
+
+DROP TABLE IF EXISTS `product_attribute_values`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `product_attribute_values` (
+  `product_id` int(11) NOT NULL,
+  `attribute_id` int(11) NOT NULL,
+  `value` varchar(255) NOT NULL,
+  PRIMARY KEY (`product_id`,`attribute_id`),
+  KEY `attribute_id` (`attribute_id`),
+  CONSTRAINT `product_attribute_values_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `product_attribute_values_ibfk_2` FOREIGN KEY (`attribute_id`) REFERENCES `attributes` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `product_attribute_values`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `product_attribute_values` WRITE;
+/*!40000 ALTER TABLE `product_attribute_values` DISABLE KEYS */;
+INSERT INTO `product_attribute_values` VALUES
+(2,6,'Youth'),
+(2,7,'6 to 9'),
+(2,8,'Good'),
+(3,6,'Large (youth)'),
+(3,7,'5 to 7'),
+(3,8,'Σχεδόν καινούργιο / as good as new'),
+(4,6,'Large (youth)'),
+(4,7,'10 to 13'),
+(4,8,'2. Καλή / good'),
+(5,6,'Jr.Medium'),
+(5,8,'Καλή / good'),
+(6,6,'Large (youth)'),
+(6,7,'5 to 7'),
+(6,8,'Καλή / good');
+/*!40000 ALTER TABLE `product_attribute_values` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `product_images`
+--
+
+DROP TABLE IF EXISTS `product_images`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `product_images` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `product_id` int(11) NOT NULL,
+  `image_path` varchar(255) NOT NULL,
+  `is_primary` tinyint(1) DEFAULT 0,
+  PRIMARY KEY (`id`),
+  KEY `product_id` (`product_id`),
+  CONSTRAINT `product_images_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `product_images`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `product_images` WRITE;
+/*!40000 ALTER TABLE `product_images` DISABLE KEYS */;
+INSERT INTO `product_images` VALUES
+(2,2,'uploads/equipment/prod_2_1790588393.jpg',1),
+(3,3,'uploads/equipment/prod_3_1790588897.jpg',1),
+(4,4,'uploads/equipment/prod_4_1790589083.jpg',1),
+(5,5,'uploads/equipment/prod_5_1790589860.jpg',1),
+(6,6,'uploads/equipment/prod_6_1790590114.jpg',1);
+/*!40000 ALTER TABLE `product_images` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `products`
+--
+
+DROP TABLE IF EXISTS `products`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `products` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `sku` varchar(50) DEFAULT NULL,
+  `seller_id` int(11) NOT NULL,
+  `category_id` int(11) NOT NULL,
+  `attribute_group_id` int(11) DEFAULT NULL,
+  `title` varchar(255) NOT NULL,
+  `description` text DEFAULT NULL,
+  `search_tags` varchar(255) DEFAULT NULL,
+  `price` decimal(10,2) NOT NULL,
+  `status` enum('available','sold','hidden') DEFAULT 'available',
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `quantity` int(11) DEFAULT 1,
+  PRIMARY KEY (`id`),
+  KEY `seller_id` (`seller_id`),
+  KEY `category_id` (`category_id`),
+  KEY `attribute_group_id` (`attribute_group_id`),
+  CONSTRAINT `products_ibfk_1` FOREIGN KEY (`seller_id`) REFERENCES `users` (`id`),
+  CONSTRAINT `products_ibfk_2` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`),
+  CONSTRAINT `products_ibfk_3` FOREIGN KEY (`attribute_group_id`) REFERENCES `attribute_groups` (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `products`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `products` WRITE;
+/*!40000 ALTER TABLE `products` DISABLE KEYS */;
+INSERT INTO `products` VALUES
+(2,NULL,1,15,2,'Bauer Supreme one 20','','gloves, γαντια, προστασια',20.00,'sold','2026-09-28 09:39:03',0),
+(3,NULL,1,17,2,'Bauer Supreme Pro','','επιαγκωνιδες, pads elbow, προστασια',15.00,'available','2026-09-28 09:45:02',1),
+(4,NULL,1,17,2,'Bauer NSX','','επιαγκωνιδες, pads elbow',15.00,'available','2026-09-28 09:51:23',1),
+(5,NULL,1,18,2,'Jr.Medium','','επικαλαμιδες, shin, guards',15.00,'available','2026-09-28 10:04:20',1),
+(6,NULL,1,18,2,'Bauer Supreme Pro','','',10.00,'available','2026-09-28 10:08:33',1);
+/*!40000 ALTER TABLE `products` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `roles`
+--
+
+DROP TABLE IF EXISTS `roles`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(50) NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `roles`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `roles` WRITE;
+/*!40000 ALTER TABLE `roles` DISABLE KEYS */;
+INSERT INTO `roles` VALUES
+(1,'administrator'),
+(2,'seller'),
+(3,'user');
+/*!40000 ALTER TABLE `roles` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `settings`
+--
+
+DROP TABLE IF EXISTS `settings`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `settings` (
+  `key` varchar(50) NOT NULL,
+  `value` text NOT NULL,
+  PRIMARY KEY (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `settings`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `settings` WRITE;
+/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
+INSERT INTO `settings` VALUES
+('active_theme','default'),
+('allow_user_to_seller','1'),
+('mail_from_address','athenshockeygear@gmail.com'),
+('mail_from_name','Athens Hockey Gear'),
+('site_logo','uploads/logo_1790669499.jpg'),
+('smtp_encryption','tls'),
+('smtp_host','smtp.gmail.com'),
+('smtp_password','mdaexymcizvutjjh'),
+('smtp_port','587'),
+('smtp_username','athenshockeygear@gmail.com');
+/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `site_texts`
+--
+
+DROP TABLE IF EXISTS `site_texts`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `site_texts` (
+  `text_key` varchar(100) NOT NULL,
+  `text_value_el` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
+  `text_value_en` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
+  `description` varchar(255) DEFAULT NULL,
+  PRIMARY KEY (`text_key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `site_texts`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `site_texts` WRITE;
+/*!40000 ALTER TABLE `site_texts` DISABLE KEYS */;
+INSERT INTO `site_texts` VALUES
+('btn_view_product','','View','     '),
+('category_subtitle_prefix','','Results :','      '),
+('category_title_prefix','','Category :','       '),
+('filter_price_from','','From','Placeholder  '),
+('filter_price_label','','Price :','    '),
+('filter_price_to','','To','Placeholder  '),
+('filter_sort_label','','Order :','   dropdown '),
+('header_cart_btn','','Cart','   '),
+('header_login_btn','','Login','     '),
+('header_logo_text','','TradeHockey','  /   e-shop    '),
+('header_search_btn','','Search','   '),
+('header_search_placeholder','...','...','    '),
+('sidebar_categories_title','','Categories','      '),
+('site_slogan','','Sell & Buy Hockey Gear','     '),
+('site_sub_slogan','','Used and new Hockey Gear','        '),
+('title_new_arrivals','','New Entries','      ');
+/*!40000 ALTER TABLE `site_texts` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+
+--
+-- Table structure for table `users`
+--
+
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `role_id` int(11) NOT NULL,
+  `username` varchar(50) NOT NULL,
+  `email` varchar(100) DEFAULT NULL,
+  `phone` varchar(50) DEFAULT NULL,
+  `password` varchar(255) NOT NULL,
+  `first_name` varchar(50) DEFAULT NULL,
+  `last_name` varchar(50) DEFAULT NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp(),
+  `is_seller` tinyint(1) DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `username` (`username`),
+  KEY `role_id` (`role_id`),
+  CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `users`
+--
+
+SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
+LOCK TABLES `users` WRITE;
+/*!40000 ALTER TABLE `users` DISABLE KEYS */;
+INSERT INTO `users` VALUES
+(1,1,'admin','sparmaksitzian@gmail.com','6943078334','$2y$12$eIpc5tWDqNHNhGwW7VJhpOIc0M1twWFgEu6MSt.kgs.GjdbAmyTjG','Super','Admin','2026-09-27 13:25:04',1),
+(4,3,'eparmak','maidawmn@gmail.com','','$2y$12$POty9nKbADIDWVf/mNOq1ePopW4Klq6rn6OXrJfETed5FUvzQLgUm','Stathis','Parmaksitzian','2026-09-27 21:50:17',1),
+(5,3,'parmaksitziand@gmail.com','parmaksitziand@gmail.com',NULL,'$2y$12$S4OUCHLVBhzPoeuUnZ1DNOvfR5SuOi92OoqOZ.yj.z9g375wBPMYO','Dimitris','Parmaksitzian','2026-09-29 07:46:37',0);
+/*!40000 ALTER TABLE `users` ENABLE KEYS */;
+UNLOCK TABLES;
+COMMIT;
+SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
+
+-- Dump completed on 2026-09-29 12:29:47

+ 201 - 0
terms.php

@@ -0,0 +1,201 @@
+<?php
+// Αρχείο: /var/www/html/hockey-cms/terms.php
+session_start();
+
+// --- Μηχανισμός Αλλαγής Γλώσσας ---
+if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
+    $_SESSION['lang'] = $_GET['lang'];
+}
+$current_lang = $_SESSION['lang'] ?? 'el';
+
+// --- Λεξικό Μεταφράσεων ---
+$translations = [
+    'el' => [
+        'title' => 'Όροι Χρήσης - Hockey CMS',
+        'h1' => 'Όροι Χρήσης (Terms of Service)',
+        'last_updated' => 'Τελευταία ενημέρωση: ' . date('d/m/Y'),
+        'p1' => 'Καλώς ήρθατε στο <strong>Hockey CMS</strong>. Η χρήση του ιστότοπου και των υπηρεσιών μας προϋποθέτει την ανεπιφύλακτη αποδοχή των παρακάτω όρων.',
+        'h2_1' => '1. Λογαριασμοί Χρηστών',
+        'p2_1' => 'Κατά τη δημιουργία λογαριασμού, οφείλετε να παρέχετε ακριβή και αληθή στοιχεία. Είστε αποκλειστικά υπεύθυνοι για τη διατήρηση της εμπιστευτικότητας του κωδικού πρόσβασής σας. Διατηρούμε το δικαίωμα να αναστείλουμε ή να διαγράψουμε λογαριασμούς που παραβιάζουν τους όρους μας.',
+        'h2_2' => '2. Παραγγελίες και Διαθεσιμότητα',
+        'p2_2' => 'Όλες οι παραγγελίες υπόκεινται σε έλεγχο διαθεσιμότητας των προϊόντων. Σε περίπτωση έλλειψης αποθέματος, το σύστημα ή οι διαχειριστές (Sellers) διατηρούν το δικαίωμα ακύρωσης της παραγγελίας, ενημερώνοντάς σας άμεσα. Οι τιμές μπορούν να αλλάξουν χωρίς προειδοποίηση.',
+        'h2_3' => '3. Πνευματική Ιδιοκτησία',
+        'p2_3' => 'Το σύνολο του περιεχομένου του ιστότοπου (λογότυπα, εικόνες, κείμενα, γραφικά) αποτελεί πνευματική ιδιοκτησία του <strong>Hockey CMS</strong> ή των αντίστοιχων πωλητών και προστατεύεται από τους σχετικούς νόμους. Απαγορεύεται η αντιγραφή ή αναδημοσίευση χωρίς έγγραφη άδεια.',
+        'h2_4' => '4. Περιορισμός Ευθύνης',
+        'p2_4' => 'Το κατάστημά μας δεν φέρει ευθύνη για τυχόν άμεσες ή έμμεσες ζημιές που μπορεί να προκύψουν από τη χρήση του ιστότοπου, καθυστερήσεις στις παραδόσεις από τρίτους (π.χ. courier) ή τεχνικά σφάλματα στην πλατφόρμα.',
+        'btn_back' => '&larr; Επιστροφή στο Κατάστημα'
+    ],
+    'en' => [
+        'title' => 'Terms of Service - Hockey CMS',
+        'h1' => 'Terms of Service',
+        'last_updated' => 'Last updated: ' . date('d/m/Y'),
+        'p1' => 'Welcome to <strong>Hockey CMS</strong>. By accessing or using our website and services, you agree to be bound by these Terms of Service.',
+        'h2_1' => '1. User Accounts',
+        'p2_1' => 'When creating an account, you must provide accurate and complete information. You are solely responsible for maintaining the confidentiality of your password. We reserve the right to suspend or terminate accounts that violate our terms.',
+        'h2_2' => '2. Orders and Availability',
+        'p2_2' => 'All orders are subject to product availability. In the event of out-of-stock items, the system or the Administrators (Sellers) reserve the right to cancel the order and notify you immediately. Prices are subject to change without notice.',
+        'h2_3' => '3. Intellectual Property',
+        'p2_3' => 'All content on this website (logos, images, text, graphics) is the intellectual property of <strong>Hockey CMS</strong> or the respective sellers and is protected by applicable laws. Copying or reproduction without written permission is strictly prohibited.',
+        'h2_4' => '4. Limitation of Liability',
+        'p2_4' => 'Our store shall not be liable for any direct or indirect damages resulting from the use of the website, delivery delays by third parties (e.g., couriers), or technical errors on the platform.',
+        'btn_back' => '&larr; Back to Store'
+    ]
+];
+
+$t = $translations[$current_lang];
+?>
+<!DOCTYPE html>
+<html lang="<?php echo $current_lang; ?>">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title><?php echo $t['title']; ?></title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    <style>
+        :root {
+            --primary: #f97316;
+            --bg-color: #f4f6f9;
+            --card-bg: #ffffff;
+            --text-main: #1e293b;
+            --text-muted: #64748b;
+            --border-color: #e2e8f0;
+        }
+
+        /* Υποστήριξη Dark Mode ανάλογα με τις ρυθμίσεις της συσκευής του χρήστη */
+        @media (prefers-color-scheme: dark) {
+            :root {
+                --bg-color: #0b1120;
+                --card-bg: #1e293b;
+                --text-main: #f8fafc;
+                --text-muted: #94a3b8;
+                --border-color: #334155;
+            }
+        }
+
+        body {
+            font-family: 'Inter', sans-serif;
+            background-color: var(--bg-color);
+            color: var(--text-main);
+            line-height: 1.6;
+            margin: 0;
+            padding: 40px 20px;
+        }
+
+        .container {
+            max-width: 800px;
+            margin: 0 auto;
+        }
+
+        .card {
+            background-color: var(--card-bg);
+            padding: 40px;
+            border-radius: 12px;
+            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+            border: 1px solid var(--border-color);
+        }
+
+        h1 {
+            color: var(--primary);
+            font-size: 28px;
+            margin-top: 0;
+            margin-bottom: 5px;
+        }
+
+        .last-updated {
+            font-size: 13px;
+            color: var(--text-muted);
+            margin-bottom: 30px;
+            display: block;
+        }
+
+        h2 {
+            font-size: 18px;
+            margin-top: 30px;
+            margin-bottom: 10px;
+            color: var(--text-main);
+            border-bottom: 2px solid var(--border-color);
+            padding-bottom: 5px;
+        }
+
+        p {
+            margin-bottom: 15px;
+            font-size: 15px;
+            color: var(--text-muted);
+        }
+
+        strong {
+            color: var(--text-main);
+        }
+
+        .lang-switch {
+            text-align: right;
+            margin-bottom: 20px;
+        }
+
+        .lang-switch a {
+            text-decoration: none;
+            font-size: 13px;
+            font-weight: 700;
+            padding: 6px 12px;
+            border-radius: 20px;
+            background: var(--card-bg);
+            color: var(--text-muted);
+            border: 1px solid var(--border-color);
+            transition: 0.2s;
+            margin-left: 5px;
+        }
+
+        .lang-switch a.active {
+            background: var(--primary);
+            color: white;
+            border-color: var(--primary);
+        }
+
+        .back-btn {
+            display: inline-block;
+            margin-top: 30px;
+            text-decoration: none;
+            color: var(--primary);
+            font-weight: 600;
+            font-size: 15px;
+            transition: 0.2s;
+        }
+
+        .back-btn:hover {
+            color: #ea580c;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="container">
+        <!-- Κουμπιά εναλλαγής γλώσσας -->
+        <div class="lang-switch">
+            <a href="?lang=el" class="<?php echo $current_lang == 'el' ? 'active' : ''; ?>">GR</a>
+            <a href="?lang=en" class="<?php echo $current_lang == 'en' ? 'active' : ''; ?>">EN</a>
+        </div>
+
+        <div class="card">
+            <h1><?php echo $t['h1']; ?></h1>
+            <span class="last-updated"><?php echo $t['last_updated']; ?></span>
+
+            <p><?php echo $t['p1']; ?></p>
+
+            <h2><?php echo $t['h2_1']; ?></h2>
+            <p><?php echo $t['p2_1']; ?></p>
+
+            <h2><?php echo $t['h2_2']; ?></h2>
+            <p><?php echo $t['p2_2']; ?></p>
+
+            <h2><?php echo $t['h2_3']; ?></h2>
+            <p><?php echo $t['p2_3']; ?></p>
+
+            <h2><?php echo $t['h2_4']; ?></h2>
+            <p><?php echo $t['p2_4']; ?></p>
+
+            <a href="index.php" class="back-btn"><?php echo $t['btn_back']; ?></a>
+        </div>
+    </div>
+
+</body>
+</html>

+ 294 - 0
themes/default/base.html.twig

@@ -0,0 +1,294 @@
+<!DOCTYPE html>
+<html lang="{{ current_lang|default('el') }}">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>{% block title %}{{ site_title|default('Hockey CMS') }}{% endblock %}</title>
+    
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+    
+    <style>
+        :root {
+            /* --- DARK THEME PALETTE --- */
+            --primary: #f97316; /* Δυναμικό Πορτοκαλί */
+            --primary-hover: #ea580c;
+            --bg-color: #0b1120; /* Βαθύ σκοτεινό Slate */
+            --panel-bg: #1e293b; /* Χρώμα για κάρτες και header */
+            --border-color: #334155;
+            --text-main: #f8fafc;
+            --text-muted: #94a3b8;
+            --accent-green: #10b981;
+            --accent-blue: #3b82f6;
+            --shadow-sm: 0 4px 6px -1px rgba(0,0,0,0.3);
+            --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.5);
+            --border-radius: 8px; /* Λιγότερο στρογγυλεμένες γωνίες για σπορ ύφος */
+        }
+
+        body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); margin: 0; color: var(--text-main); line-height: 1.5; }
+        
+        /* --- HEADER --- */
+        header { background: rgba(30, 41, 59, 0.85); backdrop-filter: blur(10px); padding: 15px 0; border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 100; box-shadow: var(--shadow-sm); }
+        .header-inner { max-width: 1400px; margin: 0 auto; padding: 0 30px; display: flex; justify-content: space-between; align-items: center; }
+        .logo { font-size: 24px; font-weight: 900; color: var(--text-main); text-decoration: none; display: flex; align-items: center; gap: 10px; letter-spacing: -0.5px; text-transform: uppercase; }
+        .logo span { color: var(--primary); }
+        
+        .header-actions { display: flex; align-items: center; gap: 25px; }
+        .search-form { display: flex; background: #0f172a; border-radius: 30px; padding: 4px; border: 1px solid var(--border-color); flex-grow: 1; transition: border-color 0.3s; }
+        .search-form:focus-within { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+        .search-form input { padding: 8px 15px; border: none; background: transparent; outline: none; width: 280px; font-family: 'Inter', sans-serif; color: var(--text-main); }
+        .search-form input::placeholder { color: var(--text-muted); }
+        .search-form button { padding: 8px 25px; border: none; background: var(--primary); color: white; border-radius: 30px; cursor: pointer; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: 0.2s; }
+        .search-form button:hover { background: var(--primary-hover); }
+        
+        /* --- LANGUAGE SWITCHER --- */
+        .lang-switch { font-size: 13px; font-weight: 700; display: flex; gap: 5px; align-items: center; background: #0f172a; padding: 4px; border-radius: 20px; border: 1px solid var(--border-color); }
+        .lang-switch a { color: var(--text-muted); text-decoration: none; padding: 4px 10px; border-radius: 15px; transition: 0.2s; }
+        .lang-switch a:hover { color: var(--text-main); }
+        .lang-switch a.active { color: #fff; background: var(--border-color); }
+
+        /* --- USER MENU --- */
+        .user-menu { display: flex; gap: 12px; align-items: center; margin-left: 10px; }
+        .cart-link { background: transparent; border: 2px solid var(--primary); color: var(--primary); padding: 8px 20px; border-radius: 30px; font-weight: 700; text-decoration: none; display: flex; align-items: center; gap: 5px; white-space: nowrap; transition: 0.2s; text-transform: uppercase; font-size: 14px; }
+        .cart-link:hover { background: var(--primary); color: white; }
+        
+        .btn-seller { background: var(--accent-blue); color: white; padding: 10px 18px; border-radius: 30px; text-decoration: none; font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; transition: 0.2s; }
+        .btn-seller:hover { background: #2563eb; box-shadow: 0 0 10px rgba(59, 130, 246, 0.5); }
+        
+        .btn-profile { background: #334155; color: var(--text-main); padding: 10px 18px; border-radius: 30px; text-decoration: none; font-weight: 600; font-size: 14px; transition: 0.2s; }
+        .btn-profile:hover { background: #475569; }
+        
+        .btn-logout-icon { color: #fca5a5; font-size: 16px; font-weight: bold; text-decoration: none; padding: 5px 10px; border-radius: 50%; display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); transition: all 0.2s; }
+        .btn-logout-icon:hover { background: #ef4444; color: white; }
+
+        /* --- GUEST LOGIN/REGISTER BUTTONS --- */
+        .btn-login {
+            background: transparent;
+            color: var(--text-main);
+            border: 1px solid var(--border-color);
+            padding: 8px 18px;
+            border-radius: 30px;
+            font-weight: 700;
+            text-decoration: none;
+            font-size: 13px;
+            text-transform: uppercase;
+            letter-spacing: 0.5px;
+            transition: 0.2s;
+        }
+        .btn-login:hover {
+            background: rgba(255, 255, 255, 0.05);
+            border-color: var(--text-muted);
+        }
+        
+        .btn-register {
+            background: var(--primary);
+            color: white;
+            padding: 8px 18px;
+            border-radius: 30px;
+            font-weight: 700;
+            text-decoration: none;
+            font-size: 13px;
+            text-transform: uppercase;
+            letter-spacing: 0.5px;
+            transition: 0.2s;
+            border: 1px solid var(--primary);
+        }
+        .btn-register:hover {
+            background: var(--primary-hover);
+            border-color: var(--primary-hover);
+        }
+
+        /* --- LAYOUT --- */
+        .main-container { max-width: 1400px; margin: 40px auto; padding: 0 30px; display: flex; gap: 40px; align-items: flex-start; }
+        .content-area { flex-grow: 1; min-width: 0; }
+        
+        /* --- MOBILE TOGGLE BUTTON --- */
+        .mobile-menu-toggle { display: none; width: 100%; padding: 14px; background: var(--panel-bg); border: 1px solid var(--border-color); color: var(--text-main); border-radius: var(--border-radius); font-weight: 700; cursor: pointer; margin-bottom: 20px; font-size: 15px; text-align: center; text-transform: uppercase; letter-spacing: 1px; }
+
+        /* --- SIDEBAR --- */
+        .sidebar { width: 280px; flex-shrink: 0; background: var(--panel-bg); padding: 25px 0; border-radius: var(--border-radius); box-shadow: var(--shadow-lg); border: 1px solid var(--border-color); }
+        .sidebar h3 { margin: 0 0 20px 25px; font-size: 13px; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 800; color: var(--primary); }
+        .tree-menu { list-style: none; padding: 0; margin: 0; }
+        .menu-item { position: relative; border-bottom: 1px solid rgba(255,255,255,0.03); }
+        .menu-item:last-child { border-bottom: none; }
+        .parent-cat { text-decoration: none; color: var(--text-main); display: flex; justify-content: space-between; align-items: center; padding: 14px 25px; font-weight: 600; font-size: 15px; border-left: 3px solid transparent; transition: 0.2s; }
+        .parent-cat:hover { background-color: rgba(255,255,255,0.03); color: var(--primary); border-left: 3px solid var(--primary); }
+        .has-children::after { content: '›'; font-size: 18px; color: var(--text-muted); }
+        
+        /* Submenu (Desktop) */
+        .tree-submenu { list-style: none; padding: 0; margin: 0; position: absolute; left: 100%; top: -1px; width: 240px; background: var(--panel-bg); border-radius: var(--border-radius); box-shadow: var(--shadow-lg); border: 1px solid var(--border-color); opacity: 0; visibility: hidden; transform: translateX(10px); transition: all 0.2s; z-index: 50; }
+        .menu-item:hover .tree-submenu { opacity: 1; visibility: visible; transform: translateX(0); }
+        .tree-submenu li a { text-decoration: none; color: var(--text-muted); display: block; padding: 12px 20px; font-size: 14px; font-weight: 500; border-bottom: 1px solid rgba(255,255,255,0.03); transition: 0.2s; }
+        .tree-submenu li a:hover { color: var(--primary); background-color: rgba(255,255,255,0.02); padding-left: 25px; }
+
+        /* --- FOOTER --- */
+        footer { background: var(--panel-bg); color: var(--text-muted); text-align: center; padding: 40px 30px; margin-top: 80px; border-top: 1px solid var(--border-color); font-size: 14px; font-weight: 500; }
+
+        @media (max-width: 992px) {
+            .main-container { flex-direction: column; }
+            .sidebar { width: 100%; display: none; margin-bottom: 30px; }
+            .sidebar.active { display: block; }
+            .mobile-menu-toggle { display: block; }
+            
+            .menu-item:hover .tree-submenu { display: none; }
+            .tree-submenu { position: relative; left: 0; top: 0; width: 100%; opacity: 1; visibility: visible; transform: none; box-shadow: none; border: none; border-top: 1px solid var(--border-color); border-radius: 0; display: block; background: rgba(0,0,0,0.2); }
+            .tree-submenu li a { padding: 12px 15px 12px 35px; }
+            .has-children::after { content: '▼'; font-size: 12px; }
+        }
+
+        @media (max-width: 768px) {
+            .header-inner { flex-direction: column; padding: 15px; gap: 20px; }
+            .header-actions { flex-direction: column; width: 100%; gap: 15px; }
+            .search-form { width: 100%; }
+            .search-form input { width: 100%; }
+            
+            .user-menu { flex-wrap: wrap; justify-content: center; width: 100%; margin-left: 0; gap: 10px; }
+            .cart-link, .btn-seller, .btn-profile { flex: 1; justify-content: center; text-align: center; font-size: 12px; padding: 10px; }
+            .btn-login, .btn-register { flex: 1; text-align: center; }
+        }
+
+        {% block extra_css %}{% endblock %}
+    </style>
+</head>
+<body>
+
+    <header>
+        <div class="header-inner">
+            <a href="index.php" class="logo">
+                {% if site_logo %}
+                    <img src="{{ site_logo }}" alt="Logo" style="max-height: 65px; width: auto; max-width: 250px; object-fit: contain; display: block;">
+                {% else %}
+                    🏒 <span>{{ text.header_logo_text|default('TradeHockey') }}</span>
+                {% endif %}
+            </a>
+            
+            <div class="header-actions">
+                
+                <div class="lang-switch">
+                    <a href="javascript:void(0);" onclick="changeShopLanguage('el');" class="{% if current_lang == 'el' %}active{% endif %}">GR</a>
+                    <a href="javascript:void(0);" onclick="changeShopLanguage('en');" class="{% if current_lang == 'en' %}active{% endif %}">EN</a>
+                </div>
+
+                <script>
+                    function changeShopLanguage(lang) {
+                        var url = new URL(window.location.href);
+                        url.searchParams.set('lang', lang);
+                        window.location.href = url.toString();
+                    }
+                </script>
+
+                <form action="index.php" method="GET" class="search-form">
+                    <input type="text" name="q" placeholder="{{ text.header_search_placeholder|default('Αναζήτηση εξοπλισμού...') }}" value="{{ search_query|default('') }}" required>
+                    <button type="submit">{{ text.header_search_btn|default('Αναζήτηση') }}</button>
+                </form>
+                
+                <div class="user-menu">
+                    {% if session.user_id %}
+                        <a href="cart.php" class="cart-link">🛒 {{ text.header_cart_btn|default('Καλάθι') }}</a>
+                        
+                        {% if session.role_id == 2 or session.is_seller == 1 %}
+                            <a href="seller_dashboard.php" class="btn-seller">
+                                🏪 {% if current_lang == 'en' %}Seller Panel{% else %}Πωλητής{% endif %}
+                            </a>
+                        {% endif %}
+                        
+                        <a href="profile.php" title="Το Προφίλ μου" class="btn-profile">👤 {{ session.username }}</a>
+                        <a href="logout.php" title="Αποσύνδεση" class="btn-logout-icon">
+                            <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+                                <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
+                                <polyline points="16 17 21 12 16 7"></polyline>
+                                <line x1="21" y1="12" x2="9" y2="12"></line>
+                            </svg>
+                        </a>
+                    {% else %}
+                        {% if current_lang == 'en' %}
+                            <a href="login.php" class="btn-login">{{ text.header_login_btn|default('Login') }}</a>
+                            <a href="register.php" class="btn-register">{{ text.header_register_btn|default('Register') }}</a>
+                        {% else %}
+                            <a href="login.php" class="btn-login">{{ text.header_login_btn|default('Σύνδεση') }}</a>
+                            <a href="register.php" class="btn-register">{{ text.header_register_btn|default('Εγγραφή') }}</a>
+                        {% endif %}
+                    {% endif %}
+                </div>
+            </div>
+        </div>
+    </header>
+
+    <div class="main-container">
+        <button id="mobileMenuBtn" class="mobile-menu-toggle">☰ {% if current_lang == "en" %}Show Categories{% else %}Εμφάνιση Κατηγοριών{% endif %}</button>
+
+        <aside class="sidebar" id="sidebarMenu">
+            <h3>{{ text.sidebar_categories_title|default('ΚΑΤΗΓΟΡΙΕΣ') }}</h3>
+            <ul class="tree-menu">
+                
+                <!-- ΚΟΥΜΠΙ "ΑΡΧΙΚΗ ΣΕΛΙΔΑ" -->
+                <li class="menu-item" style="border-bottom: 2px solid rgba(255,255,255,0.05);">
+                    <a href="index.php" class="parent-cat" style="color: var(--text-main); font-weight: 700;">
+                        <span style="display:flex; align-items:center; gap:8px;">
+                            <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+                                <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
+                                <polyline points="9 22 9 12 15 12 15 22"></polyline>
+                            </svg>
+                            {% if current_lang == 'en' %}Home{% else %}Αρχική Σελίδα{% endif %}
+                        </span>
+                    </a>
+                </li>
+                
+                {% for cat in categories %}
+                    {% if cat.parent_id is empty %}
+                        {% set has_children = false %}
+                        {% for subcat in categories %}
+                            {% if subcat.parent_id == cat.id %}
+                                {% set has_children = true %}
+                            {% endif %}
+                        {% endfor %}
+
+                        <li class="menu-item">
+                            <a href="index.php?category_id={{ cat.id }}" class="parent-cat {% if has_children %}has-children{% endif %}">
+                                {{ cat.display_name }}
+                            </a>
+                            
+                            {% if has_children %}
+                                <ul class="tree-submenu">
+                                    {% for subcat in categories %}
+                                        {% if subcat.parent_id == cat.id %}
+                                            <li><a href="index.php?category_id={{ subcat.id }}">{{ subcat.display_name }}</a></li>
+                                        {% endif %}
+                                    {% endfor %}
+                                </ul>
+                            {% endif %}
+                        </li>
+                    {% endif %}
+                {% endfor %}
+            </ul>
+        </aside>
+
+        <main class="content-area">
+            {% block content %}{% endblock %}
+        </main>
+    </div>
+
+    <footer>
+        <p>&copy; {{ "now"|date("Y") }} <strong>{{ site_title|default('Hockey CMS') }}</strong>. All rights reserved.</p>
+    </footer>
+
+    <script>
+        document.addEventListener('DOMContentLoaded', function() {
+            const menuBtn = document.getElementById('mobileMenuBtn');
+            const sidebar = document.getElementById('sidebarMenu');
+            
+            if(menuBtn && sidebar) {
+                menuBtn.addEventListener('click', function() {
+                    sidebar.classList.toggle('active');
+                    if(sidebar.classList.contains('active')) {
+                        menuBtn.innerHTML = '✕ {% if current_lang == "en" %}Hide Categories{% else %}Απόκρυψη Κατηγοριών{% endif %}';
+                    } else {
+                        menuBtn.innerHTML = '☰ {% if current_lang == "en" %}Show Categories{% else %}Εμφάνιση Κατηγοριών{% endif %}';
+                    }
+                });
+            }
+        });
+    </script>
+    
+    {% block extra_js %}{% endblock %}
+</body>
+</html>

+ 148 - 0
themes/default/cart.html.twig

@@ -0,0 +1,148 @@
+{% extends "base.html.twig" %}
+
+{% block title %}
+    {% if current_lang == 'en' %}Shopping Cart{% else %}Καλάθι Αγορών{% endif %} - {{ site_title }}
+{% endblock %}
+
+{% block extra_css %}
+<style>
+    /* --- CART STYLES (DARK THEME) --- */
+    .page-title { color: var(--text-main); margin-bottom: 25px; font-weight: 800; font-size: 28px; letter-spacing: -0.5px; text-transform: uppercase; }
+    
+    .cart-container { background: var(--panel-bg); padding: 30px; border-radius: var(--border-radius); box-shadow: var(--shadow-lg); border: 1px solid var(--border-color); }
+    
+    .cart-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
+    .cart-table th, .cart-table td { padding: 18px 15px; border-bottom: 1px solid rgba(255,255,255,0.05); text-align: left; vertical-align: middle; color: var(--text-main); }
+    .cart-table th { background: rgba(0,0,0,0.2); color: var(--text-muted); font-size: 13px; text-transform: uppercase; letter-spacing: 1px; }
+    .cart-table tr:last-child td { border-bottom: none; }
+    
+    .thumb { width: 65px; height: 65px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border-color); }
+    .no-img-placeholder { width: 65px; height: 65px; background: #0f172a; display:flex; align-items:center; justify-content:center; border-radius:6px; font-size:11px; color: var(--text-muted); border: 1px solid var(--border-color); text-transform: uppercase; font-weight: bold;}
+    
+    .btn-remove { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); padding: 8px 15px; text-decoration: none; border-radius: 6px; font-size: 13px; font-weight: 600; transition: 0.2s; text-transform: uppercase; letter-spacing: 0.5px; display: inline-block;}
+    .btn-remove:hover { background: #ef4444; color: white; border-color: #ef4444; box-shadow: 0 4px 10px rgba(239, 68, 68, 0.3);}
+
+    .total-price-box { text-align: right; font-size: 20px; font-weight: 700; margin-bottom: 30px; color: var(--text-muted); padding: 20px; background: rgba(0,0,0,0.15); border-radius: 8px; border: 1px solid var(--border-color); }
+    .total-price-box span { color: var(--primary); font-size: 28px; font-weight: 900; margin-left: 10px; }
+
+    /* --- CHECKOUT FORM --- */
+    .checkout-box { background: rgba(0,0,0,0.2); padding: 30px; border-radius: var(--border-radius); border: 1px solid var(--border-color); margin-top: 10px; }
+    .checkout-box h3 { color: var(--primary); margin-top: 0; margin-bottom: 25px; font-size: 18px; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
+    
+    .form-group { margin-bottom: 20px; }
+    .form-group label { display: block; margin-bottom: 8px; font-weight: 600; font-size: 14px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
+    .form-group input { width: 100%; padding: 14px; background: #0f172a; border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-main); font-family: 'Inter', sans-serif; font-size: 15px; outline: none; transition: 0.2s; box-sizing: border-box; }
+    .form-group input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+    
+    .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
+    
+    .btn-checkout { background: var(--primary); color: white; border: none; padding: 16px 20px; font-size: 16px; font-weight: 800; border-radius: 6px; cursor: pointer; width: 100%; text-transform: uppercase; letter-spacing: 1px; transition: 0.3s; margin-top: 10px; }
+    .btn-checkout:hover { background: var(--primary-hover); box-shadow: 0 4px 15px rgba(249, 115, 22, 0.4); transform: translateY(-1px); }
+
+    .empty-cart-msg { color: var(--text-muted); font-size: 16px; text-align: center; padding: 40px 0; }
+    .empty-cart-msg a { color: var(--primary); font-weight: bold; text-decoration: none; border-bottom: 2px solid transparent; transition: 0.2s; }
+    .empty-cart-msg a:hover { border-color: var(--primary); }
+
+    @media (max-width: 768px) {
+        .form-grid { grid-template-columns: 1fr; gap: 0; }
+        .cart-table th, .cart-table td { padding: 10px 5px; font-size: 14px; }
+        .thumb { width: 50px; height: 50px; }
+        .btn-remove { padding: 6px 10px; font-size: 11px; }
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+    <h2 class="page-title">{% if current_lang == 'en' %}Your Cart{% else %}Το Καλάθι σου{% endif %}</h2>
+
+    <div class="cart-container">
+        {% if cart_items|length > 0 %}
+            <div style="overflow-x: auto;">
+                <table class="cart-table">
+                    <thead>
+                        <tr>
+                            <th>{% if current_lang == 'en' %}Product{% else %}Προϊόν{% endif %}</th>
+                            <th>{% if current_lang == 'en' %}Price{% else %}Τιμή{% endif %}</th>
+                            <th>{% if current_lang == 'en' %}Qty{% else %}Ποσότητα{% endif %}</th>
+                            <th>{% if current_lang == 'en' %}Subtotal{% else %}Σύνολο{% endif %}</th>
+                            <th>{% if current_lang == 'en' %}Action{% else %}Ενέργεια{% endif %}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {% for item in cart_items %}
+                        <tr>
+                            <td>
+                                <div style="display: flex; align-items: center; gap: 15px;">
+                                    {% if item.image_path %}
+                                        <img src="{{ item.image_path }}" class="thumb">
+                                    {% else %}
+                                        <div class="no-img-placeholder">No IMG</div>
+                                    {% endif %}
+                                    <strong style="color: var(--text-main); font-size: 15px;">{{ item.title }}</strong>
+                                </div>
+                            </td>
+                            <td style="color: var(--text-muted); font-weight: 500;">{{ item.price|number_format(2, ',', '.') }} €</td>
+                            <td style="font-weight: bold;">{{ item.quantity }}</td>
+                            <td><strong style="color: var(--text-main); font-size: 16px;">{{ item.subtotal|number_format(2, ',', '.') }} €</strong></td>
+                            <td>
+                                <a href="cart.php?action=remove&id={{ item.id }}" class="btn-remove">
+                                    {% if current_lang == 'en' %}Remove{% else %}Αφαίρεση{% endif %}
+                                </a>
+                            </td>
+                        </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+            </div>
+
+            <div class="total-price-box">
+                {% if current_lang == 'en' %}Total Amount:{% else %}Συνολικό Ποσό:{% endif %} 
+                <span>{{ total_price|number_format(2, ',', '.') }} €</span>
+            </div>
+
+            <!-- Φόρμα Ολοκλήρωσης Παραγγελίας -->
+            <div class="checkout-box">
+                <h3>{% if current_lang == 'en' %}Shipping & Contact Details{% else %}Στοιχεία Αποστολής & Επικοινωνίας{% endif %}</h3>
+                
+                <form action="checkout.php" method="POST">
+                    <div class="form-grid">
+                        <div class="form-group">
+                            <label>{% if current_lang == 'en' %}Full Name *{% else %}Ονοματεπώνυμο *{% endif %}</label>
+                            <input type="text" name="customer_name" required>
+                        </div>
+                        <div class="form-group">
+                            <label>{% if current_lang == 'en' %}Email Address *{% else %}Email *{% endif %}</label>
+                            <input type="email" name="customer_email" required>
+                        </div>
+                    </div>
+
+                    <div class="form-grid">
+                        <div class="form-group">
+                            <label>{% if current_lang == 'en' %}Phone Number *{% else %}Τηλέφωνο Επικοινωνίας *{% endif %}</label>
+                            <input type="text" name="customer_phone" required>
+                        </div>
+                        <div class="form-group">
+                            <label>{% if current_lang == 'en' %}Shipping Address / City *{% else %}Διεύθυνση / Πόλη *{% endif %}</label>
+                            <input type="text" name="shipping_address" required>
+                        </div>
+                    </div>
+
+                    <button type="submit" class="btn-checkout">
+                        {% if current_lang == 'en' %}Place Order Now{% else %}Οριστική Υποβολή Παραγγελίας{% endif %}
+                    </button>
+                </form>
+            </div>
+
+        {% else %}
+            <div class="empty-cart-msg">
+                <p>
+                    {% if current_lang == 'en' %}
+                        Your cart is completely empty. <a href="index.php">Explore the catalog</a>.
+                    {% else %}
+                        Το καλάθι σου είναι εντελώς άδειο. <a href="index.php">Εξερεύνησε τον κατάλογο</a>.
+                    {% endif %}
+                </p>
+            </div>
+        {% endif %}
+    </div>
+{% endblock %}

+ 338 - 0
themes/default/index.html.twig

@@ -0,0 +1,338 @@
+{% extends "base.html.twig" %}
+
+{% block title %}
+    {% if current_category_name %}
+        {{ current_category_name }} - {{ site_title }}
+    {% else %}
+        {% if current_lang == 'en' %}Home{% else %}Αρχική{% endif %} - {{ site_title }}
+    {% endif %}
+{% endblock %}
+
+{% block extra_css %}
+<style>
+    /* --- HERO SECTION --- */
+    .hero { 
+        background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); 
+        padding: 60px 30px; 
+        text-align: center; 
+        border-radius: var(--border-radius); 
+        margin-bottom: 40px; 
+        border: 1px solid var(--border-color); 
+        box-shadow: 0 10px 30px rgba(0,0,0,0.3);
+        position: relative;
+        overflow: hidden;
+    }
+    .hero::before {
+        content: ''; position: absolute; top: -50px; left: -50px; width: 150px; height: 150px; background: rgba(249, 115, 22, 0.15); border-radius: 50%; filter: blur(60px); pointer-events: none;
+    }
+    
+    .hero h1 { margin-top: 0; font-size: 2.8em; color: var(--text-main); font-weight: 900; letter-spacing: -1px; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); }
+    .hero p { color: var(--text-muted); font-size: 1.2em; font-weight: 500; }
+    
+    /* --- TOP FILTER BAR --- */
+    .top-filters-bar {
+        background: var(--panel-bg);
+        padding: 20px 25px;
+        border-radius: var(--border-radius);
+        box-shadow: var(--shadow-sm);
+        margin-bottom: 35px;
+        border: 1px solid var(--border-color);
+        display: flex;
+        flex-wrap: wrap;
+        gap: 15px;
+        align-items: center;
+        justify-content: space-between;
+    }
+    
+    .filter-group { display: flex; align-items: center; gap: 10px; }
+    .filter-group label { font-size: 13px; font-weight: 700; color: var(--text-muted); white-space: nowrap; text-transform: uppercase; letter-spacing: 0.5px; }
+    
+    .filter-control {
+        padding: 10px 14px;
+        border: 1px solid var(--border-color);
+        border-radius: 6px;
+        font-family: 'Inter', sans-serif;
+        font-size: 13px;
+        background: #0f172a;
+        outline: none;
+        color: var(--text-main);
+        transition: 0.2s;
+    }
+    .filter-control:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+    
+    .price-inputs { display: flex; align-items: center; gap: 8px; }
+    .price-inputs input { width: 80px; }
+    .price-inputs span { color: var(--text-muted); font-weight: bold; }
+    
+    .btn-apply-filters {
+        padding: 10px 20px;
+        background: var(--primary);
+        color: white;
+        border: none;
+        border-radius: 6px;
+        font-weight: 800;
+        cursor: pointer;
+        font-size: 13px;
+        text-transform: uppercase;
+        letter-spacing: 1px;
+        transition: 0.2s;
+    }
+    .btn-apply-filters:hover { background: var(--primary-hover); transform: translateY(-1px); }
+    
+    .btn-clear-filters {
+        color: #fca5a5;
+        text-decoration: none;
+        font-size: 13px;
+        font-weight: 700;
+        transition: 0.2s;
+        text-transform: uppercase;
+        letter-spacing: 0.5px;
+        display: flex;
+        align-items: center;
+        gap: 4px;
+    }
+    .btn-clear-filters:hover { color: #ef4444; }
+
+    /* --- PRODUCTS GRID --- */
+    .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 25px; }
+    
+    .product-card { 
+        background: var(--panel-bg); 
+        border-radius: var(--border-radius); 
+        overflow: hidden; 
+        box-shadow: var(--shadow-sm); 
+        border: 1px solid var(--border-color);
+        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 
+        display: flex; 
+        flex-direction: column; 
+    }
+    .product-card:hover { 
+        transform: translateY(-8px); 
+        box-shadow: 0 15px 30px rgba(0,0,0,0.4); 
+        border-color: rgba(249, 115, 22, 0.5);
+    }
+    
+    .product-image-container { position: relative; width: 100%; height: 220px; background: #0f172a; overflow: hidden;}
+    .product-image { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; }
+    .product-card:hover .product-image { transform: scale(1.05); }
+    .no-img-text { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-muted); font-weight: bold; text-transform: uppercase; letter-spacing: 1px;}
+    
+    .product-info { padding: 20px; display: flex; flex-direction: column; flex-grow: 1; }
+    .product-title { margin: 0 0 12px; font-size: 1.15em; color: var(--text-main); font-weight: 700; line-height: 1.3; }
+    .product-price { font-size: 1.3em; font-weight: 900; color: var(--accent-green); margin-bottom: 15px; }
+    
+    .product-attributes { font-size: 12px; color: var(--text-muted); margin-bottom: 20px; flex-grow: 1; display: flex; flex-wrap: wrap; gap: 6px; }
+    .attr-badge { background: rgba(255,255,255,0.05); padding: 4px 10px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.1); color: #cbd5e1; font-weight: 500; }
+    .attr-badge strong { color: var(--text-main); }
+
+    .btn-view { display: block; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: var(--accent-blue); text-decoration: none; border-radius: 6px; font-size: 14px; font-weight: 800; text-align: center; margin-top: auto; text-transform: uppercase; letter-spacing: 1px; border: 1px solid rgba(59, 130, 246, 0.2); transition: 0.2s; }
+    .btn-view:hover { background: var(--accent-blue); color: white; }
+
+    .section-title { border-bottom: 2px solid var(--primary); padding-bottom: 10px; display: inline-block; margin-top: 0; color: var(--text-main); font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; }
+
+    /* --- PAGINATION STYLES --- */
+    .pagination-container { display: flex; justify-content: center; gap: 8px; margin-top: 50px; margin-bottom: 20px; flex-wrap: wrap; }
+    .page-btn { padding: 10px 16px; background: var(--panel-bg); border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; text-decoration: none; font-weight: 700; transition: 0.2s; font-size: 14px; }
+    .page-btn:hover { background: rgba(255,255,255,0.05); border-color: var(--text-muted); }
+    .page-btn.active { background: var(--primary); color: white; border-color: var(--primary); }
+    .page-dots { padding: 10px; color: var(--text-muted); font-weight: bold; pointer-events: none; }
+
+    @media (max-width: 768px) {
+        .hero h1 { font-size: 2em; }
+        .top-filters-bar { flex-direction: column; align-items: stretch; padding: 15px; }
+        .filter-group { flex-direction: column; align-items: stretch; gap: 8px; }
+        .price-inputs { width: 100%; }
+        .price-inputs input { width: 100%; }
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+    {% if search_query is defined and search_query != '' %}
+        <div style="background: rgba(59, 130, 246, 0.1); padding: 20px; border-radius: var(--border-radius); border: 1px solid rgba(59, 130, 246, 0.2); margin-bottom: 30px;">
+            <h3 style="margin-top:0; color: var(--accent-blue);">
+                {% if current_lang == 'en' %}Search results for:{% else %}Αποτελέσματα αναζήτησης για:{% endif %} "<strong>{{ search_query }}</strong>"
+            </h3>
+            <p style="margin-bottom:0; color: var(--text-main); font-weight:500;">
+                {% if current_lang == 'en' %}Found {{ total_products }} products.{% else %}Βρέθηκαν {{ total_products }} προϊόντα.{% endif %}
+            </p>
+        </div>
+    {% endif %}
+    
+    {% if current_category_name %}
+        <h2 class="section-title">
+            {{ text.category_title_prefix|default('Κατηγορία:') }} {{ current_category_name }}
+        </h2>
+        <p style="color: var(--text-muted); margin-bottom: 25px; font-size: 15px;">{{ text.category_subtitle_prefix|default('Εμφάνιση εξοπλισμού για') }} <strong>{{ current_category_name }}</strong></p>
+        
+        <!-- ΟΡΙΖΟΝΤΙΑ ΜΠΑΡΑ ΦΙΛΤΡΩΝ -->
+        <form action="index.php" method="GET" class="top-filters-bar" id="topFilterForm">
+            
+            {% if category_id %}
+                <input type="hidden" name="category_id" value="{{ category_id }}">
+            {% endif %}
+            {% if search_query %}
+                <input type="hidden" name="q" value="{{ search_query }}">
+            {% endif %}
+
+            <div style="display: flex; flex-wrap: wrap; gap: 20px; flex-grow: 1;">
+                
+                <div class="filter-group">
+                    <label>{{ text.filter_sort_label|default('Ταξινόμηση:') }}</label>
+                    <select name="sort" class="filter-control" onchange="document.getElementById('topFilterForm').submit();">
+                        <option value="newest" {% if filters.sort == 'newest' %}selected{% endif %}>
+                            {% if current_lang == 'en' %}Newest{% else %}Νεότερα{% endif %}
+                        </option>
+                        <option value="price_asc" {% if filters.sort == 'price_asc' %}selected{% endif %}>
+                            {% if current_lang == 'en' %}Price: Low to High{% else %}Φθηνότερα{% endif %}
+                        </option>
+                        <option value="price_desc" {% if filters.sort == 'price_desc' %}selected{% endif %}>
+                            {% if current_lang == 'en' %}Price: High to Low{% else %}Ακριβότερα{% endif %}
+                        </option>
+                    </select>
+                </div>
+
+                <div class="filter-group">
+                    <label>{{ text.filter_price_label|default('Τιμή (€):') }}</label>
+                    <div class="price-inputs">
+                        <input type="number" name="min_price" class="filter-control" placeholder="{{ text.filter_price_from|default('Από') }}" value="{{ filters.min_price }}">
+                        <span>-</span>
+                        <input type="number" name="max_price" class="filter-control" placeholder="{{ text.filter_price_to|default('Έως') }}" value="{{ filters.max_price }}">
+                    </div>
+                </div>
+
+                {% if category_attributes %}
+                    {% for attr in category_attributes %}
+                        {% set attr_values = attr.distinct_values|split('||') %}
+                        
+                        {% if attr_values|length > 0 and attr.distinct_values != '' %}
+                        <div class="filter-group">
+                            <label>{{ attr.display_name|default(attr.name_el) }}:</label>
+                            <select name="attr[{{ attr.id }}]" class="filter-control" onchange="document.getElementById('topFilterForm').submit();">
+                                <option value="">{% if current_lang == 'en' %}All{% else %}Όλα{% endif %}</option>
+                                {% for val in attr_values %}
+                                    <option value="{{ val }}" {% if filters.attr[attr.id] == val %}selected{% endif %}>{{ val }}{{ attr.unit ? ' ' ~ attr.unit : '' }}</option>
+                                {% endfor %}
+                            </select>
+                        </div>
+                        {% endif %}
+                    {% endfor %}
+                {% endif %}
+
+                <!-- ΝΕΟ ΦΙΛΤΡΟ: ΕΜΦΑΝΙΣΗ ΑΝΑ ΣΕΛΙΔΑ -->
+                <div class="filter-group">
+                    <label>{% if current_lang == 'en' %}Show:{% else %}Ανά σελίδα:{% endif %}</label>
+                    <select name="limit" class="filter-control" onchange="document.getElementById('topFilterForm').submit();">
+                        <option value="30" {% if limit == 30 %}selected{% endif %}>30</option>
+                        <option value="50" {% if limit == 50 %}selected{% endif %}>50</option>
+                        <option value="100" {% if limit == 100 %}selected{% endif %}>100</option>
+                    </select>
+                </div>
+
+            </div>
+
+            <div style="display: flex; align-items: center; gap: 20px;">
+                <button type="submit" class="btn-apply-filters">
+                    {% if current_lang == 'en' %}Apply{% else %}Εφαρμογή{% endif %}
+                </button>
+                
+                {% if filters.sort or filters.min_price or filters.max_price or filters.attr %}
+                    <a href="index.php?{% if category_id %}category_id={{ category_id }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}" class="btn-clear-filters">
+                        ✖ {% if current_lang == 'en' %}Clear Filters{% else %}Καθαρισμός{% endif %}
+                    </a>
+                {% endif %}
+            </div>
+            
+        </form>
+
+    {% elseif search_query is empty %}
+        <div class="hero">
+            <h1>{{ text.site_slogan|default('Βρες τον καλύτερο εξοπλισμό Χόκεϊ!') }}</h1>
+            <p>{{ text.site_sub_slogan|default('Αγορά και πώληση καινούργιου & μεταχειρισμένου εξοπλισμού.') }}</p>
+        </div>
+
+        <h2 class="section-title">{{ text.title_new_arrivals|default('Νέες Αφίξεις') }}</h2>
+    {% endif %}
+
+    <div class="products-grid">
+        {% for product in products %}
+            <div class="product-card">
+                <div class="product-image-container">
+                    {% if product.image_path %}
+                        <img src="{{ product.image_path }}" class="product-image" alt="{{ product.title }}">
+                    {% else %}
+                        <div class="no-img-text">No Image</div>
+                    {% endif %}
+                </div>
+                
+                <div class="product-info">
+                    <h3 class="product-title">{{ product.title }}</h3>
+                    
+                    {% if product.sku %}
+                        <div style="font-size: 12px; color: var(--text-muted); margin-bottom: 8px; font-weight: 600;">
+                            SKU: <span style="color: var(--text-main);">{{ product.sku }}</span>
+                        </div>
+                    {% endif %}
+
+                    <div class="product-price">{{ product.price|number_format(2, ',', '.') }} €</div>
+                    
+                    <div class="product-attributes">
+                        {% if product.attributes is not empty %}
+                            {% for attr in product.attributes %}
+                                <span class="attr-badge">
+                                    <strong>{{ attr.name }}:</strong> {{ attr.value }}{{ attr.unit ? ' ' ~ attr.unit : '' }}
+                                </span>
+                            {% endfor %}
+                        {% endif %}
+                    </div>
+                    
+                    <a href="product.php?id={{ product.id }}" class="btn-view">{{ text.btn_view_product|default('Προβολή') }}</a>
+                </div>
+            </div>
+        {% else %}
+            <div style="grid-column: 1 / -1; background: var(--panel-bg); padding: 40px; text-align: center; border-radius: var(--border-radius); border: 1px dashed var(--border-color); color: var(--text-muted); font-size: 16px;">
+                {% if current_lang == 'en' %}
+                    No products found in this category.
+                {% else %}
+                    Δεν βρέθηκαν διαθέσιμα προϊόντα.
+                {% endif %}
+            </div>
+        {% endfor %}
+    </div>
+
+    <!-- ΣΕΛΙΔΟΠΟΙΗΣΗ ΣΤΟ ΚΑΤΩ ΜΕΡΟΣ (PAGINATION) -->
+    {% if total_pages > 1 %}
+        <div class="pagination-container">
+            {% if current_page > 1 %}
+                <a href="{{ pagination_url }}{{ current_page - 1 }}" class="page-btn">
+                    &laquo; {% if current_lang == 'en' %}Prev{% else %}Προηγ.{% endif %}
+                </a>
+            {% endif %}
+
+            {# Έξυπνη εμφάνιση σελίδων (κρύβει τις ενδιάμεσες αν είναι πάνω από 5) #}
+            {% set start_page = max(1, current_page - 2) %}
+            {% set end_page = min(total_pages, current_page + 2) %}
+
+            {% if start_page > 1 %}
+                <a href="{{ pagination_url }}1" class="page-btn">1</a>
+                {% if start_page > 2 %}<span class="page-dots">...</span>{% endif %}
+            {% endif %}
+
+            {% for p in start_page..end_page %}
+                <a href="{{ pagination_url }}{{ p }}" class="page-btn {% if p == current_page %}active{% endif %}">{{ p }}</a>
+            {% endfor %}
+
+            {% if end_page < total_pages %}
+                {% if end_page < total_pages - 1 %}<span class="page-dots">...</span>{% endif %}
+                <a href="{{ pagination_url }}{{ total_pages }}" class="page-btn">{{ total_pages }}</a>
+            {% endif %}
+
+            {% if current_page < total_pages %}
+                <a href="{{ pagination_url }}{{ current_page + 1 }}" class="page-btn">
+                    {% if current_lang == 'en' %}Next{% else %}Επόμ.{% endif %} &raquo;
+                </a>
+            {% endif %}
+        </div>
+    {% endif %}
+
+{% endblock %}

+ 76 - 0
themes/default/login.html.twig

@@ -0,0 +1,76 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ t.page_title }}{% endblock %}
+
+{% block content %}
+<style>
+    /* Κρύβουμε τυχόν sidebar, λίστες κατηγοριών ή αριστερά κουτιά στην οθόνη login */
+    aside, .categories-sidebar, .sidebar, [class*="category"] {
+        display: none !important;
+    }
+    /* Κάνουμε το κυρίως πεδίο να καταλάβει όλο το πλάτος */
+    .main-content, .content, main {
+        width: 100% !important;
+        grid-column: 1 / -1 !important;
+        max-width: 100% !important;
+    }
+</style>
+
+<div style="display: flex; align-items: center; justify-content: center; min-height: 70vh; padding: 40px 20px; width: 100%;">
+    <div style="background: var(--panel-bg, #1e293b); padding: 40px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); border: 1px solid var(--border-color, #334155); width: 100%; max-width: 400px; box-sizing: border-box; text-align: center;">
+        
+        <h1 style="margin-top: 0; color: var(--primary, #f97316); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 5px;">
+            {{ t.heading }}
+        </h1>
+        <p style="color: var(--text-muted, #94a3b8); font-size: 14px; margin-bottom: 30px;">
+            {{ t.subheading }}
+        </p>
+
+        {% if error %}
+            <div style="padding: 12px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 13px; background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2);">
+                {{ error }}
+            </div>
+        {% endif %}
+
+        <form method="POST" action="login.php">
+            <div style="margin-bottom: 20px; text-align: left;">
+                <label style="display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted, #94a3b8); text-transform: uppercase; letter-spacing: 0.5px;">
+                    {{ t.username }}
+                </label>
+                <input type="text" name="username" required autofocus style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; font-size: 14px; outline: none;">
+            </div>
+            
+            <div style="margin-bottom: 20px; text-align: left;">
+                <label style="display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted, #94a3b8); text-transform: uppercase; letter-spacing: 0.5px;">
+                    {{ t.password }}
+                </label>
+                <input type="password" name="password" required style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; font-size: 14px; outline: none;">
+            </div>
+
+            <button type="submit" style="width: 100%; padding: 14px; background: var(--primary, #f97316); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; font-size: 14px; text-transform: uppercase; transition: 0.2s; margin-bottom: 15px;">
+                {{ t.btn_login }}
+            </button>
+        </form>
+
+        <div style="display: flex; align-items: center; text-align: center; margin: 20px 0; color: var(--text-muted, #94a3b8); font-size: 13px; font-weight: 600; text-transform: uppercase;">
+            <span style="flex: 1; border-bottom: 1px solid var(--border-color, #334155); margin-right: .5em;"></span> OR <span style="flex: 1; border-bottom: 1px solid var(--border-color, #334155); margin-left: .5em;"></span>
+        </div>
+
+        <!-- Σύνδεση με Google -->
+        <a href="google_auth.php" style="display: flex; align-items: center; justify-content: center; width: 100%; padding: 12px; background: #ffffff; color: #3c4043; border: 1px solid #dadce0; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 14px; text-decoration: none; box-sizing: border-box;">
+            <img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" alt="Google Logo" style="width: 18px; height: 18px; margin-right: 10px;">
+            {{ t.btn_google }}
+        </a>
+
+        <div style="margin-top: 20px; font-size: 14px; color: var(--text-muted, #94a3b8);">
+            {{ t.register_text }} <a href="register.php" style="color: var(--primary, #f97316); text-decoration: none; font-weight: 700;">{{ t.register_link }}</a>
+        </div>
+
+        <!-- Εναλλαγή Γλώσσας -->
+        <div style="margin-top: 25px;">
+            <a href="?lang=el" style="text-decoration: none; font-size: 12px; font-weight: 700; padding: 6px 12px; border-radius: 20px; background: {% if current_lang == 'el' %}var(--primary, #f97316){% else %}rgba(255,255,255,0.05){% endif %}; color: {% if current_lang == 'el' %}white{% else %}var(--text-muted, #94a3b8){% endif %}; border: 1px solid var(--border-color, #334155); margin: 0 4px;">GR</a>
+            <a href="?lang=en" style="text-decoration: none; font-size: 12px; font-weight: 700; padding: 6px 12px; border-radius: 20px; background: {% if current_lang == 'en' %}var(--primary, #f97316){% else %}rgba(255,255,255,0.05){% endif %}; color: {% if current_lang == 'en' %}white{% else %}var(--text-muted, #94a3b8){% endif %}; border: 1px solid var(--border-color, #334155); margin: 0 4px;">EN</a>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 130 - 0
themes/default/product.html.twig

@@ -0,0 +1,130 @@
+{% extends "base.html.twig" %}
+
+{% block title %}{{ product.title }} - {{ site_title }}{% endblock %}
+
+{% block extra_css %}
+<style>
+    /* --- PRODUCT PAGE (DARK THEME) --- */
+    .back-link { color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 8px; margin-bottom: 20px; font-weight: 600; font-size: 14px; transition: 0.2s; text-transform: uppercase; letter-spacing: 0.5px; }
+    .back-link:hover { color: var(--primary); transform: translateX(-5px); }
+
+    .product-container { display: flex; flex-wrap: wrap; gap: 40px; background: var(--panel-bg); padding: 35px; border-radius: var(--border-radius); box-shadow: var(--shadow-lg); border: 1px solid var(--border-color); margin-top: 10px; }
+    
+    .product-image-wrap { flex: 1; min-width: 320px; }
+    .product-image-wrap img { width: 100%; border-radius: 8px; border: 1px solid var(--border-color); object-fit: cover; box-shadow: var(--shadow-sm); }
+    .no-image-box { width: 100%; height: 400px; background: #0f172a; display: flex; align-items: center; justify-content: center; border-radius: 8px; color: var(--text-muted); font-weight: 800; text-transform: uppercase; letter-spacing: 1px; border: 1px dashed var(--border-color); }
+    
+    .product-details { flex: 1.5; min-width: 320px; display: flex; flex-direction: column; }
+    .product-details h1 { margin-top: 0; color: var(--text-main); font-size: 2.2em; font-weight: 900; line-height: 1.2; letter-spacing: -0.5px; }
+    
+    .product-meta { color: var(--text-muted); margin-bottom: 25px; font-size: 14px; padding-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.05); display: flex; flex-wrap: wrap; gap: 15px; }
+    .product-meta span { display: flex; align-items: center; gap: 5px; }
+    .product-meta strong { color: var(--text-main); font-weight: 700; }
+    
+    .product-price { font-size: 3em; color: var(--accent-green); font-weight: 900; margin: 10px 0 25px; text-shadow: 0 2px 10px rgba(16, 185, 129, 0.2); }
+    
+    /* ADD TO CART FORM */
+    .add-to-cart-form { background: rgba(0,0,0,0.15); padding: 25px; border-radius: 8px; border: 1px solid var(--border-color); margin-bottom: 30px; }
+    .quantity-wrap { display: flex; gap: 15px; align-items: center; margin-bottom: 20px; }
+    .quantity-wrap label { font-weight: 700; color: var(--text-muted); text-transform: uppercase; font-size: 13px; letter-spacing: 0.5px; }
+    .quantity-input { width: 80px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; background: #0f172a; color: var(--text-main); font-size: 16px; font-weight: bold; text-align: center; outline: none; transition: 0.2s; }
+    .quantity-input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+    
+    .buy-btn { display: block; width: 100%; text-align: center; background: var(--primary); color: white; padding: 16px; border-radius: 6px; border: none; font-size: 16px; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; box-sizing: border-box; }
+    .buy-btn:hover { background: var(--primary-hover); box-shadow: 0 4px 15px rgba(249, 115, 22, 0.4); transform: translateY(-2px); }
+    
+    /* ATTRIBUTES TABLE */
+    .attributes-table { width: 100%; border-collapse: collapse; margin-top: 20px; border-radius: 8px; overflow: hidden; border: 1px solid var(--border-color); }
+    .attributes-table th, .attributes-table td { padding: 14px 18px; border-bottom: 1px solid rgba(255,255,255,0.03); text-align: left; }
+    .attributes-table tr:last-child th, .attributes-table tr:last-child td { border-bottom: none; }
+    .attributes-table th { width: 35%; background: rgba(0,0,0,0.3); color: var(--text-muted); font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700; }
+    .attributes-table td { background: rgba(0,0,0,0.1); color: var(--text-main); font-size: 15px; }
+    
+    /* DESCRIPTION */
+    .description-box { margin-top: 35px; white-space: pre-wrap; line-height: 1.7; color: #cbd5e1; background: rgba(0,0,0,0.2); padding: 25px; border-radius: 8px; border: 1px solid var(--border-color); font-size: 15px; }
+    .description-box h3 { margin-top: 0; margin-bottom: 15px; font-size: 16px; color: var(--primary); text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
+
+    @media (max-width: 768px) {
+        .product-container { padding: 20px; gap: 25px; }
+        .product-details h1 { font-size: 1.8em; }
+        .product-price { font-size: 2.2em; }
+        .attributes-table th { width: 45%; font-size: 12px; padding: 12px; }
+        .attributes-table td { padding: 12px; font-size: 14px; }
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+    <a href="index.php" class="back-link">
+        &larr; {% if current_lang == 'en' %}Back to catalog{% else %}Πίσω στον κατάλογο{% endif %}
+    </a>
+
+    <div class="product-container">
+        <!-- Φωτογραφία -->
+        <div class="product-image-wrap">
+            {% if product.image_path %}
+                <img src="{{ product.image_path }}" alt="{{ product.title }}">
+            {% else %}
+                <div class="no-image-box">
+                    {% if current_lang == 'en' %}No Image{% else %}Χωρίς Φωτογραφία{% endif %}
+                </div>
+            {% endif %}
+        </div>
+        
+        <!-- Λεπτομέρειες -->
+        <div class="product-details">
+            <h1 style="margin-bottom: 10px;">{{ product.title }}</h1>
+            
+            {% if product.sku %}
+                <div style="font-size: 14px; color: var(--text-muted); margin-bottom: 20px; display: flex; align-items: center; gap: 8px;">
+                    <span style="background: rgba(255,255,255,0.05); border: 1px solid var(--border-color); padding: 4px 10px; border-radius: 6px;">
+                        SKU: <strong style="color: var(--text-main); letter-spacing: 0.5px;">{{ product.sku }}</strong>
+                    </span>
+                </div>
+            {% endif %}
+            
+            <div class="product-meta">
+                <span>{% if current_lang == 'en' %}Category:{% else %}Κατηγορία:{% endif %} <strong>{{ product.cat_name }}</strong></span>
+                <span style="color: var(--border-color);">|</span> 
+                <span>{% if current_lang == 'en' %}Seller:{% else %}Πωλητής:{% endif %} <strong>{{ product.seller_name }}</strong></span>
+                <span style="color: var(--border-color);">|</span>
+                <span>{% if current_lang == 'en' %}Listed:{% else %}Αναρτήθηκε:{% endif %} <strong>{{ product.created_at|date("d/m/Y") }}</strong></span>
+            </div>
+            
+            <div class="product-price">{{ product.price|number_format(2, ',', '.') }} €</div>
+            
+            <form action="cart.php?action=add" method="POST" class="add-to-cart-form">
+                <input type="hidden" name="product_id" value="{{ product.id }}">
+                <div class="quantity-wrap">
+                    <label for="quantity">{% if current_lang == 'en' %}Quantity:{% else %}Ποσότητα:{% endif %}</label>
+                    <input type="number" id="quantity" name="quantity" class="quantity-input" value="1" min="1" max="{{ product.quantity }}">
+                </div>
+                <button type="submit" class="buy-btn">
+                    {% if current_lang == 'en' %}Add to Cart{% else %}Προσθήκη στο Καλάθι{% endif %}
+                </button>
+            </form>
+            
+            <!-- Δυναμικά Χαρακτηριστικά -->
+            {% if attributes|length > 0 %}
+                <table class="attributes-table">
+                    <tbody>
+                        {% for attr in attributes %}
+                            <tr>
+                                <th>{{ attr.name }}</th>
+                                <td><strong>{{ attr.value }}</strong> {{ attr.unit }}</td>
+                            </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+            {% endif %}
+            
+            <!-- Περιγραφή -->
+            {% if product.description %}
+                <div class="description-box">
+                    <h3>{% if current_lang == 'en' %}Equipment Description{% else %}Περιγραφή Εξοπλισμού{% endif %}</h3>
+                    {{ product.description }}
+                </div>
+            {% endif %}
+        </div>
+    </div>
+{% endblock %}

+ 84 - 0
themes/default/register.html.twig

@@ -0,0 +1,84 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ t.title }} - Hockey CMS{% endblock %}
+
+{% block content %}
+<style>
+    /* Κρύβουμε sidebar και κατηγορίες */
+    aside, .categories-sidebar, .sidebar, [class*="category"] {
+        display: none !important;
+    }
+    .main-content, .content, main {
+        width: 100% !important;
+        grid-column: 1 / -1 !important;
+        max-width: 100% !important;
+    }
+</style>
+
+<div style="display: flex; align-items: center; justify-content: center; min-height: 80vh; padding: 40px 20px; width: 100%;">
+    <div style="background: var(--panel-bg, #1e293b); padding: 40px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); border: 1px solid var(--border-color, #334155); width: 100%; max-width: 420px; box-sizing: border-box;">
+        
+        <h2 style="margin-top: 0; color: var(--primary, #f97316); text-align: center; margin-bottom: 25px; text-transform: uppercase; letter-spacing: 0.5px;">
+            {{ t.title }}
+        </h2>
+
+        {{ message | raw }}
+        
+        <form method="POST" action="register.php">
+            <!-- Όνομα και Επίθετο το ένα κάτω από το άλλο -->
+            <div style="margin-bottom: 15px;">
+                <input type="text" name="reg_first_name" placeholder="{{ t.fname }}" required style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box;">
+            </div>
+            
+            <div style="margin-bottom: 15px;">
+                <input type="text" name="reg_last_name" placeholder="{{ t.lname }}" required style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box;">
+            </div>
+            
+            <div style="margin-bottom: 15px;">
+                <input type="text" name="reg_username" placeholder="{{ t.username }}" required style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box;">
+            </div>
+            <div style="margin-bottom: 15px;">
+                <input type="email" name="reg_email" placeholder="{{ t.email }}" required style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box;">
+            </div>
+            <div style="margin-bottom: 15px;">
+                <input type="tel" name="reg_phone" placeholder="{{ t.phone }}" style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box;">
+            </div>
+            <div style="margin-bottom: 15px;">
+                <input type="password" name="reg_password" placeholder="{{ t.password }}" required style="width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box;">
+            </div>
+            
+            <!-- CAPTCHA Box -->
+            <div style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px dashed var(--border-color, #334155); text-align: center;">
+                <label style="font-weight: 600; color: var(--text-muted, #94a3b8); display: block; margin-bottom: 10px; font-size: 14px;">
+                    {{ t.captcha_label }} <strong style="color: var(--text-main, #f8fafc); font-size: 18px;">{{ captcha_text }}</strong> ? *
+                </label>
+                <input type="number" name="reg_captcha" placeholder="{{ t.captcha_ph }}" required style="width: 120px; padding: 10px; background: #0f172a; border: 1px solid var(--border-color, #334155); color: var(--text-main, #f8fafc); border-radius: 6px; text-align: center; font-size: 16px; font-weight: bold; outline: none;">
+            </div>
+            
+            <button type="submit" style="width: 100%; padding: 14px; background: var(--primary, #f97316); color: white; border: none; border-radius: 6px; font-weight: 800; cursor: pointer; font-size: 14px; text-transform: uppercase; transition: 0.2s; margin-bottom: 15px;">
+                {{ t.btn_register }}
+            </button>
+        </form>
+
+        <div style="display: flex; align-items: center; text-align: center; margin: 20px 0; color: var(--text-muted, #94a3b8); font-size: 13px; font-weight: 600; text-transform: uppercase;">
+            <span style="flex: 1; border-bottom: 1px solid var(--border-color, #334155); margin-right: .5em;"></span> OR <span style="flex: 1; border-bottom: 1px solid var(--border-color, #334155); margin-left: .5em;"></span>
+        </div>
+
+        <!-- Εγγραφή με Google -->
+        <a href="google_auth.php" style="display: flex; align-items: center; justify-content: center; width: 100%; padding: 12px; background: #ffffff; color: #3c4043; border: 1px solid #dadce0; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 14px; text-decoration: none; box-sizing: border-box; margin-bottom: 15px;">
+            <img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" alt="Google Logo" style="width: 18px; height: 18px; margin-right: 10px;">
+            {{ t.btn_google }}
+        </a>
+        
+        <div style="text-align: center; margin-top: 20px; font-size: 14px; color: var(--text-muted, #94a3b8);">
+            {{ t.already_have }} <a href="login.php" style="color: var(--primary, #f97316); text-decoration: none; font-weight: 700;">{{ t.login }}</a>
+        </div>
+
+        <!-- Εναλλαγή Γλώσσας -->
+        <div style="text-align: center; margin-top: 25px;">
+            <a href="?lang=el" style="text-decoration: none; font-size: 12px; font-weight: 700; padding: 6px 12px; border-radius: 20px; background: {% if current_lang == 'el' %}var(--primary, #f97316){% else %}rgba(255,255,255,0.05){% endif %}; color: {% if current_lang == 'el' %}white{% else %}var(--text-muted, #94a3b8){% endif %}; border: 1px solid var(--border-color, #334155); margin: 0 4px;">GR</a>
+            <a href="?lang=en" style="text-decoration: none; font-size: 12px; font-weight: 700; padding: 6px 12px; border-radius: 20px; background: {% if current_lang == 'en' %}var(--primary, #f97316){% else %}rgba(255,255,255,0.05){% endif %}; color: {% if current_lang == 'en' %}white{% else %}var(--text-muted, #94a3b8){% endif %}; border: 1px solid var(--border-color, #334155); margin: 0 4px;">EN</a>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 335 - 0
themes/default/seller_dashboard.html.twig

@@ -0,0 +1,335 @@
+{% extends "base.html.twig" %}
+
+{% block title %}{{ t.seller_panel }} - {{ site_title }}{% endblock %}
+
+{% block extra_css %}
+<style>
+    /* --- SELLER DASHBOARD STYLES --- */
+    .dashboard-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; border-bottom: 2px solid var(--border-color); padding-bottom: 15px; }
+    .dashboard-header h2 { margin: 0; color: var(--primary); font-size: 24px; text-transform: uppercase; letter-spacing: 1px; font-weight: 800; display: flex; align-items: center; gap: 10px; }
+    
+    .dashboard-nav { display: flex; gap: 10px; }
+    .nav-btn { padding: 10px 15px; border-radius: 6px; text-decoration: none; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: 0.2s; border: 1px solid transparent; }
+    .nav-btn.active { background: rgba(249, 115, 22, 0.1); color: var(--primary); border-color: rgba(249, 115, 22, 0.2); }
+    .nav-btn:not(.active) { background: rgba(255,255,255,0.05); color: var(--text-main); border-color: var(--border-color); }
+    .nav-btn:hover { background: rgba(255,255,255,0.1); }
+
+    .dashboard-grid { display: grid; grid-template-columns: 1.2fr 2fr; gap: 30px; }
+    
+    .panel-card { background: var(--panel-bg); padding: 30px; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 1px solid var(--border-color); }
+    .panel-card h3 { margin-top: 0; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; color: var(--text-main); font-size: 18px; text-transform: uppercase; letter-spacing: 1px; }
+    
+    .form-group { margin-bottom: 20px; }
+    .form-group label { display: block; margin-bottom: 8px; font-weight: 700; font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;}
+    .form-group input[type="text"], .form-group input[type="number"], .form-group select, .form-group textarea { width: 100%; padding: 12px; background: #0f172a; border: 1px solid var(--border-color); border-radius: 6px; box-sizing: border-box; font-family: 'Inter', sans-serif; color: var(--text-main); outline: none; transition: 0.2s; }
+    .form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
+    .form-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
+    
+    .btn-submit { padding: 14px 20px; background: var(--primary); color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: 800; width: 100%; font-size: 15px; text-transform: uppercase; letter-spacing: 1px; transition: 0.2s; }
+    .btn-submit:hover { background: var(--primary-hover); transform: translateY(-1px); box-shadow: 0 4px 15px rgba(249, 115, 22, 0.3); }
+    
+    .btn-cancel { background: rgba(255,255,255,0.05); border: 1px solid var(--border-color); color: var(--text-main); padding: 14px 20px; border-radius: 6px; display: block; text-align: center; font-weight: 700; margin-top: 10px; text-decoration: none; text-transform: uppercase; transition: 0.2s;}
+    .btn-cancel:hover { background: rgba(255,255,255,0.1); }
+    
+    .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
+    .success { background: rgba(16, 185, 129, 0.1); color: var(--accent-green); border: 1px solid rgba(16, 185, 129, 0.2); }
+    .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+    
+    .tags-input-container { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px 10px; border: 1px solid var(--border-color); border-radius: 6px; background: #0f172a; align-items: center; cursor: text; }
+    .tags-input-container input { border: none; outline: none; flex-grow: 1; padding: 4px; min-width: 150px; font-family: 'Inter', sans-serif; font-size: 14px; background: transparent; color: var(--text-main); }
+    .tag-chip { background: rgba(255,255,255,0.1); color: var(--text-main); padding: 4px 10px; border-radius: 16px; font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 6px; border: 1px solid rgba(255,255,255,0.05);}
+    .tag-chip span { cursor: pointer; color: var(--text-muted); font-weight: bold; font-size: 15px; line-height: 1; transition: 0.2s;}
+    .tag-chip span:hover { color: #ef4444; }
+
+    /* LISTINGS */
+    .prod-list { list-style: none; padding: 0; margin: 0; }
+    .prod-item { display: flex; gap: 15px; align-items: center; padding: 20px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
+    .prod-item:last-child { border-bottom: none; }
+    .prod-info-wrap { display: flex; gap: 15px; flex-grow: 1; align-items: center; }
+    .prod-thumb { width: 75px; height: 75px; object-fit: cover; border-radius: 6px; background: #0f172a; flex-shrink: 0; border: 1px solid var(--border-color); }
+    
+    .prod-info { flex-grow: 1; min-width: 0; }
+    .prod-info h4 { margin: 0 0 4px 0; color: var(--text-main); font-size: 16px; font-weight: 700;}
+    .prod-info p { margin: 0; font-size: 13px; color: var(--text-muted); }
+    .prod-info p strong { color: var(--accent-green); font-size: 15px;}
+    
+    .prod-info .tags-display { font-size: 11px; margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
+    .prod-info .tags-display span { background: rgba(59, 130, 246, 0.1); color: var(--accent-blue); border-radius: 12px; padding: 3px 8px; border: 1px solid rgba(59, 130, 246, 0.2); font-weight: 600;}
+    
+    .badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 800; color: #fff; text-transform: uppercase; display: inline-block; margin-top: 8px; letter-spacing: 0.5px;}
+    .badge-available { background: var(--accent-green); }
+    .badge-sold { background: #ef4444; }
+    .badge-hidden { background: var(--text-muted); color: #0b1120; }
+
+    .actions { display: flex; gap: 8px; flex-shrink: 0; }
+    .act-btn { padding: 8px 15px; border-radius: 6px; font-size: 12px; text-decoration: none; font-weight: 700; cursor: pointer; border: none; text-align: center; text-transform: uppercase; transition: 0.2s;}
+    .act-edit { background: rgba(249, 115, 22, 0.1); color: var(--primary); border: 1px solid rgba(249, 115, 22, 0.2); }
+    .act-edit:hover { background: var(--primary); color: white; }
+    .act-del { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+    .act-del:hover { background: #ef4444; color: white; }
+
+    @media (max-width: 992px) {
+        .dashboard-grid { grid-template-columns: 1fr; }
+        .dashboard-header { flex-direction: column; gap: 15px; text-align: center; }
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+    <div class="dashboard-header">
+        <h2>🏪 {{ t.seller_panel }}</h2>
+        <div class="dashboard-nav">
+            <a href="seller_dashboard.php" class="nav-btn active">{{ t.my_listings }}</a>
+            <a href="seller_orders.php" class="nav-btn">{{ t.customer_orders }}</a>
+        </div>
+    </div>
+
+    <div class="dashboard-grid">
+        <!-- ΑΡΙΣΤΕΡΑ: Φόρμα Ανάρτησης -->
+        <div class="panel-card">
+            <h3>{% if edit_product %}{{ t.edit_listing }}{% else %}{{ t.add_listing }}{% endif %}</h3>
+            
+            {% if message %}
+                {{ message|raw }}
+            {% endif %}
+            
+            <form method="POST" enctype="multipart/form-data">
+                <input type="hidden" name="action" value="{% if edit_product %}edit_product{% else %}add_product{% endif %}">
+                {% if edit_product %}
+                    <input type="hidden" name="product_id" value="{{ edit_product.id }}">
+                {% endif %}
+                
+                <div class="form-group">
+                    <label>{{ t.title }}</label>
+                    <input type="text" name="title" value="{{ edit_product.title|default('') }}" required>
+                </div>
+
+                <!-- ΝΕΟ ΠΕΔΙΟ: SKU / Αύξων Αριθμός -->
+                <div class="form-group">
+                    <label>{{ t.sku|default('Κωδικός / Αύξων Αριθμός (SKU)') }}</label>
+                    <input type="text" name="sku" value="{{ edit_product.sku|default('') }}" placeholder="π.χ. 001, 002...">
+                </div>
+
+                <div class="form-group">
+                    <label>{{ t.category }}</label>
+                    <select name="category_id" id="category_select" required onchange="loadAttributes(this.value); generateSku(this.value);">
+                        <option value="">{{ t.select_cat }}</option>
+                        {% for cat in categories %}
+                            <option value="{{ cat.id }}" {% if edit_product and edit_product.category_id == cat.id %}selected{% endif %}>
+                                {{ cat.display_name }}
+                            </option>
+                        {% endfor %}
+                    </select>
+                </div>
+
+                <div class="form-cols">
+                    <div class="form-group">
+                        <label>{{ t.price }}</label>
+                        <input type="number" step="0.01" name="price" value="{{ edit_product.price|default('') }}" required>
+                    </div>
+                    <div class="form-group">
+                        <label>{{ t.stock }}</label>
+                        <input type="number" name="quantity" value="{{ edit_product.quantity|default(1) }}" min="1" required>
+                    </div>
+                </div>
+                
+                <div class="form-group">
+                    <label>{{ t.tags }}</label>
+                    <div class="tags-input-container" id="tags_container">
+                        <input type="text" id="tag_input_field" placeholder="{{ t.tags_placeholder }}">
+                    </div>
+                    <input type="hidden" name="search_tags" id="hidden_search_tags" value="{{ edit_product.search_tags|default('') }}">
+                </div>
+
+                <div id="dynamic-attributes-container" style="background: rgba(0,0,0,0.1); padding: 5px 20px; border-radius: 8px; border: 1px solid var(--border-color); margin-bottom: 20px; display: none;"></div>
+
+                <div class="form-group">
+                    <label>{{ t.description }}</label>
+                    <textarea name="description" rows="4">{{ edit_product.description|default('') }}</textarea>
+                </div>
+
+                <div class="form-group">
+                    <label>{{ t.listing_status }}</label>
+                    <select name="status">
+                        <option value="available" {% if edit_product and edit_product.status == 'available' %}selected{% endif %}>{{ t.status_opt_available }}</option>
+                        <option value="hidden" {% if edit_product and edit_product.status == 'hidden' %}selected{% endif %}>{{ t.status_opt_hidden }}</option>
+                        <option value="sold" {% if edit_product and edit_product.status == 'sold' %}selected{% endif %}>{{ t.status_opt_sold }}</option>
+                    </select>
+                </div>
+
+                <div class="form-group" style="background: rgba(0,0,0,0.15); padding: 15px; border-radius: 6px; border: 1px dashed var(--border-color);">
+                    <label style="color: var(--text-main); margin-bottom: 10px;">{{ t.photo }} <span style="color: var(--text-muted); font-size: 11px;">{% if edit_product %}{{ t.photo_opt }}{% else %}{{ t.photo_req }}{% endif %}</span></label>
+                    <input type="file" name="image" accept="image/jpeg, image/png, image/gif" style="background: transparent; border: none; padding: 0;">
+                </div>
+                
+                <button type="submit" class="btn-submit">{% if edit_product %}{{ t.btn_save }}{% else %}{{ t.btn_upload }}{% endif %}</button>
+                
+                {% if edit_product %}
+                    <a href="seller_dashboard.php" class="btn-cancel">{{ t.btn_cancel }}</a>
+                {% endif %}
+            </form>
+        </div>
+
+        <!-- ΔΕΞΙΑ: Οι αγγελίες μου -->
+        <div class="panel-card">
+            <h3>{{ t.my_listings|default('Οι Αγγελίες Μου') }} ({{ my_products|length }})</h3>
+            
+            {% if my_products|length > 0 %}
+                <ul class="prod-list">
+                    {% for prod in my_products %}
+                    {% set disp_cat = (current_lang == 'en' and prod.cat_name_en) ? prod.cat_name_en : prod.cat_name_el %}
+                    
+                    <li class="prod-item">
+                        <div class="prod-info-wrap">
+                            {% if prod.image_path %}
+                                <img src="{{ prod.image_path }}" class="prod-thumb">
+                            {% else %}
+                                <div class="prod-thumb" style="display:flex; align-items:center; justify-content:center; font-size:11px; color:var(--text-muted); font-weight: bold;">{{ t.no_img }}</div>
+                            {% endif %}
+                            
+                            <div class="prod-info">
+                                <h4>
+                                    {{ prod.title }}
+                                </h4>
+                                {% if prod.sku %}
+                                    <p style="margin-bottom: 4px; font-size: 12px; color: var(--text-muted);">
+                                        {{ t.sku|default('SKU') }}: <strong style="color: var(--text-main); font-size: 12px;">{{ prod.sku }}</strong>
+                                    </p>
+                                {% endif %}
+                                <p>{{ disp_cat }} | <strong>{{ prod.price }} €</strong> | {{ t.stock_short }} {{ prod.quantity }}</p>
+                                
+                                {% if prod.search_tags %}
+                                    <div class="tags-display">
+                                        {% for tag in prod.search_tags|split(',') %}
+                                            <span>{{ tag|trim }}</span>
+                                        {% endfor %}
+                                    </div>
+                                {% endif %}
+                                
+                                {% set status_class = 'badge-available' %}
+                                {% set status_label = t.status_active %}
+                                {% if prod.status == 'sold' %}
+                                    {% set status_class = 'badge-sold' %}
+                                    {% set status_label = t.status_sold %}
+                                {% elseif prod.status == 'hidden' %}
+                                    {% set status_class = 'badge-hidden' %}
+                                    {% set status_label = t.status_hidden %}
+                                {% endif %}
+                                
+                                <span class="badge {{ status_class }}">
+                                    {{ status_label }}
+                                </span>
+                            </div>
+                        </div>
+                        
+                        <div class="actions">
+                            <a href="seller_dashboard.php?edit={{ prod.id }}" class="act-btn act-edit">Edit</a>
+                            <form method="POST" onsubmit="return confirm('{{ t.confirm_delete }}');" style="margin:0;">
+                                <input type="hidden" name="action" value="delete_product">
+                                <input type="hidden" name="product_id" value="{{ prod.id }}">
+                                <button type="submit" class="act-btn act-del">Del</button>
+                            </form>
+                        </div>
+                    </li>
+                    {% endfor %}
+                </ul>
+            {% else %}
+                <div style="text-align: center; padding: 60px 20px; color: var(--text-muted);">
+                    <span style="font-size: 50px; display: block; margin-bottom: 15px; opacity: 0.5;">📦</span>
+                    <strong style="font-size: 16px; color: var(--text-main);">{{ t.no_listings }}</strong><br><br>
+                    {{ t.use_form }}
+                </div>
+            {% endif %}
+        </div>
+    </div>
+{% endblock %}
+
+{% block extra_js %}
+<script>
+    document.addEventListener('DOMContentLoaded', function() {
+        const tagContainer = document.getElementById('tags_container');
+        const tagInput = document.getElementById('tag_input_field');
+        const hiddenTagsInput = document.getElementById('hidden_search_tags');
+        let tags = [];
+        
+        if (hiddenTagsInput.value.trim() !== '') {
+            tags = hiddenTagsInput.value.split(',').map(t => t.trim()).filter(t => t !== '');
+            renderTags();
+        }
+
+        tagContainer.addEventListener('click', () => tagInput.focus());
+
+        tagInput.addEventListener('keydown', function(e) {
+            if (e.key === 'Enter' || e.key === ',') {
+                e.preventDefault();
+                addTag(tagInput.value);
+            } else if (e.key === 'Backspace' && tagInput.value === '' && tags.length > 0) {
+                removeTag(tags.length - 1);
+            }
+        });
+
+        function addTag(tag) {
+            tag = tag.replace(/,/g, '').trim();
+            if (tag !== '' && !tags.includes(tag)) {
+                tags.push(tag);
+                updateHiddenAndRender();
+            }
+            tagInput.value = '';
+        }
+
+        window.removeTagEl = function(index) {
+            tags.splice(index, 1);
+            updateHiddenAndRender();
+        }
+
+        function removeTag(index) { window.removeTagEl(index); }
+
+        function updateHiddenAndRender() {
+            hiddenTagsInput.value = tags.join(', ');
+            renderTags();
+        }
+
+        function renderTags() {
+            tagContainer.querySelectorAll('.tag-chip').forEach(chip => chip.remove());
+            tags.forEach((tag, index) => {
+                const chip = document.createElement('div');
+                chip.className = 'tag-chip';
+                chip.innerHTML = `${tag} <span onclick="window.removeTagEl(${index})">&times;</span>`;
+                tagContainer.insertBefore(chip, tagInput);
+            });
+        }
+    });
+
+    function loadAttributes(categoryId, productId = 0) {
+        const container = document.getElementById('dynamic-attributes-container');
+        if (!categoryId) {
+            container.style.display = 'none';
+            container.innerHTML = '';
+            return;
+        }
+        
+        let url = 'seller_dashboard.php?ajax_get_attributes=1&category_id=' + categoryId;
+        if (productId > 0) { url += '&product_id=' + productId; }
+        
+        fetch(url)
+            .then(response => response.text())
+            .then(html => {
+                html = html.replace("grid-template-columns: 1fr 1fr;", "");
+                if (html.trim() !== '') {
+                    container.innerHTML = html;
+                    container.style.display = 'block';
+                } else {
+                    container.style.display = 'none';
+                    container.innerHTML = '';
+                }
+            })
+            .catch(error => console.error('Error:', error));
+    }
+
+    {% if edit_product %}
+        window.addEventListener('DOMContentLoaded', () => {
+            loadAttributes({{ edit_product.category_id }}, {{ edit_product.id }});
+        });
+    {% endif %}
+</script>
+{% endblock %}

+ 146 - 0
themes/default/seller_orders.html.twig

@@ -0,0 +1,146 @@
+{% extends "base.html.twig" %}
+
+{% block title %}{{ t.customer_orders }} - {{ site_title }}{% endblock %}
+
+{% block extra_css %}
+<style>
+    /* --- SELLER DASHBOARD COMMON STYLES --- */
+    .dashboard-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; border-bottom: 2px solid var(--border-color); padding-bottom: 15px; }
+    .dashboard-header h2 { margin: 0; color: var(--primary); font-size: 24px; text-transform: uppercase; letter-spacing: 1px; font-weight: 800; display: flex; align-items: center; gap: 10px; }
+    
+    .dashboard-nav { display: flex; gap: 10px; }
+    .nav-btn { padding: 10px 15px; border-radius: 6px; text-decoration: none; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: 0.2s; border: 1px solid transparent; }
+    .nav-btn.active { background: rgba(249, 115, 22, 0.1); color: var(--primary); border-color: rgba(249, 115, 22, 0.2); }
+    .nav-btn:not(.active) { background: rgba(255,255,255,0.05); color: var(--text-main); border-color: var(--border-color); }
+    .nav-btn:hover { background: rgba(255,255,255,0.1); }
+
+    .alert { padding: 15px; border-radius: 6px; margin-bottom: 25px; font-weight: 600; font-size: 14px;}
+    .success { background: rgba(16, 185, 129, 0.1); color: var(--accent-green); border: 1px solid rgba(16, 185, 129, 0.2); }
+    .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
+
+    /* --- ORDERS STYLES --- */
+    .orders-container { max-width: 1000px; margin: 0 auto; }
+    
+    .order-card { background: var(--panel-bg); border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 1px solid var(--border-color); margin-bottom: 25px; overflow: hidden; display: flex; flex-direction: column;}
+    
+    .order-header { background: rgba(0,0,0,0.2); padding: 15px 25px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); }
+    .order-id { font-size: 18px; font-weight: 900; color: var(--text-main); }
+    .order-date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
+    
+    .order-body { padding: 25px; display: grid; grid-template-columns: 1fr 1.5fr; gap: 30px; }
+    
+    .customer-info h4 { margin-top: 0; color: var(--primary); font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
+    .customer-info p { margin: 0 0 8px 0; font-size: 14px; color: var(--text-main); display: flex; align-items: center; gap: 8px; }
+    .customer-info p i { color: var(--text-muted); font-size: 12px; }
+    
+    .order-items h4 { margin-top: 0; color: var(--primary); font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
+    .item-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
+    .item-row:last-child { border-bottom: none; padding-bottom: 0; }
+    .item-title { font-weight: 600; color: var(--text-main); font-size: 15px; }
+    .item-qty { background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; margin-left: 10px; color: var(--text-muted); }
+    .item-price { font-weight: 800; color: var(--accent-green); }
+    
+    /* --- ORDER ACTIONS (STATUS & DELETE) --- */
+    .order-actions { background: rgba(0,0,0,0.15); padding: 15px 25px; border-top: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
+    
+    .status-form { display: flex; align-items: center; gap: 10px; }
+    .status-select { padding: 8px 12px; background: #0f172a; border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-main); font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 600; outline: none; }
+    .status-select:focus { border-color: var(--primary); }
+    
+    .btn-update { padding: 8px 15px; background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2); color: var(--accent-green); font-size: 13px; font-weight: 700; border-radius: 6px; cursor: pointer; text-transform: uppercase; transition: 0.2s; }
+    .btn-update:hover { background: var(--accent-green); color: white; }
+    
+    .delete-form { margin: 0; }
+    .btn-delete { padding: 8px 15px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); color: #ef4444; font-size: 13px; font-weight: 700; border-radius: 6px; cursor: pointer; text-transform: uppercase; transition: 0.2s; }
+    .btn-delete:hover { background: #ef4444; color: white; }
+
+    @media (max-width: 768px) {
+        .dashboard-header { flex-direction: column; gap: 15px; text-align: center; }
+        .order-body { grid-template-columns: 1fr; gap: 20px; padding: 20px;}
+        .customer-info { border-bottom: 1px solid var(--border-color); padding-bottom: 20px; }
+        .order-actions { flex-direction: column; gap: 15px; align-items: stretch; padding: 20px;}
+        .status-form { justify-content: space-between; }
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+    <div class="dashboard-header">
+        <h2>🏪 {{ t.seller_panel }}</h2>
+        <div class="dashboard-nav">
+            <a href="seller_dashboard.php" class="nav-btn">{{ t.my_listings }}</a>
+            <a href="seller_orders.php" class="nav-btn active">{{ t.customer_orders }}</a>
+        </div>
+    </div>
+
+    <div class="orders-container">
+        
+        {% if message %}
+            {{ message|raw }}
+        {% endif %}
+
+        {% if orders|length > 0 %}
+            {% for order in orders %}
+                <div class="order-card">
+                    <div class="order-header">
+                        <div class="order-id">#{{ order.id }}</div>
+                        <div style="display: flex; align-items: center; gap: 15px;">
+                            <div class="order-date">{{ order.created_at|date("d/m/Y H:i") }}</div>
+                        </div>
+                    </div>
+                    
+                    <div class="order-body">
+                        <div class="customer-info">
+                            <h4>{{ t.customer }} & {{ t.contact }}</h4>
+                            <p>👤 <strong>{{ order.customer_name }}</strong></p>
+                            <p>✉️ <a href="mailto:{{ order.customer_email }}" style="color: var(--accent-blue); text-decoration: none;">{{ order.customer_email }}</a></p>
+                            <p>📞 {{ order.customer_phone }}</p>
+                            <p>📍 {{ order.shipping_address }}</p>
+                        </div>
+                        
+                        <div class="order-items">
+                            <h4>{{ t.my_products }}</h4>
+                            {% for item in order.items %}
+                                <div class="item-row">
+                                    <div>
+                                        <span class="item-title">{{ item.title }}</span>
+                                        <span class="item-qty">x{{ item.quantity }}</span>
+                                    </div>
+                                    <div class="item-price">{{ item.price|number_format(2, ',', '.') }} €</div>
+                                </div>
+                            {% endfor %}
+                        </div>
+                    </div>
+
+                    <!-- ORDER ACTIONS (Status & Delete) -->
+                    <div class="order-actions">
+                        <!-- Φόρμα αλλαγής Status -->
+                        <form method="POST" action="seller_orders.php" class="status-form">
+                            <input type="hidden" name="action" value="update_status">
+                            <input type="hidden" name="order_id" value="{{ order.id }}">
+                            <select name="status" class="status-select">
+                                <option value="pending" {% if order.status == 'pending' %}selected{% endif %}>{{ t.status_pending }}</option>
+                                <option value="completed" {% if order.status == 'completed' %}selected{% endif %}>{{ t.status_completed }}</option>
+                                <option value="cancelled" {% if order.status == 'cancelled' %}selected{% endif %}>{{ t.status_cancelled }}</option>
+                            </select>
+                            <button type="submit" class="btn-update">{{ t.btn_update }}</button>
+                        </form>
+                        
+                        <!-- Φόρμα Διαγραφής Παραγγελίας -->
+                        <form method="POST" action="seller_orders.php" class="delete-form" onsubmit="return confirm('{{ t.confirm_delete }}');">
+                            <input type="hidden" name="action" value="delete_order">
+                            <input type="hidden" name="order_id" value="{{ order.id }}">
+                            <button type="submit" class="btn-delete">{{ t.btn_delete }}</button>
+                        </form>
+                    </div>
+
+                </div>
+            {% endfor %}
+        {% else %}
+            <div style="text-align: center; padding: 80px 20px; color: var(--text-muted); background: var(--panel-bg); border-radius: var(--border-radius); border: 1px dashed var(--border-color);">
+                <span style="font-size: 50px; display: block; margin-bottom: 15px; opacity: 0.5;">🛒</span>
+                <strong style="font-size: 18px; color: var(--text-main);">{{ t.no_orders }}</strong>
+            </div>
+        {% endif %}
+    </div>
+{% endblock %}

二进制
uploads/equipment/prod_2_1790588393.jpg


二进制
uploads/equipment/prod_3_1790588897.jpg


二进制
uploads/equipment/prod_4_1790589083.jpg


二进制
uploads/equipment/prod_5_1790589860.jpg


二进制
uploads/equipment/prod_6_1790590114.jpg


二进制
uploads/logo_1790538884.png


二进制
uploads/logo_1790539766.jpg


二进制
uploads/logo_1790539773.jpg


二进制
uploads/logo_1790540300.jpg


二进制
uploads/logo_1790540314.jpg


二进制
uploads/logo_1790540471.jpg


二进制
uploads/logo_1790540487.jpg


二进制
uploads/logo_1790540851.jpg


二进制
uploads/logo_1790540911.png


二进制
uploads/logo_1790669112.jpg


二进制
uploads/logo_1790669218.jpg


二进制
uploads/logo_1790669312.jpg


二进制
uploads/logo_1790669499.jpg


+ 22 - 0
vendor/autoload.php

@@ -0,0 +1,22 @@
+<?php
+
+// autoload.php @generated by Composer
+
+if (PHP_VERSION_ID < 50600) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    throw new RuntimeException($err);
+}
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInit6a6772df2521c6860a448e9115f2793a::getLoader();

+ 579 - 0
vendor/composer/ClassLoader.php

@@ -0,0 +1,579 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    https://www.php-fig.org/psr/psr-0/
+ * @see    https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    /** @var \Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
+    private $vendorDir;
+
+    // PSR-4
+    /**
+     * @var array<string, array<string, int>>
+     */
+    private $prefixLengthsPsr4 = array();
+    /**
+     * @var array<string, list<string>>
+     */
+    private $prefixDirsPsr4 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    /**
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
+     */
+    private $prefixesPsr0 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr0 = array();
+
+    /** @var bool */
+    private $useIncludePath = false;
+
+    /**
+     * @var array<string, string>
+     */
+    private $classMap = array();
+
+    /** @var bool */
+    private $classMapAuthoritative = false;
+
+    /**
+     * @var array<string, bool>
+     */
+    private $missingClasses = array();
+
+    /** @var string|null */
+    private $apcuPrefix;
+
+    /**
+     * @var array<string, self>
+     */
+    private static $registeredLoaders = array();
+
+    /**
+     * @param string|null $vendorDir
+     */
+    public function __construct($vendorDir = null)
+    {
+        $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+        }
+
+        return array();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    /**
+     * @return array<string, string> Array of classname => path
+     */
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array<string, string> $classMap Class to filename map
+     *
+     * @return void
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @return void
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
+     *
+     * @return void
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     *
+     * @return void
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     *
+     * @return void
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     *
+     * @return void
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     *
+     * @return void
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+        if (null === $this->vendorDir) {
+            return;
+        }
+
+        if ($prepend) {
+            self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+        } else {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+            self::$registeredLoaders[$this->vendorDir] = $this;
+        }
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     *
+     * @return void
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+
+        if (null !== $this->vendorDir) {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+        }
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return true|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            $includeFile = self::$includeFile;
+            $includeFile($file);
+
+            return true;
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    /**
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
+     *
+     * @return array<string, self>
+     */
+    public static function getRegisteredLoaders()
+    {
+        return self::$registeredLoaders;
+    }
+
+    /**
+     * @param  string       $class
+     * @param  string       $ext
+     * @return string|false
+     */
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = \Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
+}

+ 396 - 0
vendor/composer/InstalledVersions.php

@@ -0,0 +1,396 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+    /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
+    /**
+     * @var mixed[]|null
+     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+     */
+    private static $installed;
+
+    /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
+    /**
+     * @var bool|null
+     */
+    private static $canGetVendors;
+
+    /**
+     * @var array[]
+     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static $installedByVendor = array();
+
+    /**
+     * Returns a list of all package names which are present, either by being installed, replaced or provided
+     *
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackages()
+    {
+        $packages = array();
+        foreach (self::getInstalled() as $installed) {
+            $packages[] = array_keys($installed['versions']);
+        }
+
+        if (1 === \count($packages)) {
+            return $packages[0];
+        }
+
+        return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+    }
+
+    /**
+     * Returns a list of all package names with a specific type e.g. 'library'
+     *
+     * @param  string   $type
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackagesByType($type)
+    {
+        $packagesByType = array();
+
+        foreach (self::getInstalled() as $installed) {
+            foreach ($installed['versions'] as $name => $package) {
+                if (isset($package['type']) && $package['type'] === $type) {
+                    $packagesByType[] = $name;
+                }
+            }
+        }
+
+        return $packagesByType;
+    }
+
+    /**
+     * Checks whether the given package is installed
+     *
+     * This also returns true if the package name is provided or replaced by another package
+     *
+     * @param  string $packageName
+     * @param  bool   $includeDevRequirements
+     * @return bool
+     */
+    public static function isInstalled($packageName, $includeDevRequirements = true)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (isset($installed['versions'][$packageName])) {
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks whether the given package satisfies a version constraint
+     *
+     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+     *
+     *   Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+     *
+     * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
+     * @param  string        $packageName
+     * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+     * @return bool
+     */
+    public static function satisfies(VersionParser $parser, $packageName, $constraint)
+    {
+        $constraint = $parser->parseConstraints((string) $constraint);
+        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+        return $provided->matches($constraint);
+    }
+
+    /**
+     * Returns a version constraint representing all the range(s) which are installed for a given package
+     *
+     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+     * whether a given version of a package is installed, and not just whether it exists
+     *
+     * @param  string $packageName
+     * @return string Version constraint usable with composer/semver
+     */
+    public static function getVersionRanges($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            $ranges = array();
+            if (isset($installed['versions'][$packageName]['pretty_version'])) {
+                $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+            }
+            if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+            }
+            if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+            }
+            if (array_key_exists('provided', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+            }
+
+            return implode(' || ', $ranges);
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getPrettyVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['pretty_version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+     */
+    public static function getReference($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['reference'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['reference'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+     */
+    public static function getInstallPath($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @return array
+     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+     */
+    public static function getRootPackage()
+    {
+        $installed = self::getInstalled();
+
+        return $installed[0]['root'];
+    }
+
+    /**
+     * Returns the raw installed.php data for custom implementations
+     *
+     * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+     * @return array[]
+     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+     */
+    public static function getRawData()
+    {
+        @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                self::$installed = include __DIR__ . '/installed.php';
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        return self::$installed;
+    }
+
+    /**
+     * Returns the raw data of all installed.php which are currently loaded for custom implementations
+     *
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    public static function getAllRawData()
+    {
+        return self::getInstalled();
+    }
+
+    /**
+     * Lets you reload the static array from another file
+     *
+     * This is only useful for complex integrations in which a project needs to use
+     * this class but then also needs to execute another project's autoloader in process,
+     * and wants to ensure both projects have access to their version of installed.php.
+     *
+     * A typical case would be PHPUnit, where it would need to make sure it reads all
+     * the data it needs from this class, then call reload() with
+     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+     * the project in which it runs can then also use this class safely, without
+     * interference between PHPUnit's dependencies and the project's dependencies.
+     *
+     * @param  array[] $data A vendor/composer/installed.php data set
+     * @return void
+     *
+     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+     */
+    public static function reload($data)
+    {
+        self::$installed = $data;
+        self::$installedByVendor = array();
+
+        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+        // so we have to assume it does not, and that may result in duplicate data being returned when listing
+        // all installed packages for example
+        self::$installedIsLocalDir = false;
+    }
+
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\\', '/');
+        }
+
+        return self::$selfDir;
+    }
+
+    /**
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static function getInstalled()
+    {
+        if (null === self::$canGetVendors) {
+            self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+        }
+
+        $installed = array();
+        $copiedLocalDir = false;
+
+        if (self::$canGetVendors) {
+            $selfDir = self::getSelfDir();
+            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                $vendorDir = strtr($vendorDir, '\\', '/');
+                if (isset(self::$installedByVendor[$vendorDir])) {
+                    $installed[] = self::$installedByVendor[$vendorDir];
+                } elseif (is_file($vendorDir.'/composer/installed.php')) {
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = $required;
+                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                        self::$installed = $required;
+                        self::$installedIsLocalDir = true;
+                    }
+                }
+                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+                    $copiedLocalDir = true;
+                }
+            }
+        }
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        if (self::$installed !== array() && !$copiedLocalDir) {
+            $installed[] = self::$installed;
+        }
+
+        return $installed;
+    }
+}

+ 21 - 0
vendor/composer/LICENSE

@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+

+ 44 - 0
vendor/composer/autoload_classmap.php

@@ -0,0 +1,44 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'AllowDynamicProperties' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php',
+    'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+    'Google_AccessToken_Revoke' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_AccessToken_Verify' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_AuthHandler_AuthHandlerFactory' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_AuthHandler_Guzzle6AuthHandler' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_AuthHandler_Guzzle7AuthHandler' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Client' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Collection' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Exception' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Http_Batch' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Http_MediaFileUpload' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Http_REST' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Model' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Service' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Service_Exception' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Service_Resource' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Task_Composer' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Task_Exception' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Task_Retryable' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Task_Runner' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'Google_Utils_UriTemplate' => $vendorDir . '/google/apiclient/src/aliases.php',
+    'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
+    'Random\\BrokenRandomEngineError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php',
+    'Random\\CryptoSafeEngine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php',
+    'Random\\Engine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php',
+    'Random\\Engine\\Secure' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php',
+    'Random\\RandomError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php',
+    'Random\\RandomException' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php',
+    'SensitiveParameter' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php',
+    'SensitiveParameterValue' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php',
+    'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
+    'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
+    'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
+);

+ 20 - 0
vendor/composer/autoload_files.php

@@ -0,0 +1,20 @@
+<?php
+
+// autoload_files.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
+    '5897ea0ac4cccf14d323035e65887801' => $vendorDir . '/symfony/polyfill-php82/bootstrap.php',
+    '1f87db08236948d07391152dccb70f04' => $vendorDir . '/google/apiclient-services/autoload.php',
+    '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
+    '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
+    '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
+    'a8d3953fd9959404dd22d3dfcd0a79f0' => $vendorDir . '/google/apiclient/src/aliases.php',
+    '89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php',
+    'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php',
+    'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php',
+    'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php',
+);

+ 9 - 0
vendor/composer/autoload_namespaces.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 27 - 0
vendor/composer/autoload_psr4.php

@@ -0,0 +1,27 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Twig\\' => array($vendorDir . '/twig/twig/src'),
+    'Symfony\\Polyfill\\Php82\\' => array($vendorDir . '/symfony/polyfill-php82'),
+    'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
+    'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
+    'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
+    'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
+    'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
+    'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
+    'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
+    'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
+    'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
+    'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
+    'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
+    'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
+    'Google\\Service\\' => array($vendorDir . '/google/apiclient-services/src'),
+    'Google\\Auth\\' => array($vendorDir . '/google/auth/src'),
+    'Google\\' => array($vendorDir . '/google/apiclient/src'),
+    'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
+);

+ 50 - 0
vendor/composer/autoload_real.php

@@ -0,0 +1,50 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit6a6772df2521c6860a448e9115f2793a
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        require __DIR__ . '/platform_check.php';
+
+        spl_autoload_register(array('ComposerAutoloaderInit6a6772df2521c6860a448e9115f2793a', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
+        spl_autoload_unregister(array('ComposerAutoloaderInit6a6772df2521c6860a448e9115f2793a', 'loadClassLoader'));
+
+        require __DIR__ . '/autoload_static.php';
+        call_user_func(\Composer\Autoload\ComposerStaticInit6a6772df2521c6860a448e9115f2793a::getInitializer($loader));
+
+        $loader->register(true);
+
+        $filesToLoad = \Composer\Autoload\ComposerStaticInit6a6772df2521c6860a448e9115f2793a::$files;
+        $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
+            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+                require $file;
+            }
+        }, null, null);
+        foreach ($filesToLoad as $fileIdentifier => $file) {
+            $requireFile($fileIdentifier, $file);
+        }
+
+        return $loader;
+    }
+}

+ 185 - 0
vendor/composer/autoload_static.php

@@ -0,0 +1,185 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInit6a6772df2521c6860a448e9115f2793a
+{
+    public static $files = array (
+        'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
+        '5897ea0ac4cccf14d323035e65887801' => __DIR__ . '/..' . '/symfony/polyfill-php82/bootstrap.php',
+        '1f87db08236948d07391152dccb70f04' => __DIR__ . '/..' . '/google/apiclient-services/autoload.php',
+        '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
+        '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
+        '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
+        'a8d3953fd9959404dd22d3dfcd0a79f0' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        '89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php',
+        'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php',
+        'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php',
+        'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php',
+    );
+
+    public static $prefixLengthsPsr4 = array (
+        'T' =>
+        array (
+            'Twig\\' => 5,
+        ),
+        'S' =>
+        array (
+            'Symfony\\Polyfill\\Php82\\' => 23,
+            'Symfony\\Polyfill\\Php80\\' => 23,
+            'Symfony\\Polyfill\\Mbstring\\' => 26,
+            'Symfony\\Polyfill\\Ctype\\' => 23,
+        ),
+        'P' =>
+        array (
+            'Psr\\Log\\' => 8,
+            'Psr\\Http\\Message\\' => 17,
+            'Psr\\Http\\Client\\' => 16,
+            'Psr\\Cache\\' => 10,
+            'PHPMailer\\PHPMailer\\' => 20,
+        ),
+        'M' =>
+        array (
+            'Monolog\\' => 8,
+        ),
+        'G' =>
+        array (
+            'GuzzleHttp\\Psr7\\' => 16,
+            'GuzzleHttp\\Promise\\' => 19,
+            'GuzzleHttp\\' => 11,
+            'Google\\Service\\' => 15,
+            'Google\\Auth\\' => 12,
+            'Google\\' => 7,
+        ),
+        'F' =>
+        array (
+            'Firebase\\JWT\\' => 13,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'Twig\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/twig/twig/src',
+        ),
+        'Symfony\\Polyfill\\Php82\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php82',
+        ),
+        'Symfony\\Polyfill\\Php80\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
+        ),
+        'Symfony\\Polyfill\\Mbstring\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
+        ),
+        'Symfony\\Polyfill\\Ctype\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
+        ),
+        'Psr\\Log\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/psr/log/src',
+        ),
+        'Psr\\Http\\Message\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-factory/src',
+            1 => __DIR__ . '/..' . '/psr/http-message/src',
+        ),
+        'Psr\\Http\\Client\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-client/src',
+        ),
+        'Psr\\Cache\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/psr/cache/src',
+        ),
+        'PHPMailer\\PHPMailer\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
+        ),
+        'Monolog\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
+        ),
+        'GuzzleHttp\\Psr7\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
+        ),
+        'GuzzleHttp\\Promise\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
+        ),
+        'GuzzleHttp\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
+        ),
+        'Google\\Service\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/google/apiclient-services/src',
+        ),
+        'Google\\Auth\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/google/auth/src',
+        ),
+        'Google\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/google/apiclient/src',
+        ),
+        'Firebase\\JWT\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
+        ),
+    );
+
+    public static $classMap = array (
+        'AllowDynamicProperties' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php',
+        'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+        'Google_AccessToken_Revoke' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_AccessToken_Verify' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_AuthHandler_AuthHandlerFactory' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_AuthHandler_Guzzle6AuthHandler' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_AuthHandler_Guzzle7AuthHandler' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Client' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Collection' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Exception' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Http_Batch' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Http_MediaFileUpload' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Http_REST' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Model' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Service' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Service_Exception' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Service_Resource' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Task_Composer' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Task_Exception' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Task_Retryable' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Task_Runner' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'Google_Utils_UriTemplate' => __DIR__ . '/..' . '/google/apiclient/src/aliases.php',
+        'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
+        'Random\\BrokenRandomEngineError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php',
+        'Random\\CryptoSafeEngine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php',
+        'Random\\Engine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php',
+        'Random\\Engine\\Secure' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php',
+        'Random\\RandomError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php',
+        'Random\\RandomException' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php',
+        'SensitiveParameter' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php',
+        'SensitiveParameterValue' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php',
+        'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
+        'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
+        'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInit6a6772df2521c6860a448e9115f2793a::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit6a6772df2521c6860a448e9115f2793a::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit6a6772df2521c6860a448e9115f2793a::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}

+ 1583 - 0
vendor/composer/installed.json

@@ -0,0 +1,1583 @@
+{
+    "packages": [
+        {
+            "name": "firebase/php-jwt",
+            "version": "v7.2.1",
+            "version_normalized": "7.2.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/php-jwt.git",
+                "reference": "0501f5729b9e2448516674aff07c7d1727869b8e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/0501f5729b9e2448516674aff07c7d1727869b8e",
+                "reference": "0501f5729b9e2448516674aff07c7d1727869b8e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^8.0"
+            },
+            "require-dev": {
+                "guzzlehttp/guzzle": "^7.4||^8.0",
+                "phpfastcache/phpfastcache": "^9.2",
+                "phpseclib/phpseclib": "~3.0",
+                "phpspec/prophecy-phpunit": "^2.2",
+                "phpunit/phpunit": "^9.5",
+                "psr/cache": "^2.0||^3.0",
+                "psr/http-client": "^1.0",
+                "psr/http-factory": "^1.0"
+            },
+            "suggest": {
+                "ext-sodium": "Support EdDSA (Ed25519) signatures",
+                "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
+                "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures"
+            },
+            "time": "2026-09-28T17:51:46+00:00",
+            "type": "library",
+            "extra": {
+                "component": {
+                    "id": "jwt",
+                    "path": "Jwt",
+                    "entry": "README.md",
+                    "target": "googleapis/php-jwt.git"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Firebase\\JWT\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Neuman Vong",
+                    "email": "neuman+pear@twilio.com",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Anant Narayanan",
+                    "email": "anant@php.net",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+            "homepage": "https://github.com/googleapis/php-jwt",
+            "keywords": [
+                "jwt",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/googleapis/php-jwt/issues",
+                "source": "https://github.com/googleapis/php-jwt/tree/v7.2.1"
+            },
+            "install-path": "../firebase/php-jwt"
+        },
+        {
+            "name": "google/apiclient",
+            "version": "v2.20.1",
+            "version_normalized": "2.20.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/google-api-php-client.git",
+                "reference": "31b577e7012c4e68d5289d63aaf7e6d471ec1ff1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/31b577e7012c4e68d5289d63aaf7e6d471ec1ff1",
+                "reference": "31b577e7012c4e68d5289d63aaf7e6d471ec1ff1",
+                "shasum": ""
+            },
+            "require": {
+                "firebase/php-jwt": "^6.0||^7.0",
+                "google/apiclient-services": "~0.350",
+                "google/auth": "^1.53",
+                "guzzlehttp/guzzle": "^7.8.2||^8.0",
+                "guzzlehttp/psr7": "^2.6.3||^3.0",
+                "monolog/monolog": "^2.9||^3.0",
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "cache/filesystem-adapter": "^1.1",
+                "composer/composer": "^2.9",
+                "phpcompatibility/php-compatibility": "^9.2",
+                "phpspec/prophecy-phpunit": "^2.1",
+                "phpunit/phpunit": "^9.6",
+                "squizlabs/php_codesniffer": "^3.8",
+                "symfony/css-selector": "^5.4",
+                "symfony/dom-crawler": "^5.4"
+            },
+            "suggest": {
+                "cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)"
+            },
+            "time": "2026-09-25T18:54:19+00:00",
+            "type": "library",
+            "extra": {
+                "component": {
+                    "entry": "src/Client.php"
+                },
+                "branch-alias": {
+                    "dev-main": "2.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "src/aliases.php"
+                ],
+                "psr-4": {
+                    "Google\\": "src/"
+                },
+                "classmap": [
+                    "src/aliases.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "Client library for Google APIs",
+            "homepage": "http://developers.google.com/api-client-library/php",
+            "keywords": [
+                "google"
+            ],
+            "support": {
+                "issues": "https://github.com/googleapis/google-api-php-client/issues",
+                "source": "https://github.com/googleapis/google-api-php-client/tree/v2.20.1"
+            },
+            "install-path": "../google/apiclient"
+        },
+        {
+            "name": "google/apiclient-services",
+            "version": "v0.461.0",
+            "version_normalized": "0.461.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/google-api-php-client-services.git",
+                "reference": "653eec9b7767d34f0cbe2a702cf2c2b825a4857d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/653eec9b7767d34f0cbe2a702cf2c2b825a4857d",
+                "reference": "653eec9b7767d34f0cbe2a702cf2c2b825a4857d",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.6"
+            },
+            "time": "2026-09-25T22:00:27+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "autoload.php"
+                ],
+                "psr-4": {
+                    "Google\\Service\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "Client library for Google APIs",
+            "homepage": "http://developers.google.com/api-client-library/php",
+            "keywords": [
+                "google"
+            ],
+            "support": {
+                "issues": "https://github.com/googleapis/google-api-php-client-services/issues",
+                "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.461.0"
+            },
+            "install-path": "../google/apiclient-services"
+        },
+        {
+            "name": "google/auth",
+            "version": "v1.55.1",
+            "version_normalized": "1.55.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/googleapis/google-auth-library-php.git",
+                "reference": "ef1b92cb42470b09b3caaf23bb9b4bb9a2dc787e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/ef1b92cb42470b09b3caaf23bb9b4bb9a2dc787e",
+                "reference": "ef1b92cb42470b09b3caaf23bb9b4bb9a2dc787e",
+                "shasum": ""
+            },
+            "require": {
+                "firebase/php-jwt": "^6.0||^7.0",
+                "guzzlehttp/guzzle": "^7.8.2||^8.0",
+                "guzzlehttp/psr7": "^2.6.3||^3.0",
+                "php": "^8.1",
+                "psr/cache": "^2.0||^3.0",
+                "psr/http-client": "^1.0",
+                "psr/http-message": "^1.1||^2.0",
+                "psr/log": "^2.0||^3.0"
+            },
+            "require-dev": {
+                "guzzlehttp/promises": "^2.0.3||^3.0",
+                "kelvinmo/simplejwt": "^1.1.0",
+                "phpseclib/phpseclib": "^3.0.35",
+                "phpspec/prophecy-phpunit": "^2.1",
+                "phpunit/phpunit": "^9.6.34",
+                "sebastian/comparator": ">=1.2.3",
+                "squizlabs/php_codesniffer": "^4.0",
+                "symfony/filesystem": "^6.4.39||^7.3",
+                "symfony/process": "^6.4.41||^7.0",
+                "webmozart/assert": "^1.11||^2.0"
+            },
+            "suggest": {
+                "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
+            },
+            "time": "2026-09-28T17:51:46+00:00",
+            "type": "library",
+            "extra": {
+                "component": {
+                    "id": "auth",
+                    "path": "Auth",
+                    "entry": "README.md",
+                    "target": "googleapis/google-auth-library-php.git"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Google\\Auth\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "Google Auth Library for PHP",
+            "homepage": "https://github.com/googleapis/google-auth-library-php",
+            "keywords": [
+                "Authentication",
+                "google",
+                "oauth2"
+            ],
+            "support": {
+                "docs": "https://cloud.google.com/php/docs/reference/auth/latest",
+                "issues": "https://github.com/googleapis/google-auth-library-php/issues",
+                "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.55.1"
+            },
+            "install-path": "../google/auth"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "8.2.0",
+            "version_normalized": "8.2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "93939470950a9b11e2e84204166ef5e048c55fe4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/93939470950a9b11e2e84204166ef5e048c55fe4",
+                "reference": "93939470950a9b11e2e84204166ef5e048c55fe4",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^3.0.2",
+                "guzzlehttp/psr7": "^3.1",
+                "php": "^7.4 || ^8.0",
+                "psr/http-client": "^1.0",
+                "psr/http-factory": "^1.0",
+                "symfony/polyfill-php80": "^1.25",
+                "symfony/polyfill-php82": "^1.27"
+            },
+            "provide": {
+                "psr/http-client-implementation": "1.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "ext-curl": "*",
+                "guzzle/client-integration-tests": "4.0.1",
+                "guzzlehttp/test-server": "^1.0.1",
+                "php-http/message-factory": "^1.1",
+                "phpunit/phpunit": "^9.6.34",
+                "psr/log": "^1.1 || ^2.0 || ^3.0"
+            },
+            "suggest": {
+                "ext-curl": "Required for CURL handler support",
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+                "psr/log": "Required for using the Log middleware"
+            },
+            "time": "2026-09-06T13:55:09+00:00",
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "psr-18",
+                "psr-7",
+                "rest",
+                "web service"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/8.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../guzzlehttp/guzzle"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "3.0.2",
+            "version_normalized": "3.0.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "42118e66a53c492effaf92bc357e931985d5c6f9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/42118e66a53c492effaf92bc357e931985d5c6f9",
+                "reference": "42118e66a53c492effaf92bc357e931985d5c6f9",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "phpunit/phpunit": "^9.6.34"
+            },
+            "time": "2026-08-24T10:00:26+00:00",
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/3.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../guzzlehttp/promises"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "3.1.0",
+            "version_normalized": "3.1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/a3059ba1a84c9139c4ae03cf0f45bea276c97c74",
+                "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0",
+                "psr/http-factory": "^1.1",
+                "psr/http-message": "^2.0",
+                "symfony/polyfill-php80": "^1.25",
+                "symfony/polyfill-php82": "^1.27"
+            },
+            "provide": {
+                "psr/http-factory-implementation": "1.1",
+                "psr/http-message-implementation": "2.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "http-interop/http-factory-tests": "1.1.0",
+                "jshttp/mime-db": "1.54.0.1",
+                "php-http/psr7-integration-tests": "^1.5.1",
+                "phpunit/phpunit": "^9.6.34"
+            },
+            "suggest": {
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "time": "2026-08-24T11:02:13+00:00",
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://sagikazarmark.hu"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/psr7/issues",
+                "source": "https://github.com/guzzle/psr7/tree/3.1.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../guzzlehttp/psr7"
+        },
+        {
+            "name": "monolog/monolog",
+            "version": "3.12.0",
+            "version_normalized": "3.12.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/monolog.git",
+                "reference": "72c534fc0ab181ef52d92a68382318631e301608"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/72c534fc0ab181ef52d92a68382318631e301608",
+                "reference": "72c534fc0ab181ef52d92a68382318631e301608",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "psr/log": "^2.0 || ^3.0"
+            },
+            "provide": {
+                "psr/log-implementation": "3.0.0"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^3.0",
+                "doctrine/couchdb": "~1.0@dev",
+                "elasticsearch/elasticsearch": "^7 || ^8",
+                "ext-json": "*",
+                "graylog2/gelf-php": "^1.4.2 || ^2.0",
+                "guzzlehttp/guzzle": "^7.4.5",
+                "guzzlehttp/psr7": "^2.2",
+                "mongodb/mongodb": "^1.8 || ^2.0",
+                "php-amqplib/php-amqplib": "~2.4 || ^3",
+                "php-console/php-console": "^3.1.8",
+                "phpstan/phpstan": "^2",
+                "phpstan/phpstan-deprecation-rules": "^2",
+                "phpstan/phpstan-strict-rules": "^2",
+                "phpunit/phpunit": "^10.5.17 || ^11.0.7",
+                "predis/predis": "^1.1 || ^2",
+                "psr/clock": "^1.0",
+                "rollbar/rollbar": "^4.0",
+                "ruflin/elastica": "^7 || ^8",
+                "symfony/mailer": "^5.4 || ^6",
+                "symfony/mime": "^5.4 || ^6"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+                "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
+                "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+                "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
+                "ext-mbstring": "Allow to work properly with unicode symbols",
+                "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
+                "ext-openssl": "Required to send log messages using SSL",
+                "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
+                "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+                "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
+                "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+                "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records",
+                "rollbar/rollbar": "Allow sending log messages to Rollbar",
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
+            },
+            "time": "2026-09-09T08:34:20+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Monolog\\": "src/Monolog"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "https://seld.be"
+                }
+            ],
+            "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+            "homepage": "https://github.com/Seldaek/monolog",
+            "keywords": [
+                "log",
+                "logging",
+                "psr-3"
+            ],
+            "support": {
+                "issues": "https://github.com/Seldaek/monolog/issues",
+                "source": "https://github.com/Seldaek/monolog/tree/3.12.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Seldaek",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../monolog/monolog"
+        },
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v7.1.1",
+            "version_normalized": "7.1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
+                "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "ext-hash": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+                "doctrine/annotations": "^1.2.6 || ^1.13.3",
+                "php-parallel-lint/php-console-highlighter": "^1.0.0",
+                "php-parallel-lint/php-parallel-lint": "^1.3.2",
+                "phpcompatibility/php-compatibility": "^10.0.0@dev",
+                "squizlabs/php_codesniffer": "^3.13.5",
+                "yoast/phpunit-polyfills": "^1.0.4"
+            },
+            "suggest": {
+                "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+                "directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
+                "ext-imap": "Needed to support advanced email address parsing according to RFC822",
+                "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+                "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+                "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+                "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+            },
+            "time": "2026-05-18T08:06:14+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-only"
+            ],
+            "authors": [
+                {
+                    "name": "Marcus Bointon",
+                    "email": "phpmailer@synchromedia.co.uk"
+                },
+                {
+                    "name": "Jim Jagielski",
+                    "email": "jimjag@gmail.com"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "codeworxtech@users.sourceforge.net"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "support": {
+                "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+                "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Synchro",
+                    "type": "github"
+                }
+            ],
+            "install-path": "../phpmailer/phpmailer"
+        },
+        {
+            "name": "psr/cache",
+            "version": "3.0.0",
+            "version_normalized": "3.0.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+                "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "time": "2021-02-03T23:26:27+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/cache/tree/3.0.0"
+            },
+            "install-path": "../psr/cache"
+        },
+        {
+            "name": "psr/http-client",
+            "version": "1.0.3",
+            "version_normalized": "1.0.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-client.git",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "time": "2023-09-23T14:17:50+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Client\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP clients",
+            "homepage": "https://github.com/php-fig/http-client",
+            "keywords": [
+                "http",
+                "http-client",
+                "psr",
+                "psr-18"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-client"
+            },
+            "install-path": "../psr/http-client"
+        },
+        {
+            "name": "psr/http-factory",
+            "version": "1.1.0",
+            "version_normalized": "1.1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-factory.git",
+                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "time": "2024-04-15T12:06:14+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+            "keywords": [
+                "factory",
+                "http",
+                "message",
+                "psr",
+                "psr-17",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-factory"
+            },
+            "install-path": "../psr/http-factory"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "2.0",
+            "version_normalized": "2.0.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "time": "2023-04-04T09:54:51+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/2.0"
+            },
+            "install-path": "../psr/http-message"
+        },
+        {
+            "name": "psr/log",
+            "version": "3.0.2",
+            "version_normalized": "3.0.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+                "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "time": "2024-09-11T13:17:53+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/3.0.2"
+            },
+            "install-path": "../psr/log"
+        },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v3.7.1",
+            "version_normalized": "3.7.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
+                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1"
+            },
+            "time": "2026-06-05T06:23:12+00:00",
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/contracts",
+                    "name": "symfony/contracts"
+                },
+                "branch-alias": {
+                    "dev-main": "3.7-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "function.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/deprecation-contracts"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.37.0",
+            "version_normalized": "1.37.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
+                "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "provide": {
+                "ext-ctype": "*"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "time": "2026-04-10T16:19:22+00:00",
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/polyfill-ctype"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.38.2",
+            "version_normalized": "1.38.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+                "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+                "shasum": ""
+            },
+            "require": {
+                "ext-iconv": "*",
+                "php": ">=7.2"
+            },
+            "provide": {
+                "ext-mbstring": "*"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "time": "2026-05-27T06:59:30+00:00",
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/polyfill-mbstring"
+        },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.37.0",
+            "version_normalized": "1.37.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+                "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "time": "2026-04-10T16:19:22+00:00",
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/polyfill-php80"
+        },
+        {
+            "name": "symfony/polyfill-php82",
+            "version": "v1.38.1",
+            "version_normalized": "1.38.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php82.git",
+                "reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
+                "reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "time": "2026-05-26T12:45:58+00:00",
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php82\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php82/tree/v1.38.1"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/polyfill-php82"
+        },
+        {
+            "name": "twig/twig",
+            "version": "v3.30.0",
+            "version_normalized": "3.30.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/twigphp/Twig.git",
+                "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef",
+                "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1.0",
+                "symfony/deprecation-contracts": "^2.5|^3",
+                "symfony/polyfill-ctype": "^1.8",
+                "symfony/polyfill-mbstring": "^1.3"
+            },
+            "require-dev": {
+                "php-cs-fixer/shim": "^3.0@stable",
+                "phpstan/phpstan": "^2.0@stable",
+                "psr/container": "^1.0|^2.0",
+                "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
+            },
+            "time": "2026-09-25T13:20:01+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "src/Resources/core.php",
+                    "src/Resources/debug.php",
+                    "src/Resources/escaper.php",
+                    "src/Resources/string_loader.php"
+                ],
+                "psr-4": {
+                    "Twig\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com",
+                    "homepage": "http://fabien.potencier.org",
+                    "role": "Lead Developer"
+                },
+                {
+                    "name": "Twig Team",
+                    "role": "Contributors"
+                },
+                {
+                    "name": "Armin Ronacher",
+                    "email": "armin.ronacher@active-4.com",
+                    "role": "Project Founder"
+                }
+            ],
+            "description": "Twig, the flexible, fast, and secure template language for PHP",
+            "homepage": "https://twig.symfony.com",
+            "keywords": [
+                "templating"
+            ],
+            "support": {
+                "issues": "https://github.com/twigphp/Twig/issues",
+                "source": "https://github.com/twigphp/Twig/tree/v3.30.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../twig/twig"
+        }
+    ],
+    "dev": true,
+    "dev-package-names": []
+}

+ 227 - 0
vendor/composer/installed.php

@@ -0,0 +1,227 @@
+<?php return array(
+    'root' => array(
+        'name' => '__root__',
+        'pretty_version' => '1.0.0+no-version-set',
+        'version' => '1.0.0.0',
+        'reference' => null,
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'dev' => true,
+    ),
+    'versions' => array(
+        '__root__' => array(
+            'pretty_version' => '1.0.0+no-version-set',
+            'version' => '1.0.0.0',
+            'reference' => null,
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../../',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'firebase/php-jwt' => array(
+            'pretty_version' => 'v7.2.1',
+            'version' => '7.2.1.0',
+            'reference' => '0501f5729b9e2448516674aff07c7d1727869b8e',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../firebase/php-jwt',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'google/apiclient' => array(
+            'pretty_version' => 'v2.20.1',
+            'version' => '2.20.1.0',
+            'reference' => '31b577e7012c4e68d5289d63aaf7e6d471ec1ff1',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../google/apiclient',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'google/apiclient-services' => array(
+            'pretty_version' => 'v0.461.0',
+            'version' => '0.461.0.0',
+            'reference' => '653eec9b7767d34f0cbe2a702cf2c2b825a4857d',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../google/apiclient-services',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'google/auth' => array(
+            'pretty_version' => 'v1.55.1',
+            'version' => '1.55.1.0',
+            'reference' => 'ef1b92cb42470b09b3caaf23bb9b4bb9a2dc787e',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../google/auth',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/guzzle' => array(
+            'pretty_version' => '8.2.0',
+            'version' => '8.2.0.0',
+            'reference' => '93939470950a9b11e2e84204166ef5e048c55fe4',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '3.0.2',
+            'version' => '3.0.2.0',
+            'reference' => '42118e66a53c492effaf92bc357e931985d5c6f9',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '3.1.0',
+            'version' => '3.1.0.0',
+            'reference' => 'a3059ba1a84c9139c4ae03cf0f45bea276c97c74',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'monolog/monolog' => array(
+            'pretty_version' => '3.12.0',
+            'version' => '3.12.0.0',
+            'reference' => '72c534fc0ab181ef52d92a68382318631e301608',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../monolog/monolog',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'phpmailer/phpmailer' => array(
+            'pretty_version' => 'v7.1.1',
+            'version' => '7.1.1.0',
+            'reference' => '1bc1716a507a65e039d4ac9d9adebbbd0d346e15',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phpmailer/phpmailer',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/cache' => array(
+            'pretty_version' => '3.0.0',
+            'version' => '3.0.0.0',
+            'reference' => 'aa5030cfa5405eccfdcb1083ce040c2cb8d253bf',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/cache',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-client' => array(
+            'pretty_version' => '1.0.3',
+            'version' => '1.0.3.0',
+            'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-client',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.1.0',
+            'version' => '1.1.0.0',
+            'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.1',
+            ),
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '2.0',
+            'version' => '2.0.0.0',
+            'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-message-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '2.0',
+            ),
+        ),
+        'psr/log' => array(
+            'pretty_version' => '3.0.2',
+            'version' => '3.0.2.0',
+            'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/log',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/log-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '3.0.0',
+            ),
+        ),
+        'symfony/deprecation-contracts' => array(
+            'pretty_version' => 'v3.7.1',
+            'version' => '3.7.1.0',
+            'reference' => 'f3202fa1b5097b0af062dc978b32ecf63404e31d',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'symfony/polyfill-ctype' => array(
+            'pretty_version' => 'v1.37.0',
+            'version' => '1.37.0.0',
+            'reference' => '141046a8f9477948ff284fa65be2095baafb94f2',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'symfony/polyfill-mbstring' => array(
+            'pretty_version' => 'v1.38.2',
+            'version' => '1.38.2.0',
+            'reference' => 'd3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'symfony/polyfill-php80' => array(
+            'pretty_version' => 'v1.37.0',
+            'version' => '1.37.0.0',
+            'reference' => 'dfb55726c3a76ea3b6459fcfda1ec2d80a682411',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/polyfill-php80',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'symfony/polyfill-php82' => array(
+            'pretty_version' => 'v1.38.1',
+            'version' => '1.38.1.0',
+            'reference' => '002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/polyfill-php82',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'twig/twig' => array(
+            'pretty_version' => 'v3.30.0',
+            'version' => '3.30.0.0',
+            'reference' => '8c737079b726af72ff8ef3c595be9f6a810ea1ef',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../twig/twig',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+    ),
+);

+ 25 - 0
vendor/composer/platform_check.php

@@ -0,0 +1,25 @@
+<?php
+
+// platform_check.php @generated by Composer
+
+$issues = array();
+
+if (!(PHP_VERSION_ID >= 80100)) {
+    $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
+}
+
+if ($issues) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+        } elseif (!headers_sent()) {
+            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
+        }
+    }
+    throw new \RuntimeException(
+        'Composer detected issues in your platform: ' . implode(' ', $issues)
+    );
+}

+ 272 - 0
vendor/firebase/php-jwt/CHANGELOG.md

@@ -0,0 +1,272 @@
+# Changelog
+
+## [7.1.1](https://github.com/googleapis/php-jwt/compare/v7.1.0...v7.1.1) (2026-09-02)
+
+
+### Bug Fixes
+
+* Limit the number of segments a JWT can be exploded into ([#639](https://github.com/googleapis/php-jwt/issues/639)) ([93d248c](https://github.com/googleapis/php-jwt/commit/93d248c650aea615d74a5ffa20c91c7a818e9b8d))
+
+## [7.1.0](https://github.com/googleapis/php-jwt/compare/v7.0.5...v7.1.0) (2026-06-11)
+
+
+### Features
+
+* Add support for PS256 ([#637](https://github.com/googleapis/php-jwt/issues/637)) ([aa0b447](https://github.com/googleapis/php-jwt/commit/aa0b4472c137255545b177a1840891304eac55f1))
+
+
+### Bug Fixes
+
+* Skip parsing valid unsupported curves ([#638](https://github.com/googleapis/php-jwt/issues/638)) ([8a6c2a7](https://github.com/googleapis/php-jwt/commit/8a6c2a75d7f45e781c93d618939f8a045fe69f53))
+* Update homepage URL in composer.json ([#635](https://github.com/googleapis/php-jwt/issues/635)) ([f7ae571](https://github.com/googleapis/php-jwt/commit/f7ae571efe255aed89c8f8d0f2cb7f71c5835664))
+* Validate numeric type of iat, nbf and exp claims in encode ([#634](https://github.com/googleapis/php-jwt/issues/634)) ([958e422](https://github.com/googleapis/php-jwt/commit/958e422961e874843a7fcc339e55a0101a66201d))
+
+## [7.0.5](https://github.com/firebase/php-jwt/compare/v7.0.4...v7.0.5) (2026-03-31)
+
+
+### Bug Fixes
+
+* RSA from JWK sometimes returns empty Instance ([#628](https://github.com/firebase/php-jwt/issues/628)) ([b4c78aa](https://github.com/firebase/php-jwt/commit/b4c78aa731664122198ad36c0033aa29e807397a))
+
+## [7.0.4](https://github.com/firebase/php-jwt/compare/v7.0.3...v7.0.4) (2026-03-27)
+
+
+### Bug Fixes
+
+* readme examples, add tests for all examples ([#626](https://github.com/firebase/php-jwt/issues/626)) ([510a00c](https://github.com/firebase/php-jwt/commit/510a00c0e6353bc7d68412fab67e57a13954cb46))
+* use urlsafeB64Decode everywhere ([#627](https://github.com/firebase/php-jwt/issues/627)) ([b889495](https://github.com/firebase/php-jwt/commit/b889495c83ddc3f3885ca3f0b65b41b1cb37a3b1))
+
+## [7.0.3](https://github.com/firebase/php-jwt/compare/v7.0.2...v7.0.3) (2026-02-18)
+
+
+### Miscellaneous Chores
+
+* add environment for Release Please job ([#619](https://github.com/firebase/php-jwt/issues/619)) ([300fd02](https://github.com/firebase/php-jwt/commit/300fd02c883f096c9067df652dbd23f62cb5e2a7))
+
+## [7.0.2](https://github.com/firebase/php-jwt/compare/v7.0.1...v7.0.2) (2025-12-16)
+
+
+### Bug Fixes
+
+* add key length validation for ec keys ([#615](https://github.com/firebase/php-jwt/issues/615)) ([7044f9a](https://github.com/firebase/php-jwt/commit/7044f9ae7e7d175d28cca71714feb236f1c0e252))
+
+## [7.0.0](https://github.com/firebase/php-jwt/compare/v6.11.1...v7.0.0) (2025-12-15)
+
+
+### ⚠️ ⚠️ ⚠️  Security Fixes ⚠️ ⚠️ ⚠️ 
+ * add key size validation ([#613](https://github.com/firebase/php-jwt/issues/613)) ([6b80341](https://github.com/firebase/php-jwt/commit/6b80341bf57838ea2d011487917337901cd71576))
+ **NOTE**: This fix will cause keys with a size below the minimally allowed size to break. 
+
+### Features
+
+* add SensitiveParameter attribute to security-critical parameters ([#603](https://github.com/firebase/php-jwt/issues/603)) ([4dbfac0](https://github.com/firebase/php-jwt/commit/4dbfac0260eeb0e9e643063c99998e3219cc539b))
+* store timestamp in `ExpiredException` ([#604](https://github.com/firebase/php-jwt/issues/604)) ([f174826](https://github.com/firebase/php-jwt/commit/f1748260d218a856b6a0c23715ac7fae1d7ca95b))
+
+
+### Bug Fixes
+
+* validate iat and nbf on payload ([#568](https://github.com/firebase/php-jwt/issues/568)) ([953b2c8](https://github.com/firebase/php-jwt/commit/953b2c88bb445b7e3bb82a5141928f13d7343afd))
+
+## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09)
+
+
+### Bug Fixes
+
+* update error text for consistency ([#528](https://github.com/firebase/php-jwt/issues/528)) ([c11113a](https://github.com/firebase/php-jwt/commit/c11113afa13265e016a669e75494b9203b8a7775))
+
+## [6.11.0](https://github.com/firebase/php-jwt/compare/v6.10.2...v6.11.0) (2025-01-23)
+
+
+### Features
+
+* support octet typed JWK ([#587](https://github.com/firebase/php-jwt/issues/587)) ([7cb8a26](https://github.com/firebase/php-jwt/commit/7cb8a265fa81edf2fa6ef8098f5bc5ae573c33ad))
+
+
+### Bug Fixes
+
+* refactor constructor Key to use PHP 8.0 syntax ([#577](https://github.com/firebase/php-jwt/issues/577)) ([29fa2ce](https://github.com/firebase/php-jwt/commit/29fa2ce9e0582cd397711eec1e80c05ce20fabca))
+
+## [6.10.2](https://github.com/firebase/php-jwt/compare/v6.10.1...v6.10.2) (2024-11-24)
+
+
+### Bug Fixes
+
+* Mitigate PHP8.4 deprecation warnings ([#570](https://github.com/firebase/php-jwt/issues/570)) ([76808fa](https://github.com/firebase/php-jwt/commit/76808fa227f3811aa5cdb3bf81233714b799a5b5))
+* support php 8.4 ([#583](https://github.com/firebase/php-jwt/issues/583)) ([e3d68b0](https://github.com/firebase/php-jwt/commit/e3d68b044421339443c74199edd020e03fb1887e))
+
+## [6.10.1](https://github.com/firebase/php-jwt/compare/v6.10.0...v6.10.1) (2024-05-18)
+
+
+### Bug Fixes
+
+* ensure ratelimit expiry is set every time ([#556](https://github.com/firebase/php-jwt/issues/556)) ([09cb208](https://github.com/firebase/php-jwt/commit/09cb2081c2c3bc0f61e2f2a5fbea5741f7498648))
+* ratelimit cache expiration ([#550](https://github.com/firebase/php-jwt/issues/550)) ([dda7250](https://github.com/firebase/php-jwt/commit/dda725033585ece30ff8cae8937320d7e9f18bae))
+
+## [6.10.0](https://github.com/firebase/php-jwt/compare/v6.9.0...v6.10.0) (2023-11-28)
+
+
+### Features
+
+* allow typ header override ([#546](https://github.com/firebase/php-jwt/issues/546)) ([79cb30b](https://github.com/firebase/php-jwt/commit/79cb30b729a22931b2fbd6b53f20629a83031ba9))
+
+## [6.9.0](https://github.com/firebase/php-jwt/compare/v6.8.1...v6.9.0) (2023-10-04)
+
+
+### Features
+
+* add payload to jwt exception ([#521](https://github.com/firebase/php-jwt/issues/521)) ([175edf9](https://github.com/firebase/php-jwt/commit/175edf958bb61922ec135b2333acf5622f2238a2))
+
+## [6.8.1](https://github.com/firebase/php-jwt/compare/v6.8.0...v6.8.1) (2023-07-14)
+
+
+### Bug Fixes
+
+* accept float claims but round down to ignore them ([#492](https://github.com/firebase/php-jwt/issues/492)) ([3936842](https://github.com/firebase/php-jwt/commit/39368423beeaacb3002afa7dcb75baebf204fe7e))
+* different BeforeValidException messages for nbf and iat ([#526](https://github.com/firebase/php-jwt/issues/526)) ([0a53cf2](https://github.com/firebase/php-jwt/commit/0a53cf2986e45c2bcbf1a269f313ebf56a154ee4))
+
+## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14)
+
+
+### Features
+
+* add support for P-384 curve ([#515](https://github.com/firebase/php-jwt/issues/515)) ([5de4323](https://github.com/firebase/php-jwt/commit/5de4323f4baf4d70bca8663bd87682a69c656c3d))
+
+
+### Bug Fixes
+
+* handle invalid http responses ([#508](https://github.com/firebase/php-jwt/issues/508)) ([91c39c7](https://github.com/firebase/php-jwt/commit/91c39c72b22fc3e1191e574089552c1f2041c718))
+
+## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14)
+
+
+### Features
+
+* add ed25519 support to JWK (public keys) ([#452](https://github.com/firebase/php-jwt/issues/452)) ([e53979a](https://github.com/firebase/php-jwt/commit/e53979abae927de916a75b9d239cfda8ce32be2a))
+
+## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13)
+
+
+### Features
+
+* allow get headers when decoding token ([#442](https://github.com/firebase/php-jwt/issues/442)) ([fb85f47](https://github.com/firebase/php-jwt/commit/fb85f47cfaeffdd94faf8defdf07164abcdad6c3))
+
+
+### Bug Fixes
+
+* only check iat if nbf is not used ([#493](https://github.com/firebase/php-jwt/issues/493)) ([398ccd2](https://github.com/firebase/php-jwt/commit/398ccd25ea12fa84b9e4f1085d5ff448c21ec797))
+
+## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12)
+
+
+### Bug Fixes
+
+* allow KID of '0' ([#505](https://github.com/firebase/php-jwt/issues/505)) ([9dc46a9](https://github.com/firebase/php-jwt/commit/9dc46a9c3e5801294249cfd2554c5363c9f9326a))
+
+
+### Miscellaneous Chores
+
+* drop support for PHP 7.3 ([#495](https://github.com/firebase/php-jwt/issues/495))
+
+## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08)
+
+
+### Features
+
+* add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95))
+* improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c))
+
+## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01)
+
+
+### Bug Fixes
+
+* check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd))
+
+## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01)
+
+
+### Bug Fixes
+
+* casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22))
+* string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2))
+
+## 6.3.0 / 2022-07-15
+
+ - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399))
+ - Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435))
+
+## 6.2.0 / 2022-05-14
+
+ - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397))
+ - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)).
+
+## 6.1.0 / 2022-03-23
+
+ - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0
+ - Add parameter typing and return types where possible
+
+## 6.0.0 / 2022-01-24
+
+ - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information.
+ - New Key object to prevent key/algorithm type confusion (#365)
+ - Add JWK support (#273)
+ - Add ES256 support (#256)
+ - Add ES384 support (#324)
+ - Add Ed25519 support (#343)
+
+## 5.0.0 / 2017-06-26
+- Support RS384 and RS512.
+  See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)!
+- Add an example for RS256 openssl.
+  See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)!
+- Detect invalid Base64 encoding in signature.
+  See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)!
+- Update `JWT::verify` to handle OpenSSL errors.
+  See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)!
+- Add `array` type hinting to `decode` method
+  See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)!
+- Add all JSON error types.
+  See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)!
+- Bugfix 'kid' not in given key list.
+  See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)!
+- Miscellaneous cleanup, documentation and test fixes.
+  See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115),
+  [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and
+  [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman),
+  [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)!
+
+## 4.0.0 / 2016-07-17
+- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)!
+- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)!
+- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)!
+- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)!
+
+## 3.0.0 / 2015-07-22
+- Minimum PHP version updated from `5.2.0` to `5.3.0`.
+- Add `\Firebase\JWT` namespace. See
+[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to
+[@Dashron](https://github.com/Dashron)!
+- Require a non-empty key to decode and verify a JWT. See
+[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to
+[@sjones608](https://github.com/sjones608)!
+- Cleaner documentation blocks in the code. See
+[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to
+[@johanderuijter](https://github.com/johanderuijter)!
+
+## 2.2.0 / 2015-06-22
+- Add support for adding custom, optional JWT headers to `JWT::encode()`. See
+[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to
+[@mcocaro](https://github.com/mcocaro)!
+
+## 2.1.0 / 2015-05-20
+- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew
+between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)!
+- Add support for passing an object implementing the `ArrayAccess` interface for
+`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)!
+
+## 2.0.0 / 2015-04-01
+- **Note**: It is strongly recommended that you update to > v2.0.0 to address
+  known security vulnerabilities in prior versions when both symmetric and
+  asymmetric keys are used together.
+- Update signature for `JWT::decode(...)` to require an array of supported
+  algorithms to use when verifying token signatures.

+ 30 - 0
vendor/firebase/php-jwt/LICENSE

@@ -0,0 +1,30 @@
+Copyright (c) 2011, Neuman Vong
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of the copyright holder nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 457 - 0
vendor/firebase/php-jwt/README.md

@@ -0,0 +1,457 @@
+![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg)
+[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt)
+[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt)
+[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt)
+
+PHP-JWT
+=======
+A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519).
+
+Installation
+------------
+
+Use composer to manage your dependencies and download PHP-JWT:
+
+```bash
+composer require firebase/php-jwt
+```
+
+Optionally, install the `paragonie/sodium_compat` package from composer if your
+php env does not have libsodium installed:
+
+```bash
+composer require paragonie/sodium_compat
+```
+
+## Example
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+$key = 'example_key_of_sufficient_length';
+$payload = [
+    'iss' => 'example.org',
+    'aud' => 'example.com',
+    'iat' => 1356999524,
+    'nbf' => 1357000000
+];
+
+/**
+ * IMPORTANT:
+ * You must specify supported algorithms for your application. See
+ * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
+ * for a list of spec-compliant algorithms.
+ */
+$jwt = JWT::encode($payload, $key, 'HS256');
+$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
+print_r($decoded);
+
+// Pass a stdClass in as the third parameter to get the decoded header values
+$headers = new stdClass();
+$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers);
+print_r($headers);
+
+/*
+ NOTE: This will now be an object instead of an associative array. To get
+ an associative array, you will need to cast it as such:
+*/
+
+$decoded_array = (array) $decoded;
+
+/**
+ * You can add a leeway to account for when there is a clock skew times between
+ * the signing and verifying servers. It is recommended that this leeway should
+ * not be bigger than a few minutes.
+ *
+ * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef
+ */
+JWT::$leeway = 60; // $leeway in seconds
+$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
+```
+
+## Example encode/decode headers
+
+Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by
+this library. This is because without verifying the JWT, the header values could have been tampered with.
+Any value pulled from an unverified header should be treated as if it could be any string sent in from an
+attacker.  If this is something you still want to do in your application for whatever reason, it's possible to
+decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT
+header part:
+```php
+use Firebase\JWT\JWT;
+
+$key = 'example_key_of_sufficient_length';
+$payload = [
+    'iss' => 'example.org',
+    'aud' => 'example.com',
+    'iat' => 1356999524,
+    'nbf' => 1357000000
+];
+
+$headers = [
+    'x-forwarded-for' => 'www.google.com'
+];
+
+// Encode headers in the JWT string
+$jwt = JWT::encode($payload, $key, 'HS256', null, $headers);
+
+// Decode headers from the JWT string WITHOUT validation
+// **IMPORTANT**: This operation is vulnerable to attacks, as the JWT has not yet been verified.
+// These headers could be any value sent by an attacker.
+list($headersB64, $payloadB64, $sig) = explode('.', $jwt);
+$decoded = json_decode(base64_decode($headersB64), true);
+
+print_r($decoded);
+```
+
+## Example with RS256 (openssl)
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+$privateKey = <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew
+M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S
+JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM
+78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5
+HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ
+WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k
+6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc
+VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2
+oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b
+c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW
+h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK
+bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M
+39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l
+3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG
+vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC
+6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb
+OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP
+nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y
+xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG
+8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L
+hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15
+YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44
+DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI
+RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek
+2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og
+-----END RSA PRIVATE KEY-----
+EOD;
+
+$publicKey = <<<EOD
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzWHNM5f+amCjQztc5QT
+fJfzCC5J4nuW+L/aOxZ4f8J3FrewM2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJ
+hzkPYLae7bTVro3hok0zDITR8F6SJGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548t
+u4czCuqU8BGVOlnp6IqBHhAswNMM78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vS
+opcT51koWOgiTf3C7nJUoMWZHZI5HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTz
+TTqo1SCSH2pooJl9O8at6kkRYsrZWwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/B
+wQIDAQAB
+-----END PUBLIC KEY-----
+EOD;
+
+$payload = [
+    'iss' => 'example.org',
+    'aud' => 'example.com',
+    'iat' => 1356999524,
+    'nbf' => 1357000000
+];
+
+$jwt = JWT::encode($payload, $privateKey, 'RS256');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
+
+/*
+ NOTE: This will now be an object instead of an associative array. To get
+ an associative array, you will need to cast it as such:
+*/
+
+$decoded_array = (array) $decoded;
+echo "Decode:\n" . print_r($decoded_array, true) . "\n";
+```
+
+## Example with a passphrase
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+// Your passphrase
+$passphrase = '[YOUR_PASSPHRASE]';
+
+// Your private key file with passphrase
+// Can be generated with "ssh-keygen -t rsa -m pem"
+$privateKeyFile = '/path/to/key-with-passphrase.pem';
+
+/** @var OpenSSLAsymmetricKey $privateKey */
+$privateKey = openssl_pkey_get_private(
+    file_get_contents($privateKeyFile),
+    $passphrase
+);
+
+$payload = [
+    'iss' => 'example.org',
+    'aud' => 'example.com',
+    'iat' => 1356999524,
+    'nbf' => 1357000000
+];
+
+$jwt = JWT::encode($payload, $privateKey, 'RS256');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+// Get public key from the private key, or pull from from a file.
+$publicKey = openssl_pkey_get_details($privateKey)['key'];
+
+$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
+echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
+```
+
+## Example with EdDSA (libsodium and Ed25519 signature)
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+// Public and private keys are expected to be Base64 encoded. The last
+// non-empty line is used so that keys can be generated with
+// sodium_crypto_sign_keypair(). The secret keys generated by other tools may
+// need to be adjusted to match the input expected by libsodium.
+
+$keyPair = sodium_crypto_sign_keypair();
+
+$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
+
+$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
+
+$payload = [
+    'iss' => 'example.org',
+    'aud' => 'example.com',
+    'iat' => 1356999524,
+    'nbf' => 1357000000
+];
+
+$jwt = JWT::encode($payload, $privateKey, 'EdDSA');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
+echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
+```
+
+## Example with multiple keys
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+// Example RSA keys from previous example
+// $privateRsKey = '...';
+// $publicRsKey = '...';
+
+// Example EdDSA keys from previous example
+// $privateEcKey = '...';
+// $publicEcKey = '...';
+
+$payload = [
+    'iss' => 'example.org',
+    'aud' => 'example.com',
+    'iat' => 1356999524,
+    'nbf' => 1357000000
+];
+
+$jwt1 = JWT::encode($payload, $privateRsKey, 'RS256', 'kid1');
+$jwt2 = JWT::encode($payload, $privateEcKey, 'EdDSA', 'kid2');
+echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
+echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
+
+$keys = [
+    'kid1' => new Key($publicRsKey, 'RS256'),
+    'kid2' => new Key($publicEcKey, 'EdDSA'),
+];
+
+$decoded1 = JWT::decode($jwt1, $keys);
+$decoded2 = JWT::decode($jwt2, $keys);
+
+echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n";
+echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n";
+```
+
+## Example with PS256
+
+### Note
+PHP's OpenSSL extension does not support RSASSA-PSS signatures (PS256) by default, so we provide support via a soft dependency on the [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) library. It is necessary to install this library in your project if you plan to use PS256.
+```bash
+composer install phpseclib/phpseclib:^3.0
+```
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+$privateRsKey = '-----BEGIN RSA PRIVATE KEY----- ...';
+$publicKey = '-----BEGIN PUBLIC KEY----- ...';
+
+$payload = [
+    'iss' => 'example.org',
+    'aud' => 'example.com',
+    'iat' => 1356999524,
+    'nbf' => 1357000000
+];
+
+/**
+* PS256 support requires phpseclib/phpseclib
+*/
+$jwt = JWT::encode($payload, $privateRsKey, 'PS256', 'keyid');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+$decoded = JWT::decode($jwt, new Key($publicKey, 'PS256'));
+echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
+```
+
+## Using JWKs
+
+```php
+use Firebase\JWT\JWK;
+use Firebase\JWT\JWT;
+
+// Set of keys. The "keys" key is required. For example, the JSON response to
+// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk
+$jwks = ['keys' => []];
+
+// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key
+// objects. Pass this as the second parameter to JWT::decode.
+$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks));
+print_r($decoded);
+```
+
+## Using Cached Key Sets
+
+The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI.
+This has the following advantages:
+
+1. The results are cached for performance.
+2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation.
+3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second.
+
+```php
+use Firebase\JWT\CachedKeySet;
+use Firebase\JWT\JWT;
+
+// The URI for the JWKS you wish to cache the results from
+$jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
+
+// Create an HTTP client (can be any PSR-7 compatible HTTP client)
+$httpClient = new GuzzleHttp\Client();
+
+// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory)
+$httpFactory = new GuzzleHttp\Psr7\HttpFactory();
+
+// Create a cache item pool (can be any PSR-6 compatible cache item pool)
+$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
+
+$keySet = new CachedKeySet(
+    $jwksUri,
+    $httpClient,
+    $httpFactory,
+    $cacheItemPool,
+    null, // $expiresAfter int seconds to set the JWKS to expire
+    true  // $rateLimit    true to enable rate limit of 10 RPS on lookup of invalid keys
+);
+
+$jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above
+$decoded = JWT::decode($jwt, $keySet);
+```
+
+Miscellaneous
+-------------
+
+#### Exception Handling
+
+When a call to `JWT::decode` is invalid, it will throw one of the following exceptions:
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\SignatureInvalidException;
+use Firebase\JWT\BeforeValidException;
+use Firebase\JWT\ExpiredException;
+use DomainException;
+use InvalidArgumentException;
+use UnexpectedValueException;
+
+try {
+    $decoded = JWT::decode($jwt, $keys);
+} catch (InvalidArgumentException $e) {
+    // provided key/key-array is empty or malformed.
+} catch (DomainException $e) {
+    // provided algorithm is unsupported OR
+    // provided key is invalid OR
+    // unknown error thrown in openSSL or libsodium OR
+    // libsodium is required but not available.
+} catch (SignatureInvalidException $e) {
+    // provided JWT signature verification failed.
+} catch (BeforeValidException $e) {
+    // provided JWT is trying to be used before "nbf" claim OR
+    // provided JWT is trying to be used before "iat" claim.
+} catch (ExpiredException $e) {
+    // provided JWT is trying to be used after "exp" claim.
+} catch (UnexpectedValueException $e) {
+    // provided JWT is malformed OR
+    // provided JWT is missing an algorithm / using an unsupported algorithm OR
+    // provided JWT algorithm does not match provided key OR
+    // provided key ID in key/key-array is empty or invalid.
+}
+```
+
+All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException`, and can be simplified
+like this:
+
+```php
+use Firebase\JWT\JWT;
+use UnexpectedValueException;
+try {
+    $decoded = JWT::decode($jwt, $keys);
+} catch (LogicException $e) {
+    // errors having to do with environmental setup or malformed JWT Keys
+} catch (UnexpectedValueException $e) {
+    // errors having to do with JWT signature and claims
+}
+```
+
+#### Casting to array
+
+The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays
+instead, you can do the following:
+
+```php
+// return type is stdClass
+$decoded = JWT::decode($jwt, $keys);
+
+// cast to array
+$decoded = json_decode(json_encode($decoded), true);
+```
+
+Tests
+-----
+Run the tests using phpunit:
+
+```bash
+$ composer update
+$ vendor/bin/phpunit -c phpunit.xml.dist
+PHPUnit 3.7.10 by Sebastian Bergmann.
+.....
+Time: 0 seconds, Memory: 2.50Mb
+OK (5 tests, 5 assertions)
+```
+
+New Lines in private keys
+-----
+
+If your private key contains `\n` characters, be sure to wrap it in double quotes `""`
+and not single quotes `''` in order to properly interpret the escaped characters.
+
+License
+-------
+[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause).

+ 1 - 0
vendor/firebase/php-jwt/VERSION

@@ -0,0 +1 @@
+7.2.1

+ 53 - 0
vendor/firebase/php-jwt/composer.json

@@ -0,0 +1,53 @@
+{
+    "name": "firebase/php-jwt",
+    "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+    "homepage": "https://github.com/googleapis/php-jwt",
+    "keywords": [
+        "php",
+        "jwt"
+    ],
+    "authors": [
+        {
+            "name": "Neuman Vong",
+            "email": "neuman+pear@twilio.com",
+            "role": "Developer"
+        },
+        {
+            "name": "Anant Narayanan",
+            "email": "anant@php.net",
+            "role": "Developer"
+        }
+    ],
+    "license": "BSD-3-Clause",
+    "require": {
+        "php": "^8.0"
+    },
+    "suggest": {
+        "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
+        "ext-sodium": "Support EdDSA (Ed25519) signatures",
+        "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures"
+    },
+    "autoload": {
+        "psr-4": {
+            "Firebase\\JWT\\": "src"
+        }
+    },
+    "require-dev": {
+        "guzzlehttp/guzzle": "^7.4||^8.0",
+        "phpspec/prophecy-phpunit": "^2.2",
+        "phpunit/phpunit": "^9.5",
+        "psr/cache": "^2.0||^3.0",
+        "psr/http-client": "^1.0",
+        "psr/http-factory": "^1.0",
+        "phpfastcache/phpfastcache": "^9.2",
+        "phpseclib/phpseclib": "~3.0"
+    },
+    "extra": {
+        "component": {
+            "id": "jwt",
+            "target": "googleapis/php-jwt.git",
+            "path": "Jwt",
+            "entry": "README.md"
+        }
+    }
+}

+ 18 - 0
vendor/firebase/php-jwt/src/BeforeValidException.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Firebase\JWT;
+
+class BeforeValidException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface
+{
+    private object $payload;
+
+    public function setPayload(object $payload): void
+    {
+        $this->payload = $payload;
+    }
+
+    public function getPayload(): object
+    {
+        return $this->payload;
+    }
+}

+ 275 - 0
vendor/firebase/php-jwt/src/CachedKeySet.php

@@ -0,0 +1,275 @@
+<?php
+
+namespace Firebase\JWT;
+
+use ArrayAccess;
+use InvalidArgumentException;
+use LogicException;
+use OutOfBoundsException;
+use Psr\Cache\CacheItemInterface;
+use Psr\Cache\CacheItemPoolInterface;
+use Psr\Http\Client\ClientInterface;
+use Psr\Http\Message\RequestFactoryInterface;
+use RuntimeException;
+use UnexpectedValueException;
+
+/**
+ * @implements ArrayAccess<string, Key>
+ */
+class CachedKeySet implements ArrayAccess
+{
+    /**
+     * @var string
+     */
+    private $jwksUri;
+    /**
+     * @var ClientInterface
+     */
+    private $httpClient;
+    /**
+     * @var RequestFactoryInterface
+     */
+    private $httpFactory;
+    /**
+     * @var CacheItemPoolInterface
+     */
+    private $cache;
+    /**
+     * @var ?int
+     */
+    private $expiresAfter;
+    /**
+     * @var ?CacheItemInterface
+     */
+    private $cacheItem;
+    /**
+     * @var array<string, array<mixed>>
+     */
+    private $keySet;
+    /**
+     * @var string
+     */
+    private $cacheKey;
+    /**
+     * @var string
+     */
+    private $cacheKeyPrefix = 'jwks';
+    /**
+     * @var int
+     */
+    private $maxKeyLength = 64;
+    /**
+     * @var bool
+     */
+    private $rateLimit;
+    /**
+     * @var string
+     */
+    private $rateLimitCacheKey;
+    /**
+     * @var int
+     */
+    private $maxCallsPerMinute = 10;
+    /**
+     * @var string|null
+     */
+    private $defaultAlg;
+
+    public function __construct(
+        string $jwksUri,
+        ClientInterface $httpClient,
+        RequestFactoryInterface $httpFactory,
+        CacheItemPoolInterface $cache,
+        ?int $expiresAfter = null,
+        bool $rateLimit = false,
+        ?string $defaultAlg = null
+    ) {
+        $this->jwksUri = $jwksUri;
+        $this->httpClient = $httpClient;
+        $this->httpFactory = $httpFactory;
+        $this->cache = $cache;
+        $this->expiresAfter = $expiresAfter;
+        $this->rateLimit = $rateLimit;
+        $this->defaultAlg = $defaultAlg;
+        $this->setCacheKeys();
+    }
+
+    /**
+     * @param string $keyId
+     * @return Key
+     */
+    public function offsetGet($keyId): Key
+    {
+        if (!$this->keyIdExists($keyId)) {
+            throw new OutOfBoundsException('Key ID not found');
+        }
+        return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
+    }
+
+    /**
+     * @param string $keyId
+     * @return bool
+     */
+    public function offsetExists($keyId): bool
+    {
+        return $this->keyIdExists($keyId);
+    }
+
+    /**
+     * @param string $offset
+     * @param Key $value
+     */
+    public function offsetSet($offset, $value): void
+    {
+        throw new LogicException('Method not implemented');
+    }
+
+    /**
+     * @param string $offset
+     */
+    public function offsetUnset($offset): void
+    {
+        throw new LogicException('Method not implemented');
+    }
+
+    /**
+     * @return array<mixed>
+     */
+    private function formatJwksForCache(string $jwks): array
+    {
+        $jwks = json_decode($jwks, true);
+
+        if (!isset($jwks['keys'])) {
+            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
+        }
+
+        if (empty($jwks['keys'])) {
+            throw new InvalidArgumentException('JWK Set did not contain any keys');
+        }
+
+        $keys = [];
+        foreach ($jwks['keys'] as $k => $v) {
+            $kid = isset($v['kid']) ? $v['kid'] : $k;
+            $keys[(string) $kid] = $v;
+        }
+
+        return $keys;
+    }
+
+    private function keyIdExists(string $keyId): bool
+    {
+        if (null === $this->keySet) {
+            $item = $this->getCacheItem();
+            // Try to load keys from cache
+            if ($item->isHit()) {
+                // item found! retrieve it
+                $this->keySet = $item->get();
+                // If the cached item is a string, the JWKS response was cached (previous behavior).
+                // Parse this into expected format array<kid, jwk> instead.
+                if (\is_string($this->keySet)) {
+                    $this->keySet = $this->formatJwksForCache($this->keySet);
+                }
+            }
+        }
+
+        if (!isset($this->keySet[$keyId])) {
+            if ($this->rateLimitExceeded()) {
+                return false;
+            }
+            $request = $this->httpFactory->createRequest('GET', $this->jwksUri);
+            $jwksResponse = $this->httpClient->sendRequest($request);
+            if ($jwksResponse->getStatusCode() !== 200) {
+                throw new UnexpectedValueException(
+                    \sprintf(
+                        'HTTP Error: %d %s for URI "%s"',
+                        $jwksResponse->getStatusCode(),
+                        $jwksResponse->getReasonPhrase(),
+                        $this->jwksUri,
+                    ),
+                    $jwksResponse->getStatusCode()
+                );
+            }
+            $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
+
+            if (!isset($this->keySet[$keyId])) {
+                return false;
+            }
+
+            $item = $this->getCacheItem();
+            $item->set($this->keySet);
+            if ($this->expiresAfter) {
+                $item->expiresAfter($this->expiresAfter);
+            }
+            $this->cache->save($item);
+        }
+
+        return true;
+    }
+
+    private function rateLimitExceeded(): bool
+    {
+        if (!$this->rateLimit) {
+            return false;
+        }
+
+        $cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
+
+        $cacheItemData = [];
+        if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
+            $cacheItemData = $data;
+        }
+
+        $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
+        $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
+
+        if (++$callsPerMinute > $this->maxCallsPerMinute) {
+            return true;
+        }
+
+        $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
+        $cacheItem->expiresAt($expiry);
+        $this->cache->save($cacheItem);
+        return false;
+    }
+
+    private function getCacheItem(): CacheItemInterface
+    {
+        if (\is_null($this->cacheItem)) {
+            $this->cacheItem = $this->cache->getItem($this->cacheKey);
+        }
+
+        return $this->cacheItem;
+    }
+
+    private function setCacheKeys(): void
+    {
+        if (empty($this->jwksUri)) {
+            throw new RuntimeException('JWKS URI is empty');
+        }
+
+        // ensure we do not have illegal characters
+        $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
+
+        // add prefix
+        $key = $this->cacheKeyPrefix . $key;
+
+        // Hash keys if they exceed $maxKeyLength of 64
+        if (\strlen($key) > $this->maxKeyLength) {
+            $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
+        }
+
+        $this->cacheKey = $key;
+
+        if ($this->rateLimit) {
+            // add prefix
+            $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
+
+            // Hash keys if they exceed $maxKeyLength of 64
+            if (\strlen($rateLimitKey) > $this->maxKeyLength) {
+                $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
+            }
+
+            $this->rateLimitCacheKey = $rateLimitKey;
+        }
+    }
+}

+ 30 - 0
vendor/firebase/php-jwt/src/ExpiredException.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Firebase\JWT;
+
+class ExpiredException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface
+{
+    private object $payload;
+
+    private ?int $timestamp = null;
+
+    public function setPayload(object $payload): void
+    {
+        $this->payload = $payload;
+    }
+
+    public function getPayload(): object
+    {
+        return $this->payload;
+    }
+
+    public function setTimestamp(int $timestamp): void
+    {
+        $this->timestamp = $timestamp;
+    }
+
+    public function getTimestamp(): ?int
+    {
+        return $this->timestamp;
+    }
+}

+ 376 - 0
vendor/firebase/php-jwt/src/JWK.php

@@ -0,0 +1,376 @@
+<?php
+
+namespace Firebase\JWT;
+
+use DomainException;
+use InvalidArgumentException;
+use UnexpectedValueException;
+
+/**
+ * JSON Web Key implementation, based on this spec:
+ * https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
+ *
+ * PHP version 5
+ *
+ * @category Authentication
+ * @package  Authentication_JWT
+ * @author   Bui Sy Nguyen <nguyenbs@gmail.com>
+ * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link     https://github.com/firebase/php-jwt
+ */
+class JWK
+{
+    private const OID = '1.2.840.10045.2.1';
+    private const ASN1_OBJECT_IDENTIFIER = 0x06;
+    private const ASN1_SEQUENCE = 0x10; // also defined in JWT
+    private const ASN1_BIT_STRING = 0x03;
+    private const EC_CURVES = [
+        'P-256' => '1.2.840.10045.3.1.7', // Len: 64
+        'secp256k1' => '1.3.132.0.10', // Len: 64
+        'P-384' => '1.3.132.0.34', // Len: 96
+        // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
+    ];
+
+    // Known standard curves from the IANA JOSE registry which are not supported
+    private const KNOWN_UNSUPPORTED_EC_CURVES = [
+        'P-521',   // RFC 7518
+        'Ed25519', // RFC 8037
+        'Ed448',   // RFC 8037
+        'X25519',  // RFC 8037
+        'X448'     // RFC 8037
+    ];
+
+    // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
+    // This library supports the following subtypes:
+    private const OKP_SUBTYPES = [
+        'Ed25519' => true, // RFC 8037
+    ];
+
+    /**
+     * Parse a set of JWK keys
+     *
+     * @param array<mixed> $jwks The JSON Web Key Set as an associative array
+     * @param string       $defaultAlg The algorithm for the Key object if "alg" is not set in the
+     *                                 JSON Web Key Set
+     *
+     * @return array<string, Key> An associative array of key IDs (kid) to Key objects
+     *
+     * @throws InvalidArgumentException     Provided JWK Set is empty
+     * @throws UnexpectedValueException     Provided JWK Set was invalid
+     * @throws DomainException              OpenSSL failure
+     *
+     * @uses parseKey
+     */
+    public static function parseKeySet(#[\SensitiveParameter] array $jwks, ?string $defaultAlg = null): array
+    {
+        $keys = [];
+
+        if (!isset($jwks['keys'])) {
+            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
+        }
+
+        if (empty($jwks['keys'])) {
+            throw new InvalidArgumentException('JWK Set did not contain any keys');
+        }
+
+        foreach ($jwks['keys'] as $k => $v) {
+            $kid = isset($v['kid']) ? $v['kid'] : $k;
+            if ($key = self::parseKey($v, $defaultAlg)) {
+                $keys[(string) $kid] = $key;
+            }
+        }
+
+        if (0 === \count($keys)) {
+            throw new UnexpectedValueException('No supported algorithms found in JWK Set');
+        }
+
+        return $keys;
+    }
+
+    /**
+     * Parse a JWK key
+     *
+     * @param array<mixed> $jwk An individual JWK
+     * @param string       $defaultAlg The algorithm for the Key object if "alg" is not set in the
+     *                                 JSON Web Key Set
+     *
+     * @return Key The key object for the JWK
+     *
+     * @throws InvalidArgumentException     Provided JWK is empty
+     * @throws UnexpectedValueException     Provided JWK was invalid
+     * @throws DomainException              OpenSSL failure
+     *
+     * @uses createPemFromModulusAndExponent
+     */
+    public static function parseKey(#[\SensitiveParameter] array $jwk, ?string $defaultAlg = null): ?Key
+    {
+        if (empty($jwk)) {
+            throw new InvalidArgumentException('JWK must not be empty');
+        }
+
+        if (!isset($jwk['kty'])) {
+            throw new UnexpectedValueException('JWK must contain a "kty" parameter');
+        }
+
+        if (!isset($jwk['alg'])) {
+            if (\is_null($defaultAlg)) {
+                // The "alg" parameter is optional in a KTY, but an algorithm is required
+                // for parsing in this library. Use the $defaultAlg parameter when parsing the
+                // key set in order to prevent this error.
+                // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
+                throw new UnexpectedValueException('JWK must contain an "alg" parameter');
+            }
+            $jwk['alg'] = $defaultAlg;
+        }
+
+        switch ($jwk['kty']) {
+            case 'RSA':
+                if (!empty($jwk['d'])) {
+                    throw new UnexpectedValueException('RSA private keys are not supported');
+                }
+                if (!isset($jwk['n']) || !isset($jwk['e'])) {
+                    throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
+                }
+
+                $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
+                $publicKey = \openssl_pkey_get_public($pem);
+                if (false === $publicKey) {
+                    throw new DomainException(
+                        'OpenSSL error: ' . \openssl_error_string()
+                    );
+                }
+                return new Key($publicKey, $jwk['alg']);
+            case 'EC':
+                if (isset($jwk['d'])) {
+                    // The key is actually a private key
+                    throw new UnexpectedValueException('Key data must be for a public key');
+                }
+
+                if (empty($jwk['crv'])) {
+                    throw new UnexpectedValueException('crv not set');
+                }
+
+                if (!isset(self::EC_CURVES[$jwk['crv']])) {
+                    if (!\in_array($jwk['crv'], self::KNOWN_UNSUPPORTED_EC_CURVES)) {
+                        throw new DomainException('Unrecognised EC curve');
+                    }
+                    return null;
+                }
+
+                if (empty($jwk['x']) || empty($jwk['y'])) {
+                    throw new UnexpectedValueException('x and y not set');
+                }
+
+                $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
+                return new Key($publicKey, $jwk['alg']);
+            case 'OKP':
+                if (isset($jwk['d'])) {
+                    // The key is actually a private key
+                    throw new UnexpectedValueException('Key data must be for a public key');
+                }
+
+                if (!isset($jwk['crv'])) {
+                    throw new UnexpectedValueException('crv not set');
+                }
+
+                if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
+                    throw new DomainException('Unrecognised or unsupported OKP key subtype');
+                }
+
+                if (empty($jwk['x'])) {
+                    throw new UnexpectedValueException('x not set');
+                }
+
+                // This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
+                $publicKey = JWT::convertBase64urlToBase64($jwk['x']);
+                return new Key($publicKey, $jwk['alg']);
+            case 'oct':
+                if (!isset($jwk['k'])) {
+                    throw new UnexpectedValueException('k not set');
+                }
+
+                return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']);
+            default:
+                break;
+        }
+
+        return null;
+    }
+
+    /**
+     * Converts the EC JWK values to pem format.
+     *
+     * @param   string  $crv The EC curve (only P-256 & P-384 is supported)
+     * @param   string  $x   The EC x-coordinate
+     * @param   string  $y   The EC y-coordinate
+     *
+     * @return  string
+     */
+    private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
+    {
+        $pem =
+            self::encodeDER(
+                self::ASN1_SEQUENCE,
+                self::encodeDER(
+                    self::ASN1_SEQUENCE,
+                    self::encodeDER(
+                        self::ASN1_OBJECT_IDENTIFIER,
+                        self::encodeOID(self::OID)
+                    )
+                    . self::encodeDER(
+                        self::ASN1_OBJECT_IDENTIFIER,
+                        self::encodeOID(self::EC_CURVES[$crv])
+                    )
+                ) .
+                self::encodeDER(
+                    self::ASN1_BIT_STRING,
+                    \chr(0x00) . \chr(0x04)
+                    . JWT::urlsafeB64Decode($x)
+                    . JWT::urlsafeB64Decode($y)
+                )
+            );
+
+        return \sprintf(
+            "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
+            wordwrap(base64_encode($pem), 64, "\n", true)
+        );
+    }
+
+    /**
+     * Create a public key represented in PEM format from RSA modulus and exponent information
+     *
+     * @param string $n The RSA modulus encoded in Base64
+     * @param string $e The RSA exponent encoded in Base64
+     *
+     * @return string The RSA public key represented in PEM format
+     *
+     * @uses encodeLength
+     */
+    private static function createPemFromModulusAndExponent(
+        string $n,
+        string $e
+    ): string {
+        $mod = JWT::urlsafeB64Decode($n);
+        $exp = JWT::urlsafeB64Decode($e);
+        // Correct encoding for ASN1, as ints are represented as unsigned in jwk
+        // but signed in ASN1. Prepending null byte makes it unsigned.
+        if (\strlen($mod) > 0 && \ord($mod[0]) >= 128) {
+            $mod = \chr(0) . $mod;
+        }
+        if (\strlen($exp) > 0 && \ord($exp[0]) >= 128) {
+            $exp = \chr(0) . $exp;
+        }
+
+        $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
+        $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
+
+        $rsaPublicKey = \pack(
+            'Ca*a*a*',
+            48,
+            self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
+            $modulus,
+            $publicExponent
+        );
+
+        // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
+        $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
+        $rsaPublicKey = \chr(0) . $rsaPublicKey;
+        $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
+
+        $rsaPublicKey = \pack(
+            'Ca*a*',
+            48,
+            self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
+            $rsaOID . $rsaPublicKey
+        );
+
+        return "-----BEGIN PUBLIC KEY-----\r\n" .
+            \chunk_split(\base64_encode($rsaPublicKey), 64) .
+            '-----END PUBLIC KEY-----';
+    }
+
+    /**
+     * DER-encode the length
+     *
+     * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4.  See
+     * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3}
+     * for more information.
+     *
+     * @param int $length
+     * @return string
+     */
+    private static function encodeLength(int $length): string
+    {
+        if ($length <= 0x7F) {
+            return \chr($length);
+        }
+
+        $temp = \ltrim(\pack('N', $length), \chr(0));
+
+        return \pack('Ca*', 0x80 | \strlen($temp), $temp);
+    }
+
+    /**
+     * Encodes a value into a DER object.
+     * Also defined in Firebase\JWT\JWT
+     *
+     * @param   int     $type DER tag
+     * @param   string  $value the value to encode
+     * @return  string  the encoded object
+     */
+    private static function encodeDER(int $type, string $value): string
+    {
+        $tag_header = 0;
+        if ($type === self::ASN1_SEQUENCE) {
+            $tag_header |= 0x20;
+        }
+
+        // Type
+        $der = \chr($tag_header | $type);
+
+        // Length
+        $der .= \chr(\strlen($value));
+
+        return $der . $value;
+    }
+
+    /**
+     * Encodes a string into a DER-encoded OID.
+     *
+     * @param   string $oid the OID string
+     * @return  string the binary DER-encoded OID
+     */
+    private static function encodeOID(string $oid): string
+    {
+        $octets = explode('.', $oid);
+
+        // Get the first octet
+        $first = (int) array_shift($octets);
+        $second = (int) array_shift($octets);
+        $oid = \chr($first * 40 + $second);
+
+        // Iterate over subsequent octets
+        foreach ($octets as $octet) {
+            if ($octet == 0) {
+                $oid .= \chr(0x00);
+                continue;
+            }
+            $bin = '';
+
+            while ($octet) {
+                $bin .= \chr(0x80 | ($octet & 0x7f));
+                $octet >>= 7;
+            }
+            $bin[0] = $bin[0] & \chr(0x7f);
+
+            // Convert to big endian if necessary
+            if (pack('V', 65534) == pack('L', 65534)) {
+                $oid .= strrev($bin);
+            } else {
+                $oid .= $bin;
+            }
+        }
+
+        return $oid;
+    }
+}

+ 835 - 0
vendor/firebase/php-jwt/src/JWT.php

@@ -0,0 +1,835 @@
+<?php
+
+namespace Firebase\JWT;
+
+use ArrayAccess;
+use DateTime;
+use DomainException;
+use Exception;
+use InvalidArgumentException;
+use OpenSSLAsymmetricKey;
+use OpenSSLCertificate;
+use stdClass;
+use UnexpectedValueException;
+
+/**
+ * JSON Web Token implementation, based on this spec:
+ * https://tools.ietf.org/html/rfc7519
+ *
+ * PHP version 5
+ *
+ * @category Authentication
+ * @package  Authentication_JWT
+ * @author   Neuman Vong <neuman@twilio.com>
+ * @author   Anant Narayanan <anant@php.net>
+ * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link     https://github.com/firebase/php-jwt
+ */
+class JWT
+{
+    private const ASN1_INTEGER = 0x02;
+    private const ASN1_SEQUENCE = 0x10;
+    private const ASN1_BIT_STRING = 0x03;
+
+    private const RSA_KEY_MIN_LENGTH = 2048;
+
+    /**
+     * When checking nbf, iat or expiration times,
+     * we want to provide some extra leeway time to
+     * account for clock skew.
+     *
+     * @var int
+     */
+    public static $leeway = 0;
+
+    /**
+     * Allow the current timestamp to be specified.
+     * Useful for fixing a value within unit testing.
+     * Will default to PHP time() value if null.
+     *
+     * @var ?int
+     */
+    public static $timestamp = null;
+
+    /**
+     * @var array<string, string[]>
+     */
+    public static $supported_algs = [
+        'ES384' => ['openssl', 'SHA384'],
+        'ES256' => ['openssl', 'SHA256'],
+        'ES256K' => ['openssl', 'SHA256'],
+        'HS256' => ['hash_hmac', 'SHA256'],
+        'HS384' => ['hash_hmac', 'SHA384'],
+        'HS512' => ['hash_hmac', 'SHA512'],
+        'RS256' => ['openssl', 'SHA256'],
+        'RS384' => ['openssl', 'SHA384'],
+        'RS512' => ['openssl', 'SHA512'],
+        'PS256' => ['openssl', 'SHA256'],
+        'EdDSA' => ['sodium_crypto', 'EdDSA']
+    ];
+
+    /**
+     * Decodes a JWT string into a PHP object.
+     *
+     * @param string                 $jwt            The JWT
+     * @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray  The Key or associative array of key IDs
+     *                                                                      (kid) to Key objects.
+     *                                                                      If the algorithm used is asymmetric, this is
+     *                                                                      the public key.
+     *                                                                      Each Key object contains an algorithm and
+     *                                                                      matching key.
+     *                                                                      Supported algorithms are 'ES384','ES256',
+     *                                                                      'HS256', 'HS384', 'HS512', 'RS256', 'RS384'
+     *                                                                      and 'RS512'.
+     * @param stdClass               $headers                               Optional. Populates stdClass with headers.
+     *
+     * @return stdClass The JWT's payload as a PHP object
+     *
+     * @throws InvalidArgumentException     Provided key/key-array was empty or malformed
+     * @throws DomainException              Provided JWT is malformed
+     * @throws UnexpectedValueException     Provided JWT was invalid
+     * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
+     * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
+     * @throws BeforeValidException         Provided JWT is trying to be used before it's
+     *                                      been created as defined by 'iat'
+     * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
+     *
+     * @uses jsonDecode
+     * @uses urlsafeB64Decode
+     */
+    public static function decode(
+        string $jwt,
+        #[\SensitiveParameter] $keyOrKeyArray,
+        ?stdClass &$headers = null
+    ): stdClass {
+        // Validate JWT
+        $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
+
+        if (empty($keyOrKeyArray)) {
+            throw new InvalidArgumentException('Key may not be empty');
+        }
+        $tks = \explode('.', $jwt, 4);
+        if (\count($tks) !== 3) {
+            throw new UnexpectedValueException('Wrong number of segments');
+        }
+        list($headb64, $bodyb64, $cryptob64) = $tks;
+        $headerRaw = static::urlsafeB64Decode($headb64);
+        if (null === ($header = static::jsonDecode($headerRaw))) {
+            throw new UnexpectedValueException('Invalid header encoding');
+        }
+        if ($headers !== null) {
+            $headers = $header;
+        }
+        $payloadRaw = static::urlsafeB64Decode($bodyb64);
+        if (null === ($payload = static::jsonDecode($payloadRaw))) {
+            throw new UnexpectedValueException('Invalid claims encoding');
+        }
+        if (\is_array($payload)) {
+            // prevent PHP Fatal Error in edge-cases when payload is empty array
+            $payload = (object) $payload;
+        }
+        if (!$payload instanceof stdClass) {
+            throw new UnexpectedValueException('Payload must be a JSON object');
+        }
+        if (isset($payload->iat) && !\is_numeric($payload->iat)) {
+            throw new UnexpectedValueException('Payload iat must be a number');
+        }
+        if (isset($payload->nbf) && !\is_numeric($payload->nbf)) {
+            throw new UnexpectedValueException('Payload nbf must be a number');
+        }
+        if (isset($payload->exp) && !\is_numeric($payload->exp)) {
+            throw new UnexpectedValueException('Payload exp must be a number');
+        }
+
+        $sig = static::urlsafeB64Decode($cryptob64);
+        if (empty($header->alg)) {
+            throw new UnexpectedValueException('Empty algorithm');
+        }
+        if (empty(static::$supported_algs[$header->alg])) {
+            throw new UnexpectedValueException('Algorithm not supported');
+        }
+
+        $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
+
+        // Check the algorithm
+        if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
+            // See issue #351
+            throw new UnexpectedValueException('Incorrect key for this algorithm');
+        }
+        if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) {
+            // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures
+            $sig = self::signatureToDER($sig);
+        }
+        if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
+            throw new SignatureInvalidException('Signature verification failed');
+        }
+
+        // Check the nbf if it is defined. This is the time that the
+        // token can actually be used. If it's not yet that time, abort.
+        if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
+            $ex = new BeforeValidException(
+                'Cannot handle token with nbf prior to ' . \date(DateTime::ATOM, (int) floor($payload->nbf))
+            );
+            $ex->setPayload($payload);
+            throw $ex;
+        }
+
+        // Check that this token has been created before 'now'. This prevents
+        // using tokens that have been created for later use (and haven't
+        // correctly used the nbf claim).
+        if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
+            $ex = new BeforeValidException(
+                'Cannot handle token with iat prior to ' . \date(DateTime::ATOM, (int) floor($payload->iat))
+            );
+            $ex->setPayload($payload);
+            throw $ex;
+        }
+
+        // Check if this token has expired.
+        if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
+            $ex = new ExpiredException('Expired token');
+            $ex->setPayload($payload);
+            $ex->setTimestamp($timestamp);
+            throw $ex;
+        }
+
+        return $payload;
+    }
+
+    /**
+     * Converts and signs a PHP array into a JWT string.
+     *
+     * @param array<mixed>          $payload PHP array
+     * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
+     * @param string                $alg     Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
+     *                                       'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
+     * @param string                $keyId
+     * @param array<string, string|string[]> $head  An array with header elements to attach
+     *
+     * @return string A signed JWT
+     *
+     * @uses jsonEncode
+     * @uses urlsafeB64Encode
+     */
+    public static function encode(
+        array $payload,
+        #[\SensitiveParameter] $key,
+        string $alg,
+        ?string $keyId = null,
+        ?array $head = null
+    ): string {
+        $header = ['typ' => 'JWT'];
+        if (isset($head)) {
+            $header = \array_merge($header, $head);
+        }
+        $header['alg'] = $alg;
+        if ($keyId !== null) {
+            $header['kid'] = $keyId;
+        }
+        if (isset($payload['nbf']) && !\is_numeric($payload['nbf'])) {
+            throw new UnexpectedValueException('Payload nbf must be a number');
+        }
+        if (isset($payload['iat']) && !\is_numeric($payload['iat'])) {
+            throw new UnexpectedValueException('Payload iat must be a number');
+        }
+        if (isset($payload['exp']) && !\is_numeric($payload['exp'])) {
+            throw new UnexpectedValueException('Payload exp must be a number');
+        }
+        $segments = [];
+        $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
+        $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
+        $signing_input = \implode('.', $segments);
+
+        $signature = static::sign($signing_input, $key, $alg);
+        $segments[] = static::urlsafeB64Encode($signature);
+
+        return \implode('.', $segments);
+    }
+
+    /**
+     * Sign a string with a given key and algorithm.
+     *
+     * @param string $msg  The message to sign
+     * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate  $key  The secret key.
+     * @param string $alg  Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
+     *                    'HS384', 'HS512', 'RS256', 'RS384', 'PS256' and 'RS512'
+     *
+     * @return string An encrypted message
+     *
+     * @throws DomainException Unsupported algorithm or bad key was specified
+     */
+    public static function sign(
+        string $msg,
+        #[\SensitiveParameter] $key,
+        string $alg
+    ): string {
+        if (empty(static::$supported_algs[$alg])) {
+            throw new DomainException('Algorithm not supported');
+        }
+        list($function, $algorithm) = static::$supported_algs[$alg];
+        switch ($function) {
+            case 'hash_hmac':
+                if (!\is_string($key)) {
+                    throw new InvalidArgumentException('key must be a string when using hmac');
+                }
+                self::validateHmacKeyLength($key, $algorithm);
+                return \hash_hmac($algorithm, $msg, $key, true);
+            case 'openssl':
+                if ($alg === 'PS256') {
+                    return self::signPS256($key, $msg);
+                }
+                $signature = '';
+                if (!$key = openssl_pkey_get_private($key)) {
+                    throw new DomainException('OpenSSL unable to validate key');
+                }
+                if (str_starts_with($alg, 'RS')) {
+                    self::validateRsaKeyLength($key);
+                } elseif (str_starts_with($alg, 'ES')) {
+                    self::validateEcKeyLength($key, $alg);
+                }
+                $success = \openssl_sign($msg, $signature, $key, $algorithm);
+                if (!$success) {
+                    throw new DomainException('OpenSSL unable to sign data');
+                }
+                if ($alg === 'ES256' || $alg === 'ES256K') {
+                    $signature = self::signatureFromDER($signature, 256);
+                } elseif ($alg === 'ES384') {
+                    $signature = self::signatureFromDER($signature, 384);
+                }
+                return $signature;
+            case 'sodium_crypto':
+                try {
+                    return sodium_crypto_sign_detached($msg, self::validateEdDSAKey($key));
+                } catch (Exception $e) {
+                    throw new DomainException($e->getMessage(), 0, $e);
+                }
+        }
+
+        throw new DomainException('Algorithm not supported');
+    }
+
+    /**
+     * Verify a signature with the message, key and method. Not all methods
+     * are symmetric, so we must have a separate verify and sign method.
+     *
+     * @param string $msg         The original message (header and body)
+     * @param string $signature   The original signature
+     * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate  $keyMaterial For Ed*, ES*, HS*, a string key works.
+     *                                                        for RS*, must be an instance of OpenSSLAsymmetricKey
+     * @param string $alg         The algorithm
+     *
+     * @return bool
+     *
+     * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
+     */
+    private static function verify(
+        string $msg,
+        string $signature,
+        #[\SensitiveParameter] $keyMaterial,
+        string $alg
+    ): bool {
+        if (empty(static::$supported_algs[$alg])) {
+            throw new DomainException('Algorithm not supported');
+        }
+
+        list($function, $algorithm) = static::$supported_algs[$alg];
+        switch ($function) {
+            case 'openssl':
+                if ($alg === 'PS256') {
+                    return self::verifyPS256($keyMaterial, $msg, $signature);
+                }
+                if (!$key = openssl_pkey_get_public($keyMaterial)) {
+                    throw new DomainException('OpenSSL unable to validate key');
+                }
+                if (str_starts_with($alg, 'RS')) {
+                    self::validateRsaKeyLength($key);
+                } elseif (str_starts_with($alg, 'ES')) {
+                    self::validateEcKeyLength($key, $alg);
+                }
+                $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm);
+                if ($success === 1) {
+                    return true;
+                }
+                if ($success === 0) {
+                    return false;
+                }
+                // returns 1 on success, 0 on failure, -1 on error.
+                throw new DomainException(
+                    'OpenSSL error: ' . \openssl_error_string()
+                );
+            case 'sodium_crypto':
+                try {
+                    $key = self::validateEdDSAKey($keyMaterial);
+                    if (\strlen($signature) === 0) {
+                        throw new DomainException('Signature cannot be empty string');
+                    }
+                    return sodium_crypto_sign_verify_detached($signature, $msg, $key);
+                } catch (Exception $e) {
+                    throw new DomainException($e->getMessage(), 0, $e);
+                }
+            case 'hash_hmac':
+            default:
+                if (!\is_string($keyMaterial)) {
+                    throw new InvalidArgumentException('key must be a string when using hmac');
+                }
+                self::validateHmacKeyLength($keyMaterial, $algorithm);
+                $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
+                return self::constantTimeEquals($hash, $signature);
+        }
+    }
+
+    /**
+     * Decode a JSON string into a PHP object.
+     *
+     * @param string $input JSON string
+     *
+     * @return mixed The decoded JSON string
+     *
+     * @throws DomainException Provided string was invalid JSON
+     */
+    public static function jsonDecode(string $input)
+    {
+        $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
+
+        if ($errno = \json_last_error()) {
+            self::handleJsonError($errno);
+        } elseif ($obj === null && $input !== 'null') {
+            throw new DomainException('Null result with non-null input');
+        }
+        return $obj;
+    }
+
+    /**
+     * Encode a PHP array into a JSON string.
+     *
+     * @param array<mixed> $input A PHP array
+     *
+     * @return string JSON representation of the PHP array
+     *
+     * @throws DomainException Provided object could not be encoded to valid JSON
+     */
+    public static function jsonEncode(array $input): string
+    {
+        $json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
+        if ($errno = \json_last_error()) {
+            self::handleJsonError($errno);
+        } elseif ($json === 'null') {
+            throw new DomainException('Null result with non-null input');
+        }
+        if ($json === false) {
+            throw new DomainException('Provided object could not be encoded to valid JSON');
+        }
+        return $json;
+    }
+
+    /**
+     * Decode a string with URL-safe Base64.
+     *
+     * @param string $input A Base64 encoded string
+     *
+     * @return string A decoded string
+     *
+     * @throws InvalidArgumentException invalid base64 characters
+     */
+    public static function urlsafeB64Decode(string $input): string
+    {
+        return \base64_decode(self::convertBase64UrlToBase64($input));
+    }
+
+    /**
+     * Convert a string in the base64url (URL-safe Base64) encoding to standard base64.
+     *
+     * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding)
+     *
+     * @return string A Base64 encoded string with standard characters (+/) and padding (=), when
+     * needed.
+     *
+     * @see https://www.rfc-editor.org/rfc/rfc4648
+     */
+    public static function convertBase64UrlToBase64(string $input): string
+    {
+        $remainder = \strlen($input) % 4;
+        if ($remainder) {
+            $padlen = 4 - $remainder;
+            $input .= \str_repeat('=', $padlen);
+        }
+        return \strtr($input, '-_', '+/');
+    }
+
+    /**
+     * Encode a string with URL-safe Base64.
+     *
+     * @param string $input The string you want encoded
+     *
+     * @return string The base64 encode of what you passed in
+     */
+    public static function urlsafeB64Encode(string $input): string
+    {
+        return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
+    }
+
+    /**
+     * Determine if an algorithm has been provided for each Key
+     *
+     * @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray
+     * @param string|null            $kid
+     *
+     * @throws UnexpectedValueException
+     *
+     * @return Key
+     */
+    private static function getKey(
+        #[\SensitiveParameter] $keyOrKeyArray,
+        ?string $kid
+    ): Key {
+        if ($keyOrKeyArray instanceof Key) {
+            return $keyOrKeyArray;
+        }
+
+        if (empty($kid) && $kid !== '0') {
+            throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
+        }
+
+        if ($keyOrKeyArray instanceof CachedKeySet) {
+            // Skip "isset" check, as this will automatically refresh if not set
+            return $keyOrKeyArray[$kid];
+        }
+
+        if (!isset($keyOrKeyArray[$kid])) {
+            throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
+        }
+
+        return $keyOrKeyArray[$kid];
+    }
+
+    /**
+     * @param string $left  The string of known length to compare against
+     * @param string $right The user-supplied string
+     * @return bool
+     */
+    public static function constantTimeEquals(string $left, string $right): bool
+    {
+        if (\function_exists('hash_equals')) {
+            return \hash_equals($left, $right);
+        }
+        $len = \min(self::safeStrlen($left), self::safeStrlen($right));
+
+        $status = 0;
+        for ($i = 0; $i < $len; $i++) {
+            $status |= (\ord($left[$i]) ^ \ord($right[$i]));
+        }
+        $status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
+
+        return ($status === 0);
+    }
+
+    /**
+     * Helper method to create a JSON error.
+     *
+     * @param int $errno An error number from json_last_error()
+     *
+     * @throws DomainException
+     *
+     * @return void
+     */
+    private static function handleJsonError(int $errno): void
+    {
+        $messages = [
+            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
+            JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
+            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
+            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
+            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
+        ];
+        throw new DomainException(
+            isset($messages[$errno])
+            ? $messages[$errno]
+            : 'Unknown JSON error: ' . $errno
+        );
+    }
+
+    /**
+     * Get the number of bytes in cryptographic strings.
+     *
+     * @param string $str
+     *
+     * @return int
+     */
+    private static function safeStrlen(string $str): int
+    {
+        if (\function_exists('mb_strlen')) {
+            return \mb_strlen($str, '8bit');
+        }
+        return \strlen($str);
+    }
+
+    /**
+     * Convert an ECDSA signature to an ASN.1 DER sequence
+     *
+     * @param   string $sig The ECDSA signature to convert
+     * @return  string The encoded DER object
+     */
+    private static function signatureToDER(string $sig): string
+    {
+        // Separate the signature into r-value and s-value
+        $length = max(1, (int) (\strlen($sig) / 2));
+        list($r, $s) = \str_split($sig, $length);
+
+        // Trim leading zeros
+        $r = \ltrim($r, "\x00");
+        $s = \ltrim($s, "\x00");
+
+        // Convert r-value and s-value from unsigned big-endian integers to
+        // signed two's complement
+        if (\ord($r[0]) > 0x7f) {
+            $r = "\x00" . $r;
+        }
+        if (\ord($s[0]) > 0x7f) {
+            $s = "\x00" . $s;
+        }
+
+        return self::encodeDER(
+            self::ASN1_SEQUENCE,
+            self::encodeDER(self::ASN1_INTEGER, $r) .
+            self::encodeDER(self::ASN1_INTEGER, $s)
+        );
+    }
+
+    /**
+     * Encodes a value into a DER object.
+     *
+     * @param   int     $type DER tag
+     * @param   string  $value the value to encode
+     *
+     * @return  string  the encoded object
+     */
+    private static function encodeDER(int $type, string $value): string
+    {
+        $tag_header = 0;
+        if ($type === self::ASN1_SEQUENCE) {
+            $tag_header |= 0x20;
+        }
+
+        // Type
+        $der = \chr($tag_header | $type);
+
+        // Length
+        $der .= \chr(\strlen($value));
+
+        return $der . $value;
+    }
+
+    /**
+     * Encodes signature from a DER object.
+     *
+     * @param   string  $der binary signature in DER format
+     * @param   int     $keySize the number of bits in the key
+     *
+     * @return  string  the signature
+     */
+    private static function signatureFromDER(string $der, int $keySize): string
+    {
+        // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
+        list($offset, $_) = self::readDER($der);
+        list($offset, $r) = self::readDER($der, $offset);
+        list($offset, $s) = self::readDER($der, $offset);
+
+        // Convert r-value and s-value from signed two's compliment to unsigned
+        // big-endian integers
+        $r = \ltrim($r, "\x00");
+        $s = \ltrim($s, "\x00");
+
+        // Pad out r and s so that they are $keySize bits long
+        $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
+        $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
+
+        return $r . $s;
+    }
+
+    /**
+     * Reads binary DER-encoded data and decodes into a single object
+     *
+     * @param string $der the binary data in DER format
+     * @param int $offset the offset of the data stream containing the object
+     * to decode
+     *
+     * @return array{int, string|null} the new offset and the decoded object
+     */
+    private static function readDER(string $der, int $offset = 0): array
+    {
+        $pos = $offset;
+        $size = \strlen($der);
+        $constructed = (\ord($der[$pos]) >> 5) & 0x01;
+        $type = \ord($der[$pos++]) & 0x1f;
+
+        // Length
+        $len = \ord($der[$pos++]);
+        if ($len & 0x80) {
+            $n = $len & 0x1f;
+            $len = 0;
+            while ($n-- && $pos < $size) {
+                $len = ($len << 8) | \ord($der[$pos++]);
+            }
+        }
+
+        // Value
+        if ($type === self::ASN1_BIT_STRING) {
+            $pos++; // Skip the first contents octet (padding indicator)
+            $data = \substr($der, $pos, $len - 1);
+            $pos += $len - 1;
+        } elseif (!$constructed) {
+            $data = \substr($der, $pos, $len);
+            $pos += $len;
+        } else {
+            $data = null;
+        }
+
+        return [$pos, $data];
+    }
+
+    /**
+     * Validate HMAC key length
+     *
+     * @param string $key HMAC key material
+     * @param string $algorithm The algorithm
+     *
+     * @throws DomainException Provided key is too short
+     */
+    private static function validateHmacKeyLength(string $key, string $algorithm): void
+    {
+        $keyLength = \strlen($key) * 8;
+        $minKeyLength = (int) \str_replace('SHA', '', $algorithm);
+        if ($keyLength < $minKeyLength) {
+            throw new DomainException('Provided key is too short');
+        }
+    }
+
+    /**
+     * Validate RSA key length
+     *
+     * @param OpenSSLAsymmetricKey $key RSA key material
+     * @throws DomainException Provided key is too short
+     */
+    private static function validateRsaKeyLength(#[\SensitiveParameter] OpenSSLAsymmetricKey $key): void
+    {
+        if (!$keyDetails = openssl_pkey_get_details($key)) {
+            throw new DomainException('Unable to validate key');
+        }
+        if ($keyDetails['bits'] < self::RSA_KEY_MIN_LENGTH) {
+            throw new DomainException('Provided key is too short');
+        }
+    }
+
+    /**
+     * Validate RSA key length
+     *
+     * @param OpenSSLAsymmetricKey $key RSA key material
+     * @param string $algorithm The algorithm
+     * @throws DomainException Provided key is too short
+     */
+    private static function validateEcKeyLength(
+        #[\SensitiveParameter] OpenSSLAsymmetricKey $key,
+        string $algorithm
+    ): void {
+        if (!$keyDetails = openssl_pkey_get_details($key)) {
+            throw new DomainException('Unable to validate key');
+        }
+        $minKeyLength = (int) \str_replace('ES', '', $algorithm);
+        if ($keyDetails['bits'] < $minKeyLength) {
+            throw new DomainException('Provided key is too short');
+        }
+    }
+
+    /**
+     * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate  $keyMaterial
+     * @return non-empty-string
+     */
+    private static function validateEdDSAKey(#[\SensitiveParameter] $keyMaterial): string
+    {
+        if (!\function_exists('sodium_crypto_sign_verify_detached')) {
+            throw new DomainException('libsodium is not available');
+        }
+        if (!\is_string($keyMaterial)) {
+            throw new InvalidArgumentException('key must be a string when using EdDSA');
+        }
+        // The last non-empty line is used as the key.
+        $lines = array_filter(explode("\n", $keyMaterial));
+        $key = self::urlsafeB64Decode((string) end($lines));
+        if (\strlen($key) === 0) {
+            throw new DomainException('Key cannot be empty string');
+        }
+        return $key;
+    }
+
+    /**
+     * Signs a message with a PS256 algorithm
+     *
+     * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key
+     * @param string $message
+     * @throws DomainException Provided key is invalid
+     */
+    private static function signPS256(
+        #[\SensitiveParameter] string|OpenSSLAsymmetricKey|OpenSSLCertificate $key,
+        string $message
+    ): string {
+        if (!class_exists('\phpseclib3\Crypt\RSA')) {
+            throw new DomainException('phpseclib/phpseclib is required for PS256 support');
+        }
+
+        if ($key instanceof OpenSSLCertificate) {
+            throw new DomainException('Cannot sign with an X.509 certificate. A private key is required.');
+        }
+
+        if ($key instanceof OpenSSLAsymmetricKey) {
+            if (!openssl_pkey_export($key, $pem)) {
+                throw new DomainException('OpenSSL unable to export the AsymmetricKey');
+            }
+            $key = $pem;
+        }
+
+        /** @var \phpseclib3\Crypt\RSA\PrivateKey $rsa */
+        $rsa = \phpseclib3\Crypt\PublicKeyLoader::load($key);
+
+        return $rsa->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS)
+            ->withHash('sha256')
+            ->sign($message);
+    }
+
+    /**
+     * Validates a PS256 algorithm signature.
+     *
+     * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key
+     * @param string $message
+     * @param string $signature
+     * @throws DomainException Provided key is invalid
+     */
+    private static function verifyPS256(
+        #[\SensitiveParameter] string|OpenSSLAsymmetricKey|OpenSSLCertificate $key,
+        string $message,
+        string $signature
+    ): bool {
+        if (!class_exists('\phpseclib3\Crypt\RSA')) {
+            throw new DomainException('phpseclib/phpseclib is required for PS256 support');
+        }
+
+        if ($key instanceof OpenSSLAsymmetricKey) {
+            $details = openssl_pkey_get_details($key);
+            if (!$details || !isset($details['key'])) {
+                throw new DomainException('OpenSSL unable to extract public key');
+            }
+            $key = $details['key'];
+        } elseif ($key instanceof OpenSSLCertificate) {
+            if (!openssl_x509_export($key, $pem)) {
+                throw new DomainException('OpenSSL unable to export certificate');
+            }
+            $key = $pem;
+        }
+
+        /** @var \phpseclib3\Crypt\RSA\PublicKey $rsa */
+        $rsa = \phpseclib3\Crypt\PublicKeyLoader::load($key);
+
+        return $rsa->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS)
+            ->withHash('sha256')
+            ->verify($message, $signature);
+    }
+}

+ 20 - 0
vendor/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php

@@ -0,0 +1,20 @@
+<?php
+namespace Firebase\JWT;
+
+interface JWTExceptionWithPayloadInterface
+{
+    /**
+     * Get the payload that caused this exception.
+     *
+     * @return object
+     */
+    public function getPayload(): object;
+
+    /**
+     * Get the payload that caused this exception.
+     *
+     * @param object $payload
+     * @return void
+     */
+    public function setPayload(object $payload): void;
+}

+ 53 - 0
vendor/firebase/php-jwt/src/Key.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace Firebase\JWT;
+
+use InvalidArgumentException;
+use OpenSSLAsymmetricKey;
+use OpenSSLCertificate;
+use TypeError;
+
+class Key
+{
+    /**
+     * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
+     * @param string $algorithm
+     */
+    public function __construct(
+        #[\SensitiveParameter] private $keyMaterial,
+        private string $algorithm
+    ) {
+        if (!\is_string($keyMaterial)
+            && !$keyMaterial instanceof OpenSSLAsymmetricKey
+            && !$keyMaterial instanceof OpenSSLCertificate
+        ) {
+            throw new TypeError('Key material must be a string, OpenSSLCertificate, or OpenSSLAsymmetricKey');
+        }
+
+        if (empty($keyMaterial)) {
+            throw new InvalidArgumentException('Key material must not be empty');
+        }
+
+        if (empty($algorithm)) {
+            throw new InvalidArgumentException('Algorithm must not be empty');
+        }
+    }
+
+    /**
+     * Return the algorithm valid for this key
+     *
+     * @return string
+     */
+    public function getAlgorithm(): string
+    {
+        return $this->algorithm;
+    }
+
+    /**
+     * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate
+     */
+    public function getKeyMaterial()
+    {
+        return $this->keyMaterial;
+    }
+}

+ 7 - 0
vendor/firebase/php-jwt/src/SignatureInvalidException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Firebase\JWT;
+
+class SignatureInvalidException extends \UnexpectedValueException
+{
+}

+ 203 - 0
vendor/google/apiclient-services/LICENSE

@@ -0,0 +1,203 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by
+the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems,
+and issue tracking systems that are managed by, or on behalf of, the
+Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the
+Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+(except as stated in this section) patent license to make, have made,
+use, offer to sell, sell, import, and otherwise transfer the Work,
+where such license applies only to those patent claims licensable
+by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s)
+with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work
+or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You
+meet the following conditions:
+
+(a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works
+that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work,
+excluding those notices that do not pertain to any part of
+the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained
+within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one
+of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or,
+within a display generated by the Derivative Works, if and
+wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and
+do not modify the License. You may add Your own attribution
+notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided
+that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions
+for use, reproduction, or distribution of Your modifications, or
+for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with
+the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work
+by You to the Licensor shall be under the terms and conditions of
+this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify
+the terms of any separate license agreement you may have executed
+with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+agreed to in writing, Licensor provides the Work (and each
+Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any
+risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise,
+unless required by applicable law (such as deliberate and grossly
+negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special,
+incidental, or consequential damages of any character arising as a
+result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer,
+and charge a fee for, acceptance of support, warranty, indemnity,
+or other liability obligations and/or rights consistent with this
+License. However, in accepting such obligations, You may act only
+on Your own behalf and on Your sole responsibility, not on behalf
+of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason
+of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following
+boilerplate notice, with the fields enclosed by brackets "[]"
+replaced with your own identifying information. (Don't include
+the brackets!) The text should be enclosed in the appropriate
+comment syntax for the file format. We also recommend that a
+file or class name and description of purpose be included on the
+same "printed page" as the copyright notice for easier
+identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+

+ 19 - 0
vendor/google/apiclient-services/README.md

@@ -0,0 +1,19 @@
+Google PHP API Client Services
+==============================
+
+[Reference Documentation](https://googleapis.github.io/google-api-php-client-services)
+
+**NOTE**: please check to see if the package you'd like to install is available in our
+list of [Google cloud packages](https://cloud.google.com/php/docs/reference) first, as
+these are the recommended libraries.
+
+## Requirements
+
+[Google API PHP Client](https://github.com/googleapis/google-api-php-client/releases)
+
+## Usage
+
+This library is automatically updated daily with new API changes, and tagged weekly.
+It is installed as part of the
+[Google API PHP Client](https://github.com/googleapis/google-api-php-client/releases)
+library via Composer, which will pull down the most recent tag.

+ 7 - 0
vendor/google/apiclient-services/SECURITY.md

@@ -0,0 +1,7 @@
+# Security Policy
+
+To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
+
+The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
+
+We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.

+ 50 - 0
vendor/google/apiclient-services/autoload.php

@@ -0,0 +1,50 @@
+<?php
+
+// For older (pre-2.7.2) verions of google/apiclient
+if (
+    file_exists(__DIR__ . '/../apiclient/src/Google/Client.php')
+    && !class_exists('Google_Client', false)
+) {
+    require_once(__DIR__ . '/../apiclient/src/Google/Client.php');
+    if (
+        defined('Google_Client::LIBVER')
+        && version_compare(Google_Client::LIBVER, '2.7.2', '<=')
+    ) {
+        $servicesClassMap = [
+            'Google\\Client' => 'Google_Client',
+            'Google\\Service' => 'Google_Service',
+            'Google\\Service\\Resource' => 'Google_Service_Resource',
+            'Google\\Model' => 'Google_Model',
+            'Google\\Collection' => 'Google_Collection',
+        ];
+        foreach ($servicesClassMap as $alias => $class) {
+            class_alias($class, $alias);
+        }
+    }
+}
+spl_autoload_register(function ($class) {
+    if (0 === strpos($class, 'Google_Service_')) {
+        // Autoload the new class, which will also create an alias for the
+        // old class by changing underscores to namespaces:
+        //     Google_Service_Speech_Resource_Operations
+        //      => Google\Service\Speech\Resource\Operations
+        $classExists = class_exists($newClass = str_replace('_', '\\', $class));
+        if ($classExists) {
+            return true;
+        }
+    } elseif (0 === strpos($class, 'Google\\Service\\')) {
+        $relativeClass = substr($class, strlen('Google\\Service\\'));
+        $parts = explode('\\', $relativeClass);
+        $leaf = array_pop($parts);
+        if (strlen($leaf) > 139) {
+            $shortenedLeaf = substr($leaf, 0, 80) . '_' . strtoupper(substr(md5($leaf), 0, 8));
+            $subPath = implode('/', $parts);
+            $filePath = __DIR__ . '/src/' . ($subPath ? $subPath . '/' : '') . $shortenedLeaf . '.php';
+            if (file_exists($filePath)) {
+                require_once $filePath;
+                return true;
+            }
+        }
+    }
+}, true, true);
+

+ 27 - 0
vendor/google/apiclient-services/composer.json

@@ -0,0 +1,27 @@
+{
+    "name": "google/apiclient-services",
+    "type": "library",
+    "description": "Client library for Google APIs",
+    "keywords": ["google"],
+    "homepage": "http://developers.google.com/api-client-library/php",
+    "license": "Apache-2.0",
+    "require": {
+        "php": "^8.1"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^9.6"
+    },
+    "autoload": {
+        "psr-4": {
+            "Google\\Service\\": "src"
+        },
+        "files": [
+            "autoload.php"
+        ]
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Google\\": "tests/mocks"
+        }
+    }
+}

+ 94 - 0
vendor/google/apiclient-services/src/ACMEDNS.php

@@ -0,0 +1,94 @@
+<?php
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+namespace Google\Service;
+
+use Google\Client;
+
+/**
+ * Service definition for ACMEDNS (v1).
+ *
+ * <p>
+ * Google Domains ACME DNS API that allows users to complete ACME DNS-01
+ * challenges for a domain.</p>
+ *
+ * <p>
+ * For more information about this service, see the API
+ * <a href="https://developers.google.com/domains/acme-dns/" target="_blank">Documentation</a>
+ * </p>
+ *
+ * @author Google, Inc.
+ */
+class ACMEDNS extends \Google\Service
+{
+
+
+  public $acmeChallengeSets;
+  public $rootUrlTemplate;
+
+  /**
+   * Constructs the internal representation of the ACMEDNS service.
+   *
+   * @param Client|array $clientOrConfig The client used to deliver requests, or a
+   *                                     config array to pass to a new Client instance.
+   * @param string $rootUrl The root URL used for requests to the service.
+   */
+  public function __construct($clientOrConfig = [], $rootUrl = null)
+  {
+    parent::__construct($clientOrConfig);
+    $this->rootUrl = $rootUrl ?: 'https://acmedns.googleapis.com/';
+    $this->rootUrlTemplate = $rootUrl ?: 'https://acmedns.UNIVERSE_DOMAIN/';
+    $this->servicePath = '';
+    $this->batchPath = 'batch';
+    $this->version = 'v1';
+    $this->serviceName = 'acmedns';
+
+    $this->acmeChallengeSets = new ACMEDNS\Resource\AcmeChallengeSets(
+        $this,
+        $this->serviceName,
+        'acmeChallengeSets',
+        [
+          'methods' => [
+            'get' => [
+              'path' => 'v1/acmeChallengeSets/{rootDomain}',
+              'httpMethod' => 'GET',
+              'parameters' => [
+                'rootDomain' => [
+                  'location' => 'path',
+                  'type' => 'string',
+                  'required' => true,
+                ],
+              ],
+            ],'rotateChallenges' => [
+              'path' => 'v1/acmeChallengeSets/{rootDomain}:rotateChallenges',
+              'httpMethod' => 'POST',
+              'parameters' => [
+                'rootDomain' => [
+                  'location' => 'path',
+                  'type' => 'string',
+                  'required' => true,
+                ],
+              ],
+            ],
+          ]
+        ]
+    );
+  }
+}
+
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(ACMEDNS::class, 'Google_Service_ACMEDNS');

+ 43 - 0
vendor/google/apiclient-services/src/ACMEDNS/AcmeChallengeSet.php

@@ -0,0 +1,43 @@
+<?php
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+namespace Google\Service\ACMEDNS;
+
+class AcmeChallengeSet extends \Google\Collection
+{
+  protected $collection_key = 'record';
+  protected $recordType = AcmeTxtRecord::class;
+  protected $recordDataType = 'array';
+
+  /**
+   * @param AcmeTxtRecord[]
+   */
+  public function setRecord($record)
+  {
+    $this->record = $record;
+  }
+  /**
+   * @return AcmeTxtRecord[]
+   */
+  public function getRecord()
+  {
+    return $this->record;
+  }
+}
+
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(AcmeChallengeSet::class, 'Google_Service_ACMEDNS_AcmeChallengeSet');

+ 80 - 0
vendor/google/apiclient-services/src/ACMEDNS/AcmeTxtRecord.php

@@ -0,0 +1,80 @@
+<?php
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+namespace Google\Service\ACMEDNS;
+
+class AcmeTxtRecord extends \Google\Model
+{
+  /**
+   * @var string
+   */
+  public $digest;
+  /**
+   * @var string
+   */
+  public $fqdn;
+  /**
+   * @var string
+   */
+  public $updateTime;
+
+  /**
+   * @param string
+   */
+  public function setDigest($digest)
+  {
+    $this->digest = $digest;
+  }
+  /**
+   * @return string
+   */
+  public function getDigest()
+  {
+    return $this->digest;
+  }
+  /**
+   * @param string
+   */
+  public function setFqdn($fqdn)
+  {
+    $this->fqdn = $fqdn;
+  }
+  /**
+   * @return string
+   */
+  public function getFqdn()
+  {
+    return $this->fqdn;
+  }
+  /**
+   * @param string
+   */
+  public function setUpdateTime($updateTime)
+  {
+    $this->updateTime = $updateTime;
+  }
+  /**
+   * @return string
+   */
+  public function getUpdateTime()
+  {
+    return $this->updateTime;
+  }
+}
+
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(AcmeTxtRecord::class, 'Google_Service_ACMEDNS_AcmeTxtRecord');

部分文件因为文件数量过多而无法显示