categories.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <?php
  2. // Αρχείο: /var/www/html/hockey-cms/admin/categories.php
  3. require_once 'admin_auth.php';
  4. require_once '../config/database.php';
  5. // --- Μηχανισμός Αλλαγής Γλώσσας ---
  6. if (isset($_GET['lang']) && in_array($_GET['lang'], ['el', 'en'])) {
  7. $_SESSION['lang'] = $_GET['lang'];
  8. }
  9. $current_lang = $_SESSION['lang'] ?? 'el';
  10. $translations = [
  11. 'el' => [
  12. 'manage_cats' => 'Διαχείριση Κατηγοριών',
  13. 'edit_cat' => '✏️ Επεξεργασία Κατηγορίας',
  14. 'add_cat' => '📁 Προσθήκη Νέας Κατηγορίας',
  15. 'name_el' => 'Όνομα 🇬🇷 (Ελληνικά) *',
  16. 'name_en' => 'Όνομα 🇬🇧 (Αγγλικά)',
  17. 'parent_cat' => 'Ανήκει σε... (Προαιρετικό)',
  18. 'is_main' => '-- Είναι Κεντρική Κατηγορία --',
  19. 'attr_group' => 'Ομάδα Χαρακτηριστικών',
  20. 'no_attr' => '-- Χωρίς Ειδικά Χαρακτηριστικά --',
  21. 'btn_save' => 'Αποθήκευση Αλλαγών',
  22. 'btn_add' => 'Προσθήκη Κατηγορίας',
  23. 'btn_cancel' => 'Ακύρωση',
  24. 'tree_title' => 'Δέντρο Κατηγοριών (Treeview)',
  25. 'no_cats' => 'Δεν υπάρχουν κατηγορίες ακόμα.',
  26. 'msg_add_ok' => 'Η κατηγορία προστέθηκε επιτυχώς!',
  27. 'msg_edit_ok' => 'Η κατηγορία ενημερώθηκε επιτυχώς!',
  28. 'msg_err_name' => 'Το Ελληνικό όνομα είναι υποχρεωτικό.',
  29. 'msg_err_slug' => 'Σφάλμα: (Μήπως το URL/Slug υπάρχει ήδη;)',
  30. 'msg_del_ok' => 'Η κατηγορία διεγράφη επιτυχώς!',
  31. 'msg_err_prod' => 'Αδυναμία διαγραφής! Υπάρχουν προϊόντα σε αυτή την κατηγορία.',
  32. 'msg_err_child' => 'Αδυναμία διαγραφής! Υπάρχουν υποκατηγορίες μέσα σε αυτή την κατηγορία.',
  33. 'confirm_del' => 'Είστε σίγουρος ότι θέλετε να διαγράψετε αυτή την κατηγορία;'
  34. ],
  35. 'en' => [
  36. 'manage_cats' => 'Manage Categories',
  37. 'edit_cat' => '✏️ Edit Category',
  38. 'add_cat' => '📁 Add New Category',
  39. 'name_el' => 'Name 🇬🇷 (Greek) *',
  40. 'name_en' => 'Name 🇬🇧 (English)',
  41. 'parent_cat' => 'Belongs to... (Optional)',
  42. 'is_main' => '-- Is Main Category --',
  43. 'attr_group' => 'Attribute Group',
  44. 'no_attr' => '-- No Special Attributes --',
  45. 'btn_save' => 'Save Changes',
  46. 'btn_add' => 'Add Category',
  47. 'btn_cancel' => 'Cancel',
  48. 'tree_title' => 'Category Tree (Treeview)',
  49. 'no_cats' => 'No categories found.',
  50. 'msg_add_ok' => 'Category added successfully!',
  51. 'msg_edit_ok' => 'Category updated successfully!',
  52. 'msg_err_name' => 'The Greek name is required.',
  53. 'msg_err_slug' => 'Error: (Maybe the URL/Slug already exists?)',
  54. 'msg_del_ok' => 'Category deleted successfully!',
  55. 'msg_err_prod' => 'Cannot delete! There are products in this category.',
  56. 'msg_err_child' => 'Cannot delete! There are subcategories inside this category.',
  57. 'confirm_del' => 'Are you sure you want to delete this category?'
  58. ]
  59. ];
  60. $t = $translations[$current_lang];
  61. header('Content-Type: text/html; charset=utf-8');
  62. $message = '';
  63. function createSlug($string) {
  64. $greek = ['α','ά','Α','Ά','β','Β','γ','Γ','δ','Δ','ε','έ','Ε','Έ','ζ','Ζ','η','ή','Η','Ή','θ','Θ','ι','ί','ϊ','ΐ','Ι','Ί','Ϊ','κ','Κ','λ','Λ','μ','Μ','ν','Ν','ξ','Ξ','ο','ό','Ο','Ό','π','Π','ρ','Ρ','σ','ς','Σ','τ','Τ','υ','ύ','ϋ','ΰ','Υ','Ύ','Ϋ','φ','Φ','χ','Χ','ψ','Ψ','ω','ώ','Ω','Ώ'];
  65. $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'];
  66. $string = str_replace($greek, $latin, $string);
  67. $string = strtolower(trim($string));
  68. $string = preg_replace('/[^a-z0-9-]/', '-', $string);
  69. $string = preg_replace('/-+/', "-", $string);
  70. return trim($string, '-');
  71. }
  72. // Χειρισμός Μετακίνησης
  73. if (isset($_GET['move']) && isset($_GET['id'])) {
  74. $move_id = (int)$_GET['id'];
  75. $direction = $_GET['move'];
  76. $all = $pdo->query("
  77. SELECT c1.id
  78. FROM categories c1
  79. LEFT JOIN categories c2 ON c1.parent_id = c2.id
  80. ORDER BY c1.sort_order ASC, c2.name_el ASC, c1.name_el ASC
  81. ")->fetchAll(PDO::FETCH_ASSOC);
  82. $current_index = -1;
  83. $seq = 1;
  84. foreach ($all as $idx => $row) {
  85. $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$seq, $row['id']]);
  86. $all[$idx]['sort_order'] = $seq;
  87. if ($row['id'] == $move_id) { $current_index = $idx; }
  88. $seq++;
  89. }
  90. if ($current_index !== -1) {
  91. if ($direction === 'up' && $current_index > 0) {
  92. $prev_id = $all[$current_index - 1]['id'];
  93. $current_order = $all[$current_index]['sort_order'];
  94. $prev_order = $all[$current_index - 1]['sort_order'];
  95. $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$prev_order, $move_id]);
  96. $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$current_order, $prev_id]);
  97. } elseif ($direction === 'down' && $current_index < count($all) - 1) {
  98. $next_id = $all[$current_index + 1]['id'];
  99. $current_order = $all[$current_index]['sort_order'];
  100. $next_order = $all[$current_index + 1]['sort_order'];
  101. $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$next_order, $move_id]);
  102. $pdo->prepare("UPDATE categories SET sort_order = ? WHERE id = ?")->execute([$current_order, $next_id]);
  103. }
  104. }
  105. header("Location: categories.php");
  106. exit;
  107. }
  108. // Χειρισμός POST
  109. if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
  110. $action = $_POST['action'];
  111. $name_el = trim($_POST['name_el']);
  112. $name_en = trim($_POST['name_en']);
  113. $parent_id = !empty($_POST['parent_id']) ? $_POST['parent_id'] : null;
  114. $attribute_group_id = !empty($_POST['attribute_group_id']) ? (int)$_POST['attribute_group_id'] : null;
  115. if (!empty($name_el)) {
  116. $slug_source = !empty($name_en) ? $name_en : $name_el;
  117. $slug = createSlug($slug_source);
  118. try {
  119. if ($action == 'add') {
  120. $stmt = $pdo->prepare("INSERT INTO categories (parent_id, name_el, name_en, slug, attribute_group_id, sort_order) VALUES (?, ?, ?, ?, ?, 999)");
  121. $stmt->execute([$parent_id, $name_el, $name_en, $slug, $attribute_group_id]);
  122. $message = "<div class='alert success'>{$t['msg_add_ok']}</div>";
  123. } elseif ($action == 'edit') {
  124. $category_id = (int)$_POST['category_id'];
  125. $stmt = $pdo->prepare("UPDATE categories SET parent_id = ?, name_el = ?, name_en = ?, slug = ?, attribute_group_id = ? WHERE id = ?");
  126. $stmt->execute([$parent_id, $name_el, $name_en, $slug, $attribute_group_id, $category_id]);
  127. $message = "<div class='alert success'>{$t['msg_edit_ok']}</div>";
  128. }
  129. } catch (PDOException $e) {
  130. $message = "<div class='alert error'>{$t['msg_err_slug']} " . $e->getMessage() . "</div>";
  131. }
  132. } else {
  133. $message = "<div class='alert error'>{$t['msg_err_name']}</div>";
  134. }
  135. }
  136. // Χειρισμός Διαγραφής
  137. if (isset($_GET['delete'])) {
  138. $delete_id = (int)$_GET['delete'];
  139. $checkStmt = $pdo->prepare("SELECT COUNT(*) FROM products WHERE category_id = ?");
  140. $checkStmt->execute([$delete_id]);
  141. $hasProducts = $checkStmt->fetchColumn();
  142. $checkChild = $pdo->prepare("SELECT COUNT(*) FROM categories WHERE parent_id = ?");
  143. $checkChild->execute([$delete_id]);
  144. $hasChildren = $checkChild->fetchColumn();
  145. if ($hasProducts > 0) {
  146. $message = "<div class='alert error'>{$t['msg_err_prod']}</div>";
  147. } elseif ($hasChildren > 0) {
  148. $message = "<div class='alert error'>{$t['msg_err_child']}</div>";
  149. } else {
  150. try {
  151. $stmt = $pdo->prepare("DELETE FROM categories WHERE id = ?");
  152. $stmt->execute([$delete_id]);
  153. $message = "<div class='alert success'>{$t['msg_del_ok']}</div>";
  154. } catch (PDOException $e) {
  155. $message = "<div class='alert error'>Σφάλμα: " . $e->getMessage() . "</div>";
  156. }
  157. }
  158. }
  159. $edit_category = null;
  160. if (isset($_GET['edit'])) {
  161. $edit_id = (int)$_GET['edit'];
  162. $editStmt = $pdo->prepare("SELECT * FROM categories WHERE id = ?");
  163. $editStmt->execute([$edit_id]);
  164. $edit_category = $editStmt->fetch();
  165. }
  166. $stmt = $pdo->query("
  167. SELECT c1.id, c1.parent_id, c1.name_el, c1.name_en, c1.slug, c1.sort_order, ag.name as group_name
  168. FROM categories c1
  169. LEFT JOIN attribute_groups ag ON c1.attribute_group_id = ag.id
  170. ORDER BY c1.sort_order ASC, c1.name_el ASC
  171. ");
  172. $all_categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
  173. function buildCategoryTree($categories, $parentId = null) {
  174. $branch = [];
  175. foreach ($categories as $cat) {
  176. if ($cat['parent_id'] == $parentId) {
  177. $children = buildCategoryTree($categories, $cat['id']);
  178. if ($children) {
  179. $cat['children'] = $children;
  180. }
  181. $branch[] = $cat;
  182. }
  183. }
  184. return $branch;
  185. }
  186. $categoryTree = buildCategoryTree($all_categories);
  187. function renderTreeHTML($tree, $level = 0, $t) {
  188. $html = '<ul class="' . ($level === 0 ? 'tree-root' : 'tree-branch') . '">';
  189. foreach ($tree as $node) {
  190. $padding = $level * 35;
  191. $html .= '<li class="tree-node">';
  192. $html .= '<div class="tree-row" style="padding-left: ' . ($padding + 15) . 'px;">';
  193. if ($level > 0) {
  194. $html .= '<span class="tree-line"></span>';
  195. }
  196. $html .= '<div class="tree-arrows">';
  197. $html .= '<a href="categories.php?move=up&id='.$node['id'].'">▲</a>';
  198. $html .= '<a href="categories.php?move=down&id='.$node['id'].'">▼</a>';
  199. $html .= '</div>';
  200. $html .= '<div class="tree-info">';
  201. $html .= '<strong>' . htmlspecialchars($node['name_el']) . '</strong>';
  202. if (!empty($node['name_en'])) {
  203. $html .= ' <span class="tree-en">(' . htmlspecialchars($node['name_en']) . ')</span>';
  204. }
  205. $html .= ' <span class="tree-slug">/' . htmlspecialchars($node['slug']) . '</span>';
  206. if (!empty($node['group_name'])) {
  207. $html .= ' <span class="tree-badge">' . htmlspecialchars($node['group_name']) . '</span>';
  208. }
  209. $html .= '</div>';
  210. $html .= '<div class="tree-actions">';
  211. $html .= '<a href="categories.php?edit='.$node['id'].'" class="action-link action-edit">Edit</a>';
  212. $html .= '<a href="categories.php?delete='.$node['id'].'" class="action-link action-delete" onclick="return confirm(\''.$t['confirm_del'].'\');">Delete</a>';
  213. $html .= '</div>';
  214. $html .= '</div>';
  215. if (!empty($node['children'])) {
  216. $html .= renderTreeHTML($node['children'], $level + 1, $t);
  217. }
  218. $html .= '</li>';
  219. }
  220. $html .= '</ul>';
  221. return $html;
  222. }
  223. $stmtParents = $pdo->query("SELECT id, name_el FROM categories WHERE parent_id IS NULL ORDER BY sort_order ASC, name_el ASC");
  224. $parentCategories = $stmtParents->fetchAll();
  225. $attrGroups = $pdo->query("SELECT id, name FROM attribute_groups ORDER BY name")->fetchAll();
  226. ?>
  227. <!DOCTYPE html>
  228. <html lang="<?php echo $current_lang; ?>">
  229. <head>
  230. <meta charset="UTF-8">
  231. <title><?php echo $t['manage_cats']; ?> - Hockey CMS</title>
  232. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
  233. <style>
  234. :root {
  235. --primary: #f97316;
  236. --primary-hover: #ea580c;
  237. --bg-color: #0b1120;
  238. --panel-bg: #1e293b;
  239. --border-color: #334155;
  240. --text-main: #f8fafc;
  241. --text-muted: #94a3b8;
  242. }
  243. body { font-family: 'Inter', sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; }
  244. .content { margin-left: 250px; padding: 30px; }
  245. .content h2 { margin-top: 0; color: var(--primary); font-size: 26px; text-transform: uppercase; letter-spacing: 0.5px; }
  246. .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; }
  247. .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; }
  248. .form-group { margin-bottom: 20px; }
  249. .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;}
  250. .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; }
  251. .form-group input:focus, .form-group select:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2); }
  252. .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; }
  253. .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
  254. .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;}
  255. .btn-cancel:hover { background: rgba(255,255,255,0.1); }
  256. .alert { padding: 15px; border-radius: 6px; margin-bottom: 20px; font-weight: 600; font-size: 14px;}
  257. .success { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
  258. .error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
  259. /* --- DARK TREEVIEW STYLES --- */
  260. .tree-root { list-style: none; padding: 0; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; }
  261. .tree-branch { list-style: none; padding: 0; margin: 0; }
  262. .tree-node { border-bottom: 1px solid rgba(255,255,255,0.05); }
  263. .tree-node:last-child { border-bottom: none; }
  264. .tree-row { display: flex; align-items: center; padding: 15px; background: var(--panel-bg); transition: 0.2s; position: relative; }
  265. .tree-row:hover { background: rgba(255,255,255,0.02); }
  266. .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; }
  267. .tree-arrows { display: flex; flex-direction: column; margin-right: 15px; gap: 3px; }
  268. .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;}
  269. .tree-arrows a:hover { background: var(--primary); color: white; border-color: var(--primary); }
  270. .tree-info { flex-grow: 1; }
  271. .tree-info strong { color: var(--text-main); font-size: 15px; }
  272. .tree-en { color: var(--text-muted); font-size: 13px; font-weight: 500;}
  273. .tree-slug { color: #3b82f6; font-size: 12px; margin-left: 10px; font-family: monospace;}
  274. .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;}
  275. .tree-actions { flex-shrink: 0; }
  276. .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;}
  277. .action-edit { background-color: rgba(249, 115, 22, 0.1); color: var(--primary); border: 1px solid rgba(249, 115, 22, 0.2); }
  278. .action-edit:hover { background-color: var(--primary); color: white; }
  279. .action-delete { background-color: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
  280. .action-delete:hover { background-color: #ef4444; color: white; }
  281. </style>
  282. </head>
  283. <body>
  284. <?php include 'sidebar.php'; ?>
  285. <div class="content">
  286. <h2><?php echo $t['manage_cats']; ?></h2>
  287. <?php echo $message; ?>
  288. <div class="card">
  289. <h3><?php echo $edit_category ? $t['edit_cat'] : $t['add_cat']; ?></h3>
  290. <form method="POST" action="categories.php">
  291. <input type="hidden" name="action" value="<?php echo $edit_category ? 'edit' : 'add'; ?>">
  292. <?php if ($edit_category): ?>
  293. <input type="hidden" name="category_id" value="<?php echo $edit_category['id']; ?>">
  294. <?php endif; ?>
  295. <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
  296. <div class="form-group">
  297. <label><?php echo $t['name_el']; ?></label>
  298. <input type="text" name="name_el" value="<?php echo $edit_category ? htmlspecialchars($edit_category['name_el']) : ''; ?>" required>
  299. </div>
  300. <div class="form-group">
  301. <label><?php echo $t['name_en']; ?></label>
  302. <input type="text" name="name_en" value="<?php echo $edit_category ? htmlspecialchars($edit_category['name_en'] ?? '') : ''; ?>">
  303. </div>
  304. </div>
  305. <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
  306. <div class="form-group">
  307. <label><?php echo $t['parent_cat']; ?></label>
  308. <select name="parent_id">
  309. <option value=""><?php echo $t['is_main']; ?></option>
  310. <?php foreach($parentCategories as $parent): ?>
  311. <?php if (!$edit_category || $edit_category['id'] != $parent['id']): ?>
  312. <option value="<?php echo $parent['id']; ?>" <?php echo ($edit_category && $edit_category['parent_id'] == $parent['id']) ? 'selected' : ''; ?>>
  313. <?php echo htmlspecialchars($parent['name_el']); ?>
  314. </option>
  315. <?php endif; ?>
  316. <?php endforeach; ?>
  317. </select>
  318. </div>
  319. <div class="form-group">
  320. <label><?php echo $t['attr_group']; ?></label>
  321. <select name="attribute_group_id">
  322. <option value=""><?php echo $t['no_attr']; ?></option>
  323. <?php foreach($attrGroups as $group): ?>
  324. <option value="<?php echo $group['id']; ?>" <?php echo ($edit_category && $edit_category['attribute_group_id'] == $group['id']) ? 'selected' : ''; ?>>
  325. <?php echo htmlspecialchars($group['name']); ?>
  326. </option>
  327. <?php endforeach; ?>
  328. </select>
  329. </div>
  330. </div>
  331. <button type="submit" class="btn"><?php echo $edit_category ? $t['btn_save'] : $t['btn_add']; ?></button>
  332. <?php if ($edit_category): ?>
  333. <a href="categories.php" class="btn-cancel"><?php echo $t['btn_cancel']; ?></a>
  334. <?php endif; ?>
  335. </form>
  336. </div>
  337. <div class="card">
  338. <h3>📂 <?php echo $t['tree_title']; ?></h3>
  339. <?php
  340. if (!empty($categoryTree)) {
  341. echo renderTreeHTML($categoryTree, 0, $t);
  342. } else {
  343. echo "<p style='color: var(--text-muted); padding: 20px; text-align: center;'>{$t['no_cats']}</p>";
  344. }
  345. ?>
  346. </div>
  347. </div>
  348. </body>
  349. </html>