Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a440f4357f | ||
|
|
cb3a15004c | ||
|
|
192f07588e | ||
|
|
30e21c6f6f | ||
|
|
d282dac2e9 |
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();
|
||||
File diff suppressed because it is too large
Load Diff
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 |
@ -1,39 +1,63 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
if (window.bootstrap && window.bootstrap.Toast) {
|
||||
document.querySelectorAll('.toast').forEach((element) => {
|
||||
const toast = new window.bootstrap.Toast(element);
|
||||
toast.show();
|
||||
});
|
||||
}
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
document.querySelectorAll('[data-qty-target][data-qty-step]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const targetId = button.getAttribute('data-qty-target');
|
||||
const input = targetId ? document.getElementById(targetId) : null;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
const min = parseInt(input.getAttribute('min') || '1', 10);
|
||||
const max = parseInt(input.getAttribute('max') || '20', 10);
|
||||
const step = parseInt(button.getAttribute('data-qty-step') || '0', 10);
|
||||
const current = parseInt(input.value || String(min), 10) || min;
|
||||
const next = Math.max(min, Math.min(max, current + step));
|
||||
input.value = String(next);
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
});
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
document.querySelectorAll('[data-copy-text]').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const value = button.getAttribute('data-copy-text');
|
||||
if (!value || !navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
const originalText = button.textContent;
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
button.textContent = 'Tersalin';
|
||||
button.classList.remove('btn-outline-success');
|
||||
button.classList.add('btn-success');
|
||||
window.setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-outline-success');
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error('Clipboard error', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-auto-disable]').forEach((form) => {
|
||||
form.addEventListener('submit', () => {
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
if (!submitButton || submitButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitButton.dataset.originalText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = 'Memproses…';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
BIN
assets/pasted-20260526-073837-56ed4d81.png
Normal file
BIN
assets/pasted-20260526-073837-56ed4d81.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/pasted-20260526-074053-dd1b2e31.png
Normal file
BIN
assets/pasted-20260526-074053-dd1b2e31.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/pasted-20260526-080054-2f870e73.png
Normal file
BIN
assets/pasted-20260526-080054-2f870e73.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/pasted-20260526-080630-6e52341a.png
Normal file
BIN
assets/pasted-20260526-080630-6e52341a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
260
auth.php
Normal file
260
auth.php
Normal file
@ -0,0 +1,260 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
$mode = (string)($_GET['mode'] ?? 'login');
|
||||
if ($mode !== 'register') {
|
||||
$mode = 'login';
|
||||
}
|
||||
|
||||
$redirectTo = store_safe_redirect((string)($_REQUEST['redirect_to'] ?? 'auth.php'), 'auth.php');
|
||||
$loginForm = ['email' => ''];
|
||||
$registerForm = ['full_name' => '', 'email' => ''];
|
||||
$loginErrors = [];
|
||||
$registerErrors = [];
|
||||
$loginMessage = '';
|
||||
$registerMessage = '';
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
|
||||
if ($action === 'login') {
|
||||
$result = store_login_user($_POST);
|
||||
if (!empty($result['success'])) {
|
||||
store_flash('success', (string)($result['message'] ?? 'Login berhasil.'));
|
||||
$destination = store_resume_pending_cart_add($redirectTo);
|
||||
header('Location: ' . $destination);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mode = 'login';
|
||||
$loginForm = $result['form'] ?? $loginForm;
|
||||
$loginErrors = $result['errors'] ?? [];
|
||||
$loginMessage = (string)($result['message'] ?? 'Login belum berhasil diproses.');
|
||||
}
|
||||
|
||||
if ($action === 'register') {
|
||||
$result = store_register_user($_POST);
|
||||
if (!empty($result['success'])) {
|
||||
store_flash('success', (string)($result['message'] ?? 'Akun berhasil dibuat.'));
|
||||
$destination = store_resume_pending_cart_add($redirectTo);
|
||||
header('Location: ' . $destination);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mode = 'register';
|
||||
$registerForm = $result['form'] ?? $registerForm;
|
||||
$registerErrors = $result['errors'] ?? [];
|
||||
$registerMessage = (string)($result['message'] ?? 'Pendaftaran belum berhasil diproses.');
|
||||
}
|
||||
}
|
||||
|
||||
$currentUser = store_current_user();
|
||||
$summary = store_cart_summary();
|
||||
$categories = store_categories();
|
||||
$pendingCartAdd = store_pending_cart_add();
|
||||
$accountTitle = $currentUser ? 'Akun Saya' : 'Login / Register';
|
||||
$accountDescription = $currentUser
|
||||
? 'Ringkasan akun user untuk melanjutkan belanja, checkout, dan melacak pesanan.'
|
||||
: ($pendingCartAdd
|
||||
? 'Login atau register untuk melanjutkan produk yang dipilih ke keranjang belanja.'
|
||||
: 'Halaman login dan registrasi user untuk masuk ke sistem menggunakan email dan password.');
|
||||
|
||||
store_page_start($accountTitle, $accountDescription, ['noindex' => true]);
|
||||
?>
|
||||
<section class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Login User</span>
|
||||
<h1 class="section-title">
|
||||
<?= $currentUser
|
||||
? 'Akun user aktif dan siap dipakai.'
|
||||
: ($pendingCartAdd
|
||||
? 'Silakan login untuk melanjutkan produk yang sudah dipilih.'
|
||||
: 'Tampilan login digunakan oleh pengguna yang telah terdaftar untuk masuk ke dalam sistem.') ?>
|
||||
</h1>
|
||||
<p class="section-copy mb-0">
|
||||
<?= $currentUser
|
||||
? 'Anda sudah login. Gunakan halaman ini untuk kembali ke katalog, membuka keranjang, atau melacak status pesanan.'
|
||||
: ($pendingCartAdd
|
||||
? 'Produk yang tadi dipilih akan otomatis dimasukkan ke keranjang setelah login atau registrasi berhasil.'
|
||||
: 'Masukkan email dan password pada form login, atau buat akun baru melalui form registrasi agar data pengguna tersimpan lebih rapi.') ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<aside class="auth-sidebar">
|
||||
<div class="auth-side-card">
|
||||
<div class="card-kicker">Kategori Menu</div>
|
||||
<ul class="sidebar-link-list mb-0">
|
||||
<?php foreach ($categories as $key => $category): ?>
|
||||
<?php if ($key === 'all') continue; ?>
|
||||
<li>
|
||||
<a href="index.php?category=<?= h($key) ?>#catalog">
|
||||
<strong><?= h($category['label']) ?></strong>
|
||||
<span><?= h($category['description']) ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="auth-side-card">
|
||||
<div class="card-kicker">Keranjang Belanja</div>
|
||||
<div class="sidebar-metric"><?= h((string)store_cart_count()) ?></div>
|
||||
<p class="mb-3 text-muted">Item yang sudah dipilih tetap tersimpan dan bisa dilanjutkan ke checkout kapan saja.</p>
|
||||
<a class="btn btn-outline-secondary w-100" href="cart.php">Buka keranjang</a>
|
||||
</div>
|
||||
|
||||
<div class="auth-side-card">
|
||||
<div class="card-kicker">Info Pembayaran</div>
|
||||
<ul class="list-clean compact-list mb-0">
|
||||
<li><span class="list-index">1</span><span>Pilih metode pembayaran saat checkout.</span></li>
|
||||
<li><span class="list-index">2</span><span>Simpan order number untuk pelacakan status.</span></li>
|
||||
<li><span class="list-index">3</span><span>Siapkan bukti pembayaran jika metode bayar memerlukannya.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<?php if ($currentUser): ?>
|
||||
<section class="surface-panel auth-shell">
|
||||
<div class="auth-state-chip">Login aktif</div>
|
||||
<h2 class="summary-title mb-2">Halo, <?= h(store_user_first_name((string)$currentUser['full_name'])) ?>.</h2>
|
||||
<p class="section-copy mb-4">Akun Anda sudah tersimpan di sesi browser ini dan siap digunakan untuk melanjutkan aktivitas belanja.</p>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="account-stat">
|
||||
<span class="account-stat__label">Nama pengguna</span>
|
||||
<strong><?= h((string)$currentUser['full_name']) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="account-stat">
|
||||
<span class="account-stat__label">Email login</span>
|
||||
<strong><?= h((string)$currentUser['email']) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="account-stat">
|
||||
<span class="account-stat__label">Item di keranjang</span>
|
||||
<strong><?= h((string)store_cart_count()) ?> produk</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="account-stat">
|
||||
<span class="account-stat__label">Total sementara</span>
|
||||
<strong><?= h(store_money((float)$summary['grand_total'])) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark" href="index.php#catalog">Lanjut belanja</a>
|
||||
<a class="btn btn-outline-secondary" href="cart.php">Buka keranjang</a>
|
||||
<a class="btn btn-outline-secondary" href="order_status.php">Lacak pesanan</a>
|
||||
<a class="btn btn-outline-secondary" href="logout.php">Logout</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="surface-panel auth-shell">
|
||||
<?php if ($pendingCartAdd): ?>
|
||||
<div class="alert alert-light border shadow-sm mb-4" role="alert">
|
||||
<strong><?= h($pendingCartAdd['product']['name']) ?></strong> sebanyak <?= h((string)$pendingCartAdd['quantity']) ?> item siap masuk ke keranjang.
|
||||
Login atau buat akun terlebih dahulu, lalu sistem akan otomatis melanjutkan proses ini.
|
||||
<div class="small text-muted mt-2">
|
||||
Anda juga bisa kembali ke <a href="product.php?slug=<?= h((string)$pendingCartAdd['product']['slug']) ?>">halaman detail produk</a>
|
||||
untuk meninjau harga dan informasinya lagi.
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="auth-toggle mb-4">
|
||||
<a class="toggle-pill<?= $mode === 'login' ? ' is-active' : '' ?>" href="auth.php?mode=login<?= $redirectTo !== 'auth.php' ? '&redirect_to=' . urlencode($redirectTo) : '' ?>">Login</a>
|
||||
<a class="toggle-pill<?= $mode === 'register' ? ' is-active' : '' ?>" href="auth.php?mode=register<?= $redirectTo !== 'auth.php' ? '&redirect_to=' . urlencode($redirectTo) : '' ?>">Register</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-6">
|
||||
<article class="auth-panel<?= $mode === 'login' ? ' auth-panel--active' : '' ?>">
|
||||
<div class="auth-panel__head">
|
||||
<div>
|
||||
<div class="card-kicker">Login User</div>
|
||||
<h2 class="h4 mb-1">Masuk dengan email dan password</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted mb-4"><?= $pendingCartAdd ? 'Masuk untuk melanjutkan produk yang tadi Anda pilih ke keranjang belanja.' : 'Gunakan akun yang sudah terdaftar untuk mengakses alur pemesanan dengan lebih cepat.' ?></p>
|
||||
|
||||
<?php if ($loginMessage !== ''): ?>
|
||||
<div class="alert alert-warning border-0 shadow-sm" role="alert"><?= h($loginMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="auth.php?mode=login" method="post" class="d-grid gap-3" data-auto-disable>
|
||||
<input type="hidden" name="action" value="login">
|
||||
<input type="hidden" name="redirect_to" value="<?= h($redirectTo) ?>">
|
||||
<div>
|
||||
<label class="form-label" for="login_email">Email</label>
|
||||
<input id="login_email" name="email" type="email" class="form-control<?= store_input_class($loginErrors, 'email') ?>" value="<?= h((string)($loginForm['email'] ?? '')) ?>" autocomplete="username" placeholder="nama@email.com" required>
|
||||
<?php if (!empty($loginErrors['email'])): ?><div class="invalid-feedback"><?= h((string)$loginErrors['email']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="login_password">Password</label>
|
||||
<input id="login_password" name="password" type="password" class="form-control<?= store_input_class($loginErrors, 'password') ?>" autocomplete="current-password" placeholder="Minimal 8 karakter" required>
|
||||
<?php if (!empty($loginErrors['password'])): ?><div class="invalid-feedback"><?= h((string)$loginErrors['password']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<button class="btn btn-dark" type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-note mb-0">Belum punya akun? <a href="auth.php?mode=register<?= $redirectTo !== 'auth.php' ? '&redirect_to=' . urlencode($redirectTo) : '' ?>">Buat akun sekarang</a>.</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<article class="auth-panel<?= $mode === 'register' ? ' auth-panel--active' : '' ?>">
|
||||
<div class="auth-panel__head">
|
||||
<div>
|
||||
<div class="card-kicker">Register</div>
|
||||
<h2 class="h4 mb-1">Daftarkan akun pengguna</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted mb-4"><?= $pendingCartAdd ? 'Setelah registrasi berhasil, sistem akan langsung mengaktifkan akun lalu melanjutkan produk yang sudah dipilih ke keranjang.' : 'Setelah registrasi berhasil, akun akan langsung aktif pada sesi browser ini.' ?></p>
|
||||
|
||||
<?php if ($registerMessage !== ''): ?>
|
||||
<div class="alert alert-warning border-0 shadow-sm" role="alert"><?= h($registerMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="auth.php?mode=register" method="post" class="d-grid gap-3" data-auto-disable>
|
||||
<input type="hidden" name="action" value="register">
|
||||
<input type="hidden" name="redirect_to" value="<?= h($redirectTo) ?>">
|
||||
<div>
|
||||
<label class="form-label" for="register_full_name">Nama lengkap</label>
|
||||
<input id="register_full_name" name="full_name" type="text" class="form-control<?= store_input_class($registerErrors, 'full_name') ?>" value="<?= h((string)($registerForm['full_name'] ?? '')) ?>" maxlength="120" autocomplete="name" placeholder="Nama lengkap pengguna" required>
|
||||
<?php if (!empty($registerErrors['full_name'])): ?><div class="invalid-feedback"><?= h((string)$registerErrors['full_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="register_email">Email</label>
|
||||
<input id="register_email" name="email" type="email" class="form-control<?= store_input_class($registerErrors, 'email') ?>" value="<?= h((string)($registerForm['email'] ?? '')) ?>" maxlength="160" autocomplete="username" placeholder="nama@email.com" required>
|
||||
<?php if (!empty($registerErrors['email'])): ?><div class="invalid-feedback"><?= h((string)$registerErrors['email']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="register_password">Password</label>
|
||||
<input id="register_password" name="password" type="password" class="form-control<?= store_input_class($registerErrors, 'password') ?>" minlength="8" autocomplete="new-password" placeholder="Minimal 8 karakter" required>
|
||||
<?php if (!empty($registerErrors['password'])): ?><div class="invalid-feedback"><?= h((string)$registerErrors['password']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="register_confirm_password">Konfirmasi password</label>
|
||||
<input id="register_confirm_password" name="confirm_password" type="password" class="form-control<?= store_input_class($registerErrors, 'confirm_password') ?>" minlength="8" autocomplete="new-password" placeholder="Ulangi password" required>
|
||||
<?php if (!empty($registerErrors['confirm_password'])): ?><div class="invalid-feedback"><?= h((string)$registerErrors['confirm_password']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<button class="btn btn-dark" type="submit">Buat akun</button>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php store_page_end(); ?>
|
||||
125
cart.php
Normal file
125
cart.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
$summary = store_cart_summary();
|
||||
$lines = $summary['lines'];
|
||||
$currentUser = store_current_user();
|
||||
|
||||
store_page_start('Keranjang', 'Tinjau item, ubah quantity, dan lanjutkan checkout.', ['noindex' => true]);
|
||||
?>
|
||||
<section class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Keranjang</span>
|
||||
<h1 class="section-title">Tinjau pesanan sebelum checkout.</h1>
|
||||
<p class="section-copy mb-0">Update quantity, cek ongkir, lalu lanjut ke checkout untuk menyimpan pesanan ke sistem.</p>
|
||||
</div>
|
||||
|
||||
<?php if ($lines && !$currentUser): ?>
|
||||
<div class="alert alert-light border shadow-sm mb-4" role="alert">
|
||||
Keranjang berisi item, tetapi checkout hanya bisa dilakukan setelah login.
|
||||
Silakan <a href="auth.php?mode=login&redirect_to=cart.php">login / register</a> untuk melanjutkan pesanan.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$lines): ?>
|
||||
<div class="empty-state-card text-center mx-auto">
|
||||
<span class="eyebrow">Keranjang kosong</span>
|
||||
<h2 class="section-title">Belum ada produk di keranjang.</h2>
|
||||
<p class="section-copy">Mulai dari katalog untuk mencoba alur add-to-cart lalu kembali ke halaman ini.</p>
|
||||
<div class="d-flex justify-content-center gap-2 mt-3">
|
||||
<a class="btn btn-dark" href="index.php#catalog">Buka katalog</a>
|
||||
<a class="btn btn-outline-secondary" href="order_status.php">Lacak pesanan</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<form id="cart-update-form" action="cart_action.php" method="post" class="d-grid gap-3" data-auto-disable>
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="hidden" name="redirect_to" value="cart.php">
|
||||
<?php foreach ($lines as $line): ?>
|
||||
<article class="cart-item">
|
||||
<div class="product-visual product-visual--<?= h($line['product']['tone']) ?> product-visual--mini">
|
||||
<span class="product-visual__meta"><?= h($line['product']['category_label']) ?></span>
|
||||
<strong class="product-visual__code"><?= h($line['product']['visual_code']) ?></strong>
|
||||
<span class="product-visual__name"><?= h($line['product']['name']) ?></span>
|
||||
</div>
|
||||
<div class="cart-item__body">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-3">
|
||||
<div>
|
||||
<h2 class="cart-item__title"><?= h($line['product']['name']) ?></h2>
|
||||
<p class="text-muted mb-1"><?= h($line['product']['short_description']) ?></p>
|
||||
<div class="detail-chip-group mt-2">
|
||||
<span class="detail-chip"><?= h($line['product']['lead_time']) ?></span>
|
||||
<span class="detail-chip"><?= h($line['product']['serves']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-md-end">
|
||||
<div class="price-tag price-tag--inline"><?= h(store_money((float)$line['product']['price'])) ?></div>
|
||||
<div class="small text-muted mt-1">per <?= h($line['product']['unit']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between gap-3 mt-3">
|
||||
<div class="quantity-field quantity-field--compact">
|
||||
<button type="button" class="btn btn-outline-secondary" data-qty-target="qty-<?= h($line['slug']) ?>" data-qty-step="-1" aria-label="Kurangi jumlah">−</button>
|
||||
<input id="qty-<?= h($line['slug']) ?>" class="form-control text-center" type="number" name="quantities[<?= h($line['slug']) ?>]" min="1" max="20" value="<?= h((string)$line['quantity']) ?>">
|
||||
<button type="button" class="btn btn-outline-secondary" data-qty-target="qty-<?= h($line['slug']) ?>" data-qty-step="1" aria-label="Tambah jumlah">+</button>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 ms-sm-auto">
|
||||
<div class="cart-item__total"><?= h(store_money((float)$line['line_total'])) ?></div>
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm" form="remove-<?= h($line['slug']) ?>">Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-2">
|
||||
<a class="btn btn-outline-secondary" href="index.php#catalog">Tambah produk lain</a>
|
||||
<button class="btn btn-dark" type="submit">Perbarui keranjang</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php foreach ($lines as $line): ?>
|
||||
<form id="remove-<?= h($line['slug']) ?>" action="cart_action.php" method="post" class="d-none">
|
||||
<input type="hidden" name="action" value="remove">
|
||||
<input type="hidden" name="slug" value="<?= h($line['slug']) ?>">
|
||||
<input type="hidden" name="redirect_to" value="cart.php">
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<aside class="summary-card sticky-summary">
|
||||
<div class="card-kicker">Ringkasan pembayaran</div>
|
||||
<h2 class="summary-title">Siap dilanjutkan ke checkout.</h2>
|
||||
<div class="receipt-line">
|
||||
<span>Subtotal</span>
|
||||
<strong><?= h(store_money((float)$summary['subtotal'])) ?></strong>
|
||||
</div>
|
||||
<div class="receipt-line">
|
||||
<span>Ongkir</span>
|
||||
<strong><?= h(store_money((float)$summary['shipping_fee'])) ?></strong>
|
||||
</div>
|
||||
<div class="receipt-line receipt-line--total">
|
||||
<span>Total</span>
|
||||
<strong><?= h(store_money((float)$summary['grand_total'])) ?></strong>
|
||||
</div>
|
||||
<p class="note-copy mt-3 mb-4">Gratis ongkir otomatis untuk subtotal mulai Rp 250.000.</p>
|
||||
<div class="d-grid gap-2">
|
||||
<?php if ($currentUser): ?>
|
||||
<a class="btn btn-dark btn-lg" href="checkout.php">Lanjut ke checkout</a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-dark btn-lg" href="auth.php?mode=login&redirect_to=cart.php">Login untuk lanjut checkout</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-outline-secondary" href="order_status.php">Sudah punya kode pesanan?</a>
|
||||
</div>
|
||||
<?php if (!$currentUser): ?>
|
||||
<p class="note-copy mt-3 mb-0">Setelah login berhasil, Anda bisa kembali ke keranjang dan meneruskan checkout.</p>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php store_page_end(); ?>
|
||||
59
cart_action.php
Normal file
59
cart_action.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
header('Location: cart.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
$redirectTo = store_safe_redirect((string)($_POST['redirect_to'] ?? 'cart.php'), 'cart.php');
|
||||
|
||||
switch ($action) {
|
||||
case 'add':
|
||||
$slug = (string)($_POST['slug'] ?? '');
|
||||
$quantity = (int)($_POST['quantity'] ?? 1);
|
||||
|
||||
if (!store_is_logged_in()) {
|
||||
if (store_queue_cart_add_after_login($slug, $quantity, $redirectTo)) {
|
||||
store_flash('warning', 'Silakan login terlebih dahulu untuk menambahkan produk ke keranjang.');
|
||||
header('Location: auth.php?mode=login&redirect_to=' . rawurlencode($redirectTo));
|
||||
exit;
|
||||
}
|
||||
|
||||
store_flash('danger', 'Produk tidak ditemukan.');
|
||||
break;
|
||||
}
|
||||
|
||||
if (store_add_to_cart($slug, $quantity)) {
|
||||
$product = store_product($slug);
|
||||
store_flash('success', ($product['name'] ?? 'Produk') . ' ditambahkan ke keranjang.');
|
||||
} else {
|
||||
store_flash('danger', 'Produk tidak ditemukan.');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
$quantities = $_POST['quantities'] ?? [];
|
||||
if (!is_array($quantities)) {
|
||||
$quantities = [];
|
||||
}
|
||||
store_update_cart($quantities);
|
||||
store_flash('success', 'Keranjang berhasil diperbarui.');
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
$slug = (string)($_POST['slug'] ?? '');
|
||||
store_remove_from_cart($slug);
|
||||
store_flash('warning', 'Produk dihapus dari keranjang.');
|
||||
break;
|
||||
|
||||
default:
|
||||
store_flash('danger', 'Aksi keranjang tidak dikenali.');
|
||||
break;
|
||||
}
|
||||
|
||||
header('Location: ' . $redirectTo);
|
||||
exit;
|
||||
149
checkout.php
Normal file
149
checkout.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
$summary = store_cart_summary();
|
||||
if (!$summary['lines']) {
|
||||
store_flash('warning', 'Keranjang kosong. Tambahkan produk terlebih dahulu.');
|
||||
header('Location: cart.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$currentUser = store_current_user();
|
||||
if (!$currentUser) {
|
||||
store_flash('warning', 'Silakan login terlebih dahulu untuk melanjutkan checkout.');
|
||||
header('Location: auth.php?mode=login&redirect_to=checkout.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$formData = store_checkout_defaults();
|
||||
$errors = [];
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
$result = store_create_order($_POST);
|
||||
if (!empty($result['success'])) {
|
||||
store_flash('success', 'Pesanan berhasil dibuat. Gunakan kode pesanan untuk melacak status.');
|
||||
header('Location: order_status.php?order=' . urlencode((string)$result['order_number']) . '&created=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
$formData = $result['data'] ?? $formData;
|
||||
$errors = $result['errors'] ?? [];
|
||||
}
|
||||
|
||||
store_page_start('Checkout', 'Lengkapi data pelanggan, alamat, dan metode pembayaran untuk membuat pesanan.', ['noindex' => true]);
|
||||
?>
|
||||
<section class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Checkout</span>
|
||||
<h1 class="section-title">Simpan pesanan ke sistem.</h1>
|
||||
<p class="section-copy mb-0">Status awal pesanan adalah <strong>Menunggu Pembayaran</strong>. Instruksi pembayaran akan tampil setelah order berhasil dibuat.</p>
|
||||
</div>
|
||||
|
||||
<?php if (isset($errors['cart'])): ?>
|
||||
<div class="alert alert-warning border-0 shadow-sm mb-4" role="alert">
|
||||
<?= h($errors['cart']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<form action="checkout.php" method="post" class="surface-panel p-4 p-lg-5" data-auto-disable>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="customer_name">Nama penerima</label>
|
||||
<input id="customer_name" name="customer_name" type="text" class="form-control<?= store_input_class($errors, 'customer_name') ?>" value="<?= h($formData['customer_name']) ?>" required>
|
||||
<?php if (isset($errors['customer_name'])): ?><div class="invalid-feedback"><?= h($errors['customer_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">Nomor telepon</label>
|
||||
<input id="phone" name="phone" type="tel" class="form-control<?= store_input_class($errors, 'phone') ?>" value="<?= h($formData['phone']) ?>" required>
|
||||
<?php if (isset($errors['phone'])): ?><div class="invalid-feedback"><?= h($errors['phone']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input id="email" name="email" type="email" class="form-control<?= store_input_class($errors, 'email') ?>" value="<?= h($formData['email']) ?>" required>
|
||||
<?php if (isset($errors['email'])): ?><div class="invalid-feedback"><?= h($errors['email']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="address">Alamat lengkap</label>
|
||||
<textarea id="address" name="address" rows="4" class="form-control<?= store_input_class($errors, 'address') ?>" required><?= h($formData['address']) ?></textarea>
|
||||
<?php if (isset($errors['address'])): ?><div class="invalid-feedback"><?= h($errors['address']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="note">Catatan pesanan <span class="text-muted">(opsional)</span></label>
|
||||
<textarea id="note" name="note" rows="3" class="form-control"><?= h($formData['note']) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<div>
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 mb-1">Metode pembayaran</h2>
|
||||
<p class="text-muted mb-0">Pilih salah satu. Instruksi lengkap muncul di halaman status pesanan.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach (store_payment_methods() as $value => $method): ?>
|
||||
<div class="col-md-4">
|
||||
<div class="payment-option">
|
||||
<input class="btn-check" type="radio" name="payment_method" id="payment-<?= h($value) ?>" value="<?= h($value) ?>" <?= $formData['payment_method'] === $value ? 'checked' : '' ?>>
|
||||
<label class="choice-card" for="payment-<?= h($value) ?>">
|
||||
<span class="choice-title"><?= h($method['label']) ?></span>
|
||||
<span class="choice-copy"><?= h($method['description']) ?></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if (isset($errors['payment_method'])): ?>
|
||||
<div class="text-danger small mt-2"><?= h($errors['payment_method']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between gap-2 mt-4">
|
||||
<a class="btn btn-outline-secondary" href="cart.php">Kembali ke keranjang</a>
|
||||
<button class="btn btn-dark btn-lg" type="submit">Buat pesanan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<aside class="summary-card sticky-summary">
|
||||
<div class="card-kicker">Order summary</div>
|
||||
<h2 class="summary-title">Ringkasan item yang akan disimpan.</h2>
|
||||
<div class="receipt-card receipt-card--flat">
|
||||
<?php foreach ($summary['lines'] as $line): ?>
|
||||
<div class="receipt-line">
|
||||
<span><?= h($line['product']['name']) ?> × <?= h((string)$line['quantity']) ?></span>
|
||||
<strong><?= h(store_money((float)$line['line_total'])) ?></strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="receipt-line">
|
||||
<span>Subtotal</span>
|
||||
<strong><?= h(store_money((float)$summary['subtotal'])) ?></strong>
|
||||
</div>
|
||||
<div class="receipt-line">
|
||||
<span>Ongkir</span>
|
||||
<strong><?= h(store_money((float)$summary['shipping_fee'])) ?></strong>
|
||||
</div>
|
||||
<div class="receipt-line receipt-line--total">
|
||||
<span>Total pembayaran</span>
|
||||
<strong><?= h(store_money((float)$summary['grand_total'])) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-box mt-4 mb-0">
|
||||
<div class="card-kicker">Setelah checkout</div>
|
||||
<ul class="list-clean compact-list mb-0">
|
||||
<li><span class="list-index">A</span><span>Sistem membuat order number unik.</span></li>
|
||||
<li><span class="list-index">B</span><span>Data alamat dan item pesanan tersimpan di MySQL.</span></li>
|
||||
<li><span class="list-index">C</span><span>Pelanggan bisa membuka status pesanan kapan saja.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php store_page_end(); ?>
|
||||
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;
|
||||
414
index.php
414
index.php
@ -1,150 +1,274 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
$categories = store_categories();
|
||||
$selectedCategory = (string)($_GET['category'] ?? 'all');
|
||||
if (!isset($categories[$selectedCategory])) {
|
||||
$selectedCategory = 'all';
|
||||
}
|
||||
|
||||
$products = store_filtered_products($selectedCategory);
|
||||
$summary = store_cart_summary();
|
||||
$cartLines = array_slice($summary['lines'], 0, 3);
|
||||
$currentUser = store_current_user();
|
||||
$requiresLoginToCart = $currentUser === null;
|
||||
$paymentMethods = store_payment_methods();
|
||||
|
||||
store_page_start(
|
||||
'Home',
|
||||
'Website toko online bakery dengan halaman home, daftar kue, info pembayaran, kontak, dan login user yang lebih rapi.'
|
||||
);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
<section class="hero-panel mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-7">
|
||||
<span class="eyebrow">Home Publik</span>
|
||||
<h1 class="display-title">Halaman home dapat diakses oleh semua pengguna.</h1>
|
||||
<p class="lead-copy">
|
||||
Dari halaman ini, pengunjung bisa melihat katalog bakery, memilih produk, lalu membuka
|
||||
<strong>halaman detail</strong> yang berisi informasi produk, harga, dan tombol keranjang.
|
||||
Saat tombol keranjang dipilih, sistem akan memeriksa apakah pengguna sudah login atau belum.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||
<a class="btn btn-dark btn-lg" href="#catalog">Pilih produk</a>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="auth.php"><?= $currentUser ? 'Akun saya' : 'Login / Register' ?></a>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="#payment-info">Info pembayaran</a>
|
||||
</div>
|
||||
<div class="row g-3 mt-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value"><?= h((string)count(store_products())) ?></div>
|
||||
<div class="metric-label">produk tampil di home publik</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value"><?= h((string)count($paymentMethods)) ?></div>
|
||||
<div class="metric-label">metode bayar diinformasikan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value"><?= h((string)store_cart_count()) ?></div>
|
||||
<div class="metric-label">item aktif di keranjang</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<aside class="summary-card h-100">
|
||||
<div class="card-kicker">Alur user</div>
|
||||
<h2 class="summary-title">Home → detail produk → login → keranjang.</h2>
|
||||
<ul class="list-clean compact-list mb-4">
|
||||
<li><span class="list-index">1</span><span>Halaman home dapat dibuka oleh semua pengunjung.</span></li>
|
||||
<li><span class="list-index">2</span><span>Saat produk dipilih, sistem menampilkan detail produk, harga, dan tombol keranjang.</span></li>
|
||||
<li><span class="list-index">3</span><span>Saat tombol keranjang ditekan, sistem memeriksa apakah user sudah login.</span></li>
|
||||
<li><span class="list-index">4</span><span>Jika belum login, user diarahkan ke halaman login / register.</span></li>
|
||||
<li><span class="list-index">5</span><span>Jika sudah login, produk langsung ditambahkan ke keranjang belanja.</span></li>
|
||||
</ul>
|
||||
|
||||
<div class="receipt-card">
|
||||
<div class="receipt-card__head">
|
||||
<span>Ringkasan keranjang</span>
|
||||
<strong><?= h((string)store_cart_count()) ?> item</strong>
|
||||
</div>
|
||||
<?php if ($cartLines): ?>
|
||||
<?php foreach ($cartLines as $line): ?>
|
||||
<div class="receipt-line">
|
||||
<span><?= h($line['product']['name']) ?> × <?= h((string)$line['quantity']) ?></span>
|
||||
<strong><?= h(store_money((float)$line['line_total'])) ?></strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="receipt-line receipt-line--total">
|
||||
<span>Total sementara</span>
|
||||
<strong><?= h(store_money((float)$summary['grand_total'])) ?></strong>
|
||||
</div>
|
||||
<a class="btn btn-dark w-100 mt-3" href="cart.php">Buka keranjang</a>
|
||||
<?php else: ?>
|
||||
<p class="text-muted mb-3">Keranjang masih kosong. Mulai dengan memilih produk pada katalog di bawah.</p>
|
||||
<a class="btn btn-outline-secondary w-100" href="#catalog">Buka daftar kue</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">
|
||||
<div class="feature-card h-100">
|
||||
<div class="feature-card__title">Home dapat diakses umum</div>
|
||||
<p class="feature-card__copy">Semua pengunjung dapat membuka homepage, melihat katalog, dan memilih produk tanpa harus login terlebih dahulu.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="feature-card h-100">
|
||||
<div class="feature-card__title">Memilih produk membuka detail</div>
|
||||
<p class="feature-card__copy">Setiap produk mengarahkan pengguna ke halaman detail yang menampilkan informasi produk, harga, dan tombol keranjang.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="feature-card h-100">
|
||||
<div class="feature-card__title">Keranjang selalu cek login</div>
|
||||
<p class="feature-card__copy">Saat tombol keranjang dipilih, sistem akan mengecek sesi login. Jika belum login, pengguna diarahkan ke halaman autentikasi terlebih dahulu.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="catalog" class="section-block">
|
||||
<div class="section-heading d-flex flex-column flex-lg-row align-items-lg-end justify-content-between gap-3 mb-4">
|
||||
<div>
|
||||
<span class="eyebrow">Daftar Kue</span>
|
||||
<h2 class="section-title">Pilihan cake, bread, dan pastry untuk pengguna.</h2>
|
||||
<p class="section-copy mb-0">Klik salah satu produk untuk membuka halaman detail, melihat harga, lalu lanjut ke tombol keranjang sesuai alur pengguna.</p>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 filter-pills" data-filter-bar>
|
||||
<?php foreach ($categories as $key => $category): ?>
|
||||
<?php $isActive = $key === $selectedCategory; ?>
|
||||
<a class="filter-pill<?= $isActive ? ' is-active' : '' ?>" href="index.php?category=<?= h($key) ?>#catalog">
|
||||
<?= h($category['label']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<?php foreach ($products as $product): ?>
|
||||
<div class="col-sm-6 col-xl-4">
|
||||
<article class="product-card h-100">
|
||||
<div class="product-visual product-visual--<?= h($product['tone']) ?>">
|
||||
<span class="product-visual__meta"><?= h($product['category_label']) ?></span>
|
||||
<strong class="product-visual__code"><?= h($product['visual_code']) ?></strong>
|
||||
<span class="product-visual__name"><?= h($product['name']) ?></span>
|
||||
</div>
|
||||
<div class="product-card__body">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 mb-2">
|
||||
<div>
|
||||
<h3 class="product-card__title"><?= h($product['name']) ?></h3>
|
||||
<p class="text-muted small mb-0"><?= h($product['lead_time']) ?></p>
|
||||
</div>
|
||||
<span class="price-tag"><?= h(store_money((float)$product['price'])) ?></span>
|
||||
</div>
|
||||
<p class="product-card__copy"><?= h($product['short_description']) ?></p>
|
||||
<div class="detail-chip-group mb-3">
|
||||
<?php foreach ($product['highlights'] as $highlight): ?>
|
||||
<span class="detail-chip"><?= h($highlight) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-2 mt-auto">
|
||||
<a class="btn btn-dark w-100" href="product.php?slug=<?= h($product['slug']) ?>">Lihat detail & harga</a>
|
||||
<span class="text-muted small">
|
||||
<?= $requiresLoginToCart
|
||||
? 'Tombol keranjang tersedia di halaman detail. Jika belum login, sistem akan mengarahkan Anda ke halaman login terlebih dahulu.'
|
||||
: 'Anda sudah login. Buka halaman detail untuk menambahkan produk ini langsung ke keranjang.' ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="payment-info" class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Info Pembayaran</span>
|
||||
<h2 class="section-title">Pilihan pembayaran yang bisa diakses pengguna.</h2>
|
||||
<p class="section-copy mb-0">Bagian ini menjelaskan metode pembayaran sejak awal agar proses checkout terasa lebih meyakinkan.</p>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($paymentMethods as $method): ?>
|
||||
<div class="col-lg-4">
|
||||
<article class="payment-info-card h-100">
|
||||
<div class="payment-info-card__label"><?= h($method['label']) ?></div>
|
||||
<p class="payment-info-card__copy"><?= h($method['description']) ?></p>
|
||||
<p class="payment-info-card__instruction mb-0"><?= h($method['instruction']) ?></p>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contact" class="section-block pt-0">
|
||||
<div class="row g-4 align-items-stretch">
|
||||
<div class="col-lg-6">
|
||||
<div class="surface-panel h-100 contact-panel">
|
||||
<span class="eyebrow">Kontak Kami</span>
|
||||
<h2 class="section-title">Area bantuan untuk kebutuhan pengguna.</h2>
|
||||
<p class="section-copy">Bagian kontak diposisikan sebagai tempat pengguna memahami kapan harus menghubungi admin toko.</p>
|
||||
<ul class="list-clean compact-list mb-0">
|
||||
<li><span class="list-index">1</span><span>Konfirmasi ketersediaan produk dan estimasi pengerjaan pesanan.</span></li>
|
||||
<li><span class="list-index">2</span><span>Menanyakan jadwal pengiriman, perubahan alamat, atau detail pesanan.</span></li>
|
||||
<li><span class="list-index">3</span><span>Mengirim bukti pembayaran setelah checkout jika metode bayar memerlukannya.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<aside class="summary-card h-100 contact-panel">
|
||||
<div class="card-kicker">Langkah cepat</div>
|
||||
<h2 class="summary-title">Yang sebaiknya dilakukan pengguna lebih dulu.</h2>
|
||||
<div class="contact-checklist">
|
||||
<div class="contact-checklist__item">
|
||||
<strong>Login / Register</strong>
|
||||
<span>Masuk dengan email dan password agar proses pemesanan lebih rapi.</span>
|
||||
</div>
|
||||
<div class="contact-checklist__item">
|
||||
<strong>Pilih produk</strong>
|
||||
<span>Tambahkan cake, bread, atau pastry ke keranjang sesuai kebutuhan.</span>
|
||||
</div>
|
||||
<div class="contact-checklist__item">
|
||||
<strong>Lihat status</strong>
|
||||
<span>Gunakan order number dan email untuk memeriksa perkembangan pesanan.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<a class="btn btn-dark" href="auth.php"><?= $currentUser ? 'Buka akun saya' : 'Buka login user' ?></a>
|
||||
<a class="btn btn-outline-secondary" href="order_status.php">Lacak pesanan</a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Cara Order</span>
|
||||
<h2 class="section-title">Alur sederhana yang siap dipakai pada tampilan user.</h2>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="step-card h-100">
|
||||
<div class="step-card__number">01</div>
|
||||
<h3>Login / Register</h3>
|
||||
<p>Pengguna masuk dengan email dan password, atau membuat akun baru jika belum terdaftar.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="step-card h-100">
|
||||
<div class="step-card__number">02</div>
|
||||
<h3>Pilih dan checkout</h3>
|
||||
<p>Setelah memilih produk, pengguna melanjutkan ke checkout untuk menyimpan alamat dan metode pembayaran.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="step-card h-100">
|
||||
<div class="step-card__number">03</div>
|
||||
<h3>Lacak status</h3>
|
||||
<p>Gunakan kode pesanan dan email untuk melihat instruksi pembayaran dan status order secara mandiri.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block pt-0">
|
||||
<div class="documentation-note">
|
||||
<strong>Dokumentasi use case tetap tersedia.</strong>
|
||||
<p>Halaman use case diagram versi pertama masih bisa dibuka untuk kebutuhan laporan, revisi, atau presentasi sistem.</p>
|
||||
<a class="btn btn-outline-secondary mt-3" href="use_case.php">Buka use case diagram</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php store_page_end(); ?>
|
||||
|
||||
9
logout.php
Normal file
9
logout.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
store_logout_user();
|
||||
store_flash('success', 'Anda berhasil logout dari akun user.');
|
||||
header('Location: auth.php');
|
||||
exit;
|
||||
222
order_status.php
Normal file
222
order_status.php
Normal file
@ -0,0 +1,222 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
$lookupOrder = store_sanitize_line((string)($_GET['order'] ?? ''), 30);
|
||||
$lookupEmail = trim(store_lower((string)($_GET['email'] ?? '')));
|
||||
$created = (string)($_GET['created'] ?? '') === '1';
|
||||
$lastLookup = store_last_order_lookup();
|
||||
$currentUser = store_current_user();
|
||||
|
||||
if ($lookupOrder !== '' && $lookupEmail === '' && ($lastLookup['order_number'] ?? '') === $lookupOrder) {
|
||||
$lookupEmail = (string)($lastLookup['email'] ?? '');
|
||||
}
|
||||
|
||||
if ($lookupEmail === '' && $currentUser) {
|
||||
$lookupEmail = (string)($currentUser['email'] ?? '');
|
||||
}
|
||||
|
||||
$order = null;
|
||||
$searchError = '';
|
||||
if ($lookupOrder !== '') {
|
||||
if ($lookupEmail === '') {
|
||||
$searchError = 'Masukkan email yang dipakai saat checkout untuk membuka detail pesanan.';
|
||||
} else {
|
||||
$order = store_find_order($lookupOrder, $lookupEmail);
|
||||
if (!$order) {
|
||||
$searchError = 'Pesanan tidak ditemukan. Pastikan kode pesanan dan email sudah benar.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store_page_start('Status Pesanan', 'Lacak status pesanan menggunakan order number dan email.', ['noindex' => true]);
|
||||
?>
|
||||
<section class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Status Pesanan</span>
|
||||
<h1 class="section-title">Lacak progress pesanan pelanggan.</h1>
|
||||
<p class="section-copy mb-0">Masukkan kode pesanan dan email untuk melihat item, total, metode pembayaran, dan status terbaru.</p>
|
||||
</div>
|
||||
|
||||
<?php if ($created && ($lastLookup['order_number'] ?? '') !== ''): ?>
|
||||
<div class="alert alert-success border-0 shadow-sm mb-4 d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3" role="alert">
|
||||
<div>
|
||||
<strong>Pesanan berhasil dibuat.</strong>
|
||||
<div class="small mt-1">Simpan kode <strong><?= h((string)$lastLookup['order_number']) ?></strong> untuk pelacakan status.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-success btn-sm" data-copy-text="<?= h((string)$lastLookup['order_number']) ?>">Salin kode pesanan</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<form action="order_status.php" method="get" class="surface-panel p-4 sticky-summary">
|
||||
<div class="card-kicker">Cek pesanan</div>
|
||||
<h2 class="h5 mb-3">Cari berdasarkan kode & email</h2>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="order">Kode pesanan</label>
|
||||
<input id="order" name="order" type="text" class="form-control" value="<?= h($lookupOrder) ?>" placeholder="contoh: SB260526-1234" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="email">Email checkout</label>
|
||||
<input id="email" name="email" type="email" class="form-control" value="<?= h($lookupEmail) ?>" required>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-dark" type="submit">Tampilkan status</button>
|
||||
<a class="btn btn-outline-secondary" href="index.php#catalog">Belanja lagi</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box mt-4 mb-0">
|
||||
<div class="card-kicker">Status default</div>
|
||||
<div class="status-legend">
|
||||
<?php foreach (store_status_steps() as $step): ?>
|
||||
<div class="status-legend__item">
|
||||
<span class="<?= h(store_status_class($step['value'])) ?>"><?= h($step['label']) ?></span>
|
||||
<p class="mb-0"><?= h($step['description']) ?></p>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<?php if ($searchError !== ''): ?>
|
||||
<div class="alert alert-warning border-0 shadow-sm mb-4" role="alert">
|
||||
<?= h($searchError) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($order): ?>
|
||||
<section class="surface-panel p-4 p-lg-5 mb-4">
|
||||
<div class="d-flex flex-column flex-lg-row align-items-lg-start justify-content-between gap-3 mb-4">
|
||||
<div>
|
||||
<div class="card-kicker">Order detail</div>
|
||||
<h2 class="section-title mb-2"><?= h((string)$order['order_number']) ?></h2>
|
||||
<p class="section-copy mb-0">Dibuat pada <?= h(store_format_datetime((string)$order['created_at'])) ?></p>
|
||||
</div>
|
||||
<span class="<?= h(store_status_class((string)$order['status'])) ?>"><?= h((string)$order['status']) ?></span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="metric-card h-100">
|
||||
<div class="metric-value metric-value--small"><?= h(store_money((float)$order['grand_total'])) ?></div>
|
||||
<div class="metric-label">total pembayaran</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="metric-card h-100">
|
||||
<div class="metric-value metric-value--small"><?= h((string)$order['payment_method_label']) ?></div>
|
||||
<div class="metric-label">metode bayar</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="metric-card h-100">
|
||||
<div class="metric-value metric-value--small"><?= h((string)count($order['items'])) ?> item</div>
|
||||
<div class="metric-label">jumlah item di order</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h3 class="h6 text-uppercase text-muted mb-3">Item pesanan</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle table-borderless order-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th class="text-center">Qty</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($order['items'] as $item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= h((string)$item['name']) ?></strong>
|
||||
<div class="small text-muted"><?= h(store_money((float)$item['price'])) ?> / item</div>
|
||||
</td>
|
||||
<td class="text-center"><?= h((string)$item['quantity']) ?></td>
|
||||
<td class="text-end"><?= h(store_money((float)$item['line_total'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2">Subtotal</td>
|
||||
<td class="text-end"><?= h(store_money((float)$order['subtotal'])) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">Ongkir</td>
|
||||
<td class="text-end"><?= h(store_money((float)$order['shipping_fee'])) ?></td>
|
||||
</tr>
|
||||
<tr class="fw-semibold">
|
||||
<td colspan="2">Grand total</td>
|
||||
<td class="text-end"><?= h(store_money((float)$order['grand_total'])) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="info-box mb-3">
|
||||
<div class="card-kicker">Data penerima</div>
|
||||
<p class="mb-1"><strong><?= h((string)$order['customer_name']) ?></strong></p>
|
||||
<p class="mb-1"><?= h((string)$order['email']) ?></p>
|
||||
<p class="mb-1"><?= h((string)$order['phone']) ?></p>
|
||||
<p class="mb-0"><?= nl2br(h((string)$order['address'])) ?></p>
|
||||
</div>
|
||||
<div class="info-box mb-0">
|
||||
<div class="card-kicker">Instruksi pembayaran</div>
|
||||
<p class="mb-2"><?= h((string)$order['payment_instruction']) ?></p>
|
||||
<?php if (!empty($order['note'])): ?>
|
||||
<div class="note-copy"><strong>Catatan pelanggan:</strong> <?= h((string)$order['note']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ((string)$order['status'] !== 'Batal'): ?>
|
||||
<?php $currentIndex = store_status_index((string)$order['status']); ?>
|
||||
<section class="surface-panel p-4 p-lg-5">
|
||||
<div class="card-kicker">Progress pesanan</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($step['label']) ?></div>
|
||||
<p class="timeline-step__copy mb-0"><?= h($step['description']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<section class="empty-state-card">
|
||||
<span class="eyebrow">Belum ada detail</span>
|
||||
<h2 class="section-title">Masukkan kode pesanan untuk melihat status.</h2>
|
||||
<p class="section-copy">Setelah checkout berhasil, halaman ini akan menampilkan ringkasan item, total pembayaran, dan progress status order.</p>
|
||||
<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">2</span><span>Untuk kunjungan berikutnya, pelanggan cukup ingat order number dan email.</span></li>
|
||||
<li><span class="list-index">3</span><span>Status pesanan kini bisa diperbarui lewat admin lite internal.</span></li>
|
||||
</ul>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php store_page_end(); ?>
|
||||
147
product.php
Normal file
147
product.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
$slug = (string)($_GET['slug'] ?? '');
|
||||
$product = store_product($slug);
|
||||
|
||||
if (!$product) {
|
||||
http_response_code(404);
|
||||
store_page_start('Produk tidak ditemukan', 'Produk yang diminta tidak tersedia.', ['noindex' => true]);
|
||||
?>
|
||||
<section class="empty-state-card text-center mx-auto">
|
||||
<span class="eyebrow">404</span>
|
||||
<h1 class="section-title">Produk tidak ditemukan.</h1>
|
||||
<p class="section-copy">Slug produk tidak valid atau item sudah dihapus dari katalog demo.</p>
|
||||
<div class="d-flex justify-content-center gap-2 mt-3">
|
||||
<a class="btn btn-dark" href="index.php#catalog">Kembali ke katalog</a>
|
||||
<a class="btn btn-outline-secondary" href="cart.php">Lihat keranjang</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
store_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$relatedProducts = store_related_products($product['slug'], $product['category']);
|
||||
$currentUser = store_current_user();
|
||||
|
||||
store_page_start($product['name'], $product['short_description']);
|
||||
?>
|
||||
<nav aria-label="Breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb small mb-0">
|
||||
<li class="breadcrumb-item"><a href="index.php">Katalog</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= h($product['name']) ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<section class="section-block pt-0">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-6">
|
||||
<div class="surface-panel p-4 p-lg-5 h-100">
|
||||
<div class="product-visual product-visual--<?= h($product['tone']) ?> product-visual--large mb-4">
|
||||
<span class="product-visual__meta"><?= h($product['category_label']) ?></span>
|
||||
<strong class="product-visual__code"><?= h($product['visual_code']) ?></strong>
|
||||
<span class="product-visual__name"><?= h($product['name']) ?></span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($product['highlights'] as $highlight): ?>
|
||||
<div class="col-sm-4">
|
||||
<div class="metric-card h-100">
|
||||
<div class="metric-value metric-value--small"><?= h($highlight) ?></div>
|
||||
<div class="metric-label">informasi cepat</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="surface-panel p-4 p-lg-5 h-100">
|
||||
<span class="eyebrow"><?= h($product['category_label']) ?></span>
|
||||
<h1 class="section-title mb-2"><?= h($product['name']) ?></h1>
|
||||
<p class="lead-copy mb-3"><?= h($product['description']) ?></p>
|
||||
<div class="price-block mb-4">
|
||||
<div class="price-block__amount"><?= h(store_money((float)$product['price'])) ?></div>
|
||||
<div class="price-block__caption">per <?= h($product['unit']) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid mb-4">
|
||||
<div>
|
||||
<span class="detail-grid__label">Lead time</span>
|
||||
<strong><?= h($product['lead_time']) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="detail-grid__label">Porsi</span>
|
||||
<strong><?= h($product['serves']) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="detail-grid__label">Kategori</span>
|
||||
<strong><?= h($product['category_label']) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box mb-4">
|
||||
<div class="card-kicker">Alur user</div>
|
||||
<p class="mb-0">Halaman detail ini dapat diakses semua pengunjung. Saat tombol keranjang dipilih, sistem akan mengecek login terlebih dahulu sebelum produk dimasukkan ke keranjang.</p>
|
||||
</div>
|
||||
|
||||
<form action="cart_action.php" method="post" class="purchase-box" data-auto-disable>
|
||||
<input type="hidden" name="action" value="add">
|
||||
<input type="hidden" name="slug" value="<?= h($product['slug']) ?>">
|
||||
<input type="hidden" name="redirect_to" value="product.php?slug=<?= h($product['slug']) ?>">
|
||||
<label class="form-label" for="quantity">Jumlah</label>
|
||||
<div class="quantity-field mb-3">
|
||||
<button type="button" class="btn btn-outline-secondary" data-qty-target="product-qty" data-qty-step="-1" aria-label="Kurangi jumlah">−</button>
|
||||
<input id="product-qty" class="form-control text-center" type="number" name="quantity" min="1" max="20" value="1">
|
||||
<button type="button" class="btn btn-outline-secondary" data-qty-target="product-qty" data-qty-step="1" aria-label="Tambah jumlah">+</button>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-sm-flex">
|
||||
<button class="btn btn-dark btn-lg flex-sm-fill" type="submit">Tambah ke keranjang</button>
|
||||
<a class="btn btn-outline-secondary btn-lg flex-sm-fill" href="cart.php">Lihat keranjang</a>
|
||||
</div>
|
||||
<?php if (!$currentUser): ?>
|
||||
<p class="text-muted small mt-3 mb-0">Login wajib sebelum produk masuk ke keranjang. Setelah login berhasil, item ini akan langsung ditambahkan dan Anda kembali ke halaman detail produk.</p>
|
||||
<?php else: ?>
|
||||
<p class="text-muted small mt-3 mb-0">Anda sudah login, jadi produk ini bisa langsung dimasukkan ke keranjang belanja.</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="info-box mt-4">
|
||||
<div class="card-kicker">Catatan checkout</div>
|
||||
<p class="mb-0">Saat checkout, pelanggan akan memilih metode pembayaran dan sistem langsung membuat order number untuk pelacakan status.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Produk terkait</span>
|
||||
<h2 class="section-title">Tambahkan item lain untuk melengkapi pesanan.</h2>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<?php foreach ($relatedProducts as $related): ?>
|
||||
<div class="col-md-4">
|
||||
<article class="product-card h-100">
|
||||
<div class="product-visual product-visual--<?= h($related['tone']) ?>">
|
||||
<span class="product-visual__meta"><?= h($related['category_label']) ?></span>
|
||||
<strong class="product-visual__code"><?= h($related['visual_code']) ?></strong>
|
||||
<span class="product-visual__name"><?= h($related['name']) ?></span>
|
||||
</div>
|
||||
<div class="product-card__body">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 mb-2">
|
||||
<h3 class="product-card__title"><?= h($related['name']) ?></h3>
|
||||
<span class="price-tag"><?= h(store_money((float)$related['price'])) ?></span>
|
||||
</div>
|
||||
<p class="product-card__copy"><?= h($related['short_description']) ?></p>
|
||||
<a class="btn btn-outline-secondary w-100 mt-auto" href="product.php?slug=<?= h($related['slug']) ?>">Buka detail</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php store_page_end(); ?>
|
||||
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
|
||||
226
use_case.php
Normal file
226
use_case.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/store.php';
|
||||
|
||||
$plantumlSource = <<<'PLANTUML'
|
||||
@startuml
|
||||
left to right direction
|
||||
skinparam packageStyle rectangle
|
||||
skinparam shadowing false
|
||||
skinparam actorStyle awesome
|
||||
|
||||
actor "User" as User
|
||||
actor "Admin" as Admin
|
||||
|
||||
rectangle "Sistem Penjualan Kue Online\nSekut Bakery" {
|
||||
usecase "Login User" as UC1
|
||||
usecase "Melihat Produk" as UC2
|
||||
usecase "Memasukkan Produk\nke Dalam Keranjang" as UC3
|
||||
usecase "Checkout Keranjang\nPembelian" as UC4
|
||||
usecase "Mengirim Bukti\nPembayaran" as UC5
|
||||
usecase "Login Admin" as UC6
|
||||
usecase "Mengolah Data\nKategori" as UC7
|
||||
usecase "Mengolah Data\nProduk" as UC8
|
||||
usecase "Mengolah Data\nPembelian" as UC9
|
||||
usecase "Mencetak Laporan\nPembelian" as UC10
|
||||
usecase "Mengelola Data\nPelanggan" as UC11
|
||||
}
|
||||
|
||||
User --> UC1
|
||||
User --> UC2
|
||||
User --> UC3
|
||||
User --> UC4
|
||||
User --> UC5
|
||||
Admin --> UC6
|
||||
Admin --> UC7
|
||||
Admin --> UC8
|
||||
Admin --> UC9
|
||||
Admin --> UC10
|
||||
Admin --> UC11
|
||||
@enduml
|
||||
PLANTUML;
|
||||
|
||||
store_page_start(
|
||||
'Use Case Diagram',
|
||||
'Dokumentasi use case diagram versi pertama untuk Sistem Penjualan Kue Online Sekut Bakery, menampilkan aktor User dan Admin beserta fungsi utamanya.',
|
||||
['noindex' => true]
|
||||
);
|
||||
?>
|
||||
<section class="hero-panel mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-7">
|
||||
<span class="eyebrow">Dokumentasi Proyek</span>
|
||||
<h1 class="display-title">Use case diagram versi pertama sudah tayang di web.</h1>
|
||||
<p class="lead-copy">
|
||||
Halaman ini menampilkan blueprint fungsional level tinggi untuk Sistem Penjualan Kue Online Sekut Bakery.
|
||||
Versi yang dipasang memakai asosiasi langsung antara aktor dan use case, tanpa relasi <code><<include>></code> atau <code><<extend>></code>.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||
<a class="btn btn-dark btn-lg" href="#diagram-use-case">Lihat diagram</a>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="index.php">Kembali ke katalog</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<aside class="documentation-note h-100">
|
||||
<strong>Catatan implementasi</strong>
|
||||
<p>
|
||||
Ini adalah halaman dokumentasi analisis sistem untuk kebutuhan presentasi/laporan.
|
||||
Beberapa use case di diagram merepresentasikan arah pengembangan aplikasi, jadi tidak selalu identik dengan fitur MVP yang sudah live saat ini.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="diagram-use-case" class="section-block pt-0">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">Diagram Utama</span>
|
||||
<h2 class="section-title">Use case diagram Sistem Penjualan Kue Online Sekut Bakery.</h2>
|
||||
<p class="section-copy mb-0">
|
||||
Diagram di bawah mengikuti versi pertama yang paling sederhana: dua aktor utama, satu system boundary,
|
||||
dan relasi asosiasi langsung ke setiap use case inti.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<figure class="diagram-showcase mb-0">
|
||||
<div class="diagram-scroll" aria-label="Diagram use case Sistem Penjualan Kue Online Sekut Bakery">
|
||||
<svg viewBox="0 0 1200 760" role="img" aria-labelledby="useCaseTitle useCaseDesc" xmlns="http://www.w3.org/2000/svg">
|
||||
<title id="useCaseTitle">Use case diagram versi pertama Sekut Bakery</title>
|
||||
<desc id="useCaseDesc">Diagram dengan aktor User dan Admin yang terhubung langsung ke sebelas use case utama di dalam system boundary.</desc>
|
||||
|
||||
<rect class="diagram-boundary" x="250" y="48" width="700" height="664" rx="28" />
|
||||
<text class="diagram-title" x="600" y="94" text-anchor="middle">Sistem Penjualan Kue Online</text>
|
||||
<text class="diagram-boundary-label" x="600" y="120" text-anchor="middle">Sekut Bakery</text>
|
||||
|
||||
<circle class="diagram-actor" cx="116" cy="148" r="28" />
|
||||
<line class="diagram-actor" x1="116" y1="176" x2="116" y2="258" />
|
||||
<line class="diagram-actor" x1="72" y1="206" x2="160" y2="206" />
|
||||
<line class="diagram-actor" x1="116" y1="258" x2="76" y2="320" />
|
||||
<line class="diagram-actor" x1="116" y1="258" x2="156" y2="320" />
|
||||
<text class="diagram-actor-label" x="116" y="360" text-anchor="middle">User</text>
|
||||
|
||||
<circle class="diagram-actor" cx="1084" cy="148" r="28" />
|
||||
<line class="diagram-actor" x1="1084" y1="176" x2="1084" y2="258" />
|
||||
<line class="diagram-actor" x1="1040" y1="206" x2="1128" y2="206" />
|
||||
<line class="diagram-actor" x1="1084" y1="258" x2="1044" y2="320" />
|
||||
<line class="diagram-actor" x1="1084" y1="258" x2="1124" y2="320" />
|
||||
<text class="diagram-actor-label" x="1084" y="360" text-anchor="middle">Admin</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="432" cy="170" rx="128" ry="36" />
|
||||
<text class="diagram-node-label" x="432" y="176" text-anchor="middle">Login User</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="432" cy="260" rx="128" ry="36" />
|
||||
<text class="diagram-node-label" x="432" y="254" text-anchor="middle">Melihat Produk</text>
|
||||
<text class="diagram-node-label--small" x="432" y="276" text-anchor="middle">Produk bakery</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="432" cy="350" rx="144" ry="42" />
|
||||
<text class="diagram-node-label" x="432" y="344" text-anchor="middle">Memasukkan Produk</text>
|
||||
<text class="diagram-node-label--small" x="432" y="366" text-anchor="middle">ke Dalam Keranjang</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="432" cy="450" rx="144" ry="42" />
|
||||
<text class="diagram-node-label" x="432" y="444" text-anchor="middle">Checkout Keranjang</text>
|
||||
<text class="diagram-node-label--small" x="432" y="466" text-anchor="middle">Pembelian</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="432" cy="560" rx="144" ry="42" />
|
||||
<text class="diagram-node-label" x="432" y="554" text-anchor="middle">Mengirim Bukti</text>
|
||||
<text class="diagram-node-label--small" x="432" y="576" text-anchor="middle">Pembayaran</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="768" cy="150" rx="128" ry="36" />
|
||||
<text class="diagram-node-label" x="768" y="156" text-anchor="middle">Login Admin</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="768" cy="240" rx="138" ry="42" />
|
||||
<text class="diagram-node-label" x="768" y="234" text-anchor="middle">Mengolah Data</text>
|
||||
<text class="diagram-node-label--small" x="768" y="256" text-anchor="middle">Kategori</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="768" cy="340" rx="138" ry="42" />
|
||||
<text class="diagram-node-label" x="768" y="334" text-anchor="middle">Mengolah Data</text>
|
||||
<text class="diagram-node-label--small" x="768" y="356" text-anchor="middle">Produk</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="768" cy="440" rx="138" ry="42" />
|
||||
<text class="diagram-node-label" x="768" y="434" text-anchor="middle">Mengolah Data</text>
|
||||
<text class="diagram-node-label--small" x="768" y="456" text-anchor="middle">Pembelian</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="768" cy="540" rx="148" ry="42" />
|
||||
<text class="diagram-node-label" x="768" y="534" text-anchor="middle">Mencetak Laporan</text>
|
||||
<text class="diagram-node-label--small" x="768" y="556" text-anchor="middle">Pembelian</text>
|
||||
|
||||
<ellipse class="diagram-node" cx="768" cy="640" rx="152" ry="42" />
|
||||
<text class="diagram-node-label" x="768" y="634" text-anchor="middle">Mengelola Data</text>
|
||||
<text class="diagram-node-label--small" x="768" y="656" text-anchor="middle">Pelanggan</text>
|
||||
|
||||
<line class="diagram-connector" x1="160" y1="202" x2="304" y2="170" />
|
||||
<line class="diagram-connector" x1="160" y1="214" x2="304" y2="260" />
|
||||
<line class="diagram-connector" x1="160" y1="224" x2="288" y2="350" />
|
||||
<line class="diagram-connector" x1="160" y1="234" x2="288" y2="450" />
|
||||
<line class="diagram-connector" x1="160" y1="244" x2="288" y2="560" />
|
||||
|
||||
<line class="diagram-connector" x1="1040" y1="202" x2="896" y2="150" />
|
||||
<line class="diagram-connector" x1="1040" y1="214" x2="906" y2="240" />
|
||||
<line class="diagram-connector" x1="1040" y1="224" x2="906" y2="340" />
|
||||
<line class="diagram-connector" x1="1040" y1="234" x2="906" y2="440" />
|
||||
<line class="diagram-connector" x1="1040" y1="244" x2="916" y2="540" />
|
||||
<line class="diagram-connector" x1="1040" y1="254" x2="920" y2="640" />
|
||||
</svg>
|
||||
</div>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section class="section-block pt-0">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<article class="actor-panel">
|
||||
<div class="actor-panel__head">
|
||||
<div>
|
||||
<span class="card-kicker">Aktor 01</span>
|
||||
<h3>User</h3>
|
||||
</div>
|
||||
<span class="actor-badge">User</span>
|
||||
</div>
|
||||
<p>Pelanggan berfokus pada alur pembelian: masuk ke sistem, melihat produk, mengisi keranjang, checkout, lalu mengirim bukti pembayaran.</p>
|
||||
<ul class="usecase-list">
|
||||
<li><span>1</span><div><strong>Login User</strong><br><small>Masuk ke sistem sebelum memulai transaksi.</small></div></li>
|
||||
<li><span>2</span><div><strong>Melihat Produk</strong><br><small>Menjelajahi daftar produk kue yang tersedia.</small></div></li>
|
||||
<li><span>3</span><div><strong>Memasukkan Produk ke Dalam Keranjang</strong><br><small>Menambah item pilihan ke keranjang belanja.</small></div></li>
|
||||
<li><span>4</span><div><strong>Checkout Keranjang Pembelian</strong><br><small>Menyelesaikan transaksi dan membuat pesanan.</small></div></li>
|
||||
<li><span>5</span><div><strong>Mengirim Bukti Pembayaran</strong><br><small>Mengunggah/menyerahkan bukti pembayaran setelah checkout.</small></div></li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<article class="actor-panel">
|
||||
<div class="actor-panel__head">
|
||||
<div>
|
||||
<span class="card-kicker">Aktor 02</span>
|
||||
<h3>Admin</h3>
|
||||
</div>
|
||||
<span class="actor-badge">Admin</span>
|
||||
</div>
|
||||
<p>Admin berfokus pada pengelolaan sistem: login ke dashboard lalu mengatur kategori, produk, pembelian, laporan, dan data pelanggan.</p>
|
||||
<ul class="usecase-list">
|
||||
<li><span>1</span><div><strong>Login Admin</strong><br><small>Mengakses area pengelolaan sistem.</small></div></li>
|
||||
<li><span>2</span><div><strong>Mengolah Data Kategori</strong><br><small>Tambah, ubah, atau hapus kategori produk.</small></div></li>
|
||||
<li><span>3</span><div><strong>Mengolah Data Produk</strong><br><small>Mengelola katalog produk yang dijual.</small></div></li>
|
||||
<li><span>4</span><div><strong>Mengolah Data Pembelian</strong><br><small>Memeriksa dan memproses transaksi pelanggan.</small></div></li>
|
||||
<li><span>5</span><div><strong>Mencetak Laporan Pembelian</strong><br><small>Menyusun laporan transaksi untuk kebutuhan operasional.</small></div></li>
|
||||
<li><span>6</span><div><strong>Mengelola Data Pelanggan</strong><br><small>Memonitor data customer yang tersimpan di sistem.</small></div></li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block pt-0">
|
||||
<div class="surface-panel">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="eyebrow">PlantUML</span>
|
||||
<h2 class="section-title">Kode sumber diagram versi pertama.</h2>
|
||||
<p class="section-copy mb-0">Kode ini sama dengan versi pertama yang sebelumnya sudah dipilih, jadi bisa langsung disalin untuk laporan atau digenerate ulang di PlantUML.</p>
|
||||
</div>
|
||||
<details class="code-panel" open>
|
||||
<summary>Lihat kode PlantUML</summary>
|
||||
<pre><code><?= h($plantumlSource) ?></code></pre>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
<?php store_page_end(); ?>
|
||||
Loading…
x
Reference in New Issue
Block a user