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.

Pesanan Customer Status Total Aksi
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.

Produk Kategori Lead time Harga