settings.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  1. <?php
  2. require_once __DIR__ . "/../config.php";
  3. require_once __DIR__ . "/../includes/functions.php";
  4. require_once __DIR__ . "/../includes/version.php";
  5. require_once __DIR__ . "/../includes/backup.php";
  6. if (!defined("UPDATE_MANIFEST_URL")) {
  7. define("UPDATE_MANIFEST_URL", "");
  8. }
  9. function settingsUpdaterVersionCompareValue(string $version): string
  10. {
  11. return ltrim(trim($version), "vV");
  12. }
  13. function settingsGetUpdaterStatus(): array
  14. {
  15. $manifestUrl = trim((string) UPDATE_MANIFEST_URL);
  16. if ($manifestUrl === "") {
  17. return [
  18. "label" => "Update-Ziel ist nicht konfiguriert.",
  19. "available" => false,
  20. "version" => "",
  21. ];
  22. }
  23. $context = stream_context_create([
  24. "http" => [
  25. "method" => "GET",
  26. "timeout" => 3,
  27. "ignore_errors" => true,
  28. "header" => "User-Agent: PSA-Orderform-Settings/" . APP_VERSION . "\r\n",
  29. ],
  30. ]);
  31. $body = @file_get_contents($manifestUrl, false, $context);
  32. if ($body === false) {
  33. return [
  34. "label" => "Update-Status konnte nicht geladen werden.",
  35. "available" => false,
  36. "version" => "",
  37. ];
  38. }
  39. $manifest = json_decode($body, true);
  40. if (!is_array($manifest)) {
  41. return [
  42. "label" => "Update-Status ist ungültig.",
  43. "available" => false,
  44. "version" => "",
  45. ];
  46. }
  47. $version = trim((string) ($manifest["version"] ?? $manifest["latest"] ?? ""));
  48. if (!preg_match('/^v\d+\.\d+\.\d+$/', $version)) {
  49. return [
  50. "label" => "Update-Version ist ungültig.",
  51. "available" => false,
  52. "version" => "",
  53. ];
  54. }
  55. $available =
  56. version_compare(
  57. settingsUpdaterVersionCompareValue($version),
  58. settingsUpdaterVersionCompareValue(APP_VERSION),
  59. ">",
  60. );
  61. return [
  62. "label" => $available
  63. ? "Update verfügbar: " . $version
  64. : "Kein Update verfügbar.",
  65. "available" => $available,
  66. "version" => $version,
  67. ];
  68. }
  69. function settingsFormatBackupDate(string $date): string
  70. {
  71. $timestamp = strtotime($date);
  72. if ($timestamp === false) {
  73. return $date;
  74. }
  75. return date("d.m.Y H:i", $timestamp);
  76. }
  77. function settingsGetBackupUploadLabel(array $backup): string
  78. {
  79. $uploads =
  80. isset($backup["remote_uploads"]) && is_array($backup["remote_uploads"])
  81. ? $backup["remote_uploads"]
  82. : [];
  83. if (empty($uploads)) {
  84. return "Nur lokal";
  85. }
  86. $successful = 0;
  87. foreach ($uploads as $upload) {
  88. if (is_array($upload) && !empty($upload["success"])) {
  89. $successful++;
  90. }
  91. }
  92. if ($successful === count($uploads)) {
  93. return "Remote erfolgreich (" . $successful . ")";
  94. }
  95. if ($successful > 0) {
  96. return "Teilweise erfolgreich (" . $successful . "/" . count($uploads) . ")";
  97. }
  98. return "Remote fehlgeschlagen";
  99. }
  100. function settingsGetBackupCapabilityLabel(array $capability): string
  101. {
  102. if (empty($capability["configured"])) {
  103. return "nicht konfiguriert";
  104. }
  105. return !empty($capability["available"]) ? "bereit" : "nicht verfügbar";
  106. }
  107. function settingsIsSuperAdmin(): bool
  108. {
  109. return normalizeAdminUsername($_SESSION['admin_username'] ?? "") === "admin";
  110. }
  111. function settingsGetLocalConfigPath(): string
  112. {
  113. return dirname(__DIR__) . "/config.php";
  114. }
  115. function settingsReadLocalConfig(): string
  116. {
  117. $path = settingsGetLocalConfigPath();
  118. if (!is_file($path) || !is_readable($path)) {
  119. return "";
  120. }
  121. $content = file_get_contents($path);
  122. return $content === false ? "" : $content;
  123. }
  124. function settingsWriteLocalConfig(string $content): void
  125. {
  126. $content = str_replace("\r\n", "\n", $content);
  127. if (!str_starts_with(ltrim($content), "<?php")) {
  128. throw new RuntimeException('Die Konfiguration muss mit "<?php" beginnen.');
  129. }
  130. try {
  131. token_get_all($content, TOKEN_PARSE);
  132. } catch (ParseError $parseError) {
  133. throw new RuntimeException("PHP-Syntaxfehler: " . $parseError->getMessage());
  134. }
  135. $path = settingsGetLocalConfigPath();
  136. $tmpFile = $path . ".tmp";
  137. if (file_put_contents($tmpFile, $content, LOCK_EX) === false) {
  138. throw new RuntimeException("Konfiguration konnte nicht geschrieben werden.");
  139. }
  140. $backupDir = DATA_DIR . "backups/config/";
  141. if (!is_dir($backupDir) && !mkdir($backupDir, 0775, true) && !is_dir($backupDir)) {
  142. @unlink($tmpFile);
  143. throw new RuntimeException("Sicherungsverzeichnis konnte nicht angelegt werden.");
  144. }
  145. if (is_file($path)) {
  146. $backupPath = $backupDir . "config-" . date("Ymd-His") . ".php.bak";
  147. if (!copy($path, $backupPath)) {
  148. @unlink($tmpFile);
  149. throw new RuntimeException("Sicherung der bestehenden Konfiguration ist fehlgeschlagen.");
  150. }
  151. }
  152. @chmod($tmpFile, 0664);
  153. if (!rename($tmpFile, $path)) {
  154. @unlink($tmpFile);
  155. throw new RuntimeException("Konfiguration konnte nicht gespeichert werden.");
  156. }
  157. if (function_exists("opcache_invalidate")) {
  158. opcache_invalidate($path, true);
  159. }
  160. }
  161. function settingsFindBackupByFilename(string $filename): ?array
  162. {
  163. if ($filename === "" || basename($filename) !== $filename) {
  164. return null;
  165. }
  166. foreach (backupListBackups() as $backup) {
  167. if (($backup["filename"] ?? "") === $filename) {
  168. return $backup;
  169. }
  170. }
  171. return null;
  172. }
  173. function settingsSendBackupDownload(array $backup): void
  174. {
  175. $filename = basename((string) ($backup["filename"] ?? ""));
  176. $path = backupGetDirectory() . $filename;
  177. if ($filename === "" || !is_file($path) || !is_readable($path)) {
  178. throw new RuntimeException("Backup-Datei wurde nicht gefunden.");
  179. }
  180. $size = filesize($path);
  181. if ($size === false) {
  182. throw new RuntimeException("Backup-Dateigröße konnte nicht gelesen werden.");
  183. }
  184. logAccess("Backup downloaded", [
  185. "filename" => $filename,
  186. ]);
  187. header("Content-Type: application/zip");
  188. header('Content-Disposition: attachment; filename="' . $filename . '"');
  189. header("Content-Length: " . (string) $size);
  190. header("X-Content-Type-Options: nosniff");
  191. readfile($path);
  192. exit();
  193. }
  194. if (empty($_SESSION['admin_logged_in'])) {
  195. header("Location: login.php");
  196. exit();
  197. }
  198. $pageTitle = "Einstellungen";
  199. $message = "";
  200. $messageType = "";
  201. $isSuperAdmin = settingsIsSuperAdmin();
  202. if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_settings'])) {
  203. // Validate CSRF token
  204. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  205. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  206. $messageType = "error";
  207. } else {
  208. $settings = array_merge(getSystemSettings(), [
  209. "order_recipient_email" => $_POST['order_recipient_email'] ?? "",
  210. "attach_order_pdf_to_admin_email" => isset(
  211. $_POST['attach_order_pdf_to_admin_email'],
  212. ),
  213. "nametag_product_name" => $_POST['nametag_product_name'] ?? "",
  214. ]);
  215. if (saveSystemSettings($settings)) {
  216. logAccess("Admin updated system settings");
  217. $message = "Einstellungen wurden gespeichert.";
  218. $messageType = "success";
  219. } else {
  220. $message = "Einstellungen konnten nicht gespeichert werden.";
  221. $messageType = "error";
  222. }
  223. }
  224. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['create_backup'])) {
  225. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  226. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  227. $messageType = "error";
  228. } else {
  229. try {
  230. $backup = backupCreate("manual");
  231. $message =
  232. "Backup wurde erstellt: " .
  233. $backup["filename"] .
  234. " (" .
  235. backupFormatBytes((int) $backup["size"]) .
  236. ").";
  237. $messageType = "success";
  238. } catch (Throwable $exception) {
  239. $message = "Backup konnte nicht erstellt werden: " . $exception->getMessage();
  240. $messageType = "error";
  241. }
  242. }
  243. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['download_backup'])) {
  244. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  245. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  246. $messageType = "error";
  247. } else {
  248. try {
  249. $backup = settingsFindBackupByFilename((string) ($_POST["backup_filename"] ?? ""));
  250. if ($backup === null) {
  251. throw new RuntimeException("Backup wurde nicht gefunden.");
  252. }
  253. settingsSendBackupDownload($backup);
  254. } catch (Throwable $exception) {
  255. $message = "Backup konnte nicht heruntergeladen werden: " . $exception->getMessage();
  256. $messageType = "error";
  257. }
  258. }
  259. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_local_config'])) {
  260. if (!$isSuperAdmin) {
  261. http_response_code(403);
  262. $message = "Keine Berechtigung für diese Aktion.";
  263. $messageType = "error";
  264. } elseif (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  265. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  266. $messageType = "error";
  267. } else {
  268. try {
  269. settingsWriteLocalConfig((string) ($_POST['local_config_content'] ?? ""));
  270. logAccess("Admin updated local config.php");
  271. $message = "Lokale Konfiguration wurde gespeichert.";
  272. $messageType = "success";
  273. } catch (Throwable $exception) {
  274. $message = "Konfiguration konnte nicht gespeichert werden: " . $exception->getMessage();
  275. $messageType = "error";
  276. }
  277. }
  278. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['add_category'])) {
  279. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  280. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  281. $messageType = "error";
  282. } else {
  283. $categories = getCategories();
  284. $label = normalizeCategoryLabel($_POST['category_label'] ?? "");
  285. if (!isValidCategoryLabel($label)) {
  286. $message = "Bitte geben Sie einen Kategorienamen mit maximal 80 Zeichen ein.";
  287. $messageType = "error";
  288. } else {
  289. $categoryId = generateCategoryIdFromLabel($label, $categories);
  290. $categories[] = [
  291. "id" => $categoryId,
  292. "label" => $label,
  293. ];
  294. if (saveCategories($categories)) {
  295. logAccess("Admin added category", [
  296. "category_id" => $categoryId,
  297. "label" => $label,
  298. ]);
  299. $message = "Kategorie wurde erfolgreich angelegt.";
  300. $messageType = "success";
  301. } else {
  302. $message = "Kategorie konnte nicht gespeichert werden.";
  303. $messageType = "error";
  304. }
  305. }
  306. }
  307. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['update_category'])) {
  308. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  309. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  310. $messageType = "error";
  311. } else {
  312. $categories = getCategories();
  313. $categoryId = normalizeCategoryId($_POST['category_id'] ?? "");
  314. $label = normalizeCategoryLabel($_POST['category_label'] ?? "");
  315. $found = false;
  316. if (!isValidCategoryLabel($label)) {
  317. $message = "Bitte geben Sie einen Kategorienamen mit maximal 80 Zeichen ein.";
  318. $messageType = "error";
  319. } else {
  320. foreach ($categories as &$category) {
  321. if ($category["id"] === $categoryId) {
  322. $category["label"] = $label;
  323. $found = true;
  324. break;
  325. }
  326. }
  327. unset($category);
  328. if (!$found) {
  329. $message = "Kategorie nicht gefunden.";
  330. $messageType = "error";
  331. } elseif (saveCategories($categories)) {
  332. logAccess("Admin updated category", [
  333. "category_id" => $categoryId,
  334. "label" => $label,
  335. ]);
  336. $message = "Kategorie wurde erfolgreich aktualisiert.";
  337. $messageType = "success";
  338. } else {
  339. $message = "Kategorie konnte nicht gespeichert werden.";
  340. $messageType = "error";
  341. }
  342. }
  343. }
  344. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['delete_category'])) {
  345. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  346. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  347. $messageType = "error";
  348. } else {
  349. $categoryId = normalizeCategoryId($_POST['category_id'] ?? "");
  350. $label = "";
  351. $found = false;
  352. if (isCategoryInUse($categoryId)) {
  353. $message =
  354. "Diese Kategorie wird noch von Produkten verwendet und kann nicht gelöscht werden.";
  355. $messageType = "error";
  356. } else {
  357. $categories = array_values(
  358. array_filter(getCategories(), function ($category) use (
  359. $categoryId,
  360. &$found,
  361. &$label,
  362. ) {
  363. if ($category["id"] === $categoryId) {
  364. $found = true;
  365. $label = $category["label"];
  366. return false;
  367. }
  368. return true;
  369. }),
  370. );
  371. if (!$found) {
  372. $message = "Kategorie nicht gefunden.";
  373. $messageType = "error";
  374. } elseif (saveCategories($categories)) {
  375. logAccess("Admin deleted category", [
  376. "category_id" => $categoryId,
  377. "label" => $label,
  378. ]);
  379. $message = "Kategorie wurde gelöscht.";
  380. $messageType = "success";
  381. } else {
  382. $message = "Kategorie konnte nicht gelöscht werden.";
  383. $messageType = "error";
  384. }
  385. }
  386. }
  387. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_faq'])) {
  388. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  389. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  390. $messageType = "error";
  391. } else {
  392. $content = isset($_POST['content']) ? (string) $_POST['content'] : "";
  393. $faqSettings = array_merge(getSystemSettings(), [
  394. "startpage_intro_text" => isset($_POST['startpage_intro_text'])
  395. ? (string) $_POST['startpage_intro_text']
  396. : "",
  397. ]);
  398. if (saveFaqContent($content) && saveSystemSettings($faqSettings)) {
  399. logAccess("Admin updated FAQ content");
  400. $message = "FAQ-Inhalt und Startseitentext wurden gespeichert.";
  401. $messageType = "success";
  402. } else {
  403. $message = "FAQ-Inhalt und/oder Startseitentext konnten nicht gespeichert werden.";
  404. $messageType = "error";
  405. }
  406. }
  407. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['add_organization'])) {
  408. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  409. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  410. $messageType = "error";
  411. } else {
  412. $organizations = getOrganizations(false);
  413. $label = normalizeOrganizationLabel($_POST['organization_label'] ?? "");
  414. $sortOrder = (int) ($_POST['sort_order'] ?? 0);
  415. $active = isset($_POST['active']);
  416. if (!isValidOrganizationLabel($label)) {
  417. $message = "Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.";
  418. $messageType = "error";
  419. } else {
  420. $orgId = generateOrganizationIdFromLabel($label, $organizations);
  421. $organizations[] = [
  422. "id" => $orgId,
  423. "label" => $label,
  424. "sort_order" => $sortOrder,
  425. "active" => $active,
  426. ];
  427. if (saveOrganizations($organizations)) {
  428. logAccess("Admin added organization", [
  429. "org_id" => $orgId,
  430. "label" => $label,
  431. ]);
  432. $message = "Organisation wurde angelegt.";
  433. $messageType = "success";
  434. } else {
  435. $message = "Organisation konnte nicht gespeichert werden.";
  436. $messageType = "error";
  437. }
  438. }
  439. }
  440. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['update_organization'])) {
  441. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  442. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  443. $messageType = "error";
  444. } else {
  445. $organizations = getOrganizations(false);
  446. $organizationId = normalizeOrganizationId($_POST['organization_id'] ?? "");
  447. $label = normalizeOrganizationLabel($_POST['organization_label'] ?? "");
  448. $sortOrder = (int) ($_POST['sort_order'] ?? 0);
  449. $active = isset($_POST['active']);
  450. $updated = false;
  451. if (!isValidOrganizationLabel($label)) {
  452. $message = "Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.";
  453. $messageType = "error";
  454. } else {
  455. foreach ($organizations as &$organization) {
  456. if ($organization["id"] !== $organizationId) {
  457. continue;
  458. }
  459. $organization["label"] = $label;
  460. $organization["sort_order"] = $sortOrder;
  461. $organization["active"] = $active;
  462. $updated = true;
  463. break;
  464. }
  465. unset($organization);
  466. if (!$updated) {
  467. $message = "Organisation nicht gefunden.";
  468. $messageType = "error";
  469. } elseif (saveOrganizations($organizations)) {
  470. logAccess("Admin updated organization", [
  471. "org_id" => $organizationId,
  472. "label" => $label,
  473. ]);
  474. $message = "Organisation wurde aktualisiert.";
  475. $messageType = "success";
  476. } else {
  477. $message = "Organisation konnte nicht gespeichert werden.";
  478. $messageType = "error";
  479. }
  480. }
  481. }
  482. } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['delete_organization'])) {
  483. if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
  484. $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
  485. $messageType = "error";
  486. } else {
  487. $organizationId = normalizeOrganizationId($_POST['organization_id'] ?? "");
  488. $orgLabel = "";
  489. foreach (getOrganizations(false) as $organization) {
  490. if ($organization["id"] === $organizationId) {
  491. $orgLabel = $organization["label"];
  492. break;
  493. }
  494. }
  495. $organizations = array_values(
  496. array_filter(getOrganizations(false), function ($organization) use (
  497. $organizationId,
  498. ) {
  499. return $organization["id"] !== $organizationId;
  500. }),
  501. );
  502. if (saveOrganizations($organizations)) {
  503. logAccess("Admin deleted organization", [
  504. "org_id" => $organizationId,
  505. "label" => $orgLabel,
  506. ]);
  507. $message = "Organisation wurde gelöscht.";
  508. $messageType = "success";
  509. } else {
  510. $message = "Organisation konnte nicht gelöscht werden.";
  511. $messageType = "error";
  512. }
  513. }
  514. }
  515. $settings = getSystemSettings();
  516. $updaterStatus = settingsGetUpdaterStatus();
  517. $backupCapabilities = backupRemoteCapabilities();
  518. $backups = backupListBackups();
  519. $localConfigContent = $isSuperAdmin ? settingsReadLocalConfig() : "";
  520. $categories = getCategories();
  521. $products = getProducts();
  522. $organizations = getOrganizations(false);
  523. $faqContent = getFaqContent();
  524. $startpageIntroText = (string) ($settings["startpage_intro_text"] ?? "");
  525. $editCategoryId = normalizeCategoryId($_GET['edit_category'] ?? "");
  526. $editingCategory = null;
  527. if ($editCategoryId !== "") {
  528. $editingCategory = getCategoryById($editCategoryId);
  529. if ($editingCategory === null && $message === "") {
  530. $message = "Ausgewählte Kategorie wurde nicht gefunden.";
  531. $messageType = "error";
  532. }
  533. }
  534. $editingOrganization = isset($_GET['edit_organization'])
  535. ? getOrganizationById($_GET['edit_organization'])
  536. : null;
  537. $bodyClass = "admin-page";
  538. include __DIR__ . "/../includes/header.php";
  539. ?>
  540. <div class="admin-header">
  541. <h2>Einstellungen</h2>
  542. <div>
  543. <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
  544. </div>
  545. </div>
  546. <?php if ($message !== ""): ?>
  547. <div class="alert alert-<?php echo escape($messageType); ?>">
  548. <?php echo escape($message); ?>
  549. </div>
  550. <?php endif; ?>
  551. <div class="panel panel-lg" id="kategorien">
  552. <h3>Kategorien</h3>
  553. <?php if ($editingCategory !== null): ?>
  554. <h4>Kategorie bearbeiten</h4>
  555. <form method="POST" action="settings.php#kategorien">
  556. <?php echo csrfField(); ?>
  557. <input type="hidden" name="category_id" value="<?php echo escape(
  558. $editingCategory["id"],
  559. ); ?>">
  560. <div class="form-group">
  561. <label for="category_id_display">ID</label>
  562. <input type="text" id="category_id_display" value="<?php echo escape(
  563. $editingCategory["id"],
  564. ); ?>" readonly>
  565. <small>Die ID sollte gleichbleiben, damit Produktzuordnungen und Filter-URLs gültig bleiben.</small>
  566. </div>
  567. <div class="form-group">
  568. <label for="category_label_edit">Name *</label>
  569. <input type="text" id="category_label_edit" name="category_label" maxlength="80" required value="<?php echo escape(
  570. $editingCategory["label"],
  571. ); ?>">
  572. </div>
  573. <button type="submit" name="update_category" class="btn">Kategorie aktualisieren</button>
  574. <a href="settings.php#kategorien" class="btn btn-secondary">Abbrechen</a>
  575. </form>
  576. <?php else: ?>
  577. <h4>Neue Kategorie anlegen</h4>
  578. <form method="POST" action="settings.php#kategorien">
  579. <?php echo csrfField(); ?>
  580. <div class="form-group">
  581. <label for="category_label">Name *</label>
  582. <input type="text" id="category_label" name="category_label" maxlength="80" required placeholder="z.B. Accessoires">
  583. <small>Die technische ID wird einmalig automatisch aus dem Namen erzeugt.</small>
  584. </div>
  585. <button type="submit" name="add_category" class="btn">Kategorie anlegen</button>
  586. </form>
  587. <?php endif; ?>
  588. <div class="table-responsive mt-4">
  589. <table class="responsive-table">
  590. <thead>
  591. <tr>
  592. <th>Name</th>
  593. <th>ID</th>
  594. <th>Produkte</th>
  595. <th>Aktionen</th>
  596. </tr>
  597. </thead>
  598. <tbody>
  599. <?php foreach ($categories as $category): ?>
  600. <?php
  601. $productCount = 0;
  602. foreach ($products as $product) {
  603. if (productHasCategory($product, $category["id"])) {
  604. $productCount++;
  605. }
  606. }
  607. ?>
  608. <tr>
  609. <td data-label="Name"><?php echo escape(
  610. $category["label"],
  611. ); ?></td>
  612. <td data-label="ID"><code><?php echo escape(
  613. $category["id"],
  614. ); ?></code></td>
  615. <td data-label="Produkte"><?php echo $productCount; ?></td>
  616. <td data-label="Aktionen">
  617. <a href="settings.php?edit_category=<?php echo urlencode(
  618. $category["id"],
  619. ); ?>#kategorien" class="btn btn-small btn-secondary">Bearbeiten</a>
  620. <form method="POST" action="settings.php#kategorien" class="inline-form" onsubmit="return confirm('Kategorie wirklich löschen?');">
  621. <?php echo csrfField(); ?>
  622. <input type="hidden" name="category_id" value="<?php echo escape(
  623. $category["id"],
  624. ); ?>">
  625. <button type="submit" name="delete_category" class="btn btn-small">Löschen</button>
  626. </form>
  627. </td>
  628. </tr>
  629. <?php endforeach; ?>
  630. </tbody>
  631. </table>
  632. </div>
  633. </div>
  634. <div class="panel panel-lg mt-4" id="faq">
  635. <h3>FAQ</h3>
  636. <p class="mb-2">
  637. Unterstützte Markdown-Syntax: <code>#</code>, <code>##</code>, <code>###</code>, <code>**fett**</code>, <code>*kursiv*</code>, Listen mit <code>-</code> oder <code>1.</code>, Links mit <code>[Text](https://example.com)</code>
  638. </p>
  639. <form method="POST" action="settings.php#faq">
  640. <?php echo csrfField(); ?>
  641. <div class="form-group">
  642. <label for="content">FAQ-Inhalt (Markdown)</label>
  643. <textarea id="content" name="content" rows="18"><?php echo escape(
  644. $faqContent,
  645. ); ?></textarea>
  646. </div>
  647. <div class="form-group">
  648. <label for="startpage_intro_text">Startseitentext</label>
  649. <textarea id="startpage_intro_text" name="startpage_intro_text" rows="6"><?php echo escape(
  650. $startpageIntroText,
  651. ); ?></textarea>
  652. </div>
  653. <button type="submit" name="save_faq" class="btn">Speichern</button>
  654. </form>
  655. </div>
  656. <div class="panel panel-lg mt-4" id="organisationen">
  657. <h3>Organisationen</h3>
  658. <h4><?php echo $editingOrganization
  659. ? "Organisation bearbeiten"
  660. : "Neue Organisation"; ?></h4>
  661. <form method="POST" action="settings.php#organisationen">
  662. <?php echo csrfField(); ?>
  663. <?php if ($editingOrganization): ?>
  664. <input type="hidden" name="organization_id" value="<?php echo escape(
  665. $editingOrganization["id"],
  666. ); ?>">
  667. <?php endif; ?>
  668. <div class="form-group">
  669. <label for="organization_label">Name *</label>
  670. <input type="text" id="organization_label" name="organization_label" required maxlength="120" value="<?php echo escape(
  671. $editingOrganization["label"] ?? "",
  672. ); ?>">
  673. </div>
  674. <div class="form-group">
  675. <label for="sort_order">Sortierung</label>
  676. <input type="number" id="sort_order" name="sort_order" value="<?php echo escape(
  677. (string) ($editingOrganization["sort_order"] ?? 0),
  678. ); ?>">
  679. </div>
  680. <div class="form-group">
  681. <label>
  682. <input type="checkbox" name="active" value="1" <?php echo !isset(
  683. $editingOrganization["active"],
  684. ) || !empty($editingOrganization["active"])
  685. ? "checked"
  686. : ""; ?>>
  687. Organisation ist auswählbar
  688. </label>
  689. </div>
  690. <button type="submit" name="<?php echo $editingOrganization
  691. ? "update_organization"
  692. : "add_organization"; ?>" class="btn">
  693. <?php echo $editingOrganization
  694. ? "Speichern"
  695. : "Organisation anlegen"; ?>
  696. </button>
  697. <?php if ($editingOrganization): ?>
  698. <a href="settings.php#organisationen" class="btn btn-secondary">Abbrechen</a>
  699. <?php endif; ?>
  700. </form>
  701. <div class="table-responsive mt-4">
  702. <table class="responsive-table">
  703. <thead>
  704. <tr>
  705. <th>Name</th>
  706. <th>ID</th>
  707. <th>Sortierung</th>
  708. <th>Status</th>
  709. <th>Aktionen</th>
  710. </tr>
  711. </thead>
  712. <tbody>
  713. <?php foreach ($organizations as $organization): ?>
  714. <tr>
  715. <td data-label="Name"><?php echo escape(
  716. $organization["label"],
  717. ); ?></td>
  718. <td data-label="ID"><code><?php echo escape(
  719. $organization["id"],
  720. ); ?></code></td>
  721. <td data-label="Sortierung"><?php echo (int) $organization[
  722. "sort_order"
  723. ]; ?></td>
  724. <td data-label="Status">
  725. <span class="status <?php echo !empty(
  726. $organization["active"]
  727. )
  728. ? "status-open"
  729. : "status-cancelled"; ?>">
  730. <?php echo !empty($organization["active"])
  731. ? "Aktiv"
  732. : "Inaktiv"; ?>
  733. </span>
  734. </td>
  735. <td data-label="Aktionen">
  736. <a href="settings.php?edit_organization=<?php echo urlencode(
  737. $organization["id"],
  738. ); ?>#organisationen" class="btn btn-small">Bearbeiten</a>
  739. <form method="POST" action="settings.php#organisationen" class="inline-form" onsubmit="return confirm('Organisation wirklich löschen?');">
  740. <?php echo csrfField(); ?>
  741. <input type="hidden" name="organization_id" value="<?php echo escape(
  742. $organization["id"],
  743. ); ?>">
  744. <button type="submit" name="delete_organization" class="btn btn-secondary btn-small">Löschen</button>
  745. </form>
  746. </td>
  747. </tr>
  748. <?php endforeach; ?>
  749. </tbody>
  750. </table>
  751. </div>
  752. </div>
  753. <div class="panel panel-lg mt-4" id="allgemein">
  754. <h3>Allgemein</h3>
  755. <form method="POST" action="settings.php#allgemein">
  756. <?php echo csrfField(); ?>
  757. <div class="form-group">
  758. <label for="order_recipient_email">Empfängeradresse für interne Bestellungen *</label>
  759. <input type="email" id="order_recipient_email" name="order_recipient_email" required value="<?php echo escape(
  760. $settings["order_recipient_email"],
  761. ); ?>">
  762. </div>
  763. <div class="form-group">
  764. <label class="checkbox-label">
  765. <input type="checkbox" name="attach_order_pdf_to_admin_email" value="1" <?php echo !empty(
  766. $settings["attach_order_pdf_to_admin_email"]
  767. )
  768. ? "checked"
  769. : ""; ?>>
  770. PDF an interne Bestell-E-Mails anhängen
  771. </label>
  772. </div>
  773. <div class="form-group">
  774. <label for="nametag_product_name">Artikelname für Namensschilder *</label>
  775. <input type="text" id="nametag_product_name" name="nametag_product_name" required value="<?php echo escape(
  776. $settings["nametag_product_name"],
  777. ); ?>">
  778. <small>Muss exakt dem Produktnamen im Katalog entsprechen. Wird von der Namensschilder-Übersicht verwendet.</small>
  779. </div>
  780. <button type="submit" name="save_settings" class="btn">Speichern</button>
  781. </form>
  782. </div>
  783. <div class="panel panel-lg mt-4" id="backups">
  784. <h3>Backups</h3>
  785. <p>Lokale Aufbewahrung: <?php echo (int) backupGetRetentionLimit(); ?> Backups</p>
  786. <p>Automatisches Intervall: <?php echo (int) floor(((int) BACKUP_AUTO_INTERVAL_SECONDS) / 86400); ?> Tage</p>
  787. <p>
  788. S3: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["s3"])); ?> ·
  789. SFTP: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["sftp"])); ?> ·
  790. Custom: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["custom"])); ?> ·
  791. Managed: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["managed"])); ?>
  792. </p>
  793. <form method="POST" action="settings.php#backups" class="inline-form">
  794. <?php echo csrfField(); ?>
  795. <button type="submit" name="create_backup" class="btn">Backup erstellen</button>
  796. </form>
  797. <h4 class="mt-4">Letzte Backups</h4>
  798. <?php if (empty($backups)): ?>
  799. <p>Es wurden noch keine Backups erstellt.</p>
  800. <?php else: ?>
  801. <div class="table-responsive">
  802. <table class="responsive-table">
  803. <thead>
  804. <tr>
  805. <th>Erstellt</th>
  806. <th>Auslöser</th>
  807. <th>Größe</th>
  808. <th>Dateien</th>
  809. <th>Remote</th>
  810. <th>Aktionen</th>
  811. </tr>
  812. </thead>
  813. <tbody>
  814. <?php foreach ($backups as $backup): ?>
  815. <tr>
  816. <td data-label="Erstellt"><?php echo escape(settingsFormatBackupDate((string) ($backup["created_at"] ?? ""))); ?></td>
  817. <td data-label="Auslöser"><?php echo (($backup["trigger"] ?? "") === "automatic") ? "Automatisch" : "Manuell"; ?></td>
  818. <td data-label="Größe"><?php echo escape(backupFormatBytes((int) ($backup["size"] ?? 0))); ?></td>
  819. <td data-label="Dateien"><?php echo (int) ($backup["file_count"] ?? 0); ?></td>
  820. <td data-label="Remote"><?php echo escape(settingsGetBackupUploadLabel($backup)); ?></td>
  821. <td data-label="Aktionen">
  822. <form method="POST" action="settings.php#backups" class="inline-form">
  823. <?php echo csrfField(); ?>
  824. <input type="hidden" name="backup_filename" value="<?php echo escape($backup["filename"] ?? ""); ?>">
  825. <button type="submit" name="download_backup" class="btn btn-secondary btn-small">Download</button>
  826. </form>
  827. </td>
  828. </tr>
  829. <?php endforeach; ?>
  830. </tbody>
  831. </table>
  832. </div>
  833. <?php endif; ?>
  834. </div>
  835. <div class="panel panel-lg mt-4">
  836. <h3>Updater</h3>
  837. <p>Installierte Version: <?php echo escape(APP_VERSION); ?></p>
  838. <p>Update-Status: <?php echo escape($updaterStatus["label"]); ?></p>
  839. <p><a href="updater.php" class="btn btn-secondary">Updater öffnen</a></p>
  840. </div>
  841. <?php if ($isSuperAdmin): ?>
  842. <div class="panel panel-lg mt-4" id="konfiguration">
  843. <h3>Lokale Konfiguration</h3>
  844. <p>Bearbeitet <code>config.php</code> direkt auf diesem Server. Diese Datei wird beim Update nicht überschrieben.</p>
  845. <div class="alert alert-warning">Vor dem Speichern wird die PHP-Syntax geprüft und die vorherige Version automatisch gesichert. Fehlerhafte Änderungen können die gesamte Anwendung lahmlegen.</div>
  846. <form method="POST" action="settings.php#konfiguration">
  847. <?php echo csrfField(); ?>
  848. <div class="form-group">
  849. <label for="local_config_content">Inhalt von config.php</label>
  850. <textarea id="local_config_content" name="local_config_content" rows="24" class="config-editor" spellcheck="false"><?php echo escape(
  851. $localConfigContent,
  852. ); ?></textarea>
  853. </div>
  854. <button type="submit" name="save_local_config" class="btn">Speichern</button>
  855. </form>
  856. </div>
  857. <?php endif; ?>
  858. <?php include __DIR__ . "/../includes/footer.php"; ?>