Initial import

This commit is contained in:
Flatlogic Bot 2026-04-19 12:17:53 +00:00
commit 57edfd794c
10 changed files with 17787 additions and 0 deletions

502
POSFuku/Belanja.html Normal file
View File

@ -0,0 +1,502 @@
<!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 - Riwayat Belanja</title>
<style>
body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; color: #333; padding-bottom: 40px; -webkit-font-smoothing: antialiased; }
.header { background:#111; color:white; padding:15px; text-align:center; position:sticky; top:0; z-index:100; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.container { padding:12px; max-width: 600px; margin: 0 auto; }
.card { background:white; border-radius:12px; padding:15px; margin-bottom:15px; border: 1px solid #eee; transform: translateZ(0); }
.title { font-size: 16px; font-weight: 800; margin-bottom: 12px; color: #111; text-align: center; }
.date-header { background:#fff; padding:10px 12px; font-weight:800; font-size:13px; margin:15px 0 8px 0; border-radius:10px; display:flex; justify-content:space-between; align-items:center; border-left: 4px solid #e11d48; border-top: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee; }
.total-day { font-size: 12px; color: #e11d48; font-weight: 800; }
.item-row { display:flex; justify-content:space-between; align-items:center; padding:12px; border:1px solid #eee; border-radius:12px; margin-bottom:8px; background: white; transition: background 0.1s; }
.item-row:active { background: #f9fafb; }
.item-info { display:flex; flex-direction:column; }
.item-name { font-weight:800; font-size:14px; color: #111; }
.item-kat { font-size: 9px; color: #e11d48; text-transform: uppercase; font-weight: 800; margin-bottom: 2px; }
.item-detail { font-size:11px; color:#777; margin-top:2px; }
.item-price { font-weight:800; color: #111; font-size:14px; }
#loading { position:fixed; inset:0; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index:1000; }
.spinner { border:3px solid #f3f3f3; border-top:3px solid #e11d48; border-radius:50%; width:35px; height:35px; animation:spin 0.8s linear infinite; margin-bottom: 10px; }
@keyframes spin { 0% { transform:rotate(0deg); } 100% { transform:rotate(360deg); } }
input, select, textarea {
width:100%;
box-sizing:border-box;
padding:12px;
border-radius:10px;
border:1px solid #ccc;
outline:none;
font-size: 14px;
background: #fff;
-webkit-appearance: none;
}
.btn {
width:100%;
padding:14px;
border:none;
border-radius:12px;
font-weight:800;
cursor:pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary { background:#e11d48; color:white; }
.btn:active { opacity: 0.7; }
.muted { color: #999; font-size: 12px; text-align: center; margin-top: 30px; }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: #ddd; border-radius: 10px; }
</style>
</head>
<body>
<div id="loading"><div class="spinner"></div><p>Memuat Riwayat Belanja...</p></div>
<div class="header">
<div id="store-name" style="font-weight:900; font-size:20px;">POS</div>
<div style="font-size:12px; opacity:0.8;">Manajemen Pengeluaran Belanja</div>
</div>
<div class="container">
<!-- Input Belanja Section -->
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<div class="title" style="margin:0;">Input Belanja Baru</div>
<div style="display:flex; gap:6px;">
<button class="btn" style="padding: 6px 12px; font-size: 10px; width: auto; background: #22c55e; color: white;" onclick="exportBelanjaCSV()">EXPORT</button>
<button class="btn" style="padding: 6px 12px; font-size: 10px; width: auto; background: #3b82f6; color: white;" onclick="importBelanjaCSV()">IMPORT</button>
<button class="btn" style="padding: 6px 12px; font-size: 10px; width: auto; background: #111; color: white;" onclick="syncBelanjaOneWay()">SYNC</button>
</div>
</div>
<input type="hidden" id="bl-id">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;">
<input id="bl-nama" placeholder="Nama Barang / Biaya">
<select id="bl-kat" style="width:100%; padding:12px; border:1px solid #ddd; border-radius:10px; font-size:14px;">
<option value="Modal">Modal</option>
<option value="Bawah">Bawah</option>
<option value="Dapur" selected>Dapur</option>
<option value="Makan">Makan</option>
<option value="Operasional">Operasional</option>
<option value="Pegawai">Pegawai</option>
</select>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;">
<div><small style="font-size: 10px; color: #666;">Qty</small><input id="bl-qty" type="number" value="1" oninput="recalcBelanja('qty')"></div>
<div><small style="font-size: 10px; color: #666;">Harga Satuan</small><input id="bl-harga" type="number" oninput="recalcBelanja('harga')"></div>
</div>
<div style="margin-bottom: 10px;">
<small style="font-size: 10px; color: #666;">Total (Otomatis)</small>
<input id="bl-total" type="number" oninput="recalcBelanja('total')">
</div>
<button class="btn btn-primary" onclick="simpanBelanja()" style="margin-bottom:10px;">SIMPAN DATA BELANJA</button>
<div style="border-top: 1px dashed #ccc; padding-top: 10px; margin-top: 5px;">
<div style="font-size: 12px; font-weight: bold; margin-bottom: 5px;">Input Bulk (Cepat)</div>
<textarea id="bl-bulk" placeholder="Format: Nama Qty Harga&#10;Contoh: Es Batu 5 5000" style="width:100%; height:80px; padding:10px; border:1px solid #ddd; border-radius:10px; font-size:13px; font-family:monospace; margin-bottom:10px;"></textarea>
<div style="display:flex; gap:10px; flex-wrap:wrap; margin-bottom:10px;">
<div style="flex:1; min-width:160px;">
<small style="font-size: 10px; color: #666;">Kategori Bulk</small>
<select id="bl-bulk-kat" style="width:100%; padding:12px; border:1px solid #ddd; border-radius:10px; font-size:14px;">
<option value="Modal">Modal</option>
<option value="Bawah">Bawah</option>
<option value="Dapur" selected>Dapur</option>
<option value="Makan">Makan</option>
<option value="Operasional">Operasional</option>
<option value="Pegawai">Pegawai</option>
</select>
</div>
<div style="flex:1; min-width:160px; display:flex; align-items:flex-end;">
<small style="font-size: 10px; color: #64748b; font-weight:800;">Tips: tambah "Q" di depan/akhir nama untuk catat QRIS/Transfer.</small>
</div>
</div>
<button class="btn" style="background:#111; color:white;" onclick="simpanBelanjaBulk()">SIMPAN BULK</button>
</div>
</div>
<!-- Kalkulator Section -->
<div class="card">
<div class="title" style="font-size: 14px; margin-bottom: 10px;">Kalkulator Bantu</div>
<input id="calc-input" placeholder="Misal: 5000 + 3000 * 2" style="font-family:monospace; font-size:16px; text-align: right; margin-bottom: 5px;">
<div id="calc-result" style="font-weight:900; font-size:20px; text-align:right; color: #e11d48; margin-bottom: 15px;">= 0</div>
<div style="display:grid; grid-template-columns: repeat(4, 1fr); gap:8px;">
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('7')">7</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('8')">8</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('9')">9</button>
<button class="btn" style="background:#111; color: white;" onclick="calcAdd('/')">/</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('4')">4</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('5')">5</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('6')">6</button>
<button class="btn" style="background:#111; color: white;" onclick="calcAdd('*')">*</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('1')">1</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('2')">2</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('3')">3</button>
<button class="btn" style="background:#111; color: white;" onclick="calcAdd('-')">-</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('0')">0</button>
<button class="btn" style="background:#f3f4f6;" onclick="calcAdd('.')">.</button>
<button class="btn" style="background:#e11d48; color: white;" onclick="calcEval()">=</button>
<button class="btn" style="background:#111; color: white;" onclick="calcAdd('+')">+</button>
<button class="btn" style="background:#111; color: white; grid-column: span 4;" onclick="calcClear()">CLEAR</button>
</div>
</div>
<div id="belanja-list"></div>
</div>
<script>
function showLoading(s, text) {
document.getElementById('loading').style.display = s ? 'flex' : 'none';
if (text) document.querySelector('#loading p').innerText = text;
else document.querySelector('#loading p').innerText = 'Memuat Riwayat Belanja...';
}
var currentBelanja = [];
var storeName = 'POS';
window.onload = function() {
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.belanja) {
currentBelanja = res.belanja;
renderBelanja(res.belanja);
}
if (res && res.settings) {
storeName = String(res.settings.store_name || storeName);
var el = document.getElementById('store-name');
if (el) el.innerText = storeName;
try { document.title = storeName + ' - Riwayat Belanja'; } catch(e) {}
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal memuat data belanja.\n' + (err && err.message ? err.message : err));
}).getInitialData();
};
function cleanCSVNumber(val) {
if (!val) return 0;
var s = String(val).replace(/,/g, '').trim();
return Number(s) || 0;
}
function exportBelanjaCSV() {
if (!currentBelanja || !currentBelanja.length) { alert('Tidak ada data belanja.'); return; }
var csv = 'ID,Nama,Harga,Qty,Total,Tanggal,Kategori,Catatan,Timestamp\n';
currentBelanja.forEach(function(b) {
var row = [
'"' + (b.id || '') + '"',
'"' + (b.nama || '') + '"',
(b.harga || 0),
(b.qty || 0),
(b.total || 0),
'"' + (b.tgl || '') + '"',
'"' + (b.kategori || '') + '"',
'"' + (b.catatan || '') + '"',
'"' + (b.timestamp || '') + '"'
];
csv += row.join(',') + '\n';
});
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var link = document.createElement('a');
link.setAttribute('href', URL.createObjectURL(blob));
link.setAttribute('download', 'Belanja_' + String(storeName || 'POS').replace(/[^a-zA-Z0-9]+/g, '_') + '_' + new Date().toISOString().split('T')[0] + '.csv');
link.click();
}
function importBelanjaCSV() {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.csv';
input.onchange = function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(re) {
var lines = re.target.result.split('\n');
var imported = [];
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var p = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(function(s){ return s.replace(/^"|"$/g, ''); });
if (p.length >= 8) {
imported.push({
id: p[0] || null,
nama: p[1],
harga: cleanCSVNumber(p[2]),
qty: cleanCSVNumber(p[3]),
total: cleanCSVNumber(p[4]),
tgl: p[5],
kategori: p[6],
catatan: p[7]
});
}
}
if (imported.length > 0 && confirm('Akan memproses ' + imported.length + ' data belanja. Lanjutkan?')) {
showLoading(true, 'IMPORT DATA BELANJA...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.belanja) {
currentBelanja = res.belanja;
renderBelanja(res.belanja);
}
alert('Berhasil memproses data belanja.');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal import belanja.\n' + (err && err.message ? err.message : err));
}).saveBelanjaBulk(imported);
}
};
reader.readAsText(file);
};
input.click();
}
function recalcBelanja(mode) {
var h = Number(document.getElementById('bl-harga').value) || 0;
var q = Number(document.getElementById('bl-qty').value) || 0;
var t = Number(document.getElementById('bl-total').value) || 0;
if (mode === 'qty' || mode === 'harga') {
document.getElementById('bl-total').value = h * q;
} else if (mode === 'total' && q > 0) {
document.getElementById('bl-harga').value = Math.floor(t / q);
}
}
function simpanBelanja() {
var p = {
id: document.getElementById('bl-id').value || null,
nama: document.getElementById('bl-nama').value.trim(),
kategori: document.getElementById('bl-kat').value,
harga: Number(document.getElementById('bl-harga').value) || 0,
qty: Number(document.getElementById('bl-qty').value) || 0,
total: Number(document.getElementById('bl-total').value) || 0,
tgl: new Date().toISOString().split('T')[0]
};
if (!p.nama) { alert('Nama barang wajib diisi!'); return; }
if (p.total <= 0) { alert('Total belanja tidak boleh nol!'); return; }
showLoading(true, 'Menyimpan Data...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
alert('Berhasil menyimpan belanja!');
document.getElementById('bl-id').value = '';
document.getElementById('bl-nama').value = '';
document.getElementById('bl-harga').value = '';
document.getElementById('bl-qty').value = '1';
document.getElementById('bl-total').value = '';
if (res && res.belanja) renderBelanja(res.belanja);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan belanja.\n' + (err && err.message ? err.message : err));
}).saveBelanjaBulk([p]);
}
function simpanBelanjaBulk() {
var bulkText = document.getElementById('bl-bulk').value.trim();
if (!bulkText) return;
var lines = bulkText.split('\n');
var payloads = [];
var today = new Date().toISOString().split('T')[0];
var bulkKat = (document.getElementById('bl-bulk-kat') ? document.getElementById('bl-bulk-kat').value : '') || 'Dapur';
lines.forEach(function(line) {
var parts = line.trim().split(/\s+/);
if (parts.length < 3) return;
var last = parts.pop();
var mid = parts.pop();
var first = parts.pop();
var nama = parts.join(' ');
var qty, harga, total;
if (mid.toLowerCase() === 'x') {
// Format: Nama Qty x Total (Es Batu 5 x 25000)
qty = Number(first) || 0;
total = Number(last) || 0;
harga = qty > 0 ? Math.floor(total / qty) : total;
} else if (!isNaN(Number(first)) && !isNaN(Number(mid)) && !isNaN(Number(last))) {
// Format: Nama Qty Harga Total (Es Batu 5 5000 25000)
qty = Number(first) || 0;
harga = Number(mid) || 0;
total = Number(last) || 0;
} else {
// Format: Nama Qty Harga (Es Batu 5 5000)
nama = (nama ? nama + ' ' : '') + first;
qty = Number(mid) || 0;
harga = Number(last) || 0;
total = qty * harga;
}
nama = autoTitleCase(nama);
payloads.push({
nama: nama,
qty: qty,
harga: harga,
total: total,
kategori: bulkKat,
tgl: today
});
});
if (!payloads.length) { alert('Format salah. Contoh: Es Batu 5 5000'); return; }
showLoading(true, 'Menyimpan Belanja...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
document.getElementById('bl-bulk').value = '';
alert('Berhasil menyimpan ' + payloads.length + ' item.');
if (res && res.belanja) renderBelanja(res.belanja);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan belanja.\n' + (err && err.message ? err.message : err));
}).saveBelanjaBulk(payloads);
}
function syncBelanjaOneWay() {
var today = new Date().toISOString().split('T')[0];
if (!confirm('Sinkron 1 arah belanja tanggal ' + today + ' ke Dashboard (RekapTransaksi) dan Stok&Belanja?')) return;
showLoading(true, 'SYNC BELANJA...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
var msg = 'SYNC selesai.\n\nTanggal: ' + (res && res.date ? res.date : today) +
'\nItem lokal: ' + (res && res.localCount != null ? res.localCount : '-') +
'\nRekapTransaksi: ' + (res && res.rekap && res.rekap.ok ? ('OK (insert ' + res.rekap.inserted + ', update ' + res.rekap.updated + ', skip ' + res.rekap.skipped + ')') : ('GAGAL: ' + ((res && res.rekap && res.rekap.error) ? res.rekap.error : '-'))) +
'\nStok&Belanja: ' + (res && res.stokBelanja && res.stokBelanja.ok ? ('OK (belanja ' + res.stokBelanja.belanja + ', operasional ' + res.stokBelanja.operasional + ', pegawai ' + res.stokBelanja.pegawai + ', skip ' + res.stokBelanja.skipped + ')') : ('GAGAL: ' + ((res && res.stokBelanja && res.stokBelanja.error) ? res.stokBelanja.error : '-')));
alert(msg);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal sync.\n' + (err && err.message ? err.message : err));
}).syncBelanjaOut(today);
}
function editBelanja(id, dataStr) {
var data = JSON.parse(decodeURIComponent(dataStr));
document.getElementById('bl-id').value = data.id;
document.getElementById('bl-nama').value = data.nama;
document.getElementById('bl-kat').value = mapBelanjaKategori(data.kategori || 'Dapur');
document.getElementById('bl-qty').value = data.qty;
document.getElementById('bl-harga').value = data.harga;
document.getElementById('bl-total').value = data.total;
window.scrollTo({ top: 0, behavior: 'smooth' });
document.getElementById('bl-nama').focus();
}
function mapBelanjaKategori(k) {
var s = String(k || '').trim();
var allowed = { Modal: true, Bawah: true, Dapur: true, Makan: true, Operasional: true, Pegawai: true, QRIS: true };
if (allowed[s]) return s;
var upper = s.toLowerCase();
if (upper === 'bahan baku') return 'Dapur';
if (upper === 'gaji') return 'Pegawai';
if (upper === 'listrik/air' || upper === 'listrik & air') return 'Operasional';
if (upper === 'sewa') return 'Operasional';
if (upper === 'lainnya' || upper === 'lain-lain') return 'Operasional';
return 'Dapur';
}
function autoTitleCase(s) {
var str = String(s || '').trim().replace(/\s+/g, ' ');
if (!str) return '';
return str.split(' ').map(function(w) {
if (!w) return w;
if (/[0-9]/.test(w)) return w;
if (/^[A-Z0-9\-\._]+$/.test(w) && w.length <= 4) return w;
var lower = w.toLowerCase();
return lower.charAt(0).toUpperCase() + lower.slice(1);
}).join(' ');
}
function hapusBelanja(id) {
if (!confirm('Anda yakin ingin menghapus item belanja ini?')) return;
showLoading(true, 'Menghapus...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
alert('Item berhasil dihapus.');
if (res && res.belanja) renderBelanja(res.belanja);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menghapus belanja.\n' + (err && err.message ? err.message : err));
}).deleteBelanja(id);
}
// Kalkulator Logic
function calcAdd(v) { document.getElementById('calc-input').value += v; }
function calcClear() { document.getElementById('calc-input').value = ''; document.getElementById('calc-result').innerText = '= 0'; }
function calcEval() {
try {
var res = eval(document.getElementById('calc-input').value);
document.getElementById('calc-result').innerText = '= ' + (Number(res)||0).toLocaleString();
} catch(e) { alert('Format kalkulator salah'); }
}
function renderBelanja(data) {
currentBelanja = data; // Update currentBelanja whenever data is rendered
var box = document.getElementById('belanja-list');
box.innerHTML = '';
if (!data || data.length === 0) {
box.innerHTML = '<div class="muted">Belum ada data belanja.</div>';
return;
}
// Group by date
var grouped = {};
data.forEach(function(b) {
var tgl = b.tgl ? b.tgl.split(' ')[0] : 'Tanpa Tanggal';
if (!grouped[tgl]) grouped[tgl] = [];
grouped[tgl].push(b);
});
var sortedDates = Object.keys(grouped).sort().reverse();
sortedDates.forEach(function(tgl) {
var items = grouped[tgl].slice().reverse();
var totalDay = items.reduce(function(acc, b) { return acc + (Number(b.total) || 0); }, 0);
var header = document.createElement('div');
header.className = 'date-header';
header.innerHTML = '<span>' + tgl + '</span>' +
'<span class="total-day">Rp ' + totalDay.toLocaleString() + '</span>';
box.appendChild(header);
items.forEach(function(b) {
var div = document.createElement('div');
div.className = 'item-row';
var katLabel = b.kategori ? '<span class="item-kat">[' + b.kategori + ']</span>' : '';
var dataStr = encodeURIComponent(JSON.stringify(b));
div.innerHTML = '<div class="item-info">' +
katLabel +
'<span class="item-name">' + b.nama + '</span>' +
'<span class="item-detail">' + b.qty + ' x Rp ' + (Number(b.harga)||0).toLocaleString() + '</span>' +
'</div>' +
'<div style="text-align: right;">' +
'<div class="item-price">Rp ' + (Number(b.total)||0).toLocaleString() + '</div>' +
'<div style="margin-top: 5px;">' +
'<button class="btn" style="padding: 4px 10px; font-size: 10px; width: auto; background: #111; color: white; margin-right: 5px;" onclick="editBelanja(\'' + b.id + '\', \'' + dataStr + '\')">EDIT</button>' +
'<button class="btn" style="padding: 4px 10px; font-size: 10px; width: auto; background: #e11d48; color: white;" onclick="hapusBelanja(\'' + b.id + '\')">HAPUS</button>' +
'</div>' +
'</div>';
box.appendChild(div);
});
});
}
</script>
</body>
</html>

2721
POSFuku/Code.gs Normal file

File diff suppressed because it is too large Load Diff

486
POSFuku/Dapur.html Normal file
View File

@ -0,0 +1,486 @@
<!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>

7198
POSFuku/Index.html Normal file

File diff suppressed because it is too large Load Diff

1481
POSFuku/Pelanggan.html Normal file

File diff suppressed because it is too large Load Diff

312
POSFuku/Poin.html Normal file
View File

@ -0,0 +1,312 @@
<!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 - Cek Poin & Riwayat</title>
<style>
body { margin:0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background:#f4f7f6; color: #333; padding-bottom: 40px; -webkit-font-smoothing: antialiased; }
.header { background:#111; color:white; padding:15px; text-align:center; position:sticky; top:0; z-index:100; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.container { padding:12px; max-width: 600px; margin: 0 auto; }
.card { background:white; border-radius:12px; padding:15px; border: 1px solid #eee; margin-bottom:15px; transform: translateZ(0); }
.title { font-size: 16px; font-weight: 800; margin-bottom: 12px; color: #111; text-align: center; }
input { width:100%; padding:12px; margin-bottom:12px; border:1px solid #ccc; border-radius:10px; box-sizing:border-box; font-size:14px; outline: none; background: #fff; -webkit-appearance: none; }
.btn { width:100%; padding:14px; border:none; border-radius:12px; font-weight:800; cursor:pointer; font-size:14px; display: flex; align-items: center; justify-content: center; gap: 8px; }
.btn-primary { background:#e11d48; color:white; }
.btn:active { opacity: 0.7; }
.poin-badge { background:#fff1f2; color:#e11d48; padding:15px; border-radius:12px; text-align:center; margin-bottom:15px; border: 1px dashed #e11d48; }
.poin-val { font-size: 32px; font-weight: 900; display: block; margin-top: 5px; }
.history-item { border-bottom: 1px solid #f3f4f6; padding:12px 0; }
.history-item:last-child { border-bottom: none; }
.history-header { display:flex; justify-content:space-between; font-weight:800; font-size:13px; margin-bottom:5px; }
.history-id { font-size: 10px; color: #999; font-family: monospace; }
.history-status { font-size: 9px; padding: 3px 8px; border-radius: 6px; font-weight: 800; text-transform: uppercase; }
.st-selesai { background: #dcfce7; color: #166534; }
.star-rating { display: flex; justify-content: center; gap: 10px; margin: 15px 0; font-size: 32px; }
.star { cursor: pointer; color: #ddd; transition: 0.2s; }
.star.active { color: #f59e0b; }
textarea { width: 100%; height: 80px; padding: 12px; border: 1px solid #ccc; border-radius: 10px; box-sizing: border-box; font-family: inherit; font-size: 14px; margin-bottom: 12px; resize: none; outline: none; background: #fff; -webkit-appearance: none; }
.menu-list { font-size: 12px; color: #555; margin-top: 8px; background: #f9fafb; padding: 10px; border-radius: 10px; border: 1px solid #f1f5f9; }
.menu-row { display: flex; justify-content: space-between; margin-bottom: 4px; }
#loading { position:fixed; inset:0; background:#fff; display:none; flex-direction:column; justify-content:center; align-items:center; z-index:1000; }
.spinner { border:3px solid #f3f3f3; border-top:3px solid #e11d48; border-radius:50%; width:35px; height:35px; animation:spin 0.8s linear infinite; margin-bottom: 10px; }
@keyframes spin { 0% { transform:rotate(0deg); } 100% { transform:rotate(360deg); } }
.muted { color: #999; font-size: 11px; text-align: center; margin-top: 20px; }
.error-msg { color: #e11d48; font-size: 13px; text-align: center; margin-bottom: 12px; display: none; font-weight: 700; }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: #ddd; border-radius: 10px; }
</style>
</head>
<body>
<div id="loading"><div class="spinner"></div><p>Mencari Data...</p></div>
<div class="header">
<div id="store-name" style="font-weight:900; font-size:20px;">POS</div>
<div style="font-size:12px; opacity:0.8;">Cek Poin & Riwayat Transaksi</div>
</div>
<div class="container">
<!-- Form Login -->
<div id="login-section" class="card">
<div class="title">Masuk ke Akun Anda</div>
<div id="error-msg" class="error-msg">Nama atau nomor WA tidak ditemukan.</div>
<input id="login-wa" type="tel" placeholder="Masukkan Nomor WA (Contoh: 0812...)">
<button class="btn btn-primary" onclick="cekData()">LIHAT RIWAYAT SAYA</button>
<div class="muted">Bisa input dengan 08... atau 62...</div>
</div>
<!-- Hasil Data -->
<div id="result-section" style="display:none;">
<div class="card">
<div class="title">Informasi Pelanggan</div>
<div style="text-align:center; margin-bottom:15px;">
<div id="res-nama" style="font-weight:900; font-size:18px;"></div>
<div id="res-wa" style="font-size:14px; color:#666;"></div>
</div>
<div class="poin-badge">
<span style="font-size:12px; font-weight:bold; text-transform:uppercase; letter-spacing:1px;">Total Poin Anda</span>
<span id="res-poin" class="poin-val">0</span>
</div>
<div id="review-box" style="margin-top:12px; padding:12px; border-radius:12px; border:1px solid #e2e8f0; background:#f8fafc;">
<div style="font-weight:900; font-size:12px; margin-bottom:6px; color:#111;">Status Review</div>
<div id="review-status" class="muted">Belum dicek.</div>
</div>
<!-- Feedback Form -->
<div id="feedback-section" style="border-top: 1px solid #eee; padding-top: 15px; margin-top: 15px;">
<div class="title" style="font-size: 14px;">Bagaimana pengalaman Anda?</div>
<div class="star-rating">
<span class="star" onclick="setRating(1)"></span>
<span class="star" onclick="setRating(2)"></span>
<span class="star" onclick="setRating(3)"></span>
<span class="star" onclick="setRating(4)"></span>
<span class="star" onclick="setRating(5)"></span>
</div>
<textarea id="fb-komentar" placeholder="Berikan saran atau kesan Anda (opsional)..."></textarea>
<button id="btn-feedback" class="btn btn-primary" onclick="kirimFeedback()">KIRIM FEEDBACK</button>
<div id="fb-success" style="display:none; text-align:center; color:#16a34a; font-weight:bold; font-size:13px;">Terima kasih atas feedback Anda!</div>
</div>
<button class="btn" style="background:#f3f4f6; color:#666; margin-top: 20px;" onclick="location.reload()">LOGOUT / KEMBALI</button>
</div>
<div class="card">
<div class="title">Riwayat Transaksi</div>
<div id="history-list"></div>
</div>
</div>
</div>
<script>
var currentRating = 0;
var currentCustomer = null;
var storeName = 'POS';
window.onload = function() {
google.script.run.withSuccessHandler(function(res) {
if (res && res.settings) {
storeName = String(res.settings.store_name || storeName);
var el = document.getElementById('store-name');
if (el) el.innerText = storeName;
try { document.title = storeName + ' - Cek Poin & Riwayat'; } catch(e) {}
}
}).getInitialData();
};
function showLoading(s, text) {
document.getElementById('loading').style.display = s ? 'flex' : 'none';
if (text) document.querySelector('#loading p').innerText = text;
else document.querySelector('#loading p').innerText = 'Mencari Data...';
}
function normalizeWA(wa) {
var clean = String(wa || '').replace(/\D/g, '');
if (clean.indexOf('0') === 0) clean = '62' + clean.slice(1);
else if (clean.indexOf('8') === 0) clean = '62' + clean;
return clean;
}
function cekData() {
var wa = document.getElementById('login-wa').value.trim();
if (!wa) { alert('Mohon isi Nomor WA!'); return; }
var cleanWA = normalizeWA(wa);
showLoading(true);
document.getElementById('error-msg').style.display = 'none';
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res && res.settings) {
storeName = String(res.settings.store_name || storeName);
var el = document.getElementById('store-name');
if (el) el.innerText = storeName;
try { document.title = storeName + ' - Cek Poin & Riwayat'; } catch(e) {}
}
if (res && res.pelanggan) {
displayResult(res.pelanggan, res.transaksi, cleanWA);
} else {
document.getElementById('error-msg').style.display = 'block';
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal memuat data poin.\n' + (err && err.message ? err.message : err));
}).getInitialData();
}
function displayResult(allPelanggan, allHistory, targetWa) {
var p = allPelanggan.find(function(x) {
return normalizeWA(x.wa) === targetWa;
}) || null;
var entries = allHistory.filter(function(h) {
return normalizeWA(h.wa) === targetWa && (h.status === 'Selesai' || h.status === 'Review' || String(h.buktiReview || '').trim());
}).sort(function(a,b) {
return String(b.timestamp || b.tgl || '').localeCompare(String(a.timestamp || a.tgl || ''));
});
if (!p && entries.length === 0) {
document.getElementById('error-msg').style.display = 'block';
return;
}
if (!p) p = { nama: '-', wa: document.getElementById('login-wa').value.trim(), poin: 0 };
currentCustomer = p;
document.getElementById('login-section').style.display = 'none';
document.getElementById('result-section').style.display = 'block';
document.getElementById('res-nama').innerText = p.nama;
document.getElementById('res-wa').innerText = p.wa;
document.getElementById('res-poin').innerText = Number(p.poin).toLocaleString();
var reviewBox = document.getElementById('review-status');
if (reviewBox) {
var reviewHistory = entries.filter(function(h) {
return (String(h.buktiReview || '').trim() || String(h.status || '') === 'Review');
});
if (reviewHistory.length === 0) {
reviewBox.innerHTML = 'Belum ada review yang tercatat.';
} else {
var latest = reviewHistory[0];
var url = String(latest.buktiReview || '').trim();
var html = 'Sudah pernah review: <b>' + reviewHistory.length + 'x</b>';
if (url) {
html += '<div style="margin-top:8px;"><a href="' + url + '" target="_blank" style="display:inline-block; padding:10px 12px; border-radius:10px; background:#111; color:#fff; text-decoration:none; font-weight:900; font-size:12px;">LIHAT BUKTI REVIEW</a></div>';
}
reviewBox.innerHTML = html;
}
}
var historyBox = document.getElementById('history-list');
historyBox.innerHTML = '';
if (entries.length === 0) {
historyBox.innerHTML = '<div class="muted">Belum ada riwayat.</div>';
return;
}
entries.forEach(function(h) {
var div = document.createElement('div');
div.className = 'history-item';
var itemsHtml = '';
if (h.status === 'Selesai' && h.items && h.items.length) {
itemsHtml = '<div class="menu-list">';
h.items.forEach(function(it) {
itemsHtml += '<div class="menu-row"><span>' + it.qty + 'x ' + it.nama + '</span><span>Rp ' + (it.qty * it.harga).toLocaleString() + '</span></div>';
});
// Tambahkan baris pemakaian poin jika ada
var poinUsed = Number(h.poinDipakai || 0);
if (poinUsed > 0) {
itemsHtml += '<div class="menu-row" style="color:#e11d48; border-top:1px dashed #ddd; margin-top:4px; padding-top:4px;">' +
'<span>Poin Terpakai</span>' +
'<span>-Rp ' + poinUsed.toLocaleString() + '</span>' +
'</div>';
}
var poinGet = Number(h.poinDapat || 0);
if (poinGet > 0) {
itemsHtml += '<div class="menu-row" style="color:#16a34a; border-top:1px dashed #ddd; margin-top:4px; padding-top:4px;">' +
'<span>Poin Didapat</span>' +
'<span>+' + poinGet.toLocaleString() + '</span>' +
'</div>';
}
itemsHtml += '</div>';
}
var dateText = (h.timestamp ? h.timestamp.split(' ')[0] : (h.tgl ? String(h.tgl).split(' ')[0] : '-'));
var isReview = (String(h.status || '') === 'Review') || !!String(h.buktiReview || '').trim();
var statusLabel = isReview ? '<span class="history-status" style="background:#111; color:#fff;">REVIEW</span>' : '<span class="history-status st-selesai">SELESAI</span>';
var rightVal = isReview ? '' : '<span style="font-weight:900; color:#e11d48;">Rp ' + Number(h.total || 0).toLocaleString() + '</span>';
var reviewUrl = String(h.buktiReview || '').trim();
var reviewBtn = reviewUrl ? '<div style="margin-top:10px;"><a href="' + reviewUrl + '" target="_blank" style="display:inline-block; padding:10px 12px; border-radius:10px; background:#111; color:#fff; text-decoration:none; font-weight:900; font-size:12px;">LIHAT BUKTI REVIEW</a></div>' : '';
div.innerHTML = '<div class="history-header">' +
'<span>' + dateText + '</span>' +
statusLabel +
'</div>' +
'<div style="display:flex; justify-content:space-between; align-items:center;">' +
'<span class="history-id">ID: ' + h.id + '</span>' +
rightVal +
'</div>' +
itemsHtml +
(isReview ? reviewBtn : (reviewUrl ? reviewBtn : ''));
historyBox.appendChild(div);
});
}
function setRating(n) {
currentRating = n;
var stars = document.querySelectorAll('.star');
stars.forEach(function(s, idx) {
if (idx < n) s.classList.add('active');
else s.classList.remove('active');
});
}
function kirimFeedback() {
if (currentRating === 0) {
alert('Mohon berikan rating bintang!');
return;
}
var komentar = document.getElementById('fb-komentar').value.trim();
var payload = {
nama: currentCustomer.nama,
wa: currentCustomer.wa,
rating: currentRating,
komentar: komentar
};
showLoading(true, 'Mengirim Feedback...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (res.success) {
document.getElementById('btn-feedback').style.display = 'none';
document.querySelectorAll('.star-rating, textarea').forEach(function(el) { el.style.pointerEvents = 'none'; el.style.opacity = '0.6'; });
document.getElementById('fb-success').style.display = 'block';
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal mengirim feedback.\n' + (err && err.message ? err.message : err));
}).saveFeedback(payload);
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

634
RekapTransaksi/Code.gs Normal file
View File

@ -0,0 +1,634 @@
function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle('Dashboard Laporan Fuku Shabu & Grill')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function authDigestBase64_(s) {
var bytes = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, String(s || ''), Utilities.Charset.UTF_8);
return Utilities.base64Encode(bytes);
}
function rekapLogin(action, username, password) {
var a = String(action || '').trim().toLowerCase();
var u = String(username || '').trim().toLowerCase();
var p = String(password || '');
if (!a || !u || !p) return { ok: false };
var db = {
admin: {
salt: 'rKkQ4v3Y8nP1zT6a',
hashes: [
'W54PYq8f2cj1Q/wOsZWc6gV7Aj47ZnpT/dxXmzL7CV8=',
'zrBzOp9x7UZlrE78NqoRFlichiZQrBDvY3HBVf1y1zY='
]
},
ridho: {
salt: 'H2m9Qp7sV4c1X0bN',
hashes: ['UNnM/O3rcanZCQEjL/tfZldwUBuSPRK4X11funwwcKw=']
},
mamah: {
salt: 'fZ8tK3pL0n2Qm7vR',
hashes: ['OcjVm5Bhz19P9kSqsUu+lUWl7uEu9DXtPLt2fnwdxyk=']
}
};
var allow = {
dashboard: { admin: ['W54PYq8f2cj1Q/wOsZWc6gV7Aj47ZnpT/dxXmzL7CV8='] },
kasir: { admin: ['zrBzOp9x7UZlrE78NqoRFlichiZQrBDvY3HBVf1y1zY='], ridho: ['UNnM/O3rcanZCQEjL/tfZldwUBuSPRK4X11funwwcKw='] },
stok: { admin: ['zrBzOp9x7UZlrE78NqoRFlichiZQrBDvY3HBVf1y1zY='], ridho: ['UNnM/O3rcanZCQEjL/tfZldwUBuSPRK4X11funwwcKw='], mamah: ['OcjVm5Bhz19P9kSqsUu+lUWl7uEu9DXtPLt2fnwdxyk='] }
};
if (!allow[a] || !allow[a][u]) return { ok: false };
if (!db[u] || !db[u].salt) return { ok: false };
var expectedHashes = allow[a][u];
var actualHash = authDigestBase64_(db[u].salt + ':' + p);
if (expectedHashes.indexOf(actualHash) === -1) return { ok: false };
return { ok: true, action: a, username: u };
}
function getDashboardData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetTrans = ss.getSheetByName('Transaksi');
var sheetBelanja = ss.getSheetByName('Belanja');
var transData = sheetTrans ? sheetTrans.getDataRange().getValues() : [];
var belanjaData = sheetBelanja ? sheetBelanja.getDataRange().getValues() : [];
var totalSales = 0;
var totalTransactions = 0;
var methods = {};
var dailyTrans = {};
var itemCounts = {};
// Proses Transaksi
if (transData.length > 1) {
transData.slice(1).forEach(function(row) {
var status = String(row[2] || '');
if (status !== 'Selesai') return;
var total = Number(row[17] || 0);
var method = String(row[21] || 'Tunai');
var dateObj = row[25] || row[5];
var dateStr = dateObj instanceof Date ? Utilities.formatDate(dateObj, "GMT+7", "yyyy-MM-dd") : String(dateObj).split(' ')[0];
totalSales += total;
totalTransactions++;
methods[method] = (methods[method] || 0) + total;
dailyTrans[dateStr] = (dailyTrans[dateStr] || 0) + total;
try {
var items = JSON.parse(row[7] || '[]');
items.forEach(function(it) {
itemCounts[it.nama] = (itemCounts[it.nama] || 0) + (Number(it.qty) || 0);
});
} catch (e) {}
});
}
// Proses Belanja
var totalBelanja = 0;
var dailyBelanja = {};
var kategoriBelanja = {};
var belanjaList = [];
if (belanjaData.length > 1) {
belanjaData.slice(1).forEach(function(row) {
var nama = String(row[1] || '');
var harga = Number(row[2] || 0);
var qty = Number(row[3] || 0);
var total = Number(row[4] || 0);
var dateObj = row[5];
var kat = String(row[6] || 'Lainnya');
var dateStr = dateObj instanceof Date ? Utilities.formatDate(dateObj, "GMT+7", "yyyy-MM-dd") : String(dateObj).split(' ')[0];
totalBelanja += total;
dailyBelanja[dateStr] = (dailyBelanja[dateStr] || 0) + total;
kategoriBelanja[kat] = (kategoriBelanja[kat] || 0) + total;
belanjaList.push({ nama: nama, total: total, kat: kat, tgl: dateStr });
});
}
// Sort data
var sortedItems = Object.keys(itemCounts).map(function(k) { return [k, itemCounts[k]]; }).sort(function(a, b) { return b[1] - a[1]; }).slice(0, 10);
var sortedDates = Array.from(new Set(Object.keys(dailyTrans).concat(Object.keys(dailyBelanja)))).sort();
var dailyStats = sortedDates.map(function(d) {
return { date: d, sales: dailyTrans[d] || 0, expense: dailyBelanja[d] || 0 };
});
return {
summary: {
totalSales: totalSales,
totalBelanja: totalBelanja,
netProfit: totalSales - totalBelanja,
totalTransactions: totalTransactions,
avgTransaction: totalTransactions > 0 ? (totalSales / totalTransactions) : 0
},
methods: methods,
daily: dailyStats,
topItems: sortedItems,
kategoriBelanja: kategoriBelanja,
topBelanja: belanjaList.sort(function(a,b){ return b.total - a.total; }).slice(0, 10)
};
}
function ensureHeaderRow_(sheet, headers) {
if (!sheet) return;
var h = headers || [];
if (!h.length) return;
if (sheet.getLastRow() < 1) sheet.appendRow(h);
var first = sheet.getRange(1, 1, 1, h.length).getValues()[0];
var ok = true;
for (var i = 0; i < h.length; i++) {
if (String(first[i] || '').trim() !== String(h[i] || '').trim()) { ok = false; break; }
}
if (!ok) {
sheet.getRange(1, 1, 1, h.length).setValues([h]);
}
sheet.getRange(1, 1, 1, h.length).setFontWeight('bold').setBackground('#f3f3f3');
}
function setupCalendarNotesSheet_() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('KalenderNotes');
if (!sheet) sheet = ss.insertSheet('KalenderNotes');
ensureHeaderRow_(sheet, ['ID', 'Tanggal', 'Judul', 'Catatan', 'User', 'Timestamp']);
sheet.getRange('B:B').setNumberFormat('@');
sheet.getRange('F:F').setNumberFormat('yyyy-mm-dd hh:mm:ss');
return sheet;
}
function getCalendarNotesByDate(dateStr) {
var d = normalizeDateStr_(dateStr);
var sheet = setupCalendarNotesSheet_();
var lastRow = sheet.getLastRow();
if (lastRow <= 1) return [];
var data = sheet.getRange(2, 1, lastRow - 1, 6).getValues();
var out = [];
for (var i = 0; i < data.length; i++) {
var r = data[i];
var tgl = toDateStr_(r[1]);
if (tgl !== d) continue;
out.push({
id: String(r[0] || ''),
tanggal: tgl,
judul: String(r[2] || ''),
catatan: String(r[3] || ''),
user: String(r[4] || ''),
timestamp: r[5] instanceof Date ? Utilities.formatDate(r[5], 'GMT+7', 'yyyy-MM-dd HH:mm:ss') : String(r[5] || '')
});
}
out.sort(function(a, b) { return String(b.timestamp || '').localeCompare(String(a.timestamp || '')); });
return out;
}
function saveCalendarNote(note, userName) {
var sheet = setupCalendarNotesSheet_();
var now = new Date();
var payload = note || {};
var id = String(payload.id || '').trim();
var tgl = normalizeDateStr_(payload.tanggal);
var judul = String(payload.judul || '').trim();
var catatan = String(payload.catatan || '').trim();
if (!judul && !catatan) throw new Error('Judul atau catatan harus diisi.');
var user = String(userName || payload.user || '');
var lastRow = sheet.getLastRow();
var foundRow = -1;
if (id && lastRow > 1) {
var data = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (var i = 0; i < data.length; i++) {
if (String(data[i][0] || '') === id) { foundRow = i + 2; break; }
}
}
if (!id) id = 'CAL-' + Date.now() + '-' + Math.floor(Math.random() * 1000);
var row = [id, tgl, judul, catatan, user || 'Unknown', now];
if (foundRow > -1) {
sheet.getRange(foundRow, 1, 1, row.length).setValues([row]);
} else {
sheet.appendRow(row);
}
return getCalendarNotesByDate(tgl);
}
function deleteCalendarNote(id) {
var sheet = setupCalendarNotesSheet_();
var target = String(id || '').trim();
if (!target) return true;
var lastRow = sheet.getLastRow();
if (lastRow <= 1) return true;
var data = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (var i = data.length - 1; i >= 0; i--) {
if (String(data[i][0] || '') === target) sheet.deleteRow(i + 2);
}
return true;
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
function normalizeDateStr_(s) {
var str = String(s || '').trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str;
return Utilities.formatDate(new Date(), 'GMT+7', 'yyyy-MM-dd');
}
function normalizeRange_(startDate, endDate) {
var s = normalizeDateStr_(startDate);
var e = normalizeDateStr_(endDate);
if (s > e) { var t = s; s = e; e = t; }
return { start: s, end: e };
}
function toDateStr_(val) {
if (!val) return '';
if (val instanceof Date) return Utilities.formatDate(val, 'GMT+7', 'yyyy-MM-dd');
var s = String(val);
if (!s) return '';
return s.split(' ')[0].split('T')[0];
}
function toHour_(val) {
if (!val) return -1;
if (val instanceof Date) return val.getHours();
var s = String(val);
if (!s) return -1;
var m = s.match(/(\d{1,2}):(\d{2})/);
if (m && m[1]) return Math.min(23, Math.max(0, parseInt(m[1], 10)));
return -1;
}
function daysBetweenInclusive_(startDate, endDate) {
var s = new Date(startDate + 'T00:00:00');
var e = new Date(endDate + 'T00:00:00');
var ms = e.getTime() - s.getTime();
if (isNaN(ms)) return 1;
return Math.max(1, Math.floor(ms / 86400000) + 1);
}
function normalizePorsiBaseName_(name) {
var s = String(name || '').trim();
if (!s) return '';
return s.replace(/^->\s*/, '').trim();
}
function getRekapByDate(dateStr) {
var d = String(dateStr || '').trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) {
d = Utilities.formatDate(new Date(), "GMT+7", "yyyy-MM-dd");
}
return buildRekap_(d, d);
}
function getRekapByRange(startDate, endDate) {
var s = String(startDate || '').trim();
var e = String(endDate || '').trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(s) || !/^\d{4}-\d{2}-\d{2}$/.test(e)) {
throw new Error('Format tanggal tidak valid. Gunakan YYYY-MM-DD.');
}
if (s > e) {
var tmp = s; s = e; e = tmp;
}
return buildRekap_(s, e);
}
function getSalesAnalyticsByRange(startDate, endDate) {
var r = normalizeRange_(startDate, endDate);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetTrans = ss.getSheetByName('Transaksi');
if (!sheetTrans || sheetTrans.getLastRow() <= 1) {
return {
range: { start: r.start, end: r.end },
summary: { totalSales: 0, totalTransactions: 0, avgTransaction: 0 },
methods: {},
dailySales: [],
topItems: [],
hourlyCounts: new Array(24).fill(0),
buckets: { lt100: 0, gte100: 0, gte200: 0 },
avgMenuPerDay: []
};
}
var lastRow = sheetTrans.getLastRow();
var data = sheetTrans.getRange(2, 1, lastRow - 1, Math.max(30, sheetTrans.getLastColumn())).getValues();
var totalSales = 0;
var totalTransactions = 0;
var methods = {};
var dailyTrans = {};
var itemCounts = {};
var hourlyCounts = new Array(24).fill(0);
var buckets = { lt100: 0, gte100: 0, gte200: 0 };
data.forEach(function(row) {
var status = String(row[2] || '');
if (status !== 'Selesai') return;
var dateObj = row[25] || row[5];
var dateStr = toDateStr_(dateObj);
if (!dateStr || dateStr < r.start || dateStr > r.end) return;
var total = Number(row[17] || 0);
totalSales += total;
totalTransactions++;
if (total < 100000) buckets.lt100++;
if (total >= 100000) buckets.gte100++;
if (total >= 200000) buckets.gte200++;
var method = String(row[21] || 'Tunai');
if (method === 'Multi') {
var catatan = String(row[27] || '');
var details = {};
try { details = JSON.parse(catatan || '{}'); } catch (e) { details = {}; }
Object.keys(details || {}).forEach(function(k) {
methods[k] = (methods[k] || 0) + (Number(details[k]) || 0);
});
} else {
methods[method] = (methods[method] || 0) + total;
}
dailyTrans[dateStr] = (dailyTrans[dateStr] || 0) + total;
var hour = toHour_(row[25] || row[6] || row[5]);
if (hour >= 0) hourlyCounts[hour] = (hourlyCounts[hour] || 0) + 1;
try {
var items = JSON.parse(row[7] || '[]');
items.forEach(function(it) {
var nmRaw = String(it.nama || '').trim();
if (!nmRaw) return;
var nm = normalizePorsiBaseName_(nmRaw) || nmRaw;
itemCounts[nm] = (itemCounts[nm] || 0) + (Number(it.qty) || 0);
});
} catch (e) {}
});
var dailySales = Object.keys(dailyTrans).sort().map(function(d) { return { date: d, sales: dailyTrans[d] || 0 }; });
var topItems = Object.keys(itemCounts).map(function(k) { return [k, itemCounts[k]]; }).sort(function(a, b) { return b[1] - a[1]; }).slice(0, 10);
var dayCount = daysBetweenInclusive_(r.start, r.end);
var avgMenuPerDay = Object.keys(itemCounts).map(function(k) {
return { nama: k, avg: (itemCounts[k] || 0) / dayCount, total: itemCounts[k] || 0 };
}).sort(function(a, b) { return b.avg - a.avg; }).slice(0, 10);
return {
range: { start: r.start, end: r.end },
summary: { totalSales: totalSales, totalTransactions: totalTransactions, avgTransaction: totalTransactions > 0 ? (totalSales / totalTransactions) : 0 },
methods: methods,
dailySales: dailySales,
topItems: topItems,
hourlyCounts: hourlyCounts,
buckets: buckets,
avgMenuPerDay: avgMenuPerDay
};
}
function getExpenseAnalyticsByRange(startDate, endDate) {
var r = normalizeRange_(startDate, endDate);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetBelanja = ss.getSheetByName('Belanja');
if (!sheetBelanja || sheetBelanja.getLastRow() <= 1) {
return { range: { start: r.start, end: r.end }, summary: { totalBelanja: 0, maxBelanja: 0 }, dailyBelanja: [], kategoriBelanja: {}, topBelanja: [] };
}
var lastRow = sheetBelanja.getLastRow();
var data = sheetBelanja.getRange(2, 1, lastRow - 1, Math.max(9, sheetBelanja.getLastColumn())).getValues();
var totalBelanja = 0;
var dailyBelanja = {};
var kategoriBelanja = {};
var belanjaList = [];
data.forEach(function(row) {
var dateStr = toDateStr_(row[5]);
if (!dateStr || dateStr < r.start || dateStr > r.end) return;
var nama = String(row[1] || '');
var total = Number(row[4] || 0);
var kat = String(row[6] || 'Lainnya');
totalBelanja += total;
dailyBelanja[dateStr] = (dailyBelanja[dateStr] || 0) + total;
kategoriBelanja[kat] = (kategoriBelanja[kat] || 0) + total;
belanjaList.push({ nama: nama, total: total, kat: kat, tgl: dateStr });
});
var daily = Object.keys(dailyBelanja).sort().map(function(d) { return { date: d, total: dailyBelanja[d] || 0 }; });
var topBelanja = belanjaList.sort(function(a, b) { return b.total - a.total; }).slice(0, 10);
var maxBelanja = topBelanja.length ? topBelanja[0].total : 0;
return { range: { start: r.start, end: r.end }, summary: { totalBelanja: totalBelanja, maxBelanja: maxBelanja }, dailyBelanja: daily, kategoriBelanja: kategoriBelanja, topBelanja: topBelanja };
}
function getDashboardBundleByRange(startDate, endDate) {
var sales = getSalesAnalyticsByRange(startDate, endDate);
var exp = getExpenseAnalyticsByRange(startDate, endDate);
var profit = Number((sales && sales.summary && sales.summary.totalSales) || 0) - Number((exp && exp.summary && exp.summary.totalBelanja) || 0);
return { range: sales.range, sales: sales, expense: exp, profit: profit };
}
function getTransaksiHistoryPage(startDate, endDate, query, offset, limit) {
var r = normalizeRange_(startDate, endDate);
var q = String(query || '').trim().toLowerCase();
var off = Math.max(0, Number(offset) || 0);
var lim = Math.min(300, Math.max(20, Number(limit) || 50));
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetTrans = ss.getSheetByName('Transaksi');
if (!sheetTrans || sheetTrans.getLastRow() <= 1) return { range: { start: r.start, end: r.end }, items: [], offset: off, nextOffset: off, hasMore: false };
var lastRow = sheetTrans.getLastRow();
var data = sheetTrans.getRange(2, 1, lastRow - 1, Math.max(30, sheetTrans.getLastColumn())).getValues();
var out = [];
var skipped = 0;
var stoppedAt = -1;
for (var i = data.length - 1; i >= 0; i--) {
var row = data[i];
var status = String(row[2] || '');
var id = String(row[0] || '');
if (!id) continue;
if (!status) continue;
var dateStr = toDateStr_(row[25] || row[5]);
if (!dateStr || dateStr < r.start || dateStr > r.end) continue;
var meja = String(row[1] || '');
var nama = String(row[3] || '');
var wa = String(row[4] || '');
var metode = String(row[21] || '');
var total = Number(row[17] || 0);
var ts = row[25] || row[5];
var tsStr = (ts instanceof Date) ? Utilities.formatDate(ts, 'GMT+7', 'yyyy-MM-dd HH:mm:ss') : String(ts || '');
if (q) {
var hay = (id + ' ' + meja + ' ' + nama + ' ' + wa + ' ' + metode + ' ' + status).toLowerCase();
if (hay.indexOf(q) === -1) continue;
}
if (skipped < off) { skipped++; continue; }
var items = [];
try { items = JSON.parse(row[7] || '[]'); } catch (e) { items = []; }
out.push({ id: id, meja: meja, status: status, nama: nama, wa: wa, metodeBayar: metode, total: total, timestamp: tsStr, tgl: dateStr, items: items, catatan: String(row[27] || '') });
if (out.length >= lim) { stoppedAt = i - 1; break; }
}
var nextOffset = off + out.length;
var hasMore = false;
if (stoppedAt >= 0) {
for (var j = stoppedAt; j >= 0; j--) {
var row2 = data[j];
var status2 = String(row2[2] || '');
var id2 = String(row2[0] || '');
if (!id2) continue;
if (!status2) continue;
var dateStr2 = toDateStr_(row2[25] || row2[5]);
if (!dateStr2 || dateStr2 < r.start || dateStr2 > r.end) continue;
var meja2 = String(row2[1] || '');
var nama2 = String(row2[3] || '');
var wa2 = String(row2[4] || '');
var metode2 = String(row2[21] || '');
if (q) {
var hay2 = (id2 + ' ' + meja2 + ' ' + nama2 + ' ' + wa2 + ' ' + metode2 + ' ' + status2).toLowerCase();
if (hay2.indexOf(q) === -1) continue;
}
hasMore = true;
break;
}
}
return { range: { start: r.start, end: r.end }, items: out, offset: off, nextOffset: nextOffset, hasMore: hasMore };
}
function buildRekap_(startDate, endDate) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetTrans = ss.getSheetByName('Transaksi');
var sheetBelanja = ss.getSheetByName('Belanja');
var transData = sheetTrans ? sheetTrans.getDataRange().getValues() : [];
var belanjaData = sheetBelanja ? sheetBelanja.getDataRange().getValues() : [];
var methods = { 'Tunai': 0, 'QRIS': 0, 'Debit': 0, 'Credit': 0, 'Transfer': 0 };
var portions = {};
var totalNota = 0;
var totalSales = 0;
var totalBelanja = 0;
if (transData.length > 1) {
transData.slice(1).forEach(function(row) {
var status = String(row[2] || '');
if (status !== 'Selesai') return;
var dateObj = row[25] || row[5];
var dateStr = dateObj instanceof Date ? Utilities.formatDate(dateObj, "GMT+7", "yyyy-MM-dd") : String(dateObj).split(' ')[0];
if (dateStr < startDate || dateStr > endDate) return;
totalNota++;
var total = Number(row[17] || 0);
totalSales += total;
var method = String(row[21] || 'Tunai');
if (method === 'Multi') {
var catatan = String(row[27] || '');
var details = {};
try { details = JSON.parse(catatan || '{}'); } catch (e) { details = {}; }
Object.keys(details || {}).forEach(function(k) {
if (methods.hasOwnProperty(k)) methods[k] += (Number(details[k]) || 0);
});
} else {
if (methods.hasOwnProperty(method)) methods[method] += total;
else methods[method] = (methods[method] || 0) + total;
}
try {
var items = JSON.parse(row[7] || '[]');
items.forEach(function(it) {
var nm = String(it.nama || '').trim();
if (!nm) return;
portions[nm] = (portions[nm] || 0) + (Number(it.qty) || 0);
});
} catch (e) {}
});
}
if (belanjaData.length > 1) {
belanjaData.slice(1).forEach(function(row) {
var dateObj = row[5];
var dateStr = dateObj instanceof Date ? Utilities.formatDate(dateObj, "GMT+7", "yyyy-MM-dd") : String(dateObj).split(' ')[0];
if (dateStr < startDate || dateStr > endDate) return;
totalBelanja += (Number(row[4] || 0));
});
}
var sortedPortions = Object.keys(portions).map(function(k) { return { nama: k, qty: portions[k] }; })
.sort(function(a, b) { return b.qty - a.qty; });
return {
startDate: startDate,
endDate: endDate,
isRange: startDate !== endDate,
summary: {
totalNota: totalNota,
totalSales: totalSales,
totalBelanja: totalBelanja,
netProfit: totalSales - totalBelanja
},
methods: methods,
portions: sortedPortions
};
}
function importTransactions(data) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Transaksi');
if (!sheet) throw new Error('Sheet Transaksi tidak ditemukan.');
var existingIds = [];
if (sheet.getLastRow() > 1) {
existingIds = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues().map(function(r) { return String(r[0]); });
}
var rowsToAdd = [];
data.forEach(function(t) {
if (existingIds.indexOf(String(t[0])) > -1) return; // Index 0 adalah ID
// Pastikan format tanggal benar
var row = t.map(function(val, idx) {
if ((idx === 5 || idx === 20 || idx === 25) && val) { // Kolom Tanggal, Tgl DP, Timestamp
return new Date(val);
}
return val;
});
// Pastikan panjang baris sesuai (28 kolom)
while (row.length < 28) row.push('');
rowsToAdd.push(row);
});
if (rowsToAdd.length > 0) {
sheet.getRange(sheet.getLastRow() + 1, 1, rowsToAdd.length, 28).setValues(rowsToAdd);
}
return { success: true, count: rowsToAdd.length };
}
function importBelanja(data) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Belanja');
if (!sheet) throw new Error('Sheet Belanja tidak ditemukan.');
var rowsToAdd = [];
data.forEach(function(b) {
// Pastikan format tanggal/timestamp benar
var row = b.map(function(val, idx) {
if ((idx === 5 || idx === 8) && val) { // Kolom Tanggal, Timestamp
return new Date(val);
}
return val;
});
// Pastikan panjang baris sesuai (9 kolom)
while (row.length < 9) row.push('');
rowsToAdd.push(row);
});
if (rowsToAdd.length > 0) {
sheet.getRange(sheet.getLastRow() + 1, 1, rowsToAdd.length, 9).setValues(rowsToAdd);
}
return { success: true, count: rowsToAdd.length };
}

1148
RekapTransaksi/Index.html Normal file

File diff suppressed because it is too large Load Diff