Compare commits

...

1 Commits
main ... ai-dev

Author SHA1 Message Date
Flatlogic Bot
8c2a5d487c POSFuku_v2 2026-04-19 12:46:29 +00:00
12 changed files with 1598 additions and 7 deletions

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

View File

@ -8,6 +8,7 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>POS - Riwayat Belanja</title> <title>POS - Riwayat Belanja</title>
<script src="/local-preview-bridge.js"></script>
<style> <style>
body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; color: #333; padding-bottom: 40px; -webkit-font-smoothing: antialiased; } body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; color: #333; padding-bottom: 40px; -webkit-font-smoothing: antialiased; }
.header { background:#111; color:white; padding:15px; text-align:center; position:sticky; top:0; z-index:100; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .header { background:#111; color:white; padding:15px; text-align:center; position:sticky; top:0; z-index:100; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }

View File

@ -8,6 +8,7 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>POS - DAPUR</title> <title>POS - DAPUR</title>
<script src="/local-preview-bridge.js"></script>
<style> <style>
body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; padding: 12px; -webkit-font-smoothing: antialiased; } body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; padding: 12px; -webkit-font-smoothing: antialiased; }
.nav { background:#111; color:white; padding:12px; border-radius:12px; margin-bottom:15px; display:flex; justify-content:space-between; align-items:center; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .nav { background:#111; color:white; padding:12px; border-radius:12px; margin-bottom:15px; display:flex; justify-content:space-between; align-items:center; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }

View File

@ -628,7 +628,7 @@
<button class="btn-dark" onclick="calcAdd('4')">4</button><button class="btn-dark" onclick="calcAdd('5')">5</button><button class="btn-dark" onclick="calcAdd('6')">6</button><button class="btn-main" onclick="calcAdd('*')">*</button> <button class="btn-dark" onclick="calcAdd('4')">4</button><button class="btn-dark" onclick="calcAdd('5')">5</button><button class="btn-dark" onclick="calcAdd('6')">6</button><button class="btn-main" onclick="calcAdd('*')">*</button>
<button class="btn-dark" onclick="calcAdd('1')">1</button><button class="btn-dark" onclick="calcAdd('2')">2</button><button class="btn-dark" onclick="calcAdd('3')">3</button><button class="btn-main" onclick="calcAdd('-')">-</button> <button class="btn-dark" onclick="calcAdd('1')">1</button><button class="btn-dark" onclick="calcAdd('2')">2</button><button class="btn-dark" onclick="calcAdd('3')">3</button><button class="btn-main" onclick="calcAdd('-')">-</button>
<button class="btn-dark" onclick="calcAdd('0')">0</button><button class="btn-dark" onclick="calcAdd('.')">.</button><button class="btn-green" onclick="calcEval()">=</button><button class="btn-main" onclick="calcAdd('+')">+</button> <button class="btn-dark" onclick="calcAdd('0')">0</button><button class="btn-dark" onclick="calcAdd('.')">.</button><button class="btn-green" onclick="calcEval()">=</button><button class="btn-main" onclick="calcAdd('+')">+</button>
<button class="btn-dark" style="grid-column: span 4" onclick="document.getElementById('calc-input').value='';document.getElementById('calc-result').innerText='= 0'">CLEAR</button> <button class="btn-dark" style="grid-column: span 4" onclick="document.getElementById('calc-input').value='';document.getElementById('calc-result').innerText='= 0'">BERSIHKAN</button>
</div> </div>
</div> </div>
@ -645,7 +645,7 @@
<div id="sec-dapur" class="page"> <div id="sec-dapur" class="page">
<div class="card"> <div class="card">
<div style="display:flex; justify-content:space-between; align-items:center;"> <div style="display:flex; justify-content:space-between; align-items:center;">
<div class="title-sm" style="margin:0;">Kitchen Display System (Dapur)</div> <div class="title-sm" style="margin:0;">Sistem Tampilan Dapur</div>
<button class="pill on" style="width:auto; padding:6px 12px;" onclick="syncData(false)">REFRESH PESANAN</button> <button class="pill on" style="width:auto; padding:6px 12px;" onclick="syncData(false)">REFRESH PESANAN</button>
</div> </div>
<div id="dapur-list" style="margin-top:15px; display:grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap:12px;"></div> <div id="dapur-list" style="margin-top:15px; display:grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap:12px;"></div>
@ -842,7 +842,7 @@
<div class="card"> <div class="card">
<div class="title-sm">Simulasi Paket Kustom</div> <div class="title-sm">Simulasi Paket Kustom</div>
<div style="margin-bottom: 15px;"> <div style="margin-bottom: 15px;">
<small>Nama Paket Simulation</small> <small>Nama Simulasi Paket</small>
<input type="text" id="custom-paket-name" placeholder="Contoh: Simulasi Paket A"> <input type="text" id="custom-paket-name" placeholder="Contoh: Simulasi Paket A">
</div> </div>
@ -873,12 +873,12 @@
<input type="number" id="custom-paket-harga" placeholder="Contoh: 50000" style="width:100%; padding:10px; border-radius:10px; border:1px solid #ddd; font-size:16px; font-weight:bold;"> <input type="number" id="custom-paket-harga" placeholder="Contoh: 50000" style="width:100%; padding:10px; border-radius:10px; border:1px solid #ddd; font-size:16px; font-weight:bold;">
<div style="font-size:10px; color:#666; margin-top:4px;">Kosongkan jika harga paket sama dengan total harga asli.</div> <div style="font-size:10px; color:#666; margin-top:4px;">Kosongkan jika harga paket sama dengan total harga asli.</div>
</div> </div>
<button class="btn-main" style="margin-top: 15px;" onclick="saveCustomPaket()">SIMPAN SIMULASI PAKET</button> <button class="btn-main" style="margin-top: 15px;" onclick="saveCustomPaket()">SIMPAN PAKET SIMULASI</button>
<button class="btn-dark" style="margin-top: 8px; background: #666;" onclick="clearPaketSimulation()">RESET PILIHAN</button> <button class="btn-dark" style="margin-top: 8px; background: #666;" onclick="clearPaketSimulation()">RESET PILIHAN</button>
</div> </div>
<div class="card"> <div class="card">
<div class="title-sm">Daftar Simulasi Tersimpan</div> <div class="title-sm">Daftar Paket Simulasi Tersimpan</div>
<div id="saved-paket-list" style="max-height: 300px; overflow-y: auto;"> <div id="saved-paket-list" style="max-height: 300px; overflow-y: auto;">
<!-- Saved packages will be rendered here --> <!-- Saved packages will be rendered here -->
</div> </div>
@ -983,7 +983,7 @@
}; };
request.onsuccess = function(e) { request.onsuccess = function(e) {
db = e.target.result; db = e.target.result;
console.log("IndexedDB Ready"); console.log("IndexedDB siap");
if (callback) callback(); if (callback) callback();
}; };
request.onerror = function(e) { console.error("IndexedDB Error", e); }; request.onerror = function(e) { console.error("IndexedDB Error", e); };
@ -1438,7 +1438,7 @@
return; return;
} }
var target = prompt('Reset Password User\n\nUsername target:', ''); var target = prompt('Reset Password Pengguna\n\nUsername target:', '');
if (target === null) return; if (target === null) return;
target = String(target || '').trim(); target = String(target || '').trim();
if (!target) { alert('Username target tidak boleh kosong.'); return; } if (!target) { alert('Username target tidak boleh kosong.'); return; }

View File

@ -5,6 +5,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Menu Pelanggan - POS</title> <title>Menu Pelanggan - POS</title>
<script src="/local-preview-bridge.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;800;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script> <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<style> <style>

View File

@ -8,6 +8,7 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>POS - Cek Poin & Riwayat</title> <title>POS - Cek Poin & Riwayat</title>
<script src="/local-preview-bridge.js"></script>
<style> <style>
body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; color: #333; padding-bottom: 40px; -webkit-font-smoothing: antialiased; } body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; color: #333; padding-bottom: 40px; -webkit-font-smoothing: antialiased; }
.header { background:#111; color:white; padding:15px; text-align:center; position:sticky; top:0; z-index:100; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .header { background:#111; color:white; padding:15px; text-align:center; position:sticky; top:0; z-index:100; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }

View File

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fuku - Stok & Belanja</title> <title>Fuku - Stok & Belanja</title>
<script src="/local-preview-bridge.js"></script>
<style> <style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f8fafc; color: #1e293b; -webkit-font-smoothing: antialiased; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f8fafc; color: #1e293b; -webkit-font-smoothing: antialiased; }
.header { background: #111; color: white; padding: 12px 15px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .header { background: #111; color: white; padding: 12px 15px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }

View File

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard Laporan Fuku</title> <title>Dashboard Laporan Fuku</title>
<script src="/local-preview-bridge.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f0f2f5; margin: 0; color: #333; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f0f2f5; margin: 0; color: #333; }

17
db/config.php Normal file
View File

@ -0,0 +1,17 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_39735');
define('DB_USER', 'app_39735');
define('DB_PASS', '619a836d-9eea-474a-b180-1f8ed80b6b05');
function db() {
static $pdo;
if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}

696
index.html Normal file
View File

@ -0,0 +1,696 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>POSFuku | ERP Restoran</title>
<meta name="description" content="POSFuku adalah halaman masuk ERP restoran untuk kasir, dapur, pelanggan, stok, belanja, dan rekap operasional.">
<style>
:root {
--bg: #0f172a;
--bg-2: #111827;
--panel: rgba(255,255,255,.08);
--panel-strong: rgba(15,23,42,.72);
--line: rgba(255,255,255,.12);
--text: #f8fafc;
--muted: #cbd5e1;
--soft: #94a3b8;
--primary: #fb7185;
--primary-2: #e11d48;
--accent: #f59e0b;
--ok: #22c55e;
--shadow: 0 24px 70px rgba(2, 6, 23, .35);
--radius-xl: 28px;
--radius-lg: 20px;
--radius-md: 16px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(251, 113, 133, .22), transparent 26%),
radial-gradient(circle at top right, rgba(245, 158, 11, .18), transparent 22%),
linear-gradient(140deg, #020617 0%, var(--bg) 40%, var(--bg-2) 100%);
min-height: 100vh;
}
a { color: inherit; }
.wrap { max-width: 1200px; margin: 0 auto; padding: 26px 18px 52px; }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
padding: 14px 18px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(15, 23, 42, .62);
backdrop-filter: blur(14px);
box-shadow: var(--shadow);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.logo {
width: 44px;
height: 44px;
border-radius: 14px;
display: grid;
place-items: center;
font-weight: 900;
color: white;
background: linear-gradient(135deg, var(--primary), var(--primary-2));
box-shadow: 0 14px 28px rgba(225, 29, 72, .35);
flex: 0 0 auto;
}
.brand small {
display: block;
color: var(--soft);
letter-spacing: .08em;
text-transform: uppercase;
font-weight: 800;
font-size: 11px;
margin-bottom: 2px;
}
.brand strong {
display: block;
font-size: 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.top-links {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
}
.chip-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 0 14px;
border-radius: 999px;
text-decoration: none;
font-weight: 800;
font-size: 13px;
border: 1px solid var(--line);
background: rgba(255,255,255,.06);
color: var(--text);
}
.chip-link.primary {
background: linear-gradient(135deg, var(--primary), var(--primary-2));
border-color: transparent;
box-shadow: 0 14px 28px rgba(225, 29, 72, .3);
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(320px, .95fr);
gap: 22px;
margin-bottom: 22px;
}
.panel {
border: 1px solid var(--line);
border-radius: var(--radius-xl);
background: var(--panel);
backdrop-filter: blur(14px);
box-shadow: var(--shadow);
}
.hero-main {
padding: 34px;
position: relative;
overflow: hidden;
}
.hero-main::after {
content: "";
position: absolute;
right: -70px;
bottom: -90px;
width: 260px;
height: 260px;
border-radius: 50%;
background: radial-gradient(circle, rgba(251,113,133,.22) 0%, transparent 70%);
pointer-events: none;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(251,113,133,.12);
color: #fecdd3;
font-size: 12px;
font-weight: 900;
letter-spacing: .08em;
text-transform: uppercase;
}
h1 {
margin: 18px 0 12px;
font-size: clamp(34px, 5.7vw, 56px);
line-height: 1.04;
max-width: 760px;
}
.lead {
margin: 0;
color: var(--muted);
font-size: 18px;
line-height: 1.65;
max-width: 760px;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 26px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 50px;
padding: 0 18px;
border-radius: 16px;
text-decoration: none;
font-weight: 900;
transition: transform .14s ease, opacity .14s ease;
}
.btn:hover,
.chip-link:hover,
.module-card:hover,
.step:hover,
.quick-card:hover {
transform: translateY(-2px);
}
.btn.primary {
color: #fff;
background: linear-gradient(135deg, var(--primary), var(--primary-2));
box-shadow: 0 16px 32px rgba(225, 29, 72, .28);
}
.btn.secondary {
color: #fff;
border: 1px solid var(--line);
background: rgba(255,255,255,.07);
}
.hero-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 22px;
}
.quick-card,
.stat,
.module-card,
.step,
.help-card { transition: transform .14s ease, border-color .14s ease; }
.quick-card {
padding: 16px;
border-radius: 18px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(15,23,42,.42);
}
.quick-card strong,
.stat .value,
.module-card h2,
.step strong,
.help-card h3 { display: block; }
.quick-card strong { font-size: 15px; margin-bottom: 4px; }
.quick-card span,
.stat .label,
.module-card p,
.step p,
.help-card p,
.help-card li { color: var(--muted); }
.hero-side {
padding: 28px;
display: flex;
flex-direction: column;
gap: 18px;
}
.section-kicker {
margin: 0;
color: #fde68a;
font-size: 12px;
font-weight: 900;
letter-spacing: .08em;
text-transform: uppercase;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.stat {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(15,23,42,.5);
}
.stat .label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--soft);
margin-bottom: 6px;
}
.stat .value {
font-size: 22px;
font-weight: 900;
line-height: 1.15;
}
.note {
padding: 16px;
border-radius: 18px;
background: rgba(34,197,94,.12);
border: 1px solid rgba(34,197,94,.22);
color: #dcfce7;
line-height: 1.6;
font-size: 14px;
}
.section-head {
display: flex;
align-items: end;
justify-content: space-between;
gap: 14px;
margin: 28px 0 16px;
}
.section-head h2 {
margin: 0;
font-size: clamp(24px, 4vw, 34px);
}
.section-head p {
margin: 6px 0 0;
color: var(--muted);
max-width: 760px;
line-height: 1.65;
}
.module-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.module-card {
display: flex;
flex-direction: column;
min-height: 100%;
padding: 22px;
border-radius: 22px;
border: 1px solid var(--line);
background: rgba(255,255,255,.06);
box-shadow: var(--shadow);
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
width: fit-content;
min-height: 30px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 900;
letter-spacing: .04em;
background: rgba(255,255,255,.08);
border: 1px solid var(--line);
color: #e2e8f0;
}
.module-card h2 { margin: 16px 0 10px; font-size: 22px; }
.module-card p { margin: 0 0 16px; line-height: 1.62; min-height: 78px; }
.module-meta { margin-top: auto; display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.module-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.text-link {
color: #fda4af;
text-decoration: none;
font-weight: 800;
font-size: 14px;
}
.steps {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
margin-top: 6px;
}
.step {
padding: 22px;
border-radius: 22px;
border: 1px solid var(--line);
background: rgba(15,23,42,.42);
}
.step .num {
width: 42px;
height: 42px;
border-radius: 14px;
display: grid;
place-items: center;
font-weight: 900;
margin-bottom: 14px;
background: linear-gradient(135deg, rgba(251,113,133,.24), rgba(245,158,11,.18));
border: 1px solid rgba(251,255,255,.12);
}
.step strong { font-size: 18px; margin-bottom: 8px; }
.step p { margin: 0; line-height: 1.65; }
.help-grid {
display: grid;
grid-template-columns: 1.05fr .95fr;
gap: 18px;
margin-top: 22px;
}
.help-card {
padding: 24px;
border-radius: 22px;
border: 1px solid var(--line);
background: rgba(255,255,255,.06);
box-shadow: var(--shadow);
}
.help-card h3 { margin: 0 0 8px; font-size: 22px; }
.help-card p { margin: 0 0 14px; line-height: 1.65; }
.help-card ul { margin: 0; padding-left: 18px; line-height: 1.75; }
.footer {
margin-top: 26px;
padding: 18px 20px;
border-radius: 22px;
border: 1px solid var(--line);
background: rgba(15,23,42,.46);
color: var(--soft);
line-height: 1.7;
font-size: 14px;
text-align: center;
}
@media (max-width: 1040px) {
.hero,
.module-grid,
.steps,
.help-grid { grid-template-columns: 1fr; }
.module-card p { min-height: 0; }
}
@media (max-width: 720px) {
.wrap { padding: 16px 14px 36px; }
.topbar,
.hero-main,
.hero-side,
.module-card,
.step,
.help-card { padding: 20px; }
.topbar { border-radius: 24px; }
.top-links,
.hero-actions,
.module-actions { flex-direction: column; }
.hero-list,
.stat-grid { grid-template-columns: 1fr; }
.btn,
.chip-link { width: 100%; }
.section-head { align-items: start; }
}
</style>
</head>
<body>
<main class="wrap">
<header class="topbar">
<div class="brand">
<div class="logo">PF</div>
<div>
<small>ERP Restoran</small>
<strong>POSFuku Workspace</strong>
</div>
</div>
<nav class="top-links" aria-label="Navigasi cepat">
<a class="chip-link" href="#modul">Daftar Modul</a>
<a class="chip-link" href="#panduan">Cara Mulai</a>
<a class="chip-link primary" href="/POSFuku/Index.html">Buka Kasir</a>
</nav>
</header>
<section class="hero">
<div class="panel hero-main">
<div class="eyebrow">Satu pintu untuk operasional restoran</div>
<h1>Kelola kasir, dapur, stok, belanja, pelanggan, dan rekap dari satu halaman yang mudah dibuka.</h1>
<p class="lead">Halaman utama ini dirapikan khusus untuk kebutuhan bisnis restoran. Anda bisa langsung masuk ke modul kasir, memantau antrean dapur, meninjau stok dan pembelian, lalu melihat rekap operasional tanpa harus mencari file satu per satu.</p>
<div class="hero-actions">
<a class="btn primary" href="/POSFuku/Index.html">Masuk ke POS Kasir</a>
<a class="btn secondary" href="/POSFuku/Stok%20%26%20Belanja/Index.html">Buka Stok &amp; Pembelian</a>
<a class="btn secondary" href="/RekapTransaksi/Index.html">Lihat Dashboard Rekap</a>
</div>
<div class="hero-list" aria-label="Keunggulan utama">
<div class="quick-card">
<strong>Alur kerja lebih jelas</strong>
<span>Kasir, dapur, pelanggan, dan back office terhubung dari satu beranda.</span>
</div>
<div class="quick-card">
<strong>Ramah untuk non-teknis</strong>
<span>Tiap modul diberi nama, fungsi, dan tombol masuk yang mudah dikenali.</span>
</div>
<div class="quick-card">
<strong>Aman untuk demo lokal</strong>
<span>Mode preview tetap dapat dijelajahi walau backend Google Apps Script belum aktif.</span>
</div>
<div class="quick-card">
<strong>Siap untuk operasional harian</strong>
<span>Mulai dari pesanan meja sampai pemantauan belanja dan laporan.</span>
</div>
</div>
</div>
<aside class="panel hero-side">
<div>
<p class="section-kicker">Ringkasan cepat</p>
<div class="stat-grid">
<div class="stat">
<span class="label">Jenis bisnis</span>
<span class="value">Restoran</span>
</div>
<div class="stat">
<span class="label">Tipe aplikasi</span>
<span class="value">ERP operasional</span>
</div>
<div class="stat">
<span class="label">Jumlah modul utama</span>
<span class="value">7 modul</span>
</div>
<div class="stat">
<span class="label">Mode saat ini</span>
<span class="value">Preview lokal</span>
</div>
</div>
</div>
<div class="note">
<strong>Catatan preview:</strong> versi yang berjalan di VM ini adalah mode pratinjau lokal. Halaman tetap bisa dieksplorasi untuk demo, sedangkan data bisnis nyata dan sinkronisasi penuh tetap memerlukan backend Google Apps Script saat dihubungkan nanti.
</div>
</aside>
</section>
<section id="modul" aria-labelledby="judul-modul">
<div class="section-head">
<div>
<h2 id="judul-modul">Modul operasional</h2>
<p>Pilih modul sesuai alur kerja restoran Anda. Setiap kartu di bawah ini sudah diarahkan ke halaman yang tepat supaya staf atau pemilik usaha bisa langsung mulai.</p>
</div>
</div>
<div class="module-grid">
<article class="module-card">
<span class="badge">Front Office</span>
<h2>POS Kasir</h2>
<p>Mencatat pesanan meja, checkout, metode pembayaran, pelanggan, riwayat transaksi, hingga operasional kasir dalam satu layar kerja utama.</p>
<div class="module-meta">
<span class="badge">Pesanan</span>
<span class="badge">Pembayaran</span>
<span class="badge">Riwayat</span>
</div>
<div class="module-actions">
<a class="btn primary" href="/POSFuku/Index.html">Buka POS</a>
</div>
</article>
<article class="module-card">
<span class="badge">Produksi</span>
<h2>Dapur</h2>
<p>Memantau antrean pesanan aktif, memisahkan item prioritas, dan berkomunikasi dengan kasir saat pesanan siap diantar ke meja.</p>
<div class="module-meta">
<span class="badge">Antrean</span>
<span class="badge">Status siap</span>
<span class="badge">Chat internal</span>
</div>
<div class="module-actions">
<a class="btn primary" href="/POSFuku/Dapur.html">Buka Dapur</a>
</div>
</article>
<article class="module-card">
<span class="badge">Layanan pelanggan</span>
<h2>Pelanggan &amp; Poin</h2>
<p>Menangani pesanan pelanggan, review, bukti promo, dan pengecekan poin loyalitas dengan alur yang lebih aman di mode preview.</p>
<div class="module-meta">
<span class="badge">Customer</span>
<span class="badge">Review</span>
<span class="badge">Loyalitas</span>
</div>
<div class="module-actions">
<a class="btn primary" href="/POSFuku/Pelanggan.html">Buka Halaman Pelanggan</a>
<a class="text-link" href="/POSFuku/Poin.html">Lihat modul poin</a>
</div>
</article>
<article class="module-card">
<span class="badge">Back Office</span>
<h2>Belanja Harian</h2>
<p>Mencatat pembelian harian, impor data belanja, sinkron satu arah, dan menjaga daftar pengeluaran tetap rapi untuk kebutuhan operasional.</p>
<div class="module-meta">
<span class="badge">Belanja</span>
<span class="badge">Supplier</span>
<span class="badge">Sinkron</span>
</div>
<div class="module-actions">
<a class="btn primary" href="/POSFuku/Belanja.html">Buka Belanja</a>
</div>
</article>
<article class="module-card">
<span class="badge">Inventori</span>
<h2>Stok &amp; Pembelian</h2>
<p>Mengelola stok bahan, belanja, operasional, pegawai, supplier, dan pengaturan pendukung lain dari satu dashboard inventori.</p>
<div class="module-meta">
<span class="badge">Stok</span>
<span class="badge">Operasional</span>
<span class="badge">Pegawai</span>
</div>
<div class="module-actions">
<a class="btn primary" href="/POSFuku/Stok%20%26%20Belanja/Index.html">Buka Stok &amp; Pembelian</a>
</div>
</article>
<article class="module-card">
<span class="badge">Analitik</span>
<h2>Rekap Transaksi</h2>
<p>Melihat rekap harian atau rentang tanggal, grafik, pengeluaran, catatan kalender, serta ringkasan performa penjualan restoran.</p>
<div class="module-meta">
<span class="badge">Dashboard</span>
<span class="badge">Grafik</span>
<span class="badge">Laporan</span>
</div>
<div class="module-actions">
<a class="btn primary" href="/RekapTransaksi/Index.html">Buka Rekap</a>
</div>
</article>
</div>
</section>
<section id="panduan" aria-labelledby="judul-panduan">
<div class="section-head">
<div>
<h2 id="judul-panduan">Cara mulai untuk pemilik restoran</h2>
<p>Jika Anda belum punya pengalaman web development, gunakan urutan sederhana ini untuk mulai mencoba aplikasi dengan nyaman.</p>
</div>
</div>
<div class="steps">
<article class="step">
<div class="num">1</div>
<strong>Mulai dari kasir</strong>
<p>Buka modul POS Kasir untuk melihat alur pesanan, meja, checkout, dan riwayat. Ini modul inti yang paling mudah dipahami lebih dulu.</p>
</article>
<article class="step">
<div class="num">2</div>
<strong>Lanjut ke dapur &amp; stok</strong>
<p>Setelah memahami alur pesanan, buka Dapur dan Stok &amp; Pembelian untuk melihat bagaimana pesanan diteruskan ke produksi dan kebutuhan bahan.</p>
</article>
<article class="step">
<div class="num">3</div>
<strong>Tutup dengan rekap</strong>
<p>Gunakan Rekap Transaksi untuk menilai penjualan, belanja, dan catatan operasional agar keputusan bisnis bisa diambil lebih cepat.</p>
</article>
</div>
</section>
<section class="help-grid" aria-label="Bantuan penggunaan">
<article class="help-card">
<h3>Yang sudah saya rapikan</h3>
<p>Beranda ini dibuat lebih jelas untuk penggunaan restoran dan lebih nyaman bagi pengguna non-teknis.</p>
<ul>
<li>Navigasi modul disusun ulang supaya fungsi tiap halaman langsung terlihat.</li>
<li>Bahasa pada elemen baru dibuat konsisten ke Bahasa Indonesia.</li>
<li>Mode preview lokal dibuat lebih informatif agar tetap berguna saat backend belum tersambung.</li>
<li>Audit modul awal difokuskan ke POS, Dapur, dan Stok untuk mengurangi titik bingung saat demo.</li>
</ul>
</article>
<article class="help-card">
<h3>Saran alur uji coba</h3>
<p>Untuk demo ke tim restoran, biasanya urutan berikut paling mudah dipahami.</p>
<ul>
<li>Buka <strong>POS Kasir</strong> untuk simulasi input pesanan dan pembayaran.</li>
<li>Buka <strong>Dapur</strong> untuk melihat antrean dan status siap saji.</li>
<li>Buka <strong>Stok &amp; Pembelian</strong> untuk stok bahan dan pengeluaran.</li>
<li>Buka <strong>Rekap</strong> untuk melihat gambaran hasil operasional.</li>
</ul>
</article>
</section>
<div class="footer">
POSFuku berjalan di halaman ini sebagai pusat akses modul restoran. Jika nanti Anda ingin, beranda ini juga bisa dikembangkan lagi menjadi dashboard pemilik dengan KPI harian, shortcut staf, dan ringkasan penjualan otomatis.
</div>
</main>
</body>
</html>

872
local-preview-bridge.js Normal file
View File

@ -0,0 +1,872 @@
(function () {
var host = window.location.hostname || '';
var isGAS = /script\.google\.|googleusercontent\.com/.test(host);
if (isGAS) return;
var STORAGE_KEY = '__posfuku_local_preview_state_v3__';
var today = formatYmd(new Date());
var yesterday = shiftDate(today, -1);
var twoDaysAgo = shiftDate(today, -2);
function pad(n) { return String(n).padStart(2, '0'); }
function formatYmd(date) {
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
}
function shiftDate(ymd, delta) {
var d = ymd ? new Date(ymd + 'T00:00:00') : new Date();
d.setDate(d.getDate() + Number(delta || 0));
return formatYmd(d);
}
function makeTs(ymd, hm) {
return String(ymd) + ' ' + String(hm || '09:00') + ':00';
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function toRp(n) {
return Number(n || 0);
}
function escHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function svgDataUri(label, bg, fg) {
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="720" height="420" viewBox="0 0 720 420">' +
'<rect width="720" height="420" fill="' + (bg || '#111827') + '"/>' +
'<circle cx="610" cy="110" r="72" fill="rgba(255,255,255,0.08)"/>' +
'<circle cx="120" cy="320" r="96" fill="rgba(255,255,255,0.06)"/>' +
'<text x="50%" y="46%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, sans-serif" font-size="38" font-weight="700" fill="' + (fg || '#ffffff') + '">' + escHtml(label || 'POSFuku') + '</text>' +
'<text x="50%" y="58%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" fill="rgba(255,255,255,0.78)">Aset pratinjau lokal</text>' +
'</svg>';
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
}
function sum(list, key) {
return (list || []).reduce(function (acc, item) { return acc + Number(item && item[key] || 0); }, 0);
}
function normalizeWa(wa) {
var clean = String(wa || '').replace(/\D/g, '');
if (!clean) return '';
if (clean.indexOf('0') === 0) clean = '62' + clean.slice(1);
else if (clean.indexOf('8') === 0) clean = '62' + clean;
return clean;
}
function isCountedTransaction(t) {
var status = String((t && t.status) || '').toLowerCase();
return status === 'selesai' || status === 'review' || status === 'ready' || status === 'paid';
}
function inRange(dateStr, from, to) {
if (!dateStr) return false;
if (from && dateStr < from) return false;
if (to && dateStr > to) return false;
return true;
}
function nextId(prefix, list) {
var max = 0;
(list || []).forEach(function (item) {
var match = String((item && item.id) || '').match(/(\d+)$/);
if (match) max = Math.max(max, Number(match[1] || 0));
});
return prefix + '-' + String(max + 1).padStart(3, '0');
}
function baseState() {
var qris = svgDataUri('POSFuku QRIS', '#0f172a', '#ffffff');
return {
settings: {
store_name: 'POSFuku Demo Restoran',
store_address: 'Jl. Preview Lokal No. 12, Jakarta',
store_whatsapp: '6281234567890',
social_instagram_url: '#',
social_tiktok_url: '#',
social_gmaps_url: '#',
social_linktree_url: '#',
wifi_lock: 'false',
qris_image_url: qris,
wifi_ssid: 'POSFuku-Guest',
wifi_password: 'preview123'
},
menu: [
{ nama: 'Tom Yum Seafood', kategori: 'Ala Carte', harga: 38000, minStok: 6, gambar: svgDataUri('Tom Yum Seafood', '#b91c1c', '#fff') },
{ nama: 'Nasi Putih', kategori: 'Ala Carte', harga: 6000, minStok: 20, gambar: svgDataUri('Nasi Putih', '#7c2d12', '#fff') },
{ nama: 'Thai Tea', kategori: 'Drinks', harga: 18000, minStok: 8, gambar: svgDataUri('Thai Tea', '#ea580c', '#fff') },
{ nama: 'Es Jeruk', kategori: 'Drinks', harga: 14000, minStok: 8, gambar: svgDataUri('Es Jeruk', '#d97706', '#fff') },
{ nama: 'Paket Hemat 2 Pax', kategori: 'Paket', harga: 69000, minStok: 3, gambar: svgDataUri('Paket Hemat', '#be123c', '#fff') },
{ nama: 'Ayam Sambal Matah', kategori: 'Ala Carte', harga: 32000, minStok: 5, gambar: svgDataUri('Ayam Sambal Matah', '#7f1d1d', '#fff') }
],
pelanggan: [
{ nama: 'Alya', wa: '6281234567890', poin: 28 },
{ nama: 'Bima', wa: '6289876543210', poin: 12 },
{ nama: 'Citra', wa: '628111223344', poin: 7 }
],
transaksi: [
{
id: 'TRX-101', nama: 'Alya', wa: '6281234567890', meja: 'A3', status: 'Pending',
tgl: today, timestamp: makeTs(today, '10:15'), metodeBayar: 'QRIS',
items: [
{ nama: 'Thai Tea', qty: 2, harga: 18000, kat: 'Drinks' },
{ nama: 'Tom Yum Seafood', qty: 1, harga: 38000, kat: 'Ala Carte' }
],
catatan: 'Tom yum pedas sedang', total: 74000, poinDipakai: 0, poinDapat: 7
},
{
id: 'TRX-102', nama: 'Bima', wa: '6289876543210', meja: 'B1', status: 'Ready',
tgl: today, timestamp: makeTs(today, '11:05'), metodeBayar: 'Tunai',
items: [
{ nama: 'Ayam Sambal Matah', qty: 1, harga: 32000, kat: 'Ala Carte' },
{ nama: 'Nasi Putih', qty: 2, harga: 6000, kat: 'Ala Carte' },
{ nama: 'Es Jeruk', qty: 2, harga: 14000, kat: 'Drinks' }
],
catatan: '', total: 72000, poinDipakai: 0, poinDapat: 7
},
{
id: 'TRX-103', nama: 'Alya', wa: '6281234567890', meja: 'A3', status: 'Selesai',
tgl: yesterday, timestamp: makeTs(yesterday, '19:20'), metodeBayar: 'Debit',
items: [
{ nama: 'Tom Yum Seafood', qty: 1, harga: 38000, kat: 'Ala Carte' },
{ nama: 'Nasi Putih', qty: 2, harga: 6000, kat: 'Ala Carte' },
{ nama: 'Thai Tea', qty: 1, harga: 18000, kat: 'Drinks' }
],
catatan: 'Ulang tahun kecil-kecilan', total: 68000, poinDipakai: 0, poinDapat: 7
},
{
id: 'TRX-104', nama: 'Alya', wa: '6281234567890', meja: 'A3', status: 'Review',
tgl: today, timestamp: makeTs(today, '09:10'), metodeBayar: '-',
items: [], catatan: 'Review Google sudah diupload', total: 0,
poinDipakai: 0, poinDapat: 5,
buktiReview: svgDataUri('Bukti Review', '#15803d', '#fff')
},
{
id: 'TRX-105', nama: 'Citra', wa: '628111223344', meja: 'C2', status: 'Selesai',
tgl: twoDaysAgo, timestamp: makeTs(twoDaysAgo, '13:40'), metodeBayar: 'QRIS',
items: [
{ nama: 'Paket Hemat 2 Pax', qty: 1, harga: 69000, kat: 'Paket' },
{ nama: 'Es Jeruk', qty: 1, harga: 14000, kat: 'Drinks' }
],
catatan: 'Take away', total: 83000, poinDipakai: 0, poinDapat: 8
}
],
belanja: [
{ id: 'BL-001', nama: 'Udang', harga: 95000, qty: 2, total: 190000, kategori: 'BAHAN BASAH', catatan: 'Pagi', tgl: today, tanggal: today, timestamp: makeTs(today, '07:40') },
{ id: 'BL-002', nama: 'Teh Thailand', harga: 85000, qty: 1, total: 85000, kategori: 'BAHAN KERING', catatan: 'Supplier utama', tgl: today, tanggal: today, timestamp: makeTs(today, '08:10') },
{ id: 'BL-003', nama: 'Cup plastik', harga: 45000, qty: 3, total: 135000, kategori: 'NON MAKANAN', catatan: '', tgl: yesterday, tanggal: yesterday, timestamp: makeTs(yesterday, '09:00') }
],
stok: [
{ id: 'ST-001', tanggal: today, lokasi: 'Lantai Atas', menu: 'Nasi Putih', stokAwal: 35, restock: 15, terpakai: 18, sisa: 32, foto: '' },
{ id: 'ST-002', tanggal: today, lokasi: 'Lantai Atas', menu: 'Thai Tea', stokAwal: 12, restock: 6, terpakai: 7, sisa: 11, foto: '' },
{ id: 'ST-003', tanggal: today, lokasi: 'Lantai Bawah', menu: 'Es Jeruk', stokAwal: 10, restock: 4, terpakai: 8, sisa: 6, foto: '' },
{ id: 'ST-004', tanggal: today, lokasi: 'Showcase', menu: 'Tom Yum Seafood', stokAwal: 8, restock: 3, terpakai: 5, sisa: 6, foto: '' }
],
operasional: [
{ id: 'OP-001', nama: 'Gas LPG', harga: 220000, qty: 1, total: 220000, catatan: 'Ganti tabung', tanggal: today, tgl: today },
{ id: 'OP-002', nama: 'Kebersihan', harga: 75000, qty: 1, total: 75000, catatan: 'Sabun + pel', tanggal: yesterday, tgl: yesterday }
],
pegawaiList: [
{ id: 'PL-001', nama: 'Rina', status: 'Aktif', catatan: 'Kasir' },
{ id: 'PL-002', nama: 'Dimas', status: 'Aktif', catatan: 'Dapur' },
{ id: 'PL-003', nama: 'Sari', status: 'Aktif', catatan: 'Service' }
],
pegawai: [
{ id: 'PG-001', nama: 'Rina', nilai: 120000, kategori: 'Gaji', catatan: 'Shift pagi', tanggal: today, tgl: today },
{ id: 'PG-002', nama: 'Dimas', nilai: 140000, kategori: 'Gaji', catatan: 'Dapur', tanggal: today, tgl: today },
{ id: 'PG-003', nama: 'Rina', nilai: 30000, kategori: 'Kasbon', catatan: 'Transport', tanggal: yesterday, tgl: yesterday }
],
paketKustom: [
{
id: 'PK-001', nama: 'Paket Lunch 2 Pax', aktif: true, harga: 79000,
items: [
{ nama: 'Tom Yum Seafood', qty: 1, harga: 38000 },
{ nama: 'Nasi Putih', qty: 2, harga: 6000 },
{ nama: 'Thai Tea', qty: 2, harga: 18000 }
],
logo_url: svgDataUri('Paket Lunch', '#0f766e', '#fff')
}
],
supplierHistory: [
{ nama: 'Supplier Laut Segar', wa: '628222111333', item: 'Udang' },
{ nama: 'Toko Kemasan Maju', wa: '628555444222', item: 'Cup plastik' }
],
calendarNotes: [
{ id: 'CAL-001', tanggal: today, judul: 'Promo lunch', catatan: 'Push menu lunch set ke meja kantor sekitar jam 11.30.', user: 'Owner', timestamp: makeTs(today, '08:00') },
{ id: 'CAL-002', tanggal: yesterday, judul: 'Tindak lanjut supplier', catatan: 'Konfirmasi harga seafood untuk minggu depan.', user: 'Pembelian', timestamp: makeTs(yesterday, '16:20') }
],
chatMessages: [
{ id: 'CH-001', from: 'Kasir', to: 'Dapur', sender: 'Kasir', message: '2 Thai Tea untuk meja A3, tolong diprioritaskan.', text: '2 Thai Tea untuk meja A3, tolong diprioritaskan.', timestamp: makeTs(today, '10:18'), read: false },
{ id: 'CH-002', from: 'Dapur', to: 'Kasir', sender: 'Dapur', message: 'Pesanan meja B1 siap diambil.', text: 'Pesanan meja B1 siap diambil.', timestamp: makeTs(today, '11:10'), read: false }
],
bellStatus: { status: 'Inactive' }
};
}
function loadState() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
} catch (e) {}
return baseState();
}
function saveState() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
}
function resetState() {
state = baseState();
saveState();
}
var state = loadState();
function getInitialDataPayload() {
return {
menu: clone(state.menu),
transaksi: clone(state.transaksi),
pelanggan: clone(state.pelanggan),
rekap: clone(state.transaksi),
belanja: clone(state.belanja),
paketKustom: clone(state.paketKustom),
supplierHistory: clone(state.supplierHistory),
settings: clone(state.settings)
};
}
function getTransactionsInRange(from, to) {
return clone((state.transaksi || []).filter(function (t) {
return isCountedTransaction(t) && inRange(String(t.tgl || '').slice(0, 10), from, to);
}));
}
function getBelanjaInRange(from, to) {
return clone((state.belanja || []).filter(function (b) {
var dt = String(b.tanggal || b.tgl || '').slice(0, 10);
return inRange(dt, from, to);
}));
}
function buildSalesBundle(from, to) {
var tx = getTransactionsInRange(from, to);
var expRows = getBelanjaInRange(from, to);
var totalSales = sum(tx, 'total');
var totalTransactions = tx.length;
var avgTransaction = totalTransactions ? Math.round(totalSales / totalTransactions) : 0;
var topMap = {};
var avgMap = {};
var methods = {};
var hourlyCounts = [];
var dailyMap = {};
for (var i = 0; i < 24; i++) hourlyCounts.push(0);
tx.forEach(function (t) {
var hour = Number(String(t.timestamp || '').slice(11, 13)) || 0;
hourlyCounts[hour] = (hourlyCounts[hour] || 0) + 1;
var day = String(t.tgl || '').slice(0, 10);
if (!dailyMap[day]) dailyMap[day] = { date: day, total: 0 };
dailyMap[day].total += Number(t.total || 0);
var method = String(t.metodeBayar || 'Lainnya');
methods[method] = (methods[method] || 0) + Number(t.total || 0);
(t.items || []).forEach(function (it) {
var key = String(it.nama || 'Item');
var qty = Number(it.qty || 0);
topMap[key] = (topMap[key] || 0) + qty;
if (!avgMap[key]) avgMap[key] = { total: 0, days: {} };
avgMap[key].total += qty;
avgMap[key].days[day] = true;
});
});
var topItems = Object.keys(topMap)
.map(function (k) { return [k, topMap[k]]; })
.sort(function (a, b) { return b[1] - a[1]; })
.slice(0, 8);
var avgMenuPerDay = Object.keys(avgMap)
.map(function (k) {
var activeDays = Object.keys(avgMap[k].days).length || 1;
return { nama: k, avg: Number((avgMap[k].total / activeDays).toFixed(1)) };
})
.sort(function (a, b) { return b.avg - a.avg; })
.slice(0, 8);
var dailySales = Object.keys(dailyMap).sort().map(function (k) { return dailyMap[k]; });
var expenseSummary = buildExpenseAnalytics(from, to);
return {
sales: {
summary: { totalSales: totalSales, totalTransactions: totalTransactions, avgTransaction: avgTransaction },
buckets: {
lt100: tx.filter(function (t) { return Number(t.total || 0) < 100000; }).length,
gte100: tx.filter(function (t) { return Number(t.total || 0) >= 100000; }).length,
gte200: tx.filter(function (t) { return Number(t.total || 0) >= 200000; }).length
},
topItems: topItems,
avgMenuPerDay: avgMenuPerDay,
hourlyCounts: hourlyCounts,
dailySales: dailySales,
methods: methods
},
expense: expenseSummary,
profit: totalSales - Number((expenseSummary.summary && expenseSummary.summary.totalBelanja) || 0)
};
}
function buildExpenseAnalytics(from, to) {
var rows = getBelanjaInRange(from, to);
var kategoriBelanja = {};
var dailyMap = {};
rows.forEach(function (b) {
var total = Number(b.total || 0);
var day = String(b.tanggal || b.tgl || '').slice(0, 10);
var cat = String(b.kategori || 'Lainnya');
if (!dailyMap[day]) dailyMap[day] = { date: day, total: 0 };
dailyMap[day].total += total;
kategoriBelanja[cat] = (kategoriBelanja[cat] || 0) + total;
});
var topBelanja = rows.slice().sort(function (a, b) { return Number(b.total || 0) - Number(a.total || 0); }).slice(0, 8)
.map(function (b) { return { nama: b.nama, total: Number(b.total || 0), tgl: b.tgl || b.tanggal || '', kat: b.kategori || '' }; });
var dailyBelanja = Object.keys(dailyMap).sort().map(function (k) { return dailyMap[k]; });
return {
summary: { totalBelanja: sum(rows, 'total'), maxBelanja: topBelanja.length ? Number(topBelanja[0].total || 0) : 0 },
topBelanja: topBelanja,
dailyBelanja: dailyBelanja,
kategoriBelanja: kategoriBelanja
};
}
function buildRekap(from, to, isRange) {
var tx = getTransactionsInRange(from, to);
var exp = getBelanjaInRange(from, to);
var methods = {};
var portionMap = {};
tx.forEach(function (t) {
var method = String(t.metodeBayar || 'Lainnya');
methods[method] = (methods[method] || 0) + Number(t.total || 0);
(t.items || []).forEach(function (it) {
var key = String(it.nama || 'Item');
portionMap[key] = (portionMap[key] || 0) + Number(it.qty || 0);
});
});
var portions = Object.keys(portionMap).map(function (k) { return { nama: k, qty: portionMap[k] }; })
.sort(function (a, b) { return b.qty - a.qty; });
return {
isRange: !!isRange,
startDate: from,
endDate: to,
summary: {
totalNota: tx.length,
totalSales: sum(tx, 'total'),
totalBelanja: sum(exp, 'total'),
netProfit: sum(tx, 'total') - sum(exp, 'total')
},
methods: methods,
portions: portions
};
}
function getHistoryPage(from, to, q, offset, limit) {
var needle = String(q || '').trim().toLowerCase();
var items = clone((state.transaksi || []).filter(function (t) {
var tgl = String(t.tgl || '').slice(0, 10);
if (!inRange(tgl, from, to)) return false;
if (!needle) return true;
var hay = [t.id, t.nama, t.meja, t.metodeBayar, t.status, t.wa].join(' ').toLowerCase();
return hay.indexOf(needle) > -1;
})).sort(function (a, b) {
return String(b.timestamp || b.tgl || '').localeCompare(String(a.timestamp || a.tgl || ''));
});
var start = Number(offset || 0);
var size = Number(limit || 50);
return {
items: items.slice(start, start + size),
nextOffset: start + size,
hasMore: start + size < items.length
};
}
function getCalendarNotesByDate(dateStr) {
return clone((state.calendarNotes || []).filter(function (n) {
return String(n.tanggal || '').slice(0, 10) === String(dateStr || '').slice(0, 10);
}).sort(function (a, b) {
return String(b.timestamp || '').localeCompare(String(a.timestamp || ''));
}));
}
function updateCustomerPoints(tx) {
var wa = normalizeWa(tx && tx.wa);
var nama = String((tx && tx.nama) || 'Pelanggan');
if (!wa) return;
var found = null;
(state.pelanggan || []).forEach(function (p) {
if (normalizeWa(p.wa) === wa) found = p;
});
if (!found) {
found = { nama: nama, wa: wa, poin: 0 };
state.pelanggan.push(found);
}
if (tx.status === 'Selesai' || tx.status === 'Review') {
found.poin = Number(found.poin || 0) + Number(tx.poinDapat || 0);
}
if (nama && found.nama !== nama) found.nama = nama;
}
function mergeBelanjaRows(rows) {
(rows || []).forEach(function (row) {
var incoming = clone(row || {});
incoming.id = incoming.id || nextId('BL', state.belanja);
incoming.qty = Number(incoming.qty || 0);
incoming.harga = Number(incoming.harga || 0);
incoming.total = Number(incoming.total || (incoming.harga * incoming.qty) || 0);
incoming.tgl = incoming.tgl || incoming.tanggal || today;
incoming.tanggal = incoming.tanggal || incoming.tgl;
incoming.timestamp = incoming.timestamp || makeTs(incoming.tgl, '09:00');
var idx = state.belanja.findIndex(function (b) { return String(b.id) === String(incoming.id); });
if (idx > -1) state.belanja[idx] = Object.assign({}, state.belanja[idx], incoming);
else state.belanja.push(incoming);
});
state.belanja.sort(function (a, b) { return String(b.timestamp || '').localeCompare(String(a.timestamp || '')); });
saveState();
return { success: true, belanja: clone(state.belanja), mode: 'local-preview', note: 'Disimpan di browser preview ini.' };
}
function saveStokRows(rows, dateStr) {
var useDate = String(dateStr || today);
state.stok = (state.stok || []).filter(function (s) { return String(s.tanggal || '') !== useDate; });
(rows || []).forEach(function (row) {
var r = clone(row || {});
r.id = r.id || nextId('ST', state.stok);
r.tanggal = useDate;
r.stokAwal = Number(r.stokAwal || 0);
r.restock = Number(r.restock || 0);
r.terpakai = Number(r.terpakai || 0);
r.sisa = Math.max(0, r.stokAwal + r.restock - r.terpakai);
state.stok.push(r);
});
saveState();
return clone(state.stok.filter(function (s) { return String(s.tanggal || '') === useDate; }));
}
function saveOperasionalRows(rows, dateStr) {
var useDate = String(dateStr || today);
state.operasional = (state.operasional || []).filter(function (o) { return String(o.tanggal || '') !== useDate; });
(rows || []).forEach(function (row) {
var r = clone(row || {});
r.id = r.id || nextId('OP', state.operasional);
r.tanggal = useDate;
r.tgl = useDate;
r.harga = Number(r.harga || 0);
r.qty = Number(r.qty || 0);
r.total = Number(r.total || (r.harga * r.qty) || 0);
state.operasional.push(r);
});
saveState();
return clone(state.operasional.filter(function (o) { return String(o.tanggal || '') === useDate; }));
}
function savePegawaiRows(rows, dateStr) {
var useDate = String(dateStr || today);
state.pegawai = (state.pegawai || []).filter(function (p) { return String(p.tanggal || '') !== useDate; });
(rows || []).forEach(function (row) {
var r = clone(row || {});
r.id = r.id || nextId('PG', state.pegawai);
r.tanggal = useDate;
r.tgl = useDate;
r.nilai = Number(r.nilai || 0);
state.pegawai.push(r);
});
saveState();
return clone(state.pegawai.filter(function (p) { return String(p.tanggal || '') === useDate; }));
}
function perItemOperasional(from, to) {
var map = {};
(state.operasional || []).forEach(function (o) {
if (!inRange(String(o.tanggal || o.tgl || ''), from, to)) return;
var key = String(o.nama || 'Operasional');
if (!map[key]) map[key] = { nama: key, harga: 0, qty: 0, total: 0 };
map[key].qty += Number(o.qty || 0);
map[key].total += Number(o.total || 0);
});
return Object.keys(map).map(function (k) {
var item = map[k];
item.harga = item.qty ? Math.round(item.total / item.qty) : item.total;
return item;
}).sort(function (a, b) { return b.total - a.total; });
}
function perPegawai(from, to) {
var map = {};
(state.pegawai || []).forEach(function (p) {
if (!inRange(String(p.tanggal || p.tgl || ''), from, to)) return;
var key = String(p.nama || 'Pegawai');
if (!map[key]) map[key] = { nama: key, gaji: 0, kasbon: 0, total: 0 };
var nilai = Number(p.nilai || 0);
if (String(p.kategori || '').toLowerCase() === 'kasbon') map[key].kasbon += nilai;
else map[key].gaji += nilai;
map[key].total += nilai;
});
return Object.keys(map).map(function (k) { return map[k]; }).sort(function (a, b) { return b.total - a.total; });
}
function responseFor(fn, args) {
var from = args && args[0] ? String(args[0]) : today;
var to = args && args[1] ? String(args[1]) : from;
switch (fn) {
case 'getInitialData':
return getInitialDataPayload();
case 'getChatMessages':
return clone(state.chatMessages || []);
case 'markChatAsRead':
(state.chatMessages || []).forEach(function (m) {
if (!args[0] || String(m.to || '').toLowerCase() === String(args[0] || '').toLowerCase()) m.read = true;
});
saveState();
return { success: true, mode: 'local-preview' };
case 'sendChatMessage':
state.chatMessages.push({
id: nextId('CH', state.chatMessages),
from: String(args[0] || 'User'),
to: String(args[0] || 'User') === 'Kasir' ? 'Dapur' : 'Kasir',
sender: String(args[0] || 'User'),
message: String(args[1] || ''),
text: String(args[1] || ''),
timestamp: makeTs(today, '12:00'),
read: false
});
saveState();
return { success: true, messages: clone(state.chatMessages) };
case 'triggerBell':
state.bellStatus = { status: 'Active' };
saveState();
return { success: true, status: 'Active', mode: 'local-preview' };
case 'getBellStatus':
return clone(state.bellStatus || { status: 'Inactive' });
case 'resetBell':
state.bellStatus = { status: 'Inactive' };
saveState();
return { success: true, status: 'Inactive' };
case 'rekapLogin':
return { ok: true, mode: 'local-preview' };
case 'getDashboardBundleByRange':
return buildSalesBundle(from, to);
case 'getExpenseAnalyticsByRange':
return buildExpenseAnalytics(from, to);
case 'getRekapByDate':
return buildRekap(from, from, false);
case 'getRekapByRange':
return buildRekap(from, to, true);
case 'getTransaksiHistoryPage':
return getHistoryPage(from, to, args[2], args[3], args[4]);
case 'loadCalendarNotes':
case 'getCalendarNotes':
case 'getCalendarNotesByDate':
return getCalendarNotesByDate(from);
case 'saveCalendarNote':
var note = clone(args[0] || {});
note.id = note.id || nextId('CAL', state.calendarNotes);
note.tanggal = note.tanggal || today;
note.timestamp = makeTs(note.tanggal, '09:30');
var noteIdx = state.calendarNotes.findIndex(function (n) { return String(n.id) === String(note.id); });
if (noteIdx > -1) state.calendarNotes[noteIdx] = Object.assign({}, state.calendarNotes[noteIdx], note);
else state.calendarNotes.push(note);
saveState();
return getCalendarNotesByDate(note.tanggal);
case 'deleteCalendarNote':
state.calendarNotes = (state.calendarNotes || []).filter(function (n) { return String(n.id) !== String(args[0]); });
saveState();
return [];
case 'getRingkasanPeriode':
return {
from: from,
to: to,
summary: {
totalQty: sum(getBelanjaInRange(from, to), 'qty'),
totalNilai: sum(getBelanjaInRange(from, to), 'total')
},
menu: clone(state.menu),
stok: clone((state.stok || []).filter(function (s) { return inRange(String(s.tanggal || ''), from, to); })),
belanja: clone(getBelanjaInRange(from, to)),
operasional: clone((state.operasional || []).filter(function (o) { return inRange(String(o.tanggal || o.tgl || ''), from, to); })),
pegawaiList: clone(state.pegawaiList || []),
pegawai: clone((state.pegawai || []).filter(function (p) { return inRange(String(p.tanggal || p.tgl || ''), from, to); }))
};
case 'getLastBelanjaHargaMap':
var map = {};
(state.belanja || []).forEach(function (b) { map[String(b.nama || '')] = Number(b.harga || 0); });
return map;
case 'getDebugInfo':
return { mode: 'local-preview', source: 'bridge-pratinjau-lokal', stored: true };
case 'getMenuListExtended':
return clone(state.menu || []);
case 'getBelanjaData':
return clone((state.belanja || []).filter(function (b) { return String(b.tanggal || b.tgl || '') === from; }));
case 'getStokData':
return clone((state.stok || []).filter(function (s) { return String(s.tanggal || '') === from; }));
case 'getOperasionalData':
return clone((state.operasional || []).filter(function (o) { return String(o.tanggal || o.tgl || '') === from; }));
case 'getPegawaiData':
return clone((state.pegawai || []).filter(function (p) { return String(p.tanggal || p.tgl || '') === from; }));
case 'getPegawaiList':
return clone(state.pegawaiList || []);
case 'getOperasionalRekapPeriode':
return { total: sum(perItemOperasional(from, to), 'total'), perItem: perItemOperasional(from, to) };
case 'getPegawaiRekapPeriode':
var perPg = perPegawai(from, to);
return {
totals: {
gaji: perPg.reduce(function (a, b) { return a + Number(b.gaji || 0); }, 0),
kasbon: perPg.reduce(function (a, b) { return a + Number(b.kasbon || 0); }, 0),
total: perPg.reduce(function (a, b) { return a + Number(b.total || 0); }, 0)
},
perPegawai: perPg
};
case 'getStokFotoMap':
var fotoMap = {};
(state.menu || []).forEach(function (m) { fotoMap[String(m.nama || '')] = m.gambar || ''; });
return fotoMap;
case 'saveCustomerInfo':
var customer = clone(args[0] || {});
customer.wa = normalizeWa(customer.wa);
if (customer.wa) {
var existing = state.pelanggan.find(function (p) { return normalizeWa(p.wa) === customer.wa; });
if (existing) existing.nama = customer.nama || existing.nama;
else state.pelanggan.push({ nama: customer.nama || 'Pelanggan', wa: customer.wa, poin: 0 });
saveState();
}
return { success: true, pelanggan: clone(state.pelanggan) };
case 'saveTransaction':
var tx = clone(args[0] || {});
tx.id = tx.id || nextId('TRX', state.transaksi);
tx.tgl = tx.tgl || today;
tx.timestamp = tx.timestamp || makeTs(tx.tgl, '12:30');
tx.status = tx.status || 'Pending';
tx.items = Array.isArray(tx.items) ? tx.items : [];
if (!tx.total) {
tx.total = tx.items.reduce(function (a, it) { return a + (Number(it.qty || 0) * Number(it.harga || 0)); }, 0);
}
if (!tx.poinDapat && tx.total > 0) tx.poinDapat = Math.max(1, Math.floor(Number(tx.total || 0) / 10000));
var txIdx = state.transaksi.findIndex(function (t) { return String(t.id) === String(tx.id); });
if (txIdx > -1) state.transaksi[txIdx] = Object.assign({}, state.transaksi[txIdx], tx);
else state.transaksi.push(tx);
updateCustomerPoints(tx);
saveState();
return { ok: true, transaksi: clone(state.transaksi), pelanggan: clone(state.pelanggan), mode: 'local-preview' };
case 'uploadReviewProof':
case 'uploadPaymentProof':
case 'uploadPaketLogo':
return { success: true, url: svgDataUri('Unggah Preview', '#2563eb', '#fff'), mode: 'local-preview' };
case 'updateTransactionReviewPhoto':
case 'updateTransactionPhoto':
var id = args[0], url = args[1];
(state.transaksi || []).forEach(function (t) {
if (String(t.id) === String(id)) t.buktiReview = url || svgDataUri('Review', '#15803d', '#fff');
});
saveState();
return { success: true, transaksi: clone(state.transaksi) };
case 'saveFeedback':
return { success: true, mode: 'local-preview' };
case 'saveBelanjaBulk':
case 'saveBulkBelanja':
return mergeBelanjaRows(args[0] || []);
case 'syncBelanjaOut':
case 'syncBelanjaToRekap':
case 'syncBelanjaToRekapAll':
return { success: true, belanja: clone(state.belanja), mode: 'local-preview' };
case 'bulkUpdateBelanjaById':
(args[0] || []).forEach(function (u) {
var item = state.belanja.find(function (b) { return String(b.id) === String(u.id); });
if (item) Object.assign(item, { harga: Number(u.harga || 0), qty: Number(u.qty || 0), total: Number(u.total || 0), catatan: u.catatan || item.catatan || '' });
});
saveState();
return { success: true, belanja: clone(state.belanja) };
case 'updateBelanjaById':
var itemById = state.belanja.find(function (b) { return String(b.id) === String(args[0]); });
if (itemById) Object.assign(itemById, args[1] || {});
saveState();
return { success: true, belanja: clone(state.belanja) };
case 'updateBelanjaItem':
var bl = state.belanja.find(function (b) { return String(b.tgl || b.tanggal || '') === String(args[0]) && String(b.kategori || '') === String(args[1]) && String(b.nama || '') === String(args[2]); });
if (bl) { bl.harga = Number(args[3] || 0); bl.qty = Number(args[4] || 0); bl.total = Number(args[5] || 0); bl.catatan = args[6] || bl.catatan || ''; }
saveState();
return { success: true, belanja: clone(state.belanja) };
case 'deleteBelanjaById':
state.belanja = (state.belanja || []).filter(function (b) { return String(b.id) !== String(args[0]); });
saveState();
return { success: true, belanja: clone(state.belanja) };
case 'deleteBelanjaItem':
case 'deleteBelanja':
state.belanja = (state.belanja || []).filter(function (b) {
return !(String(b.tgl || b.tanggal || '') === String(args[0]) && String(b.kategori || '') === String(args[1]) && String(b.nama || '') === String(args[2]));
});
saveState();
return { success: true, belanja: clone(state.belanja) };
case 'saveBulkStok':
return saveStokRows(args[0] || [], args[2] || today);
case 'bulkUpdateStokItems':
(args[0] || []).forEach(function (u) {
var item = state.stok.find(function (s) {
return String(s.tanggal || '') === String(u.tanggal || '') && String(s.lokasi || '') === String(u.lokasi || '') && String(s.menu || '') === String(u.menu || '');
});
if (item) {
item.stokAwal = Number(u.stokAwal || 0);
item.restock = Number(u.restock || 0);
item.terpakai = Number(u.terpakai || 0);
item.sisa = Math.max(0, item.stokAwal + item.restock - item.terpakai);
}
});
saveState();
return { success: true, stok: clone(state.stok) };
case 'updateStokItem':
var stokItem = state.stok.find(function (s) { return String(s.tanggal || '') === String(args[0]) && String(s.lokasi || '') === String(args[1]) && String(s.menu || '') === String(args[2]); });
if (stokItem) {
stokItem.stokAwal = Number(args[3] || 0);
stokItem.restock = Number(args[4] || 0);
stokItem.terpakai = Number(args[5] || 0);
stokItem.sisa = Math.max(0, stokItem.stokAwal + stokItem.restock - stokItem.terpakai);
}
saveState();
return { success: true, stok: clone(state.stok) };
case 'deleteStokItem':
state.stok = (state.stok || []).filter(function (s) {
return !(String(s.tanggal || '') === String(args[0]) && String(s.lokasi || '') === String(args[1]) && String(s.menu || '') === String(args[2]));
});
saveState();
return { success: true, stok: clone(state.stok) };
case 'saveBulkOperasional':
return saveOperasionalRows(args[0] || [], args[2] || today);
case 'saveBulkPegawai':
return savePegawaiRows(args[0] || [], args[2] || today);
case 'savePegawaiList':
state.pegawaiList = (args[0] || []).map(function (r, idx) {
return { id: r.id || ('PL-' + String(idx + 1).padStart(3, '0')), nama: r.nama || '', status: r.status || 'Aktif', catatan: r.catatan || '' };
}).filter(function (r) { return String(r.nama || '').trim(); });
saveState();
return clone(state.pegawaiList);
case 'deletePegawaiById':
state.pegawai = (state.pegawai || []).filter(function (p) { return String(p.id) !== String(args[0]); });
saveState();
return { success: true, pegawai: clone(state.pegawai) };
case 'updateKitchenStatus':
var kitchenId = args[0], nextStatus = args[1] || 'Ready';
(state.transaksi || []).forEach(function (t) { if (String(t.id) === String(kitchenId)) t.status = nextStatus; });
saveState();
return getInitialDataPayload();
}
if (/Bundle|Analytics/.test(fn)) return buildSalesBundle(from, to);
if (/HistoryPage/.test(fn)) return getHistoryPage(from, to, '', 0, 50);
if (/Rekap/.test(fn)) return buildRekap(from, to, true);
if (/Notes/.test(fn)) return getCalendarNotesByDate(from);
if (/save|update|delete|import|reset|sync|trigger|mark/i.test(fn)) return { success: true, mode: 'local-preview', note: 'Ditangani oleh bridge pratinjau lokal.' };
if (/get|load|search|find/i.test(fn)) return {};
return { success: true, mode: 'local-preview' };
}
if (!window.google) window.google = {};
if (!window.google.script) window.google.script = {};
if (!window.google.script.host) window.google.script.host = { close: function () {} };
if (window.google.script.run) return;
function createRunner(stateRef) {
stateRef = stateRef || { success: null, failure: null };
return new Proxy({}, {
get: function (_, prop) {
if (prop === 'withSuccessHandler') {
return function (cb) {
stateRef.success = cb;
return createRunner(stateRef);
};
}
if (prop === 'withFailureHandler') {
return function (cb) {
stateRef.failure = cb;
return createRunner(stateRef);
};
}
return function () {
var args = Array.prototype.slice.call(arguments);
var result = clone(responseFor(String(prop), args));
window.console && console.info && console.info('[pratinjau-lokal]', String(prop), args, result);
setTimeout(function () {
try {
if (typeof stateRef.success === 'function') stateRef.success(result);
} catch (err) {
if (typeof stateRef.failure === 'function') stateRef.failure(err);
}
}, 0);
return createRunner({ success: null, failure: null });
};
}
});
}
window.google.script.run = createRunner();
window.__POSFUKU_LOCAL_PREVIEW__ = true;
window.__resetPosfukuLocalPreview = function () {
resetState();
window.location.reload();
};
function pageTip() {
var path = window.location.pathname || '';
if (/Poin\.html$/i.test(path)) return 'Coba WA demo: 0812-3456-7890.';
if (/Pelanggan\.html$/i.test(path)) return 'Pesanan dan review disimpan lokal di browser preview ini.';
if (/Dapur\.html$/i.test(path)) return 'Contoh antrean kasir dan kitchen chat sudah diisi.';
if (/Belanja\.html$/i.test(path) || /Stok/i.test(path)) return 'Perubahan stok/belanja di preview ini tersimpan lokal di browser.';
if (/RekapTransaksi/i.test(path)) return 'Grafik dan rekap memakai data demo restoran lokal.';
return 'Data demo ini aman untuk eksplorasi browser.';
}
function injectBanner() {
if (document.getElementById('local-preview-banner')) return;
var banner = document.createElement('div');
banner.id = 'local-preview-banner';
banner.innerHTML = '<strong>Mode pratinjau lokal.</strong> Google Apps Script backend tidak aktif di VM ini, jadi halaman memakai data demo yang tersimpan lokal di browser. <span style="opacity:.9;">' + escHtml(pageTip()) + '</span>';
banner.style.cssText = [
'position:sticky','top:0','z-index:9999','padding:10px 14px','font:700 12px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif',
'background:#fff7ed','color:#9a3412','border-bottom:1px solid #fdba74','text-align:center','box-shadow:0 2px 10px rgba(0,0,0,.06)'
].join(';');
document.body.insertBefore(banner, document.body.firstChild);
}
function injectQuickNav() {
if (document.getElementById('local-preview-nav')) return;
var path = window.location.pathname || '';
if (path === '/' || path === '/index.html') return;
var nav = document.createElement('div');
nav.id = 'local-preview-nav';
nav.innerHTML = '' +
'<a href="/">Beranda</a>' +
'<a href="/POSFuku/Index.html">POS</a>' +
'<a href="/POSFuku/Dapur.html">Dapur</a>' +
'<a href="/POSFuku/Pelanggan.html">Pelanggan</a>' +
'<a href="/POSFuku/Poin.html">Poin</a>' +
'<a href="/POSFuku/Belanja.html">Belanja</a>' +
'<a href="/POSFuku/Stok%20%26%20Belanja/Index.html">Stok &amp; Belanja</a>' +
'<a href="/RekapTransaksi/Index.html">Rekap &amp; Grafik</a>' +
'<button type="button" id="local-preview-reset">Reset data demo</button>';
nav.style.cssText = [
'position:sticky','top:42px','z-index:9998','display:flex','gap:8px','flex-wrap:wrap','align-items:center',
'padding:10px 12px','background:#0f172a','border-bottom:1px solid rgba(255,255,255,.08)','box-shadow:0 6px 18px rgba(0,0,0,.12)'
].join(';');
document.body.insertBefore(nav, document.body.children[1] || document.body.firstChild);
Array.prototype.forEach.call(nav.querySelectorAll('a'), function (a) {
a.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 10px;border-radius:999px;background:rgba(255,255,255,.08);color:#fff;text-decoration:none;font:800 12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;';
});
var btn = nav.querySelector('#local-preview-reset');
if (btn) {
btn.style.cssText = 'margin-left:auto;min-height:32px;padding:0 12px;border:none;border-radius:999px;background:#fb7185;color:#fff;font:800 12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;cursor:pointer;';
btn.onclick = function () {
if (window.confirm('Reset semua data demo lokal di browser ini?')) window.__resetPosfukuLocalPreview();
};
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
injectBanner();
injectQuickNav();
});
} else {
injectBanner();
injectQuickNav();
}
})();