Sekut bakery

This commit is contained in:
Flatlogic Bot 2026-05-26 08:29:37 +00:00
parent 192f07588e
commit cb3a15004c
10 changed files with 1262 additions and 45 deletions

View File

@ -904,3 +904,427 @@ textarea:focus {
grid-template-columns: 44px minmax(72px, 1fr) 44px;
}
}
.diagram-showcase,
.actor-panel,
.documentation-note,
.code-panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.documentation-note {
padding: 1rem 1.15rem;
background: linear-gradient(180deg, #ffffff 0%, #f8f7f4 100%);
}
.documentation-note strong {
display: block;
margin-bottom: 0.35rem;
}
.documentation-note p {
margin: 0;
color: var(--muted);
line-height: 1.7;
}
.diagram-showcase {
padding: clamp(1rem, 2vw, 1.5rem);
background: linear-gradient(180deg, #fffcf7 0%, #f6f4ef 100%);
}
.diagram-scroll {
overflow-x: auto;
}
.diagram-scroll svg {
display: block;
width: 100%;
height: auto;
min-width: 960px;
}
.diagram-boundary {
fill: rgba(255, 255, 255, 0.92);
stroke: #2b2927;
stroke-width: 2;
}
.diagram-node {
fill: #ffffff;
stroke: #2b2927;
stroke-width: 2.25;
}
.diagram-connector {
fill: none;
stroke: #a8a29e;
stroke-width: 2;
stroke-linecap: round;
}
.diagram-actor {
fill: none;
stroke: #2b2927;
stroke-width: 2.75;
stroke-linecap: round;
}
.diagram-title {
font: 700 24px 'Inter', sans-serif;
fill: #171717;
}
.diagram-actor-label,
.diagram-node-label {
font: 600 18px 'Inter', sans-serif;
fill: #171717;
}
.diagram-node-label--small,
.diagram-boundary-label {
font: 500 16px 'Inter', sans-serif;
fill: #57534e;
}
.actor-panel {
padding: 1.4rem;
height: 100%;
}
.actor-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.actor-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 5rem;
padding: 0.45rem 0.8rem;
border-radius: 999px;
background: var(--accent-soft);
border: 1px solid var(--line);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.actor-panel h2,
.actor-panel h3 {
font-size: 1.2rem;
margin: 0;
}
.actor-panel p {
color: var(--muted);
line-height: 1.7;
}
.usecase-list {
list-style: none;
padding: 0;
margin: 1rem 0 0;
display: grid;
gap: 0.75rem;
}
.usecase-list li {
display: flex;
gap: 0.8rem;
align-items: flex-start;
padding: 0.9rem 1rem;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: var(--surface-alt);
}
.usecase-list span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
flex-shrink: 0;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--line);
font-size: 0.8rem;
font-weight: 700;
}
.code-panel {
overflow: hidden;
}
.code-panel summary {
cursor: pointer;
padding: 1rem 1.15rem;
font-weight: 600;
list-style: none;
}
.code-panel summary::-webkit-details-marker {
display: none;
}
.code-panel pre {
margin: 0;
padding: 0 1.15rem 1.15rem;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.92rem;
line-height: 1.65;
color: var(--text);
background: transparent;
}
.code-panel code {
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', monospace;
}
.payment-info-card,
.auth-side-card,
.auth-panel,
.account-stat {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.payment-info-card {
padding: 1.35rem;
background: linear-gradient(180deg, #ffffff 0%, #f8f7f4 100%);
}
.payment-info-card__label {
display: inline-flex;
align-items: center;
padding: 0.4rem 0.7rem;
margin-bottom: 0.85rem;
border-radius: 999px;
background: var(--accent-soft);
color: var(--text);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.payment-info-card__copy,
.payment-info-card__instruction {
color: var(--muted);
line-height: 1.7;
}
.payment-info-card__instruction {
padding-top: 0.85rem;
margin-top: 0.85rem;
border-top: 1px solid var(--line);
}
.contact-panel {
height: 100%;
}
.contact-checklist {
display: grid;
gap: 0.85rem;
}
.contact-checklist__item {
display: grid;
gap: 0.25rem;
padding: 0.95rem 1rem;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: var(--surface-alt);
}
.contact-checklist__item span {
color: var(--muted);
line-height: 1.65;
}
.auth-sidebar {
display: grid;
gap: 1rem;
position: sticky;
top: 6rem;
}
.auth-side-card {
padding: 1.25rem;
}
.sidebar-link-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.75rem;
}
.sidebar-link-list li a {
display: block;
padding: 0.95rem 1rem;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: var(--surface-alt);
transition: all 0.2s ease;
}
.sidebar-link-list li a:hover {
border-color: var(--text);
background: var(--surface);
transform: translateY(-1px);
}
.sidebar-link-list strong {
display: block;
margin-bottom: 0.2rem;
}
.sidebar-link-list span {
display: block;
color: var(--muted);
font-size: 0.9rem;
line-height: 1.6;
}
.sidebar-metric {
font-size: clamp(2.1rem, 4vw, 2.8rem);
line-height: 1;
font-weight: 800;
letter-spacing: -0.05em;
margin: 0.3rem 0 0.75rem;
}
.auth-shell {
padding: clamp(1.2rem, 2vw, 2rem);
}
.auth-toggle {
display: inline-flex;
gap: 0.4rem;
padding: 0.35rem;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface-alt);
}
.toggle-pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.65rem 1rem;
border-radius: 999px;
color: var(--muted);
font-weight: 600;
transition: all 0.2s ease;
}
.toggle-pill:hover,
.toggle-pill.is-active {
color: var(--text);
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.auth-panel {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1.35rem;
height: 100%;
}
.auth-panel--active {
border-color: var(--line-strong);
box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.08), var(--shadow-sm);
}
.auth-panel__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.auth-note {
margin-top: 1rem;
color: var(--muted);
}
.auth-note a {
text-decoration: underline;
text-underline-offset: 0.15em;
}
.auth-state-chip {
display: inline-flex;
align-items: center;
justify-content: center;
width: fit-content;
padding: 0.45rem 0.75rem;
margin-bottom: 0.8rem;
border-radius: 999px;
background: #ecfdf3;
border: 1px solid #bbf7d0;
color: #166534;
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.account-stat {
padding: 1rem 1.1rem;
height: 100%;
background: linear-gradient(180deg, #ffffff 0%, #f8f7f4 100%);
}
.account-stat__label {
display: block;
margin-bottom: 0.35rem;
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
@media (max-width: 991.98px) {
.auth-sidebar {
position: static;
}
}
@media (max-width: 767.98px) {
.auth-shell,
.auth-panel,
.auth-side-card,
.payment-info-card,
.account-stat {
padding: 1rem;
}
.auth-toggle {
display: flex;
width: 100%;
}
.toggle-pill {
flex: 1;
text-align: center;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

239
auth.php Normal file
View File

@ -0,0 +1,239 @@
<?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.'));
header('Location: ' . $redirectTo);
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.'));
header('Location: ' . $redirectTo);
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();
$accountTitle = $currentUser ? 'Akun Saya' : 'Login / Register';
$accountDescription = $currentUser
? 'Ringkasan akun user untuk melanjutkan belanja, checkout, dan melacak pesanan.'
: '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.' : '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.'
: '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">
<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">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">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(); ?>

View File

@ -12,6 +12,7 @@ if (!$summary['lines']) {
$formData = store_checkout_defaults();
$errors = [];
$currentUser = store_current_user();
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$result = store_create_order($_POST);
@ -34,6 +35,12 @@ store_page_start('Checkout', 'Lengkapi data pelanggan, alamat, dan metode pembay
<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 (!$currentUser): ?>
<div class="alert alert-light border shadow-sm mb-4" role="alert">
Belum login? <a href="auth.php?redirect_to=checkout.php">Masuk atau daftar akun</a> agar nama dan email terisi otomatis saat checkout.
</div>
<?php endif; ?>
<?php if (isset($errors['cart'])): ?>
<div class="alert alert-warning border-0 shadow-sm mb-4" role="alert">
<?= h($errors['cart']) ?>

144
index.php
View File

@ -12,55 +12,59 @@ if (!isset($categories[$selectedCategory])) {
$products = store_filtered_products($selectedCategory);
$summary = store_cart_summary();
$cartLines = array_slice($summary['lines'], 0, 3);
$currentUser = store_current_user();
$paymentMethods = store_payment_methods();
store_page_start(
'Toko Online Bakery',
'Pilih cake, roti, dan pastry favorit, simpan ke keranjang, checkout, lalu pantau status pesanan dari satu halaman.'
'Home',
'Website toko online bakery dengan halaman home, daftar kue, info pembayaran, kontak, dan login user yang lebih rapi.'
);
?>
<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">Initial MVP slice</span>
<h1 class="display-title">Belanja bakery tanpa alur yang berantakan.</h1>
<span class="eyebrow">Home</span>
<h1 class="display-title">Pesan kue lebih jelas dengan menu user yang lengkap.</h1>
<p class="lead-copy">
Katalog, keranjang, checkout, dan halaman status pesanan sekarang terhubung dalam satu alur yang rapi.
Cocok untuk validasi toko online berbasis PHP + MySQL sebelum lanjut ke admin panel penuh.
Tampilan user bakery kini dibuat lebih mudah dipahami: ada menu <strong>Home</strong>,
<strong>Daftar Kue</strong>, <strong>Info Pembayaran</strong>, <strong>Kontak Kami</strong>,
serta <strong>Login / Register</strong> agar alur pemesanan terasa lebih terstruktur.
</p>
<div class="d-flex flex-wrap gap-2 mt-4">
<a class="btn btn-dark btn-lg" href="#catalog">Belanja sekarang</a>
<a class="btn btn-outline-secondary btn-lg" href="order_status.php">Cek status pesanan</a>
<a class="btn btn-dark btn-lg" href="#catalog">Daftar kue</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 demo siap dijual</div>
<div class="metric-label">produk bakery tampil di daftar kue</div>
</div>
</div>
<div class="col-sm-4">
<div class="metric-card">
<div class="metric-value">1 tabel</div>
<div class="metric-label">pesanan tersimpan di MySQL</div>
<div class="metric-value"><?= h((string)count($paymentMethods)) ?></div>
<div class="metric-label">metode pembayaran siap dipilih</div>
</div>
</div>
<div class="col-sm-4">
<div class="metric-card">
<div class="metric-value">4 status</div>
<div class="metric-label">progress order yang mudah dipahami</div>
<div class="metric-value"><?= h((string)store_cart_count()) ?></div>
<div class="metric-label">item tersimpan di keranjang</div>
</div>
</div>
</div>
</div>
<div class="col-lg-5">
<aside class="summary-card h-100">
<div class="card-kicker">Workflow pelanggan</div>
<h2 class="summary-title">Dari pilih produk sampai tracking status.</h2>
<div class="card-kicker">Alur pengguna</div>
<h2 class="summary-title">Dari login sampai cek status pesanan.</h2>
<ul class="list-clean compact-list mb-4">
<li><span class="list-index">1</span><span>Pelanggan pilih produk dari katalog atau halaman detail.</span></li>
<li><span class="list-index">2</span><span>Keranjang menyimpan item, quantity, subtotal, dan ongkir.</span></li>
<li><span class="list-index">3</span><span>Checkout membuat order number dan menyimpan data di sistem.</span></li>
<li><span class="list-index">4</span><span>Halaman status menampilkan detail pesanan dan instruksi pembayaran.</span></li>
<li><span class="list-index">1</span><span>Pengguna login dengan email dan password atau membuat akun baru.</span></li>
<li><span class="list-index">2</span><span>Pengguna membuka daftar kue dan memilih produk bakery yang diinginkan.</span></li>
<li><span class="list-index">3</span><span>Checkout menyimpan pesanan, alamat, dan metode pembayaran ke sistem.</span></li>
<li><span class="list-index">4</span><span>Kode pesanan dipakai lagi untuk melacak status dan konfirmasi pembayaran.</span></li>
</ul>
<div class="receipt-card">
@ -81,8 +85,8 @@ store_page_start(
</div>
<a class="btn btn-dark w-100 mt-3" href="cart.php">Lanjutkan checkout</a>
<?php else: ?>
<p class="text-muted mb-3">Keranjang masih kosong. Tambahkan produk di bawah untuk mencoba alur checkout.</p>
<a class="btn btn-outline-secondary w-100" href="#catalog">Mulai dari katalog</a>
<p class="text-muted mb-3">Keranjang masih kosong. Mulai dari daftar kue untuk mencoba alur pemesanan user.</p>
<a class="btn btn-outline-secondary w-100" href="#catalog">Buka daftar kue</a>
<?php endif; ?>
</div>
</aside>
@ -94,20 +98,20 @@ store_page_start(
<div class="row g-3">
<div class="col-lg-4">
<div class="feature-card h-100">
<div class="feature-card__title">Katalog yang langsung bisa dijual</div>
<p class="feature-card__copy">Ada kategori, harga, deskripsi singkat, dan halaman detail produk untuk tiap item.</p>
<div class="feature-card__title">Login user lebih jelas</div>
<p class="feature-card__copy">Halaman login/register kini menjadi pintu masuk yang jelas bagi pengguna yang sudah atau belum memiliki akun.</p>
</div>
</div>
<div class="col-lg-4">
<div class="feature-card h-100">
<div class="feature-card__title">Checkout tersimpan di sistem</div>
<p class="feature-card__copy">Order number dibuat otomatis, item pesanan disimpan di MySQL, dan status awal langsung tercatat.</p>
<div class="feature-card__title">Daftar kue lebih mudah dicari</div>
<p class="feature-card__copy">Produk dibagi per kategori agar pengguna lebih cepat menemukan cake, bread, atau pastry yang dibutuhkan.</p>
</div>
</div>
<div class="col-lg-4">
<div class="feature-card h-100">
<div class="feature-card__title">Status pesanan mandiri</div>
<p class="feature-card__copy">Pelanggan cukup masukkan kode pesanan dan email untuk melihat progress dan instruksi pembayaran.</p>
<div class="feature-card__title">Pembayaran & status tetap terhubung</div>
<p class="feature-card__copy">Setelah checkout, pelanggan tetap bisa melihat metode bayar dan memeriksa progress pesanan dari halaman status.</p>
</div>
</div>
</div>
@ -116,9 +120,9 @@ store_page_start(
<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">Katalog Produk</span>
<h2 class="section-title">Pilihan cake, bread, dan pastry untuk toko demo.</h2>
<p class="section-copy mb-0">Gunakan katalog ini sebagai starting point sebelum menambahkan admin, stok, kupon, dan pembayaran real.</p>
<span class="eyebrow">Daftar Kue</span>
<h2 class="section-title">Pilihan cake, bread, dan pastry untuk pengguna.</h2>
<p class="section-copy mb-0">Gunakan filter kategori untuk melihat produk yang paling relevan sebelum ditambahkan ke keranjang belanja.</p>
</div>
<div class="d-flex flex-wrap gap-2 filter-pills" data-filter-bar>
<?php foreach ($categories as $key => $category): ?>
@ -170,33 +174,101 @@ store_page_start(
</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 sebagai base toko online.</h2>
<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>Pilih item</h3>
<p>Pelanggan lihat katalog, buka detail produk, lalu tambahkan item ke keranjang.</p>
<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>Checkout</h3>
<p>Isi nama, email, telepon, alamat, dan metode pembayaran. Order number dibuat otomatis.</p>
<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 order number dan email untuk melihat status pesanan dan instruksi pembayaran kapan saja.</p>
<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;

View File

@ -7,11 +7,16 @@ $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 !== '') {

253
store.php
View File

@ -15,6 +15,7 @@ if (basename((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === basename(__FILE__)
const STORE_CART_KEY = 'sekut_cart';
const STORE_FLASH_KEY = 'sekut_flash';
const STORE_LAST_ORDER_KEY = 'sekut_last_order';
const STORE_USER_KEY = 'sekut_user';
function app_env(string $key, string $fallback = ''): string
{
@ -41,6 +42,212 @@ function store_brand(): string
return 'SEKUT BAKERY';
}
function store_current_user(): ?array
{
$user = $_SESSION[STORE_USER_KEY] ?? null;
if (!is_array($user)) {
return null;
}
$id = (int)($user['id'] ?? 0);
$fullName = store_sanitize_line((string)($user['full_name'] ?? ''), 120);
$email = store_lower(trim((string)($user['email'] ?? '')));
if ($id <= 0 || $fullName === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return null;
}
return [
'id' => $id,
'full_name' => $fullName,
'email' => $email,
];
}
function store_is_logged_in(): bool
{
return store_current_user() !== null;
}
function store_user_first_name(string $fullName): string
{
$fullName = trim(preg_replace('/\s+/', ' ', $fullName) ?? '');
if ($fullName === '') {
return 'Pelanggan';
}
$parts = explode(' ', $fullName);
return store_substr((string)$parts[0], 0, 24);
}
function store_set_user_session(array $user): void
{
$_SESSION[STORE_USER_KEY] = [
'id' => (int)($user['id'] ?? 0),
'full_name' => store_sanitize_line((string)($user['full_name'] ?? ''), 120),
'email' => store_lower(trim((string)($user['email'] ?? ''))),
];
}
function store_logout_user(): void
{
unset($_SESSION[STORE_USER_KEY]);
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
}
}
function store_auth_password_error(string $password): string
{
if (store_strlen($password) < 8) {
return 'Password minimal 8 karakter.';
}
return '';
}
function store_register_user(array $source): array
{
store_ensure_schema();
$form = [
'full_name' => store_sanitize_line((string)($source['full_name'] ?? ''), 120),
'email' => store_lower(trim((string)($source['email'] ?? ''))),
];
$password = (string)($source['password'] ?? '');
$confirmPassword = (string)($source['confirm_password'] ?? '');
$errors = [];
if (store_strlen($form['full_name']) < 3) {
$errors['full_name'] = 'Nama lengkap minimal 3 karakter.';
}
if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Masukkan email yang valid.';
}
$passwordError = store_auth_password_error($password);
if ($passwordError !== '') {
$errors['password'] = $passwordError;
}
if ($confirmPassword === '') {
$errors['confirm_password'] = 'Ulangi password untuk konfirmasi.';
} elseif ($password !== $confirmPassword) {
$errors['confirm_password'] = 'Konfirmasi password belum cocok.';
}
if (!isset($errors['email'])) {
$exists = db()->prepare('SELECT id FROM customer_users WHERE email = :email LIMIT 1');
$exists->bindValue(':email', $form['email']);
$exists->execute();
if ($exists->fetch()) {
$errors['email'] = 'Email sudah terdaftar. Silakan login.';
}
}
if ($errors) {
return [
'success' => false,
'message' => 'Pendaftaran belum berhasil. Periksa kembali data Anda.',
'errors' => $errors,
'form' => $form,
];
}
$stmt = db()->prepare('INSERT INTO customer_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'],
];
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
}
store_set_user_session($user);
return [
'success' => true,
'message' => 'Akun berhasil dibuat. Anda sudah login.',
'user' => $user,
'form' => ['full_name' => '', 'email' => ''],
];
}
function store_login_user(array $source): array
{
store_ensure_schema();
$form = [
'email' => store_lower(trim((string)($source['email'] ?? ''))),
];
$password = (string)($source['password'] ?? '');
if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL) || $password === '') {
$errors = [];
if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Masukkan email yang valid.';
}
if ($password === '') {
$errors['password'] = 'Password wajib diisi.';
}
return [
'success' => false,
'message' => 'Masukkan email dan password yang benar.',
'errors' => $errors,
'form' => $form,
];
}
$stmt = db()->prepare('SELECT id, full_name, email, password_hash FROM customer_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 atau password tidak cocok.',
'errors' => [],
'form' => $form,
];
}
if (password_needs_rehash((string)($user['password_hash'] ?? ''), PASSWORD_DEFAULT)) {
$rehash = db()->prepare('UPDATE customer_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'], PDO::PARAM_INT);
$rehash->execute();
}
$touch = db()->prepare('UPDATE customer_users SET last_login_at = NOW() WHERE id = :id LIMIT 1');
$touch->bindValue(':id', (int)$user['id'], PDO::PARAM_INT);
$touch->execute();
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
}
store_set_user_session($user);
return [
'success' => true,
'message' => 'Login berhasil. Selamat datang kembali.',
'user' => [
'id' => (int)$user['id'],
'full_name' => (string)$user['full_name'],
'email' => (string)$user['email'],
],
'form' => ['email' => ''],
];
}
function store_categories(): array
{
return [
@ -417,9 +624,11 @@ function store_input_class(array $errors, string $field): string
function store_checkout_defaults(): array
{
$currentUser = store_current_user();
return [
'customer_name' => '',
'email' => '',
'customer_name' => (string)($currentUser['full_name'] ?? ''),
'email' => (string)($currentUser['email'] ?? ''),
'phone' => '',
'address' => '',
'note' => '',
@ -505,6 +714,19 @@ function store_ensure_schema(): void
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
db()->exec(
"CREATE TABLE IF NOT EXISTS customer_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,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_login_at TIMESTAMP NULL DEFAULT NULL,
INDEX idx_customer_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$ready = true;
}
@ -735,6 +957,7 @@ function store_page_start(string $title, string $description = '', array $option
}
$robots = !empty($options['noindex']) ? '<meta name="robots" content="noindex, nofollow" />' : '';
$cartCount = store_cart_count();
$currentUser = store_current_user();
echo '<!doctype html>';
echo '<html lang="id">';
@ -771,16 +994,23 @@ function store_page_start(string $title, string $description = '', array $option
echo ' <div class="container-xxl">';
echo ' <a class="navbar-brand brand-mark" href="index.php">';
echo ' <span class="brand-mark__title">' . h(store_brand()) . '</span>';
echo ' <span class="brand-mark__subtitle">Online bakery store</span>';
echo ' <span class="brand-mark__subtitle">Cake • Bread • Pastry</span>';
echo ' </a>';
echo ' <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#siteNav" aria-controls="siteNav" aria-expanded="false" aria-label="Toggle navigation">';
echo ' <span class="navbar-toggler-icon"></span>';
echo ' </button>';
echo ' <div class="collapse navbar-collapse" id="siteNav">';
echo ' <ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">';
echo store_nav_link('index.php', 'Katalog', $currentPath);
echo store_nav_link('cart.php', 'Keranjang', $currentPath);
echo store_nav_link('order_status.php', 'Status Pesanan', $currentPath);
echo store_nav_link('index.php', 'Home', $currentPath);
echo store_nav_link('index.php#catalog', 'Daftar Kue', $currentPath);
echo store_nav_link('index.php#payment-info', 'Info Pembayaran', $currentPath);
echo store_nav_link('index.php#contact', 'Kontak Kami', $currentPath);
if ($currentUser) {
echo store_nav_link('auth.php', 'Akun', $currentPath);
echo ' <li class="nav-item"><a class="btn btn-outline-secondary btn-sm" href="logout.php">Logout</a></li>';
} else {
echo store_nav_link('auth.php', 'Login / Register', $currentPath);
}
echo ' <li class="nav-item ms-lg-2">';
echo ' <a class="btn btn-dark btn-sm btn-cart" href="cart.php">Keranjang <span class="badge rounded-pill text-bg-light ms-2">' . h((string)$cartCount) . '</span></a>';
echo ' </li>';
@ -815,6 +1045,7 @@ function store_page_start(string $title, string $description = '', array $option
function store_page_end(): void
{
$jsVersion = file_exists(__DIR__ . '/assets/js/main.js') ? (string)filemtime(__DIR__ . '/assets/js/main.js') : (string)time();
$currentUser = store_current_user();
echo ' </div>';
echo '</main>';
@ -822,11 +1053,15 @@ function store_page_end(): void
echo ' <div class="container-xxl d-flex flex-column flex-lg-row justify-content-between gap-3 py-4">';
echo ' <div>';
echo ' <div class="footer-title">' . h(store_brand()) . '</div>';
echo ' <p class="footer-copy">MVP toko online dengan katalog, keranjang, checkout tersimpan, dan pelacakan status pesanan.</p>';
echo ' <p class="footer-copy">Tampilan user bakery dengan halaman home, daftar kue, info pembayaran, kontak, login user, dan tracking pesanan.</p>';
echo ' </div>';
echo ' <div class="footer-links d-flex flex-wrap gap-3">';
echo ' <a href="index.php#catalog">Lihat katalog</a>';
echo ' <a href="cart.php">Buka keranjang</a>';
echo ' <a href="index.php">Home</a>';
echo ' <a href="index.php#catalog">Daftar kue</a>';
echo ' <a href="index.php#payment-info">Info pembayaran</a>';
echo ' <a href="index.php#contact">Kontak kami</a>';
echo ' <a href="' . h($currentUser ? 'auth.php' : 'auth.php') . '">' . h($currentUser ? 'Akun saya' : 'Login / Register') . '</a>';
echo ' <a href="use_case.php">Use case diagram</a>';
echo ' <a href="order_status.php">Lacak pesanan</a>';
echo ' </div>';
echo ' </div>';

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(); ?>