From 30e21c6f6fad32ff6e314e8c56d82bda1beee237 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 26 May 2026 07:35:51 +0000 Subject: [PATCH] Sekut Bakery 1 --- admin.php | 974 ++++++++++++++++++ assets/images/favicon.svg | 21 + db/migrations/20260526_create_admin_users.sql | 12 + order_status.php | 2 +- robots.txt | 5 + store.php | 8 + 6 files changed, 1021 insertions(+), 1 deletion(-) create mode 100644 admin.php create mode 100644 assets/images/favicon.svg create mode 100644 db/migrations/20260526_create_admin_users.sql create mode 100644 robots.txt diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..7d3de08 --- /dev/null +++ b/admin.php @@ -0,0 +1,974 @@ +exec($sql); + $ready = true; + + return; + } + } + + db()->exec( + "CREATE TABLE IF NOT EXISTS admin_users ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + full_name VARCHAR(120) NOT NULL, + email VARCHAR(160) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + last_login_at TIMESTAMP NULL DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_admin_email (email), + INDEX idx_admin_created_at (created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + $ready = true; +} + +function admin_has_users(): bool +{ + admin_ensure_auth_schema(); + + $stmt = db()->query('SELECT COUNT(*) AS total FROM admin_users'); + $row = $stmt->fetch() ?: []; + + return (int)($row['total'] ?? 0) > 0; +} + +function admin_current_user(): ?array +{ + $sessionUser = $_SESSION[STORE_ADMIN_SESSION_KEY] ?? null; + if (!is_array($sessionUser)) { + return null; + } + + $id = (int)($sessionUser['id'] ?? 0); + $fullName = store_sanitize_line((string)($sessionUser['full_name'] ?? ''), 120); + $email = store_lower(trim((string)($sessionUser['email'] ?? ''))); + + if ($id <= 0 || $fullName === '' || $email === '') { + unset($_SESSION[STORE_ADMIN_SESSION_KEY]); + + return null; + } + + return [ + 'id' => $id, + 'full_name' => $fullName, + 'email' => $email, + ]; +} + +function admin_store_session(array $user): void +{ + $_SESSION[STORE_ADMIN_SESSION_KEY] = [ + 'id' => (int)($user['id'] ?? 0), + 'full_name' => store_sanitize_line((string)($user['full_name'] ?? ''), 120), + 'email' => store_lower(trim((string)($user['email'] ?? ''))), + ]; + + session_regenerate_id(true); +} + +function admin_logout(): void +{ + unset($_SESSION[STORE_ADMIN_SESSION_KEY]); + session_regenerate_id(true); +} + +function admin_is_authenticated(): bool +{ + return admin_current_user() !== null; +} + +function admin_csrf_token(): string +{ + $token = $_SESSION[STORE_ADMIN_CSRF_KEY] ?? ''; + if (!is_string($token) || $token === '') { + try { + $token = bin2hex(random_bytes(32)); + } catch (Throwable $exception) { + $token = sha1(session_id() . '|' . microtime(true)); + } + + $_SESSION[STORE_ADMIN_CSRF_KEY] = $token; + } + + return $token; +} + +function admin_verify_csrf(?string $token): bool +{ + if (!is_string($token) || $token === '') { + return false; + } + + return hash_equals(admin_csrf_token(), $token); +} + +function admin_auth_form_defaults(array $source = []): array +{ + return [ + 'full_name' => store_sanitize_line((string)($source['full_name'] ?? ''), 120), + 'email' => store_lower(trim((string)($source['email'] ?? ''))), + ]; +} + +function admin_password_error(string $password): string +{ + if (store_strlen($password) < 8) { + return 'Password admin minimal 8 karakter.'; + } + + return ''; +} + +function admin_create_first_user(array $source): array +{ + admin_ensure_auth_schema(); + + $form = admin_auth_form_defaults($source); + $password = (string)($source['password'] ?? ''); + $confirmPassword = (string)($source['confirm_password'] ?? ''); + + if (admin_has_users()) { + return [ + 'success' => false, + 'message' => 'Akun admin sudah dibuat. Silakan login dengan email dan password admin.', + 'form' => ['full_name' => '', 'email' => $form['email']], + ]; + } + + if (store_strlen($form['full_name']) < 3) { + return [ + 'success' => false, + 'message' => 'Nama admin minimal 3 karakter.', + 'form' => $form, + ]; + } + + if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) { + return [ + 'success' => false, + 'message' => 'Masukkan email admin yang valid.', + 'form' => $form, + ]; + } + + $passwordError = admin_password_error($password); + if ($passwordError !== '') { + return [ + 'success' => false, + 'message' => $passwordError, + 'form' => $form, + ]; + } + + if ($password !== $confirmPassword) { + return [ + 'success' => false, + 'message' => 'Konfirmasi password admin belum cocok.', + 'form' => $form, + ]; + } + + $stmt = db()->prepare( + 'INSERT INTO admin_users (full_name, email, password_hash, last_login_at) VALUES (:full_name, :email, :password_hash, NOW())' + ); + $stmt->bindValue(':full_name', $form['full_name']); + $stmt->bindValue(':email', $form['email']); + $stmt->bindValue(':password_hash', password_hash($password, PASSWORD_DEFAULT)); + $stmt->execute(); + + $user = [ + 'id' => (int)db()->lastInsertId(), + 'full_name' => $form['full_name'], + 'email' => $form['email'], + ]; + admin_store_session($user); + + return [ + 'success' => true, + 'message' => 'Akun admin pertama berhasil dibuat dan sesi login sudah aktif.', + 'form' => ['full_name' => '', 'email' => ''], + ]; +} + +function admin_attempt_login(array $source): array +{ + admin_ensure_auth_schema(); + + $form = admin_auth_form_defaults($source); + $password = (string)($source['password'] ?? ''); + + if (!admin_has_users()) { + return [ + 'success' => false, + 'message' => 'Belum ada akun admin. Buat akun admin pertama terlebih dahulu.', + 'form' => $form, + ]; + } + + if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL) || $password === '') { + return [ + 'success' => false, + 'message' => 'Masukkan email admin dan password yang benar.', + 'form' => $form, + ]; + } + + $stmt = db()->prepare('SELECT id, full_name, email, password_hash FROM admin_users WHERE email = :email LIMIT 1'); + $stmt->bindValue(':email', $form['email']); + $stmt->execute(); + $user = $stmt->fetch(); + + if (!$user || !password_verify($password, (string)($user['password_hash'] ?? ''))) { + return [ + 'success' => false, + 'message' => 'Email admin atau password tidak cocok.', + 'form' => $form, + ]; + } + + $update = db()->prepare('UPDATE admin_users SET last_login_at = NOW() WHERE id = :id LIMIT 1'); + $update->bindValue(':id', (int)($user['id'] ?? 0), PDO::PARAM_INT); + $update->execute(); + + if (password_needs_rehash((string)($user['password_hash'] ?? ''), PASSWORD_DEFAULT)) { + $rehash = db()->prepare('UPDATE admin_users SET password_hash = :password_hash WHERE id = :id LIMIT 1'); + $rehash->bindValue(':password_hash', password_hash($password, PASSWORD_DEFAULT)); + $rehash->bindValue(':id', (int)($user['id'] ?? 0), PDO::PARAM_INT); + $rehash->execute(); + } + + admin_store_session($user); + + return [ + 'success' => true, + 'message' => 'Login admin berhasil.', + 'form' => ['full_name' => '', 'email' => ''], + ]; +} + +function admin_status_options(): array +{ + $options = []; + foreach (store_status_steps() as $step) { + $value = (string)($step['value'] ?? ''); + $label = (string)($step['label'] ?? $value); + if ($value !== '') { + $options[$value] = $label; + } + } + $options['Batal'] = 'Batal'; + + return $options; +} + +function admin_filters_from_source(array $source): array +{ + $statuses = admin_status_options(); + $search = store_sanitize_line((string)($source['search'] ?? ''), 120); + $status = store_sanitize_line((string)($source['status'] ?? 'all'), 40); + $selected = store_sanitize_line((string)($source['selected'] ?? ''), 30); + + if ($status !== 'all' && !isset($statuses[$status])) { + $status = 'all'; + } + + return [ + 'search' => $search, + 'status' => $status, + 'selected' => $selected, + ]; +} + +function admin_url(array $filters, array $overrides = []): string +{ + $params = $filters; + foreach ($overrides as $key => $value) { + if (is_string($key) && is_string($value)) { + $params[$key] = $value; + } + } + + $query = []; + foreach ($params as $key => $value) { + if (!is_string($key) || !is_string($value) || $value === '') { + continue; + } + if ($key === 'status' && $value === 'all') { + continue; + } + $query[$key] = $value; + } + + return 'admin.php' . ($query ? '?' . http_build_query($query) : ''); +} + +function admin_order_dashboard(): array +{ + store_ensure_schema(); + + $stmt = db()->query( + "SELECT + COUNT(*) AS total_orders, + COALESCE(SUM(CASE WHEN status = 'Menunggu Pembayaran' THEN 1 ELSE 0 END), 0) AS pending_orders, + COALESCE(SUM(CASE WHEN status IN ('Diproses', 'Dikirim') THEN 1 ELSE 0 END), 0) AS active_orders, + COALESCE(SUM(CASE WHEN status = 'Selesai' THEN 1 ELSE 0 END), 0) AS completed_orders, + COALESCE(SUM(grand_total), 0) AS gross_revenue, + COALESCE(SUM(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 ELSE 0 END), 0) AS recent_orders + FROM orders" + ); + + $row = $stmt->fetch() ?: []; + + return [ + 'total_orders' => (int)($row['total_orders'] ?? 0), + 'pending_orders' => (int)($row['pending_orders'] ?? 0), + 'active_orders' => (int)($row['active_orders'] ?? 0), + 'completed_orders' => (int)($row['completed_orders'] ?? 0), + 'gross_revenue' => (float)($row['gross_revenue'] ?? 0), + 'recent_orders' => (int)($row['recent_orders'] ?? 0), + ]; +} + +function admin_list_orders(array $filters): array +{ + store_ensure_schema(); + + $conditions = []; + $params = []; + + if (($filters['search'] ?? '') !== '') { + $conditions[] = '(order_number LIKE :search OR customer_name LIKE :search OR email LIKE :search OR phone LIKE :search)'; + $params['search'] = '%' . $filters['search'] . '%'; + } + + if (($filters['status'] ?? 'all') !== 'all') { + $conditions[] = 'status = :status'; + $params['status'] = $filters['status']; + } + + $sql = 'SELECT order_number, customer_name, email, phone, status, grand_total, payment_method, items_json, created_at, updated_at FROM orders'; + if ($conditions) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + $sql .= ' ORDER BY created_at DESC LIMIT 100'; + + $stmt = db()->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + + $methods = store_payment_methods(); + $orders = $stmt->fetchAll() ?: []; + foreach ($orders as &$order) { + $items = json_decode((string)($order['items_json'] ?? '[]'), true); + $itemCount = 0; + if (is_array($items)) { + foreach ($items as $item) { + if (is_array($item)) { + $itemCount += (int)($item['quantity'] ?? 0); + } + } + } + $methodKey = (string)($order['payment_method'] ?? ''); + $order['payment_method_label'] = $methods[$methodKey]['label'] ?? $methodKey; + $order['item_count'] = $itemCount; + } + unset($order); + + return $orders; +} + +function admin_update_order_status(string $orderNumber, string $status): array +{ + $orderNumber = store_sanitize_line($orderNumber, 30); + $status = store_sanitize_line($status, 40); + $options = admin_status_options(); + + if ($orderNumber === '') { + return ['success' => false, 'message' => 'Kode pesanan tidak valid.']; + } + + if (!isset($options[$status])) { + return ['success' => false, 'message' => 'Status pesanan tidak dikenali.']; + } + + $existing = store_find_order($orderNumber); + if (!$existing) { + return ['success' => false, 'message' => 'Pesanan tidak ditemukan.']; + } + + if ((string)$existing['status'] === $status) { + return ['success' => true, 'message' => 'Status pesanan sudah berada di tahap ' . $options[$status] . '.']; + } + + $stmt = db()->prepare('UPDATE orders SET status = :status WHERE order_number = :order_number LIMIT 1'); + $stmt->bindValue(':status', $status); + $stmt->bindValue(':order_number', $orderNumber); + $stmt->execute(); + + return [ + 'success' => true, + 'message' => 'Status pesanan ' . $orderNumber . ' diubah ke ' . $options[$status] . '.', + ]; +} + +function admin_catalog_snapshot(): array +{ + $categories = store_categories(); + $snapshot = []; + + foreach ($categories as $slug => $category) { + if ($slug === 'all') { + continue; + } + + $snapshot[$slug] = [ + 'slug' => $slug, + 'label' => (string)($category['label'] ?? $slug), + 'description' => (string)($category['description'] ?? ''), + 'count' => 0, + 'min_price' => null, + 'max_price' => null, + 'products' => [], + ]; + } + + foreach (store_products() as $product) { + $categoryKey = (string)($product['category'] ?? ''); + if (!isset($snapshot[$categoryKey])) { + continue; + } + + $price = (float)($product['price'] ?? 0); + $snapshot[$categoryKey]['count']++; + $snapshot[$categoryKey]['min_price'] = $snapshot[$categoryKey]['min_price'] === null + ? $price + : min((float)$snapshot[$categoryKey]['min_price'], $price); + $snapshot[$categoryKey]['max_price'] = $snapshot[$categoryKey]['max_price'] === null + ? $price + : max((float)$snapshot[$categoryKey]['max_price'], $price); + $snapshot[$categoryKey]['products'][] = [ + 'name' => (string)($product['name'] ?? ''), + 'price' => $price, + 'lead_time' => (string)($product['lead_time'] ?? ''), + 'slug' => (string)($product['slug'] ?? ''), + ]; + } + + return array_values($snapshot); +} + +$authForm = admin_auth_form_defaults(); +$authError = ''; +$filters = admin_filters_from_source($_GET); + +if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { + $action = store_sanitize_line((string)($_POST['action'] ?? ''), 40); + + if (!admin_verify_csrf((string)($_POST['csrf_token'] ?? ''))) { + if ($action === 'setup_admin' || $action === 'login_admin') { + $authForm = admin_auth_form_defaults($_POST); + $authError = 'Sesi keamanan sudah berakhir. Muat ulang halaman admin lalu coba lagi.'; + } else { + store_flash('danger', 'Sesi keamanan admin sudah berakhir. Coba ulangi tindakannya.'); + header('Location: admin.php'); + exit; + } + } elseif ($action === 'logout_admin') { + admin_logout(); + store_flash('success', 'Sesi admin berhasil ditutup.'); + header('Location: admin.php'); + exit; + } elseif ($action === 'setup_admin') { + $result = admin_create_first_user($_POST); + if (!empty($result['success'])) { + store_flash('success', (string)($result['message'] ?? 'Akun admin berhasil dibuat.')); + header('Location: admin.php'); + exit; + } + + $authForm = is_array($result['form'] ?? null) ? $result['form'] : admin_auth_form_defaults($_POST); + $authError = (string)($result['message'] ?? 'Setup admin gagal diproses.'); + } elseif ($action === 'login_admin') { + $result = admin_attempt_login($_POST); + if (!empty($result['success'])) { + store_flash('success', (string)($result['message'] ?? 'Login admin berhasil.')); + header('Location: admin.php'); + exit; + } + + $authForm = is_array($result['form'] ?? null) ? $result['form'] : admin_auth_form_defaults($_POST); + $authError = (string)($result['message'] ?? 'Login admin gagal diproses.'); + } elseif ($action === 'update_status') { + if (!admin_is_authenticated()) { + store_flash('danger', 'Silakan login admin terlebih dahulu.'); + header('Location: admin.php'); + exit; + } + + $filters = admin_filters_from_source($_POST); + $result = admin_update_order_status( + (string)($_POST['order_number'] ?? ''), + (string)($_POST['status_value'] ?? '') + ); + + store_flash(!empty($result['success']) ? 'success' : 'danger', (string)($result['message'] ?? 'Aksi admin gagal diproses.')); + header('Location: ' . admin_url($filters, ['selected' => (string)($_POST['order_number'] ?? '')])); + exit; + } else { + store_flash('danger', 'Aksi admin tidak dikenali.'); + header('Location: admin.php'); + exit; + } +} + +$adminUser = admin_current_user(); + +if (!$adminUser) { + $hasAdminAccount = admin_has_users(); + + store_page_start( + $hasAdminAccount ? 'Login Admin' : 'Setup Admin', + $hasAdminAccount + ? 'Masuk ke dashboard admin untuk memantau dan memperbarui pesanan bakery.' + : 'Buat akun admin pertama untuk melindungi dashboard operasional bakery.', + ['noindex' => true] + ); + ?> +
+
+
+ Admin access +

+

+ +

+ + + + + +
+
+ +
+
+
+ true] +); +?> +
+
+
+ Internal admin +

Pantau order masuk tanpa panel yang berat.

+

+ Slice ini fokus ke kebutuhan operasional paling dekat: melihat antrean pesanan, membuka detail customer, + lalu mengubah status order saat pembayaran masuk, produksi dimulai, atau pengiriman selesai. +

+ +
+
+ +
+
+
+ +
+
+
+
+
+
total pesanan tersimpan
+
+
+
+
+
+
menunggu pembayaran
+
+
+
+
+
+
sedang diproses / dikirim
+
+
+
+
+
+
gross value seluruh order
+
+
+
+
+ +
+
+
+
+
Filter antrean
+

Cari order yang perlu ditindak.

+

Gunakan pencarian berdasarkan kode order, nama customer, email, atau telepon.

+
+
Pesanan 7 hari terakhir:
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+ Belum ada hasil +

Tidak ada pesanan yang cocok dengan filter ini.

+

Coba reset pencarian atau ubah status filter untuk melihat antrean yang lain.

+
+ +
+
+
Order queue
+

pesanan tampil di dashboard.

+

Klik detail untuk membuka panel ringkasan dan mengubah status order.

+
+
+ + + + + + + + + + + + + + + + + + + + + +
PesananCustomerStatusTotalAksi
+ +
item •
+
+ +
+
+
+ + + +
+
+ Detail +
+
+
+ +
+
+ + + +
+ Pilih pesanan +

Belum ada order yang dibuka.

+

Klik tombol detail pada tabel agar admin bisa melihat item, data customer, dan mengganti status pesanan.

+
+ +
+
+
+ +
+
+ Catalog snapshot +

Produk dan kategori tampil sebagai referensi operasional.

+

Di slice ini catalog belum editable dari admin. Fokusnya masih order handling agar alur transaksi sudah bisa dijalankan dulu.

+
+ +
+ +
+
+
+
+
produk aktif di kategori ini
+

+ +
+ +
+
+ +
+ +
+
+
Produk aktif
+

produk ada di katalog saat ini.

+

Langkah berikutnya bila dibutuhkan: pindahkan katalog ke tabel DB agar admin bisa menambah/edit produk dari UI.

+
+
+ + + + + + + + + + + + + + + + + + + +
ProdukKategoriLead timeHarga
+ +
+
+
+
+
+ + Sekut Bakery favicon + Rounded square bakery icon with SB monogram. + + + + + + + + + + + + + + + + + SB + diff --git a/db/migrations/20260526_create_admin_users.sql b/db/migrations/20260526_create_admin_users.sql new file mode 100644 index 0000000..eee4f79 --- /dev/null +++ b/db/migrations/20260526_create_admin_users.sql @@ -0,0 +1,12 @@ +/* Admin auth table for the Sekut Bakery dashboard. */ +CREATE TABLE IF NOT EXISTS admin_users ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + full_name VARCHAR(120) NOT NULL, + email VARCHAR(160) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + last_login_at TIMESTAMP NULL DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_admin_email (email), + INDEX idx_admin_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/order_status.php b/order_status.php index 7a4add6..db40b68 100644 --- a/order_status.php +++ b/order_status.php @@ -207,7 +207,7 @@ store_page_start('Status Pesanan', 'Lacak status pesanan menggunakan order numbe
  • 1Masuk dari checkout otomatis akan mengisi kode pesanan terbaru.
  • 2Untuk kunjungan berikutnya, pelanggan cukup ingat order number dan email.
  • -
  • 3Status berikutnya bisa diubah admin di iterasi lanjutan.
  • +
  • 3Status pesanan kini bisa diperbarui lewat admin lite internal.
diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..369c779 --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Disallow: /checkout.php +Disallow: /order_status.php +Disallow: /admin.php diff --git a/store.php b/store.php index d13b7c7..b2113bc 100644 --- a/store.php +++ b/store.php @@ -7,6 +7,11 @@ if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } +if (basename((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === basename(__FILE__)) { + header('Location: index.php'); + exit; +} + const STORE_CART_KEY = 'sekut_cart'; const STORE_FLASH_KEY = 'sekut_flash'; const STORE_LAST_ORDER_KEY = 'sekut_last_order'; @@ -723,6 +728,7 @@ function store_page_start(string $title, string $description = '', array $option $metaDescription = $description !== '' ? $description : $projectDescription; $fullTitle = trim($title) !== '' ? $title . ' • ' . $projectName : $projectName; $cssVersion = file_exists(__DIR__ . '/assets/css/custom.css') ? (string)filemtime(__DIR__ . '/assets/css/custom.css') : (string)time(); + $faviconVersion = file_exists(__DIR__ . '/assets/images/favicon.svg') ? (string)filemtime(__DIR__ . '/assets/images/favicon.svg') : $cssVersion; $currentPath = basename(parse_url($_SERVER['REQUEST_URI'] ?? '/index.php', PHP_URL_PATH) ?: 'index.php'); if ($currentPath === '' || $currentPath === '/') { $currentPath = 'index.php'; @@ -735,6 +741,7 @@ function store_page_start(string $title, string $description = '', array $option echo ''; echo ''; echo ''; + echo ''; echo '' . h($fullTitle) . ''; echo $robots; @@ -754,6 +761,7 @@ function store_page_start(string $title, string $description = '', array $option echo ''; echo ''; echo ''; + echo ''; echo ''; echo ''; echo '';