873 lines
40 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
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 & Belanja</a>' +
|
|
'<a href="/RekapTransaksi/Index.html">Rekap & 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();
|
|
}
|
|
})();
|