39735-vm/POSFuku/Belanja.html
Flatlogic Bot 8c2a5d487c POSFuku_v2
2026-04-19 12:46:29 +00:00

504 lines
25 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#111111">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>POS - Riwayat Belanja</title>
<script src="/local-preview-bridge.js"></script>
<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>