7199 lines
337 KiB
HTML
7199 lines
337 KiB
HTML
<!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 1–10 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 Contoh: Beras 1 15000 15000 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(/&/g, '&<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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
}
|
||
function escapeJson(obj) {
|
||
return JSON.stringify(obj).replace(/</g,'\\u003c').replace(/>/g,'\\u003e').replace(/&/g,'\\u0026');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|