40087-vm/admin.php
2026-05-26 07:35:51 +00:00

975 lines
43 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
require_once __DIR__ . '/store.php';
const STORE_ADMIN_SESSION_KEY = 'sekut_admin_user';
const STORE_ADMIN_CSRF_KEY = 'sekut_admin_csrf';
function admin_ensure_auth_schema(): void
{
static $ready = false;
if ($ready) {
return;
}
$migrationPath = __DIR__ . '/db/migrations/20260526_create_admin_users.sql';
if (is_file($migrationPath)) {
$sql = file_get_contents($migrationPath);
if (is_string($sql) && trim($sql) !== '') {
db()->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]
);
?>
<section class="hero-panel mb-4 mb-lg-5">
<div class="row g-4 align-items-stretch">
<div class="col-lg-6">
<span class="eyebrow">Admin access</span>
<h1 class="display-title"><?= $hasAdminAccount ? 'Masuk ke dashboard internal yang sudah dilindungi.' : 'Aktifkan login admin sebelum panel dipakai rutin.' ?></h1>
<p class="lead-copy mb-0">
<?= $hasAdminAccount
? 'Dashboard order sekarang butuh sesi login, jadi update status pesanan tidak lagi terbuka untuk publik.'
: 'Langkah setup ini hanya muncul sekali untuk membuat akun admin pertama. Setelah tersimpan, halaman otomatis berubah jadi form login.' ?>
</p>
<?php if ($authError !== ''): ?>
<div class="alert alert-danger mt-4 mb-0 border-0 shadow-sm" role="alert"><?= h($authError) ?></div>
<?php else: ?>
<div class="alert alert-info mt-4 mb-0 border-0 shadow-sm" role="alert">
<strong><?= $hasAdminAccount ? 'Akses aman:' : 'Setup sekali:' ?></strong>
<?= $hasAdminAccount
? 'gunakan email admin dan password yang sudah dibuat untuk membuka dashboard.'
: 'buat email admin internal dan password minimal 8 karakter agar halaman admin tidak lagi terbuka bebas.' ?>
</div>
<?php endif; ?>
</div>
<div class="col-lg-6">
<aside class="summary-card h-100">
<div class="card-kicker"><?= $hasAdminAccount ? 'Login required' : 'Setup pertama' ?></div>
<h2 class="summary-title"><?= $hasAdminAccount ? 'Masukkan email dan password admin.' : 'Buat akun admin internal untuk toko ini.' ?></h2>
<form action="admin.php" method="post" class="mt-4" data-auto-disable>
<input type="hidden" name="action" value="<?= $hasAdminAccount ? 'login_admin' : 'setup_admin' ?>">
<input type="hidden" name="csrf_token" value="<?= h(admin_csrf_token()) ?>">
<?php if (!$hasAdminAccount): ?>
<div class="mb-3">
<label class="form-label" for="full_name">Nama admin</label>
<input id="full_name" class="form-control" type="text" name="full_name" value="<?= h($authForm['full_name']) ?>" maxlength="120" autocomplete="name" placeholder="Contoh: Sekut Ops" required>
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label" for="email">Email admin</label>
<input id="email" class="form-control" type="email" name="email" value="<?= h($authForm['email']) ?>" maxlength="160" autocomplete="username" placeholder="admin@sekutbakery.test" required>
</div>
<div class="mb-3">
<label class="form-label" for="password"><?= $hasAdminAccount ? 'Password' : 'Password admin' ?></label>
<input id="password" class="form-control" type="password" name="password" minlength="8" autocomplete="<?= $hasAdminAccount ? 'current-password' : 'new-password' ?>" placeholder="Minimal 8 karakter" required>
</div>
<?php if (!$hasAdminAccount): ?>
<div class="mb-4">
<label class="form-label" for="confirm_password">Konfirmasi password</label>
<input id="confirm_password" class="form-control" type="password" name="confirm_password" minlength="8" autocomplete="new-password" placeholder="Ulangi password admin" required>
</div>
<?php endif; ?>
<div class="d-grid gap-2">
<button class="btn btn-dark" type="submit"><?= $hasAdminAccount ? 'Masuk ke admin' : 'Buat akun admin' ?></button>
<a class="btn btn-outline-secondary" href="index.php">Kembali ke toko</a>
</div>
</form>
<ul class="list-clean compact-list mt-4 mb-0">
<?php if ($hasAdminAccount): ?>
<li><span class="list-index">1</span><span>Gunakan email admin yang dibuat saat setup pertama.</span></li>
<li><span class="list-index">2</span><span>Password admin disimpan sebagai hash aman di MySQL.</span></li>
<li><span class="list-index">3</span><span>Setelah login, kamu bisa lanjut mengubah status order dari dashboard.</span></li>
<?php else: ?>
<li><span class="list-index">1</span><span>Buat satu akun admin internal dengan email yang aktif.</span></li>
<li><span class="list-index">2</span><span>Gunakan password minimal 8 karakter agar panel tidak lagi terbuka publik.</span></li>
<li><span class="list-index">3</span><span>Sesudah akun pertama tersimpan, halaman ini otomatis berubah jadi form login.</span></li>
<?php endif; ?>
</ul>
</aside>
</div>
</div>
</section>
<?php
store_page_end();
exit;
}
$dashboard = admin_order_dashboard();
$orders = admin_list_orders($filters);
$selectedOrderNumber = $filters['selected'];
if ($selectedOrderNumber === '' && !empty($orders[0]['order_number'])) {
$selectedOrderNumber = (string)$orders[0]['order_number'];
}
$selectedOrder = $selectedOrderNumber !== '' ? store_find_order($selectedOrderNumber) : null;
$catalogSnapshot = admin_catalog_snapshot();
$statusOptions = admin_status_options();
store_page_start(
'Admin Lite',
'Dashboard internal untuk memantau pesanan bakery, melihat ringkasan katalog, dan mengubah status order.',
['noindex' => true]
);
?>
<section class="hero-panel mb-4 mb-lg-5">
<div class="row g-4 align-items-start">
<div class="col-lg-7">
<span class="eyebrow">Internal admin</span>
<h1 class="display-title">Pantau order masuk tanpa panel yang berat.</h1>
<p class="lead-copy mb-0">
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.
</p>
<div class="alert alert-success mt-4 mb-0 border-0 shadow-sm" role="alert">
<strong>Akses aman aktif:</strong> dashboard ini sekarang memerlukan login admin.
Sesi aktif untuk <strong><?= h($adminUser['full_name']) ?></strong> (<?= h($adminUser['email']) ?>).
</div>
</div>
<div class="col-lg-5">
<aside class="summary-card h-100">
<div class="card-kicker">Ruang lingkup iterasi ini</div>
<h2 class="summary-title">Order management dulu, catalog CRUD nanti.</h2>
<ul class="list-clean compact-list mb-4">
<li><span class="list-index">1</span><span>Dashboard sekarang dilindungi login berbasis session.</span></li>
<li><span class="list-index">2</span><span>Status order bisa diubah dari dashboard admin.</span></li>
<li><span class="list-index">3</span><span>Produk dan kategori tampil sebagai snapshot read-only.</span></li>
</ul>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-dark" href="index.php">Kembali ke toko</a>
<?php if ($selectedOrder): ?>
<a class="btn btn-outline-secondary" href="<?= h('order_status.php?order=' . urlencode((string)$selectedOrder['order_number']) . '&email=' . urlencode((string)$selectedOrder['email'])) ?>">Lihat halaman customer</a>
<?php endif; ?>
<form action="admin.php" method="post" class="d-inline">
<input type="hidden" name="action" value="logout_admin">
<input type="hidden" name="csrf_token" value="<?= h(admin_csrf_token()) ?>">
<button class="btn btn-outline-danger" type="submit">Logout</button>
</form>
</div>
<div class="small text-muted mt-3">Login sebagai <?= h($adminUser['full_name']) ?> • <?= h($adminUser['email']) ?></div>
</aside>
</div>
</div>
</section>
<section class="mb-4 mb-lg-5">
<div class="row g-3">
<div class="col-sm-6 col-xl-3">
<div class="metric-card h-100">
<div class="metric-value"><?= h((string)$dashboard['total_orders']) ?></div>
<div class="metric-label">total pesanan tersimpan</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="metric-card h-100">
<div class="metric-value"><?= h((string)$dashboard['pending_orders']) ?></div>
<div class="metric-label">menunggu pembayaran</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="metric-card h-100">
<div class="metric-value"><?= h((string)$dashboard['active_orders']) ?></div>
<div class="metric-label">sedang diproses / dikirim</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="metric-card h-100">
<div class="metric-value metric-value--small"><?= h(store_money((float)$dashboard['gross_revenue'])) ?></div>
<div class="metric-label">gross value seluruh order</div>
</div>
</div>
</div>
</section>
<section class="section-block pt-0">
<div class="surface-panel p-4 mb-4">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-end">
<div>
<div class="card-kicker">Filter antrean</div>
<h2 class="summary-title mb-1">Cari order yang perlu ditindak.</h2>
<p class="section-copy mb-0">Gunakan pencarian berdasarkan kode order, nama customer, email, atau telepon.</p>
</div>
<div class="text-muted small">Pesanan 7 hari terakhir: <strong class="text-dark"><?= h((string)$dashboard['recent_orders']) ?></strong></div>
</div>
<form action="admin.php" method="get" class="row g-3 mt-1">
<div class="col-lg-7">
<label class="form-label" for="search">Cari pesanan</label>
<input id="search" class="form-control" type="search" name="search" value="<?= h($filters['search']) ?>" placeholder="Contoh: SB260526 atau nama customer">
</div>
<div class="col-lg-3">
<label class="form-label" for="status">Filter status</label>
<select id="status" class="form-control" name="status">
<option value="all">Semua status</option>
<?php foreach ($statusOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= $filters['status'] === $value ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-lg-2 d-flex align-items-end gap-2">
<button class="btn btn-dark w-100" type="submit">Terapkan</button>
</div>
</form>
</div>
<div class="row g-4 align-items-start">
<div class="col-xl-7">
<?php if (!$orders): ?>
<div class="empty-state-card">
<span class="eyebrow">Belum ada hasil</span>
<h2 class="section-title">Tidak ada pesanan yang cocok dengan filter ini.</h2>
<p class="section-copy mb-0">Coba reset pencarian atau ubah status filter untuk melihat antrean yang lain.</p>
</div>
<?php else: ?>
<div class="surface-panel p-0 overflow-hidden">
<div class="p-4 border-bottom">
<div class="card-kicker">Order queue</div>
<h2 class="summary-title mb-1"><?= h((string)count($orders)) ?> pesanan tampil di dashboard.</h2>
<p class="section-copy mb-0">Klik detail untuk membuka panel ringkasan dan mengubah status order.</p>
</div>
<div class="table-responsive">
<table class="table order-table align-middle mb-0">
<thead>
<tr>
<th scope="col">Pesanan</th>
<th scope="col">Customer</th>
<th scope="col">Status</th>
<th scope="col" class="text-end">Total</th>
<th scope="col" class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($orders as $order): ?>
<tr class="<?= $selectedOrderNumber === (string)$order['order_number'] ? 'table-active' : '' ?>">
<td>
<strong><?= h((string)$order['order_number']) ?></strong>
<div class="small text-muted"><?= h((string)$order['item_count']) ?> item • <?= h((string)$order['payment_method_label']) ?></div>
</td>
<td>
<strong><?= h((string)$order['customer_name']) ?></strong>
<div class="small text-muted"><?= h((string)$order['email']) ?></div>
<div class="small text-muted"><?= h((string)$order['phone']) ?></div>
</td>
<td>
<span class="<?= h(store_status_class((string)$order['status'])) ?>"><?= h((string)$order['status']) ?></span>
</td>
<td class="text-end">
<strong><?= h(store_money((float)$order['grand_total'])) ?></strong>
<div class="small text-muted"><?= h(store_format_datetime((string)$order['created_at'])) ?></div>
</td>
<td class="text-end">
<a class="btn btn-outline-secondary btn-sm" href="<?= h(admin_url($filters, ['selected' => (string)$order['order_number']])) ?>">Detail</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<div class="col-xl-5">
<?php if ($selectedOrder): ?>
<aside class="summary-card">
<div class="d-flex flex-wrap justify-content-between gap-2 align-items-start">
<div>
<div class="card-kicker">Pesanan dipilih</div>
<h2 class="summary-title mb-1"><?= h((string)$selectedOrder['order_number']) ?></h2>
<p class="text-muted mb-0">Dibuat pada <?= h(store_format_datetime((string)$selectedOrder['created_at'])) ?></p>
</div>
<span class="<?= h(store_status_class((string)$selectedOrder['status'])) ?>"><?= h((string)$selectedOrder['status']) ?></span>
</div>
<form action="admin.php" method="post" class="mt-4" data-auto-disable>
<input type="hidden" name="action" value="update_status">
<input type="hidden" name="csrf_token" value="<?= h(admin_csrf_token()) ?>">
<input type="hidden" name="order_number" value="<?= h((string)$selectedOrder['order_number']) ?>">
<input type="hidden" name="search" value="<?= h($filters['search']) ?>">
<input type="hidden" name="status" value="<?= h($filters['status']) ?>">
<input type="hidden" name="selected" value="<?= h((string)$selectedOrder['order_number']) ?>">
<label class="form-label" for="status_value">Ubah status pesanan</label>
<div class="d-grid gap-2">
<select id="status_value" class="form-control" name="status_value">
<?php foreach ($statusOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= (string)$selectedOrder['status'] === $value ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
<button class="btn btn-dark" type="submit">Simpan status</button>
</div>
</form>
<div class="info-box mt-4">
<div class="card-kicker">Data customer</div>
<p class="mb-1"><strong><?= h((string)$selectedOrder['customer_name']) ?></strong></p>
<p class="mb-1"><?= h((string)$selectedOrder['email']) ?></p>
<p class="mb-1"><?= h((string)$selectedOrder['phone']) ?></p>
<p class="mb-0"><?= nl2br(h((string)$selectedOrder['address'])) ?></p>
</div>
<div class="info-box mt-3">
<div class="card-kicker">Pembayaran & catatan</div>
<p class="mb-1"><strong><?= h((string)$selectedOrder['payment_method_label']) ?></strong></p>
<p class="mb-0"><?= h((string)$selectedOrder['payment_instruction']) ?></p>
<?php if (!empty($selectedOrder['note'])): ?>
<div class="note-copy mt-3"><strong>Catatan customer:</strong> <?= h((string)$selectedOrder['note']) ?></div>
<?php endif; ?>
</div>
<div class="surface-panel p-4 mt-3">
<div class="card-kicker">Ringkasan item</div>
<div class="receipt-card receipt-card--flat">
<?php foreach ($selectedOrder['items'] as $item): ?>
<div class="receipt-line">
<span><?= h((string)$item['name']) ?> × <?= h((string)$item['quantity']) ?></span>
<strong><?= h(store_money((float)$item['line_total'])) ?></strong>
</div>
<?php endforeach; ?>
<div class="receipt-line">
<span>Subtotal</span>
<strong><?= h(store_money((float)$selectedOrder['subtotal'])) ?></strong>
</div>
<div class="receipt-line">
<span>Ongkir</span>
<strong><?= h(store_money((float)$selectedOrder['shipping_fee'])) ?></strong>
</div>
<div class="receipt-line receipt-line--total">
<span>Grand total</span>
<strong><?= h(store_money((float)$selectedOrder['grand_total'])) ?></strong>
</div>
</div>
</div>
<?php if ((string)$selectedOrder['status'] !== 'Batal'): ?>
<?php $currentIndex = store_status_index((string)$selectedOrder['status']); ?>
<div class="surface-panel p-4 mt-3">
<div class="card-kicker">Progress customer-facing</div>
<div class="timeline-list mt-3">
<?php foreach (store_status_steps() as $index => $step): ?>
<?php
$stateClass = 'timeline-step';
if ($index < $currentIndex) {
$stateClass .= ' is-complete';
} elseif ($index === $currentIndex) {
$stateClass .= ' is-current';
}
?>
<div class="<?= h($stateClass) ?>">
<div class="timeline-step__dot"></div>
<div>
<div class="timeline-step__title"><?= h((string)$step['label']) ?></div>
<p class="timeline-step__copy mb-0"><?= h((string)$step['description']) ?></p>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<div class="info-box mt-3 mb-0">
<div class="card-kicker">Status batal</div>
<p class="mb-0">Pesanan ini sudah dibatalkan, jadi timeline customer tidak lagi berjalan.</p>
</div>
<?php endif; ?>
</aside>
<?php else: ?>
<div class="empty-state-card">
<span class="eyebrow">Pilih pesanan</span>
<h2 class="section-title">Belum ada order yang dibuka.</h2>
<p class="section-copy mb-0">Klik tombol detail pada tabel agar admin bisa melihat item, data customer, dan mengganti status pesanan.</p>
</div>
<?php endif; ?>
</div>
</div>
</section>
<section class="section-block pt-0">
<div class="section-heading mb-4">
<span class="eyebrow">Catalog snapshot</span>
<h2 class="section-title">Produk dan kategori tampil sebagai referensi operasional.</h2>
<p class="section-copy mb-0">Di slice ini catalog belum editable dari admin. Fokusnya masih order handling agar alur transaksi sudah bisa dijalankan dulu.</p>
</div>
<div class="row g-3 mb-4">
<?php foreach ($catalogSnapshot as $category): ?>
<div class="col-md-6 col-xl-4">
<div class="feature-card h-100">
<div class="card-kicker"><?= h((string)$category['label']) ?></div>
<div class="metric-value"><?= h((string)$category['count']) ?></div>
<div class="metric-label">produk aktif di kategori ini</div>
<p class="feature-card__copy mt-3 mb-2"><?= h((string)$category['description']) ?></p>
<?php if ($category['min_price'] !== null && $category['max_price'] !== null): ?>
<div class="price-tag"><?= h(store_money((float)$category['min_price'])) ?> — <?= h(store_money((float)$category['max_price'])) ?></div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="surface-panel p-0 overflow-hidden">
<div class="p-4 border-bottom">
<div class="card-kicker">Produk aktif</div>
<h2 class="summary-title mb-1"><?= h((string)count(store_products())) ?> produk ada di katalog saat ini.</h2>
<p class="section-copy mb-0">Langkah berikutnya bila dibutuhkan: pindahkan katalog ke tabel DB agar admin bisa menambah/edit produk dari UI.</p>
</div>
<div class="table-responsive">
<table class="table order-table align-middle mb-0">
<thead>
<tr>
<th scope="col">Produk</th>
<th scope="col">Kategori</th>
<th scope="col">Lead time</th>
<th scope="col" class="text-end">Harga</th>
</tr>
</thead>
<tbody>
<?php foreach (store_products() as $product): ?>
<tr>
<td>
<strong><?= h((string)$product['name']) ?></strong>
<div class="small text-muted"><?= h((string)$product['slug']) ?></div>
</td>
<td><?= h((string)$product['category_label']) ?></td>
<td><?= h((string)$product['lead_time']) ?></td>
<td class="text-end"><strong><?= h(store_money((float)$product['price'])) ?></strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</section>
<?php store_page_end();