Compare commits

...

5 Commits

Author SHA1 Message Date
Flatlogic Bot
a440f4357f Autosave: 20260526-085434 2026-05-26 08:54:27 +00:00
Flatlogic Bot
cb3a15004c Sekut bakery 2026-05-26 08:29:37 +00:00
Flatlogic Bot
192f07588e Autosave: 20260526-075134 2026-05-26 07:51:27 +00:00
Flatlogic Bot
30e21c6f6f Sekut Bakery 1 2026-05-26 07:35:51 +00:00
Flatlogic Bot
d282dac2e9 Sekut Bakery 2026-05-26 04:49:58 +00:00
20 changed files with 4963 additions and 517 deletions

974
admin.php Normal file
View 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
View 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

View File

@ -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…';
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

260
auth.php Normal file
View 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' ? '&amp;redirect_to=' . urlencode($redirectTo) : '' ?>">Login</a>
<a class="toggle-pill<?= $mode === 'register' ? ' is-active' : '' ?>" href="auth.php?mode=register<?= $redirectTo !== 'auth.php' ? '&amp;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' ? '&amp;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
View 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&amp;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&amp;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
View 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
View 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(); ?>

View 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
View File

@ -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 &amp; 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /checkout.php
Disallow: /order_status.php
Disallow: /admin.php

1162
store.php Normal file

File diff suppressed because it is too large Load Diff

226
use_case.php Normal file
View 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>&lt;&lt;include&gt;&gt;</code> atau <code>&lt;&lt;extend&gt;&gt;</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(); ?>