39735-vm/POSFuku/Index.html
Flatlogic Bot 8c2a5d487c POSFuku_v2
2026-04-19 12:46:29 +00:00

7199 lines
337 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#111111">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="manifest" id="manifest-link" href="">
<title>Fuku POS</title>
<script>
// PWA Installation Handler
var deferredPrompt;
console.log('PWA Init...');
var __baseUrl = window.location.href.split('?')[0];
var __isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
var __isGAS = window.location.hostname.indexOf('script.google.') > -1 || window.location.hostname.indexOf('googleusercontent.com') > -1;
var __hasGoogleRun = typeof google !== 'undefined' && google.script && google.script.run;
if (!__hasGoogleRun && __isLocalhost) {
console.log('Google Apps Script API unavailable on localhost; using local fallback.');
}
if (__isGAS) {
var manifestEl = document.getElementById('manifest-link');
if (manifestEl) manifestEl.href = __baseUrl + '?p=manifest';
}
if (__isGAS && 'serviceWorker' in navigator) {
window.addEventListener('load', function() {
var swUrl = __baseUrl + '?p=sw';
console.log('Registering SW:', swUrl);
navigator.serviceWorker.register(swUrl)
.then(function(reg) { console.log('SW Registered:', reg.scope); })
.catch(function(err) { console.error('SW Error:', err); });
});
} else if (__isLocalhost) {
console.log('PWA Disabled on localhost');
}
window.addEventListener('beforeinstallprompt', function(e) {
console.log('PWA Install Prompt detected');
e.preventDefault();
deferredPrompt = e;
var installBtn = document.getElementById('pwa-install-btn');
if (installBtn) installBtn.style.display = 'block';
});
function triggerPwaInstall() {
if (!deferredPrompt) {
alert('Gunakan menu browser (titik tiga di kanan atas) lalu pilih "Install App" atau "Tambahkan ke Layar Utama".');
return;
}
deferredPrompt.prompt();
deferredPrompt.userChoice.then(function(choiceResult) {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted PWA install');
document.getElementById('pwa-install-btn').style.display = 'none';
}
deferredPrompt = null;
});
}
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<style>
:root { --header-h: 110px; }
body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; padding-bottom: 80px; -webkit-font-smoothing: antialiased; }
/* Optimasi untuk Low-end: Hindari shadow berlebih, animasi berat, dan gunakan GPU acceleration jika mungkin */
.card, .table-box, .menu-card, .history-week-block, .history-mini-card {
background:white;
border:1px solid #eee;
border-radius:12px;
padding:12px;
margin-bottom:12px;
box-shadow: none; /* Hilangkan shadow berat */
transform: translateZ(0); /* Force GPU acceleration */
}
.nav { position:fixed; top:0; left:0; right:0; height:55px; background:#111; color:white; display:flex; align-items:center; padding:0 12px; z-index:1000; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.nav .title { font-weight:800; letter-spacing:0.5px; }
.nav .nav-top { display:flex; justify-content:space-between; width:100%; align-items:center; flex-wrap:wrap; gap:8px; }
.nav .right { margin-left:auto; display:flex; gap:10px; align-items:center; flex-wrap:wrap; justify-content:flex-end; }
.nav .link { color:#ff4d6d; font-weight:800; cursor:pointer; font-size:12px; }
.nav .btn { cursor:pointer; font-size:16px; padding: 5px 10px; border: 1px solid #444; border-radius: 6px; }
@media (max-width: 420px) {
.nav .right { gap:6px; }
.nav .btn { font-size:11px; padding:4px 8px; }
}
#bell-indicator { color: #fff; font-size: 20px; cursor: pointer; margin-right: 10px; }
#bell-indicator.active { animation: bellRing 1s infinite; color: #f59e0b; }
@keyframes bellRing {
0% { transform: rotate(0deg); }
10% { transform: rotate(15deg); }
20% { transform: rotate(-15deg); }
30% { transform: rotate(15deg); }
40% { transform: rotate(-15deg); }
50% { transform: rotate(0deg); }
100% { transform: rotate(0deg); }
}
#loading { position:fixed; inset:0; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index:2000; }
.spinner { width:40px; height:40px; border:3px solid #f3f3f3; border-top:3px solid #e11d48; border-radius:50%; animation: spin 0.8s linear infinite; margin-bottom:12px; }
@keyframes spin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
.page { display:none; padding:10px; padding-top: calc(var(--header-h, 110px) + 10px); }
.page.active { display:block !important; }
.bottom-nav { position:fixed; bottom:0; left:0; right:0; height:60px; background:#111; border-top:2px solid #e11d48; display:flex; z-index:1000; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.nav-btn { flex:1; min-width: 55px; border:none; background:none; color:#999; font-weight:700; font-size:9px; cursor:pointer; padding: 4px 2px; display:flex; flex-direction:column; align-items:center; justify-content:center; }
.nav-btn.active { color:#ff4d6d; background: rgba(225, 29, 72, 0.05); }
.nav-btn .icon { display:block; font-size:18px; margin-bottom:2px; }
.title-sm { font-weight:800; color:#111; margin:0 0 10px 0; font-size:14px; }
/* Grid responsif untuk HP kecil */
.table-grid { display:grid; grid-template-columns: repeat(4, 1fr); gap:8px; }
@media (max-width: 360px) { .table-grid { grid-template-columns: repeat(3, 1fr); } }
.table-box { background:white; border:1px solid #ddd; border-radius:12px; padding:8px; text-align:center; cursor:pointer; position:relative; min-height: 50px; display:flex; flex-direction:column; justify-content:center; }
.table-box.occupied { border-color:#e11d48; background: #fff1f2; }
.table-box.booked { border-color:#2563eb; background: #eff6ff; }
.table-num { font-weight:900; font-size:16px; display:block; color: #111; }
.table-status { font-size:9px; color:#666; margin-top: 2px; }
.badge { position:absolute; top:-5px; right:-5px; background:#2563eb; color:white; border-radius:10px; padding:2px 6px; font-size:9px; font-weight:900; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.menu-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(85px, 1fr)); gap:8px; margin-top:10px; }
.menu-card { background:white; border:1px solid #eee; border-radius:10px; overflow:hidden; position:relative; cursor:pointer; padding: 0; }
.menu-card img { width:100%; aspect-ratio:1/1; object-fit:cover; display: block; background: #f0f0f0; }
.menu-info { padding:8px; }
.menu-name { font-size:11px; font-weight:700; height:2.8em; overflow:hidden; line-height: 1.4; color: #333; }
.menu-price { font-size:11px; font-weight:800; color:#e11d48; margin-top: 4px; }
.stok-badge { position:absolute; top:5px; right:5px; background:rgba(16, 185, 129, 0.9); color:white; font-weight:800; font-size:9px; padding:2px 6px; border-radius:6px; }
.qty-badge { position:absolute; top:5px; left:5px; background:#e11d48; color:white; font-weight:900; font-size:12px; padding:4px 8px; border-radius:8px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); z-index: 10; border: 2px solid white; }
.menu-card.selected { border: 2px solid #e11d48; background: #fff1f2; }
.cat-wrapper { display:flex; gap:6px; overflow-x:auto; padding:5px 0; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.cat-wrapper::-webkit-scrollbar { display: none; }
.tag { padding:8px 15px; border-radius:12px; border:1px solid #ddd; background:white; font-weight:700; font-size:12px; cursor:pointer; white-space:nowrap; color:#555; transition: 0.1s; }
.tag.active { background:#e11d48; border-color:#e11d48; color:white; box-shadow: 0 2px 6px rgba(225, 29, 72, 0.3); }
.row2 { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.row2-flex { display:flex; flex-direction: column; gap:12px; } /* Mobile first: stack vertically */
@media (min-width: 768px) { .row2-flex { flex-direction: row; } }
input, select, textarea {
width:100%;
box-sizing:border-box;
padding:12px;
border-radius:10px;
border:1px solid #ccc;
outline:none;
font-size: 14px; /* Cegah auto-zoom di iOS */
background: #fff;
-webkit-appearance: none;
}
small { color:#666; font-size: 11px; margin-bottom: 4px; display: block; }
.btn-main, .btn-dark, .btn-blue, .btn-green {
width:100%;
padding:14px;
border:none;
border-radius:12px;
font-weight:800;
cursor:pointer;
font-size: 14px;
transition: opacity 0.1s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-main:active, .btn-dark:active, .btn-blue:active, .btn-green:active { opacity: 0.7; }
.btn-main { background:#e11d48; color:white; }
.btn-dark { background:#111; color:white; }
.btn-blue { background:#2563eb; color:white; }
.btn-green { background:#16a34a; color:white; }
.item-order { display:flex; justify-content:space-between; align-items:center; padding:12px; border:1px solid #eee; border-radius:12px; margin-bottom:8px; background: #fff; }
.qty { display:flex; gap:10px; align-items:center; }
.qty-btn { width:32px; height:32px; border-radius:10px; display:flex; align-items:center; justify-content:center; background:#111; color:white; font-weight:900; cursor:pointer; }
.muted { color:#777; font-size:11px; }
.trans-card { background:white; border:1px solid #eee; border-radius:14px; padding:15px; margin-bottom:12px; position:relative; }
.status { font-weight:800; font-size:10px; padding:4px 10px; border-radius:8px; color:white; text-transform: uppercase; }
.modal { position:fixed; inset:0; background:rgba(0,0,0,0.6); display:none; align-items:flex-end; z-index:1500; }
.modal .sheet { background:white; width:100%; border-top-left-radius:24px; border-top-right-radius:24px; padding:20px; max-height:90vh; overflow-y:auto; box-sizing: border-box; }
.modal .close { font-weight:800; cursor:pointer; color: #e11d48; padding: 5px; }
#audio-permission { position:fixed; inset:0; background:rgba(0,0,0,0.9); z-index:2000; display:none; flex-direction:column; justify-content:center; align-items:center; color:white; text-align:center; padding:20px; }
.btn-audio { background:#16a34a; color:white; border:none; padding:15px 30px; border-radius:12px; font-weight:800; font-size:16px; cursor:pointer; margin-top:20px; }
.summary { background:#f8fafc; border:1px solid #e2e8f0; border-radius:12px; padding:12px; margin-top:10px; }
.sum-row { display:flex; justify-content:space-between; font-size:13px; color:#444; margin-bottom:8px; }
.sum-strong { display:flex; justify-content:space-between; font-weight:800; color:#000; font-size: 15px; }
.pill-row { display:flex; gap:8px; margin-top:8px; overflow-x: auto; padding-bottom: 5px; }
.pill { min-width: 80px; padding:10px; border-radius:10px; border:1px solid #ddd; background:#f3f4f6; color:#111; font-weight:700; cursor:pointer; font-size:11px; white-space: nowrap; }
.pill.on { background:#e11d48; border-color:#e11d48; color:white; }
/* History Grid Mobile Friendly */
.history-grid-container { display: flex; flex-direction: column; gap: 15px; }
.history-week-block { background: white; border: 1px solid #eee; border-radius: 12px; padding: 12px; width: 100%; box-sizing: border-box; }
.history-grid { display: flex; flex-direction: column; gap: 10px; }
@media (min-width: 768px) {
.history-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.history-week-block { min-width: 800px; }
}
.history-col { background: #f9fafb; border-radius: 10px; padding: 10px; border: 1px solid #f1f5f9; text-align: center; }
.history-mini-card { background: white; border: 1px solid #eee; border-radius: 8px; padding: 10px; margin-bottom: 8px; font-size: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.02); }
.hist-item { display:flex; justify-content:space-between; align-items:center; padding:12px; border-bottom:1px solid #eee; background:#fff; cursor:pointer; }
.hist-item:last-child { border-bottom:none; }
.hist-item .main .id { font-weight:900; color:#111; font-size:13px; }
.hist-item .main .meja { color:#666; font-size:11px; margin-top:2px; }
.hist-item .side .total { font-weight:900; color:#e11d48; font-size:13px; }
.hist-item .side .time { color:#999; font-size:10px; margin-top:2px; }
.hist-item.void { opacity:0.5; }
.hist-item.void .total { text-decoration:line-through; color:#666; }
#cart-bar { position:fixed; left:10px; right:10px; bottom:75px; background:#e11d48; color:white; border-radius:16px; padding:15px; display:none; justify-content:space-between; align-items:center; z-index:1100; box-shadow: 0 4px 12px rgba(225, 29, 72, 0.4); }
#cart-bar .info { font-weight:800; font-size:14px; }
#cart-bar .go { font-weight:900; background: white; color: #e11d48; padding: 6px 15px; border-radius: 10px; font-size: 12px; }
/* Hide scrollbars but keep functionality */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-thumb { background: #ddd; border-radius: 10px; }
/* Printing Thermal 58mm */
@media print {
body * { visibility: hidden !important; }
#print-area, #print-area * { visibility: visible !important; }
#print-area {
position: absolute; left: 0; top: 0; width: 58mm;
font-family: 'Courier New', Courier, monospace; font-size: 10px; line-height: 1.2;
padding: 0; margin: 0; color: #000; background: #fff;
}
.thermal-print { width: 58mm; padding: 2mm; box-sizing: border-box; }
.thermal-print .header { text-align: center; border-bottom: 1px dashed #000; padding-bottom: 5px; margin-bottom: 5px; }
.thermal-print .row { display: flex; justify-content: space-between; margin-bottom: 2px; }
.thermal-print .section { border-top: 1px dashed #000; padding-top: 5px; margin-top: 5px; }
.thermal-print .footer { text-align: center; border-top: 1px dashed #000; margin-top: 5px; padding-top: 5px; font-size: 9px; }
.thermal-print b { font-size: 12px; }
@page { size: 58mm auto; margin: 0; }
}
</style>
</head>
<body>
<div id="print-area" style="position: absolute; left: -9999px; top: -9999px;"></div>
<div id="image-modal" style="position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:3000; display:none; flex-direction:column; justify-content:center; align-items:center; padding:20px;">
<div style="background:#fff; border-radius:12px; padding:20px; text-align:center; max-width:100%; box-sizing:border-box;">
<div class="title-sm">Struk iPhone</div>
<p style="font-size:12px; color:#666;">Tekan lama pada gambar di bawah ini, lalu pilih <b>Simpan ke Foto</b> atau <b>Salin</b>.</p>
<div style="max-height:60vh; overflow-y:auto; border:1px solid #eee; margin:15px 0; border-radius:8px;">
<img id="struk-img" style="max-width:100%; height:auto;">
</div>
<div style="display:flex; gap:10px; margin-bottom:10px;">
<button class="btn-dark" style="flex:1; background:#444;" onclick="copyStrukRaw()">📋 SALIN TEKS (RAW)</button>
<button class="btn-main" style="flex:1;" onclick="document.getElementById('image-modal').style.display='none'">TUTUP</button>
</div>
</div>
</div>
<div id="data-modal" style="position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:3100; display:none; flex-direction:column; justify-content:center; align-items:center; padding:20px;">
<div style="background:#fff; border-radius:12px; padding:20px; text-align:left; max-width:420px; width:100%; box-sizing:border-box;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<div class="title-sm" style="margin:0;">Data Lokal</div>
<button class="btn-dark" style="width:auto; padding:8px 12px; font-size:11px;" onclick="closeDataModal()">TUTUP</button>
</div>
<div id="data-modal-info" style="font-size:11px; color:#444; margin-top:10px; line-height:1.5;"></div>
<div style="display:flex; gap:10px; margin-top:14px;">
<button class="btn-main" style="flex:1; font-weight:900;" onclick="exportLocalData()">EXPORT</button>
<button class="btn-dark" style="flex:1; font-weight:900;" onclick="triggerImportLocalData()">IMPORT</button>
<input id="import-local-file" type="file" accept="application/json" style="display:none;" onchange="handleImportLocalFile(event)">
</div>
<div style="font-size:10px; color:#666; margin-top:12px;">
Export dilakukan dari perangkat ini. Untuk menyalin data dari WebApp ke localhost: buka WebApp → EXPORT, lalu buka localhost → IMPORT.
</div>
</div>
</div>
<div id="kasir-login" style="position:fixed; inset:0; background:rgba(0,0,0,0.75); z-index:3200; display:none; flex-direction:column; justify-content:center; align-items:center; padding:20px;">
<div style="background:#fff; border-radius:16px; padding:18px; width:100%; max-width:380px; box-sizing:border-box;">
<div style="font-weight:900; font-size:16px; color:#111; text-align:center;">LOGIN KASIR</div>
<div style="font-size:11px; color:#64748b; text-align:center; margin-top:6px;">Masukkan username & password</div>
<div style="margin-top:14px;">
<div style="font-size:11px; font-weight:800; color:#666; margin-bottom:6px;">Username</div>
<input id="kasir-user" autocomplete="username" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:10px; font-size:14px;">
</div>
<div style="margin-top:10px;">
<div style="font-size:11px; font-weight:800; color:#666; margin-bottom:6px;">Password</div>
<input id="kasir-pass" type="password" autocomplete="current-password" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:10px; font-size:14px;">
</div>
<button class="btn-main" style="width:100%; margin-top:14px; font-weight:900;" onclick="kasirDoLogin()">MASUK</button>
</div>
</div>
<div id="loading">
<div class="spinner"></div>
<div id="loading-text" style="font-weight:800; color:#e11d48; font-size:13px; text-transform: uppercase; letter-spacing: 1px;">Memuat Data Fuku...</div>
<div style="display:flex; gap:10px; margin-top:20px;">
<button id="btn-setup-aman" class="btn-main" style="display:none; width:auto; padding:8px 15px; font-size:11px;" onclick="jalankanSetupAman()">SETUP AMAN</button>
<button id="btn-reset-total" class="btn-dark" style="display:none; width:auto; padding:8px 15px; font-size:11px;" onclick="jalankanResetTotal()">RESET TOTAL</button>
</div>
</div>
<!-- Audio Permission Overlay -->
<div id="audio-permission">
<div style="font-weight:800; font-size:20px;">🔔 AKTIFKAN SUARA NOTIFIKASI</div>
<p style="opacity:0.8; margin-top:10px;">Klik tombol di bawah agar sistem kasir bisa berbunyi saat dipanggil dari Dapur.</p>
<button class="btn-audio" onclick="enableAudio()">AKTIFKAN SEKARANG</button>
</div>
<div id="header-app" class="nav" style="height: auto; padding: 12px; flex-direction: column; align-items: flex-start;">
<div class="nav-top" style="display:flex; justify-content:space-between; width:100%; align-items: center;">
<div class="title" style="font-size: 18px; display:flex; align-items:center; gap:10px;">
<span id="hdr-store-name">POS</span>
<span id="conn-badge" style="font-size:9px; padding:2px 8px; border-radius:6px; background:#16a34a; color:white; font-weight:900; letter-spacing:0.5px;">ONLINE</span>
</div>
<div class="right">
<div id="pwa-install-btn" class="btn" style="display:none; color:#16a34a; border-color:#16a34a; font-size:10px; font-weight:800;" onclick="triggerPwaInstall()">📲 INSTALL</div>
<div id="bell-indicator" onclick="resetBellKasir()" style="display:none;">🔔</div>
<div class="btn" onclick="openDataModal()" style="font-size: 12px; font-weight: 800;">DATA</div>
<div class="btn" onclick="syncData(false)" style="font-size: 12px; font-weight: 800;">SYNC</div>
<div id="btn-ganti-pass" class="btn" onclick="kasirChangePassword()" style="display:none; font-size: 12px; font-weight: 900; border-color:#111; color:#111;">PASSWORD</div>
<div id="btn-logout" class="btn" onclick="kasirLogout()" style="display:none; font-size: 12px; font-weight: 900; border-color:#ef4444; color:#ef4444;">LOGOUT</div>
</div>
</div>
<div id="hdr-store-address" style="font-size: 10px; color: #999; margin-top: 4px; font-weight: 500;"></div>
<div style="display:flex; gap:15px; margin-top:12px; align-items: center; width: 100%;">
<a id="hdr-social-ig" href="#" target="_blank"><img src="https://cdn-icons-png.flaticon.com/512/174/174855.png" width="18"></a>
<a id="hdr-social-tt" href="#" target="_blank"><img src="https://cdn-icons-png.flaticon.com/512/3046/3046121.png" width="18"></a>
<a id="hdr-social-maps" href="#" target="_blank"><img src="https://cdn-icons-png.flaticon.com/512/2991/2991231.png" width="18"></a>
<a id="hdr-social-link" href="#" target="_blank"><img src="https://cdn-icons-png.flaticon.com/512/9102/9102575.png" width="18"></a>
<a id="hdr-store-wa-link" class="link" style="margin-left: auto; font-size: 11px; cursor:pointer; color:inherit; text-decoration:none;" href="#" target="_blank">📞 <span id="hdr-store-wa-text">-</span></a>
</div>
</div>
<div id="sec-meja" class="page active">
<div id="meja-home">
<div class="card">
<div class="title-sm">Pilih Nomor Meja</div>
<div id="grid-meja" class="table-grid"></div>
<div class="title-sm" style="margin-top:14px;">Meja QR (Pelanggan)</div>
<div class="muted" style="margin-top:-6px; margin-bottom:8px;">Meja 110 khusus pesanan dari scan QR pelanggan.</div>
<div id="grid-meja-qr" class="table-grid"></div>
<div id="url-info" style="margin-top:20px; padding:15px; background:#f9fafb; border-radius:12px; font-size:11px; line-height:1.6; border:1px solid #eee;">
<div style="font-weight:900; color:#111; margin-bottom:8px; border-bottom:1px solid #ddd; padding-bottom:4px;">🔗 URL AKSES CEPAT</div>
<div id="url-list">Memuat URL...</div>
</div>
<div class="card" style="margin-top:12px; border:2px solid #e11d48; background:#fff1f2;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div>
<div style="font-weight:900; font-size:13px; color:#111;">KUNCI INFO WIFI 🔒</div>
<div style="font-size:10px; color:#666; margin-top:2px;">Pelanggan wajib isi Nama & WA untuk melihat password</div>
</div>
<div style="display:flex; align-items:center; gap:8px;">
<span id="wifi-lock-status" style="font-weight:900; font-size:10px; color:#e11d48;">AKTIF</span>
<button id="btn-toggle-wifi-lock" class="pill on" style="width:auto; padding:6px 12px; font-size:10px;" onclick="toggleWifiLock()">MATIKAN</button>
</div>
</div>
</div>
</div>
<div class="card">
<div class="title-sm">Daftar Pesanan Aktif</div>
<div id="list-active"></div>
</div>
</div>
<div id="order-view" style="display:none;">
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div class="title-sm" style="margin:0;">Pesanan: <span id="ov-meja"></span></div>
<div style="display:flex; gap:8px; overflow-x:auto; -webkit-overflow-scrolling:touch; max-width:70%; padding-bottom:4px;">
<button class="pill on" style="width:auto; padding:4px 10px; font-size:10px; background:#2563eb; color:white; border:none; flex:0 0 auto;" onclick="pindahMeja()">PINDAH MEJA</button>
<button class="pill on" style="width:auto; padding:4px 10px; font-size:10px; background:#111; color:white; border:none; flex:0 0 auto;" onclick="showQrCodeMeja()">QR MEJA</button>
<button class="pill on" style="width:auto; padding:4px 10px; font-size:10px; background:#f59e0b; color:white; border:none; flex:0 0 auto;" onclick="panggilPelayan(mejaAktif)">PANGGIL PELAYAN</button>
<button class="pill" style="width:auto; padding:4px 10px; font-size:10px; background:#fff; color:#e11d48; border:1px solid #e11d48; flex:0 0 auto;" onclick="closeOrderView()">KEMBALI</button>
</div>
</div>
<div style="margin-top:10px;">
<select id="ov-mode" onchange="applyMode()">
<option value="Pending">Langsung Makan (Pending)</option>
<option value="Booking">Booking (Reserved)</option>
<option value="Selesai">Langsung Bayar (Selesai)</option>
</select>
</div>
</div>
<div class="card">
<div class="title-sm">Data Pemesan</div>
<div class="row2">
<input id="ov-nama" placeholder="Nama">
<input id="ov-wa" placeholder="WhatsApp">
</div>
<div id="wa-poin-info" style="margin-top:6px; color:#2563eb; font-size:11px; display:none;"></div>
<div id="wa-review-info" style="margin-top:6px; font-size:11px; display:none;"></div>
<div class="row2" style="margin-top:8px;">
<div>
<small>Tanggal</small>
<input id="ov-tgl" type="date">
</div>
<div id="ov-jam-wrap" style="display:none;">
<small>Jam Booking</small>
<input id="ov-jam" type="time">
</div>
</div>
</div>
<div id="ov-dp" class="card" style="display:none;">
<div class="title-sm">DP Booking</div>
<div class="row2">
<input id="ov-dp-nilai" type="number" value="0" placeholder="Nilai DP">
<select id="ov-dp-metode">
<option value="Tunai">Tunai</option>
<option value="QRIS">QRIS</option>
</select>
</div>
<div style="margin-top:8px;">
<small>Tgl Bayar DP</small>
<input id="ov-dp-tgl" type="date">
</div>
</div>
<div class="row2-flex">
<div id="ov-menu" class="card" style="flex: 1.8;">
<div class="title-sm">Pilih Menu</div>
<div id="cat-container" class="cat-wrapper"></div>
<div id="menu-list" class="menu-grid"></div>
</div>
<div id="ov-cart" class="card" style="flex: 1;">
<div class="title-sm">Keranjang</div>
<div id="cart-list" style="margin-bottom:10px;"></div>
<div style="margin-bottom:15px;">
<small style="font-weight:700; color:#4b5563; font-size:11px; margin-bottom:4px; display:block;">CATATAN PESANAN </small>
<textarea id="ov-catatan" placeholder="Contoh: Minta cabe rawit, minta tambahan saos bbq, minta tambahan es batu, dll" style="width:100%; height:60px; border-radius:10px; border:1px solid #ddd; padding:10px; font-size:12px; font-family:inherit; outline:none; resize:none; box-sizing:border-box; background:#fff; color:#111;"></textarea>
</div>
<div class="summary">
<div class="sum-row"><span>Subtotal</span><span id="sum-subtotal">Rp 0</span></div>
<div class="sum-row" id="sum-poin-row" style="display:none;"><span>Anda mendapatkan poin</span><span id="sum-poin">0</span></div>
</div>
<div id="ov-actions" style="margin-top:10px;"></div>
</div>
</div>
</div>
</div>
<div id="sec-booking" class="page">
<div class="card">
<div class="title-sm">Daftar Booking</div>
<div id="list-booking"></div>
</div>
</div>
<div id="sec-laporan" class="page">
<div class="card">
<div class="title-sm">Grafik Penjualan (7 Hari Terakhir)</div>
<div style="height: 180px; position: relative;">
<canvas id="salesChart"></canvas>
</div>
</div>
<div class="card">
<div class="title-sm">Laporan Filter</div>
<div class="row2">
<div><small>Dari</small><input id="lap-dari" type="date" onchange="loadReport()"></div>
<div><small>Sampai</small><input id="lap-sampai" type="date" onchange="loadReport()"></div>
</div>
<div class="pill-row" style="margin-top:8px;">
<button class="pill" onclick="setLapRange('today')">HARI INI</button>
<button class="pill" onclick="setLapRange('week')">MINGGU INI</button>
<button class="pill" onclick="setLapRange('month')">BULAN INI</button>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:6px; margin-top:10px;">
<button class="btn-green" style="font-size:10px;" onclick="sendReportWA('daily')">Kirim Laporan Hari Ini</button>
<button class="btn-blue" style="font-size:10px;" onclick="sendReportWA('range')">Kirim Range Ini</button>
<button class="btn-dark" style="grid-column: 1 / -1; font-size:11px; font-weight:bold; padding:10px;" onclick="sendCombinedWA()">KIRIM WA GABUNGAN (OMZET + BELANJA + REKAP)</button>
</div>
</div>
<div class="row2">
<div class="card">
<div class="title-sm">Omzet</div>
<div style="font-weight:900; font-size:16px; color:#16a34a" id="lap-omzet">Rp 0</div>
</div>
<div class="card">
<div class="title-sm">Belanja</div>
<div style="font-weight:900; font-size:16px; color:#e11d48" id="lap-belanja">Rp 0</div>
</div>
</div>
<div class="card">
<div class="title-sm">Metode Pembayaran</div>
<div id="lap-methods" style="display:grid; grid-template-columns: repeat(2, 1fr); gap:8px;"></div>
</div>
<div class="card">
<div class="title-sm">Produk Terlaris</div>
<div id="lap-items" style="max-height: 250px; overflow-y: auto; border: 1px solid #eee; border-radius: 8px; padding: 5px; background: #fafafa;"></div>
</div>
</div>
<div id="sec-stok" class="page">
<div class="row2-flex">
<div style="flex: 1.5;">
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<div class="title-sm" style="margin:0;">Set Stok Awal</div>
<button class="btn-main" style="width:auto; padding:8px 15px;" onclick="simpanStokAwalBulk()">SIMPAN SEMUA</button>
</div>
<div style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; border-radius: 12px;">
<table style="width:100%; border-collapse: collapse; font-size: 13px;">
<thead style="position: sticky; top: 0; background: #f8fafc; z-index: 10;">
<tr style="border-bottom: 2px solid #eee;">
<th style="padding: 10px; text-align: left;">Menu</th>
<th style="padding: 10px; text-align: center; width: 100px;">Stok Awal</th>
</tr>
</thead>
<tbody id="stok-awal-table-body">
<!-- Rendered by JS -->
</tbody>
</table>
</div>
</div>
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; margin-bottom:10px;">
<div class="title-sm" style="margin:0;">Daftar Stok Saat Ini</div>
<div style="display:flex; gap:6px;">
<button class="btn-main" style="width:auto; padding:6px 12px; font-size:10px;" onclick="resetSemuaStok()">RESET TERPAKAI</button>
<button class="btn-green" style="width:auto; padding:6px 12px; font-size:10px;" onclick="sendStokWA()">KIRIM WA</button>
</div>
</div>
<div id="stok-list" class="stok-grid"></div>
</div>
</div>
<div style="flex: 1;">
<div class="card">
<div class="title-sm">Ringkasan Stok</div>
<div id="stok-summary" style="font-size:12px;"></div>
</div>
</div>
</div>
</div>
<div id="sec-belanja" class="page">
<div class="row2-flex">
<div style="flex: 1.5;">
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<div class="title-sm" style="margin:0;">Input Belanja Satuan</div>
<div style="display:flex; gap:6px;">
<button class="btn-green" style="width:auto; padding:4px 10px; font-size:10px;" onclick="exportBelanjaCSV()">EXPORT</button>
<button class="btn-blue" style="width:auto; padding:4px 10px; font-size:10px;" onclick="importBelanjaCSV()">IMPORT</button>
<button class="btn-dark" style="width:auto; padding:4px 10px; font-size:10px;" onclick="syncBelanjaOneWay()">SYNC</button>
</div>
</div>
<input type="hidden" id="bl-id">
<div class="row2">
<input id="bl-nama" placeholder="Nama Barang / Biaya">
<select id="bl-kat">
<option value="Modal">Modal</option>
<option value="Bawah">Bawah</option>
<option value="Dapur">Dapur</option>
<option value="Makan">Makan</option>
<option value="Operasional">Operasional</option>
<option value="Pegawai">Pegawai</option>
</select>
</div>
<div class="row2" style="margin-top:8px;">
<div><small>Qty</small><input id="bl-qty" type="number" value="1" oninput="recalcBelanja('qty')"></div>
<div><small>Harga Satuan</small><input id="bl-harga" type="number" oninput="recalcBelanja('harga')"></div>
</div>
<div style="margin-top:8px;">
<small>Total (Otomatis)</small>
<input id="bl-total" type="number" oninput="recalcBelanja('total')">
</div>
<div style="margin-top:10px;">
<button class="btn-main" onclick="simpanBelanjaSatuan()">SIMPAN SATUAN</button>
</div>
</div>
<div class="card">
<div class="title-sm">Input Belanja (Bulk)</div>
<textarea id="bl-bulk" placeholder="Format: Nama Qty Harga Total&#10;Contoh:&#10;Beras 1 15000 15000&#10;Es Batu 2 x 22000 (Otomatis Harga 11000)" style="width:100%; height:120px; border-radius:10px; border:1px solid #ddd; padding:10px; font-family:monospace;"></textarea>
<div class="row2" style="margin-top:8px;">
<div>
<small>Kategori Bulk</small>
<select id="bl-bulk-kat">
<option value="Modal">Modal</option>
<option value="Bawah">Bawah</option>
<option value="Dapur" selected>Dapur</option>
<option value="Makan">Makan</option>
<option value="Operasional">Operasional</option>
<option value="Pegawai">Pegawai</option>
</select>
</div>
<div style="display:flex; align-items:flex-end;">
<div style="width:100%; color:#64748b; font-size:11px; font-weight:800;">
Tips: tambah "Q" di depan/akhir nama untuk catat QRIS/Transfer.
</div>
</div>
</div>
<div style="margin-top:10px;">
<button class="btn-main" onclick="simpanBelanjaBulk()">SIMPAN BULK</button>
</div>
</div>
<div class="card">
<div class="title-sm">Riwayat Belanja Hari Ini</div>
<div id="bl-list-today" style="margin-top:10px;"></div>
</div>
</div>
<div style="flex: 1;">
<div class="card">
<div class="title-sm">Kalkulator</div>
<input id="calc-input" placeholder="Misal: 5000 + 3000 * 2" style="font-family:monospace; font-size:16px;">
<div id="calc-result" style="margin-top:10px; font-weight:900; font-size:18px; text-align:right;">= 0</div>
<div style="display:grid; grid-template-columns: repeat(4, 1fr); gap:6px; margin-top:10px;">
<button class="btn-dark" onclick="calcAdd('7')">7</button><button class="btn-dark" onclick="calcAdd('8')">8</button><button class="btn-dark" onclick="calcAdd('9')">9</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('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'">BERSIHKAN</button>
</div>
</div>
<div class="card">
<div class="title-sm">Pencarian Riwayat (Global)</div>
<input id="bl-search-text" placeholder="Cari belanja / transaksi..." style="margin-bottom:8px;" oninput="renderGlobalSearch()">
<div id="bl-list-search" style="max-height:220px; overflow-y:auto;"></div>
<div id="tx-list-search" style="max-height:220px; overflow-y:auto; margin-top:10px;"></div>
</div>
</div>
</div>
</div>
<div id="sec-dapur" class="page">
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center;">
<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>
</div>
<div id="dapur-list" style="margin-top:15px; display:grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap:12px;"></div>
</div>
</div>
<div id="sec-rekap" class="page">
<div class="card">
<div class="title-sm">Rekap Kasir Hari Ini</div>
<div class="row2">
<div>
<small>Saldo Awal (Kasir Buka)</small>
<input id="rk-saldo-awal" type="number" placeholder="Input saldo awal">
</div>
<div style="display:flex; align-items:flex-end;">
<button class="btn-main" onclick="simpanSaldoAwal()">SIMPAN SALDO</button>
</div>
</div>
</div>
<div class="row2">
<div class="card">
<div class="title-sm">Penjualan per Metode</div>
<div id="rk-methods"></div>
</div>
<div class="card">
<div class="title-sm">Porsi Terjual</div>
<div id="rk-potensi-profit" class="muted" style="font-weight:900; margin-bottom:6px;">Potensi Profit (Estimasi): Rp 0</div>
<div id="rk-portions"></div>
</div>
</div>
<div class="card">
<div class="title-sm">Catatan Rekap</div>
<textarea id="rk-catatan" placeholder="Tambahkan catatan jika ada..." style="width:100%; height:60px; border-radius:10px; border:1px solid #ddd; padding:10px;"></textarea>
<div style="margin-top:10px;">
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn-green" onclick="sendRekapWA()">KIRIM REKAP WA</button>
<button class="btn-blue" onclick="exportPorsiTerjualCSV()">EXPORT CSV PORSI</button>
</div>
</div>
</div>
</div>
<div id="sec-poin" class="page">
<div class="card" style="max-width: 500px; margin-left: auto; margin-right: auto;">
<div class="title-sm">Data Pelanggan</div>
<input id="cust-search" placeholder="Cari nama / WA" oninput="renderCustomers()">
</div>
<div id="cust-list" style="max-width: 500px; margin-left: auto; margin-right: auto;"></div>
</div>
<div id="sec-riwayat" class="page">
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<div class="title-sm" style="margin:0;">Riwayat</div>
<div style="display:flex; gap:6px;">
<button class="btn-blue" style="width:auto; padding:6px 12px; font-size:11px;" onclick="importRiwayatCSV()">IMPORT CSV</button>
<button class="btn-green" style="width:auto; padding:6px 12px; font-size:11px;" onclick="exportRiwayatCSV()">EXPORT CSV</button>
<button class="btn-red" style="width:auto; padding:6px 12px; font-size:11px; background:#fee2e2; color:#ef4444; border:none;" onclick="clearRiwayatKeepLast()">HAPUS RIWAYAT</button>
</div>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-top:10px;">
<input id="his-search" placeholder="Cari ID / Meja / Nama" oninput="renderHistory()">
<select id="his-filter-metode" onchange="renderHistory()" style="width:100%; padding:10px; border-radius:10px; border:1px solid #ddd; font-size:13px;">
<option value="">-- Semua Metode --</option>
<option value="Tunai">Tunai</option>
<option value="QRIS">QRIS</option>
<option value="Debit">Debit</option>
<option value="Credit">Credit</option>
<option value="Transfer">Transfer</option>
</select>
</div>
<div style="margin-top:10px; display:flex; align-items:center; gap:8px;">
<input type="checkbox" id="his-filter-bukti" onchange="renderHistory()" style="width:auto; height:auto; margin:0;">
<label for="his-filter-bukti" style="font-size:12px; color:#444; font-weight:bold; cursor:pointer;">Hanya yang ada bukti bayar 📷</label>
</div>
<button class="btn-green" style="width:100%; margin-top:10px; font-weight:bold; padding:12px;" onclick="sendDailyHistoryWA()">🚀 KIRIM RIWAYAT HARI INI KE WA</button>
</div>
<div id="his-list"></div>
</div>
<div id="sec-supplier" class="page">
<div class="row2-flex">
<div style="flex: 1.2;">
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<div class="title-sm" style="margin:0;">Database Master Supplier</div>
<button class="pill on" style="width:auto; padding:4px 10px; font-size:10px;" onclick="toggleAddSupplier()">+ TAMBAH BARU</button>
</div>
<div id="add-sup-form" style="display:none; margin-bottom:15px; background:#f9fafb; padding:10px; border-radius:10px;">
<div class="row2">
<input id="sup-master-id" type="hidden">
<input id="sup-master-nama" placeholder="Nama Supplier (Baru)">
<input id="sup-master-wa" placeholder="WA (08xxxxxxxx)">
</div>
<div style="margin-top:8px;">
<input id="sup-master-rek" placeholder="Rekening (Bank - No Rek - Nama)">
</div>
<div style="margin-top:10px;">
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button id="sup-master-save-btn" class="btn-main" onclick="simpanMasterSupplier()">SIMPAN KE DATABASE</button>
<button id="sup-master-cancel-btn" class="btn-dark" style="display:none; width:auto;" onclick="batalEditSupplier()">BATAL EDIT</button>
</div>
</div>
</div>
<input id="sup-master-search" placeholder="Cari Master Supplier..." oninput="renderSuppliersMaster()" style="margin-bottom:10px;">
<div id="sup-master-list" style="max-height: 300px; overflow-y: auto;"></div>
</div>
<div id="card-kirim-wa" class="card" style="display:none; border:2px solid #22c55e;">
<div class="title-sm">Kirim Pesan WA ke <span id="target-sup-nama">...</span></div>
<div class="row2">
<div>
<small>Supplier Terpilih</small>
<input id="sup-selected-display" readonly style="background:#f3f4f6;">
<input id="sup-select" type="hidden">
</div>
<div>
<small>WhatsApp (Otomatis)</small>
<input id="sup-wa" placeholder="08xxxxxxxxxx" readonly style="background:#f3f4f6;">
</div>
</div>
<div style="margin-top:8px;">
<small>Rekening (Otomatis)</small>
<input id="sup-rek" readonly style="background:#f3f4f6;">
</div>
<div id="sup-deposit-box" style="margin-top:10px; padding:10px; border:1px dashed #e2e8f0; border-radius:12px; background:#f8fafc;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<small style="font-weight:900;">Deposit/Potong (khusus Es Batu / Galon)</small>
<small id="sup-deposit-status" style="color:#999; font-weight:900;">NONAKTIF</small>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px; margin-top:8px;">
<input id="sup-deposit" type="number" placeholder="Deposit (Rp)">
<input id="sup-deposit-potong" type="number" placeholder="Potong (Rp)">
</div>
</div>
<div style="margin-top:8px;">
<small>Pesan (Paragraf)</small>
<textarea id="sup-pesan" placeholder="Tulis pesan yang ingin dikirim..." style="width:100%; height:120px; border-radius:10px; border:1px solid #ddd; padding:10px;"></textarea>
</div>
<div style="margin-top:10px;">
<button class="btn-green" onclick="kirimWASupplier()">KIRIM WA & SIMPAN RIWAYAT</button>
</div>
</div>
</div>
<div style="flex: 1;">
<div class="card">
<div class="title-sm">Riwayat Pengiriman WA</div>
<input id="sup-search" placeholder="Cari Riwayat..." oninput="renderSupplierHistory()" style="margin-bottom:10px;">
<div id="sup-history-list" style="max-height: 500px; overflow-y: auto;"></div>
</div>
</div>
</div>
</div>
<div id="sec-pelayan" class="page">
<div class="card">
<div class="title-sm">Manajemen Database Pelayan</div>
<div class="row2">
<input id="pel-nama" placeholder="Nama Pelayan">
<input id="pel-wa" placeholder="WA (08xxxxxxxx)">
</div>
<div style="margin-top:10px;">
<button class="btn-main" onclick="simpanPelayan()">SIMPAN PELAYAN</button>
</div>
</div>
<div class="card">
<div class="title-sm">Daftar Pelayan Aktif</div>
<div id="pel-list" style="max-height: 400px; overflow-y: auto;"></div>
</div>
</div>
<div id="sec-users" class="page">
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<div class="title-sm" style="margin:0;">Manajemen User Kasir</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn-green" style="width:auto; padding:6px 12px; font-size:11px;" onclick="renderUsersAdmin(true)">REFRESH</button>
<button class="btn-blue" style="width:auto; padding:6px 12px; font-size:11px;" onclick="promptAddUser()">TAMBAH USER</button>
</div>
</div>
<div id="users-list" style="margin-top:12px;"></div>
</div>
</div>
<div id="sec-paket" class="page">
<div class="row2-flex">
<div style="flex: 1.5;">
<div class="card">
<div class="title-sm">Simulasi Paket Kustom</div>
<div style="margin-bottom: 15px;">
<small>Nama Simulasi Paket</small>
<input type="text" id="custom-paket-name" placeholder="Contoh: Simulasi Paket A">
</div>
<div style="margin-bottom: 10px;">
<small>Cari Menu</small>
<input type="text" id="paket-menu-search" placeholder="Ketik nama menu..." oninput="renderPaketMenuList()">
</div>
<div id="paket-cat-container" class="cat-wrapper" style="margin-bottom: 10px;"></div>
<div id="all-menu-for-paket" class="menu-grid" style="max-height: 400px; overflow-y: auto; padding: 5px; border: 1px solid #eee; border-radius: 12px;">
<!-- All menu items will be rendered here for selection -->
</div>
</div>
</div>
<div style="flex: 1;">
<div class="card">
<div class="title-sm">Item Terpilih</div>
<div id="selected-paket-items" style="min-height: 100px; border-bottom: 1px dashed #eee; margin-bottom: 10px;">
<div class="muted" style="text-align:center; padding:20px;">Belum ada menu yang dipilih.</div>
</div>
<div class="summary" style="margin-bottom:15px;">
<div class="sum-strong"><span>Total Harga Asli</span><span id="custom-paket-total-asli">Rp 0</span></div>
</div>
<div style="margin-bottom: 10px;">
<label class="title-sm" style="display:block; margin-bottom:5px;">Harga Jual Paket (Rp)</label>
<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>
<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>
</div>
<div class="card">
<div class="title-sm">Daftar Paket Simulasi Tersimpan</div>
<div id="saved-paket-list" style="max-height: 300px; overflow-y: auto;">
<!-- Saved packages will be rendered here -->
</div>
</div>
</div>
</div>
</div>
<div id="modal" class="modal" onclick="if(event.target.id==='modal') closeModal()">
<div class="sheet">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div class="title-sm" style="margin:0;">Lihat Pesanan</div>
<div class="close" onclick="closeModal()">TUTUP</div>
</div>
<div id="modal-body"></div>
</div>
</div>
<div id="bottom-nav" class="bottom-nav">
<button class="nav-btn active" onclick="showTab('sec-meja', this)"><span class="icon">#</span>MEJA</button>
<button class="nav-btn" onclick="showTab('sec-booking', this)"><span class="icon">#</span>BOOKING</button>
<button class="nav-btn" onclick="showTab('sec-laporan', this)"><span class="icon">#</span>LAPORAN</button>
<button class="nav-btn" onclick="showTab('sec-belanja', this)"><span class="icon">#</span>BELANJA</button>
<button class="nav-btn" onclick="showTab('sec-stok', this)"><span class="icon">#</span>STOK</button>
<button class="nav-btn" onclick="showTab('sec-dapur', this)"><span class="icon">#</span>DAPUR</button>
<button class="nav-btn" onclick="showTab('sec-rekap', this)"><span class="icon">#</span>REKAP</button>
<button class="nav-btn" onclick="showTab('sec-poin', this)"><span class="icon">#</span>POIN</button>
<button class="nav-btn" onclick="showTab('sec-riwayat', this)"><span class="icon">#</span>RIWAYAT</button>
<button class="nav-btn" onclick="showTab('sec-supplier', this)"><span class="icon">#</span>SUPPLIER</button>
<button class="nav-btn" onclick="showTab('sec-pelayan', this)"><span class="icon">#</span>PELAYAN</button>
<button id="nav-users" class="nav-btn" onclick="showTab('sec-users', this)" style="display:none;"><span class="icon">#</span>USERS</button>
<button class="nav-btn" onclick="openChatModal()"><span class="icon">#</span>CHAT</button>
<button class="nav-btn" onclick="showTab('sec-paket', this)" style="position:relative;">
<span class="icon">#</span>PAKET
<span id="badge-paket" class="badge" style="display:none; top:-2px; right:2px;">0</span>
</button>
</div>
<div id="sync-indicator" style="position:fixed; top:calc(var(--header-h, 60px) + 5px); right:10px; background:rgba(0,0,0,0.6); color:white; padding:4px 8px; border-radius:10px; font-size:9px; z-index:1000; display:none;">
<span class="spinner-sm"></span> Syncing...
</div>
<script>
var menu = [];
var transaksi = [];
var pelanggan = [];
var belanja = [];
var rekap = [];
var supplierHistory = [];
var suppliersMaster = [];
var waiters = [];
var customPaketItems = []; // Menyimpan item yang dipilih untuk paket kustom
var settings = {}; // Menyimpan pengaturan global
var scriptUrl = '';
var kategoriAktif = 'Semua';
var mejaAktif = null;
var transAktifId = null;
var keranjang = [];
var pendingSnapshot = '';
var lastReset = '';
var isAdmin = false; // Status Login Kasir
var currentUserRole = '';
var currentUsername = '';
var superAuth = null;
var isCustomerMode = false; // Mode Pelanggan (via QR)
var isKitchenMode = false; // Mode Khusus Dapur
function updateLayoutOffsets() {
try {
var header = document.getElementById('header-app');
var h = 0;
if (header && header.style.display !== 'none') {
h = Math.ceil(header.getBoundingClientRect().height || 0);
}
document.documentElement.style.setProperty('--header-h', (h || 0) + 'px');
} catch (e) {}
}
try {
window.addEventListener('resize', function() { setTimeout(updateLayoutOffsets, 50); });
window.addEventListener('orientationchange', function() { setTimeout(updateLayoutOffsets, 50); });
} catch (e) {}
var lastChatCount = 0;
var chatBeepInterval = null;
// --- SISTEM LOCAL STORAGE & OFFLINE (IndexedDB) ---
var db = null;
var dbName = "POSFukuLocalDB";
var dbVersion = 1;
function initLocalDB(callback) {
var request = indexedDB.open(dbName, dbVersion);
request.onupgradeneeded = function(e) {
var db = e.target.result;
if (!db.objectStoreNames.contains("dataCache")) {
db.createObjectStore("dataCache", { keyPath: "key" });
}
if (!db.objectStoreNames.contains("pendingSync")) {
db.createObjectStore("pendingSync", { keyPath: "id", autoIncrement: true });
}
};
request.onsuccess = function(e) {
db = e.target.result;
console.log("IndexedDB siap");
if (callback) callback();
};
request.onerror = function(e) { console.error("IndexedDB Error", e); };
}
function saveLocal(key, data) {
if (!db) return;
var tx = db.transaction("dataCache", "readwrite");
tx.objectStore("dataCache").put({ key: key, value: data, timestamp: Date.now() });
}
function getLocal(key, callback) {
if (!db) { callback(null); return; }
var tx = db.transaction("dataCache", "readonly");
var request = tx.objectStore("dataCache").get(key);
request.onsuccess = function(e) {
callback(e.target.result ? e.target.result.value : null);
};
}
// --- MOCK SERVER UNTUK LOCALHOST ---
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || !window.google || !google.script) {
console.warn("Aplikasi berjalan di LOCALHOST / Mode Offline. Menggunakan Mock Server.");
if (!window.google) window.google = {};
if (!google.script) google.script = {
run: {
withSuccessHandler: function(s) {
this._success = s;
return this;
},
withFailureHandler: function(f) {
this._failure = f;
return this;
},
// Mock functions
getInitialData: function() {
var self = this;
setTimeout(function() {
getLocal("initialData", function(cached) {
if (cached) self._success(cached);
else self._failure({ message: "Tidak ada data lokal. Hubungkan internet sekali untuk sync awal." });
});
}, 500);
},
saveTransaction: function(p) {
var self = this;
setTimeout(function() {
// Simpan lokal saja
var idx = transaksi.findIndex(function(t) { return t.id === p.id; });
if (idx > -1) transaksi[idx] = p; else transaksi.push(p);
saveLocal("initialData", { menu: menu, transaksi: transaksi, pelanggan: pelanggan, rekap: rekap, belanja: belanja });
self._success({ success: true, mode: "LocalOnly" });
}, 300);
},
// Tambahkan mock lainnya jika perlu
getBellStatus: function() { this._success({ status: "Inactive" }); },
getChatMessages: function() { this._success([]); }
}
};
// Tambahkan sisanya secara otomatis agar tidak error
var mockFns = ['setupAman', 'resetTotal', 'resetBell', 'markChatAsRead', 'uploadPaymentProof', 'voidTransaction', 'saveStokBulk', 'saveBelanjaBulk', 'saveRekap'];
mockFns.forEach(function(fn) {
if (!google.script.run[fn]) {
google.script.run[fn] = function() {
console.log("Mock Call:", fn, arguments);
if (this._success) this._success({ success: true, note: "Mock response" });
};
}
});
}
// Fitur Offline & Sinkronisasi
var pendingSync = JSON.parse(localStorage.getItem('fuku_pending_sync') || '[]');
var isOnline = navigator.onLine;
window.addEventListener('online', function() {
isOnline = true;
updateConnectionStatus();
playOnlineSound();
syncPendingQueue();
});
window.addEventListener('offline', function() {
isOnline = false;
updateConnectionStatus();
playOfflineSound();
});
function playOnlineSound() {
if (!audioContext) return;
var osc = audioContext.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, audioContext.currentTime);
osc.frequency.exponentialRampToValueAtTime(1200, audioContext.currentTime + 0.1);
var gain = audioContext.createGain();
gain.gain.setValueAtTime(0.2, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
osc.connect(gain);
gain.connect(audioContext.destination);
osc.start();
osc.stop(audioContext.currentTime + 0.3);
}
function playOfflineSound() {
if (!audioContext) return;
var osc = audioContext.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, audioContext.currentTime);
osc.frequency.exponentialRampToValueAtTime(600, audioContext.currentTime + 0.2);
var gain = audioContext.createGain();
gain.gain.setValueAtTime(0.2, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4);
osc.connect(gain);
gain.connect(audioContext.destination);
osc.start();
osc.stop(audioContext.currentTime + 0.4);
}
function updateConnectionStatus() {
var el = document.getElementById('sync-indicator');
var badge = document.getElementById('conn-badge');
if (isOnline) {
if (badge) {
badge.innerText = 'ONLINE';
badge.style.background = '#16a34a';
}
if (el) {
el.style.background = 'rgba(0,0,0,0.6)';
if (pendingSync.length === 0) el.style.display = 'none';
else {
el.style.display = 'block';
el.innerHTML = '<span class="spinner-sm"></span> Sinkronisasi (' + pendingSync.length + ')';
}
}
} else {
if (badge) {
badge.innerText = 'OFFLINE';
badge.style.background = '#ef4444';
}
if (el) {
el.style.display = 'block';
el.style.background = '#ef4444';
el.innerHTML = '🚫 OFFLINE - Data Tersimpan Lokal (' + pendingSync.length + ')';
}
}
}
function syncPendingQueue() {
if (!isOnline || pendingSync.length === 0) return;
var item = pendingSync[0];
updateConnectionStatus();
google.script.run.withSuccessHandler(function(res) {
pendingSync.shift();
localStorage.setItem('fuku_pending_sync', JSON.stringify(pendingSync));
if (pendingSync.length > 0) {
syncPendingQueue(); // Kirim item selanjutnya
} else {
updateConnectionStatus();
saveLocal("initialData", res); // Update IndexedDB Cache
updateLocalData(res);
alert('Berhasil: Semua data offline telah disinkronkan ke server!');
}
}).withFailureHandler(function(err) {
console.error('Gagal sinkronisasi antrian:', err);
updateConnectionStatus();
})[item.func](item.args);
}
function safeRun(funcName, args, success, failure) {
// Anggap fungsi yang diawali 'save', 'update', 'add', 'mark', 'delete', 'void' sebagai aksi yang perlu antrian offline
var isAction = /^(save|update|add|mark|delete|void)/.test(funcName);
if (!isOnline && isAction) {
// Mode Offline - Simpan ke Antrian
pendingSync.push({ func: funcName, args: args, time: Date.now() });
localStorage.setItem('fuku_pending_sync', JSON.stringify(pendingSync));
updateConnectionStatus();
// Update Data Lokal secara Optimis (untuk UI instan)
if (funcName === 'saveTransaction') {
var idx = transaksi.findIndex(function(t) { return t.id === args.id; });
if (idx > -1) transaksi[idx] = args; else transaksi.push(args);
renderTables(); renderActiveList(); renderHistory();
} else if (funcName === 'saveBelanjaBulk') {
if (Array.isArray(args)) belanja = belanja.concat(args);
renderBelanja();
} else if (funcName === 'updateKitchenStatus') {
// args untuk ini biasanya adalah [id, status] jika kita ubah pemanggilannya
var id = Array.isArray(args) ? args[0] : args;
var status = Array.isArray(args) ? args[1] : 'Ready';
var idx = transaksi.findIndex(function(t) { return t.id === id; });
if (idx > -1) transaksi[idx].status = status;
renderActiveList();
}
if (success) success({ transaksi: transaksi, menu: menu, pelanggan: pelanggan, rekap: rekap, belanja: belanja });
return;
}
// Mode Online - Kirim Langsung
var runner = google.script.run.withSuccessHandler(function(res) {
if (success) success(res);
}).withFailureHandler(function(err) {
if (failure) failure(err);
else alert('Gagal: ' + err.message);
});
// Handle multiple arguments jika args adalah array (kecuali untuk fungsi yang memang kirim array sebagai argumen pertama)
var arrayFunctions = ['saveStokBulk', 'saveBelanjaBulk'];
if (Array.isArray(args) && arrayFunctions.indexOf(funcName) === -1) {
runner[funcName].apply(runner, args);
} else {
runner[funcName](args);
}
}
function openDataModal() {
var modal = document.getElementById('data-modal');
var info = document.getElementById('data-modal-info');
if (info) {
var host = window.location.hostname || '-';
var cacheTime = localStorage.getItem('fuku_cache_time');
var cacheText = cacheTime ? new Date(Number(cacheTime)).toLocaleString() : '-';
info.innerHTML =
'<div><b>Host:</b> ' + host + '</div>' +
'<div><b>Status:</b> ' + (isOnline ? 'ONLINE' : 'OFFLINE') + '</div>' +
'<div><b>Cache Time:</b> ' + cacheText + '</div>' +
'<div><b>Antrian Offline:</b> ' + (pendingSync ? pendingSync.length : 0) + '</div>';
}
if (modal) modal.style.display = 'flex';
}
function closeDataModal() {
var modal = document.getElementById('data-modal');
if (modal) modal.style.display = 'none';
}
function exportLocalData() {
var doExport = function(data) {
if (!data) { alert('Tidak ada data lokal untuk di-export.'); return; }
var payload = {
schema: 1,
exportedAt: new Date().toISOString(),
initialData: data,
pendingSync: pendingSync || []
};
var json = JSON.stringify(payload);
var blob = new Blob([json], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
var stamp = new Date().toISOString().replace(/[:.]/g, '-');
a.href = url;
a.download = 'POSFuku_cache_' + stamp + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(url); }, 500);
};
if (!db) {
initLocalDB(function() {
getLocal("initialData", function(cached) {
if (cached) doExport(cached);
else {
var ls = localStorage.getItem('fuku_data_cache');
if (ls) { try { doExport(JSON.parse(ls)); } catch(e) { doExport(null); } }
else doExport(null);
}
});
});
return;
}
getLocal("initialData", function(cached) {
if (cached) doExport(cached);
else {
var ls = localStorage.getItem('fuku_data_cache');
if (ls) { try { doExport(JSON.parse(ls)); } catch(e) { doExport(null); } }
else doExport(null);
}
});
}
function triggerImportLocalData() {
var input = document.getElementById('import-local-file');
if (input) input.click();
}
function handleImportLocalFile(event) {
var file = event && event.target ? event.target.files[0] : null;
if (!file) return;
var reader = new FileReader();
reader.onload = function(e) {
var text = e.target.result;
var parsed = null;
try { parsed = JSON.parse(text); } catch(err) { alert('File JSON tidak valid.'); return; }
var initialData = parsed && parsed.initialData ? parsed.initialData : parsed;
if (!initialData || !initialData.menu || !initialData.transaksi) {
alert('Struktur data tidak cocok. Pastikan file berasal dari tombol EXPORT.');
return;
}
var applyData = function() {
saveLocal("initialData", initialData);
try { localStorage.setItem('fuku_data_cache', JSON.stringify(initialData)); } catch(ex) {}
try { localStorage.setItem('fuku_cache_time', String(Date.now())); } catch(ex2) {}
if (parsed && Array.isArray(parsed.pendingSync)) {
pendingSync = parsed.pendingSync;
localStorage.setItem('fuku_pending_sync', JSON.stringify(pendingSync));
}
updateLocalData(initialData);
updateConnectionStatus();
alert('Import berhasil. Data lokal sudah diterapkan di perangkat ini.');
closeDataModal();
};
if (!db) initLocalDB(applyData);
else applyData();
};
reader.readAsText(file);
event.target.value = '';
}
var __kasirAppStarted = false;
var __activePollStarted = false;
var __activePollSnapshot = '';
function mergeTransaksiLite(list) {
if (!Array.isArray(list)) return false;
var changed = false;
list.forEach(function(t) {
if (!t || !t.id) return;
var idx = transaksi.findIndex(function(x) { return String(x.id) === String(t.id); });
if (idx > -1) {
var oldStr = JSON.stringify(transaksi[idx] || {});
var newStr = JSON.stringify(t || {});
if (oldStr !== newStr) { transaksi[idx] = t; changed = true; }
} else {
transaksi.push(t);
changed = true;
}
});
return changed;
}
function startActivePolling() {
if (__activePollStarted) return;
__activePollStarted = true;
setInterval(function() {
try {
if (!isOnline) return;
if (document.hidden) return;
google.script.run.withSuccessHandler(function(res) {
if (!res || res.error) return;
var list = res.transaksi || [];
var snap = JSON.stringify(list.map(function(t) { return [t.id, t.status, t.total, t.timestamp]; }));
if (snap === __activePollSnapshot) return;
__activePollSnapshot = snap;
var changed = mergeTransaksiLite(list);
if (changed) {
renderTables();
renderActiveList();
renderBookingTab();
}
}).withFailureHandler(function(err) {
}).getActiveTransaksiLite();
} catch (e) {}
}, 8000);
}
function startKasirApp() {
if (__kasirAppStarted) return;
__kasirAppStarted = true;
updateLayoutOffsets();
initLocalDB(function() {
checkUrlMeja();
initAutoAudioUnlock();
syncData(true, function() {
checkUrlMeja();
initAutoAudioUnlock();
updateConnectionStatus();
if (pendingSync.length > 0) syncPendingQueue();
});
});
setInterval(checkNewChat, 10000);
startActivePolling();
}
function showKasirLogin() {
showLoading(false);
updateLayoutOffsets();
var overlay = document.getElementById('kasir-login');
if (overlay) overlay.style.display = 'flex';
var u = document.getElementById('kasir-user');
var p = document.getElementById('kasir-pass');
if (u) u.value = localStorage.getItem('posfuku_kasir_user') || '';
if (p) p.value = '';
setTimeout(function() { try { if (u) u.focus(); } catch(e) {} }, 100);
}
function kasirDoLogin() {
var u = String(document.getElementById('kasir-user').value || '').trim();
var p = String(document.getElementById('kasir-pass').value || '');
if (!u || !p) { alert('Username dan password wajib diisi.'); return; }
showLoading(true, 'LOGIN...');
var runner = google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || !res.ok) {
alert('Login gagal.');
return;
}
localStorage.setItem('posfuku_kasir_user', u);
localStorage.setItem('posfuku_kasir_role', String(res.role || ''));
currentUsername = u;
currentUserRole = String(res.role || '');
isAdmin = (currentUserRole === 'admin' || currentUserRole === 'superadmin');
var overlay = document.getElementById('kasir-login');
if (overlay) overlay.style.display = 'none';
var btn = document.getElementById('btn-logout');
if (btn) btn.style.display = 'inline-block';
var btnPass = document.getElementById('btn-ganti-pass');
if (btnPass) btnPass.style.display = 'inline-block';
var navUsers = document.getElementById('nav-users');
if (navUsers) navUsers.style.display = (currentUserRole === 'superadmin' || currentUserRole === 'admin') ? '' : 'none';
startKasirApp();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal login: ' + (err && err.message ? err.message : err));
});
if (runner.loginUser) runner.loginUser(u, p);
else runner.loginAdmin(u, p);
}
function kasirChangePassword() {
if (!currentUsername) { alert('Harus login dulu.'); return; }
var mode = prompt('PASSWORD\n\n1) Ganti password sendiri\n2) Reset password user kasir (Superadmin)\n\nKetik 1 atau 2:', '1');
if (mode === null) return;
mode = String(mode || '').trim();
if (mode === '2') {
if (currentUserRole !== 'superadmin' && currentUserRole !== 'admin') {
alert('Hanya Superadmin yang bisa reset password user kasir.');
return;
}
var target = prompt('Reset Password Pengguna\n\nUsername target:', '');
if (target === null) return;
target = String(target || '').trim();
if (!target) { alert('Username target tidak boleh kosong.'); return; }
var newPass2 = prompt('Password baru untuk ' + target + ':');
if (newPass2 === null) return;
if (!String(newPass2 || '')) { alert('Password baru tidak boleh kosong.'); return; }
var confirm2 = prompt('Ulangi password baru:');
if (confirm2 === null) return;
if (String(confirm2 || '') !== String(newPass2 || '')) { alert('Konfirmasi password tidak sama.'); return; }
var superPass = prompt('Konfirmasi Password Superadmin (' + currentUsername + '):');
if (superPass === null) return;
if (!String(superPass || '')) { alert('Password superadmin tidak boleh kosong.'); return; }
showLoading(true, 'RESET PASSWORD...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || !res.ok) {
alert('Gagal reset password: ' + (res && res.error ? res.error : 'Tidak diketahui'));
return;
}
alert('Password berhasil direset untuk user: ' + target + '\\n\\nPassword baru: ' + newPass2);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal reset password: ' + (err && err.message ? err.message : err));
}).resetUserPasswordBySuperAdmin(currentUsername, superPass, target, newPass2);
return;
}
var oldPass = prompt('Password lama untuk ' + currentUsername + ':');
if (oldPass === null) return;
if (!String(oldPass || '')) { alert('Password lama tidak boleh kosong.'); return; }
var newPass = prompt('Password baru untuk ' + currentUsername + ':');
if (newPass === null) return;
if (!String(newPass || '')) { alert('Password baru tidak boleh kosong.'); return; }
var confirmPass = prompt('Ulangi password baru:');
if (confirmPass === null) return;
if (String(confirmPass || '') !== String(newPass || '')) { alert('Konfirmasi password tidak sama.'); return; }
showLoading(true, 'MENGUBAH PASSWORD...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || !res.ok) {
alert('Gagal mengubah password: ' + (res && res.error ? res.error : 'Tidak diketahui'));
return;
}
alert('Password berhasil diubah.\\n\\nSimpan password baru ini baik-baik.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal mengubah password: ' + (err && err.message ? err.message : err));
}).changeUserPassword(currentUsername, oldPass, newPass);
}
function kasirLogout() {
localStorage.removeItem('posfuku_kasir_user');
localStorage.removeItem('posfuku_kasir_role');
currentUsername = '';
currentUserRole = '';
isAdmin = false;
var btn = document.getElementById('btn-logout');
if (btn) btn.style.display = 'none';
var btnPass = document.getElementById('btn-ganti-pass');
if (btnPass) btnPass.style.display = 'none';
var navUsers = document.getElementById('nav-users');
if (navUsers) navUsers.style.display = 'none';
showKasirLogin();
}
function ensureSuperAuth(callback) {
if (currentUserRole !== 'superadmin' && currentUserRole !== 'admin') {
alert('Hanya Superadmin yang bisa mengakses fitur ini.');
return;
}
if (superAuth && superAuth.username && superAuth.password) {
callback(superAuth);
return;
}
var p = prompt('Konfirmasi Password Superadmin (' + currentUsername + '):');
if (p === null) return;
p = String(p || '');
if (!p) { alert('Password tidak boleh kosong.'); return; }
superAuth = { username: currentUsername, password: p };
callback(superAuth);
}
function renderUsersAdmin(forceReauth) {
var box = document.getElementById('users-list');
if (!box) return;
if (currentUserRole !== 'superadmin' && currentUserRole !== 'admin') {
box.innerHTML = '<div class="muted" style="text-align:center; padding:20px;">Hanya Superadmin yang bisa melihat daftar user.</div>';
return;
}
if (forceReauth) superAuth = null;
ensureSuperAuth(function(auth) {
showLoading(true, 'MEMUAT USERS...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || !res.ok) {
superAuth = null;
box.innerHTML = '<div class="muted" style="text-align:center; padding:20px;">Gagal memuat users.</div>';
alert(res && res.error ? res.error : 'Gagal memuat users');
return;
}
var users = res.users || [];
if (!users.length) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:20px;">Belum ada user.</div>';
return;
}
box.innerHTML = users.map(function(u) {
var role = String(u.role || '');
var unameJson = JSON.stringify(String(u.username || ''));
var actions = (u.username !== 'admin') ? ('<button class="btn-dark" style="width:auto; padding:6px 10px; font-size:11px;" onclick=\'promptResetUserPassword(' + unameJson + ')\'>RESET PASS</button>') : '';
return '<div class="card" style="padding:10px; margin-bottom:8px; border-left:4px solid #111;">' +
'<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">' +
'<div>' +
'<div style="font-weight:900;">' + u.username + '</div>' +
'<div class="muted" style="font-size:11px;">Role: ' + role + '</div>' +
'</div>' +
'<div style="display:flex; gap:8px;">' + actions + '</div>' +
'</div>' +
'</div>';
}).join('');
}).withFailureHandler(function(err) {
showLoading(false);
superAuth = null;
alert('Gagal memuat users: ' + (err && err.message ? err.message : err));
}).listAuthUsers(auth.username, auth.password);
});
}
function promptAddUser() {
if (currentUserRole !== 'superadmin' && currentUserRole !== 'admin') { alert('Hanya Superadmin yang bisa menambah user.'); return; }
ensureSuperAuth(function(auth) {
var u = prompt('Tambah User\n\nUsername (huruf/angka/._-):');
if (u === null) return;
u = String(u || '').trim();
if (!u) { alert('Username tidak boleh kosong.'); return; }
var p = prompt('Password untuk ' + u + ':');
if (p === null) return;
p = String(p || '');
if (!p) { alert('Password tidak boleh kosong.'); return; }
var role = prompt('Role user (kasir/admin):', 'kasir');
if (role === null) return;
role = String(role || '').trim().toLowerCase();
if (role !== 'admin') role = 'kasir';
showLoading(true, 'MENAMBAH USER...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || !res.ok) {
alert('Gagal tambah user: ' + (res && res.error ? res.error : ''));
return;
}
alert('User berhasil ditambahkan: ' + u);
renderUsersAdmin(false);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal tambah user: ' + (err && err.message ? err.message : err));
}).addAuthUser(auth.username, auth.password, u, p, role);
});
}
function promptResetUserPassword(targetUsername) {
if (currentUserRole !== 'superadmin' && currentUserRole !== 'admin') { alert('Hanya Superadmin yang bisa reset password user.'); return; }
ensureSuperAuth(function(auth) {
var u = String(targetUsername || '').trim();
if (!u) return;
var p = prompt('Reset Password\n\nPassword baru untuk ' + u + ':');
if (p === null) return;
p = String(p || '');
if (!p) { alert('Password baru tidak boleh kosong.'); return; }
var c = prompt('Ulangi password baru:');
if (c === null) return;
if (String(c || '') !== p) { alert('Konfirmasi password tidak sama.'); return; }
showLoading(true, 'RESET PASSWORD...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || !res.ok) {
alert('Gagal reset password: ' + (res && res.error ? res.error : ''));
return;
}
alert('Password user ' + u + ' sudah direset.\n\nPassword baru: ' + p);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal reset password: ' + (err && err.message ? err.message : err));
}).resetUserPasswordBySuperAdmin(auth.username, auth.password, u, p);
});
}
window.onload = function() {
var params = new URLSearchParams(window.location.search);
var bypassLogin = params.get('mode') === 'dapur' || params.get('meja');
if (bypassLogin) {
startKasirApp();
return;
}
var saved = localStorage.getItem('posfuku_kasir_user');
var savedRole = localStorage.getItem('posfuku_kasir_role');
if (saved && savedRole) {
currentUsername = saved;
currentUserRole = String(savedRole || '');
isAdmin = (currentUserRole === 'admin' || currentUserRole === 'superadmin');
var btn = document.getElementById('btn-logout');
if (btn) btn.style.display = 'inline-block';
var btnPass = document.getElementById('btn-ganti-pass');
if (btnPass) btnPass.style.display = 'inline-block';
var navUsers = document.getElementById('nav-users');
if (navUsers) navUsers.style.display = (currentUserRole === 'superadmin' || currentUserRole === 'admin') ? '' : 'none';
startKasirApp();
} else {
if (saved) localStorage.removeItem('posfuku_kasir_user');
if (savedRole) localStorage.removeItem('posfuku_kasir_role');
showKasirLogin();
}
};
function checkNewChat() {
google.script.run.withSuccessHandler(function(messages) {
// Cari apakah ada pesan baru dari Dapur yang belum dibaca
var unreadFromDapur = messages.some(function(m) {
return m.sender === 'Dapur' && m.status === 'Belum Dibaca';
});
if (unreadFromDapur) {
if (!chatBeepInterval) {
playChatSound(); // Bunyi langsung sekali
chatBeepInterval = setInterval(playChatSound, 3000); // Ulang tiap 3 detik
}
} else {
if (chatBeepInterval) {
clearInterval(chatBeepInterval);
chatBeepInterval = null;
}
}
lastChatCount = messages.length;
// Jika modal chat sedang terbuka, update UI dan tandai sudah dibaca
if (document.getElementById('chat-messages')) {
renderChatList(messages);
google.script.run.markChatAsRead('Kasir');
}
}).getChatMessages();
}
function checkUrlMeja() {
var params = new URLSearchParams(window.location.search);
// Mode Dapur Khusus
if (params.get('mode') === 'dapur') {
isKitchenMode = true;
var bNav = document.getElementById('bottom-nav');
var hApp = document.getElementById('header-app');
if (bNav) bNav.style.display = 'none';
if (hApp) hApp.style.display = 'none';
// Paksa pindah ke tab dapur
showTab('sec-dapur');
// Auto refresh data setiap 30 detik untuk dapur
if (!window.__kitchenInterval) {
window.__kitchenInterval = setInterval(function() { syncData(false); }, 30000);
}
return;
}
var meja = params.get('meja');
if (meja) {
isCustomerMode = true;
var bNav = document.getElementById('bottom-nav');
if (bNav) bNav.style.display = 'none';
// Jika menu masih kosong (sedang sync), tunggu sebentar lalu coba lagi
if (!menu || menu.length === 0) {
setTimeout(checkUrlMeja, 500);
return;
}
openTable(meja);
}
}
function showQrCodeMeja() {
var meja = mejaAktif;
if (!meja) return;
// Gunakan scriptUrl dari server jika ada, fallback ke URL saat ini
var baseUrl = scriptUrl || window.location.href.split('?')[0];
baseUrl = baseUrl.split('?')[0];
// Arahkan ke halaman order khusus pelanggan
var tableUrl = baseUrl + '?p=order&meja=' + encodeURIComponent(meja);
var qrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=' + encodeURIComponent(tableUrl);
var body = '<div style="text-align:center; padding:10px;">' +
'<div class="title-sm" style="margin-bottom:15px;">Scan QR: Meja ' + meja + '</div>' +
'<div id="qr-container" style="position:relative; width:220px; height:220px; margin:0 auto; background:#f9fafb; border-radius:12px; display:flex; align-items:center; justify-content:center; border:1px solid #eee;">' +
'<div id="qr-loading" style="position:absolute; font-size:12px; color:#666;">⌛ Sedang memuat QR...</div>' +
'<img src="' + qrUrl + '" style="width:200px; height:200px; position:relative; z-index:1; border-radius:8px;" onload="document.getElementById(\'qr-loading\').style.display=\'none\'" onerror="document.getElementById(\'qr-loading\').innerText=\'Gagal memuat QR Code\'">' +
'</div>' +
'<div class="muted" style="margin-top:15px; font-size:11px;">Pelanggan bisa langsung pesan dari HP mereka</div>' +
'<div style="font-size:9px; word-break:break-all; margin-top:8px; color:#2563eb; background:#eff6ff; padding:6px; border-radius:6px; border:1px solid #dbeafe;">' + tableUrl + '</div>' +
'</div>';
document.getElementById('modal').style.display = 'flex';
document.getElementById('modal-body').innerHTML = body;
}
function showLoading(show, msg) {
var el = document.getElementById('loading');
el.style.display = show ? 'flex' : 'none';
if (msg) document.getElementById('loading-text').innerText = msg;
}
var currentStrukRaw = ""; // Global variable to store current receipt text
function showImageModal(src, raw) {
currentStrukRaw = raw;
document.getElementById('struk-img').src = src;
document.getElementById('image-modal').style.display = 'flex';
}
function copyStrukRaw() {
if (!currentStrukRaw) return;
var textArea = document.createElement("textarea");
textArea.value = currentStrukRaw;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('Berhasil menyalin data teks struk! Silakan paste di aplikasi printer Anda.');
} catch (err) {
alert('Gagal menyalin teks.');
}
document.body.removeChild(textArea);
}
function jalankanSetupAman() {
var btn = document.getElementById('btn-setup-aman');
btn.disabled = true;
btn.innerText = 'SEDANG SETUP...';
google.script.run.withSuccessHandler(function(msg) {
alert(msg);
syncData(true);
}).withFailureHandler(function(err) {
alert('Gagal Setup: ' + err.message);
btn.disabled = false;
btn.innerText = 'SETUP AMAN';
}).setupAman();
}
function jalankanResetTotal() {
if(!confirm('RESET TOTAL akan menghapus sheet Menu/Transaksi/Pelanggan dan membuat ulang dari kosong. Lanjutkan?')) return;
var btn = document.getElementById('btn-reset-total');
btn.disabled = true;
btn.innerText = 'RESET...';
google.script.run.withSuccessHandler(function(msg) {
alert(msg);
syncData(true);
}).withFailureHandler(function(err) {
alert('Gagal Reset: ' + err.message);
btn.disabled = false;
btn.innerText = 'RESET TOTAL';
}).resetTotal();
}
function syncData(isInit, cb) {
var hasAnyCache = !!localStorage.getItem('fuku_data_cache');
if (isInit) {
// 1. Coba ambil dari IndexedDB (Tercepat)
getLocal("initialData", function(cached) {
if (cached) {
hasAnyCache = true;
updateLocalData(cached);
showLoading(false);
document.getElementById('sync-indicator').style.display = 'block';
if (cb) cb();
} else {
// 2. Fallback ke LocalStorage (Kompatibilitas)
var oldCached = localStorage.getItem('fuku_data_cache');
if (oldCached) {
try {
var res = JSON.parse(oldCached);
hasAnyCache = true;
updateLocalData(res);
showLoading(false);
document.getElementById('sync-indicator').style.display = 'block';
if (cb) cb();
} catch(e) { showLoading(true, 'SINKRONISASI DATA FUKU...'); }
} else {
showLoading(true, 'SINKRONISASI DATA FUKU...');
}
}
});
} else {
document.getElementById('sync-indicator').style.display = 'block';
}
if (!__hasGoogleRun) {
console.warn('google.script.run unavailable: local-only mode');
showLoading(false);
if (!hasAnyCache) {
updateLocalData({
menu: [],
transaksi: [],
pelanggan: [],
belanja: [],
rekap: [],
supplierHistory: [],
suppliersMaster: [],
waiters: [],
paketKustom: [],
settings: { wifi_lock: 'false' },
lastReset: '',
scriptUrl: ''
});
alert('Tidak ada data lokal. Aplikasi berjalan dengan data kosong di localhost. Untuk menggunakan data dari WebApp online, lakukan IMPORT dari menu DATA setelah membuka WebApp online.');
try { openDataModal(); } catch(e) {}
}
return;
}
var didTimeout = false;
var timeoutId = setTimeout(function() {
didTimeout = true;
document.getElementById('sync-indicator').style.display = 'none';
showLoading(false);
alert('Koneksi ke server terlalu lama (timeout).\n\nCoba:\n- Refresh halaman\n- Pastikan internet stabil\n- Jika baru update script, deploy ulang versi terbaru\n\nJika masih gagal, coba buka dari Chrome (bukan browser in-app).');
}, 25000);
google.script.run.withSuccessHandler(function(res) {
if (didTimeout) return;
clearTimeout(timeoutId);
console.log('Sinkronisasi berhasil:', res);
document.getElementById('sync-indicator').style.display = 'none';
if (res && res.error) {
document.getElementById('btn-setup-aman').style.display = 'inline-block';
document.getElementById('btn-reset-total').style.display = 'inline-block';
showLoading(true, 'Sheet Belum Siap');
alert('Pesan Sistem: ' + res.error);
return;
}
try {
// Simpan ke IndexedDB
saveLocal("initialData", res);
// Backup ke localStorage
try { localStorage.setItem('fuku_data_cache', JSON.stringify(res)); } catch(e) {}
updateLocalData(res);
showLoading(false);
if (cb && typeof cb === 'function' && !isInit) cb();
} catch (err) {
console.error('Error saat update UI:', err);
showLoading(true, 'Error Render UI');
}
}).withFailureHandler(function(err) {
if (didTimeout) return;
clearTimeout(timeoutId);
console.error('Gagal koneksi ke server:', err);
document.getElementById('sync-indicator').style.display = 'none';
var errMsg = (err && err.message) ? err.message : (typeof err === 'string' ? err : 'Terjadi kesalahan sistem yang tidak diketahui');
if (!hasAnyCache) {
showLoading(false);
alert('Tidak ada data lokal.\n\nUntuk menjalankan di localhost:\n1) Buka POSFuku WebApp (online)\n2) Klik DATA → EXPORT\n3) Kembali ke localhost → DATA → IMPORT\n\nDetail: ' + errMsg);
try { openDataModal(); } catch(e) {}
return;
}
showLoading(false);
}).getInitialData();
}
function updateLocalData(res) {
if (!res) return;
menu = res.menu || [];
transaksi = res.transaksi || [];
pelanggan = res.pelanggan || [];
belanja = res.belanja || [];
rekap = res.rekap || [];
supplierHistory = res.supplierHistory || [];
suppliersMaster = res.suppliersMaster || [];
waiters = res.waiters || [];
paketKustom = res.paketKustom || [];
settings = res.settings || {};
lastReset = res.lastReset || '';
scriptUrl = res.scriptUrl || '';
initDefaults();
renderUrlInfo();
renderSettingsUI();
applyStoreBranding();
var activePage = document.querySelector('.page.active');
var activeId = activePage ? activePage.id : 'sec-meja';
if (activeId === 'sec-meja') { renderTables(); renderActiveList(); }
else if (activeId === 'sec-booking') renderBookingTab();
else if (activeId === 'sec-poin') renderCustomers();
else if (activeId === 'sec-riwayat') renderHistory();
else if (activeId === 'sec-stok') { renderStok(); renderTableStokAwal(); }
else if (activeId === 'sec-belanja') renderBelanja();
else if (activeId === 'sec-rekap') renderRekap();
else if (activeId === 'sec-dapur') renderDapur();
else if (activeId === 'sec-laporan') renderLaporanLokal();
else if (activeId === 'sec-supplier') { renderSupplierHistory(); renderSuppliersMaster(); }
else if (activeId === 'sec-pelayan') renderWaiters();
else if (activeId === 'sec-users') renderUsersAdmin(false);
else if (activeId === 'sec-paket') { renderPaketMenuList(); renderPaketSimulationSummary(); renderSavedPakets(); }
}
function renderSettingsUI() {
var wifiLock = settings.wifi_lock === 'true';
var statusEl = document.getElementById('wifi-lock-status');
var btnEl = document.getElementById('btn-toggle-wifi-lock');
if (statusEl && btnEl) {
statusEl.innerText = wifiLock ? 'AKTIF' : 'MATI';
statusEl.style.color = wifiLock ? '#e11d48' : '#666';
btnEl.innerText = wifiLock ? 'MATIKAN' : 'AKTIFKAN';
btnEl.className = 'pill ' + (wifiLock ? 'on' : 'dark');
}
}
function toggleWifiLock() {
var current = settings.wifi_lock === 'true';
var newValue = !current;
showLoading(true, 'MENGUPDATE PENGATURAN...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal: ' + err.message);
}).updateSetting('wifi_lock', String(newValue));
}
function initDefaults() {
var today = new Date().toISOString().split('T')[0];
if (!document.getElementById('lap-dari').value) document.getElementById('lap-dari').value = today;
if (!document.getElementById('lap-sampai').value) document.getElementById('lap-sampai').value = today;
renderPaketMenuList();
renderPaketSimulationSummary();
renderSavedPakets();
}
function showTab(id, btn) {
// Jika mode dapur, paksa tetap di tab dapur
if (isKitchenMode && id !== 'sec-dapur') return;
if (id === 'sec-users' && currentUserRole !== 'superadmin' && currentUserRole !== 'admin') {
alert('Hanya Superadmin yang bisa membuka tab USERS.');
return;
}
// Daftar tab yang diproteksi Admin
var protectedTabs = ['sec-laporan', 'sec-belanja', 'sec-stok', 'sec-rekap', 'sec-supplier', 'sec-pelayan', 'sec-users'];
if (protectedTabs.indexOf(id) > -1 && !isAdmin && !isKitchenMode) {
requestAdminAccess(function() {
showTab(id, btn);
});
return;
}
document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
document.getElementById(id).classList.add('active');
document.querySelectorAll('.nav-btn').forEach(function(b) { b.classList.remove('active'); });
if (btn) btn.classList.add('active');
if (id === 'sec-meja') { renderTables(); renderActiveList(); }
if (id === 'sec-booking') renderBookingTab();
if (id === 'sec-poin') renderCustomers();
if (id === 'sec-riwayat') renderHistory();
if (id === 'sec-laporan') renderLaporanLokal();
if (id === 'sec-stok') { renderStok(); renderTableStokAwal(); }
if (id === 'sec-belanja') renderBelanja();
if (id === 'sec-rekap') renderRekap();
if (id === 'sec-dapur') renderDapur();
if (id === 'sec-supplier') { renderSupplierHistory(); renderSuppliersMaster(); }
if (id === 'sec-pelayan') renderWaiters();
if (id === 'sec-users') renderUsersAdmin(false);
if (id === 'sec-paket') { renderPaketMenuList(); renderPaketSimulationSummary(); renderSavedPakets(); }
}
function requestAdminAccess(onSuccess) {
var lastU = localStorage.getItem('admin_last_user') || 'admin';
var username = prompt('Login Admin\n\nUsername (ketik RESET jika lupa password):', lastU);
if (username === null) return;
username = String(username || '').trim();
if (!username) { alert('Username tidak boleh kosong.'); return; }
if (username.toLowerCase() === 'reset') {
resetAdminPasswordFlow(onSuccess);
return;
}
var password = prompt('Password Admin:');
if (password === null) return;
showLoading(true, 'LOGIN ADMIN...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.ok) {
isAdmin = true;
localStorage.setItem('admin_last_user', username);
if (onSuccess) onSuccess();
} else {
alert('Login gagal. Username atau password salah.');
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal login: ' + (err && err.message ? err.message : err));
}).loginAdmin(username, password);
}
function resetAdminPasswordFlow(onSuccess) {
var recovery = prompt('RESET ADMIN\n\nMasukkan Recovery Code:');
if (recovery === null) return;
recovery = String(recovery || '').trim();
if (!recovery) { alert('Recovery code tidak boleh kosong.'); return; }
var newUsername = prompt('Username baru Admin:', 'admin');
if (newUsername === null) return;
newUsername = String(newUsername || '').trim();
if (!newUsername) { alert('Username baru tidak boleh kosong.'); return; }
var newPassword = prompt('Password baru Admin:');
if (newPassword === null) return;
newPassword = String(newPassword || '');
if (!newPassword) { alert('Password baru tidak boleh kosong.'); return; }
showLoading(true, 'RESET PASSWORD...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.ok) {
isAdmin = true;
localStorage.setItem('admin_last_user', newUsername);
alert('Reset berhasil. Silakan lanjut.');
if (onSuccess) onSuccess();
} else {
alert('Reset gagal: ' + (res && res.error ? res.error : 'Tidak diketahui'));
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Reset gagal: ' + (err && err.message ? err.message : err));
}).resetAdminWithRecovery(recovery, newUsername, newPassword);
}
function renderWaiters() {
var box = document.getElementById('pel-list');
if (!box) return;
box.innerHTML = '';
if (waiters.length === 0) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:20px;">Belum ada pelayan.</div>';
return;
}
waiters.forEach(function(w) {
var div = document.createElement('div');
div.className = 'list-item';
div.style.padding = '12px';
div.style.borderBottom = '1px solid #eee';
div.style.display = 'flex';
div.style.justifyContent = 'space-between';
div.style.alignItems = 'center';
var statusClass = w.status === 'Aktif' ? 'st-pending' : 'st-booking';
div.innerHTML = '<div>' +
'<strong>' + escapeHtml(w.nama) + '</strong><br>' +
'<small class="muted">' + escapeHtml(w.wa) + '</small> ' +
'<span class="status ' + statusClass + '" style="font-size:9px; padding:2px 6px;">' + w.status.toUpperCase() + '</span>' +
'</div>' +
'<div style="display:flex; gap:5px;">' +
'<button class="pill ' + (w.status === 'Aktif' ? 'dark' : 'on') + '" style="width:auto; padding:4px 8px; font-size:10px;" onclick="toggleStatusPelayan(\'' + w.nama.replace(/'/g, "\\'") + '\', \'' + w.status + '\')">' + (w.status === 'Aktif' ? 'NONAKTIFKAN' : 'AKTIFKAN') + '</button>' +
'<button class="pill" style="width:auto; padding:4px 8px; font-size:10px; background:#ef4444; color:white;" onclick="hapusPelayan(\'' + w.nama.replace(/'/g, "\\'") + '\')">HAPUS</button>' +
'</div>';
box.appendChild(div);
});
}
function simpanPelayan() {
var nama = document.getElementById('pel-nama').value.trim();
var wa = document.getElementById('pel-wa').value.trim();
if (!nama || !wa) { alert('Nama dan WA wajib diisi!'); return; }
showLoading(true, 'MENYIMPAN PELAYAN...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
document.getElementById('pel-nama').value = '';
document.getElementById('pel-wa').value = '';
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal: ' + err.message);
}).saveWaiter({ nama: nama, wa: wa, status: 'Aktif' });
}
function toggleStatusPelayan(nama, statusLama) {
var statusBaru = statusLama === 'Aktif' ? 'Nonaktif' : 'Aktif';
var w = waiters.find(function(x) { return x.nama === nama; });
var wa = w ? w.wa : '';
showLoading(true, 'MENGUPDATE STATUS...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal: ' + err.message);
}).saveWaiter({ nama: nama, wa: wa, status: statusBaru });
}
function hapusPelayan(nama) {
if (!confirm('Hapus pelayan ' + nama + '?')) return;
showLoading(true, 'MENGHAPUS PELAYAN...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal: ' + err.message);
}).deleteWaiter(nama);
}
function renderUrlInfo() {
var box = document.getElementById('url-list');
if (!box || !scriptUrl) return;
var urls = [
{ name: 'Kasir', url: scriptUrl },
{ name: 'Dapur.html', url: scriptUrl + '?p=dapur' },
{ name: 'Pelanggan.html / Meja', url: scriptUrl + '?p=order&meja=1' },
{ name: 'Poin.html', url: scriptUrl + '?p=poin' },
{ name: 'Belanja.html', url: scriptUrl + '?p=belanja' }
];
var html = '';
urls.forEach(function(u) {
html += '<div style="margin-bottom:8px; display:flex; flex-direction:column;">' +
'<span style="font-weight:bold; color:#444; margin-bottom:2px;">' + u.name + ':</span>' +
'<a href="' + u.url + '" target="_blank" style="color:#e11d48; text-decoration:underline; word-break:break-all; font-family:monospace;">' + u.url + '</a>' +
'</div>';
});
box.innerHTML = html;
}
// --- Paket Menu & Simulasi ---
var paketKustom = []; // Diisi dari server
var paketSimulationItems = [];
var paketKategoriAktif = 'Semua';
function renderPaketMenuList() {
var search = document.getElementById('paket-menu-search').value.toLowerCase();
var box = document.getElementById('all-menu-for-paket');
if (!box) return;
var cats = ['Semua'];
menu.forEach(function(m) { if (cats.indexOf(m.kategori) === -1) cats.push(m.kategori); });
var catContainer = document.getElementById('paket-cat-container');
catContainer.innerHTML = cats.map(function(c) {
return '<div class="tag ' + (paketKategoriAktif === c ? 'active' : '') + '" onclick="setPaketKategori(\'' + c + '\')">' + c + '</div>';
}).join('');
var filtered = menu.filter(function(m) {
var matchSearch = m.nama.toLowerCase().indexOf(search) > -1;
var matchCat = paketKategoriAktif === 'Semua' || m.kategori === paketKategoriAktif;
return matchSearch && matchCat;
});
box.innerHTML = filtered.map(function(m) {
var sel = paketSimulationItems.find(function(it) { return it.nama === m.nama; });
var isSelected = !!sel;
return '<div class="menu-card ' + (isSelected ? 'selected' : '') + '" onclick="togglePaketSimulationItem(\'' + m.nama + '\')">' +
'<img src="' + m.gambar + '" loading="lazy">' +
(isSelected ? '<span class="badge" style="top:5px; right:5px; background:#e11d48;">' + sel.qty + '</span>' : '') +
'<div class="menu-info">' +
'<div class="menu-name">' + m.nama + '</div>' +
'<div class="menu-price">Rp ' + m.harga.toLocaleString() + '</div>' +
'</div>' +
'</div>';
}).join('');
}
function setPaketKategori(cat) {
paketKategoriAktif = cat;
renderPaketMenuList();
}
function togglePaketSimulationItem(nama) {
var found = paketSimulationItems.find(function(it) { return it.nama === nama; });
if (found) {
found.qty += 1;
} else {
var m = menu.find(function(i) { return i.nama === nama; });
if (m) paketSimulationItems.push({ nama: m.nama, harga: m.harga, qty: 1 });
}
renderPaketSimulationSummary();
renderPaketMenuList();
}
function renderPaketSimulationSummary() {
var box = document.getElementById('selected-paket-items');
var navBadge = document.getElementById('badge-paket');
if (paketSimulationItems.length === 0) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:20px;">Belum ada menu yang dipilih.</div>';
document.getElementById('custom-paket-total-asli').innerText = 'Rp 0';
document.getElementById('custom-paket-harga').value = '';
if (navBadge) navBadge.style.display = 'none';
return;
}
if (navBadge) {
navBadge.innerText = paketSimulationItems.length;
navBadge.style.display = 'block';
}
var total = 0;
var html = paketSimulationItems.map(function(it, idx) {
total += (it.harga * it.qty);
return '<div class="item-order" style="padding:8px; margin-bottom:5px;">' +
'<div>' +
'<div style="font-weight:800; font-size:12px;">' + it.nama + '</div>' +
'<div class="muted">Rp ' + it.harga.toLocaleString() + '</div>' +
'</div>' +
'<div class="qty">' +
'<div class="qty-btn" onclick="updatePaketSimQty(' + idx + ', -1)" style="width:24px; height:24px; font-size:14px;">-</div>' +
'<span style="font-weight:900; font-size:14px; min-width:20px; text-align:center;">' + it.qty + '</span>' +
'<div class="qty-btn" onclick="updatePaketSimQty(' + idx + ', 1)" style="width:24px; height:24px; font-size:14px;">+</div>' +
'</div>' +
'</div>';
}).join('');
box.innerHTML = html;
document.getElementById('custom-paket-total-asli').innerText = 'Rp ' + total.toLocaleString();
// Auto fill if empty
var hargaInput = document.getElementById('custom-paket-harga');
if (!hargaInput.value || Number(hargaInput.value) === 0 || Number(hargaInput.value) > total) {
// You could optionally auto fill here, but it's better to let user type it or leave it blank
}
}
function updatePaketSimQty(idx, delta) {
paketSimulationItems[idx].qty += delta;
if (paketSimulationItems[idx].qty <= 0) {
paketSimulationItems.splice(idx, 1);
}
renderPaketSimulationSummary();
renderPaketMenuList();
}
function clearPaketSimulation() {
paketSimulationItems = [];
document.getElementById('custom-paket-name').value = '';
document.getElementById('custom-paket-harga').value = '';
renderPaketSimulationSummary();
renderPaketMenuList();
}
function saveCustomPaket() {
var name = document.getElementById('custom-paket-name').value.trim();
var customHargaStr = document.getElementById('custom-paket-harga').value.trim();
if (!name) { alert('Harap isi nama paket simulasi.'); return; }
if (paketSimulationItems.length === 0) { alert('Harap pilih minimal satu menu.'); return; }
var totalAsli = paketSimulationItems.reduce(function(acc, it) { return acc + (it.harga * it.qty); }, 0);
var finalTotal = customHargaStr ? Number(customHargaStr) : totalAsli;
var payload = {
nama: name,
items: paketSimulationItems,
total: finalTotal
};
showLoading(true, 'Menyimpan Simulasi Paket...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
clearPaketSimulation();
alert('Simulasi paket berhasil disimpan!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan: ' + err.message);
}).saveCustomPaket(payload);
}
function renderSavedPakets() {
var box = document.getElementById('saved-paket-list');
if (!box) return;
if (!paketKustom || paketKustom.length === 0) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:20px;">Belum ada simulasi tersimpan.</div>';
return;
}
var html = paketKustom.sort(function(a, b) { return b.timestamp.localeCompare(a.timestamp); }).map(function(p) {
var itemsText = p.items.map(function(it) { return it.qty + 'x ' + it.nama; }).join(', ');
var logoHtml = p.gambar ? '<img src="' + p.gambar + '" style="width:50px; height:50px; object-fit:cover; border-radius:6px; margin-right:10px;" />' : '<div style="width:50px; height:50px; background:#f1f5f9; border-radius:6px; margin-right:10px; display:flex; align-items:center; justify-content:center; font-size:10px; color:#94a3b8;">No Logo</div>';
var toggleBtnHtml = p.aktif ?
'<button onclick="togglePaketStatus(\'' + p.id + '\', false)" style="padding:4px 8px; font-size:10px; border-radius:6px; border:none; font-weight:800; background:#f97316; color:#fff; cursor:pointer;">NON-AKTIFKAN</button>' :
'<button onclick="togglePaketStatus(\'' + p.id + '\', true)" style="padding:4px 8px; font-size:10px; border-radius:6px; border:none; font-weight:800; background:#10b981; color:#fff; cursor:pointer;">AKTIFKAN</button>';
return '<div class="card" style="padding:10px; margin-bottom:8px; border-left:4px solid ' + (p.aktif ? '#10b981' : '#e11d48') + ';">' +
'<div style="display:flex; justify-content:space-between; align-items:flex-start;">' +
'<div style="display:flex;">' +
logoHtml +
'<div>' +
'<div style="font-weight:900; font-size:13px;">' + p.nama + (p.aktif ? ' <span style="font-size:9px; background:#10b981; color:#fff; padding:2px 4px; border-radius:4px; vertical-align:middle;">AKTIF</span>' : '') + '</div>' +
'<div style="font-size:10px; color:#666; margin-top:2px;">' + itemsText + '</div>' +
'<div style="font-weight:900; color:#e11d48; margin-top:5px; font-size:14px;">Total: Rp ' + p.total.toLocaleString() + '</div>' +
'</div>' +
'</div>' +
'<div style="display:flex; flex-direction:column; gap:5px;">' +
toggleBtnHtml +
'<label class="btn-green" style="padding:4px 8px; font-size:10px; border-radius:6px; border:none; font-weight:800; cursor:pointer; text-align:center;">' +
'UPLOAD LOGO' +
'<input type="file" style="display:none;" accept="image/*" onchange="handleUploadPaketLogo(\'' + p.id + '\', this)">' +
'</label>' +
'<button class="btn-blue" onclick="loadCustomPaket(\'' + p.id + '\')" style="padding:4px 8px; font-size:10px; border-radius:6px; border:none; font-weight:800;">MUAT ULANG</button>' +
'<button class="btn-dark" onclick="editCustomPaketName(\'' + p.id + '\', \'' + p.nama.replace(/'/g, "\\'") + '\')" style="padding:4px 8px; font-size:10px; border-radius:6px; border:none; font-weight:800;">EDIT NAMA</button>' +
'<button class="btn-red" onclick="deleteSavedPaket(\'' + p.id + '\')" style="padding:4px 8px; font-size:10px; border-radius:6px; background:#fee2e2; color:#ef4444; border:none; font-weight:800;">HAPUS</button>' +
'</div>' +
'</div>' +
'</div>';
}).join('');
box.innerHTML = html;
}
function deleteSavedPaket(id) {
if (!confirm('Hapus simulasi paket ini?')) return;
showLoading(true, 'Menghapus...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menghapus: ' + err.message);
}).deleteCustomPaket(id);
}
function loadCustomPaket(id) {
var paket = paketKustom.find(function(p) { return p.id === id; });
if (!paket) {
alert('Paket tidak ditemukan!');
return;
}
// Clear current simulation
clearPaketSimulation();
// Load saved package into simulation
document.getElementById('custom-paket-name').value = paket.nama;
document.getElementById('custom-paket-harga').value = paket.total;
paketSimulationItems = JSON.parse(JSON.stringify(paket.items)); // Deep copy to avoid reference issues
renderPaketSimulationSummary();
renderPaketMenuList();
alert('Paket "' + paket.nama + '" berhasil dimuat ulang ke simulasi.');
}
function editCustomPaketName(id, currentName) {
var newName = prompt('Masukkan nama baru untuk paket ini:', currentName);
if (newName === null || newName.trim() === '') {
alert('Nama paket tidak boleh kosong.');
return;
}
newName = newName.trim();
showLoading(true, 'Mengupdate nama paket...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
alert('Nama paket berhasil diupdate menjadi "' + newName + '".');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal mengupdate nama paket: ' + err.message);
}).updateCustomPaketName(id, newName);
}
function togglePaketStatus(id, isActive) {
var statusText = isActive ? 'menampilkan' : 'menyembunyikan';
showLoading(true, statusText.toUpperCase() + ' PAKET...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
renderCategories();
renderMenu();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal ' + statusText + ' paket: ' + err.message);
}).toggleCustomPaketStatus(id, isActive);
}
function handleUploadPaketLogo(id, input) {
var file = input.files[0];
if (!file) return;
if (file.size > 2 * 1024 * 1024) {
alert('Ukuran file maksimal 2MB');
input.value = '';
return;
}
var reader = new FileReader();
reader.onload = function(e) {
var base64Data = e.target.result.split(',')[1];
showLoading(true, 'MENGUPLOAD LOGO PAKET...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res.error) {
alert('Gagal mengupload logo: ' + res.error);
} else {
updateLocalData(res);
alert('Logo berhasil diupload.');
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Terjadi kesalahan: ' + err.message);
}).uploadPaketLogo(id, base64Data, file.type, file.name);
};
reader.readAsDataURL(file);
input.value = ''; // Reset input
}
function renderLaporanLokal() {
var dari = document.getElementById('lap-dari').value;
var sampai = document.getElementById('lap-sampai').value;
var filtered = transaksi.filter(function(t) {
if (t.status !== 'Selesai') return false;
var tgl = t.timestamp ? t.timestamp.split(' ')[0] : (t.tgl ? t.tgl.split(' ')[0] : '');
return tgl >= dari && tgl <= sampai;
});
var omzet = filtered.reduce(function(a, c) { return a + (Number(c.total) || 0); }, 0);
var belanjaTotal = 0;
if (belanja && belanja.length) {
belanja.forEach(function(b) {
var tgl = b.tgl ? b.tgl.split(' ')[0] : '';
if (tgl >= dari && tgl <= sampai) belanjaTotal += (Number(b.total) || 0);
});
}
document.getElementById('lap-omzet').innerText = 'Rp ' + omzet.toLocaleString();
document.getElementById('lap-belanja').innerText = 'Rp ' + belanjaTotal.toLocaleString();
var methods = {};
var itemCounter = {};
filtered.forEach(function(t) {
var m = t.metodeBayar || 'Tunai';
methods[m] = (methods[m] || 0) + (Number(t.total) || 0);
(t.items || []).forEach(function(it) {
itemCounter[it.nama] = (itemCounter[it.nama] || 0) + Number(it.qty);
});
});
var methBox = document.getElementById('lap-methods');
methBox.innerHTML = '';
for (var m in methods) {
var d = document.createElement('div');
d.className = 'sum-row';
d.innerHTML = '<span>' + m + '</span><span>Rp ' + methods[m].toLocaleString() + '</span>';
methBox.appendChild(d);
}
var box = document.getElementById('lap-items');
box.innerHTML = '';
var itemsSorted = Object.keys(itemCounter).map(function(k) { return [k, itemCounter[k]]; }).sort(function(a, b) { return b[1] - a[1]; });
itemsSorted.slice(0, 10).forEach(function(it) {
var div = document.createElement('div');
div.className = 'item-order';
div.innerHTML = '<div style="font-weight:900">' + escapeHtml(it[0]) + '</div><div style="font-weight:900">' + it[1] + ' porsi</div>';
box.appendChild(div);
});
window.__lastReport = { omzet: omzet, belanja: belanjaTotal, methods: methods, items: itemsSorted };
renderSalesChart();
}
var myChart = null;
function renderSalesChart() {
var ctx = document.getElementById('salesChart');
if (!ctx) return;
var labels = [];
var data = [];
for (var i = 6; i >= 0; i--) {
var d = new Date();
d.setDate(d.getDate() - i);
var dateStr = d.toISOString().split('T')[0];
labels.push(dateStr.slice(5));
var dayOmzet = transaksi.filter(function(t) {
if (t.status !== 'Selesai') return false;
var tgl = t.timestamp ? t.timestamp.split(' ')[0] : (t.tgl ? t.tgl.split(' ')[0] : '');
return tgl === dateStr;
}).reduce(function(a, c) { return a + (Number(c.total) || 0); }, 0);
data.push(dayOmzet);
}
if (myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Omzet Harian',
data: data,
backgroundColor: '#e11d48',
borderRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true, ticks: { font: { size: 9 } } }, x: { ticks: { font: { size: 9 } } } },
plugins: { legend: { display: false } }
}
});
}
function panggilPelayan(meja) {
var activeWaiters = (waiters || []).filter(function(w) { return w.status === 'Aktif'; });
if (activeWaiters.length === 0) {
alert('Maaf, tidak ada pelayan yang sedang aktif bertugas.');
return;
}
// Pilih pelayan pertama yang aktif
var target = activeWaiters[0];
var pesan = 'Halo, saya di Meja ' + meja + ' memerlukan bantuan pelayan. Mohon segera ke meja saya. Terima kasih.';
safeRun('notifyWaiterTelegram', { meja: meja, waiter: target.nama || '', message: pesan, nama: document.getElementById('ov-nama') ? document.getElementById('ov-nama').value : '' }, function(res) {
if (res && res.success && res.sent) {
alert('Notifikasi pelayan terkirim: ' + (target.nama || '-') + ' (Meja ' + meja + ')');
return;
}
var cleanWA = (target.wa || '').replace(/\D/g, '');
var dest = cleanWA.indexOf('0') === 0 ? ('62' + cleanWA.slice(1)) : (cleanWA.indexOf('8') === 0 ? ('62' + cleanWA) : cleanWA);
alert('Memanggil ' + target.nama + ' (Meja ' + meja + ')... Anda akan diarahkan ke WhatsApp.');
window.open('https://wa.me/' + dest + '?text=' + encodeURIComponent(pesan), '_blank');
}, function() {
var cleanWA = (target.wa || '').replace(/\D/g, '');
var dest = cleanWA.indexOf('0') === 0 ? ('62' + cleanWA.slice(1)) : (cleanWA.indexOf('8') === 0 ? ('62' + cleanWA) : cleanWA);
alert('Memanggil ' + target.nama + ' (Meja ' + meja + ')... Anda akan diarahkan ke WhatsApp.');
window.open('https://wa.me/' + dest + '?text=' + encodeURIComponent(pesan), '_blank');
});
}
function renderSuppliersMaster() {
var box = document.getElementById('sup-master-list');
if (!box) return;
var q = document.getElementById('sup-master-search').value.toLowerCase();
if (!box) return;
box.innerHTML = '';
var filtered = suppliersMaster.filter(function(s) {
return (s.nama || '').toLowerCase().includes(q) || (s.wa || '').includes(q) || (s.rekening || '').toLowerCase().includes(q);
}).sort(function(a, b) {
return (a.nama || '').localeCompare(b.nama || '');
});
if (filtered.length === 0) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:10px;">Belum ada supplier di database.</div>';
return;
}
filtered.forEach(function(s) {
var div = document.createElement('div');
div.className = 'list-item';
div.style.padding = '10px';
div.style.borderBottom = '1px solid #eee';
div.style.display = 'flex';
div.style.justifyContent = 'space-between';
div.style.alignItems = 'center';
div.innerHTML = '<div>' +
'<strong>' + escapeHtml(s.nama) + '</strong><br>' +
'<small class="muted">' + escapeHtml(s.wa) + (s.rekening ? (' | ' + escapeHtml(s.rekening)) : '') + '</small>' +
'</div>' +
'<div style="display:flex; gap:6px;">' +
'<button class="pill" style="width:auto; padding:4px 10px; background:#111; color:#fff;" onclick="editSupplier(\'' + String(s.id || '').replace(/'/g, "\\'") + '\')">EDIT</button>' +
'<button class="pill" style="width:auto; padding:4px 10px; background:#fee2e2; color:#ef4444;" onclick="hapusSupplier(\'' + String(s.id || '').replace(/'/g, "\\'") + '\', \'' + String(s.nama || '').replace(/'/g, "\\'") + '\')">HAPUS</button>' +
'<button class="pill on" style="width:auto; padding:4px 10px;" onclick="pilihSupplier(\'' + s.nama.replace(/'/g, "\\'") + '\')">PILIH</button>' +
'</div>';
box.appendChild(div);
});
}
function toggleAddSupplier() {
var form = document.getElementById('add-sup-form');
var show = form.style.display === 'none';
form.style.display = show ? 'block' : 'none';
if (show) setSupplierFormMode(false);
}
function setSupplierFormMode(isEdit) {
var btnSave = document.getElementById('sup-master-save-btn');
var btnCancel = document.getElementById('sup-master-cancel-btn');
if (btnSave) btnSave.innerText = isEdit ? 'UPDATE SUPPLIER' : 'SIMPAN KE DATABASE';
if (btnCancel) btnCancel.style.display = isEdit ? 'inline-flex' : 'none';
}
function editSupplier(id) {
var sup = (suppliersMaster || []).find(function(s) { return String(s.id) === String(id); });
if (!sup) { alert('Supplier tidak ditemukan.'); return; }
var form = document.getElementById('add-sup-form');
if (form) form.style.display = 'block';
document.getElementById('sup-master-id').value = sup.id || '';
document.getElementById('sup-master-nama').value = sup.nama || '';
document.getElementById('sup-master-wa').value = sup.wa || '';
document.getElementById('sup-master-rek').value = sup.rekening || '';
setSupplierFormMode(true);
if (form) form.scrollIntoView({ behavior: 'smooth' });
}
function batalEditSupplier() {
document.getElementById('sup-master-id').value = '';
document.getElementById('sup-master-nama').value = '';
document.getElementById('sup-master-wa').value = '';
document.getElementById('sup-master-rek').value = '';
setSupplierFormMode(false);
}
function hapusSupplier(id, nama) {
var key = String(id || '').trim();
var nm = String(nama || '').trim();
if (!key && !nm) { alert('Supplier tidak valid.'); return; }
if (!confirm('Hapus supplier' + (nm ? (' "' + nm + '"') : '') + ' dari database master?')) return;
showLoading(true, 'MENGHAPUS SUPPLIER...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
if (key && String(document.getElementById('sup-master-id').value || '') === key) batalEditSupplier();
if (!key && nm && String(document.getElementById('sup-master-nama').value || '').trim().toLowerCase() === nm.toLowerCase()) batalEditSupplier();
alert('Supplier berhasil dihapus.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal hapus supplier: ' + err.message);
}).deleteMasterSupplier(key || nm);
}
function pilihSupplier(nama) {
var card = document.getElementById('card-kirim-wa');
var inputSelect = document.getElementById('sup-select');
var inputDisplay = document.getElementById('sup-selected-display');
var targetNama = document.getElementById('target-sup-nama');
inputSelect.value = nama;
inputDisplay.value = nama;
targetNama.innerText = nama;
autoFillSupplierWA();
autoFillSupplierRekening();
updateSupplierDepositUI(nama);
card.style.display = 'block';
card.scrollIntoView({ behavior: 'smooth' });
}
function isDepositSupplierName(nama) {
var s = String(nama || '').toLowerCase();
s = s.replace(/\u00a0/g, ' ');
s = s.replace(/[^a-z0-9]+/g, ' ').trim().replace(/\s+/g, ' ');
if (s.indexOf('galon') > -1) return true;
if (s.indexOf('batu') > -1) return true;
return s.indexOf('esbatu') > -1 || s.indexOf('es batu') > -1;
}
function updateSupplierDepositUI(nama) {
var box = document.getElementById('sup-deposit-box');
if (!box) return;
var isDeposit = isDepositSupplierName(nama);
var a = document.getElementById('sup-deposit');
var b = document.getElementById('sup-deposit-potong');
var st = document.getElementById('sup-deposit-status');
if (a) a.disabled = !isDeposit;
if (b) b.disabled = !isDeposit;
box.style.opacity = isDeposit ? '1' : '0.55';
if (st) {
st.innerText = isDeposit ? 'AKTIF' : 'NONAKTIF';
st.style.color = isDeposit ? '#16a34a' : '#999';
}
if (!isDeposit) {
if (a) a.value = '';
if (b) b.value = '';
}
}
function autoFillSupplierWA() {
var name = document.getElementById('sup-select').value;
var waInput = document.getElementById('sup-wa');
var found = (suppliersMaster || []).find(function(s) { return s.nama === name; });
if (found) {
waInput.value = found.wa;
} else {
waInput.value = '';
}
}
function autoFillSupplierRekening() {
var name = document.getElementById('sup-select').value;
var rekInput = document.getElementById('sup-rek');
if (!rekInput) return;
var found = (suppliersMaster || []).find(function(s) { return s.nama === name; });
if (found) {
rekInput.value = found.rekening || '';
} else {
rekInput.value = '';
}
}
function simpanMasterSupplier() {
var id = document.getElementById('sup-master-id').value.trim();
var nama = document.getElementById('sup-master-nama').value.trim();
var wa = document.getElementById('sup-master-wa').value.trim();
var rek = document.getElementById('sup-master-rek').value.trim();
if (!nama || !wa) {
alert('Nama dan WA Supplier wajib diisi!');
return;
}
var payload = { id: id || null, nama: nama, wa: wa, rekening: rek };
showLoading(true, 'MENYIMPAN KE DATABASE...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
batalEditSupplier();
alert('Berhasil disimpan ke database master.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan database: ' + err.message);
}).saveMasterSupplier(payload);
}
function renderSupplierHistory() {
var box = document.getElementById('sup-history-list');
var q = document.getElementById('sup-search').value.toLowerCase();
box.innerHTML = '';
var filtered = (supplierHistory || []).filter(function(h) {
return (h.nama || '').toLowerCase().includes(q) || (h.wa || '').includes(q) || (h.pesan || '').toLowerCase().includes(q);
}).reverse();
if (filtered.length === 0) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:20px;">Belum ada riwayat pengiriman.</div>';
return;
}
filtered.forEach(function(h) {
var div = document.createElement('div');
div.className = 'card';
div.style.padding = '10px';
div.style.fontSize = '12px';
div.innerHTML = '<div style="display:flex; justify-content:space-between; align-items:center;">' +
'<strong>' + escapeHtml(h.nama) + '</strong>' +
'</div>' +
'<div style="margin-top:4px; color:#666;">WA: ' + escapeHtml(h.wa) + '</div>' +
'<div style="margin-top:4px; background:#f9fafb; padding:8px; border-radius:8px; white-space: pre-wrap; font-size:11px;">' + escapeHtml(h.pesan) + '</div>' +
'<div style="margin-top:6px; text-align:right; font-size:10px; color:#999;">' + h.timestamp + '</div>';
box.appendChild(div);
});
}
function openChatModal() {
var body = '<div id="chat-container" style="height: 70vh; display: flex; flex-direction: column;">' +
'<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 10px; border: 1px solid #eee; border-radius: 12px; margin-bottom: 10px;"></div>' +
'<div style="display: flex; gap: 10px;">' +
'<input id="chat-input" type="text" placeholder="Ketik pesan..." style="flex-grow: 1;">' +
'<button class="btn-main" onclick="sendChat()">KIRIM</button>' +
'</div>' +
'</div>';
openModalCustom(body, 'Chat dengan Dapur');
renderChat();
}
function renderChat() {
google.script.run.withSuccessHandler(function(messages) {
renderChatList(messages);
}).getChatMessages();
}
function renderChatList(messages) {
var chatBox = document.getElementById('chat-messages');
if (!chatBox) return;
chatBox.innerHTML = '';
messages.forEach(function(msg) {
var msgDiv = document.createElement('div');
var isKasir = msg.sender === 'Kasir';
msgDiv.style.textAlign = isKasir ? 'right' : 'left';
msgDiv.innerHTML = '<div style="background:' + (isKasir ? '#e11d48' : '#f3f4f6') + '; color:' + (isKasir ? 'white' : '#111') + '; display:inline-block; padding:8px 12px; border-radius:12px; margin-bottom:8px; max-width:80%;">' +
'<div style="font-weight:bold; font-size:11px; margin-bottom:4px;">' + msg.sender + '</div>' +
'<div>' + msg.message + '</div>' +
'<div style="font-size:9px; opacity:0.7; margin-top:4px;">' + new Date(msg.timestamp).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }) + '</div>' +
'</div>';
chatBox.appendChild(msgDiv);
});
chatBox.scrollTop = chatBox.scrollHeight;
}
function sendChat() {
var input = document.getElementById('chat-input');
var message = input.value.trim();
if (!message) return;
input.value = '';
google.script.run.withSuccessHandler(function() {
renderChat();
}).sendChatMessage('Kasir', message);
}
var beepInterval = null;
var audioContext = null;
function shouldShowAudioPermission() {
return false;
}
function showAudioPermissionIfNeeded() {
var el = document.getElementById('audio-permission');
if (!el) return;
el.style.display = 'none';
}
function enableAudio() {
try {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext && audioContext.state === 'suspended' && audioContext.resume) {
audioContext.resume();
}
var buffer = audioContext.createBuffer(1, 1, 22050);
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);
} catch (e) {
audioContext = null;
}
var el = document.getElementById('audio-permission');
if (el) el.style.display = 'none';
}
function initAutoAudioUnlock() {
try { enableAudio(); } catch(e) {}
var once = function() {
try { enableAudio(); } catch(e) {}
document.removeEventListener('pointerdown', once, true);
document.removeEventListener('touchstart', once, true);
document.removeEventListener('click', once, true);
document.removeEventListener('keydown', once, true);
};
document.addEventListener('pointerdown', once, true);
document.addEventListener('touchstart', once, true);
document.addEventListener('click', once, true);
document.addEventListener('keydown', once, true);
}
function playBellSound() {
if (!audioContext) return;
var osc = audioContext.createOscillator();
var gain = audioContext.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(1200, audioContext.currentTime); // Frekuensi bel
gain.gain.setValueAtTime(0.8, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 1.2);
osc.connect(gain);
gain.connect(audioContext.destination);
osc.start();
osc.stop(audioContext.currentTime + 1.2);
}
function playChatSound() {
if (!audioContext) return;
// Nada ding-ding cepat
[0, 0.15].forEach(function(delay) {
var osc = audioContext.createOscillator();
var gain = audioContext.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1500, audioContext.currentTime + delay);
gain.gain.setValueAtTime(0.5, audioContext.currentTime + delay);
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + delay + 0.1);
osc.connect(gain);
gain.connect(audioContext.destination);
osc.start(audioContext.currentTime + delay);
osc.stop(audioContext.currentTime + delay + 0.1);
});
}
function resetBellKasir() {
google.script.run.withSuccessHandler(function() {
document.getElementById('bell-indicator').style.display = 'none';
document.getElementById('bell-indicator').classList.remove('active');
if (beepInterval) {
clearInterval(beepInterval);
beepInterval = null;
}
alert('Bel Dapur telah direset.');
}).resetBell('DapurKeKasir');
}
setInterval(function() {
google.script.run.withSuccessHandler(function(res) {
var bellIndicator = document.getElementById('bell-indicator');
if (res.status === 'Active') {
bellIndicator.style.display = 'block';
bellIndicator.classList.add('active');
if (!beepInterval) {
playBellSound();
beepInterval = setInterval(playBellSound, 3000); // Bunyi setiap 3 detik
}
} else {
bellIndicator.style.display = 'none';
bellIndicator.classList.remove('active');
if (beepInterval) {
clearInterval(beepInterval);
beepInterval = null;
}
}
}).getBellStatus('DapurKeKasir');
}, 5000); // Cek status bel setiap 5 detik
function kirimWASupplier() {
var nama = document.getElementById('sup-select').value;
var wa = document.getElementById('sup-wa').value.trim();
var pesan = document.getElementById('sup-pesan').value.trim();
if (!nama || !wa || !pesan) {
alert('Pilih Supplier dan isi Pesan!');
return;
}
var isDeposit = isDepositSupplierName(nama);
var deposit = isDeposit ? (Number(document.getElementById('sup-deposit').value) || 0) : 0;
var potong = isDeposit ? (Number(document.getElementById('sup-deposit-potong').value) || 0) : 0;
if (deposit || potong) {
var depLine = deposit ? ('Deposit: Rp ' + deposit.toLocaleString('id-ID')) : '';
var potLine = potong ? ('Potong deposit: Rp ' + potong.toLocaleString('id-ID')) : '';
pesan += '\n\n' + [depLine, potLine].filter(function(x) { return x; }).join('\n');
}
var payload = {
nama: nama,
wa: wa,
pesan: pesan
};
showLoading(true, 'MENYIMPAN RIWAYAT...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
// Kirim WA
var cleanWA = wa.replace(/\D/g, '');
var dest = cleanWA.indexOf('0') === 0 ? ('62' + cleanWA.slice(1)) : (cleanWA.indexOf('8') === 0 ? ('62' + cleanWA) : cleanWA);
window.open('https://wa.me/' + dest + '?text=' + encodeURIComponent(pesan), '_blank');
// Reset Form Pesan saja
document.getElementById('sup-pesan').value = '';
document.getElementById('sup-deposit').value = '';
document.getElementById('sup-deposit-potong').value = '';
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan riwayat: ' + err.message);
}).saveSupplierWA(payload);
}
function renderTables() {
var grid = document.getElementById('grid-meja');
if (grid) grid.innerHTML = '';
var kasirCount = Math.max(1, Math.floor(getNumberSetting('kasir_table_count', 10)));
var comboRaw = settings && settings.kasir_table_combos ? String(settings.kasir_table_combos) : '';
var combos = comboRaw.split(',').map(function(s) { return String(s || '').trim(); }).filter(function(s) { return s; });
var includeDump = String((settings && settings.kasir_include_dump) || '').toLowerCase() !== 'false';
var tableLabels = [];
for (var i = 1; i <= kasirCount; i++) tableLabels.push('Meja-' + i);
combos.forEach(function(c) { tableLabels.push('Meja-' + c); });
if (includeDump) tableLabels.push('Meja-dump');
if (grid) {
tableLabels.forEach(function(label) {
var displayNum = label.replace('Meja-', '');
var isPending = transaksi.some(function(t) {
return t.meja === label && (t.status === 'Pending' || t.status === 'Ready');
});
var bookingCount = transaksi.filter(function(t) { return t.meja === label && t.status === 'Booking'; }).length;
var div = document.createElement('div');
div.className = 'table-box' + (isPending ? ' occupied' : (bookingCount > 0 ? ' booked' : ''));
// Jika meja gabungan, buat font sedikit lebih kecil agar muat
var numStyle = label.includes('&') ? 'font-size:14px;' : '';
div.innerHTML = '<span class="table-num" style="' + numStyle + '">' + displayNum + '</span><span class="table-status">' + (isPending ? 'Terisi' : (bookingCount > 0 ? 'Booking' : 'Tersedia')) + '</span>' + (bookingCount > 0 ? '<div class="badge">' + bookingCount + '</div>' : '');
div.onclick = function() { openTable(label); };
grid.appendChild(div);
});
}
var gridQr = document.getElementById('grid-meja-qr');
if (gridQr) {
gridQr.innerHTML = '';
var qrCount = Math.max(1, Math.floor(getNumberSetting('qr_table_count', 10)));
for (var n = 1; n <= qrCount; n++) {
var label = String(n);
var isPending = transaksi.some(function(t) {
return String(t.meja) === String(label) && (t.status === 'Pending' || t.status === 'Ready');
});
var div = document.createElement('div');
div.className = 'table-box' + (isPending ? ' occupied' : '');
div.innerHTML = '<span class="table-num">' + label + '</span><span class="table-status">' + (isPending ? 'Terisi' : 'Tersedia') + '</span>';
div.onclick = (function(l) { return function() { openTable(l); }; })(label);
gridQr.appendChild(div);
}
}
}
function openTable(label) {
mejaAktif = label;
transAktifId = null;
keranjang = [];
var displayLabel = label;
if (/^\d+$/.test(String(label || '').trim())) displayLabel = 'Meja ' + String(label || '').trim();
document.getElementById('ov-meja').innerText = displayLabel;
document.getElementById('order-view').style.display = 'block';
document.getElementById('meja-home').style.display = 'none';
document.getElementById('ov-nama').value = '';
document.getElementById('ov-wa').value = '';
document.getElementById('ov-tgl').value = new Date().toISOString().split('T')[0];
document.getElementById('ov-jam').value = '';
document.getElementById('ov-dp-nilai').value = 0;
document.getElementById('ov-dp-metode').value = 'Tunai';
document.getElementById('ov-dp-tgl').value = new Date().toISOString().split('T')[0];
var pending = transaksi.filter(function(t) {
return t.meja === label && (t.status === 'Pending' || t.status === 'Ready');
}).slice().reverse()[0] || null;
if (pending) {
transAktifId = pending.id;
document.getElementById('ov-nama').value = pending.nama || '';
document.getElementById('ov-wa').value = pending.wa || '';
document.getElementById('ov-catatan').value = pending.catatan || '';
keranjang = Array.isArray(pending.items) ? pending.items : [];
} else {
document.getElementById('ov-catatan').value = '';
}
pendingSnapshot = '';
document.getElementById('ov-mode').value = 'Pending';
kategoriAktif = 'Semua';
renderCategories();
renderMenu();
renderCart();
applyMode();
cekPoin();
cekReviewBonus();
pendingSnapshot = computePendingSnapshot();
}
function closeOrderView() {
mejaAktif = null;
transAktifId = null;
keranjang = [];
document.getElementById('order-view').style.display = 'none';
document.getElementById('meja-home').style.display = 'block';
document.getElementById('cart-bar').style.display = 'none';
}
function applyMode() {
var mode = document.getElementById('ov-mode').value;
var jamWrap = document.getElementById('ov-jam-wrap');
var dp = document.getElementById('ov-dp');
var actions = document.getElementById('ov-actions');
var sumPoinRow = document.getElementById('sum-poin-row');
actions.innerHTML = '';
if (mode === 'Pending') {
jamWrap.style.display = 'none';
dp.style.display = 'none';
document.getElementById('ov-menu').style.display = 'block';
document.getElementById('ov-cart').style.display = 'block';
sumPoinRow.style.display = keranjang.length ? 'flex' : 'none';
actions.innerHTML = '<button class="btn-main" onclick="simpanPending()">SIMPAN SEMENTARA</button><div style="height:8px"></div><button class="btn-dark" onclick="openCheckout()">LIHAT PESANAN</button>';
} else if (mode === 'Booking') {
jamWrap.style.display = 'block';
dp.style.display = 'block';
document.getElementById('ov-menu').style.display = 'block';
document.getElementById('ov-cart').style.display = 'block';
sumPoinRow.style.display = 'none';
actions.innerHTML = '<button class="btn-blue" onclick="simpanBooking()">SIMPAN BOOKING</button>';
} else {
jamWrap.style.display = 'none';
dp.style.display = 'none';
document.getElementById('ov-menu').style.display = 'block';
document.getElementById('ov-cart').style.display = 'block';
sumPoinRow.style.display = 'none';
actions.innerHTML = '<button class="btn-green" onclick="openCheckout(true)">BAYAR SEKARANG</button>';
}
renderCart();
}
function renderCategories() {
var catBox = document.getElementById('cat-container');
var cats = ['Semua'];
menu.forEach(function(m) { if (cats.indexOf(m.kategori) === -1) cats.push(m.kategori); });
var hasActivePaket = typeof paketKustom !== 'undefined' && paketKustom && paketKustom.some(function(p) { return p.aktif; });
if (hasActivePaket) {
cats.push('Paket Kustom');
}
if (cats.indexOf(kategoriAktif) === -1) {
kategoriAktif = 'Semua';
}
catBox.innerHTML = '';
cats.forEach(function(c) {
var b = document.createElement('div');
b.className = 'tag' + (kategoriAktif === c ? ' active' : '');
b.innerText = c;
b.onclick = function() { kategoriAktif = c; renderCategories(); renderMenu(); };
catBox.appendChild(b);
});
}
function fallbackImageData(text) {
var safeText = String(text || 'No Image').replace(/[<>]/g, '');
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><rect width="200" height="200" fill="#f3f4f6"/><text x="100" y="105" text-anchor="middle" font-family="Arial" font-size="18" fill="#6b7280">' + safeText + '</text></svg>';
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
}
function renderMenu() {
var list = document.getElementById('menu-list');
list.innerHTML = '';
// Render Regular Menu
if (kategoriAktif === 'Semua' || kategoriAktif !== 'Paket Kustom') {
menu.forEach(function(m) {
if (kategoriAktif !== 'Semua' && m.kategori !== kategoriAktif) return;
var qty = keranjang.reduce(function(sum, i) { return i.nama === m.nama ? sum + i.qty : sum; }, 0);
var d = document.createElement('div');
d.className = 'menu-card' + (qty > 0 ? ' selected' : '');
var noImg = fallbackImageData('No Image');
var imgUrl = m.gambar ? m.gambar : noImg;
var stokColor = (m.stok > 5) ? '#10b981' : '#e11d48';
var qtyBadge = qty > 0 ? ('<div class="qty-badge">' + qty + 'x</div>') : '';
d.innerHTML = qtyBadge +
'<img referrerpolicy="no-referrer" src="' + imgUrl + '" onerror="this.onerror=null;this.src=\'' + noImg + '\';">' +
'<div class="stok-badge" style="background:' + (m.stok > 0 ? stokColor : '#333') + '">' + m.stok + '</div>' +
'<div class="menu-info"><div class="menu-name">' + m.nama + '</div><div class="menu-price">Rp ' + Number(m.harga).toLocaleString() + '</div></div>';
d.onclick = function() { if (m.stok > 0) addToCart(m); else alert('Menu habis'); };
list.appendChild(d);
});
}
// Render Paket Kustom
if (kategoriAktif === 'Semua' || kategoriAktif === 'Paket Kustom') {
if (typeof paketKustom !== 'undefined' && paketKustom && paketKustom.length > 0) {
paketKustom.forEach(function(p) {
if (!p.aktif) return;
var qtyPaket = keranjang.reduce(function(sum, i) {
if (i && i.isPaketHeader && i.nama === p.nama && Number(i.harga) === Number(p.total)) return sum + (Number(i.qty) || 0);
return sum;
}, 0);
var d = document.createElement('div');
d.className = 'menu-card' + (qtyPaket > 0 ? ' selected' : '');
d.style.border = '2px solid #e11d48';
var noImg = fallbackImageData('Paket Kustom');
var imgUrl = p.gambar ? p.gambar : noImg;
var qtyBadge = qtyPaket > 0 ? ('<div class="qty-badge">' + qtyPaket + 'x</div>') : '';
d.innerHTML = qtyBadge +
'<img referrerpolicy="no-referrer" src="' + imgUrl + '" onerror="this.onerror=null;this.src=\'' + noImg + '\';">' +
'<div class="stok-badge" style="background:#e11d48; right:auto; left:5px;">PAKET</div>' +
'<div class="menu-info"><div class="menu-name">' + p.nama + '</div><div class="menu-price">Rp ' + Number(p.total).toLocaleString() + '</div></div>';
d.onclick = function() { addPaketToCart(p); };
list.appendChild(d);
});
}
}
}
function addPaketToCart(p) {
if (!p.items || p.items.length === 0) return;
var failedItems = [];
var allFound = true;
// Cek dulu apakah semua item paket ada di menu
p.items.forEach(function(pItem) {
var menuItem = menu.find(function(m) { return m.nama === pItem.nama; });
if (!menuItem) {
failedItems.push(pItem.nama);
allFound = false;
}
});
if (!allFound) {
alert('Beberapa item dalam paket tidak ditemukan di menu aktif:\\n' + failedItems.join(', '));
return;
}
var paketKey = p.id ? String(p.id) : (String(p.nama) + '|' + String(p.total));
var header = keranjang.find(function(i) {
if (!i || !i.isPaketHeader) return false;
if (Number(i.harga) !== Number(p.total)) return false;
if (String(i.nama) !== String(p.nama)) return false;
if (i.paketKey) return String(i.paketKey) === paketKey;
return true;
});
if (header) {
header.qty += 1;
} else {
header = { nama: p.nama, harga: p.total, qty: 1, kat: 'Paket Kustom', isPaketHeader: true, paketKey: paketKey };
keranjang.push(header);
}
// Sinkronkan rincian (selalu mengikuti qty header)
p.items.forEach(function(pItem) {
var detailName = '-> ' + pItem.nama;
var detail = keranjang.find(function(i) {
return i && i.isPaketDetail && String(i.paketKey) === paketKey && String(i.nama) === detailName && Number(i.harga) === 0;
});
if (!detail) {
detail = { nama: detailName, harga: 0, qty: 0, kat: 'Paket Kustom Item', isPaketDetail: true, paketKey: paketKey, baseQty: pItem.qty };
keranjang.push(detail);
}
detail.baseQty = pItem.qty;
detail.qty = header.qty * pItem.qty;
});
renderCart();
renderMenu();
}
function addToCart(m, skipRender) {
var found = keranjang.find(function(i) { return i.nama === m.nama && i.harga === m.harga && !i.isPaketHeader && !i.isPaketDetail; });
if (found) found.qty += 1;
else keranjang.push({ nama: m.nama, harga: m.harga, qty: 1, kat: m.kategori });
if (!skipRender) {
renderCart();
renderMenu(); // Update badge on menu card
}
}
function updateQty(idx, delta) {
if (!keranjang[idx]) return;
var item = keranjang[idx];
if (item.isPaketDetail) {
// Jangan biarkan edit qty manual untuk detail paket
alert('Jumlah rincian paket akan mengikuti jumlah paket utamanya.');
return;
}
item.qty += delta;
if (item.isPaketHeader) {
var key = item.paketKey ? String(item.paketKey) : (item.paketId ? String(item.paketId) : '');
// Update qty dari rinciannya juga
keranjang.forEach(function(kItem) {
if (!kItem || !kItem.isPaketDetail) return;
var kKey = kItem.paketKey ? String(kItem.paketKey) : (kItem.paketId ? String(kItem.paketId) : '');
if (key && kKey === key) {
kItem.qty = item.qty * kItem.baseQty;
}
});
}
if (item.qty <= 0) {
if (item.isPaketHeader) {
var delKey = item.paketKey ? String(item.paketKey) : (item.paketId ? String(item.paketId) : '');
// Hapus rinciannya juga
keranjang = keranjang.filter(function(kItem) {
if (!kItem || !kItem.isPaketDetail) return true;
var kKey = kItem.paketKey ? String(kItem.paketKey) : (kItem.paketId ? String(kItem.paketId) : '');
return !(delKey && kKey === delKey);
});
}
// Hapus header atau item biasa
var newIdx = keranjang.indexOf(item);
if (newIdx > -1) keranjang.splice(newIdx, 1);
}
renderCart();
renderMenu(); // Update badge on menu card
}
function renderCart() {
var list = document.getElementById('cart-list');
list.innerHTML = '';
if (!keranjang.length) {
list.innerHTML = '<div class="muted" style="text-align:center">Keranjang kosong</div>';
} else {
keranjang.forEach(function(it, idx) {
var row = document.createElement('div');
row.className = 'item-order';
var typeLabel = it.type ? ('<span style="font-size:9px; color:#e11d48; border:1px solid #e11d48; padding:1px 4px; border-radius:4px; margin-right:4px;">' + it.type.toUpperCase() + '</span>') : '';
var itemTotal = (Number(it.qty) * Number(it.harga)).toLocaleString();
var qtyHtml = '';
if (it.isPaketDetail) {
qtyHtml = '<div class="qty"><div style="min-width:18px; text-align:center; font-weight:900">' + it.qty + '</div></div>';
} else {
qtyHtml = '<div class="qty"><div class="qty-btn" onclick="updateQty(' + idx + ',-1);event.stopPropagation();">-</div><div style="min-width:18px; text-align:center; font-weight:900">' + it.qty + '</div><div class="qty-btn" onclick="updateQty(' + idx + ',1);event.stopPropagation();">+</div></div>';
}
row.innerHTML = '<div>' +
'<div style="font-weight:900">' + typeLabel + it.nama + '</div>' +
'<div class="muted">' + it.qty + ' x ' + Number(it.harga).toLocaleString() + ' = Rp ' + itemTotal + '</div>' +
'</div>' +
qtyHtml;
list.appendChild(row);
});
}
var subtotal = keranjang.reduce(function(a, c) { return a + (Number(c.qty) * Number(c.harga)); }, 0);
document.getElementById('sum-subtotal').innerText = 'Rp ' + subtotal.toLocaleString();
var poin = Math.floor(subtotal / getPoinEarnRupiah());
var wa = document.getElementById('ov-wa').value.trim();
document.getElementById('sum-poin').innerText = poin;
document.getElementById('sum-poin-row').style.display = (wa && document.getElementById('ov-mode').value === 'Pending' && keranjang.length) ? 'flex' : 'none';
}
function normalizeWAClient(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 getNumberSetting(key, fallback) {
var v = settings && settings[key] !== undefined ? settings[key] : '';
var n = Number(String(v).replace(',', '.'));
if (!isFinite(n) || isNaN(n)) return fallback;
return n;
}
function getPoinEarnRupiah() {
var n = getNumberSetting('poin_earn_rupiah', 10000);
return n > 0 ? n : 10000;
}
function getPoinRedeemRupiah() {
var n = getNumberSetting('poin_redeem_rupiah', 100);
return n > 0 ? n : 100;
}
function getPajakPersen() {
var n = getNumberSetting('pajak_persen', 0);
return n > 0 ? n : 0;
}
function getServicePersen() {
var n = getNumberSetting('service_persen', 0);
return n > 0 ? n : 0;
}
function getStringSetting(key, fallback) {
var v = (settings && settings[key] !== undefined) ? String(settings[key]) : '';
v = String(v || '').trim();
return v ? v : String(fallback || '');
}
function getStoreName() { return getStringSetting('store_name', 'POS'); }
function getStoreAddress() { return getStringSetting('store_address', ''); }
function getStoreWhatsapp() { return normalizeWAClient(getStringSetting('store_whatsapp', '')); }
function getSocialInstagramUrl() { return getStringSetting('social_instagram_url', ''); }
function getSocialTiktokUrl() { return getStringSetting('social_tiktok_url', ''); }
function getSocialGmapsUrl() { return getStringSetting('social_gmaps_url', ''); }
function getSocialLinkUrl() { return getStringSetting('social_linktree_url', getStringSetting('social_link_url', '')); }
function getWhatsappDisplay(wa62) {
var wa = String(wa62 || '').replace(/\D/g, '');
if (!wa) return '-';
if (wa.indexOf('62') === 0) return '0' + wa.slice(2);
return wa;
}
function applyStoreBranding() {
var name = getStoreName();
var addr = getStoreAddress();
var wa = getStoreWhatsapp();
var elName = document.getElementById('hdr-store-name');
if (elName) elName.innerText = name;
var elAddr = document.getElementById('hdr-store-address');
if (elAddr) elAddr.innerText = addr;
var elWaLink = document.getElementById('hdr-store-wa-link');
if (elWaLink) elWaLink.href = wa ? ('https://wa.me/' + wa) : '#';
var elWaText = document.getElementById('hdr-store-wa-text');
if (elWaText) elWaText.innerText = getWhatsappDisplay(wa);
var elIg = document.getElementById('hdr-social-ig');
if (elIg) elIg.href = getSocialInstagramUrl() || elIg.href;
var elTt = document.getElementById('hdr-social-tt');
if (elTt) elTt.href = getSocialTiktokUrl() || elTt.href;
var elMaps = document.getElementById('hdr-social-maps');
if (elMaps) elMaps.href = getSocialGmapsUrl() || elMaps.href;
var elLink = document.getElementById('hdr-social-link');
if (elLink) elLink.href = getSocialLinkUrl() || elLink.href;
var loadingText = document.getElementById('loading-text');
if (loadingText) loadingText.innerText = 'Memuat Data ' + name + '...';
try { document.title = name; } catch(e) {}
updateLayoutOffsets();
}
function getReportRecipients() {
var raw = getStringSetting('report_wa_numbers', '');
var list = raw.split(',').map(function(s) { return normalizeWAClient(s); }).filter(function(s) { return !!s; });
if (!list.length) {
var fallback = getStoreWhatsapp();
if (fallback) list = [fallback];
}
return list;
}
function openWhatsAppToMany(recipients, text) {
(recipients || []).forEach(function(dest) {
if (!dest) return;
window.open('https://wa.me/' + dest + '?text=' + encodeURIComponent(text), '_blank');
});
}
function buildStoreFooterText() {
var addrRaw = String(getStoreAddress() || '');
var addrLines = addrRaw.split(',').map(function(s) { return String(s || '').trim(); }).filter(function(s) { return s; });
var mapsUrl = getSocialGmapsUrl();
var linkUrl = getSocialLinkUrl();
var poinUrl = scriptUrl ? (scriptUrl + '?p=poin') : '';
var footer = '\n📍 *Lokasi Toko:*\n' +
(addrLines.length ? (addrLines.join(', ') + '\n') : '') +
(mapsUrl ? ('Google Maps: ' + mapsUrl + '\n') : '') +
(linkUrl ? ('Link: ' + linkUrl + '\n') : '') +
(poinUrl ? ('\n*Cek Poin & Riwayat:* ' + poinUrl + '\n') : '');
return footer;
}
function cekPoin() {
var wa = normalizeWAClient(document.getElementById('ov-wa').value.trim());
var info = document.getElementById('wa-poin-info');
if (!wa || wa.length < 6) { info.style.display = 'none'; return; }
var cust = pelanggan.find(function(p) { return normalizeWAClient(p.wa) === wa; });
if (!cust) { info.style.display = 'none'; return; }
info.style.display = 'block';
var redeem = getPoinRedeemRupiah();
info.innerText = 'Poin tersedia: ' + (cust.poin || 0) + ' (= Rp ' + ((cust.poin || 0) * redeem).toLocaleString() + ')';
}
function cekReviewBonus() {
var wa = normalizeWAClient(document.getElementById('ov-wa').value.trim());
var info = document.getElementById('wa-review-info');
if (!info) return;
if (!wa || wa.length < 6) { info.style.display = 'none'; return; }
var list = transaksi.filter(function(t) {
var tw = normalizeWAClient(t.wa);
if (!tw || tw !== wa) return false;
if (String(t.status || '') === 'Review') return true;
return !!String(t.buktiReview || '').trim();
}).slice().sort(function(a, b) {
var da = String(a.timestamp || a.tgl || '');
var db = String(b.timestamp || b.tgl || '');
return db.localeCompare(da);
});
if (!list.length) { info.style.display = 'none'; return; }
var latest = list[0];
var url = String(latest.buktiReview || '').trim();
info.style.display = 'block';
info.style.color = '#16a34a';
info.innerHTML = '🎁 Ada bukti review (' + list.length + 'x). Bisa klaim FREE minuman 1.' + (url ? ' <a href=\"' + url + '\" target=\"_blank\" style=\"color:#16a34a; font-weight:900;\">Lihat</a>' : '');
}
document.addEventListener('input', function(e) {
if (e.target && e.target.id === 'ov-wa') {
cekPoin();
cekReviewBonus();
renderActiveList();
}
});
function save(payload, cb) {
showLoading(true);
safeRun('saveTransaction', payload, function(res) {
showLoading(false);
if (res && res.error) { alert(res.error); return; }
if (isOnline && res) updateLocalData(res);
if (cb) cb(res);
}, function(err) {
showLoading(false);
alert('Gagal Simpan: ' + (err.message || err));
});
}
function applyLiteUpdate(res) {
if (!res || !res.lite) return;
try {
var td = res.transaksiDelta || [];
td.forEach(function(t) {
var idx = transaksi.findIndex(function(x) { return String(x.id) === String(t.id); });
if (idx > -1) transaksi[idx] = t;
else transaksi.push(t);
});
var md = res.menuDelta || [];
md.forEach(function(m) {
var idx = menu.findIndex(function(x) { return String(x.nama) === String(m.nama); });
if (idx > -1) menu[idx].stok = Number(m.stok) || 0;
});
if (res.pelangganDelta && res.pelangganDelta.wa) {
var c = res.pelangganDelta;
var idx = pelanggan.findIndex(function(x) { return String(x.wa) === String(c.wa); });
if (idx > -1) pelanggan[idx] = c;
else pelanggan.push(c);
}
if (res.lastReset !== undefined) lastReset = res.lastReset;
if (res.scriptUrl !== undefined) scriptUrl = res.scriptUrl;
} catch (e) {}
}
function saveFast(payload, cb) {
if (!isOnline) {
save(payload, cb);
return;
}
var p = Object.assign({}, payload, { __lite: true });
showLoading(true);
safeRun('saveTransaction', p, function(res) {
showLoading(false);
if (res && res.error) { alert(res.error); return; }
if (res && res.lite) applyLiteUpdate(res);
if (cb) cb(res);
}, function(err) {
showLoading(false);
alert('Gagal Simpan: ' + (err.message || err));
});
}
function basePayload(status) {
return {
id: transAktifId || ('F-' + Date.now()),
meja: mejaAktif,
status: status,
nama: document.getElementById('ov-nama').value.trim(),
wa: normalizeWAClient(document.getElementById('ov-wa').value.trim()),
tgl: document.getElementById('ov-tgl').value,
jam: document.getElementById('ov-jam').value,
items: keranjang.slice(),
catatan: document.getElementById('ov-catatan').value.trim(),
dp: Number(document.getElementById('ov-dp-nilai').value) || 0,
metodeDp: document.getElementById('ov-dp-metode').value,
tglDp: document.getElementById('ov-dp-tgl').value,
diskon: 0,
poinDipakai: 0,
pajakPersen: 0,
servicePersen: 0,
adminPersen: 0,
metodeBayar: '',
bayar: 0
};
}
function computePendingSnapshot() {
var items = (keranjang || []).map(function(it) {
return { nama: String(it.nama || ''), harga: Number(it.harga) || 0, qty: Number(it.qty) || 0, type: String(it.type || '') };
});
var payload = {
meja: String(mejaAktif || ''),
nama: String(document.getElementById('ov-nama').value || '').trim(),
wa: String(document.getElementById('ov-wa').value || '').trim(),
catatan: String(document.getElementById('ov-catatan').value || '').trim(),
items: items
};
return JSON.stringify(payload);
}
function autoSavePendingIfNeeded(next) {
var mode = document.getElementById('ov-mode') ? document.getElementById('ov-mode').value : '';
if (mode !== 'Pending') { if (next) next(); return; }
if (!mejaAktif || !keranjang.length) { if (next) next(); return; }
var snap = computePendingSnapshot();
if (snap === pendingSnapshot) { if (next) next(); return; }
var p = basePayload('Pending');
save(p, function(res) {
transAktifId = p.id;
pendingSnapshot = computePendingSnapshot();
if (next) next(res);
});
}
function simpanPending() {
if (!mejaAktif) return;
if (!keranjang.length) { alert('Keranjang kosong'); return; }
var p = basePayload('Pending');
save(p, function() {
transAktifId = p.id;
alert('Tersimpan (Pending)');
pendingSnapshot = computePendingSnapshot();
closeOrderView();
});
}
function simpanBooking() {
var wa = document.getElementById('ov-wa').value.trim();
if (!wa) { alert('WA wajib diisi untuk booking agar mudah dihubungi.'); return; }
if (!document.getElementById('ov-tgl').value) { alert('Tanggal wajib'); return; }
if (!document.getElementById('ov-jam').value) { alert('Jam wajib'); return; }
var dp = Number(document.getElementById('ov-dp-nilai').value) || 0;
if (dp <= 0) { alert('DP wajib'); return; }
var p = basePayload('Booking');
save(p, function() {
if (confirm('Booking tersimpan. Cetak struk booking?')) {
var saved = transaksi.filter(function(t) { return t.meja === p.meja && t.status === 'Booking' && t.wa === p.wa; }).slice().reverse()[0] || p;
cetakStruk(saved);
}
closeOrderView();
});
}
function handleFotoUpload(event) {
var file = event.target.files[0];
if (!file) return;
var status = document.getElementById('foto-status');
var preview = document.getElementById('foto-preview');
var container = document.getElementById('foto-preview-container');
status.innerText = 'Memproses...';
var reader = new FileReader();
reader.onload = function(e) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
// Target ukuran sekitar 300KB
// Jika file asli besar, kita perkecil dimensinya
var maxDim = 1200;
var scale = 1;
if (img.width > maxDim || img.height > maxDim) {
scale = maxDim / Math.max(img.width, img.height);
}
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Kompresi kualitas agar ukuran 200kb-500kb
// Kita coba 0.7 dulu, jika masih terlalu besar/kecil bisa disesuaikan
var quality = 0.7;
var base64 = canvas.toDataURL('image/jpeg', quality);
// Cek estimasi ukuran (Base64 is ~33% larger than binary)
var estSize = Math.round((base64.length * 3) / 4);
console.log('Estimated Compressed Size:', estSize, 'bytes');
window.__buktiBayarBase64 = base64.split(',')[1];
preview.src = base64;
container.style.display = 'block';
status.innerText = 'Siap (±' + Math.round(estSize/1024) + 'KB)';
status.style.color = '#16a34a';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function handleFotoUploadHistory(event) {
var file = event.target.files[0];
if (!file) return;
var status = document.getElementById('foto-status-his');
var preview = document.getElementById('foto-preview-his');
var container = document.getElementById('foto-preview-container-his');
var btnSave = document.getElementById('btn-save-foto-his');
status.innerText = 'Memproses...';
var reader = new FileReader();
reader.onload = function(e) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var maxDim = 1200;
var scale = 1;
if (img.width > maxDim || img.height > maxDim) {
scale = maxDim / Math.max(img.width, img.height);
}
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
var quality = 0.7;
var base64 = canvas.toDataURL('image/jpeg', quality);
var estSize = Math.round((base64.length * 3) / 4);
window.__buktiBayarBase64History = base64.split(',')[1];
preview.src = base64;
container.style.display = 'block';
btnSave.style.display = 'block';
status.innerText = 'Siap (±' + Math.round(estSize/1024) + 'KB)';
status.style.color = '#16a34a';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function saveHistoryPhoto(id) {
if (!window.__buktiBayarBase64History) {
alert('Foto belum siap.');
return;
}
showLoading(true, 'MENGUNGGAH FOTO...');
google.script.run.withSuccessHandler(function(uploadRes) {
if (uploadRes.error) {
showLoading(false);
alert('Gagal upload: ' + uploadRes.error);
} else {
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
window.__buktiBayarBase64History = null;
updateLocalData(res);
alert('Foto bukti bayar berhasil disimpan!');
showHistoryDetail(id); // Refresh modal rincian
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal update data: ' + err.message);
}).updateTransactionPhoto(id, uploadRes.url);
}
}).uploadPaymentProof({ id: id + '-his', base64: window.__buktiBayarBase64History });
}
function showQRStatic() {
var qrUrl = (settings && settings.qris_image_url) ? String(settings.qris_image_url) : "https://lh3.googleusercontent.com/d/1hGPL3AlVGRMeZTzpwOHp-l-JzOG5tS09";
// Karena saya tidak bisa mengupload gambar asli ke Drive Anda,
// saya akan gunakan modal gambar yang sudah ada
var modalHtml = '<div style="text-align:center;">' +
'<img src="' + qrUrl + '" style="width:100%; max-width:400px; border-radius:12px; box-shadow:0 4px 12px rgba(0,0,0,0.1);">' +
'<div style="margin-top:15px; font-weight:800; color:#111;">SCAN QRIS FUKU</div>' +
'<button class="btn-main" style="margin-top:20px;" onclick="closeModal()">TUTUP</button>' +
'</div>';
openModalCustom(modalHtml, 'QRIS Pembayaran');
}
function openModalCustom(html, title) {
document.getElementById('modal').style.display = 'flex';
document.getElementById('modal-body').innerHTML = html;
var modalTitle = document.querySelector('#modal .title-sm');
if (modalTitle) modalTitle.innerText = title || 'Info';
}
function openCheckout(forcePayNow) {
if (!keranjang.length) { alert('Keranjang kosong'); return; }
autoSavePendingIfNeeded(function() {
openModal(forcePayNow ? 'Selesai' : (document.getElementById('ov-mode').value === 'Pending' ? 'Selesai' : 'Selesai'));
});
}
function openModal(mode) {
document.getElementById('modal').style.display = 'flex';
var body = document.getElementById('modal-body');
var subtotal = keranjang.reduce(function(a, c) { return a + Number(c.qty) * Number(c.harga); }, 0);
var dp = (document.getElementById('ov-mode').value === 'Booking' || document.getElementById('ov-mode').value === 'Pending') ? (Number(document.getElementById('ov-dp-nilai').value) || 0) : 0;
var wa = document.getElementById('ov-wa').value.trim();
var cust = pelanggan.find(function(p) { return p.wa === wa; });
var poinAvail = cust ? (Number(cust.poin) || 0) : 0;
var ovNamaEl = document.getElementById('ov-nama');
if (ovNamaEl && (!ovNamaEl.value || !String(ovNamaEl.value).trim()) && transAktifId) {
var t = transaksi.find(function(x) { return String(x.id) === String(transAktifId); });
if (t && t.nama) ovNamaEl.value = String(t.nama);
}
// Inisialisasi keranjangSplit dengan salinan dari keranjang
// Kita gunakan .map() agar objek item tidak saling terikat
window.__keranjangSplit = keranjang.map(function(it) {
return { nama: it.nama, harga: it.harga, qty: it.qty, type: it.type || '' };
});
window.__isSplit = false;
window.__isMulti = false;
window.__bayarManual = false; // Flag untuk melacak input manual pada jumlah bayar
var poinHtml = '';
if (wa) {
poinHtml = '<div>' +
'<small>Tukar Poin (Rp)</small>' +
'<input id="m-poin" type="number" value="0" oninput="recalcModal()">' +
'<div class="pill-row">' +
'<button class="pill dark on" onclick="cekPoinModal(' + poinAvail + ')">CEK POIN</button>' +
'<button class="pill on" onclick="tukarPoinModal(' + poinAvail + ')">TUKAR POIN</button>' +
'</div>' +
'</div>';
}
body.innerHTML = '<div class="card">' +
'<div class="row2">' +
'<div><small>Meja</small><input value="' + (mejaAktif || '') + '" readonly></div>' +
'<div><small>Status</small><input value="Selesai" readonly></div>' +
'</div>' +
'<div class="row2" style="margin-top:8px;">' +
'<div><small>Nama</small><input id="m-nama" value="' + escapeHtml(document.getElementById('ov-nama').value) + '"></div>' +
'<div><small>WA</small><input id="m-wa" value="' + escapeHtml(document.getElementById('ov-wa').value) + '" readonly></div>' +
'</div>' +
'</div>' +
'<div class="card">' +
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">' +
'<div class="title-sm" style="margin:0;">Keranjang</div>' +
'<button id="btn-toggle-split" class="pill on" style="width:auto; padding:4px 10px; font-size:10px;" onclick="toggleSplitMode()">SPLIT BILL: MATI</button>' +
'</div>' +
'<div id="m-cart"></div>' +
'</div>' +
'<div class="card">' +
'<div class="title-sm">Penyesuaian Harga</div>' +
'<div class="row2">' +
'<div>' +
'<small>Diskon (Rp)</small>' +
'<input id="m-diskon" type="number" value="0" oninput="recalcModal()">' +
'</div>' +
poinHtml +
'</div>' +
'<div class="pill-row">' +
'<button class="pill on" onclick="setDiskonPercent(10)">DISKON 10%</button>' +
'</div>' +
'<div class="pill-row">' +
'<button id="m-tax" class="pill" onclick="toggleTaxModal()">PAJAK 10%</button>' +
'<button id="m-svc" class="pill" onclick="toggleSvcModal()">SERVICE 5%</button>' +
'</div>' +
'<div class="pill-row">' +
'<button id="m-adm-deb" class="pill" onclick="setAdminModal(\'Debit\')">ADMIN DEBIT 1%</button>' +
'<button id="m-adm-cred" class="pill" onclick="setAdminModal(\'Credit\')">ADMIN CREDIT 3%</button>' +
'</div>' +
'</div>' +
'<div class="card">' +
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">' +
'<div class="title-sm" style="margin:0;">QRIS Pembayaran</div>' +
'<button class="pill on" style="width:auto; padding:4px 10px; font-size:10px;" onclick="showQRStatic()">LIHAT QRIS</button>' +
'</div>' +
'<div id="bukti-bayar-section" style="margin-top:5px; padding:12px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">' +
'<div style="font-weight:800; font-size:11px; margin-bottom:10px; display:flex; justify-content:space-between; align-items:center;">' +
'<span>UPLOAD BUKTI BAYAR</span>' +
'<span id="foto-status" style="font-size:10px; color:#64748b;">Belum ada foto</span>' +
'</div>' +
'<div style="display:flex; gap:10px; align-items:center;">' +
'<label class="btn-dark" style="width:auto; flex:1; padding:10px; font-size:11px; cursor:pointer; margin:0;">' +
'📸 AMBIL FOTO' +
'<input type="file" id="input-foto" accept="image/*" capture="camera" style="display:none;" onchange="handleFotoUpload(event)">' +
'</label>' +
'<div id="foto-preview-container" style="width:50px; height:50px; border-radius:8px; background:#eee; overflow:hidden; display:none;">' +
'<img id="foto-preview" style="width:100%; height:100%; object-fit:cover;">' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="card">' +
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">' +
'<div class="title-sm" style="margin:0;">Pembayaran</div>' +
'<button id="btn-toggle-multi" class="pill" style="width:auto; padding:4px 10px; font-size:10px;" onclick="toggleMultiPayment()">MULTI-PEMBAYARAN: MATI</button>' +
'</div>' +
'<div id="payment-standard" class="row2">' +
'<select id="m-metode" onchange="recalcModal()">' +
'<option value="QRIS" selected>QRIS</option>' +
'<option value="Tunai">Tunai</option>' +
'<option value="Transfer">Transfer</option>' +
'<option value="Debit">Debit</option>' +
'<option value="Credit">Credit</option>' +
'</select>' +
'<input id="m-bayar" type="number" value="0" placeholder="Jumlah Bayar" oninput="window.__bayarManual=true;recalcModal()">' +
'</div>' +
'<div id="payment-multi" style="display:none; background:#f8fafc; padding:10px; border-radius:10px; margin-bottom:10px; border:1px dashed #cbd5e1;">' +
'<div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px; margin-bottom:8px;">' +
'<div><small style="font-size:10px; color:#666;">Tunai</small><input id="multi-tunai" type="number" value="0" oninput="recalcModal()"></div>' +
'<div><small style="font-size:10px; color:#666;">QRIS</small><input id="multi-qris" type="number" value="0" oninput="recalcModal()"></div>' +
'</div>' +
'<div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;">' +
'<div><small style="font-size:10px; color:#666;">Debit</small><input id="multi-debit" type="number" value="0" oninput="recalcModal()"></div>' +
'<div><small style="font-size:10px; color:#666;">Credit</small><input id="multi-credit" type="number" value="0" oninput="recalcModal()"></div>' +
'</div>' +
'</div>' +
'<div class="summary">' +
'<div class="sum-row"><span>Subtotal</span><span id="m-sub">Rp 0</span></div>' +
'<div class="sum-row"><span>Diskon</span><span id="m-diskon-val">Rp 0</span></div>' +
'<div class="sum-row"><span>Poin</span><span id="m-poin-val">Rp 0</span></div>' +
'<div class="sum-row"><span>DP</span><span id="m-dp">Rp 0</span></div>' +
'<div class="sum-row"><span>Pajak + Service</span><span id="m-taxsvc">Rp 0</span></div>' +
'<div class="sum-row"><span>Biaya Admin</span><span id="m-admin">Rp 0</span></div>' +
'<div class="sum-strong"><span>Total Bayar Saat Ini</span><span id="m-total">Rp 0</span></div>' +
'<div class="sum-strong"><span id="m-ket">Kembalian</span><span id="m-kembali">Rp 0</span></div>' +
'</div>' +
'<div style="margin-top:10px; display:flex; gap:8px;">' +
'<button class="btn-dark" style="flex:1; background:#444;" onclick="cetakThermalLokal()">CETAK CEK STRUK</button>' +
'<button id="btn-proses-selesai" class="btn-green" style="flex:2;" onclick="prosesSelesai()">BAYAR & WA</button>' +
'</div>' +
'</div>';
window.__m = { taxOn:(getPajakPersen() > 0), svcOn:(getServicePersen() > 0), adminMode:'', dp: dp, subtotal: subtotal };
renderModalCart();
var taxBtn = document.getElementById('m-tax');
if (taxBtn) {
taxBtn.innerText = 'PAJAK ' + getPajakPersen() + '%';
taxBtn.className = 'pill' + (window.__m.taxOn ? ' on' : '');
}
var svcBtn = document.getElementById('m-svc');
if (svcBtn) {
svcBtn.innerText = 'SERVICE ' + getServicePersen() + '%';
svcBtn.className = 'pill' + (window.__m.svcOn ? ' on dark' : '');
}
document.getElementById('m-dp').innerText = 'Rp ' + dp.toLocaleString();
recalcModal();
}
function setDiskonPercent(persen) {
try {
var pct = Number(persen) || 0;
var el = document.getElementById('m-diskon');
if (!el) return;
var subtotal = window.__keranjangSplit.reduce(function(a, c) { return a + Number(c.qty) * Number(c.harga); }, 0);
var diskon = Math.round(subtotal * (pct / 100));
el.value = diskon;
window.__bayarManual = false;
recalcModal();
} catch (e) {}
}
function toggleSplitMode() {
window.__isSplit = !window.__isSplit;
var btn = document.getElementById('btn-toggle-split');
if (window.__isSplit) {
btn.innerText = 'SPLIT BILL: AKTIF';
btn.classList.add('dark');
} else {
btn.innerText = 'SPLIT BILL: MATI';
btn.classList.remove('dark');
// Reset jumlah yang akan dibayar ke jumlah aslinya
window.__keranjangSplit = keranjang.map(function(it) {
return { nama: it.nama, harga: it.harga, qty: it.qty, type: it.type || '' };
});
}
renderModalCart();
recalcModal();
}
function toggleMultiPayment() {
window.__isMulti = !window.__isMulti;
var btn = document.getElementById('btn-toggle-multi');
var std = document.getElementById('payment-standard');
var multi = document.getElementById('payment-multi');
if (window.__isMulti) {
btn.innerText = 'MULTI-PEMBAYARAN: AKTIF';
btn.classList.add('dark');
std.style.display = 'none';
multi.style.display = 'block';
} else {
btn.innerText = 'MULTI-PEMBAYARAN: MATI';
btn.classList.remove('dark');
std.style.display = 'flex';
multi.style.display = 'none';
// Reset multi inputs
['tunai', 'qris', 'debit', 'credit'].forEach(function(m) {
document.getElementById('multi-' + m).value = 0;
});
}
recalcModal();
}
function renderModalCart() {
var box = document.getElementById('m-cart');
box.innerHTML = '';
window.__keranjangSplit.forEach(function(it, idx) {
var row = document.createElement('div');
row.className = 'item-order';
var typeLabel = it.type ? ('<span style="font-size:9px; color:#e11d48; border:1px solid #e11d48; padding:1px 4px; border-radius:4px; margin-right:4px;">' + it.type.toUpperCase() + '</span>') : '';
var itemTotal = (Number(it.qty) * Number(it.harga)).toLocaleString();
var qtyControl = '';
if (window.__isSplit) {
// Dalam mode split, tampilkan tombol +/- untuk jumlah yang akan DIBAYAR SEKARANG
qtyControl = '<div class="qty">' +
'<div class="qty-btn" onclick="updateSplitQty(' + idx + ',-1)">-</div>' +
'<div style="min-width:18px; text-align:center; font-weight:900">' + it.qty + '</div>' +
'<div class="qty-btn" onclick="updateSplitQty(' + idx + ',1)">+</div>' +
'</div>';
} else {
qtyControl = '<div style="font-weight:900">' + it.qty + 'x</div>';
}
row.innerHTML = '<div>' +
'<div style="font-weight:900">' + typeLabel + it.nama + '</div>' +
'<div class="muted">' + it.qty + ' x ' + Number(it.harga).toLocaleString() + ' = Rp ' + itemTotal + '</div>' +
'</div>' +
qtyControl;
box.appendChild(row);
});
}
function updateSplitQty(idx, delta) {
var itSplit = window.__keranjangSplit[idx];
var itAsli = keranjang[idx];
var newVal = itSplit.qty + delta;
// Batas bawah 0, batas atas jumlah asli di keranjang
if (newVal < 0) newVal = 0;
if (newVal > itAsli.qty) newVal = itAsli.qty;
itSplit.qty = newVal;
renderModalCart();
recalcModal();
}
function cetakThermalLokal() {
if (!window.__calc) return;
var c = window.__calc;
var p = {
id: transAktifId || ('F-' + Date.now()),
meja: mejaAktif,
nama: document.getElementById('m-nama').value,
wa: document.getElementById('m-wa').value,
items: c.items,
subtotal: c.subtotal,
diskon: c.diskon,
poinDipakai: c.poin,
pajak: c.pajak,
service: c.service,
adminFee: c.admin,
pajakPersen: c.pajakPersen,
servicePersen: c.servicePersen,
adminPersen: c.adminPersen,
total: c.total,
bayar: c.bayar,
kembali: (c.dp + c.bayar) - c.total,
dp: c.dp,
metodeBayar: c.metode,
catatan: c.isMulti ? JSON.stringify(c.multiDetails) : (document.getElementById('ov-catatan').value || ''),
status: 'Pending',
timestamp: new Date().toISOString().replace('T', ' ').split('.')[0]
};
// Simpan ke database agar tercatat di riwayat (sebagai Pending)
safeRun('saveTransaction', p, function(res) {
if (res && res.transaksi) transaksi = res.transaksi;
transAktifId = p.id; // Update transAktifId agar tidak double jika cetak lagi
renderTables();
});
// Cari poin awal jika ada WA
if (p.wa) {
var cust = pelanggan.find(function(x) { return x.wa === p.wa; });
if (cust) p.poinAwal = Number(cust.poin) || 0;
}
cetakStruk(p, true, true);
}
function recalcModal() {
// Gunakan window.__keranjangSplit untuk kalkulasi total bayar saat ini
var subtotal = window.__keranjangSplit.reduce(function(a, c) { return a + Number(c.qty) * Number(c.harga); }, 0);
var diskon = Math.round(Number(document.getElementById('m-diskon').value) || 0);
var poinEl = document.getElementById('m-poin');
var poin = poinEl ? Math.round(Number(poinEl.value) || 0) : 0;
var base = Math.max(0, subtotal - diskon - poin);
var pajakPersen = getPajakPersen();
var svcPersen = getServicePersen();
var svc = window.__m.svcOn ? Math.round(base * (svcPersen / 100)) : 0;
var pajakBase = base + svc;
var pajak = window.__m.taxOn ? Math.round(pajakBase * (pajakPersen / 100)) : 0;
var beforeAdmin = base + svc + pajak;
var metode = 'Tunai';
var bayar = 0;
var admin = 0;
var adminPersen = 0;
if (window.__isMulti) {
var mT = Number(document.getElementById('multi-tunai').value) || 0;
var mQ = Number(document.getElementById('multi-qris').value) || 0;
var mD = Number(document.getElementById('multi-debit').value) || 0;
var mC = Number(document.getElementById('multi-credit').value) || 0;
bayar = mT + mQ + mD + mC;
// Hitung admin jika ada pembayaran debit/credit di dalam multi
var adminD = window.__m.adminMode === 'Debit' ? Math.round(mD * 0.01) : 0;
var adminC = window.__m.adminMode === 'Credit' ? Math.round(mC * 0.03) : 0;
admin = adminD + adminC;
metode = 'Multi';
} else {
metode = document.getElementById('m-metode').value;
if (window.__m.adminMode === 'Debit' && metode === 'Debit') adminPersen = 1;
if (window.__m.adminMode === 'Credit' && metode === 'Credit') adminPersen = 3;
admin = Math.round(beforeAdmin * (adminPersen / 100));
var bayarEl = document.getElementById('m-bayar');
if (!window.__bayarManual) {
bayarEl.value = Math.max(0, (beforeAdmin + admin) - (window.__m.dp || 0));
}
bayar = Number(bayarEl.value) || 0;
}
var total = beforeAdmin + admin;
var dp = Math.round(Number(window.__m.dp) || 0);
var kembali = (dp + bayar) - total;
var label = 'Kembalian';
if (metode !== 'Tunai' && metode !== 'Multi' && kembali > 0) label = 'Cashback';
if (kembali < 0) { label = 'Kurang'; kembali = Math.abs(kembali); }
document.getElementById('m-sub').innerText = 'Rp ' + subtotal.toLocaleString();
document.getElementById('m-diskon-val').innerText = 'Rp ' + diskon.toLocaleString();
document.getElementById('m-poin-val').innerText = 'Rp ' + poin.toLocaleString();
document.getElementById('m-taxsvc').innerText = 'Rp ' + (pajak + svc).toLocaleString();
document.getElementById('m-admin').innerText = 'Rp ' + admin.toLocaleString();
document.getElementById('m-total').innerText = 'Rp ' + total.toLocaleString();
document.getElementById('m-ket').innerText = label;
document.getElementById('m-kembali').innerText = 'Rp ' + kembali.toLocaleString();
window.__calc = {
items: window.__keranjangSplit.filter(function(i){ return i.qty > 0; }),
subtotal: subtotal,
diskon: diskon,
poin: poin,
base: base,
pajakPersen: (window.__m.taxOn ? pajakPersen : 0),
servicePersen: (window.__m.svcOn ? svcPersen : 0),
adminPersen: adminPersen,
pajak: pajak,
service: svc,
admin: admin,
total: total,
metode: metode,
bayar: bayar,
dp: dp,
poinDapat: Math.floor(base / getPoinEarnRupiah()),
isMulti: window.__isMulti,
multiDetails: window.__isMulti ? {
Tunai: Number(document.getElementById('multi-tunai').value) || 0,
QRIS: Number(document.getElementById('multi-qris').value) || 0,
Debit: Number(document.getElementById('multi-debit').value) || 0,
Credit: Number(document.getElementById('multi-credit').value) || 0
} : null
};
}
function prosesSelesai() {
if (!window.__calc || !window.__calc.items.length) { alert('Tidak ada item yang akan dibayar.'); return; }
var c = window.__calc;
// Simpan sisa keranjang jika mode split aktif
var sisaKeranjang = [];
if (window.__isSplit) {
// Hitung sisa item
keranjang.forEach(function(it, idx) {
var splitIt = window.__keranjangSplit[idx];
var sisaQty = (Number(it.qty) || 0) - (Number(splitIt.qty) || 0);
if (sisaQty > 0) {
sisaKeranjang.push({ nama: it.nama, harga: it.harga, qty: sisaQty, type: it.type || '' });
}
});
if (sisaKeranjang.length > 0) {
if (!confirm('Anda akan melakukan pembayaran sebagian. Sisa pesanan akan tetap ada di meja. Lanjutkan?')) return;
} else {
// Jika sisa kosong, matikan mode split (anggap bayar semua)
window.__isSplit = false;
}
}
var p = basePayload('Selesai');
// Gunakan ID baru hanya jika ini split bill DAN masih ada sisa item.
if (window.__isSplit && sisaKeranjang.length > 0) {
p.id = 'F-SPLIT-' + Date.now();
}
// Jika bukan split atau sisa habis, p.id sudah benar dari basePayload (transAktifId atau generate baru)
p.nama = document.getElementById('m-nama').value.trim();
p.wa = document.getElementById('m-wa').value.trim();
p.items = c.items; // Item yang dibayar saja
p.subtotal = c.subtotal;
p.diskon = c.diskon;
p.poinDipakai = c.poin;
p.pajak = c.pajak;
p.service = c.service;
p.adminFee = c.admin;
p.pajakPersen = c.pajakPersen;
p.servicePersen = c.servicePersen;
p.adminPersen = c.adminPersen;
p.metodeBayar = c.metode;
if (c.isMulti) {
p.metodeBayar = 'Multi';
p.catatan = JSON.stringify(c.multiDetails); // Simpan rincian multi-bayar di catatan
}
p.total = c.total;
p.bayar = c.bayar;
p.kembali = (c.dp + c.bayar) - c.total;
p.dp = c.dp;
p.poinDapat = c.poinDapat;
p.buktiBayar = window.__buktiBayarUrl || '';
var proceedSave = function() {
showLoading(true, 'MENYIMPAN PEMBAYARAN...');
saveFast(p, function(res) {
window.__buktiBayarBase64 = null;
window.__buktiBayarUrl = null;
// Jika masih ada sisa pesanan, update row pending yang asli
if (window.__isSplit && sisaKeranjang.length > 0) {
showLoading(true, 'MEMPERBARUI SISA PESANAN...');
var pendingPayload = basePayload('Pending');
pendingPayload.id = transAktifId; // ID pending asli (e.g. F-123)
pendingPayload.items = sisaKeranjang;
// Penting: Kosongkan DP di sisa pesanan jika DP sudah digunakan di pembayaran split ini
if (c.dp > 0) pendingPayload.dp = 0;
saveFast(pendingPayload, function(res2) {
// HANYA panggil handleFinishSuccess setelah save terakhir selesai
handleFinishSuccess(res2 || res, p);
});
} else {
// Normal (bukan split atau sisa habis)
handleFinishSuccess(res, p);
}
});
};
if (window.__buktiBayarBase64) {
if (!isOnline) {
if (confirm('Anda sedang OFFLINE. Foto bukti bayar tidak bisa diunggah sekarang. Simpan transaksi tanpa foto?')) {
window.__buktiBayarBase64 = null;
proceedSave();
} else {
showLoading(false);
}
return;
}
showLoading(true, 'MENGUNGGAH BUKTI BAYAR...');
google.script.run.withSuccessHandler(function(uploadRes) {
if (uploadRes.error) {
showLoading(false);
if (!confirm('Gagal upload foto: ' + uploadRes.error + '. Lanjutkan simpan tanpa foto?')) return;
proceedSave();
} else {
p.buktiBayar = uploadRes.url;
proceedSave();
}
}).withFailureHandler(function(err) {
showLoading(false);
if (confirm('Gagal koneksi: ' + err.message + '. Simpan tanpa foto?')) proceedSave();
}).uploadPaymentProof({ id: p.id, base64: window.__buktiBayarBase64 });
} else {
proceedSave();
}
}
function handleFinishSuccess(res, p) {
showLoading(false);
if (res && res.lite) {
applyLiteUpdate(res);
} else {
if (!res || (!res.menu && !res.transaksi && !res.pelanggan)) {
res = { menu: menu, transaksi: transaksi, pelanggan: pelanggan, belanja: belanja, rekap: rekap, settings: settings };
}
menu = res.menu || [];
transaksi = res.transaksi || [];
pelanggan = res.pelanggan || [];
}
renderTables();
renderActiveList();
renderHistory();
var saved = transaksi.find(function(t) { return String(t.id) === String(p.id); }) || p;
if (!saved.timestamp) saved.timestamp = new Date().toISOString().replace('T', ' ').split('.')[0];
closeModal();
closeOrderView();
// --- ALUR OTOMATIS BAYAR & WA (Sesuai Permintaan User) ---
// 1. Simpan sudah dilakukan (handleFinishSuccess dipanggil setelah simpan sukses)
// 2. Kirim WA dulu
if (saved.wa && saved.wa.length > 5) {
kirimWA(saved);
}
// 3. Lakukan pencetakan Full Struk (dengan jeda agar tidak diblokir browser)
setTimeout(function() {
cetakStruk(saved, true);
}, 1200);
alert('Pembayaran Berhasil! Pesanan disimpan, WhatsApp dikirim, dan jendela cetak akan terbuka.');
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
function cekPoinModal(poin) {
var redeem = getPoinRedeemRupiah();
alert('Poin tersedia: ' + (poin || 0) + ' (= Rp ' + ((poin || 0) * redeem).toLocaleString() + ')');
}
function tukarPoinModal(poin) {
var inputPoin = prompt('Masukkan jumlah poin yang ingin digunakan (Tersedia: ' + (poin || 0) + '):', poin || 0);
if (inputPoin === null) return;
var pUsed = Math.min(Number(inputPoin) || 0, poin || 0);
var rupiah = pUsed * getPoinRedeemRupiah();
var subtotal = window.__keranjangSplit.reduce(function(a, c) { return a + Number(c.qty) * Number(c.harga); }, 0);
var diskon = Number(document.getElementById('m-diskon').value) || 0;
var maxUse = Math.max(0, subtotal - diskon);
document.getElementById('m-poin').value = Math.min(rupiah, maxUse);
recalcModal();
}
function toggleTaxModal() {
window.__m.taxOn = !window.__m.taxOn;
var el = document.getElementById('m-tax');
if (el) {
el.innerText = 'PAJAK ' + getPajakPersen() + '%';
el.className = 'pill' + (window.__m.taxOn ? ' on' : '');
}
recalcModal();
}
function toggleSvcModal() {
window.__m.svcOn = !window.__m.svcOn;
var el = document.getElementById('m-svc');
if (el) {
el.innerText = 'SERVICE ' + getServicePersen() + '%';
el.className = 'pill' + (window.__m.svcOn ? ' on dark' : '');
}
recalcModal();
}
function setAdminModal(mode) {
if (window.__m.adminMode === mode) window.__m.adminMode = '';
else window.__m.adminMode = mode;
document.getElementById('m-adm-deb').className = 'pill' + (window.__m.adminMode === 'Debit' ? ' on' : '');
document.getElementById('m-adm-cred').className = 'pill' + (window.__m.adminMode === 'Credit' ? ' on' : '');
if (window.__m.adminMode) document.getElementById('m-metode').value = window.__m.adminMode;
recalcModal();
}
function openCheckoutFromActive(id) {
var t = transaksi.find(function(x) { return String(x.id) === String(id); });
if (!t) { alert('Data tidak ditemukan'); return; }
mejaAktif = t.meja;
transAktifId = t.id;
keranjang = Array.isArray(t.items) ? t.items.slice() : [];
document.getElementById('ov-meja').innerText = mejaAktif;
document.getElementById('ov-nama').value = t.nama || '';
document.getElementById('ov-wa').value = t.wa || '';
document.getElementById('ov-tgl').value = (t.tgl && String(t.tgl).includes('T')) ? String(t.tgl).split('T')[0] : (t.tgl || new Date().toISOString().split('T')[0]);
document.getElementById('ov-dp-nilai').value = Number(t.dp) || 0;
document.getElementById('ov-dp-metode').value = t.metodeDp || 'Tunai';
document.getElementById('ov-dp-tgl').value = (t.tglDp && String(t.tglDp).includes('T')) ? String(t.tglDp).split('T')[0] : (t.tglDp || new Date().toISOString().split('T')[0]);
document.getElementById('order-view').style.display = 'block';
document.getElementById('meja-home').style.display = 'none';
document.getElementById('ov-mode').value = 'Pending';
renderCategories();
renderMenu();
renderCart();
applyMode();
openCheckout(true);
}
function hapusPesananAktif(id, meja) {
if (!confirm('Hapus/Cancel pesanan aktif ' + meja + '?')) return;
showLoading(true, 'MENGHAPUS PESANAN...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
alert('Pesanan ' + meja + ' berhasil dibatalkan.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal hapus: ' + err.message);
}).deleteTransaction(id);
}
function renderActiveList() {
var box = document.getElementById('list-active');
if (!box) return;
box.innerHTML = '';
var act = transaksi.filter(function(t) { return t.status === 'Pending' || t.status === 'Ready'; }).slice().reverse();
if (!act.length) {
box.innerHTML = '<div class="muted" style="text-align:center">Belum ada pesanan aktif.</div>';
return;
}
act.forEach(function(t) {
var div = document.createElement('div');
div.className = 'trans-card';
var jamPesan = t.timestamp ? (t.timestamp.includes(' ') ? t.timestamp.split(' ')[1].slice(0, 5) : '') : '';
var itemsLabel = (Array.isArray(t.items) ? t.items : []).map(function(i) { return i.qty + 'x ' + i.nama; }).join(', ');
div.innerHTML = '<div style="display:flex; justify-content:space-between; align-items:center;">' +
'<div>' +
'<div style="font-weight:900">' + t.meja + '</div>' +
'<div class="muted">' + t.id + ' | Jam: ' + jamPesan + '</div>' +
'</div>' +
'<div class="status st-pending">PENDING</div>' +
'</div>' +
'<div style="margin-top:8px; font-weight:900">Pemesan: ' + escapeHtml(t.nama || '-') + '</div>' +
'<div class="muted" style="margin-top:4px;">' + escapeHtml(itemsLabel) + '</div>' +
'<div style="display:flex; gap:8px; margin-top:10px;">' +
'<button class="btn-dark" style="flex:1;" onclick="openCheckoutFromActive(\'' + t.id + '\')">LIHAT PESANAN</button>' +
'<button class="btn-green" style="flex:1;" onclick="openCheckoutFromActive(\'' + t.id + '\')">BAYAR</button>' +
'<button class="btn-void" style="position:static; padding:10px; flex:0.5;" onclick="hapusPesananAktif(\'' + t.id + '\', \'' + t.meja + '\')">CANCEL</button>' +
'</div>';
box.appendChild(div);
});
}
function renderBookingTab() {
var box = document.getElementById('list-booking');
if (!box) return;
box.innerHTML = '';
var list = transaksi.filter(function(t) { return t.status === 'Booking'; }).slice().reverse();
if (!list.length) {
box.innerHTML = '<div class="muted" style="text-align:center">Belum ada booking.</div>';
return;
}
list.forEach(function(t) {
var div = document.createElement('div');
div.className = 'trans-card';
var tglStr = t.tgl ? t.tgl.split(' ')[0] : '';
var jamStr = t.jam ? (t.jam.includes(' ') ? t.jam.split(' ')[1] : t.jam) : '';
if (jamStr.length > 5) jamStr = jamStr.slice(0, 5); // Ambil HH:mm saja
div.innerHTML = '<div style="display:flex; justify-content:space-between; align-items:center;">' +
'<div><div style="font-weight:900">' + t.meja + '</div><div class="muted">' + t.id + '</div></div>' +
'<div class="status st-booking">BOOKING</div>' +
'</div>' +
'<div style="margin-top:8px; font-weight:900">Pemesan: ' + escapeHtml(t.nama || '-') + '</div>' +
'<div style="margin-top:4px; font-weight:bold; color:#2563eb;">Jadwal: ' + escapeHtml(tglStr) + ' | Jam: ' + escapeHtml(jamStr) + '</div>' +
'<div class="muted" style="margin-top:4px;">DP: Rp ' + (Number(t.dp)||0).toLocaleString() + ' (' + escapeHtml(t.metodeDp||'') + ')</div>' +
'<div style="display:flex; gap:8px; margin-top:10px;">' +
'<button class="btn-blue" style="flex:1;" onclick="doActivateBooking(\'' + t.id + '\')">AKTIFKAN MEJA</button>' +
'<button class="btn-green" style="flex:1;" onclick="kirimWAById(\'' + t.id + '\')">KIRIM WA</button>' +
'<button class="btn-dark" style="flex:1;" onclick="cetakStrukById(\'' + t.id + '\', true)">🖨️ CETAK</button>' +
'</div>';
box.appendChild(div);
});
}
function doActivateBooking(id) {
showLoading(true);
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.error) { alert(res.error); return; }
menu = res.menu || [];
transaksi = res.transaksi || [];
pelanggan = res.pelanggan || [];
renderTables();
renderActiveList();
renderBookingTab();
alert('Booking diaktifkan menjadi Pending.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal: ' + err.message);
}).activateBooking(id);
}
function renderCustomers() {
var q = (document.getElementById('cust-search').value || '').toLowerCase();
var box = document.getElementById('cust-list');
if (!box) return;
box.innerHTML = '';
// Grouping berdasarkan WA unik
var uniqueCust = {};
pelanggan.forEach(function(p) {
if (!uniqueCust[p.wa]) uniqueCust[p.wa] = { nama: p.nama, wa: p.wa, poin: 0, riwayat: [] };
uniqueCust[p.wa].poin = Math.max(uniqueCust[p.wa].poin, Number(p.poin) || 0);
});
// Cari riwayat dari transaksi yang SELESAI
transaksi.forEach(function(t) {
if (t.status === 'Selesai' && t.wa && uniqueCust[t.wa]) {
var poinDapat = Number(t.poinDapat) || 0;
var poinPakai = Math.floor((Number(t.poinDipakai) || 0) / getPoinRedeemRupiah());
if (poinDapat > 0 || poinPakai > 0) {
uniqueCust[t.wa].riwayat.push({
tgl: t.timestamp ? t.timestamp.split(' ')[0] : '',
dapat: poinDapat,
pakai: poinPakai, // Ini adalah nilai poin, bukan rupiah
id: t.id
});
}
}
});
var list = [];
for (var key in uniqueCust) {
var p = uniqueCust[key];
if ((p.nama || '').toLowerCase().indexOf(q) !== -1 || (p.wa || '').indexOf(q) !== -1) {
list.push(p);
}
}
if (!list.length) {
box.innerHTML = '<div class="muted" style="text-align:center">Tidak ditemukan.</div>';
return;
}
list.forEach(function(p, idx) {
var div = document.createElement('div');
div.className = 'card';
div.style.padding = '10px';
div.style.marginBottom = '8px';
var riwayatHtml = p.riwayat.length ? p.riwayat.map(function(r) {
return '<div style="font-size:10px; border-top:1px solid #eee; padding:4px 0; display:flex; justify-content:space-between;">' +
'<span>' + r.tgl + ' (ID: ' + r.id + ')</span>' +
'<span>' + (r.dapat > 0 ? '+' + r.dapat : '') + ' ' + (r.pakai > 0 ? '-' + r.pakai : '') + '</span>' +
'</div>';
}).join('') : '<div class="muted" style="font-size:10px; text-align:center;">Belum ada riwayat poin.</div>';
var waBtn = p.wa ? '<button class="btn-green" style="width:100%; font-size:11px; margin-top:10px; padding:6px;" onclick="sendPoinWA(' + idx + ')">KIRIM RIWAYAT KE WA</button>' : '';
div.innerHTML = '<div style="display:flex; justify-content:space-between; align-items:center; cursor:pointer;" onclick="toggleRiwayatPoin(' + idx + ')">' +
'<div>' +
'<div style="font-weight:900; font-size:13px;">' + escapeHtml(p.nama || '-') + '</div>' +
'<div class="muted">' + escapeHtml(p.wa) + '</div>' +
'</div>' +
'<div style="text-align:right">' +
'<div style="font-weight:900; font-size:16px; color:#16a34a">' + p.poin + ' Poin</div>' +
'<div style="font-size:9px; color:#2563eb;">Klik untuk riwayat</div>' +
'</div>' +
'</div>' +
'<div id="riwayat-poin-' + idx + '" style="display:none; margin-top:10px;">' +
riwayatHtml +
waBtn +
'</div>';
box.appendChild(div);
});
}
function sendPoinWA(idx) {
// Kita butuh data pelanggan lagi dari list yang dirender
var q = (document.getElementById('cust-search').value || '').toLowerCase();
var uniqueCust = {};
pelanggan.forEach(function(p) {
if (!uniqueCust[p.wa]) uniqueCust[p.wa] = { nama: p.nama, wa: p.wa, poin: 0, riwayat: [] };
uniqueCust[p.wa].poin = Math.max(uniqueCust[p.wa].poin, Number(p.poin) || 0);
});
transaksi.forEach(function(t) {
if (t.status === 'Selesai' && t.wa && uniqueCust[t.wa]) {
var poinDapat = Number(t.poinDapat) || 0;
var poinPakai = Math.floor((Number(t.poinDipakai) || 0) / getPoinRedeemRupiah());
if (poinDapat > 0 || poinPakai > 0) {
uniqueCust[t.wa].riwayat.push({ tgl: t.timestamp ? t.timestamp.split(' ')[0] : '', dapat: poinDapat, pakai: poinPakai, id: t.id });
}
}
});
var list = [];
for (var key in uniqueCust) {
var p = uniqueCust[key];
if ((p.nama || '').toLowerCase().indexOf(q) !== -1 || (p.wa || '').indexOf(q) !== -1) list.push(p);
}
var cust = list[idx];
if (!cust || !cust.wa) return;
var text = '*RIWAYAT POIN PELANGGAN*\n' +
'*' + getStoreName() + '*\n\n' +
'Nama: ' + (cust.nama || '-') + '\n' +
'WA: ' + cust.wa + '\n' +
'-------------------------\n' +
'*Total Poin Saat Ini: ' + cust.poin + '*\n\n' +
'*RIWAYAT TRANSAKSI POIN:*\n';
if (cust.riwayat.length) {
cust.riwayat.slice(-10).forEach(function(r) {
text += '- ' + r.tgl + ' (ID:' + r.id.slice(-6) + '): ' + (r.dapat > 0 ? '+' + r.dapat : '') + (r.pakai > 0 ? ' -' + r.pakai : '') + '\n';
});
} else {
text += 'Belum ada riwayat.\n';
}
text += buildStoreFooterText();
var cleanWA = cust.wa.replace(/\D/g, '');
var dest = cleanWA.indexOf('0') === 0 ? ('62' + cleanWA.slice(1)) : (cleanWA.indexOf('8') === 0 ? ('62' + cleanWA) : cleanWA);
window.open('https://wa.me/' + dest + '?text=' + encodeURIComponent(text), '_blank');
}
function toggleRiwayatPoin(idx) {
var el = document.getElementById('riwayat-poin-' + idx);
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
function renderHistory() {
var box = document.getElementById('his-list');
if (!box) return;
// --- Filter & Group History ---
box.innerHTML = '';
// Termasuk status 'Pending' agar bisa melihat transaksi yang sudah dicetak struk cek tapi belum bayar
var filtered = transaksi.filter(function(t) { return t.status === 'Selesai' || t.status === 'Void' || t.status === 'Pending'; });
var q = document.getElementById('his-search').value.toLowerCase();
var m = document.getElementById('his-filter-metode').value;
var hasBukti = document.getElementById('his-filter-bukti').checked;
if (q || m || hasBukti) {
filtered = filtered.filter(function(t) {
var matchQ = !q || String(t.id).toLowerCase().indexOf(q) !== -1 || String(t.meja).toLowerCase().indexOf(q) !== -1 || String(t.nama).toLowerCase().indexOf(q) !== -1;
var matchM = !m || t.metodeBayar === m;
var matchB = !hasBukti || !!t.buktiBayar;
return matchQ && matchM && matchB;
});
}
var groups = {};
filtered.forEach(function(t) {
var date = t.timestamp ? t.timestamp.split(' ')[0] : (t.tgl ? t.tgl.split(' ')[0] : 'Tanpa Tanggal');
if (!groups[date]) groups[date] = { items: [], total: 0 };
groups[date].items.push(t);
if (t.status === 'Selesai') groups[date].total += (Number(t.total) || 0);
});
var sortedDates = Object.keys(groups).sort().reverse();
sortedDates.forEach(function(date) {
var g = groups[date];
var header = document.createElement('div');
header.className = 'title-sm';
header.style.marginTop = '20px';
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.background = '#f8fafc';
header.style.padding = '10px 15px';
header.style.borderRadius = '12px';
header.style.border = '1px solid #e2e8f0';
header.innerHTML = '<span>📅 ' + date + '</span><span style="color:#e11d48;">Rp ' + fmtRp(g.total) + '</span>';
box.appendChild(header);
g.items.sort(function(a,b){
var da = new Date(a.timestamp || a.tgl);
var db = new Date(b.timestamp || b.tgl);
return db - da;
});
g.items.forEach(function(t) {
var d = new Date(t.timestamp);
var time = !isNaN(d.getTime()) ? d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }) : '??:??';
var row = document.createElement('div');
row.className = 'hist-item';
if (t.status === 'Void') row.classList.add('void');
// Penanda status untuk Pending
var statusBadge = '';
if (t.status === 'Pending') {
statusBadge = ' <span style="background:#f59e0b; color:white; font-size:8px; padding:2px 5px; border-radius:4px; font-weight:bold;">STRUK CEK</span>';
}
row.onclick = function() { showHistoryDetail(t.id); };
var metode = t.metodeBayar || (t.status === 'Pending' ? 'BELUM BAYAR' : 'Tunai');
var buktiIcon = t.buktiBayar ? ' <span style="color:#6366f1; font-size:10px;">📷</span>' : '';
row.innerHTML = '<div class="main">' +
'<div class="id">' + t.id + statusBadge + buktiIcon + '</div>' +
'<div class="meja">Meja ' + t.meja + ' | ' + (t.nama || '-') + '</div>' +
'</div>' +
'<div class="side" style="text-align:right;">' +
'<div class="total">Rp ' + fmtRp(t.total) + '</div>' +
'<div class="time">' + metode + ' | ' + time + '</div>' +
'</div>';
box.appendChild(row);
});
});
if (!sortedDates.length) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:40px;">Tidak ada riwayat transaksi ditemukan.</div>';
} else {
// Tambahkan tombol load more di akhir
var loadMoreBtn = document.createElement('button');
loadMoreBtn.className = 'btn-main';
loadMoreBtn.style.marginTop = '20px';
loadMoreBtn.innerText = 'MUAT LEBIH BANYAK';
loadMoreBtn.onclick = function() {
showLoading(true, 'MEMUAT DATA...');
var currentCount = transaksi.filter(function(t) { return t.status === 'Selesai' || t.status === 'Void' || t.status === 'Pending'; }).length;
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.length > 0) {
transaksi = transaksi.concat(res);
renderHistory();
} else {
loadMoreBtn.innerText = 'DATA SUDAH HABIS';
loadMoreBtn.disabled = true;
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal memuat: ' + err.message);
}).getMoreHistory(currentCount);
};
box.appendChild(loadMoreBtn);
}
}
function saveNewPaymentMethod(id) {
var newMethod = document.getElementById('edit-metode-' + id).value;
if (!confirm('Ubah metode pembayaran menjadi ' + newMethod + '?')) return;
showLoading(true, 'SEDANG MENGUPDATE...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
transaksi = res.transaksi;
renderHistory();
closeModal();
alert('Metode pembayaran berhasil diubah!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal: ' + err.message);
}).updatePaymentMethod(id, newMethod);
}
function pindahMeja() {
if (!mejaAktif || !transAktifId) {
alert('Pilih pesanan aktif terlebih dahulu.');
return;
}
var targetMeja = prompt('Pindah pesanan ' + mejaAktif + ' ke meja mana?\n(Contoh: Meja-5)', '');
if (!targetMeja) return;
// Validasi target meja (pastikan meja target kosong)
var isOccupied = transaksi.find(function(t) {
return t.meja === targetMeja && (t.status === 'Pending' || t.status === 'Ready');
});
if (isOccupied) {
alert('Gagal: ' + targetMeja + ' sedang digunakan oleh pesanan lain.');
return;
}
if (!confirm('Pindahkan pesanan dari ' + mejaAktif + ' ke ' + targetMeja + '?')) return;
showLoading(true, 'MEMINDAHKAN MEJA...');
// Ambil data transaksi saat ini
var p = basePayload('Pending');
p.meja = targetMeja; // Ganti meja target
save(p, function(res) {
showLoading(false);
updateLocalData(res);
alert('Pesanan berhasil dipindah ke ' + targetMeja);
closeOrderView();
});
}
function showHistoryDetail(val) {
var t = null;
if (typeof val === 'number') {
t = transaksi[val];
} else {
t = transaksi.find(function(x) { return String(x.id) === String(val); });
}
if (!t) return;
var idx = (typeof val === 'number') ? val : transaksi.findIndex(function(x) { return String(x.id) === String(t.id); });
if (idx < 0) idx = 0;
var isVoid = String(t.status).toLowerCase() === 'void';
var statusClass = t.status === 'Booking' ? 'st-booking' : (isVoid ? 'st-void' : 'st-selesai');
var catatanHtml = '';
if (t.catatan && t.metodeBayar !== 'Multi') {
catatanHtml = '<div style="background:#fff7ed; padding:10px; border-radius:8px; margin-bottom:10px; border:1px dashed #fb923c; font-size:12px; color:#9a3412;">' +
'<b>Catatan:</b> ' + escapeHtml(t.catatan) +
'</div>';
}
var itemsHtml = (t.items || []).map(function(it) {
var itTotal = fmtRp(Number(it.qty) * Number(it.harga));
return '<div class="sum-row" style="margin-bottom:8px; align-items:flex-start;">' +
'<div style="flex:1;">' +
'<div style="font-weight:900;">' + it.nama + '</div>' +
'<div class="muted">' + it.qty + ' x ' + fmtRp(it.harga) + '</div>' +
'</div>' +
'<div style="font-weight:900;">Rp ' + itTotal + '</div>' +
'</div>';
}).join('');
var html = '<div class="trans-card' + (isVoid ? ' voided' : '') + '">' +
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">' +
'<div><div style="font-weight:900">' + t.meja + '</div><div class="muted">' + t.id + '</div></div>' +
'<div class="status ' + statusClass + '">' + t.status.toUpperCase() + '</div>' +
'</div>' +
'<div class="muted" style="margin-bottom:10px;">' + (t.nama || '-') + ' | ' + (t.wa || '-') + '</div>' +
catatanHtml +
'<div style="background:#f9fafb; padding:10px; border-radius:8px; margin-bottom:10px;">' + itemsHtml + '</div>' +
'<div class="sum-strong"><span>TOTAL</span><span>Rp ' + fmtRp(t.total) + '</span></div>' +
'<div style="margin-top:10px; padding:10px; background:#f1f5f9; border-radius:12px;">' +
'<small style="font-weight:800; color:#64748b; display:block; margin-bottom:5px;">METODE PEMBAYARAN</small>' +
'<div style="display:flex; gap:8px; align-items:center;">' +
'<select id="edit-metode-' + t.id + '" style="flex:1; padding:8px; border-radius:8px; border:1px solid #cbd5e1; font-size:12px; font-weight:700;">' +
'<option value="Tunai"' + (t.metodeBayar === 'Tunai' ? ' selected' : '') + '>Tunai</option>' +
'<option value="QRIS"' + (t.metodeBayar === 'QRIS' ? ' selected' : '') + '>QRIS</option>' +
'<option value="Transfer"' + (t.metodeBayar === 'Transfer' ? ' selected' : '') + '>Transfer</option>' +
'<option value="Debit"' + (t.metodeBayar === 'Debit' ? ' selected' : '') + '>Debit</option>' +
'<option value="Credit"' + (t.metodeBayar === 'Credit' ? ' selected' : '') + '>Credit</option>' +
'<option value="Multi"' + (t.metodeBayar === 'Multi' ? ' selected' : '') + '>Multi</option>' +
'</select>' +
'<button class="btn-main" style="width:auto; padding:8px 12px; font-size:10px; margin:0;" onclick="saveNewPaymentMethod(\'' + t.id + '\')">UPDATE</button>' +
'</div>' +
'</div>' +
(function(){
if (t.metodeBayar === 'Multi' && t.catatan) {
var details = {}; try { details = JSON.parse(t.catatan); } catch(e) {}
var res = '';
for (var m in details) {
if (details[m] > 0) {
res += '<div class="sum-row" style="font-size:11px; color:#e11d48;"><span>- ' + m + '</span><span>Rp ' + fmtRp(details[m]) + '</span></div>';
}
}
return '<div style="background:#fff1f2; padding:5px 10px; border-radius:6px; margin-top:5px;">' + res + '</div>';
}
return '';
})() +
(t.buktiBayar ? '<div style="margin-top:10px;"><a href="' + t.buktiBayar + '" target="_blank" class="btn-main" style="display:inline-block; width:auto; padding:8px 15px; font-size:11px; background:#6366f1;">👁️ LIHAT BUKTI BAYAR</a></div>' :
'<div style="margin-top:10px; padding:12px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">' +
'<div style="font-weight:800; font-size:11px; margin-bottom:10px; display:flex; justify-content:space-between; align-items:center;">' +
'<span>UPLOAD BUKTI BAYAR</span>' +
'<span id="foto-status-his" style="font-size:10px; color:#64748b;">Belum ada foto</span>' +
'</div>' +
'<div style="display:flex; gap:10px; align-items:center;">' +
'<label class="btn-dark" style="width:auto; flex:1; padding:10px; font-size:11px; cursor:pointer; margin:0; background:#444;">' +
'📸 AMBIL FOTO' +
'<input type="file" id="input-foto-his" accept="image/*" capture="camera" style="display:none;" onchange="handleFotoUploadHistory(event)">' +
'</label>' +
'<div id="foto-preview-container-his" style="width:50px; height:50px; border-radius:8px; background:#eee; overflow:hidden; display:none;">' +
'<img id="foto-preview-his" style="width:100%; height:100%; object-fit:cover;">' +
'</div>' +
'<button id="btn-save-foto-his" class="btn-green" style="width:auto; padding:10px; font-size:11px; margin:0; display:none;" onclick="saveHistoryPhoto(\'' + t.id + '\')">SIMPAN FOTO</button>' +
'</div>' +
'</div>'
) +
(isVoid ? '' : '<div style="display:grid; gap:8px; margin-top:15px;">' +
'<div style="display:flex; gap:8px;">' +
'<button class="btn-dark" style="flex:1;" onclick="cetakStrukByIndex(' + idx + ')">CETAK (BIASA)</button>' +
'<button class="btn-dark" style="flex:1; background:#444;" onclick="cetakStrukTajamByIndex(' + idx + ')">⚡ CETAK (TAJAM)</button>' +
'</div>' +
'<div style="display:flex; gap:8px;">' +
'<button class="btn-green" style="flex:1;" onclick="kirimWAByIndex(' + idx + ')">KIRIM WA</button>' +
(t.status === 'Selesai' ? '<button class="btn-red" style="flex:0.5; background:#ef4444; color:white; border:none; border-radius:12px; font-weight:900;" onclick="voidTransaction(\'' + t.id + '\')">VOID</button>' : '') +
'</div>' +
'</div>') +
'</div>';
document.getElementById('modal-body').innerHTML = html;
var modalTitle = document.querySelector('#modal .title-sm');
if (modalTitle) modalTitle.innerText = 'Rincian Riwayat';
document.getElementById('modal').style.display = 'flex';
}
function loadReport() {
var dari = document.getElementById('lap-dari').value;
var sampai = document.getElementById('lap-sampai').value;
// Pastikan default hari ini jika kosong
if (!dari) {
dari = new Date().toISOString().split('T')[0];
document.getElementById('lap-dari').value = dari;
}
if (!sampai) {
sampai = new Date().toISOString().split('T')[0];
document.getElementById('lap-sampai').value = sampai;
}
showLoading(true, 'MENYIAPKAN LAPORAN...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
document.getElementById('lap-omzet').innerText = 'Rp ' + Number(res.omzet || 0).toLocaleString();
document.getElementById('lap-belanja').innerText = 'Rp ' + Number(res.belanja || 0).toLocaleString();
var methBox = document.getElementById('lap-methods');
methBox.innerHTML = '';
for (var m in res.methods) {
if (res.methods[m] > 0) {
var d = document.createElement('div');
d.className = 'sum-row';
d.innerHTML = '<span>' + m + '</span><span>Rp ' + res.methods[m].toLocaleString() + '</span>';
methBox.appendChild(d);
}
}
var box = document.getElementById('lap-items');
box.innerHTML = '';
if (!res.items || !res.items.length) {
box.innerHTML = '<div class="muted" style="text-align:center">Belum ada data transaksi pada periode ini.</div>';
} else {
res.items.slice(0, 15).forEach(function(it) {
var div = document.createElement('div');
div.className = 'item-order';
div.innerHTML = '<div style="font-weight:900">' + escapeHtml(it[0]) + '</div><div style="font-weight:900">' + it[1] + ' porsi</div>';
box.appendChild(div);
});
}
window.__lastReport = res; // simpan untuk kirim WA
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal memuat laporan: ' + err.message);
}).getReport(dari, sampai);
}
function setLapRange(type) {
var now = new Date();
var from = new Date();
if (type === 'today') {
// from is already now
} else if (type === 'week') {
from.setDate(now.getDate() - now.getDay());
} else if (type === 'month') {
from.setDate(1);
}
document.getElementById('lap-dari').value = from.toISOString().split('T')[0];
document.getElementById('lap-sampai').value = now.toISOString().split('T')[0];
// Update warna tombol
var pills = document.querySelectorAll('#sec-laporan .pill');
for (var i = 0; i < pills.length; i++) {
pills[i].classList.remove('on');
}
var idx = (type === 'today' ? 0 : (type === 'week' ? 1 : 2));
pills[idx].classList.add('on');
loadReport();
}
function sendReportWA(type) {
var res = window.__lastReport;
if (!res || !res.omzet) {
alert('Data laporan belum dimuat atau omzet masih 0. Tunggu sampai data muncul.');
return;
}
var dari = document.getElementById('lap-dari').value;
var sampai = document.getElementById('lap-sampai').value;
var period = (type === 'daily' || dari === sampai) ? dari : (dari + ' s/d ' + sampai);
var footerText = buildStoreFooterText();
var text = '*LAPORAN ' + getStoreName().toUpperCase() + '*\nPeriode: ' + period + '\n\n' +
'*RINGKASAN:*\n' +
'Omzet: Rp ' + res.omzet.toLocaleString() + '\n' +
'Belanja: Rp ' + (res.belanja || 0).toLocaleString() + '\n' +
'-------------------------\n' +
'*NET: Rp ' + (res.omzet - (res.belanja || 0)).toLocaleString() + '*\n\n' +
'*DETAIL PEMBAYARAN:*\n';
for (var m in res.methods) {
if (res.methods[m] > 0) text += '- ' + m + ': Rp ' + res.methods[m].toLocaleString() + '\n';
}
if (res.items && res.items.length) {
var totalPorsi = 0;
res.items.forEach(function(it) { totalPorsi += it[1]; });
text += '\n*PORSI TERJUAL:* (Total: ' + totalPorsi + ')\n';
var groups = groupPortionsFromPairs(res.items);
text = appendGroupedPortionsToText(text, groups);
}
text += footerText;
openWhatsAppToMany(getReportRecipients(), text);
}
function sendDailyHistoryWA() {
var today = new Date().toISOString().split('T')[0];
var itemsRiwayat = transaksi.filter(function(t) {
var tgl = t.timestamp ? t.timestamp.split(' ')[0] : (t.tgl ? t.tgl.split(' ')[0] : '');
return tgl === today && (t.status === 'Selesai' || t.status === 'Void');
});
if (itemsRiwayat.length === 0) {
alert('Belum ada riwayat transaksi hari ini.');
return;
}
itemsRiwayat.sort(function(a, b) {
var da = new Date(a.timestamp || a.tgl);
var db = new Date(b.timestamp || b.tgl);
return da - db;
});
var text = '📝 *RIWAYAT TRANSAKSI HARI INI* 📝\n' +
'📅 Tanggal: ' + today + '\n\n';
var totalHariIni = 0;
itemsRiwayat.forEach(function(t) {
var st = t.status === 'Void' ? '[VOID] ' : '';
var met = t.metodeBayar || 'Tunai';
var tot = Number(t.total) || 0;
text += '- ' + st + t.id + ' | ' + t.meja + ' | ' + (t.nama || '-') + ' | Rp ' + tot.toLocaleString() + ' (' + met + ')\n';
if (t.status === 'Selesai') totalHariIni += tot;
});
text += '\n*TOTAL SELESAI: Rp ' + totalHariIni.toLocaleString() + '*\n\n' +
'📍 *' + getStoreName() + '*';
openWhatsAppToMany(getReportRecipients(), text);
}
function sendCombinedWA() {
var today = new Date().toISOString().split('T')[0];
var res = window.__lastReport;
// 1. Ambil data dari laporan (Omzet)
if (!res || !res.omzet) {
alert('Mohon tunggu data laporan dimuat (SYNC).');
return;
}
// 2. Ambil data Rekap
var saldoAwal = Number(document.getElementById('rk-saldo-awal').value) || 0;
var catatan = document.getElementById('rk-catatan').value.trim();
var methods = { 'Tunai': 0, 'QRIS': 0, 'Debit': 0, 'Credit': 0, 'Transfer': 0 };
var dpMethods = { 'Tunai': 0, 'QRIS': 0 };
var totalNota = 0;
var itemCounter = {};
transaksi.forEach(function(t) {
var isToday = t.timestamp && t.timestamp.indexOf(today) === 0;
if (t.status === 'Selesai' && isToday) {
totalNota++;
if (t.metodeBayar === 'Multi' && t.catatan) {
var details = {}; try { details = JSON.parse(t.catatan); } catch(e) {}
for (var m in details) { if (methods.hasOwnProperty(m)) methods[m] += (Number(details[m]) || 0); }
} else {
var m = t.metodeBayar || 'Tunai';
if (methods.hasOwnProperty(m)) methods[m] += (Number(t.total) || 0);
}
// Hitung porsi terjual
(t.items || []).forEach(function(it) {
itemCounter[it.nama] = (itemCounter[it.nama] || 0) + Number(it.qty);
});
}
if (t.status === 'Booking' && isToday) {
var dm = t.metodeDp || 'Tunai';
if (dpMethods.hasOwnProperty(dm)) dpMethods[dm] += (Number(t.dp) || 0);
}
});
// 3. Ambil data Belanja
var itemsBelanja = belanja.filter(function(b) { return b.tgl && b.tgl.indexOf(today) === 0; });
var totalBelanja = itemsBelanja.reduce(function(a, c) { return a + (Number(c.total) || 0); }, 0);
// 3.1 Ambil data Riwayat Transaksi Hari Ini
var itemsRiwayat = transaksi.filter(function(t) {
var tgl = t.timestamp ? t.timestamp.split(' ')[0] : (t.tgl ? t.tgl.split(' ')[0] : '');
return tgl === today && (t.status === 'Selesai' || t.status === 'Void');
});
itemsRiwayat.sort(function(a, b) {
var da = new Date(a.timestamp || a.tgl);
var db = new Date(b.timestamp || b.tgl);
return da - db;
});
var totalPenjualan = 0;
for (var m in methods) totalPenjualan += methods[m];
var saldoTunaiAkhir = saldoAwal + methods.Tunai + dpMethods.Tunai - totalBelanja;
// 4. Susun Pesan WA
var text = '🔥 *LAPORAN GABUNGAN ' + getStoreName().toUpperCase() + '* 🔥\n' +
'📅 Tanggal: ' + today + '\n\n' +
'💰 *RINGKASAN OMZET:*\n' +
'- Omzet: Rp ' + totalPenjualan.toLocaleString() + '\n' +
'- Total Nota: ' + totalNota + '\n\n' +
'💳 *DETAIL PEMBAYARAN:*\n';
for (var m in methods) {
if (methods[m] > 0) text += '- ' + m + ': Rp ' + methods[m].toLocaleString() + '\n';
}
text += '\n🛒 *LAPORAN BELANJA:*\n';
if (itemsBelanja.length > 0) {
itemsBelanja.forEach(function(b) {
text += '- ' + b.nama + ' (' + b.qty + 'x) : Rp ' + (Number(b.total)||0).toLocaleString() + '\n';
});
text += '*TOTAL BELANJA: Rp ' + totalBelanja.toLocaleString() + '*\n';
} else {
text += '_Tidak ada pengeluaran belanja hari ini._\n';
}
text += '\n📊 *REKAP KASIR:*\n' +
'- Saldo Awal: Rp ' + saldoAwal.toLocaleString() + '\n' +
'- DP Tunai: Rp ' + dpMethods.Tunai.toLocaleString() + '\n' +
'- DP QRIS: Rp ' + dpMethods.QRIS.toLocaleString() + '\n' +
'-------------------------\n' +
'✅ *SALDO TUNAI LACI: Rp ' + saldoTunaiAkhir.toLocaleString() + '*\n';
if (catatan) text += '\n📝 *CATATAN:* ' + catatan + '\n';
text += '\n📝 *RIWAYAT TRANSAKSI HARI INI:*\n';
if (itemsRiwayat.length > 0) {
itemsRiwayat.forEach(function(t) {
var st = t.status === 'Void' ? '[VOID] ' : '';
var met = t.metodeBayar || 'Tunai';
text += '- ' + st + t.id + ' | ' + t.meja + ' | ' + (t.nama || '-') + ' | Rp ' + (Number(t.total)||0).toLocaleString() + ' (' + met + ')\n';
});
} else {
text += '_Belum ada transaksi hari ini._\n';
}
var totalPorsi = 0;
var itemsSortedToday = Object.keys(itemCounter).map(function(k) {
totalPorsi += itemCounter[k];
return [k, itemCounter[k]];
}).sort(function(a, b) { return b[1] - a[1]; });
if (itemsSortedToday.length > 0) {
text += '\n🍱 *PORSI TERJUAL:* (Total: ' + totalPorsi + ')\n';
var groups2 = groupPortionsFromPairs(itemsSortedToday);
text = appendGroupedPortionsToText(text, groups2);
}
text += '\n📍 *' + getStoreName() + '*\n' + (getStoreAddress() || '');
openWhatsAppToMany(getReportRecipients(), text);
}
function sendBelanjaWA(tgl) {
var items = belanja.filter(function(b) { return b.tgl && b.tgl.indexOf(tgl) === 0; });
if (!items.length) return;
var total = items.reduce(function(a, c) { return a + (Number(c.total) || 0); }, 0);
var text = '*LAPORAN BELANJA ' + getStoreName().toUpperCase() + '*\nTanggal: ' + tgl + '\n\n';
items.forEach(function(b) {
text += '- ' + b.nama + ' (' + b.qty + 'x) : Rp ' + (Number(b.total)||0).toLocaleString() + '\n';
});
text += '\n*TOTAL BELANJA: Rp ' + total.toLocaleString() + '*';
openWhatsAppToMany(getReportRecipients(), text);
}
function sendStokWA() {
var today = new Date().toISOString().split('T')[0];
var text = '*LAPORAN STOK ' + getStoreName().toUpperCase() + '*\nTanggal: ' + today + '\n\n';
menu.forEach(function(m) {
var terpakai = 0;
if (transaksi && transaksi.length) {
transaksi.forEach(function(t) {
if (t.status === 'Selesai' && t.timestamp && t.timestamp.indexOf(today) === 0) {
(t.items || []).forEach(function(it) {
if (it.nama === m.nama) terpakai += Number(it.qty);
});
}
});
}
text += '- *' + m.nama + '*\n Stok Awal: ' + (terpakai + Number(m.stok)) + '\n Terpakai: ' + terpakai + '\n Sisa: ' + m.stok + '\n\n';
});
openWhatsAppToMany(getReportRecipients(), text);
}
function sendHistoryWA(tgl) {
var items = transaksi.filter(function(t) {
var dt = t.timestamp ? t.timestamp.split(' ')[0] : (t.tgl ? t.tgl.split(' ')[0] : '');
return dt === tgl && t.status === 'Selesai';
});
if (!items.length) { alert('Tidak ada transaksi selesai pada tanggal ' + tgl); return; }
var total = items.reduce(function(a, c) { return a + (Number(c.total) || 0); }, 0);
var text = '*RIWAYAT TRANSAKSI ' + getStoreName().toUpperCase() + '*\nTanggal: ' + tgl + '\n\n';
items.forEach(function(t) {
text += '- ' + t.id.slice(-6) + ' | ' + t.meja + ' | ' + (t.nama || '-') + ' : Rp ' + (Number(t.total)||0).toLocaleString() + ' (' + (t.metodeBayar||'Tunai') + ')\n';
});
text += '\n*TOTAL OMZET: Rp ' + total.toLocaleString() + '*';
openWhatsAppToMany(getReportRecipients(), text);
}
function renderStok() {
var box = document.getElementById('stok-list');
if (!box) return;
// Simpan nilai input sementara sebelum render ulang
var tempValues = {};
menu.forEach(function(m) {
var inp = document.getElementById('stok-val-' + m.nama);
if (inp) tempValues[m.nama] = inp.value;
});
box.innerHTML = '';
var today = new Date().toISOString().split('T')[0];
if (!menu || !menu.length) {
box.innerHTML = '<div class="muted" style="text-align:center; grid-column: 1/-1;">Belum ada data menu.</div>';
return;
}
// Update Select Menu di Input Satuan
var select = document.getElementById('st-nama');
if (select && select.options.length <= 1) {
menu.forEach(function(m) {
var opt = document.createElement('option');
opt.value = m.nama;
opt.text = m.nama;
select.appendChild(opt);
});
}
var totalStokAwal = 0;
var totalTerpakai = 0;
var totalSisa = 0;
menu.forEach(function(m) {
var terpakai = 0;
if (transaksi && transaksi.length) {
transaksi.forEach(function(t) {
if (t.status === 'Selesai' && t.timestamp && t.timestamp.indexOf(today) === 0) {
if (lastReset && t.timestamp <= lastReset) return;
(t.items || []).forEach(function(it) {
if (it.nama === m.nama) terpakai += Number(it.qty);
});
}
});
}
var stokAwal = terpakai + Number(m.stok);
totalStokAwal += stokAwal;
totalTerpakai += terpakai;
totalSisa += Number(m.stok);
var div = document.createElement('div');
div.className = 'stok-card';
var stokAwalInput = tempValues[m.nama] || '';
var isMatch = stokAwalInput ? (Number(stokAwalInput) === stokAwal) : true;
var indicator = stokAwalInput ? (isMatch ? ' <span style="color:#16a34a;">(OK)</span>' : ' <span style="color:#e11d48;">(!!)</span>') : '';
div.innerHTML = '<div style="font-weight:900; margin-bottom:5px;">' + escapeHtml(m.nama) + indicator + '</div>' +
'<div style="display:flex; justify-content:space-between; margin-bottom:3px;">' +
'<span>Terpakai:</span><span class="stok-val">' + terpakai + '</span>' +
'</div>' +
'<div style="display:flex; justify-content:space-between; margin-bottom:5px;">' +
'<span>Sisa:</span><span class="stok-val" style="color:' + (m.stok > 5 ? '#16a34a' : '#e11d48') + '">' + m.stok + '</span>' +
'</div>' +
'<div style="border-top:1px dashed #ddd; padding-top:5px;">' +
'<div style="display:flex; justify-content:space-between; margin-bottom:2px;">' +
'<small>Stok Awal:</small>' +
'<small style="color:#2563eb; font-weight:bold;">' + stokAwal + '</small>' +
'</div>' +
'<div style="display:flex; gap:4px;">' +
'<input type="number" id="stok-val-' + escapeHtml(m.nama) + '" style="padding:4px; font-size:11px; width:100%;" placeholder="Cek stok..." value="' + stokAwalInput + '" oninput="renderStok()">' +
'<button class="btn-dark" style="padding:4px 8px; width:auto; font-size:10px;" onclick="updateStok(\'' + escapeHtml(m.nama) + '\',' + terpakai + ')">SET</button>' +
'</div>' +
'</div>';
box.appendChild(div);
});
// Render Ringkasan Stok
var summaryBox = document.getElementById('stok-summary');
if (summaryBox) {
summaryBox.innerHTML = '<div style="display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid #eee;"><span>Total Stok Awal:</span><b>' + totalStokAwal + '</b></div>' +
'<div style="display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid #eee;"><span>Total Terpakai:</span><b>' + totalTerpakai + '</b></div>' +
'<div style="display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid #eee;"><span>Total Sisa:</span><b>' + totalSisa + '</b></div>';
}
}
function renderTableStokAwal() {
var tbody = document.getElementById('stok-awal-table-body');
if (!tbody) return;
tbody.innerHTML = '';
var today = new Date().toISOString().split('T')[0];
var filtered = menu.filter(function(m) {
var name = m.nama || '';
var cat = m.kategori || '';
// Kecuali Paket*
if (name.toLowerCase().indexOf('paket') === 0) return false;
// Kecuali Nasi Putih, Kuah Tomyum
if (name === 'Nasi Putih' || name === 'Kuah Tomyum') return false;
// Kecuali Drinks
if (cat === 'Drinks') return false;
return true;
});
filtered.forEach(function(m) {
var terpakai = 0;
if (transaksi && transaksi.length) {
transaksi.forEach(function(t) {
if (t.status === 'Selesai' && t.timestamp && t.timestamp.indexOf(today) === 0) {
if (lastReset && t.timestamp <= lastReset) return;
(t.items || []).forEach(function(it) {
if (it.nama === m.nama) terpakai += Number(it.qty);
});
}
});
}
var stokAwal = terpakai + Number(m.stok);
var tr = document.createElement('tr');
tr.style.borderBottom = '1px solid #f1f5f9';
tr.innerHTML = '<td style="padding: 10px; font-weight: 700;">' + escapeHtml(m.nama) + '</td>' +
'<td style="padding: 10px;">' +
'<input type="number" class="stok-awal-input" data-nama="' + escapeHtml(m.nama) + '" data-terpakai="' + terpakai + '" value="' + stokAwal + '" style="padding: 8px; border-radius: 8px; border: 1px solid #ddd; width: 100%; text-align: center;">' +
'</td>';
tbody.appendChild(tr);
});
}
function simpanStokAwalBulk() {
var inputs = document.querySelectorAll('.stok-awal-input');
var payloads = [];
inputs.forEach(function(inp) {
var nama = inp.dataset.nama;
var terpakai = Number(inp.dataset.terpakai) || 0;
var stokAwal = Number(inp.value) || 0;
var sisa = Math.max(0, stokAwal - terpakai);
payloads.push({ nama: nama, sisa: sisa, terpakai: terpakai });
});
if (payloads.length === 0) return;
showLoading(true, 'MENYIMPAN STOK...');
safeRun('saveStokBulk', payloads, function(res) {
showLoading(false);
updateLocalData(res);
alert('Berhasil menyimpan seluruh stok awal.');
});
}
function updateStok(nama, terpakai) {
var val = document.getElementById('stok-val-' + nama).value;
if (val === '') return;
// Hitung sisa baru: Input (Stok Awal) - Terpakai
var newSisa = Number(val) - terpakai;
if (newSisa < 0) newSisa = 0;
showLoading(true, 'UPDATING...');
safeRun('saveStokBulk', [{ nama: nama, sisa: newSisa, terpakai: terpakai }], function(res) {
showLoading(false);
updateLocalData(res);
alert('Stok ' + nama + ' diperbarui.');
});
}
function resetSemuaStok() {
if (!confirm('RESET TERPAKAI akan menganggap semua penjualan sebelum ini sudah dihitung dan stok hari ini akan dimulai dari nol kembali (Buka Toko). Lanjutkan?')) return;
showLoading(true, 'Proses Reset Stok...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
alert('Berhasil Reset Stok Terpakai. Selamat berjualan!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal reset: ' + err.message);
}).resetAllStock();
}
function recalcBelanja(mode) {
var h = Number(document.getElementById('bl-harga').value) || 0;
var q = Number(document.getElementById('bl-qty').value) || 0;
var t = Number(document.getElementById('bl-total').value) || 0;
if (mode === 'harga' || mode === 'qty') {
document.getElementById('bl-total').value = h * q;
} else if (mode === 'total' && q > 0) {
document.getElementById('bl-harga').value = Math.floor(t / q);
}
}
function simpanBelanjaSatuan() {
var p = {
id: document.getElementById('bl-id').value || null,
nama: document.getElementById('bl-nama').value.trim(),
kategori: document.getElementById('bl-kat').value.trim(),
harga: Number(document.getElementById('bl-harga').value) || 0,
qty: Number(document.getElementById('bl-qty').value) || 0,
total: Number(document.getElementById('bl-total').value) || 0,
tgl: new Date().toISOString().split('T')[0]
};
if (!p.nama) { alert('Nama barang wajib'); return; }
showLoading(true, 'Menyimpan Belanja...');
safeRun('saveBelanjaBulk', [p], function(res) {
showLoading(false);
belanja = res.belanja;
document.getElementById('bl-nama').value = '';
document.getElementById('bl-harga').value = '';
document.getElementById('bl-qty').value = '1';
document.getElementById('bl-total').value = '';
document.getElementById('bl-id').value = ''; // Reset ID
renderBelanja();
alert('Belanja satuan tersimpan.');
});
}
function syncBelanjaOneWay() {
var today = new Date().toISOString().split('T')[0];
if (!confirm('Sinkron 1 arah belanja tanggal ' + today + ' ke Dashboard (RekapTransaksi) dan Stok&Belanja?')) return;
showLoading(true, 'SYNC BELANJA...');
safeRun('syncBelanjaOut', [today], function(res) {
showLoading(false);
updateLocalData(res);
var msg = 'SYNC selesai.\n\nTanggal: ' + (res && res.date ? res.date : today) +
'\nItem lokal: ' + (res && res.localCount != null ? res.localCount : '-') +
'\nRekapTransaksi: ' + (res && res.rekap && res.rekap.ok ? ('OK (insert ' + res.rekap.inserted + ', update ' + res.rekap.updated + ', skip ' + res.rekap.skipped + ')') : ('GAGAL: ' + ((res && res.rekap && res.rekap.error) ? res.rekap.error : '-'))) +
'\nStok&Belanja: ' + (res && res.stokBelanja && res.stokBelanja.ok ? ('OK (belanja ' + res.stokBelanja.belanja + ', operasional ' + res.stokBelanja.operasional + ', pegawai ' + res.stokBelanja.pegawai + ', skip ' + res.stokBelanja.skipped + ')') : ('GAGAL: ' + ((res && res.stokBelanja && res.stokBelanja.error) ? res.stokBelanja.error : '-')));
alert(msg);
}, function(err) {
showLoading(false);
alert('Gagal sync.\n' + (err && err.message ? err.message : err));
});
}
function simpanBelanjaBulk() {
var bulkText = document.getElementById('bl-bulk').value.trim();
if (!bulkText) return;
var lines = bulkText.split('\n');
var payloads = [];
var today = new Date().toISOString().split('T')[0];
var bulkKat = (document.getElementById('bl-bulk-kat') ? document.getElementById('bl-bulk-kat').value : '') || 'Dapur';
lines.forEach(function(line) {
var parts = line.trim().split(/\s+/);
if (parts.length < 3) return;
var last = parts.pop();
var mid = parts.pop();
var first = parts.pop();
var nama = parts.join(' ');
var qty, harga, total, kategori = bulkKat;
// CEK "Q" untuk QRIS/Transfer
if (nama.toUpperCase().indexOf('Q ') === 0 || nama.toUpperCase() === 'Q') {
kategori = 'QRIS';
if (nama.toUpperCase() === 'Q') {
nama = parts.join(' '); // Ambil nama asli jika Q terpisah
} else {
nama = nama.substring(2); // Buang "Q " di depan
}
} else if (nama.toUpperCase().endsWith(' Q')) {
kategori = 'QRIS';
nama = nama.substring(0, nama.length - 2); // Buang " Q" di belakang
}
if (mid.toLowerCase() === 'x') {
// Format: Nama Qty x Total (Es Batu 5 x 25000)
qty = Number(first) || 0;
total = Number(last) || 0;
harga = qty > 0 ? Math.floor(total / qty) : total;
} else if (!isNaN(Number(first)) && !isNaN(Number(mid)) && isNaN(Number(last))) {
// Seharusnya tidak mungkin jika last adalah angka
} else if (!isNaN(Number(first)) && !isNaN(Number(mid)) && !isNaN(Number(last))) {
// Format: Nama Qty Harga Total (Es Batu 5 5000 25000)
qty = Number(first) || 0;
harga = Number(mid) || 0;
total = Number(last) || 0;
} else {
// Format: Nama Qty Harga (Es Batu 5 5000) -> last adalah harga, mid adalah qty, first adalah bagian dari nama
// Kembalikan first ke nama
nama = (nama ? nama + ' ' : '') + first;
qty = Number(mid) || 0;
harga = Number(last) || 0;
total = qty * harga;
}
nama = autoTitleCase(nama);
payloads.push({
nama: nama,
qty: qty,
harga: harga,
total: total,
kategori: kategori,
tgl: today
});
});
if (!payloads.length) { alert('Format salah. Contoh: Beras 1 15000 15000'); return; }
showLoading(true, 'Menyimpan Belanja...');
safeRun('saveBelanjaBulk', payloads, function(res) {
showLoading(false);
belanja = res.belanja;
document.getElementById('bl-bulk').value = '';
renderBelanja();
alert('Berhasil menyimpan ' + payloads.length + ' barang belanja bulk.');
});
}
function editBelanja(id) {
var item = belanja.find(function(b) { return b.id === id; });
if (!item) return;
document.getElementById('bl-id').value = item.id;
document.getElementById('bl-nama').value = item.nama;
document.getElementById('bl-kat').value = mapBelanjaKategori(item.kategori || 'Dapur');
document.getElementById('bl-qty').value = item.qty;
document.getElementById('bl-harga').value = item.harga;
document.getElementById('bl-total').value = item.total;
document.getElementById('bl-nama').focus();
}
function mapBelanjaKategori(k) {
var s = String(k || '').trim();
var allowed = { Modal: true, Bawah: true, Dapur: true, Makan: true, Operasional: true, Pegawai: true, QRIS: true };
if (allowed[s]) return s;
var upper = s.toLowerCase();
if (upper === 'bahan baku') return 'Dapur';
if (upper === 'gaji') return 'Pegawai';
if (upper === 'listrik/air' || upper === 'listrik & air') return 'Operasional';
if (upper === 'sewa') return 'Operasional';
if (upper === 'lainnya' || upper === 'lain-lain') return 'Operasional';
return 'Dapur';
}
function autoTitleCase(s) {
var str = String(s || '').trim().replace(/\s+/g, ' ');
if (!str) return '';
return str.split(' ').map(function(w) {
if (!w) return w;
if (/[0-9]/.test(w)) return w;
if (/^[A-Z0-9\-\._]+$/.test(w) && w.length <= 4) return w;
var lower = w.toLowerCase();
return lower.charAt(0).toUpperCase() + lower.slice(1);
}).join(' ');
}
function hapusBelanja(id) {
if (!confirm('Anda yakin ingin menghapus item belanja ini?')) return;
showLoading(true, 'Menghapus...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
belanja = res.belanja;
renderBelanja();
alert('Item berhasil dihapus.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal hapus: ' + err.message);
}).deleteBelanja(id);
}
function renderBelanja() {
var boxToday = document.getElementById('bl-list-today');
if (!boxToday) return;
boxToday.innerHTML = '';
var today = new Date().toISOString().split('T')[0];
var grouped = {};
belanja.forEach(function(b) {
var tgl = b.tgl ? b.tgl.split(' ')[0] : 'Tanpa Tanggal';
if (!grouped[tgl]) grouped[tgl] = [];
grouped[tgl].push(b);
});
var sortedDates = Object.keys(grouped).sort().reverse();
sortedDates.forEach(function(tgl) {
var items = grouped[tgl].slice().reverse();
var totalBelanjaTgl = items.reduce(function(acc, b) { return acc + (Number(b.total) || 0); }, 0);
var dateHeader = document.createElement('div');
dateHeader.style.cssText = 'background:#f3f4f6; padding:6px 10px; font-weight:bold; font-size:12px; margin:10px 0 5px 0; border-radius:6px; display:flex; justify-content:space-between; align-items:center;';
dateHeader.innerHTML = '<div style="display:flex; align-items:center; gap:8px;">' +
'<span>' + tgl + (tgl === today ? ' (HARI INI)' : '') + '</span>' +
'<button class="pill on" style="width:auto; padding:2px 8px; font-size:9px;" onclick="sendBelanjaWA(\'' + tgl + '\')">KIRIM WA</button>' +
'</div>' +
'<span style="font-size:10px; color:#e11d48;">Total: Rp ' + totalBelanjaTgl.toLocaleString() + '</span>';
boxToday.appendChild(dateHeader);
items.forEach(function(b) {
var div = document.createElement('div');
div.className = 'item-order';
div.style.margin = '0 0 4px 0';
var katLabel = b.kategori ? ('<span style="font-size:8px; color:#e11d48; font-weight:bold;">[' + b.kategori + '] </span>') : '';
div.innerHTML = '<div>' +
'<div style="font-weight:900">' + katLabel + escapeHtml(b.nama) + '</div>' +
'<small>' + b.qty + ' x Rp ' + b.harga.toLocaleString() + '</small>' +
'</div>' +
'<div style="display:flex; align-items:center; gap:8px;">' +
'<div style="font-weight:900">Rp ' + b.total.toLocaleString() + '</div>' +
'<button class="btn-dark" style="padding: 2px 6px; font-size:9px;" onclick="editBelanja(\'' + b.id + '\')">E</button>' +
'<button class="btn-red" style="padding: 2px 6px; font-size:9px;" onclick="hapusBelanja(\'' + b.id + '\')">X</button>' +
'</div>';
boxToday.appendChild(div);
});
});
renderGlobalSearch();
}
var globalSearchTimer = null;
function renderGlobalSearch() {
var qText = (document.getElementById('bl-search-text').value || '').trim().toLowerCase();
renderBelanjaSearchLocal(qText);
scheduleTransaksiSearch(qText);
}
function renderBelanjaSearchLocal(qText) {
var boxSearch = document.getElementById('bl-list-search');
if (!boxSearch) return;
boxSearch.innerHTML = '';
if (!qText) {
boxSearch.innerHTML = '<div class="muted" style="text-align:center; padding:10px;">Ketik untuk mencari belanja / transaksi...</div>';
return;
}
var filtered = belanja.filter(function(b) {
return b.nama && b.nama.toLowerCase().indexOf(qText) !== -1;
});
boxSearch.innerHTML = '<div style="font-weight:900; font-size:12px; margin-bottom:6px;">BELANJA</div>';
if (!filtered.length) {
boxSearch.innerHTML += '<div class="muted" style="text-align:center; padding:10px;">Tidak ditemukan.</div>';
return;
}
var items = filtered.slice().reverse().slice(0, 50);
items.forEach(function(b) {
var div = document.createElement('div');
div.className = 'item-order';
div.style.marginBottom = '4px';
div.innerHTML = '<div>' +
'<div style="font-weight:900">' + escapeHtml(b.nama) + '</div>' +
'<small>' + (b.tgl ? b.tgl.split(' ')[0] : '') + ' | ' + (b.kategori || '-') + ' | ' + b.qty + ' x Rp ' + (Number(b.harga) || 0).toLocaleString() + '</small>' +
'</div>' +
'<div style="font-weight:900">Rp ' + (Number(b.total) || 0).toLocaleString() + '</div>';
boxSearch.appendChild(div);
});
}
function scheduleTransaksiSearch(qText) {
var boxTx = document.getElementById('tx-list-search');
if (!boxTx) return;
if (globalSearchTimer) clearTimeout(globalSearchTimer);
boxTx.innerHTML = '';
if (!qText) return;
if (qText.length < 2) {
boxTx.innerHTML = '<div style="font-weight:900; font-size:12px; margin-bottom:6px;">TRANSAKSI</div><div class="muted" style="text-align:center; padding:10px;">Minimal 2 huruf.</div>';
return;
}
if (!isOnline) {
boxTx.innerHTML = '<div style="font-weight:900; font-size:12px; margin-bottom:6px;">TRANSAKSI</div><div class="muted" style="text-align:center; padding:10px;">Mode offline: pencarian transaksi tidak tersedia.</div>';
return;
}
boxTx.innerHTML = '<div style="font-weight:900; font-size:12px; margin-bottom:6px;">TRANSAKSI</div><div class="muted" style="text-align:center; padding:10px;">Mencari...</div>';
globalSearchTimer = setTimeout(function() {
google.script.run.withSuccessHandler(function(res) {
renderTransaksiSearchResults(qText, res);
}).withFailureHandler(function(err) {
boxTx.innerHTML = '<div style="font-weight:900; font-size:12px; margin-bottom:6px;">TRANSAKSI</div><div class="muted" style="text-align:center; padding:10px;">Gagal mencari transaksi.</div>';
}).searchTransactionsGlobal(qText, 50);
}, 300);
}
function renderTransaksiSearchResults(qText, res) {
var boxTx = document.getElementById('tx-list-search');
if (!boxTx) return;
boxTx.innerHTML = '<div style="font-weight:900; font-size:12px; margin-bottom:6px;">TRANSAKSI</div>';
var items = (res && res.items) ? res.items : [];
if (!items.length) {
boxTx.innerHTML += '<div class="muted" style="text-align:center; padding:10px;">Tidak ditemukan.</div>';
return;
}
items.forEach(function(t) {
var div = document.createElement('div');
div.className = 'item-order';
div.style.marginBottom = '4px';
var tgl = t.tanggal || '';
var label = (t.meja ? ('Meja ' + t.meja) : (t.nama || '')) + (t.metodeBayar ? (' | ' + t.metodeBayar) : '');
var small = tgl + (t.status ? (' | ' + t.status) : '') + (t.itemsText ? (' | ' + t.itemsText) : '');
div.innerHTML = '<div>' +
'<div style="font-weight:900">' + escapeHtml(label) + '</div>' +
'<small>' + escapeHtml(small) + '</small>' +
'</div>' +
'<div style="font-weight:900">Rp ' + (Number(t.total) || 0).toLocaleString() + '</div>';
boxTx.appendChild(div);
});
}
function renderRekap() {
var today = new Date().toISOString().split('T')[0];
var boxMethods = document.getElementById('rk-methods');
if (!boxMethods) return;
var boxPortions = document.getElementById('rk-portions');
var foundRekap = null;
if (rekap && rekap.length) {
var revRekap = rekap.slice().reverse();
for (var i = 0; i < revRekap.length; i++) {
if (revRekap[i].tgl && revRekap[i].tgl.indexOf(today) === 0) {
foundRekap = revRekap[i];
break;
}
}
}
var saldoAwal = foundRekap ? Number(foundRekap.saldoAwal) || 0 : 0;
if (foundRekap) {
document.getElementById('rk-saldo-awal').value = saldoAwal;
document.getElementById('rk-catatan').value = foundRekap.catatan || '';
}
var methods = { 'Tunai': 0, 'QRIS': 0, 'Debit': 0, 'Credit': 0, 'Transfer': 0 };
var dpMethods = { 'Tunai': 0, 'QRIS': 0 };
var portions = {};
var totalNota = 0;
var upcomingBookings = [];
// Hitung Total Belanja Hari Ini (HANYA TUNAI)
var totalBelanjaHariIni = 0;
if (belanja && belanja.length) {
belanja.forEach(function(b) {
if (b.tgl && b.tgl.indexOf(today) === 0) {
if (b.kategori !== 'QRIS') { // Jika kategori bukan QRIS, anggap tunai
totalBelanjaHariIni += (Number(b.total) || 0);
}
}
});
}
if (transaksi && transaksi.length) {
transaksi.forEach(function(t) {
var isToday = t.timestamp && t.timestamp.indexOf(today) === 0;
if (t.status === 'Selesai' && isToday) {
totalNota++;
if (t.metodeBayar === 'Multi' && t.catatan) {
var details = {}; try { details = JSON.parse(t.catatan); } catch(e) {}
for (var m in details) {
if (methods.hasOwnProperty(m)) methods[m] += (Number(details[m]) || 0);
}
} else {
var m = t.metodeBayar || 'Tunai';
if (methods.hasOwnProperty(m)) methods[m] += (Number(t.total) || 0);
}
if (t.items && t.items.length) {
t.items.forEach(function(it) {
portions[it.nama] = (portions[it.nama] || 0) + Number(it.qty);
});
}
}
if (t.status === 'Booking') {
var dpVal = Number(t.dp) || 0;
if (isToday) {
var dm = t.metodeDp || 'Tunai';
if (dpMethods.hasOwnProperty(dm)) dpMethods[dm] += dpVal;
}
if (t.tgl >= today) upcomingBookings.push(t);
}
});
}
boxMethods.innerHTML = '<div class="sum-row" style="border-bottom:1px solid #eee; padding-bottom:4px; margin-bottom:8px;">' +
'<span style="font-weight:900;">TOTAL NOTA</span><span style="font-weight:900;">' + totalNota + '</span>' +
'</div>';
var order = ['Tunai', 'QRIS', 'Debit', 'Credit', 'Transfer'];
var totalSemuaMetode = 0;
order.forEach(function(m) {
var val = methods[m];
totalSemuaMetode += val;
var d = document.createElement('div');
d.className = 'sum-row';
d.innerHTML = '<span>' + m + '</span><span>Rp ' + val.toLocaleString() + '</span>';
boxMethods.appendChild(d);
});
// Baris Total Semua Metode
var divTotalMetode = document.createElement('div');
divTotalMetode.style.cssText = 'margin-top:4px; border-top:1px solid #eee; padding-top:4px; font-weight:900;';
divTotalMetode.innerHTML = '<div class="sum-row"><span>TOTAL PENJUALAN</span><span>Rp ' + totalSemuaMetode.toLocaleString() + '</span></div>';
boxMethods.appendChild(divTotalMetode);
var divDp = document.createElement('div');
divDp.style.cssText = 'margin-top:10px; border-top:1px dashed #ddd; padding-top:8px;';
divDp.innerHTML = '<div class="sum-row"><small>Total DP Tunai (Hari Ini)</small><small>Rp ' + dpMethods.Tunai.toLocaleString() + '</small></div>' +
'<div class="sum-row" style="color:#e11d48;"><small>Total Belanja (Tunai)</small><small>- Rp ' + totalBelanjaHariIni.toLocaleString() + '</small></div>' +
'<div class="sum-row" style="font-weight:900; color:#16a34a;"><small>Sisa Uang Belanja</small><small>Rp ' + (saldoAwal - totalBelanjaHariIni).toLocaleString() + '</small></div>' +
'<div class="sum-row"><small>Total DP QRIS (Hari Ini)</small><small>Rp ' + dpMethods.QRIS.toLocaleString() + '</small></div>' +
'<div class="sum-strong" style="margin-top:4px; color:#2563eb;">' +
'<span>SALDO TUNAI SAAT INI</span>' +
'<span>Rp ' + (saldoAwal + methods.Tunai + dpMethods.Tunai - totalBelanjaHariIni).toLocaleString() + '</span>' +
'</div>';
boxMethods.appendChild(divDp);
var potensi = calcPotensiProfitFromPortions(portions);
var ppEl = document.getElementById('rk-potensi-profit');
if (ppEl) ppEl.innerText = 'Potensi Profit (Estimasi): Rp ' + (Number(potensi) || 0).toLocaleString('id-ID');
boxPortions.innerHTML = '';
renderGroupedPortionsToBox(boxPortions, portions, 'porsi');
var divUpcoming = document.createElement('div');
divUpcoming.style.cssText = 'margin-top:10px; border-top:1px solid #eee; padding-top:10px;';
divUpcoming.innerHTML = '<div class="title-sm" style="font-size:11px;">BOOKING MENDATANG</div>';
if (!upcomingBookings.length) {
divUpcoming.innerHTML += '<div class="muted" style="text-align:center;">Tidak ada booking.</div>';
} else {
var sortedBookings = upcomingBookings.sort(function(a,b) { return a.tgl.localeCompare(b.tgl); }).slice(0, 5);
sortedBookings.forEach(function(b) {
divUpcoming.innerHTML += '<div style="font-size:10px; margin-bottom:4px; border-bottom:1px solid #f9f9f9;">' +
'<b>' + b.tgl.split(' ')[0] + ' ' + (b.jam || '') + '</b> - ' + escapeHtml(b.nama || '-') + ' (' + b.wa + ')<br>' +
'<span style="color:#16a34a">DP: Rp ' + (Number(b.dp)||0).toLocaleString() + ' (' + b.metodeDp + ')</span>' +
'</div>';
});
}
boxPortions.appendChild(divUpcoming);
}
function normalizePortionName(name) {
var s = String(name || '').trim();
var isArrow = /^->\s*/.test(s);
var base = s.replace(/^->\s*/, '').trim();
return { raw: s, base: base, isArrow: isArrow };
}
function calcPotensiProfitFromPortions(portionsMap) {
var menuMap = {};
try {
(menu || []).forEach(function(m) {
if (!m || !m.nama) return;
menuMap[String(m.nama).trim().toLowerCase()] = { harga: Number(m.harga) || 0, modal: Number(m.modal) || 0 };
});
} catch (e) {}
var paketMap = {};
try {
(paketKustom || []).forEach(function(p) {
if (!p || !p.nama) return;
paketMap[String(p.nama).trim().toLowerCase()] = { harga: Number(p.total) || 0, modal: Number(p.modal) || 0 };
});
} catch (e2) {}
var totalProfit = 0;
Object.keys(portionsMap || {}).forEach(function(k) {
var qty = Number(portionsMap[k]) || 0;
if (!qty) return;
var base = normalizePortionName(k).base;
var key = String(base || '').trim().toLowerCase();
var ref = menuMap[key] || paketMap[key];
if (!ref) return;
var margin = (Number(ref.harga) || 0) - (Number(ref.modal) || 0);
totalProfit += margin * qty;
});
return Math.round(totalProfit);
}
function groupPortionsFromMap(portionsMap) {
var grouped = {};
Object.keys(portionsMap || {}).forEach(function(k) {
var qty = Number(portionsMap[k]) || 0;
if (!qty) return;
var info = normalizePortionName(k);
var base = info.base || info.raw;
if (!grouped[base]) grouped[base] = { base: base, total: 0, arrow: 0, normal: 0 };
grouped[base].total += qty;
if (info.isArrow) grouped[base].arrow += qty;
else grouped[base].normal += qty;
});
return Object.keys(grouped).map(function(k) { return grouped[k]; })
.sort(function(a, b) { return (b.total || 0) - (a.total || 0); });
}
function groupPortionsFromPairs(pairs) {
var map = {};
(pairs || []).forEach(function(it) {
if (!it) return;
var name = it[0];
var qty = Number(it[1]) || 0;
if (!name || !qty) return;
map[String(name)] = (map[String(name)] || 0) + qty;
});
return groupPortionsFromMap(map);
}
function renderGroupedPortionsToBox(box, portionsMap, unit) {
if (!box) return;
var groups = groupPortionsFromMap(portionsMap);
if (!groups.length) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:10px;">Belum ada porsi terjual.</div>';
return;
}
groups.forEach(function(g) {
var main = document.createElement('div');
main.className = 'sum-row';
main.style.fontWeight = '900';
main.innerHTML = '<span>' + escapeHtml(g.base) + '</span><span>' + (Number(g.total) || 0) + ' ' + (unit || '') + '</span>';
box.appendChild(main);
if ((g.arrow || 0) > 0 && (g.normal || 0) > 0) {
var sub1 = document.createElement('div');
sub1.className = 'sum-row';
sub1.style.paddingLeft = '12px';
sub1.style.color = '#64748b';
sub1.innerHTML = '<span>-> ' + escapeHtml(g.base) + '</span><span>' + (Number(g.arrow) || 0) + '</span>';
box.appendChild(sub1);
var sub2 = document.createElement('div');
sub2.className = 'sum-row';
sub2.style.paddingLeft = '12px';
sub2.style.color = '#64748b';
sub2.innerHTML = '<span>' + escapeHtml(g.base) + '</span><span>' + (Number(g.normal) || 0) + '</span>';
box.appendChild(sub2);
}
});
}
function appendGroupedPortionsToText(text, groups) {
var out = String(text || '');
(groups || []).forEach(function(g) {
out += '- ' + g.base + ': ' + (Number(g.total) || 0) + ' porsi\n';
if ((g.arrow || 0) > 0 && (g.normal || 0) > 0) {
out += ' -> ' + g.base + ': ' + (Number(g.arrow) || 0) + '\n';
out += ' ' + g.base + ': ' + (Number(g.normal) || 0) + '\n';
}
});
return out;
}
function renderDapur() {
var box = document.getElementById('dapur-list');
if (!box) return;
box.innerHTML = '';
var today = new Date().toISOString().split('T')[0];
// Logika pencarian pesanan aktif (status Pending)
var list = transaksi.filter(function(t) {
// Normalisasi status (mungkin case sensitive)
var status = String(t.status || '').trim();
var isPending = (status === 'Pending' || status === 'PENDING');
// Normalisasi tanggal (untuk filter hari ini saja)
var tgl = "";
if (t.timestamp) {
tgl = t.timestamp.split(' ')[0];
} else if (t.tgl) {
tgl = (t.tgl instanceof Date) ? t.tgl.toISOString().split('T')[0] : String(t.tgl).split(' ')[0];
}
var isToday = (tgl === today);
return isPending && isToday;
});
if (!list.length) {
box.innerHTML = '<div class="muted" style="text-align:center; grid-column: 1/-1; padding:20px;">Tidak ada pesanan aktif di dapur.</div>';
return;
}
// Pisahkan item prioritas (Drinks, Nasi & Kuah Tomyum di Ala Carte)
var renderCard = function(t, isPriorityOnly) {
var filteredItems = (t.items || []).filter(function(it) {
var n = it.nama.toLowerCase();
// Fallback: Jika kat tidak ada di item, cari di menu global
var k = it.kat || (menu.find(function(m) { return m.nama === it.nama; }) || {}).kategori || '';
var isPrio = (k === 'Drinks') ||
(k === 'Ala Carte' && (n.includes('nasi') || n.includes('kuah tomyum')));
return isPriorityOnly ? isPrio : !isPrio;
});
if (filteredItems.length === 0) return null;
var div = document.createElement('div');
div.className = 'trans-card';
div.style.borderLeft = isPriorityOnly ? '5px solid #16a34a' : '5px solid #e11d48';
var itemsHtml = '';
filteredItems.forEach(function(it) {
itemsHtml += '<div style="display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid #f9f9f9;">' +
'<div style="font-weight:900; font-size:14px;">' + (isPriorityOnly ? '🥤 ' : '🍲 ') + it.nama + '</div>' +
'<div style="font-weight:900; font-size:16px; color:' + (isPriorityOnly ? '#16a34a' : '#e11d48') + ';">x' + it.qty + '</div>' +
'</div>';
});
div.innerHTML = '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">' +
'<div style="font-weight:900; font-size:16px;">' + t.meja + (isPriorityOnly ? ' (PRIORITAS)' : '') + '</div>' +
'<div class="muted">' + (t.timestamp ? t.timestamp.split(' ')[1] : '') + '</div>' +
'</div>' +
'<div style="margin-bottom:10px;">' + itemsHtml + '</div>' +
'<button class="btn-green" style="font-size:12px; padding:8px; background:' + (isPriorityOnly ? '#16a34a' : '#111') + '" onclick="tandaiSelesaiMasak(\'' + t.id + '\')">SELESAI</button>';
return div;
};
// Render Prioritas Dulu
var prioTitle = document.createElement('div');
prioTitle.className = 'title-sm';
prioTitle.style.gridColumn = '1/-1';
prioTitle.style.color = '#16a34a';
prioTitle.innerText = '⚡ PRIORITAS: DRINKS, NASI & TOMYUM';
box.appendChild(prioTitle);
list.forEach(function(t) {
var card = renderCard(t, true);
if (card) box.appendChild(card);
});
// Render Sisanya
var otherTitle = document.createElement('div');
otherTitle.className = 'title-sm';
otherTitle.style.gridColumn = '1/-1';
otherTitle.style.marginTop = '20px';
otherTitle.innerText = '🥘 MENU UTAMA / GRILL';
box.appendChild(otherTitle);
list.forEach(function(t) {
var card = renderCard(t, false);
if (card) box.appendChild(card);
});
}
function tandaiSelesaiMasak(id) {
if (!confirm('Tandai pesanan ini sudah selesai dimasak?')) return;
showLoading(true, 'MEMPROSES...');
safeRun('updateKitchenStatus', [id, 'Ready'], function(res) {
showLoading(false);
updateLocalData(res);
alert('Pesanan ditandai selesai dimasak.');
});
}
function voidTransaction(id) {
var modalHtml = '<div class="card">' +
'<div class="title-sm">Alasan Pembatalan (VOID)</div>' +
'<textarea id="void-reason" style="width:100%; min-height:80px; padding:10px; border-radius:8px; border:1px solid #cbd5e1; margin-bottom:15px;" placeholder="Tulis alasan void di sini..."></textarea>' +
'<div style="margin-top:5px; padding:12px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0; margin-bottom:15px;">' +
'<div style="font-weight:800; font-size:11px; margin-bottom:10px; display:flex; justify-content:space-between; align-items:center;">' +
'<span>UPLOAD FOTO BUKTI VOID</span>' +
'<span id="foto-status" style="font-size:10px; color:#64748b;">Belum ada foto</span>' +
'</div>' +
'<div style="display:flex; gap:10px; align-items:center;">' +
'<label class="btn-dark" style="width:auto; flex:1; padding:10px; font-size:11px; cursor:pointer; margin:0;">' +
'📸 AMBIL FOTO' +
'<input type="file" id="input-foto-void" accept="image/*" capture="camera" style="display:none;" onchange="handleFotoUpload(event)">' +
'</label>' +
'<div id="foto-preview-container" style="width:50px; height:50px; border-radius:8px; background:#eee; overflow:hidden; display:none;">' +
'<img id="foto-preview" style="width:100%; height:100%; object-fit:cover;">' +
'</div>' +
'</div>' +
'</div>' +
'<div style="display:flex; gap:10px;">' +
'<button class="btn-void" style="flex:1; position:static;" onclick="closeModal()">BATAL</button>' +
'<button class="btn-green" style="flex:2;" onclick="prosesVoidFinal(\'' + id + '\')">KONFIRMASI VOID</button>' +
'</div>' +
'</div>';
window.__buktiBayarBase64 = null; // Reset foto
openModalCustom(modalHtml, 'VOID TRANSAKSI');
}
function prosesVoidFinal(id) {
var reason = document.getElementById('void-reason').value.trim();
if (!reason) { alert('Alasan void wajib diisi!'); return; }
if (!confirm('Apakah Anda yakin ingin membatalkan transaksi ini?')) return;
var executeVoid = function(fotoUrl) {
showLoading(true, 'PROSES VOID...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.error) { alert(res.error); return; }
alert('Transaksi berhasil dibatalkan (VOID).');
updateLocalData(res);
closeModal();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal proses void: ' + err.message);
}).voidTransaction(id, reason, fotoUrl);
};
if (window.__buktiBayarBase64) {
showLoading(true, 'MENGUNGGAH FOTO VOID...');
google.script.run.withSuccessHandler(function(uploadRes) {
if (uploadRes.error) {
if (!confirm('Gagal upload foto: ' + uploadRes.error + '. Lanjutkan void tanpa foto?')) {
showLoading(false);
return;
}
executeVoid('');
} else {
executeVoid(uploadRes.url);
}
}).uploadPaymentProof({ id: id + '-void', base64: window.__buktiBayarBase64 });
} else {
executeVoid('');
}
}
function sendRekapWA() {
var today = new Date().toISOString().split('T')[0];
var saldoAwal = Number(document.getElementById('rk-saldo-awal').value) || 0;
var catatan = document.getElementById('rk-catatan').value.trim();
var methods = { 'Tunai': 0, 'QRIS': 0, 'Debit': 0, 'Credit': 0, 'Transfer': 0 };
var dpMethods = { 'Tunai': 0, 'QRIS': 0 };
var portions = {};
var totalNota = 0;
// Hitung Belanja
var totalBelanjaTunai = 0;
var totalBelanjaNonTunai = 0;
if (belanja && belanja.length) {
belanja.forEach(function(b) {
if (b.tgl && b.tgl.indexOf(today) === 0) {
var val = (Number(b.total) || 0);
if (b.kategori === 'QRIS') {
totalBelanjaNonTunai += val;
} else {
totalBelanjaTunai += val;
}
}
});
}
// Hitung Transaksi
if (transaksi && transaksi.length) {
transaksi.forEach(function(t) {
var isToday = t.timestamp && t.timestamp.indexOf(today) === 0;
if (t.status === 'Selesai' && isToday) {
totalNota++;
if (t.metodeBayar === 'Multi' && t.catatan) {
var details = {}; try { details = JSON.parse(t.catatan); } catch(e) {}
for (var m in details) {
if (methods.hasOwnProperty(m)) methods[m] += (Number(details[m]) || 0);
}
} else {
var m = t.metodeBayar || 'Tunai';
if (methods.hasOwnProperty(m)) methods[m] += (Number(t.total) || 0);
}
if (t.items && t.items.length) {
t.items.forEach(function(it) {
portions[it.nama] = (portions[it.nama] || 0) + Number(it.qty);
});
}
}
if (t.status === 'Booking' && isToday) {
var dm = t.metodeDp || 'Tunai';
if (dpMethods.hasOwnProperty(dm)) dpMethods[dm] += (Number(t.dp) || 0);
}
});
}
var totalPenjualan = 0;
for (var m in methods) totalPenjualan += methods[m];
var saldoTunaiAkhir = saldoAwal + methods.Tunai + dpMethods.Tunai - totalBelanjaTunai;
var footerText = buildStoreFooterText();
var text = '*REKAP KASIR ' + getStoreName().toUpperCase() + '*\n' +
'Tanggal: ' + today + '\n\n' +
'*SALDO AWAL:* Rp ' + saldoAwal.toLocaleString() + '\n' +
'*TOTAL NOTA:* ' + totalNota + '\n\n' +
'*PENJUALAN PER METODE:*\n';
for (var m in methods) {
if (methods[m] > 0) text += '- ' + m + ': Rp ' + methods[m].toLocaleString() + '\n';
}
text += '*TOTAL PENJUALAN: Rp ' + totalPenjualan.toLocaleString() + '*\n\n';
text += '*PENGELUARAN & DP:*\n' +
'- DP Tunai: Rp ' + dpMethods.Tunai.toLocaleString() + '\n' +
'- DP QRIS: Rp ' + dpMethods.QRIS.toLocaleString() + '\n' +
'- Belanja Tunai: Rp ' + totalBelanjaTunai.toLocaleString() + '\n' +
'- Belanja QRIS/Tf: Rp ' + totalBelanjaNonTunai.toLocaleString() + '\n' +
'- Sisa Uang Belanja: Rp ' + (saldoAwal - totalBelanjaTunai).toLocaleString() + '\n\n' +
'*SALDO TUNAI SAAT INI (LACI):*\n' +
'*Rp ' + saldoTunaiAkhir.toLocaleString() + '*\n\n';
if (catatan) text += '*CATATAN:* ' + catatan + '\n\n';
text += '*PORSI TERJUAL:*\n';
var groups3 = groupPortionsFromMap(portions);
text = appendGroupedPortionsToText(text, groups3);
text += footerText;
openWhatsAppToMany(getReportRecipients(), text);
}
function simpanSaldoAwal() {
var saldoEl = document.getElementById('rk-saldo-awal');
var saldo = Number(saldoEl ? saldoEl.value : 0) || 0;
var catatanEl = document.getElementById('rk-catatan');
var catatan = catatanEl ? catatanEl.value.trim() : '';
var today = new Date().toISOString().split('T')[0];
var methods = { 'Tunai': 0, 'QRIS': 0, 'Debit': 0, 'Credit': 0, 'Transfer': 0 };
if (transaksi && transaksi.length) {
transaksi.forEach(function(t) {
if (t.status === 'Selesai' && t.timestamp && t.timestamp.indexOf(today) === 0) {
if (t.metodeBayar === 'Multi' && t.catatan) {
var details = {}; try { details = JSON.parse(t.catatan); } catch(e) {}
for (var m in details) {
if (methods.hasOwnProperty(m)) methods[m] += (Number(details[m]) || 0);
}
} else {
var m = t.metodeBayar || 'Tunai';
if (methods.hasOwnProperty(m)) methods[m] += (Number(t.total) || 0);
}
}
});
}
var foundRekap = null;
if (rekap && rekap.length) {
var revRekap = rekap.slice().reverse();
for (var i = 0; i < revRekap.length; i++) {
if (revRekap[i].tgl && revRekap[i].tgl.indexOf(today) === 0) {
foundRekap = revRekap[i];
break;
}
}
}
var p = {
id: foundRekap ? foundRekap.id : null,
tgl: today,
saldoAwal: saldo,
tunai: methods.Tunai,
qris: methods.QRIS,
debit: methods.Debit,
credit: methods.Credit,
transfer: methods.Transfer,
catatan: catatan
};
showLoading(true);
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.rekap) rekap = res.rekap;
alert('Data rekap tersimpan.');
renderRekap();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan: ' + err.message);
}).saveRekap(p);
}
function exportRiwayatCSV() {
if (!transaksi || !transaksi.length) { alert('Tidak ada data untuk diexport.'); return; }
var headers = [
'ID', 'Meja', 'Status', 'Nama', 'WA', 'Tanggal', 'Jam Booking', 'Items (JSON)',
'Subtotal', 'Diskon', 'Poin Dipakai (Rp)', 'Pajak %', 'Service %', 'Admin %',
'Pajak (Rp)', 'Service (Rp)', 'Admin (Rp)', 'Total Akhir', 'DP', 'Metode DP',
'Tgl DP', 'Metode Bayar', 'Jumlah Bayar', 'Kembali', 'Poin Didapat', 'Timestamp',
'Poin Awal', 'Catatan'
];
var csv = headers.join(',') + '\n';
// Urutkan transaksi terbaru di atas
var sorted = transaksi.slice().sort(function(a, b) {
var t1 = a.timestamp || a.tgl || '';
var t2 = b.timestamp || b.tgl || '';
return t2.localeCompare(t1);
});
sorted.forEach(function(t) {
var itemsJson = JSON.stringify(t.items || []).replace(/"/g, '""');
var row = [
'"' + (t.id || '') + '"',
'"' + (t.meja || '') + '"',
'"' + (t.status || '') + '"',
'"' + (t.nama || '-') + '"',
'"' + (t.wa || '-') + '"',
'"' + (t.tgl || '') + '"',
'"' + (t.jam || '') + '"',
'"' + itemsJson + '"',
(t.subtotal || 0),
(t.diskon || 0),
(t.poinDipakai || 0),
(t.pajakPersen || 0),
(t.servicePersen || 0),
(t.adminPersen || 0),
(t.pajak || 0),
(t.service || 0),
(t.adminFee || 0),
(t.total || 0),
(t.dp || 0),
'"' + (t.metodeDp || '') + '"',
'"' + (t.tglDp || '') + '"',
'"' + (t.metodeBayar || 'Tunai') + '"',
(t.bayar || 0),
(t.kembali || 0),
(t.poinDapat || 0),
'"' + (t.timestamp || '') + '"',
(t.poinAwal || 0),
'"' + (t.catatan || '') + '"'
];
csv += row.join(',') + '\n';
});
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var link = document.createElement('a');
var url = URL.createObjectURL(blob);
link.setAttribute('href', url);
var dateStr = new Date().toISOString().split('T')[0];
link.setAttribute('download', 'Riwayat_Transaksi_Fuku_Lengkap_' + dateStr + '.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function importRiwayatCSV() {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.csv';
input.onchange = function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(re) {
var content = re.target.result;
var lines = content.split('\n');
if (lines.length < 2) return;
var importedData = [];
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var parts = parseCsvLine(line);
if (parts.length >= 28) {
importedData.push(parts);
}
}
if (importedData.length > 0) {
if (confirm('Import ' + importedData.length + ' transaksi lengkap? Ini akan menggabungkan dengan data yang ada.')) {
showLoading(true, 'IMPORT DATA...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
alert('Berhasil import ' + importedData.length + ' transaksi.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal import: ' + err.message);
}).importTransactions(importedData);
}
}
};
reader.readAsText(file);
};
input.click();
}
function clearRiwayatKeepLast() {
if (!isAdmin && !isKitchenMode) {
requestAdminAccess(function() { clearRiwayatKeepLast(); });
return;
}
var ok = confirm('Hapus seluruh RIWAYAT TRANSAKSI dan sisakan 1 transaksi terakhir?\n\nAksi ini tidak bisa dibatalkan.');
if (!ok) return;
var kode = prompt('Ketik HAPUS untuk konfirmasi:');
if (kode === null) return;
if (String(kode).trim().toUpperCase() !== 'HAPUS') {
alert('Konfirmasi dibatalkan.');
return;
}
showLoading(true, 'MENGHAPUS RIWAYAT...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
renderHistory();
alert('Riwayat sudah dibersihkan (menyisakan 1 transaksi terakhir).');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal hapus riwayat: ' + (err && err.message ? err.message : err));
}).clearTransactionHistoryKeepLast();
}
function parseCsvLine(line) {
var parts = [];
var current = '';
var inQuotes = false;
for (var j = 0; j < line.length; j++) {
var char = line[j];
if (char === '"') {
if (inQuotes && line[j+1] === '"') {
current += '"';
j++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
parts.push(current.trim());
current = '';
} else {
current += char;
}
}
parts.push(current.trim());
return parts.map(function(p) { return p.replace(/^"|"$/g, ''); });
}
function cleanCSVNumber(val) {
if (!val) return 0;
// Hapus koma pemisah ribuan dan ambil angka saja
var s = String(val).replace(/,/g, '').trim();
return Number(s) || 0;
}
function normalizePorsiName(name) {
var raw = String(name || '').replace(/\(Paket\)/i, '').replace(/^->\s*/, '').trim();
var key = raw.toLowerCase().replace(/\s+/g, ' ');
var map = {
'beef slice': 'Beef Plate',
'saikoro': 'Saikoro Plate',
'enoki beef roll': 'Enoki Beef Plate',
'ayam bbq': 'Ayam Plate',
'jamur enoki': 'Enoki Plate',
'tahu': 'Tahu Plate',
'mie': 'Mie Plate',
'sate frozen food': 'Sate',
'air mineral': 'Air Mineral Botol',
'teh pucuk': 'Teh Pucuk Botol'
};
return map[key] || raw;
}
function exportPorsiTerjualCSV() {
var today = new Date().toISOString().split('T')[0];
var portions = {};
if (transaksi && transaksi.length) {
transaksi.forEach(function(t) {
var isToday = t.timestamp && t.timestamp.indexOf(today) === 0;
if (t.status === 'Selesai' && isToday && t.items && t.items.length) {
t.items.forEach(function(it) {
var n = normalizePorsiName(it.nama);
portions[n] = (portions[n] || 0) + (Number(it.qty) || 0);
});
}
});
}
var names = Object.keys(portions).filter(function(n) { return portions[n] > 0; });
if (!names.length) { alert('Belum ada porsi terjual (Selesai) untuk hari ini.'); return; }
names.sort(function(a, b) { return portions[b] - portions[a]; });
var csv = 'Tanggal,Item,Qty\n';
names.forEach(function(n) {
csv += '"' + today + '","' + String(n).replace(/"/g, '""') + '",' + portions[n] + '\n';
});
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var link = document.createElement('a');
link.setAttribute('href', URL.createObjectURL(blob));
var safeStore = String(getStoreName() || 'POS').replace(/[^a-zA-Z0-9]+/g, '_');
link.setAttribute('download', 'Porsi_Terjual_' + safeStore + '_' + today + '.csv');
link.click();
}
function exportBelanjaCSV() {
if (!belanja || !belanja.length) { alert('Tidak ada data belanja.'); return; }
// Header sesuai struktur sheet user
var csv = 'ID,Nama,Harga,Qty,Total,Tanggal,Kategori,Catatan,Timestamp\n';
belanja.forEach(function(b) {
var row = [
'"' + (b.id || '') + '"',
'"' + (b.nama || '') + '"',
(b.harga || 0),
(b.qty || 0),
(b.total || 0),
'"' + (b.tgl || '') + '"',
'"' + (b.kategori || '') + '"',
'"' + (b.catatan || '') + '"',
'"' + (b.timestamp || '') + '"'
];
csv += row.join(',') + '\n';
});
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var link = document.createElement('a');
link.setAttribute('href', URL.createObjectURL(blob));
link.setAttribute('download', 'Belanja_Fuku_' + new Date().toISOString().split('T')[0] + '.csv');
link.click();
}
function importBelanjaCSV() {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.csv';
input.onchange = function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(re) {
var lines = re.target.result.split('\n');
var imported = [];
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var p = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(function(s){ return s.replace(/^"|"$/g, ''); });
// Struktur: ID(0), Nama(1), Harga(2), Qty(3), Total(4), Tanggal(5), Kategori(6), Catatan(7)
if (p.length >= 8) {
imported.push({
id: p[0] || null,
nama: p[1],
harga: cleanCSVNumber(p[2]),
qty: cleanCSVNumber(p[3]),
total: cleanCSVNumber(p[4]),
tgl: p[5],
kategori: p[6],
catatan: p[7]
});
}
}
if (imported.length > 0 && confirm('Akan memproses ' + imported.length + ' data belanja. Lanjutkan?')) {
showLoading(true, 'IMPORT DATA BELANJA...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
updateLocalData(res);
alert('Berhasil memproses data belanja.');
}).saveBelanjaBulk(imported);
}
};
reader.readAsText(file);
};
input.click();
}
function calcAdd(v) { document.getElementById('calc-input').value += v; }
function calcEval() {
try {
var res = eval(document.getElementById('calc-input').value);
document.getElementById('calc-result').innerText = '= ' + Number(res).toLocaleString();
} catch (e) { alert('Format salah'); }
}
function cetakStrukById(val, forceText, tajamSize, isCheck) {
var p = null;
if (typeof val === 'number') {
p = transaksi[val];
} else {
p = transaksi.find(function(t) { return String(t.id) === String(val); });
}
if (p) cetakStruk(p, forceText, isCheck, tajamSize);
else alert('Data tidak ditemukan');
}
function cetakStrukByIndex(idx, forceText, tajamSize, isCheck) {
cetakStrukById(idx, forceText, tajamSize, isCheck);
}
function pilihUkuranCetakTajam() {
var input = prompt('Pilih Ukuran CETAK (TAJAM):\n1. Kecil (hemat kertas)\n2. Normal\n3. Besar (lebih jelas)', '1');
if (input === null) return null;
var v = String(input || '').trim().toLowerCase();
if (v === '1' || v === 'kecil') return 'kecil';
if (v === '2' || v === 'normal') return 'normal';
if (v === '3' || v === 'besar') return 'besar';
return 'kecil';
}
function cetakStrukTajamByIndex(idx, isCheck) {
var size = pilihUkuranCetakTajam();
if (!size) return;
cetakStrukByIndex(idx, true, size, isCheck);
}
function sanitizeFileName(name) {
return String(name || 'Struk')
.replace(/[\\\/\:\*\?\"\<\>\|]/g, '_')
.replace(/\s+/g, '_')
.slice(0, 80);
}
function downloadPdfFromBase64(base64, fileName) {
try {
var binary = atob(base64);
var len = binary.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
var blob = new Blob([bytes], { type: 'application/pdf' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = fileName || 'Struk.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { try { URL.revokeObjectURL(url); } catch(e) {} }, 2000);
} catch (e) {
try { window.open('data:application/pdf;base64,' + base64, '_blank'); } catch(e2) {}
}
}
function simpanPdfBiasaAndroid(p, isCheck) {
var inner = generateReceiptInnerHtml(p, isCheck);
var html = '<html><head><meta charset="UTF-8"><style>' +
'@media print { @page { size: 58mm auto; margin: 0; } html, body { margin: 0; padding: 0; } }' +
'body { font-family: "Courier New", monospace; width:58mm; margin:0; padding:2mm; color:#000; background:#fff; }' +
'</style></head><body>' + inner + '</body></html>';
var idFmt = 'F1' + String(p.id || '').replace(/\\D/g, '').slice(-6).padStart(6, '0');
var fileName = sanitizeFileName('Struk_' + idFmt + '_' + new Date().toISOString().split('T')[0]) + '.pdf';
showLoading(true, 'MENYIAPKAN PDF...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || res.error || !res.base64) {
alert('Gagal membuat PDF: ' + (res && res.error ? res.error : 'Tidak ada respons'));
return;
}
downloadPdfFromBase64(res.base64, res.fileName || fileName);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal membuat PDF: ' + (err && err.message ? err.message : err));
}).generateReceiptPdfFromHtml(html, fileName);
}
function kirimWAById(val) {
var p = null;
if (typeof val === 'number') {
p = transaksi[val];
} else {
p = transaksi.find(function(t) { return String(t.id) === String(val); });
}
if (p) kirimWA(p);
else alert('Data tidak ditemukan');
}
function kirimWAByIndex(idx) {
kirimWAById(idx);
}
function fmtRp(v) {
var n = Math.floor(Number(v) || 0);
if (n === 0) return '-';
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
function generateReceiptInnerHtml(p, isCheck) {
var isBooking = p.status === 'Booking';
var dp = Number(p.dp) || 0;
var tgl = p.tgl ? String(p.tgl).split(' ')[0].split('T')[0] : '';
var jam = p.jam ? (String(p.jam).includes(' ') ? String(p.jam).split(' ')[1] : String(p.jam)) : '';
// ID: F1 + 6 digit angka terakhir dari ID database
var idFmt = 'F1' + String(p.id).replace(/\D/g, '').slice(-6).padStart(6, '0');
var waSensor = p.wa ? String(p.wa).replace(/.(?=.{4})/g, '*') : '-';
var poinTerpakai = Math.floor(Number(p.poinDipakai || 0) / getPoinRedeemRupiah());
var poinDapat = Number(p.poinDapat || 0);
var poinAwalRaw = p.poinAwal;
var poinAwal = (typeof poinAwalRaw === 'number' && poinAwalRaw > -1000000) ? poinAwalRaw : 0;
var totalPoin = poinAwal - poinTerpakai + poinDapat;
var itemsHtml = (p.items || []).map(function(i) {
var unitPrice = fmtRp(i.harga);
var totalPrice = fmtRp((Number(i.qty) || 0) * (Number(i.harga) || 0));
var typeLabel = i.type ? ('[' + i.type.toUpperCase() + '] ') : '';
return '<tr><td colspan="2" style="padding-top:4px;">' + typeLabel + escapeHtml(i.nama) + '</td></tr>' +
'<tr><td style="font-size:10px;">' + i.qty + ' x ' + unitPrice + '</td>' +
'<td style="text-align:right; font-size:10px;">' + totalPrice + '</td></tr>';
}).join('');
var addrLines = String(getStoreAddress() || '').split(',').map(function(s) { return String(s || '').trim(); }).filter(function(s) { return s; });
if (!addrLines.length && getStoreAddress()) addrLines = [String(getStoreAddress())];
var footerLink = getSocialLinkUrl() || getSocialInstagramUrl() || '';
var footerLinkHtml = '';
if (footerLink) {
footerLinkHtml = escapeHtml(footerLink);
footerLinkHtml = footerLinkHtml
.replace(/\//g, '/<wbr>')
.replace(/\./g, '.<wbr>')
.replace(/-/g, '-<wbr>')
.replace(/\?/g, '?<wbr>')
.replace(/=/g, '=<wbr>')
.replace(/&amp;/g, '&amp;<wbr>');
}
var footer = footerLink ? ('<div style="font-weight:900; font-size:9px;">Follow:</div>' +
'<div style="font-size:8px; font-weight:700; overflow-wrap:anywhere; word-break:break-word;">' + footerLinkHtml + '</div>') : '';
var html = '<div class="thermal-print" style="width: 58mm; font-family: \'Courier New\', monospace; font-size: 11px; color: #000;">' +
'<div style="text-align:center; border-bottom:1px dashed #000; padding-bottom:6px; margin-bottom:6px;">' +
'<b style="font-size: 14px;">' + escapeHtml(getStoreName()) + '</b><br>' +
(addrLines[0] ? '<small>' + escapeHtml(addrLines[0]) + '</small><br>' : '') +
(addrLines[1] ? '<small>' + escapeHtml(addrLines[1]) + '</small><br>' : '') +
(addrLines[2] ? '<small>' + escapeHtml(addrLines[2]) + '</small><br>' : '') +
'<small>Telp/WA: ' + escapeHtml(getWhatsappDisplay(getStoreWhatsapp())) + '</small><br>' +
'<small>' + new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + '</small>' +
'</div>';
if (isCheck) {
html += '<div style="text-align:center; border-bottom:1px dashed #000; padding-bottom:6px; margin-bottom:6px; font-weight:bold;">** STRUK PENGECEKAN **</div>';
}
html += '<table style="width:100%; border-collapse:collapse;">' +
'<tr><td>ID</td><td style="text-align:right;">' + escapeHtml(idFmt) + '</td></tr>' +
'<tr><td>Meja</td><td style="text-align:right;">' + escapeHtml(p.meja) + '</td></tr>' +
'<tr><td>Pelanggan</td><td style="text-align:right;">' + escapeHtml(p.nama || '-') + '</td></tr>' +
'<tr><td>WA</td><td style="text-align:right;">' + waSensor + '</td></tr>' +
'<tr><td>Status</td><td style="text-align:right;">' + escapeHtml(p.status) + '</td></tr>' +
(isBooking ? '<tr><td>Jadwal</td><td style="text-align:right;">' + escapeHtml(tgl) + ' ' + escapeHtml(jam) + '</td></tr>' : '') +
'</table>' +
'<table style="width:100%; border-top:1px dashed #000; margin-top:6px; padding-top:6px; border-collapse:collapse;">' +
itemsHtml +
'</table>' +
'<table style="width:100%; border-top:1px dashed #000; margin-top:6px; padding-top:6px; border-collapse:collapse;">' +
(function(){
var hasExtras = (p.pajak > 0 || p.service > 0 || p.adminFee > 0 || p.diskon > 0 || p.poinDipakai > 0 || dp > 0);
return hasExtras ? '<tr><td>Subtotal</td><td style="text-align:right;">' + fmtRp(p.subtotal||0) + '</td></tr>' : '';
})() +
(dp > 0 ? '<tr><td>DP ' + (p.tglDp ? escapeHtml(p.tglDp.split(' ')[0]) : '') + '</td><td style="text-align:right;">' + fmtRp(dp) + '</td></tr>' : '') +
(!isBooking && !isCheck ?
(p.pajak > 0 ? '<tr><td>Pajak (' + p.pajakPersen + '%)</td><td style="text-align:right;">' + fmtRp(p.pajak||0) + '</td></tr>' : '') +
(p.service > 0 ? '<tr><td>Service (' + p.servicePersen + '%)</td><td style="text-align:right;">' + fmtRp(p.service||0) + '</td></tr>' : '') +
(p.adminFee > 0 ? '<tr><td>Admin (' + p.adminPersen + '%)</td><td style="text-align:right;">' + fmtRp(p.adminFee||0) + '</td></tr>' : '') +
(p.diskon > 0 ? '<tr><td>Diskon</td><td style="text-align:right;">' + fmtRp(p.diskon||0) + '</td></tr>' : '') +
(p.poinDipakai > 0 ? '<tr><td>Poin Terpakai (Rp)</td><td style="text-align:right;">' + fmtRp(p.poinDipakai||0) + '</td></tr>' : '') +
'<tr><td style="font-weight:bold; border-top:1px solid #000; padding-top:4px;">Total Akhir</td>' +
'<td style="font-weight:bold; border-top:1px solid #000; padding-top:4px; text-align:right;">' + fmtRp(p.total||0) + '</td></tr>' +
'<tr><td>Metode</td><td style="text-align:right;">' + escapeHtml(p.metodeBayar||'') + '</td></tr>' +
(function(){
if (p.metodeBayar === 'Multi' && p.catatan) {
var details = {}; try { details = JSON.parse(p.catatan); } catch(e) {}
var res = '';
for (var m in details) {
if (details[m] > 0) {
res += '<tr><td style="font-size:10px; padding-left:10px;">- ' + m + '</td><td style="font-size:10px; text-align:right;">' + fmtRp(details[m]) + '</td></tr>';
}
}
return res;
}
return '';
})() +
'<tr><td>Bayar</td><td style="text-align:right;">' + fmtRp(p.bayar||0) + '</td></tr>' +
'<tr><td>Kembali</td><td style="text-align:right;">' + fmtRp(p.kembali||0) + '</td></tr>' +
'<tr><td colspan="2" style="text-align:center; font-weight:bold; padding-top:4px;">*** LUNAS ***</td></tr>' +
'<tr><td colspan="2" style="border-top:1px dashed #000; margin-top:6px; padding-top:6px; font-size:10px;">' +
'<table style="width:100%; border-collapse:collapse;">' +
'<tr><td>Poin Awal</td><td style="text-align:right;">' + poinAwal + '</td></tr>' +
(poinTerpakai > 0 ? '<tr><td>Poin Digunakan</td><td style="text-align:right;">-' + poinTerpakai + '</td></tr>' : '') +
(poinDapat > 0 ? '<tr><td>Poin Diperoleh</td><td style="text-align:right;">+' + poinDapat + '</td></tr>' : '') +
'<tr><td style="font-weight:bold;">Total Poin</td><td style="font-weight:bold; text-align:right;">' + (poinAwal - poinTerpakai + poinDapat) + '</td></tr>' +
'</table>' +
'</td></tr>'
:
'<tr><td style="font-weight:bold; border-top:1px solid #000; padding-top:4px;">Total</td>' +
'<td style="font-weight:bold; border-top:1px solid #000; padding-top:4px; text-align:right;">' + fmtRp(p.total||0) + '</td></tr>'
) +
'</table>';
if (isCheck) {
html += '<div style="text-align:center; margin-top:10px; font-weight:bold;">** BELUM LUNAS **</div>';
} else if (isBooking) {
html += '<div style="border-top:1px dashed #000; padding-top:6px; margin-top:6px; text-align:center; font-size:10px; font-weight:900;">Ditunggu kedatangannya pada tanggal ' + escapeHtml(tgl) + ', jam ' + escapeHtml(jam) + '.<br>Lewat 15 menit, DP hangus.</div>';
} else {
html += '<div style="text-align:center; margin-top:10px; font-weight:bold;">Terima Kasih Atas Kedatangannya!</div>';
}
html += '<div style="text-align:center; border-top:1px dashed #000; margin-top:8px; padding-top:8px;">' + footer + '</div>' +
'</div>';
return html;
}
function isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
function generateReceiptText(p, isCheck) {
var width = 28;
var divider = new Array(width + 1).join('-') + '\n';
var pad = function(n) { return new Array(Math.max(0, n) + 1).join(' '); };
var center = function(txt) {
var space = Math.floor((width - txt.length) / 2);
if (space < 0) space = 0;
return pad(space) + txt + '\n';
};
var justify = function(l, r) {
var sl = String(l || '');
var sr = String(r || '');
var space = width - (sl.length + sr.length);
if (space < 1) return sl + '\n' + pad(width - sr.length) + sr + '\n';
return sl + pad(space) + sr + '\n';
};
var wrap = function(s) {
var str = String(s || '');
var lines = [];
while (str.length > width) {
lines.push(str.slice(0, width));
str = str.slice(width);
}
if (str) lines.push(str);
return lines;
};
var normalizeUrl = function(u) {
var s = String(u || '').trim();
s = s.replace(/^https?:\/\//i, '');
return s;
};
var urlToLines = function(u) {
var s = normalizeUrl(u);
if (!s) return [];
var slash = s.indexOf('/');
if (slash === -1) return wrap(s);
var host = s.slice(0, slash + 1);
var rest = s.slice(slash + 1);
var out = [];
wrap(host).forEach(function(line) { out.push(line); });
wrap(rest).forEach(function(line) { out.push(line); });
return out;
};
// Tambah enter di awal untuk bersihkan buffer printer
var txt = '\n\n';
txt += divider;
txt += center(String(getStoreName() || 'POS').toUpperCase());
var addrLines = String(getStoreAddress() || '').split(',').map(function(s) { return String(s || '').trim(); }).filter(function(s) { return s; });
if (!addrLines.length && getStoreAddress()) addrLines = [String(getStoreAddress())];
for (var ai = 0; ai < Math.min(2, addrLines.length); ai++) {
wrap(addrLines[ai]).forEach(function(line) { txt += center(line); });
}
wrap(getWhatsappDisplay(getStoreWhatsapp())).forEach(function(line) { txt += center(line); });
if (isCheck) {
txt += divider;
txt += center("** PENGECEKAN **");
}
txt += divider;
var idFmt = 'F1' + String(p.id).replace(/\D/g, '').slice(-6).padStart(6, '0');
txt += justify("ID:" + idFmt, "Meja:" + p.meja);
if (p.nama && p.nama !== '-') txt += "Pelanggan: " + p.nama + '\n';
txt += "Waktu: " + new Date().toLocaleString('id-ID', {day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit'}) + '\n';
txt += divider;
(p.items || []).forEach(function(i) {
var name = i.nama;
if (name.length > width) name = name.slice(0, width-3) + '...';
txt += name + '\n';
var detail = i.qty + 'x' + fmtRp(i.harga);
var sub = fmtRp(Number(i.qty) * Number(i.harga));
txt += justify(' ' + detail, sub);
});
txt += divider;
// Tampilkan Subtotal hanya jika ada pajak, service, admin, diskon, poin, atau DP
var hasExtras = (p.pajak > 0 || p.service > 0 || p.adminFee > 0 || p.diskon > 0 || p.poinDipakai > 0 || p.dp > 0);
if (hasExtras) {
txt += justify("Subtotal", fmtRp(p.subtotal||0));
if (p.diskon) txt += justify("Diskon", "-" + fmtRp(p.diskon));
if (p.poinDipakai) txt += justify("Poin (Rp)", "-" + fmtRp(p.poinDipakai));
if (p.dp) txt += justify("DP", "-" + fmtRp(p.dp));
}
txt += justify("TOTAL", fmtRp(p.total||0));
if (!isCheck) {
txt += justify("Metode", (p.metodeBayar || '-'));
if (p.metodeBayar === 'Multi' && p.catatan) {
var details = {}; try { details = JSON.parse(p.catatan); } catch(e) {}
for (var m in details) {
if (details[m] > 0) {
txt += justify(" " + m, fmtRp(details[m]));
}
}
}
txt += justify("Bayar", fmtRp(p.bayar||0));
txt += justify("Kembali", fmtRp(p.kembali||0));
var pDapat = Number(p.poinDapat) || 0;
var pPakai = Math.floor((Number(p.poinDipakai) || 0) / getPoinRedeemRupiah());
var pAwal = (typeof p.poinAwal === 'number' && p.poinAwal > -1000000) ? p.poinAwal : 0;
var pTotal = pAwal - pPakai + pDapat;
txt += divider;
txt += "INFO POIN\n";
txt += justify("Awal", String(pAwal));
if (pPakai > 0) txt += justify("Pakai", "-" + String(pPakai));
if (pDapat > 0) txt += justify("Dapat", "+" + String(pDapat));
txt += justify("Total", String(pTotal));
txt += divider;
txt += center("TERIMA KASIH");
var follow = getSocialLinkUrl() || getSocialInstagramUrl() || '';
if (follow) {
txt += center("FOLLOW:");
urlToLines(follow).forEach(function(line) { txt += center(line); });
}
} else {
txt += divider;
txt += center("** BELUM LUNAS **");
}
// 10 baris kosong + titik (.) di akhir agar printer dipaksa menggulung kertas
// melewati pisau pemotong. Titik di akhir mencegah printer mengabaikan baris kosong.
return txt + '\n\n\n.';
}
function cetakStruk(p, forceText, isCheck, tajamSize) {
var isAndroid = /Android/i.test(navigator.userAgent);
var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
var isMobile = isMobileDevice();
// Gunakan format Teks Polos hanya jika dipaksa (CETAK TAJAM)
if (forceText) {
var mode = tajamSize || 'kecil';
var androidEsc = (mode === 'besar' ? '\x1B\x21\x10' : (mode === 'normal' ? '\x1B\x21\x00' : '\x1B\x21\x01'));
var iosFontSize = (mode === 'besar' ? '12pt' : (mode === 'normal' ? '10pt' : '9pt'));
var iosScale = (mode === 'besar' ? 2.5 : 2);
var deskFontSize = (mode === 'besar' ? '11pt' : (mode === 'normal' ? '9pt' : '8pt'));
var deskLineHeight = (mode === 'besar' ? '1.1' : (mode === 'normal' ? '1.0' : '0.95'));
var text = generateReceiptText(p, isCheck);
if (isAndroid) {
// Android via RawBT
try {
var fullText = androidEsc + text;
var base64Text = btoa(unescape(encodeURIComponent(fullText)));
window.location.href = 'rawbt:base64,' + base64Text;
} catch(e) { alert('Gagal memproses teks cetak.'); }
} else if (isIOS) {
// iPhone/iPad: Simpan sebagai GAMBAR
var printArea = document.getElementById('print-area');
if (printArea) {
printArea.innerHTML = '<pre style="font-family: \'Courier New\', monospace; font-size: ' + iosFontSize + '; line-height: 1.1; background:#fff; color:#000; padding:20px; margin:0; width:300px; white-space: pre-wrap;">' + text + '</pre>';
printArea.style.display = 'block';
printArea.style.position = 'fixed';
printArea.style.left = '0';
printArea.style.top = '0';
printArea.style.zIndex = '9999';
showLoading(true, 'MENYIAPKAN GAMBAR...');
setTimeout(function() {
html2canvas(printArea, { scale: iosScale }).then(function(canvas) {
var imgData = canvas.toDataURL('image/png');
showImageModal(imgData, text);
printArea.style.display = 'none';
showLoading(false);
});
}, 500);
} else {
alert('Area cetak tidak ditemukan.');
}
} else {
// Laptop via Browser (Sangat Rapat & Tajam)
var w = window.open('', '_blank', 'width=300,height=500');
if (w) {
var doc = '<html><head><style>' +
'@media print { @page { margin: 0; size: 58mm auto; } html, body { margin: 0; padding: 0; } }' +
'body { font-family: \"Courier New\", Courier, monospace; width: 58mm; margin: 0; padding: 0; ' +
'font-size: ' + deskFontSize + '; line-height: ' + deskLineHeight + '; white-space: pre-wrap; word-break: break-word; color: #000; background: #fff; ' +
'-webkit-print-color-adjust: exact; }' +
'pre { margin: 0; padding: 0; border: none; font-family: inherit; white-space: pre-wrap; }' +
'</style></head><bo' + 'dy>' +
'<pre>' + text + '</pre>' +
'</bo' + 'dy></ht' + 'ml>';
w.document.open();
w.document.write(doc);
w.document.close();
w.focus();
setTimeout(function() {
try { w.print(); } catch(e) {}
setTimeout(function() { try { w.close(); } catch(e2) {} }, 500);
}, 300);
} else {
var printArea = document.getElementById('print-area');
if (printArea) {
printArea.innerHTML = '<pre style="font-family: \'Courier New\', monospace; font-size: ' + deskFontSize + '; line-height: ' + deskLineHeight + '; background:#fff; color:#000; padding:0; margin:0; white-space: pre-wrap; width:58mm;">' + text + '</pre>';
printArea.style.display = 'block';
window.print();
setTimeout(function() { printArea.style.display = 'none'; }, 500);
}
}
}
return;
}
// Cetak Biasa (HTML). Android: langsung simpan PDF (download). iOS: pakai print dialog.
var inner = generateReceiptInnerHtml(p, isCheck);
if (isMobile) {
if (isAndroid) {
simpanPdfBiasaAndroid(p, isCheck);
return;
}
var printAreaMobile = document.getElementById('print-area');
if (!printAreaMobile) { alert('Area cetak tidak ditemukan.'); return; }
var prev = {
display: printAreaMobile.style.display,
position: printAreaMobile.style.position,
left: printAreaMobile.style.left,
top: printAreaMobile.style.top,
zIndex: printAreaMobile.style.zIndex,
width: printAreaMobile.style.width
};
printAreaMobile.innerHTML = inner;
printAreaMobile.style.display = 'block';
// Jangan tampil di layar (hindari overlay), cukup siapkan untuk @media print
printAreaMobile.style.position = 'absolute';
printAreaMobile.style.left = '-9999px';
printAreaMobile.style.top = '-9999px';
printAreaMobile.style.zIndex = '';
printAreaMobile.style.width = '58mm';
var cleaned = false;
var cleanup = function() {
if (cleaned) return;
cleaned = true;
printAreaMobile.style.display = prev.display || 'none';
printAreaMobile.style.position = prev.position || 'absolute';
printAreaMobile.style.left = prev.left || '-9999px';
printAreaMobile.style.top = prev.top || '-9999px';
printAreaMobile.style.zIndex = prev.zIndex || '';
printAreaMobile.style.width = prev.width || '';
printAreaMobile.innerHTML = '';
try { window.removeEventListener('afterprint', onAfterPrint); } catch(e) {}
};
var onAfterPrint = function() { cleanup(); };
try { window.addEventListener('afterprint', onAfterPrint); } catch(e) {}
// Fallback cleanup kalau afterprint tidak terpanggil
setTimeout(function() { cleanup(); }, 30000);
// PENTING: panggil window.print() sinkron (agar tidak diblokir Android)
try { window.print(); } catch(e) {}
return;
}
// Laptop/Desktop: buka window baru agar halaman utama tidak ikut ter-print
var html = '<html><head><style>' +
'@media print { @page { size: 58mm auto; margin: 0; } html, body { margin: 0; padding: 0; } }' +
'body { font-family: \'Courier New\', monospace; width:58mm; margin:0; padding:2mm; font-size:11px; line-height:1.2; }' +
'.row { display:flex; justify-content:space-between; margin-bottom:2px; }' +
'b { font-size: 14px; }' +
'small { font-size: 10px; }' +
'</style></head><bo' + 'dy>' +
inner +
'</bo' + 'dy></ht' + 'ml>';
var w = window.open('', '_blank', 'width=320,height=600');
if (!w) {
var printArea = document.getElementById('print-area');
printArea.innerHTML = inner;
printArea.style.display = 'block';
window.print();
setTimeout(function() { printArea.style.display = 'none'; }, 500);
} else {
w.document.open();
w.document.write(html);
w.document.close();
w.focus();
setTimeout(function() {
try { w.print(); } catch(e) {}
setTimeout(function() { try { w.close(); } catch(e2) {} }, 500);
}, 300);
}
}
function kirimWA(p) {
var isBooking = p.status === 'Booking';
var tgl = p.tgl ? String(p.tgl).split(' ')[0].split('T')[0] : '';
var jam = p.jam ? (String(p.jam).includes(' ') ? String(p.jam).split(' ')[1] : String(p.jam)) : '';
var storeName = getStoreName();
var addrRaw = String(getStoreAddress() || '');
var addrLines = addrRaw.split(',').map(function(s) { return String(s || '').trim(); }).filter(function(s) { return s; });
var waDisp = getWhatsappDisplay(getStoreWhatsapp());
var mapsUrl = getSocialGmapsUrl();
var linkUrl = getSocialLinkUrl();
var poinUrl = scriptUrl ? (scriptUrl + '?p=poin') : 'https://s.id/FukuPoin';
var footerText = '\n📍 *Lokasi Toko:*\n' +
(addrLines.length ? (addrLines.join(', ') + '\n') : '') +
(mapsUrl ? ('Google Maps: ' + mapsUrl + '\n') : '') +
(linkUrl ? ('Link: ' + linkUrl + '\n') : '') +
'\n*Cek Poin & Riwayat:* ' + poinUrl + '\n';
var idFmt = 'F1' + String(p.id).replace(/\D/g, '').slice(-6).padStart(6, '0');
var text = '*' + storeName + '*\n' + (addrLines[0] ? (addrLines[0] + '\n') : '') + (addrLines[1] ? (addrLines[1] + '\n') : '') + 'Kontak: ' + waDisp + '\n\n' +
'ID: ' + idFmt + '\nMeja: ' + p.meja + '\nTamu: ' + (p.nama || '-') + '\nStatus: ' + p.status + '\n';
if (isBooking) text += 'Jadwal: ' + tgl + ' ' + jam + '\nDP: Rp ' + fmtRp(p.dp) + '\n';
text += '\n*DETAIL PESANAN:*\n';
(p.items || []).forEach(function(i){
var type = i.type ? ('[' + i.type.toUpperCase() + '] ') : '';
text += '- ' + type + i.qty + 'x ' + i.nama + ' (Rp ' + fmtRp(Number(i.qty)*Number(i.harga)) + ')\n';
});
if (!isBooking) {
text += '\nTotal: Rp ' + fmtRp(p.total||0) + '\nMetode: ' + (p.metodeBayar || '-');
if (p.metodeBayar === 'Multi' && p.catatan) {
var details = {}; try { details = JSON.parse(p.catatan); } catch(e) {}
for (var m in details) {
if (details[m] > 0) {
text += '\n - ' + m + ': Rp ' + fmtRp(details[m]);
}
}
}
text += '\nBayar: Rp ' + fmtRp(p.bayar||0) + '\nKembali: Rp ' + fmtRp(p.kembali||0) + '\n*LUNAS*\n';
// Tambahkan Detail Poin
if (p.wa) {
var pDapat = Number(p.poinDapat) || 0;
var pPakai = Math.floor((Number(p.poinDipakai) || 0) / getPoinRedeemRupiah());
var pAwal = (typeof p.poinAwal === 'number' && p.poinAwal > -1000000) ? p.poinAwal : 0;
var pTotal = pAwal - pPakai + pDapat;
text += '\n*INFORMASI POIN:*\n' +
'- Poin Awal: ' + pAwal + '\n' +
(pPakai > 0 ? '- Poin Digunakan: -' + pPakai + '\n' : '') +
(pDapat > 0 ? '- Poin Diperoleh: +' + pDapat + '\n' : '') +
'- *Total Poin: ' + pTotal + '*\n';
}
} else {
text += '\nCatatan: Ditunggu kedatangannya pada tanggal ' + tgl + ', jam ' + jam + '. Lewat 15 menit, DP hangus.\n';
}
text += footerText;
var wa = String(p.wa || '').replace(/\D/g, '');
if (!wa) { alert('WA pelanggan kosong'); return; }
var dest = wa.indexOf('0') === 0 ? ('62' + wa.slice(1)) : (wa.indexOf('8') === 0 ? ('62' + wa) : wa);
var w = window.open('https://wa.me/' + dest + '?text=' + encodeURIComponent(text), '_blank');
if (!w) {
alert('Popup terblokir! Silakan izinkan popup untuk mengirim WhatsApp.');
}
}
function escapeHtml(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;');
}
function escapeJson(obj) {
return JSON.stringify(obj).replace(/</g,'\\u003c').replace(/>/g,'\\u003e').replace(/&/g,'\\u0026');
}
</script>
</body>
</html>