KASBI
Keuangan Bisnis Cerdas
Terhubung ke Server
Created by TrezarRG © 2026
|
KELUAR

KASBI

Keuangan Usaha Cerdas
Omset
0
Laba Kotor
0
Total Biaya
0
Laba Bersih
0
Grafik Omset & Profit
Produk Terlaris

GRAFIK PROFIT MINGGUAN
Rp 0
KEMBALI / SISA Rp 0
Semua Piutang Biaya
💡 Kalkulator HPP & Margin
Hitung modal bersih 1 Resep/Adonan.
TOTAL MODAL 1 RESEP Rp 0
Rp 0
1. Pilih Target Pasar

Edit angka di atas jika ingin harga manual
%
HARGA DI MENU / STRUK

Rp 0

(Termasuk Pajak)
Profit akan muncul disini...
Buku Resep Digital
Arsip resep & perhitungan modal.
Catat Pengeluaran
RIWAYAT Total: Rp 0
Gudang & Stok
Input hasil produksi / stok masuk

*Gunakan angka negatif (cth: -5) untuk barang rusak/keluar.
Identitas & Nota

Pilih batas usia data transaksi yang ingin dipindahkan ke Sheet "Arsip" agar aplikasi kembali ngebut.
`; // KODE PERINGATAN POP-UP BAWAAN ANDA (TETAP AMAN) let w = window.open('', '_blank', 'width=350,height=600'); if (!w) { Swal.fire({ title: 'Pop-up Diblokir!', html: 'Browser mencegah struk terbuka.
Silakan lihat pojok kanan atas browser (kolom URL), klik ikon , lalu pilih "Selalu Izinkan" (Always Allow).', icon: 'warning', confirmButtonColor: '#0f172a' }); return; } w.document.write(htmlContent); w.document.close(); } function reprintOrder(id, detailStr, total, bayar, sisa, cust, tipe, nmKasir) { let items = []; let parts = detailStr.split(', '); parts.forEach(p => { let m = p.match(/(.+)\s\((\d+)\)/); if(m) { let prodData = DB.products.find(x => x[0] === m[1]); items.push({ nama: m[1], jumlah: parseInt(m[2]), jual: prodData ? prodData[2] : 0 }); } else { items.push({ nama: p, jumlah: 1, jual: 0 }); } }); let d = new Date(); let timeStr = String(d.getHours()).padStart(2,'0') + ":" + String(d.getMinutes()).padStart(2,'0'); printStruk(items, total, bayar, (bayar - total), "REPRINT", id, timeStr, cust, tipe); } // RUMUS RATA TENGAH (AUTO-WRAP UNTUK TEKS PANJANG) function alignCenter(text, width) { if (!text) return ""; let str = String(text).trim(); // Jika teksnya muat dalam 1 baris if (str.length <= width) { let pad = Math.floor((width - str.length) / 2); return ' '.repeat(pad) + str; } // Jika teks KEPANJANGAN (contoh: Alamat), pecah jadi beberapa baris & tengahkan semuanya! let lines = []; while (str.length > 0) { let chunk = str.substring(0, width).trim(); let pad = Math.floor((width - chunk.length) / 2); lines.push(' '.repeat(pad) + chunk); str = str.substring(width); // Ambil sisa teksnya } return lines.join('\n'); } // RUMUS UNTUK KANAN-KIRI (Contoh: "TOTAL" di kiri, "Rp 10.000" di kanan) function alignPair(left, right, width) { let l = String(left).trim(); let r = String(right).trim(); let spaces = width - l.length - r.length; if (spaces < 1) spaces = 1; return l + ' '.repeat(spaces) + r; } function renderHistory() { const q = document.getElementById('h-search').value.toLowerCase(); let combined = []; const esc = (s) => String(s).replace(/'/g, "\\'").replace(/"/g, '"'); const cleanTime = (t) => { if(!t) return "00:00"; if(t.length===5 && t.includes(':')) return t; let d = new Date(t); return isNaN(d.getTime()) ? "00:00" : `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; }; const makeTs = (dVal, tStr) => { let d=new Date(dVal); if(isNaN(d.getTime())) d=new Date(); let p=tStr.split(':'); d.setHours(parseInt(p[0]),parseInt(p[1]),0,0); return d.getTime(); }; DB.salesHistory.forEach(r => { let jam = cleanTime(r[2]); combined.push({ type: 'Sale', sort: makeTs(r[1], jam), dDate: new Date(r[1]).toLocaleDateString('id-ID', {day:'numeric', month:'short'}), dTime: jam, id: r[0], name: r[3], tipe: r[4], detail: r[5], tot: r[6], stat: r[9], bayar: r[10], sisa: r[11], kasir: r[13] || 'Admin', rawIdx: r[r.length - 1] }); }); if (USER_ROLE === 'OWNER') { const proc = (lst, type) => lst.forEach(r => combined.push({ type: 'Biaya', sort: makeTs(r[0], "00:00"), dDate: new Date(r[0]).toLocaleDateString('id-ID', {day:'numeric', month:'short'}), dTime: '', id: type, name: r[1], detail: r[3] || type, tot: r[2], stat: 'Biaya', rawIdx: r[r.length - 1] })); proc(DB.expOps, 'Operasional'); proc(DB.expFix, 'Tetap'); } let html = combined.sort((a, b) => { if (b.sort !== a.sort) return b.sort - a.sort; return (b.rawIdx || 0) - (a.rawIdx || 0); }) .filter(x => (CUR_FILTER === 'all' || x.stat === CUR_FILTER || (CUR_FILTER === 'Biaya' && x.type === 'Biaya')) && x.name.toLowerCase().includes(q)) .map(x => { let sId = esc(x.id), sDet = esc(x.detail), sNam = esc(x.name), sTip = esc(x.tipe), sKas = esc(x.kasir); return `
${x.name}${x.type === 'Sale' ? `` : ''}
${x.stat}
${x.detail}
${toIDR(x.tot)}${x.dDate} ${x.dTime}
${x.stat === 'Hutang' ? `` : ''}
`; }).join(''); document.getElementById('list-hist').innerHTML = html || '
Kosong
'; } function filterHist(f, el) { CUR_FILTER = f; document.querySelectorAll('.filter-tab').forEach(x => x.classList.remove('active')); el.classList.add('active'); renderHistory(); } function payPiutang(id, sisa) { Swal.fire({ title: 'PELUNASAN PIUTANG', html: `

${toIDR(sisa)}

*Input angka saja, titik ribuan muncul otomatis.
`, showCancelButton: true, confirmButtonText: 'PROSES BAYAR', confirmButtonColor: '#10b981', cancelButtonText: 'BATAL', reverseButtons: true, preConfirm: () => { const val = clean(document.getElementById('input-bayar-piutang').value); if (!val || val <= 0) { Swal.showValidationMessage('Masukkan nominal bayar!'); return false; } if (val > sisa) { Swal.showValidationMessage('Bayar tidak boleh melebihi sisa hutang!'); return false; } return val; } }).then(async (r) => { if (r.isConfirmed) { document.getElementById('loader').style.display = 'flex'; try{ await panggilAPI('payInvoice', {invId: id, amount: r.value}); document.getElementById('loader').style.display = 'none'; Swal.fire({ title: 'Sukses!', text: 'Pembayaran piutang berhasil dicatat.', icon: 'success', timer: 1500, showConfirmButton: false }); initApp(true); }catch(e){ document.getElementById('loader').style.display = 'none'; } } }); } function renderLocalLists() { if(document.getElementById('pg-exp').classList.contains('active-page')) updateExpEdu(); const q = document.getElementById('p-search') ? document.getElementById('p-search').value.toLowerCase() : ""; if(document.getElementById('list-prod')) { let p = DB.products.filter(x => x[0].toLowerCase().includes(q)).sort((a,b) => a[0].localeCompare(b[0])); document.getElementById('list-prod').innerHTML = p.map(r => `
${r[0]}
HPP: ${toIDR(r[1])} | Jual: ${toIDR(r[2])}
`).join(''); } if(document.getElementById('list-exp')) { let isFix = (document.getElementById('e-type').value === 'FIX'); let list = isFix ? DB.expFix : DB.expOps; if(!list || list.length === 0) { document.getElementById('list-exp').innerHTML = '
Belum ada data pengeluaran.
'; document.getElementById('e-total-show').innerText = "Total: Rp 0"; return; } let tot = 0; let sortedList = list.slice().sort((a, b) => { let tglA = new Date(a[0]).getTime(); let tglB = new Date(b[0]).getTime(); if (tglA !== tglB) return tglB - tglA; return b[b.length - 1] - a[a.length - 1]; }); document.getElementById('list-exp').innerHTML = sortedList.map(r => { tot += Number(r[2]); let tgl = new Date(r[0]).toLocaleDateString('id-ID', {day:'numeric', month:'short'}); return `
${tgl.split(' ')[1]}${tgl.split(' ')[0]}
${r[1]}${r[3] || '-'}
${toIDR(r[2])}
`; }).join(''); document.getElementById('e-total-show').innerText = "Total: " + toIDR(tot); } } async function checkAndEdit(namaMenu) { document.getElementById('loader').style.display = 'flex'; try{ const res = await panggilAPI('checkMenuType', {namaMenu: namaMenu}); document.getElementById('loader').style.display = 'none'; if(res.isRecipe) { Swal.fire({ title: 'Edit Menu: ' + namaMenu, text: 'Menu ini memiliki Resep Bahan Baku.', icon: 'question', showCancelButton: true, showDenyButton: true, confirmButtonText: '🧑‍🍳 Edit Resep (HPP)', denyButtonText: '⚡ Edit Harga Jual Saja', cancelButtonText: 'Batal' }).then((result) => { if (result.isConfirmed) loadRecipeToEdit(namaMenu); else if (result.isDenied) showSimpleEditForm(res.detail); }); } else { showSimpleEditForm(res.detail); } }catch(e){ document.getElementById('loader').style.display = 'none'; } } async function saveProd(p) { document.getElementById('loader').style.display='flex'; try{ await panggilAPI('saveProduct', {p: p}); document.getElementById('loader').style.display='none'; Swal.fire('Sukses','','success'); initApp(true); }catch(e){} } async function saveExp() { let o = { isFixed: document.getElementById('e-type').value === 'FIX', tgl: document.getElementById('e-tgl').value, kat: document.getElementById('e-nama').value, nom: clean(document.getElementById('e-nom').value), cat: document.getElementById('e-kat').value }; if(!o.kat || o.nom <= 0) return Swal.fire('Error', 'Lengkapi Nama Pengeluaran & Nominal', 'warning'); document.getElementById('loader').style.display = 'flex'; try{ await panggilAPI('saveExpense', {obj: o}); document.getElementById('loader').style.display = 'none'; Swal.fire({title: 'Sukses', icon: 'success', timer: 1500, showConfirmButton: false}); document.getElementById('e-nama').value = ''; document.getElementById('e-nom').value = ''; document.getElementById('e-kat').value = ''; initApp(true); }catch(e){ document.getElementById('loader').style.display = 'none'; } } function delRow(s, i) { Swal.fire({title:'Hapus?', showCancelButton:true}).then(async (r)=>{ if(r.isConfirmed){ document.getElementById('loader').style.display = 'flex'; await panggilAPI('deleteRow', {sheetName: s, rowIndex: i}); initApp(true); } }); } function nav(p, el) { document.querySelectorAll('.page').forEach(x=>x.classList.remove('active-page')); document.getElementById('pg-'+p).classList.add('active-page'); document.querySelectorAll('.nav-btn').forEach(x=>x.classList.remove('active')); if(el) { el.classList.add('active'); el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } if(p==='hist') renderHistory(); else if(p==='users') renderUsers(); else if(p==='resep') renderRecipeList(); else renderLocalLists(); } function clean(v) { return Number(v.toString().replace(/[^0-9]/g, ""))||0; } function maskMoney(el) { el.value = Number(el.value.replace(/\D/g,"")).toLocaleString('id-ID'); } function setOrderType(t) { ORDER_TYPE = t; document.querySelectorAll('.btn-order-type').forEach(b=>b.classList.remove('active')); if(t==='Dine-in')document.getElementById('btn-dinein').classList.add('active'); if(t==='Takeaway')document.getElementById('btn-takeaway').classList.add('active'); if(t==='Online')document.getElementById('btn-online').classList.add('active'); } function updateExpEdu() { let t = document.getElementById('e-type').value; let card = document.getElementById('exp-edu-card'); let lblHist = document.getElementById('lbl-history-type'); card.className = "d-flex align-items-center gap-3 p-3 rounded-3 mb-2"; if (t === 'OPS') { card.style.background = "#fff7ed"; card.style.borderLeft = "4px solid #f97316"; card.style.color = "#9a3412"; card.innerHTML = `
BIAYA OPERASIONAL (Harian)Pengeluaran naik-turun sesuai aktivitas jualan. (Cth: Gas, Plastik, Parkir).
`; if(lblHist) lblHist.innerText = "RIWAYAT OPERASIONAL"; } else { card.style.background = "#eff6ff"; card.style.borderLeft = "4px solid #3b82f6"; card.style.color = "#1e3a8a"; card.innerHTML = `
BIAYA TETAP (Bulanan)Pengeluaran wajib walau toko tutup. (Cth: Gaji, Listrik, Sewa Ruko, Wifi).
`; if(lblHist) lblHist.innerText = "RIWAYAT BIAYA TETAP"; } } function addSimRow(n='', hrg='', qtyB='1', satB='Kg', qtyP='', satP='Gr') { let valB = hrg ? Number(hrg).toLocaleString('id-ID') : ''; const optBeli = ``; const optPakai = ``; let html = `
*Isi total berat bersih (cth: 370)
`; document.getElementById('sim-container').insertAdjacentHTML('beforeend', html); calcSim(); } function checkManual(el) { const row = el.closest('.sim-row'); const hint = row.querySelector('.manual-hint'); if(el.value === 'Manual') { hint.classList.remove('d-none'); row.querySelector('.s-qty-buy').value = ''; row.querySelector('.s-qty-buy').focus(); } else { hint.classList.add('d-none'); if(!row.querySelector('.s-qty-buy').value) row.querySelector('.s-qty-buy').value = '1'; } } function calcSim() { let tot = 0; document.querySelectorAll('.sim-row').forEach(r => { let hrgBeli = clean(r.querySelector('.s-buy').value); let qtyDapat = clean(r.querySelector('.s-qty-buy').value); let satBeli = r.querySelector('.s-sat-buy').value; let qtyPakai = clean(r.querySelector('.s-use').value); let satPakai = r.querySelector('.s-sat-use').value; let faktor = 1; switch(satBeli) { case 'Kg': faktor = 1000; break; case 'Liter': faktor = 1000; break; case 'Ons': faktor = 100; break; case 'Galon': faktor = 19000; break; case 'Lusin': faktor = 12; break; case 'Renceng': faktor = 10; break; case 'Tray': faktor = 30; break; case 'Rim': faktor = 500; break; default: faktor = 1; break; } if(satPakai === 'Kg') qtyPakai = qtyPakai * 1000; if(satPakai === 'Liter') qtyPakai = qtyPakai * 1000; if(satPakai === 'Sdm') qtyPakai = qtyPakai * 15; if(satPakai === 'Sdt') qtyPakai = qtyPakai * 5; let totalVolume = qtyDapat * faktor; if(totalVolume > 0) tot += (hrgBeli / totalVolume) * qtyPakai; }); document.getElementById('sim-batch-total').innerText = toIDR(Math.ceil(tot)); let yieldVal = clean(document.getElementById('sim-yield').value) || 1; CURRENT_HPP = Math.ceil(tot / yieldVal); document.getElementById('sim-hpp').innerText = toIDR(CURRENT_HPP); if(typeof updateRecommendations === "function") updateRecommendations(CURRENT_HPP); if(typeof calcFinal === "function") calcFinal(); } function calcFinal() { let priceInput = clean(document.getElementById('sim-margin-price').value); let isTax = document.getElementById('sim-tax-active').checked; let taxRateVal = parseFloat(document.getElementById('sim-tax-rate').value) || 0; let taxInput = document.getElementById('sim-tax-rate'); let taxGroup = document.getElementById('tax-input-group'); if(isTax) { taxInput.disabled = false; taxGroup.style.opacity = '1'; } else { taxInput.disabled = true; taxGroup.style.opacity = '0.5'; } let tax = isTax ? (priceInput * (taxRateVal / 100)) : 0; let finalJual = priceInput + tax; let profit = priceInput - CURRENT_HPP; document.getElementById('lbl-jual-final').innerText = toIDR(finalJual); document.getElementById('lbl-tax-detail').innerText = isTax ? `Dasar: ${toIDR(priceInput)} + Pajak: ${toIDR(tax)}` : '(Tanpa Pajak)'; let infoBox = document.getElementById('sim-info'); if(priceInput > 0) { if(profit < 0) { infoBox.innerHTML = ` RUGI: ${toIDR(profit)}`; infoBox.className = 'p-2 rounded text-center small mb-3 bg-danger text-white fw-bold'; } else { let margin = (profit / priceInput) * 100; if(!isFinite(margin)) margin = 0; infoBox.innerHTML = `PROFIT BERSIH: ${toIDR(profit)} ${Math.round(margin)}%`; infoBox.className = 'p-2 rounded text-center small mb-3 bg-success text-white fw-bold'; } } else { infoBox.innerHTML = "Input harga jual di atas..."; infoBox.className = 'p-2 rounded text-center small mb-3 bg-light text-muted border border-dashed'; } } function clearSim() { document.getElementById('sim-container').innerHTML = ''; document.getElementById('sim-name').value = ''; document.getElementById('sim-margin-price').value = ''; document.getElementById('sim-yield').value = '1'; document.getElementById('sim-tax-active').checked = false; addSimRow(); } function updateRecommendations(hpp) { if(!hpp || hpp <= 0) return; document.getElementById('rec-price-grosir').innerText = toIDR(Math.ceil((hpp * 1.3)/100)*100); document.getElementById('rec-price-retail').innerText = toIDR(Math.ceil((hpp * 2.0)/100)*100); document.getElementById('rec-price-resto').innerText = toIDR(Math.ceil((hpp * 4.0)/100)*100); } function applyMargin(factor) { if(CURRENT_HPP <= 0) return Swal.fire('HPP Kosong', 'Hitung bahan baku dulu.', 'warning'); let finalPrice = Math.ceil((CURRENT_HPP * factor) / 100) * 100; document.getElementById('sim-margin-price').value = finalPrice.toLocaleString('id-ID'); calcFinal(); } async function saveFullRecipe() { let name = document.getElementById('sim-name').value; let priceInput = clean(document.getElementById('sim-margin-price').value); if(!name || priceInput <= 0) return Swal.fire('Error','Nama & Harga Jual wajib diisi','warning'); if(CURRENT_HPP <= 0) return Swal.fire('Error','Bahan baku masih kosong','warning'); let ingredients = []; document.querySelectorAll('.sim-row').forEach(r => { let hrgBeli = clean(r.querySelector('.s-buy').value); let qtyBeli = clean(r.querySelector('.s-qty-buy').value); let satBeli = r.querySelector('.s-sat-buy').value; let qtyPakai = clean(r.querySelector('.s-use').value); let satPakai = r.querySelector('.s-sat-use').value; let faktor = 1; switch(satBeli) { case 'Kg': faktor=1000; break; case 'Liter': faktor=1000; break; case 'Ons': faktor=100; break; case 'Galon': faktor=19000; break; case 'Lusin': faktor=12; break; case 'Renceng': faktor=10; break; case 'Tray': faktor=30; break; case 'Rim': faktor=500; break; default: faktor=1; break; } let qtyPakaiFinal = qtyPakai; if(satPakai === 'Kg') qtyPakaiFinal = qtyPakai * 1000; if(satPakai === 'Liter') qtyPakaiFinal = qtyPakai * 1000; if(satPakai === 'Sdm') qtyPakaiFinal = qtyPakai * 15; if(satPakai === 'Sdt') qtyPakaiFinal = qtyPakai * 5; let totalIsi = qtyBeli * faktor; let biayaBahan = 0; if(totalIsi > 0) biayaBahan = (hrgBeli / totalIsi) * qtyPakaiFinal; ingredients.push({ bahan: r.querySelector('.s-name').value, beli: hrgBeli, qtyBeli: qtyBeli, satBeli: satBeli, qtyPakai: qtyPakai, satPakai: satPakai, biaya: Math.ceil(biayaBahan) }); }); let isTax = document.getElementById('sim-tax-active').checked; let taxRateVal = parseFloat(document.getElementById('sim-tax-rate').value) || 0; let tax = isTax ? (priceInput * (taxRateVal/100)) : 0; let finalJual = priceInput + tax; let header = { nama: name, hpp: CURRENT_HPP, jual: finalJual }; document.getElementById('loader').style.display = 'flex'; try { await panggilAPI('saveFullRecipe', {header: header, ingredients: ingredients}); document.getElementById('loader').style.display = 'none'; Swal.fire('Sukses', 'Menu tersimpan rapi!', 'success'); ALL_RECIPES = []; // Kosongkan cache resep agar saat buka tab resep, dia memuat yang baru clearSim(); initApp(true); } catch(e) { document.getElementById('loader').style.display = 'none'; } } let ALL_RECIPES = []; let CURRENT_RESEP_VIEW = ''; async function renderRecipeList() { // 🚀 TRIK CACHING: Jika data sudah ada di memori, tampilkan INSTAN! if (ALL_RECIPES.length > 0) { filterRecipeList(); return; } // Jika belum ada, baru kita munculkan loading dan minta ke server document.getElementById('list-resep').innerHTML = '
'; try { ALL_RECIPES = await panggilAPI('getRecipeList'); filterRecipeList(); } catch(e) { document.getElementById('list-resep').innerHTML = '
Gagal mengambil data resep.
'; } } function filterRecipeList() { const q = document.getElementById('r-search').value.toLowerCase(); const filtered = ALL_RECIPES.filter(r => r.nama.toLowerCase().includes(q)); if(filtered.length === 0) { document.getElementById('list-resep').innerHTML = '
Belum ada resep tersimpan.
'; return; } document.getElementById('list-resep').innerHTML = filtered.map(r => `
${r.nama.charAt(0).toUpperCase()}
${r.nama}Klik untuk detail bahan
${toIDR(r.totalHpp)}HPP Total
`).join(''); } async function viewResepDetail(nama) { CURRENT_RESEP_VIEW = nama; document.getElementById('lbl-resep-nama').innerText = nama; document.getElementById('detail-resep-list').innerHTML = '
'; var myModal = new bootstrap.Modal(document.getElementById('modalResep')); myModal.show(); try{ let details = await panggilAPI('getRecipeDetail', {namaMenu: nama}); let tot = 0; let html = details.map(d => { tot += d.biaya; return `
${d.bahan}Beli: ${toIDR(d.beli)} / ${d.qtyBeli} ${d.satBeli}Pakai: ${d.qtyPakai} ${d.satPakai}
${toIDR(d.biaya)}
`; }).join(''); document.getElementById('detail-resep-list').innerHTML = html; document.getElementById('lbl-resep-total').innerText = toIDR(tot); }catch(e){} } function editResepAction() { const modalEl = document.getElementById('modalResep'); const modal = bootstrap.Modal.getInstance(modalEl); modal.hide(); loadRecipeToEdit(CURRENT_RESEP_VIEW); } async function loadRecipeToEdit(namaMenu) { document.getElementById('loader').style.display = 'flex'; try{ let ingredients = await panggilAPI('getRecipeDetail', {namaMenu: namaMenu}); document.getElementById('loader').style.display = 'none'; nav('sim', document.querySelectorAll('.nav-btn')[3]); document.getElementById('sim-container').innerHTML = ''; document.getElementById('sim-name').value = namaMenu; if(ingredients.length > 0) { ingredients.forEach(i => { addSimRow(i.bahan, i.beli, i.qtyBeli, i.satBeli, i.qtyPakai, i.satPakai); }); } else { addSimRow(); } calcSim(); Swal.fire({ toast: true, position: 'top-end', icon: 'info', title: 'Mode Edit: ' + namaMenu, showConfirmButton: false, timer: 3000 }); }catch(e){ document.getElementById('loader').style.display = 'none'; } } function showSimpleEditForm(d) { Swal.fire({ title: d.nama ? 'Edit Menu Simple' : 'Buat Menu Simple', html: `
`, showCancelButton: true, confirmButtonText: 'SIMPAN', confirmButtonColor: '#0f172a', preConfirm: () => { return { nama: document.getElementById('swal-nama').value, jual: clean(document.getElementById('swal-jual').value), hpp: clean(document.getElementById('swal-hpp').value) } } }).then((result) => { if (result.isConfirmed) saveProd(result.value); }); } function printLaporanA4() { const bln = document.getElementById('f-month').value; const tgl = new Date().toLocaleString('id-ID'); let d = DB.salesHistory.sort((a,b)=>new Date(a[1])-new Date(b[1])); let tot = 0; let r = d.map((x,i)=>{ tot += Number(x[6]); return `${i+1}${new Date(x[1]).toLocaleDateString('id-ID')}
${x[0]}${x[3]}
${x[5]}${x[9]}${toIDR(x[6])}`; }).join(''); let w = window.open('','','height=800,width=1000'); w.document.write(`Laporan Penjualan

LAPORAN TRANSAKSI: ${bln}

${r}
NOTANGGALDETAILSTATUSTOTAL

TOTAL: ${toIDR(tot)}

`); w.document.close(); w.focus(); // HILANGKAN w.close() DI SINI setTimeout(() => { w.print(); }, 800); } // ========================================== // LAPORAN LABA RUGI (INCOME STATEMENT) PRO // ========================================== function printLaporanKeuangan() { const bln = document.getElementById('f-month').value; const tglCetak = new Date().toLocaleString('id-ID'); // 1. AMBIL ANGKA DARI DASHBOARD let omset = DB.dashboard.totalOmset || 0; let labaKotor = DB.dashboard.totalGrossProfit || 0; let hpp = omset - labaKotor; let biayaOps = 0; if(DB.expOps) DB.expOps.forEach(x => biayaOps += Number(x[2])); let biayaTetap = 0; if(DB.expFix) DB.expFix.forEach(x => biayaTetap += Number(x[2])); let totalBiaya = biayaOps + biayaTetap; let labaSebelumPajak = labaKotor - totalBiaya; let pajakUMKM = omset * 0.005; // Pajak UMKM 0.5% Final let labaBersih = labaSebelumPajak - pajakUMKM; // Ambil data Piutang (Mini Neraca) let piutangSisa = DB.dashboard.hut_sisa || 0; // 2. DESAIN LAPORAN STANDAR AKUNTANSI let htmlContent = ` Laba Rugi - ${bln}

LAPORAN LABA RUGI KOMPREHENSIF

Periode: ${bln} | Dicetak: ${tglCetak}


1. PENDAPATAN OPERASIONAL
Penjualan Kotor (Omset)${toIDR(omset)}
2. BEBAN POKOK PENJUALAN
Total Harga Pokok Produksi (HPP)(${toIDR(hpp)})
LABA KOTOR (GROSS PROFIT)${toIDR(labaKotor)}
3. BIAYA PENGELUARAN
Biaya Operasional (Variabel)(${toIDR(biayaOps)})
Biaya Tetap (Fixed Cost)(${toIDR(biayaTetap)})
Total Beban Operasional(${toIDR(totalBiaya)})
LABA SEBELUM PAJAK (EBT)${toIDR(labaSebelumPajak)}
4. PAJAK & POTONGAN
Pajak Penghasilan (PP23/PP55 UMKM 0.5%)(${toIDR(pajakUMKM)})
LABA BERSIH (NET INCOME) ${toIDR(labaBersih)}

RINGKASAN ASET & PIUTANG (MINI NERACA)

Piutang Pelanggan (Uang di luar yang belum ditagih) ${toIDR(piutangSisa)}

Laporan ini di-generate otomatis oleh sistem KASBI.

`; let w = window.open('','','height=800,width=1000'); w.document.write(htmlContent); w.document.close(); w.focus(); setTimeout(() => { w.print(); }, 800); } function applyRoleAccess(role) { const navBtns = document.querySelectorAll('.nav-btn'); const navWrap = document.querySelector('.nav-wrapper'); const filterBiaya = document.getElementById('filter-biaya-btn'); navBtns.forEach(b => b.style.display = 'none'); if(filterBiaya) filterBiaya.style.display = 'none'; if (role === 'OWNER') { navBtns.forEach(b => b.style.display = 'block'); if(filterBiaya) filterBiaya.style.display = 'inline-block'; if(navWrap) navWrap.style.justifyContent = ''; } else if (role === 'KASIR') { navBtns[1].style.display = 'block'; navBtns[2].style.display = 'block'; if(navWrap) navWrap.style.justifyContent = 'center'; } else if (role === 'GUDANG') { navBtns[3].style.display = 'block'; navBtns[4].style.display = 'block'; navBtns[5].style.display = 'block'; if(navWrap) navWrap.style.justifyContent = 'center'; } } // --- LOGIKA GUDANG --- let SELECTED_PROD_ID = null; function searchGudangWidget() { const input = document.getElementById('g-search-input'); const resBox = document.getElementById('g-search-results'); const q = input.value.toLowerCase(); if (q.length < 1) { resBox.style.display = 'none'; return; } let matches = DB.products.filter(p => p[0].toLowerCase().includes(q)).slice(0, 10); if (matches.length === 0) { resBox.innerHTML = '
Tidak ditemukan
'; } else { resBox.innerHTML = matches.map(p => `
${p[0]}Sisa: ${p[3]||0}
`).join(''); } resBox.style.display = 'block'; } function selectGudangProd(nama, stok) { document.getElementById('g-search-area').style.display = 'none'; document.getElementById('g-search-results').style.display = 'none'; document.getElementById('g-search-input').value = ''; document.getElementById('g-selected-area').style.display = 'block'; document.getElementById('g-sel-name').innerText = nama; document.getElementById('g-sel-stok').innerText = stok; SELECTED_PROD_ID = nama; let qtyInput = document.getElementById('g-qty'); qtyInput.value = ""; qtyInput.placeholder = "0"; qtyInput.focus(); } function resetGudangSelection() { document.getElementById('g-selected-area').style.display = 'none'; document.getElementById('g-search-area').style.display = 'block'; SELECTED_PROD_ID = null; document.getElementById('g-qty').value = 0; } function maskStock(el) { let val = el.value; let isMin = val.includes('-'); let raw = val.replace(/\D/g, ''); if (isMin && raw === '') { el.value = '-'; return; } if (raw === '') { el.value = ''; return; } let fmt = Number(raw).toLocaleString('id-ID'); el.value = isMin ? '-' + fmt : fmt; } function parseStock(valStr) { if(!valStr) return 0; let cleanStr = valStr.toString().replace(/\./g, ''); return parseInt(cleanStr) || 0; } function adjustQtyGudang(amount) { let el = document.getElementById('g-qty'); let currentVal = parseStock(el.value); let newVal = currentVal + amount; el.value = newVal.toLocaleString('id-ID'); } async function simpanStok() { let qty = parseStock(document.getElementById('g-qty').value); if(!SELECTED_PROD_ID) return Swal.fire('Error','Belum pilih produk','warning'); if(qty == 0) return Swal.fire('Error','Jumlah tidak boleh 0','warning'); document.getElementById('loader').style.display='flex'; try{ const res = await panggilAPI('updateStock', {namaProduk: SELECTED_PROD_ID, qtyMasuk: qty, user: CURRENT_USER}); document.getElementById('loader').style.display='none'; if(res.success) { Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: 'Stok Terupdate!', showConfirmButton: false, timer: 1500 }); resetGudangSelection(); initApp(true); } else { Swal.fire('Gagal', res.message, 'error'); } }catch(e){ document.getElementById('loader').style.display='none'; } } // ========================================== // FUNGSI PENGATURAN TOKO // ========================================== function switchSetting(type) { const bT = document.getElementById('btn-tab-toko'); const bU = document.getElementById('btn-tab-user'); const sT = document.getElementById('sub-pg-toko'); const sU = document.getElementById('sub-pg-user'); if(type === 'toko') { bT.style.background='#0f172a'; bT.style.color='white'; bU.style.background='#f1f5f9'; bU.style.color='#64748b'; sT.style.display='block'; sU.style.display='none'; } else { bU.style.background='#0f172a'; bU.style.color='white'; bT.style.background='#f1f5f9'; bT.style.color='#64748b'; sT.style.display='none'; sU.style.display='block'; renderUsers(); } } // --- LOGIKA BATAS KARAKTER & PRINTER --- let MAX_CHAR = 32; // Default 58mm // --- FUNGSI PEMBATAS KARAKTER DINAMIS & DISIPLIN --- function updateCharLimits() { let pSize = document.getElementById('set-printer').value; let maxC = (pSize === '80') ? 48 : 32; document.getElementById('set-toko').maxLength = maxC; document.getElementById('set-f1').maxLength = maxC; document.getElementById('set-f2').maxLength = maxC; // Tambahkan pengunci untuk ke-2 baris alamat document.getElementById('set-alamat1').maxLength = maxC; document.getElementById('set-alamat2').maxLength = maxC; checkLength(document.getElementById('set-toko'), 'char-nama'); checkLength(document.getElementById('set-f1'), 'char-f1'); checkLength(document.getElementById('set-f2'), 'char-f2'); // Update indikator alamat checkLength(document.getElementById('set-alamat1'), 'char-a1'); checkLength(document.getElementById('set-alamat2'), 'char-a2'); } function checkLength(el, labelId, isAddress = false) { let pSize = document.getElementById('set-printer').value || '58'; let maxC = (pSize === '80') ? 48 : 32; let limit = isAddress ? (maxC * 2) : maxC; // Potong paksa jika pengguna melakukan Copy-Paste teks yang kepanjangan if (el.value.length > limit) { el.value = el.value.substring(0, limit); } let lbl = document.getElementById(labelId); if(lbl) { lbl.innerText = `(${el.value.length}/${limit})`; // Ubah indikator jadi MERAH TEBAL jika karakter sudah mentok lbl.className = (el.value.length === limit) ? "text-danger ms-1 fw-bold" : "text-muted ms-1"; } } async function loadSettings() { try { const res = await panggilAPI('getSettings'); // Load setelan ukuran printer dari memori browser const pSize = localStorage.getItem('KASBI_PRINTER_SIZE') || '58'; document.getElementById('set-printer').value = pSize; updateCharLimits(); if(res && res.nama) { document.getElementById('set-toko').value = res.nama; document.getElementById('set-alamat').value = res.alamat; document.getElementById('set-hp').value = res.hp; document.getElementById('set-f1').value = res.f1; document.getElementById('set-f2').value = res.f2; TOKO.nama = res.nama; TOKO.alamat = res.alamat; TOKO.wa = res.hp; TOKO.footer1 = res.f1; TOKO.footer2 = res.f2; TOKO.printer = pSize; updateCharLimits(); // Panggil lagi agar angka (0/32) terupdate otomatis } } catch(e) { console.error("Gagal load setting", e); } } async function saveSettings() { // 1. Ambil nomor HP asli tanpa kutip dari ketikan user let nomorAsli = document.getElementById('set-hp').value; // 2. Siapkan data untuk Google Sheet (Kutip HANYA ditambahkan di sini) // Ambil kedua alamat lalu gabungkan dengan baris baru (\n) let almt1 = document.getElementById('set-alamat1').value.trim(); let almt2 = document.getElementById('set-alamat2').value.trim(); let alamatGabungan = almt1; if (almt2 !== "") alamatGabungan += "\n" + almt2; const payload = { nama: document.getElementById('set-toko').value, alamat: alamatGabungan, // Alamat yang sudah digabung hp: "'" + nomorAsli, f1: document.getElementById('set-f1').value, f2: document.getElementById('set-f2').value }; // 3. Simpan memori untuk di aplikasi (GUNAKAN NOMOR ASLI TANPA KUTIP) const pSize = document.getElementById('set-printer').value; localStorage.setItem('KASBI_PRINTER_SIZE', pSize); TOKO.printer = pSize; TOKO.nama = payload.nama; TOKO.alamat = payload.alamat; TOKO.wa = nomorAsli; // <-- Sekarang memori aplikasi bersih dari kutip! TOKO.footer1 = payload.f1; TOKO.footer2 = payload.f2; // --- GUNAKAN LOADING MODERN KITA --- let loaderText = document.querySelector('#loader b'); loaderText.innerText = "MENYIMPAN PENGATURAN..."; document.getElementById('loader').style.display = 'flex'; try { // Kirim ke Database (Master Library) const res = await panggilAPI('saveSettings', payload); // Matikan loading & kembalikan teks document.getElementById('loader').style.display = 'none'; loaderText.innerText = "MEMUAT SYSTEM..."; if(res.success) { Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: 'Pengaturan Tersimpan!', showConfirmButton: false, timer: 2000 }); } else { Swal.fire('Gagal', res.message, 'error'); } } catch(e) { document.getElementById('loader').style.display = 'none'; loaderText.innerText = "MEMUAT SYSTEM..."; Swal.fire('Error', 'Terjadi kesalahan koneksi', 'error'); } } // --- FUNGSI SMART ARCHIVE DINAMIS --- function arsipkanData() { const bulan = document.getElementById('set-arsip-waktu').value; const teksPilihan = document.getElementById('set-arsip-waktu').options[document.getElementById('set-arsip-waktu').selectedIndex].text; Swal.fire({ title: 'Bersihkan & Arsipkan?', html: `Sistem akan mencari data transaksi yang ${teksPilihan} dan memindahkannya ke Sheet Arsip.`, icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc3545', cancelButtonColor: '#0f172a', confirmButtonText: 'Ya, Bersihkan!', cancelButtonText: 'Batal', reverseButtons: true }).then(async (result) => { if (result.isConfirmed) { // --- LOADING MODERN --- let loaderText = document.querySelector('#loader b'); loaderText.innerText = "MENGARSIPKAN DATABASE..."; document.getElementById('loader').style.display = 'flex'; try { const res = await panggilAPI('archiveData', { bulanMundur: parseInt(bulan) }); // Matikan loading & kembalikan teks document.getElementById('loader').style.display = 'none'; loaderText.innerText = "MEMUAT SYSTEM..."; if(res.success) { Swal.fire('Selesai!', res.message, 'success'); initApp(true); } else { Swal.fire('Info', res.message, 'info'); } } catch(e) { // Matikan loading jika error document.getElementById('loader').style.display = 'none'; loaderText.innerText = "MEMUAT SYSTEM..."; Swal.fire('Gagal', 'Terjadi kesalahan jaringan.', 'error'); } } }); }