Sekut Bakery 1
This commit is contained in:
parent
d282dac2e9
commit
30e21c6f6f
974
admin.php
Normal file
974
admin.php
Normal file
@ -0,0 +1,974 @@
|
|||||||
|
<?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();
|
||||||
21
assets/images/favicon.svg
Normal file
21
assets/images/favicon.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Sekut Bakery favicon</title>
|
||||||
|
<desc id="desc">Rounded square bakery icon with SB monogram.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="16" y1="12" x2="112" y2="116" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#3B3531"/>
|
||||||
|
<stop offset="1" stop-color="#B07B4F"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="glow" x1="26" y1="28" x2="102" y2="102" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#F5E9D8" stop-opacity="0.95"/>
|
||||||
|
<stop offset="1" stop-color="#F8F3EC" stop-opacity="0.65"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="128" height="128" rx="28" fill="url(#bg)"/>
|
||||||
|
<circle cx="64" cy="64" r="39" fill="url(#glow)" opacity="0.92"/>
|
||||||
|
<path d="M35 74c5-14 18-23 29-23 14 0 27 9 31 23" fill="none" stroke="#8B5E3C" stroke-linecap="round" stroke-width="6"/>
|
||||||
|
<circle cx="49" cy="52" r="4" fill="#8B5E3C" opacity="0.75"/>
|
||||||
|
<circle cx="65" cy="46" r="4" fill="#8B5E3C" opacity="0.75"/>
|
||||||
|
<circle cx="80" cy="53" r="4" fill="#8B5E3C" opacity="0.75"/>
|
||||||
|
<text x="64" y="86" text-anchor="middle" font-size="28" font-family="Inter, Arial, sans-serif" font-weight="800" fill="#2B2927">SB</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
12
db/migrations/20260526_create_admin_users.sql
Normal file
12
db/migrations/20260526_create_admin_users.sql
Normal file
@ -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;
|
||||||
@ -207,7 +207,7 @@ store_page_start('Status Pesanan', 'Lacak status pesanan menggunakan order numbe
|
|||||||
<ul class="list-clean compact-list mt-4 mb-0">
|
<ul class="list-clean compact-list mt-4 mb-0">
|
||||||
<li><span class="list-index">1</span><span>Masuk dari checkout otomatis akan mengisi kode pesanan terbaru.</span></li>
|
<li><span class="list-index">1</span><span>Masuk dari checkout otomatis akan mengisi kode pesanan terbaru.</span></li>
|
||||||
<li><span class="list-index">2</span><span>Untuk kunjungan berikutnya, pelanggan cukup ingat order number dan email.</span></li>
|
<li><span class="list-index">2</span><span>Untuk kunjungan berikutnya, pelanggan cukup ingat order number dan email.</span></li>
|
||||||
<li><span class="list-index">3</span><span>Status berikutnya bisa diubah admin di iterasi lanjutan.</span></li>
|
<li><span class="list-index">3</span><span>Status pesanan kini bisa diperbarui lewat admin lite internal.</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
5
robots.txt
Normal file
5
robots.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Disallow: /checkout.php
|
||||||
|
Disallow: /order_status.php
|
||||||
|
Disallow: /admin.php
|
||||||
@ -7,6 +7,11 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
|
|||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (basename((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === basename(__FILE__)) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
const STORE_CART_KEY = 'sekut_cart';
|
const STORE_CART_KEY = 'sekut_cart';
|
||||||
const STORE_FLASH_KEY = 'sekut_flash';
|
const STORE_FLASH_KEY = 'sekut_flash';
|
||||||
const STORE_LAST_ORDER_KEY = 'sekut_last_order';
|
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;
|
$metaDescription = $description !== '' ? $description : $projectDescription;
|
||||||
$fullTitle = trim($title) !== '' ? $title . ' • ' . $projectName : $projectName;
|
$fullTitle = trim($title) !== '' ? $title . ' • ' . $projectName : $projectName;
|
||||||
$cssVersion = file_exists(__DIR__ . '/assets/css/custom.css') ? (string)filemtime(__DIR__ . '/assets/css/custom.css') : (string)time();
|
$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');
|
$currentPath = basename(parse_url($_SERVER['REQUEST_URI'] ?? '/index.php', PHP_URL_PATH) ?: 'index.php');
|
||||||
if ($currentPath === '' || $currentPath === '/') {
|
if ($currentPath === '' || $currentPath === '/') {
|
||||||
$currentPath = 'index.php';
|
$currentPath = 'index.php';
|
||||||
@ -735,6 +741,7 @@ function store_page_start(string $title, string $description = '', array $option
|
|||||||
echo '<head>';
|
echo '<head>';
|
||||||
echo '<meta charset="utf-8" />';
|
echo '<meta charset="utf-8" />';
|
||||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1" />';
|
echo '<meta name="viewport" content="width=device-width, initial-scale=1" />';
|
||||||
|
echo '<meta name="theme-color" content="#2B2927" />';
|
||||||
echo '<title>' . h($fullTitle) . '</title>';
|
echo '<title>' . h($fullTitle) . '</title>';
|
||||||
echo $robots;
|
echo $robots;
|
||||||
|
|
||||||
@ -754,6 +761,7 @@ function store_page_start(string $title, string $description = '', array $option
|
|||||||
echo '<link rel="preconnect" href="https://fonts.googleapis.com">';
|
echo '<link rel="preconnect" href="https://fonts.googleapis.com">';
|
||||||
echo '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
|
echo '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
|
||||||
echo '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">';
|
echo '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">';
|
||||||
|
echo '<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg?v=' . h($faviconVersion) . '">';
|
||||||
echo '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">';
|
echo '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">';
|
||||||
echo '<link rel="stylesheet" href="assets/css/custom.css?v=' . h($cssVersion) . '">';
|
echo '<link rel="stylesheet" href="assets/css/custom.css?v=' . h($cssVersion) . '">';
|
||||||
echo '</head>';
|
echo '</head>';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user