39735-vm/POSFuku/Dapur.html
2026-04-19 12:17:53 +00:00

487 lines
22 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="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>POS - DAPUR</title>
<style>
body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; padding: 12px; -webkit-font-smoothing: antialiased; }
.nav { background:#111; color:white; padding:12px; border-radius:12px; margin-bottom:15px; display:flex; justify-content:space-between; align-items:center; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.title { font-weight:800; letter-spacing:0.5px; font-size: 14px; }
.btn-refresh { background:#e11d48; color:white; border:none; padding:10px 15px; border-radius:10px; font-weight:800; cursor:pointer; font-size: 12px; }
.btn-refresh:active { opacity: 0.7; }
.dapur-grid { display:grid; grid-template-columns: 1fr; gap:12px; }
@media (min-width: 768px) { .dapur-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); } }
.trans-card { background:white; border:1px solid #eee; border-radius:12px; padding:15px; position:relative; transform: translateZ(0); }
.title-sm { font-weight:800; color:#111; margin-bottom:10px; font-size:14px; }
.muted { color:#777; font-size:11px; }
.item-row { display:flex; justify-content:space-between; align-items: center; padding:10px 0; border-bottom:1px solid #f3f4f6; }
.item-nama { font-weight:800; font-size:15px; color: #111; }
.item-qty { font-weight:900; font-size:20px; color:#e11d48; }
.item-qty.prio { color:#16a34a; }
.btn-selesai { width:100%; padding:14px; border:none; border-radius:12px; background:#111; color:white; font-weight:800; cursor:pointer; margin-top:12px; font-size: 14px; }
.btn-selesai.prio { background:#16a34a; }
.btn-selesai:active { opacity: 0.7; }
.btn-print { width:100%; padding:14px; border:none; border-radius:12px; background:#444; color:white; font-weight:800; cursor:pointer; margin-top:12px; font-size: 14px; }
.btn-print:active { opacity: 0.7; }
#sync-indicator { position:fixed; top:15px; right:15px; background:rgba(0,0,0,0.8); color:white; padding:6px 12px; border-radius:20px; font-size:11px; display:none; z-index: 1000; font-weight: 800; }
#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; }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: #ddd; border-radius: 10px; }
.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; }
</style>
</head>
<body>
<div class="nav">
<div class="title" id="store-title">DAPUR</div>
<div style="display:flex; gap:10px;">
<button class="btn-refresh" onclick="openChatModal()">CHAT</button>
<button class="btn-refresh" onclick="triggerBellDapur()">PANGGIL KASIR</button>
<button class="btn-refresh" onclick="syncData()">REFRESH PESANAN</button>
</div>
</div>
<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 HP bisa mengeluarkan bunyi pip saat ada pesanan masuk.</p>
<button class="btn-audio" onclick="enableAudio()">AKTIFKAN SEKARANG</button>
</div>
<div id="sync-indicator">⌛ Syncing...</div>
<div id="dapur-content">
<div class="title-sm" style="color:#16a34a; font-size:18px;">⚡ PRIORITAS: DRINKS, NASI & TOMYUM</div>
<div id="list-prio" class="dapur-grid" style="margin-bottom:30px;"></div>
<div class="title-sm" style="color:#e11d48; font-size:18px;">🥘 MENU UTAMA / GRILL</div>
<div id="list-main" class="dapur-grid"></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;">Info</div>
<div class="close" onclick="closeModal()">TUTUP</div>
</div>
<div id="modal-body"></div>
</div>
</div>
<script>
var transaksi = [];
var menu = [];
var audioContext = null;
var beepInterval = null;
var chatBeepInterval = null;
var lastChatCount = 0;
window.onload = function() {
initAutoAudioUnlock();
syncData();
setInterval(syncData, 15000); // Auto refresh lebih cepat (15 detik)
setInterval(checkNewChat, 10000); // Polling chat global
};
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 checkNewChat() {
google.script.run.withSuccessHandler(function(messages) {
// Cari apakah ada pesan baru dari Kasir yang belum dibaca
var unreadFromKasir = messages.some(function(m) {
return m.sender === 'Kasir' && m.status === 'Belum Dibaca';
});
if (unreadFromKasir) {
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('Dapur');
}
}).getChatMessages();
}
function enableAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
document.getElementById('audio-permission').style.display = 'none';
// Play silent buffer to unlock audio on iOS
var buffer = audioContext.createBuffer(1, 1, 22050);
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);
}
function playBeep() {
if (!audioContext) return;
var osc = audioContext.createOscillator();
var gain = audioContext.createGain();
// Menggunakan 'triangle' agar suara lebih padat dan keras dibanding 'sine'
osc.type = 'triangle';
osc.frequency.setValueAtTime(1000, audioContext.currentTime); // Frekuensi lebih tinggi (lebih tajam)
// Volume ditingkatkan ke 0.8 (dari 0.1)
gain.gain.setValueAtTime(0.8, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.8);
osc.connect(gain);
gain.connect(audioContext.destination);
osc.start();
osc.stop(audioContext.currentTime + 0.8);
}
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 syncData() {
document.getElementById('sync-indicator').style.display = 'block';
google.script.run.withSuccessHandler(function(res) {
document.getElementById('sync-indicator').style.display = 'none';
transaksi = res.transaksi || [];
menu = res.menu || [];
if (res && res.settings && res.settings.store_name) {
var storeName = String(res.settings.store_name || 'POS');
var el = document.getElementById('store-title');
if (el) el.innerText = storeName + ' - DAPUR';
try { document.title = storeName + ' - DAPUR'; } catch(e) {}
}
renderDapur();
}).withFailureHandler(function(err) {
document.getElementById('sync-indicator').style.display = 'none';
alert('Gagal sinkronisasi dapur.\n' + (err && err.message ? err.message : err));
}).getInitialData();
}
function renderDapur() {
var boxPrio = document.getElementById('list-prio');
var boxMain = document.getElementById('list-main');
boxPrio.innerHTML = '';
boxMain.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;
});
var shouldMuteSound = function(t) {
var nama = String((t && t.nama) || '').trim();
return /x123/i.test(nama);
};
// Bunyi Pip berulang jika ada pesanan masuk
var beepList = list.filter(function(t) { return !shouldMuteSound(t); });
if (beepList.length > 0) {
if (!beepInterval) {
playBeep(); // Bunyi langsung sekali
beepInterval = setInterval(playBeep, 3000); // Ulang tiap 3 detik
}
} else {
if (beepInterval) {
clearInterval(beepInterval);
beepInterval = null;
}
}
// Tampilkan pesan jika kosong
if (!list.length) {
boxMain.innerHTML = '<div style="text-align:center; grid-column:1/-1; padding:50px; color:#aaa;">Belum ada pesanan aktif.</div>';
return;
}
list.forEach(function(t) {
var items = t.items || [];
// Pisahkan item prioritas (Drinks, Nasi & Kuah Tomyum di Ala Carte)
var prioItems = items.filter(function(it) {
var n = it.nama.toLowerCase();
var k = it.kat || (menu.find(function(m) { return m.nama === it.nama; }) || {}).kategori || '';
return (k === 'Drinks') || (k === 'Ala Carte' && (n.includes('nasi') || n.includes('kuah tomyum')));
});
var mainItems = items.filter(function(it) {
var n = it.nama.toLowerCase();
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 !isPrio;
});
// Render kartu Prioritas jika ada
if (prioItems.length > 0) {
boxPrio.appendChild(createCard(t, prioItems, true));
}
// Render kartu Utama jika ada
if (mainItems.length > 0) {
boxMain.appendChild(createCard(t, mainItems, false));
}
});
}
function createCard(t, items, isPrio) {
var div = document.createElement('div');
div.className = 'trans-card';
if (isPrio) div.style.borderLeft = '6px solid #16a34a';
else div.style.borderLeft = '6px solid #e11d48';
var mejaText = String(t.meja || '').trim();
if (mejaText.toLowerCase().indexOf('meja') === 0) {
mejaText = mejaText.replace(/^\s*meja\s*/i, '').trim();
}
mejaText = mejaText || '?';
var itemsHtml = '';
items.forEach(function(it) {
itemsHtml += '<div class="item-row">' +
'<div class="item-nama">' + (isPrio ? '🥤 ' : '🍲 ') + it.nama + '</div>' +
'<div class="item-qty ' + (isPrio ? 'prio' : '') + '">x' + it.qty + '</div>' +
'</div>';
});
var catatanHtml = '';
if (t.catatan) {
catatanHtml = '<div style="margin-top:10px; padding:8px; background:#fff7ed; border:1px dashed #fb923c; border-radius:8px; font-size:12px; color:#9a3412;">' +
'<b>Catatan:</b> ' + t.catatan +
'</div>';
}
div.innerHTML = '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; border-bottom:2px solid #eee; padding-bottom:8px; gap:10px;">' +
'<div style="display:flex; flex-direction:column; gap:2px;">' +
'<div style="font-weight:900; font-size:20px;">Meja ' + mejaText + '</div>' +
'<div style="font-size:12px; font-weight:800; color:#111;">' + (t.nama ? t.nama : '-') + '</div>' +
'</div>' +
'<div class="muted" style="white-space:nowrap;">' + (t.timestamp ? t.timestamp.split(' ')[1] : '') + '</div>' +
'</div>' +
'<div>' + itemsHtml + '</div>' +
catatanHtml +
'<div style="display:flex; gap:10px; margin-top:12px;">' +
'<button class="btn-print" style="flex:1; margin-top:0;" onclick="cetakStrukDapur(\'' + t.id + '\', ' + (isPrio ? 'true' : 'false') + ')">CETAK</button>' +
'<button class="btn-selesai ' + (isPrio ? 'prio' : '') + '" style="flex:1; margin-top:0;" onclick="tandaiSelesai(\'' + t.id + '\')">TANDAI SELESAI</button>' +
'</div>';
return div;
}
function cetakStrukDapur(id, isPrio) {
var t = transaksi.find(function(x) { return String(x.id) === String(id); });
if (!t) { alert('Transaksi tidak ditemukan.'); return; }
var items = Array.isArray(t.items) ? t.items : [];
var filtered = items.filter(function(it) {
var n = String(it.nama || '').toLowerCase();
var k = it.kat || (menu.find(function(m) { return m.nama === it.nama; }) || {}).kategori || '';
var prio = (k === 'Drinks') || (k === 'Ala Carte' && (n.indexOf('nasi') > -1 || n.indexOf('kuah tomyum') > -1));
return isPrio ? prio : !prio;
});
var mejaText = String(t.meja || '').trim();
if (mejaText.toLowerCase().indexOf('meja') === 0) mejaText = mejaText.replace(/^\s*meja\s*/i, '').trim();
mejaText = mejaText || '?';
var lines = [];
lines.push('MEJA ' + mejaText);
if (t.nama) lines.push(String(t.nama));
lines.push('------------------------------');
filtered.forEach(function(it) {
var qty = Number(it.qty || 0) || 0;
var name = String(it.nama || '');
if (qty <= 0 || !name) return;
lines.push('x' + qty + ' ' + name);
});
if (t.catatan) {
lines.push('------------------------------');
lines.push('CATATAN: ' + String(t.catatan));
}
var text = lines.join('\n');
var isAndroid = /Android/i.test(navigator.userAgent || '');
if (isAndroid) {
var url = 'rawbt:' + encodeURIComponent(text + '\n\n');
window.location.href = url;
return;
}
var w = window.open('', '_blank', 'width=320,height=600');
if (!w) {
alert(text);
return;
}
var doc = '<html><head><style>' +
'@media print { @page { margin: 0; size: 58mm auto; } body { margin: 0; padding: 0; } }' +
'body { font-family: \"Courier New\", Courier, monospace; width: 48mm; margin: 0; padding: 0; font-size: 10pt; line-height: 1.1; white-space: pre-wrap; word-break: break-word; color: #000; background:#fff; }' +
'pre { margin: 0; padding: 6px; }' +
'</style></head><bo' + 'dy>' +
'<pre>' + text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</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);
}
function tandaiSelesai(id) {
if (!confirm('Pesanan meja ini sudah siap saji?')) return;
document.getElementById('sync-indicator').style.display = 'block';
google.script.run.withSuccessHandler(function(res) {
document.getElementById('sync-indicator').style.display = 'none';
transaksi = res.transaksi || [];
renderDapur();
alert('Pesanan selesai! Silakan diantar ke meja.');
}).withFailureHandler(function(err) {
document.getElementById('sync-indicator').style.display = 'none';
alert('Gagal update: ' + err.message);
}).updateKitchenStatus(id, 'Ready');
}
// --- Fungsi Chat & Bel ---
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-refresh" onclick="sendChat()">KIRIM</button>' +
'</div>' +
'</div>';
openModalCustom(body, 'Chat dengan Kasir');
renderChat();
}
function renderChat() {
google.script.run.withSuccessHandler(function(messages) {
renderChatList(messages);
}).withFailureHandler(function(err) {
alert('Gagal memuat chat.\n' + (err && err.message ? err.message : err));
}).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 isDapur = msg.sender === 'Dapur';
msgDiv.style.textAlign = isDapur ? 'right' : 'left';
msgDiv.innerHTML = '<div style="background:' + (isDapur ? '#111' : '#f3f4f6') + '; color:' + (isDapur ? '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();
}).withFailureHandler(function(err) {
alert('Gagal mengirim chat.\n' + (err && err.message ? err.message : err));
}).sendChatMessage('Dapur', message);
}
function triggerBellDapur() {
if (!confirm('Panggil Kasir untuk mengambil pesanan?')) return;
google.script.run.withSuccessHandler(function() {
alert('Bel Kasir dipicu!');
}).withFailureHandler(function(err) {
alert('Gagal memicu bel.\n' + (err && err.message ? err.message : err));
}).triggerBell('DapurKeKasir');
}
// Modal functions (copy from Index.html)
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 closeModal() {
document.getElementById('modal').style.display = 'none';
}
</script>
</body>
</html>