39735-vm/local-preview-bridge.js
Flatlogic Bot 8c2a5d487c POSFuku_v2
2026-04-19 12:46:29 +00:00

873 lines
40 KiB
JavaScript

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