Initial import
This commit is contained in:
commit
57edfd794c
502
POSFuku/Belanja.html
Normal file
502
POSFuku/Belanja.html
Normal 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 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
2721
POSFuku/Code.gs
Normal file
File diff suppressed because it is too large
Load Diff
486
POSFuku/Dapur.html
Normal file
486
POSFuku/Dapur.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>') + '</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
7198
POSFuku/Index.html
Normal file
File diff suppressed because it is too large
Load Diff
1481
POSFuku/Pelanggan.html
Normal file
1481
POSFuku/Pelanggan.html
Normal file
File diff suppressed because it is too large
Load Diff
312
POSFuku/Poin.html
Normal file
312
POSFuku/Poin.html
Normal 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>
|
||||
1192
POSFuku/Stok & Belanja/Code.gs
Normal file
1192
POSFuku/Stok & Belanja/Code.gs
Normal file
File diff suppressed because it is too large
Load Diff
2113
POSFuku/Stok & Belanja/Index.html
Normal file
2113
POSFuku/Stok & Belanja/Index.html
Normal file
File diff suppressed because it is too large
Load Diff
634
RekapTransaksi/Code.gs
Normal file
634
RekapTransaksi/Code.gs
Normal 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
1148
RekapTransaksi/Index.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user