Flatlogic Bot 8c2a5d487c POSFuku_v2
2026-04-19 12:46:29 +00:00

2115 lines
106 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fuku - Stok & Belanja</title>
<script src="/local-preview-bridge.js"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f8fafc; color: #1e293b; -webkit-font-smoothing: antialiased; }
.header { background: #111; color: white; padding: 12px 15px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.container { padding: 10px; max-width: 1200px; margin: 0 auto; box-sizing: border-box; }
.tabs { display: flex; background: white; border-radius: 12px; margin-bottom: 12px; overflow-x: auto; border: 1px solid #e2e8f0; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.tabs::-webkit-scrollbar { display: none; }
.tab { flex: none; padding: 12px 15px; text-align: center; cursor: pointer; font-weight: 800; color: #64748b; transition: 0.2s; border-bottom: 3px solid transparent; white-space: nowrap; font-size: 12px; }
.tab.active { background: #fff1f2; color: #e11d48; border-bottom-color: #e11d48; }
.card { background: white; border-radius: 12px; padding: 0; margin-bottom: 15px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); overflow: hidden; }
.card-header { padding: 12px 15px; background: #f8fafc; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
.card-title { font-size: 14px; font-weight: 900; color: #0f172a; display: flex; align-items: center; gap: 8px; }
.table-container { overflow-x: auto; width: 100%; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: collapse; min-width: 500px; }
th { background: #f1f5f9; color: #475569; text-align: left; padding: 10px 12px; font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #e2e8f0; position: sticky; top: 0; }
td { padding: 8px 12px; border-bottom: 1px solid #f1f5f9; font-size: 13px; }
tr:hover td { background: #f8fafc; }
input, select, textarea { width: 100%; padding: 10px; border: 1.5px solid #cbd5e1; border-radius: 8px; font-size: 14px; box-sizing: border-box; outline: none; transition: 0.2s; -webkit-appearance: none; background: #fff; }
input:focus { border-color: #e11d48; box-shadow: 0 0 0 3px rgba(225, 29, 72, 0.1); }
input[readonly] { background: #f1f5f9; color: #64748b; cursor: not-allowed; border-color: #e2e8f0; }
input[type="number"] { min-width: 80px; } /* Global minimum width for numbers */
.bl-harga, .bl-total, .op-harga, .op-total, .pg-nilai { min-width: 120px !important; text-align: right; } /* Specifically wider for prices */
.btn { padding: 10px 16px; border: none; border-radius: 10px; font-weight: 800; font-size: 12px; cursor: pointer; transition: 0.2s; display: inline-flex; align-items: center; gap: 6px; justify-content: center; }
.btn-primary { background: #e11d48; color: white; }
.btn-secondary { background: #111; color: white; }
.btn-danger { background: #fee2e2; color: #ef4444; padding: 8px; }
.btn:active { transform: scale(0.97); opacity: 0.9; }
.footer-actions { padding: 12px 15px; background: #f8fafc; border-top: 1px solid #e2e8f0; text-align: right; display: flex; justify-content: flex-end; gap: 10px; }
#loading { position: fixed; inset: 0; background: rgba(255,255,255,0.9); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 2000; display: none; backdrop-filter: blur(5px); }
.spinner { border: 4px solid #f3f3f3; border-top: 4px 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); } }
.total-label { color: #e11d48; font-weight: 900; font-size: 14px; }
.row-num { color: #94a3b8; font-size: 10px; font-weight: 600; width: 20px; }
details.rk-day { border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; margin-top: 8px; background: #fff; }
details.rk-day summary { list-style: none; cursor: pointer; padding: 10px 12px; background: #f8fafc; font-weight: 900; color: #0f172a; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
details.rk-day summary::-webkit-details-marker { display: none; }
.rk-table { width: 100%; border-collapse: collapse; min-width: auto; }
.rk-table th { background: #f1f5f9; }
.rk-actions { white-space: nowrap; text-align: right; }
.rk-mini { padding: 6px 8px; font-size: 10px; width: auto; }
.rk-editbox { padding: 10px; background: #fff; border-top: 1px solid #e2e8f0; }
.rk-editgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.rk-editgrid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
tr.rk-dirty td { background: #fff7ed; }
.photo-btn { background: #f1f5f9; border: 2px dashed #cbd5e1; border-radius: 8px; padding: 8px; cursor: pointer; text-align: center; transition: 0.2s; min-width: 40px; }
.photo-btn:hover { border-color: #e11d48; background: #fff1f2; }
.photo-preview-container { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.photo-preview { width: 45px; height: 45px; border-radius: 6px; object-fit: cover; border: 1px solid #e2e8f0; cursor: pointer; }
#login-screen { position: fixed; inset: 0; background: #111; z-index: 3000; display: flex; justify-content: center; align-items: center; padding: 20px; }
.login-card { background: white; width: 100%; max-width: 350px; padding: 30px; border-radius: 20px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }
.login-title { font-size: 22px; font-weight: 900; text-align: center; margin-bottom: 30px; color: #111; }
.login-input { margin-bottom: 15px; }
.login-input label { display: block; font-weight: 800; font-size: 11px; color: #64748b; margin-bottom: 8px; text-transform: uppercase; }
#main-app { display: none; }
@media (max-width: 600px) {
.tab { padding: 12px 12px; font-size: 11px; }
.card-header { flex-direction: column; align-items: flex-start; }
.btn { width: 100%; }
.footer-actions { flex-direction: column; }
input, select, textarea { font-size: 16px; padding: 8px 6px; } /* Prevent iOS zoom, reduce padding */
input[type="number"] { min-width: 60px; text-align: center; } /* Ensure numbers are visible */
.bl-harga, .bl-total, .op-harga, .op-total, .pg-nilai { min-width: 100px !important; text-align: right !important; } /* Wider for currency */
td, th { padding: 6px 4px; }
table { min-width: 750px; } /* Force horizontal scroll so columns aren't squished */
details.rk-day summary { padding: 8px 10px; font-size: 12px; }
.rk-table td, .rk-table th { padding: 6px 6px; }
.rk-mini { padding: 8px 10px; font-size: 11px; }
.rk-editgrid { grid-template-columns: 1fr; }
.rk-editgrid-3 { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div id="loading"><div class="spinner"></div><p id="loading-text" style="font-weight:800; color:#111; font-size: 12px;">Memproses...</p></div>
<!-- LOGIN SCREEN -->
<div id="login-screen">
<div class="login-card">
<div class="login-title">FUKU <span style="color:#e11d48;">POS</span></div>
<div class="login-input">
<label>Username</label>
<input type="text" id="login-user" placeholder="Nama Anda" style="padding: 12px;">
</div>
<div class="login-input">
<label>Password</label>
<input type="password" id="login-pass" placeholder="••••••••" style="padding: 12px;">
</div>
<button class="btn btn-primary" style="width: 100%; padding: 12px;" onclick="prosesLogin()">MASUK APLIKASI</button>
<p id="login-err" style="color: #ef4444; font-size: 12px; font-weight: 700; text-align: center; margin-top: 15px; display: none;">Username atau Password salah!</p>
</div>
</div>
<!-- MAIN APP -->
<div id="main-app">
<div class="header">
<div style="font-weight:900; font-size:14px; letter-spacing:0.5px;">FUKU <span style="color:#e11d48;">STOK</span></div>
<div style="display: flex; align-items: center; gap: 8px;">
<input id="date-picker" type="date" style="width: 135px; padding: 6px 8px; font-size: 11px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; color: #111;" onchange="onDateChange(this.value)">
<div id="user-display" style="font-size:10px; font-weight:800; color:#fff; background:#e11d48; padding:4px 8px; border-radius:12px;">User: -</div>
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 9px; width: auto;" onclick="logout()">LOGOUT</button>
</div>
</div>
<div class="container">
<div class="tabs">
<div id="tab-stok_atas" class="tab active" onclick="switchTab('stok_atas')">STOK ATAS</div>
<div id="tab-stok_bawah" class="tab" onclick="switchTab('stok_bawah')">STOK BAWAH</div>
<div id="tab-stok_showcase" class="tab" onclick="switchTab('stok_showcase')">SHOWCASE</div>
<div id="tab-belanja_modal" class="tab" onclick="switchTab('belanja_modal')">BELANJA MODAL</div>
<div id="tab-belanja_bawah" class="tab" onclick="switchTab('belanja_bawah')">BELANJA BAWAH</div>
<div id="tab-belanja_dapur" class="tab" onclick="switchTab('belanja_dapur')">BELANJA DAPUR</div>
<div id="tab-belanja_makan" class="tab" onclick="switchTab('belanja_makan')">BELANJA MAKAN</div>
<div id="tab-draft_pesanan" class="tab" onclick="switchTab('draft_pesanan')">DRAFT PESANAN</div>
<div id="tab-operasional" class="tab" onclick="switchTab('operasional')" style="display:none;">OPERASIONAL</div>
<div id="tab-pegawai" class="tab" onclick="switchTab('pegawai')" style="display:none;">PEGAWAI</div>
<div id="tab-ringkasan" class="tab" onclick="switchTab('ringkasan')" style="background: #111; color: #fff;">RINGKASAN</div>
</div>
<!-- TEMPLATE SECTION STOK -->
<div id="section-stok" class="section">
<div class="card">
<div class="card-header">
<div class="card-title" id="stok-title">📦 Tabel Stok</div>
<div style="display:flex; gap:8px; width: 100%;">
<button class="btn btn-secondary" style="flex:1" onclick="tambahBarisStok()"> TAMBAH</button>
<button id="btn-import-porsi" class="btn btn-secondary" style="flex:1; display:none;" onclick="importPorsiTerjualCSV()">⬆️ IMPORT CSV</button>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th style="width: 35%;">Item</th>
<th>Awal</th>
<th style="color:#2563eb">Re-Stok</th>
<th>Pakai</th>
<th>Sisa</th>
<th id="th-foto" style="display:none">Foto (Ops)</th>
<th></th>
</tr>
</thead>
<tbody id="body-stok"></tbody>
</table>
</div>
<div class="footer-actions">
<button class="btn btn-primary" onclick="simpanSemuaStok()">💾 SIMPAN STOK</button>
</div>
</div>
</div>
<!-- TEMPLATE SECTION BELANJA -->
<div id="section-belanja" class="section" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title" id="belanja-title">🛒 Tabel Belanja</div>
<div style="display:flex; gap:8px;">
<button id="btn-sync-belanja" class="btn btn-secondary" style="display:none;" onclick="syncBelanjaToRekap()">🔁 SYNC REKAP</button>
<button id="btn-sync-belanja-all" class="btn btn-secondary" style="display:none;" onclick="syncBelanjaToRekapAll()">🗓️ SYNC SEMUA</button>
<button class="btn btn-secondary" onclick="tambahBarisBelanja()"> TAMBAH</button>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th style="width: 30%;">Barang</th>
<th>Harga</th>
<th>Qty</th>
<th>Total</th>
<th style="width: 25%;">Catatan</th>
<th></th>
</tr>
</thead>
<tbody id="body-belanja"></tbody>
</table>
</div>
<div class="card-header" style="background:white; border-top:1px solid #e2e8f0;">
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL:</div>
<div id="grand-total-belanja" class="total-label">Rp 0</div>
</div>
<div class="footer-actions">
<button class="btn btn-primary" onclick="simpanSemuaBelanja()">💾 SIMPAN BELANJA</button>
</div>
</div>
</div>
<!-- TEMPLATE SECTION DRAFT PESANAN -->
<div id="section-draft" class="section" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title">🧾 Draft Pesanan (Modal & Bawah)</div>
<button class="btn btn-secondary" onclick="refreshDraftPesanan()">🔄 REFRESH</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th>Kategori</th>
<th style="width: 40%;">Item</th>
<th>Harga Sebelum</th>
<th>Qty</th>
<th>Total</th>
</tr>
</thead>
<tbody id="body-draft"></tbody>
</table>
</div>
<div class="card-header" style="background:white; border-top:1px solid #e2e8f0;">
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL DRAFT:</div>
<div id="grand-total-draft" class="total-label">Rp 0</div>
</div>
<div style="padding: 12px 15px; border-top:1px solid #e2e8f0; background:#fff;">
<div style="font-weight:800; color:#64748b; font-size:12px; margin-bottom:6px;">ITEM (QTY &gt; 0):</div>
<textarea id="draft-picked" readonly style="width:100%; min-height: 110px; border:1px solid #e2e8f0; border-radius:12px; padding:10px; font-size:13px; background:#f8fafc;"></textarea>
</div>
</div>
</div>
<!-- SECTION OPERASIONAL -->
<div id="section-operasional" class="section" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title">⚙️ Biaya Operasional</div>
<button class="btn btn-secondary" onclick="tambahBarisOperasional()"> TAMBAH</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th style="width: 40%;">Keterangan Biaya</th>
<th>Harga</th>
<th>Qty</th>
<th>Total</th>
<th>Catatan</th>
<th></th>
</tr>
</thead>
<tbody id="body-operasional"></tbody>
</table>
</div>
<div class="card-header" style="background:white; border-top:1px solid #e2e8f0;">
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL:</div>
<div id="grand-total-operasional" class="total-label">Rp 0</div>
</div>
<div class="footer-actions">
<button class="btn btn-primary" onclick="simpanSemuaOperasional()">💾 SIMPAN OPERASIONAL</button>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">📅 Rekap Operasional (Periode)</div>
<div style="display:flex; gap:8px; width: 100%; flex-wrap:wrap;">
<input id="op-rekap-from" type="date" style="flex:1; min-width:150px;" onchange="localStorage.setItem('fuku_op_from', this.value)">
<input id="op-rekap-to" type="date" style="flex:1; min-width:150px;" onchange="localStorage.setItem('fuku_op_to', this.value)">
<button class="btn btn-secondary" style="flex:1; min-width:120px;" onclick="lihatRekapOperasionalPeriode()">LIHAT</button>
</div>
</div>
<div class="card-header" style="background:white; border-top:1px solid #e2e8f0;">
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL PERIODE:</div>
<div id="op-rekap-total" class="total-label">Rp 0</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th>Keterangan</th>
<th>Harga</th>
<th>Qty</th>
<th>Total</th>
</tr>
</thead>
<tbody id="body-operasional-rekap"></tbody>
</table>
</div>
</div>
</div>
<!-- SECTION PEGAWAI -->
<div id="section-pegawai" class="section" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title"><EFBFBD> Rincian Gaji & Kasbon</div>
<button class="btn btn-secondary" onclick="tambahBarisPegawai()"> TAMBAH</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th>Nama Pegawai</th>
<th>Nilai (Rp)</th>
<th>Kategori</th>
<th>Catatan</th>
<th></th>
</tr>
</thead>
<tbody id="body-pegawai"></tbody>
</table>
</div>
<div class="card-header" style="background:white; border-top:1px solid #e2e8f0;">
<div style="display:flex; gap:12px; flex-wrap:wrap; width:100%;">
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL GAJI: <span id="total-gaji-pegawai" class="total-label" style="font-size:12px;">Rp 0</span></div>
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL KASBON: <span id="total-kasbon-pegawai" class="total-label" style="font-size:12px;">Rp 0</span></div>
</div>
<div id="grand-total-pegawai" class="total-label">Rp 0</div>
</div>
<div class="footer-actions">
<button class="btn btn-primary" onclick="simpanSemuaPegawai()">💾 SIMPAN DATA PEGAWAI</button>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><EFBFBD> Rekap Pegawai (Periode)</div>
<div style="display:flex; gap:8px; width: 100%; flex-wrap:wrap;">
<input id="pg-rekap-from" type="date" style="flex:1; min-width:150px;" onchange="localStorage.setItem('fuku_pg_from', this.value)">
<input id="pg-rekap-to" type="date" style="flex:1; min-width:150px;" onchange="localStorage.setItem('fuku_pg_to', this.value)">
<button class="btn btn-secondary" style="flex:1; min-width:120px;" onclick="lihatRekapPegawaiPeriode()">LIHAT</button>
</div>
</div>
<div class="card-header" style="background:white; border-top:1px solid #e2e8f0;">
<div style="display:flex; gap:12px; flex-wrap:wrap; width:100%;">
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL GAJI: <span id="pg-rekap-gaji" class="total-label" style="font-size:12px;">Rp 0</span></div>
<div style="font-weight:800; color:#64748b; font-size:12px;">TOTAL KASBON: <span id="pg-rekap-kasbon" class="total-label" style="font-size:12px;">Rp 0</span></div>
</div>
<div id="pg-rekap-total" class="total-label">Rp 0</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th>Nama</th>
<th>Gaji</th>
<th>Kasbon</th>
<th>Total</th>
</tr>
</thead>
<tbody id="body-pegawai-rekap"></tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">👥 Daftar Pegawai</div>
<div style="display:flex; gap:8px; width: 100%;">
<button class="btn btn-secondary" style="flex:1" onclick="tambahBarisPegawaiList()"> TAMBAH</button>
<button class="btn btn-primary" style="flex:1" onclick="simpanDaftarPegawai()">💾 SIMPAN</button>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th class="row-num">#</th>
<th>Nama</th>
<th>Status</th>
<th>Catatan</th>
<th></th>
</tr>
</thead>
<tbody id="body-pegawai-list"></tbody>
</table>
</div>
</div>
</div>
<!-- SECTION RINGKASAN -->
<div id="section-ringkasan" class="section" style="display:none;">
<div class="card">
<div class="card-header" style="background: #111; color: white;">
<div class="card-title" style="color: white;">📊 Ringkasan Hari Ini</div>
<div style="display:flex; gap:8px; width: 100%;">
<button class="btn btn-primary" style="background:#16a34a; flex:1" onclick="kirimRingkasanWA()">📱 KIRIM WA</button>
<button class="btn btn-secondary" style="flex:1" onclick="loadData()">🔄 REFRESH</button>
</div>
</div>
<div id="ringkasan-content" style="padding: 15px;"></div>
<div id="chart-container" style="padding: 15px; border-top: 1px solid #eee;">
<h3 style="border-bottom: 2px solid #e11d48; padding-bottom: 5px; font-size: 14px;">📈 GRAFIK PENGELUARAN</h3>
<canvas id="expenseChart" style="max-height: 250px;"></canvas>
</div>
</div>
</div>
</div>
<!-- Include Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
var currentTab = 'stok_atas';
var dataMaster = { menu: [], stok: [], belanja: [], operasional: [], pegawai: [], pegawaiList: [] };
var currentUser = null;
var selectedDate = null;
var ringkasanOverride = null;
var ringkasanOverrideRange = null;
var users = {
'mama': 'mama123',
'ridho': 'ridho123',
'tanto': 'tanto123',
'rivan': 'rivan123',
'admin': 'admin123'
};
var listPegawai = ['Mama', 'Ridho', 'Tanto', 'Rivan'];
function parseRupiahValue(v) {
var s = String(v == null ? '' : v);
var digits = s.replace(/[^\d]/g, '');
return Number(digits) || 0;
}
function formatRupiahValue(n) {
return (Number(n) || 0).toLocaleString('id-ID');
}
function onCurrencyInput(el) {
if (!el) return;
var raw = parseRupiahValue(el.value);
el.dataset.raw = String(raw);
el.value = raw ? formatRupiahValue(raw) : '';
try { el.setSelectionRange(el.value.length, el.value.length); } catch (e) {}
}
function normalizeKey_(s) {
return String(s || '').trim().toLowerCase();
}
function collapseSpaces_(s) {
return String(s || '').trim().replace(/\s+/g, ' ');
}
function canonicalizeFromDefaults_(name, defaults) {
var key = normalizeKey_(name);
if (!key) return '';
for (var i = 0; i < (defaults || []).length; i++) {
var d = String(defaults[i] || '');
if (normalizeKey_(d) === key) return d;
}
return collapseSpaces_(name);
}
function normalizeBelanjaNameInput(el) {
if (!el) return;
var defaults = defaultBelanja[currentTab] || [];
var v = canonicalizeFromDefaults_(el.value, defaults);
el.value = v;
}
var defaultStok = {
'stok_atas': [
'Saos BBQ', 'Saos Blackpepper', 'Saos Extra Hot', 'Kecap Asin', 'Kecap Manis', 'Mie Kuning',
'Tomyam', 'Beras', 'Minyak Goreng', 'Gula', 'Royco', 'Puding', 'Susu Kental Manis', 'Sunlight', 'Tisu'
],
'stok_bawah': [
'Beef', 'Saikoro', 'Chikuwa', 'Fish Roll', 'Odeng Original', 'Odeng Pedas', 'Bakso Ikan', 'Bakso Salmon',
'Dumpling Ayam', 'Dumpling Keju', 'Kue Ikan Pedas', 'Tahu Seafood', 'Bakso Sapi', 'Sosis', 'Duo Twister',
'Shrimp Tail', 'Otak-Otak', 'Enoki', 'Soklin Pel', 'Sabun Tangan', 'Pembersih Toilet', 'Air Mineral',
'Teh Pucuk', 'Gas Butana'
],
'stok_showcase': [
'Sate', 'Beef Plate', 'Saikoro Plate', 'Ayam Plate', 'Puding', 'Tahu Plate', 'Selada Plate', 'Pakcoi Plate',
'Enoki Beef Plate', 'Enoki Plate', 'Mie Plate', 'Air Mineral Botol', 'Teh Pucuk Botol'
]
};
var defaultBelanja = {
'belanja_modal': [
'Beef', 'Saikoro', 'Chikuwa', 'Fish Roll', 'Odeng Original', 'Odeng Pedas', 'Bakso Ikan', 'Bakso Salmon',
'Dumpling Ayam', 'Dumpling Keju', 'Kue Ikan Pedas', 'Tahu Seafood', 'Bakso Sapi', 'Sosis', 'Duo Twister',
'Shrimp Tail', 'Otak-Otak', 'Saos BBQ', 'Saos Blackpepper', 'Saos Extra Hot', 'Kecap Asin', 'Kecap Manis',
'Mie Kuning', 'Tomyam', 'Beras', 'Minyak Goreng', 'Gula', 'Royco', 'Puding', 'Susu Kental Manis',
'Sunlight', 'Tisu', 'Keju', 'Teh Biasa', 'Jeruk', 'Garam', 'Tempat Puding', 'Sendok Puding', 'Sedotan',
'Tusuk Gigi', 'Plastik Sampah', 'Plastik Es Batu', 'Plastik Wrapping'
],
'belanja_bawah': ['Soklin Pel', 'Sabun Tangan', 'Pembersih Toilet', 'Gas Butana', 'Air Mineral', 'Teh Pucuk'],
'belanja_dapur': ['Ayam', 'Bawang Putih', 'Cabe', 'Pakcoi', 'Selada', 'Tahu', 'Es Batu', 'Galon', 'Gas'],
'belanja_makan': []
};
window.onload = function() {
var now = new Date();
// Cek session lama jika ada
var savedUser = localStorage.getItem('fuku_user');
if (savedUser) {
currentUser = savedUser;
showApp();
} else {
try { showLoading(false); } catch(e) {}
var login = document.getElementById('login-screen');
var app = document.getElementById('main-app');
if (app) app.style.display = 'none';
if (login) login.style.display = 'flex';
}
};
function getTodayStr() {
return new Date().toISOString().split('T')[0];
}
function formatDateLocal(d) {
var y = d.getFullYear();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
function getMonthRange(dateStr) {
var base = (dateStr && /^\d{4}-\d{2}-\d{2}$/.test(dateStr)) ? new Date(dateStr + 'T00:00:00') : new Date();
var from = new Date(base.getFullYear(), base.getMonth(), 1);
var to = new Date(base.getFullYear(), base.getMonth() + 1, 0);
return { from: formatDateLocal(from), to: formatDateLocal(to) };
}
function initRekapPeriodeInputs() {
var range = getMonthRange(selectedDate || getTodayStr());
var pgFrom = document.getElementById('pg-rekap-from');
var pgTo = document.getElementById('pg-rekap-to');
if (pgFrom && pgTo) {
pgFrom.value = localStorage.getItem('fuku_pg_from') || pgFrom.value || range.from;
pgTo.value = localStorage.getItem('fuku_pg_to') || pgTo.value || range.to;
}
var opFrom = document.getElementById('op-rekap-from');
var opTo = document.getElementById('op-rekap-to');
if (opFrom && opTo) {
opFrom.value = localStorage.getItem('fuku_op_from') || opFrom.value || range.from;
opTo.value = localStorage.getItem('fuku_op_to') || opTo.value || range.to;
}
}
function initDatePicker() {
var el = document.getElementById('date-picker');
if (!el) return;
var saved = localStorage.getItem('fuku_date');
selectedDate = (saved && /^\d{4}-\d{2}-\d{2}$/.test(saved)) ? saved : getTodayStr();
el.value = selectedDate;
}
function onDateChange(val) {
if (!val) return;
selectedDate = val;
localStorage.setItem('fuku_date', selectedDate);
loadData(selectedDate);
}
function prosesLogin() {
var user = document.getElementById('login-user').value.toLowerCase();
var pass = document.getElementById('login-pass').value;
var err = document.getElementById('login-err');
if (users[user] && users[user] === pass) {
currentUser = user;
localStorage.setItem('fuku_user', user);
err.style.display = 'none';
showApp();
} else {
err.style.display = 'block';
}
}
function showApp() {
document.getElementById('login-screen').style.display = 'none';
document.getElementById('main-app').style.display = 'block';
document.getElementById('user-display').innerText = 'User: ' + currentUser.toUpperCase();
if (!dataMaster) dataMaster = { menu: [], stok: [], belanja: [], operasional: [], pegawai: [], pegawaiList: [] };
if (!currentTab) currentTab = 'stok_atas';
initDatePicker();
initRekapPeriodeInputs();
// Tab Admin
if (currentUser === 'admin') {
document.getElementById('tab-pegawai').style.display = 'block';
document.getElementById('tab-operasional').style.display = 'block';
}
try { switchTab(currentTab || 'stok_atas'); } catch (e) {}
loadData(selectedDate || getTodayStr());
}
function logout() {
localStorage.removeItem('fuku_user');
currentUser = null;
dataMaster = null;
currentTab = null;
try { showLoading(false); } catch(e) {}
var login = document.getElementById('login-screen');
var app = document.getElementById('main-app');
if (app) app.style.display = 'none';
if (login) login.style.display = 'flex';
}
function showLoading(s, text) {
document.getElementById('loading').style.display = s ? 'flex' : 'none';
if (text) document.getElementById('loading-text').innerText = text;
}
function switchTab(t) {
currentTab = t;
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
document.getElementById('tab-' + t).classList.add('active');
// Hide all sections
document.querySelectorAll('.section').forEach(s => s.style.display = 'none');
if (t === 'ringkasan') {
document.getElementById('section-ringkasan').style.display = 'block';
renderRingkasan();
} else if (t.startsWith('stok_')) {
document.getElementById('section-stok').style.display = 'block';
renderStokTable();
} else if (t === 'draft_pesanan') {
document.getElementById('section-draft').style.display = 'block';
renderDraftPesanan();
} else if (t === 'pegawai') {
document.getElementById('section-pegawai').style.display = 'block';
initRekapPeriodeInputs();
renderPegawaiTable();
} else if (t === 'operasional') {
document.getElementById('section-operasional').style.display = 'block';
initRekapPeriodeInputs();
renderOperasionalTable();
} else {
document.getElementById('section-belanja').style.display = 'block';
renderBelanjaTable();
}
}
function renderDraftPesanan() {
var body = document.getElementById('body-draft');
if (!body) return;
body.innerHTML = '<tr><td colspan="6" style="color:#94a3b8; font-style:italic;">Memuat harga sebelum...</td></tr>';
refreshDraftPesanan();
}
function refreshDraftPesanan() {
var body = document.getElementById('body-draft');
if (!body) return;
body.innerHTML = '';
var itemsModal = (defaultBelanja['belanja_modal'] || []).slice();
var itemsBawah = (defaultBelanja['belanja_bawah'] || []).slice();
showLoading(true, 'Memuat Draft...');
google.script.run.withSuccessHandler(function(mapModal) {
google.script.run.withSuccessHandler(function(mapBawah) {
showLoading(false);
buildDraftTable(itemsModal, itemsBawah, mapModal || {}, mapBawah || {});
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal memuat harga Bawah.\n' + (err && err.message ? err.message : err));
}).getLastBelanjaHargaMap('BAWAH', itemsBawah);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal memuat harga Modal.\n' + (err && err.message ? err.message : err));
}).getLastBelanjaHargaMap('MODAL', itemsModal);
}
function buildDraftTable(itemsModal, itemsBawah, mapModal, mapBawah) {
var body = document.getElementById('body-draft');
if (!body) return;
body.innerHTML = '';
var idx = 1;
function addRow(kat, name, harga) {
var tr = document.createElement('tr');
var h = Number(harga) || 0;
tr.dataset.harga = String(h);
tr.dataset.nama = String(name || '');
tr.innerHTML =
'<td class="row-num">' + (idx++) + '</td>' +
'<td style="font-weight:900; color:#64748b;">' + kat + '</td>' +
'<td style="font-weight:900;">' + name + '</td>' +
'<td><input type="text" class="dr-harga" value="' + (h ? h.toLocaleString('id-ID') : '') + '" inputmode="numeric" oninput="onCurrencyInput(this); onDraftHargaInput(this)"></td>' +
'<td><input type="number" class="dr-qty" value="0" min="0" oninput="recalcDraftRow(this)"></td>' +
'<td style="text-align:right; font-weight:900;"><span class="dr-total">0</span></td>';
body.appendChild(tr);
}
(itemsModal || []).forEach(function(n) { addRow('MODAL', n, mapModal[n]); });
(itemsBawah || []).forEach(function(n) { addRow('BAWAH', n, mapBawah[n]); });
recalcGrandTotalDraft();
}
function onDraftHargaInput(el) {
var tr = el.closest('tr');
if (!tr) return;
var harga = parseRupiahValue(el.value);
tr.dataset.harga = String(harga || 0);
recalcDraftRow(el);
}
function recalcDraftRow(el) {
var tr = el.closest('tr');
if (!tr) return;
var harga = Number(tr.dataset.harga) || 0;
var qty = Number(tr.querySelector('.dr-qty').value) || 0;
var total = harga * qty;
var box = tr.querySelector('.dr-total');
if (box) box.innerText = total.toLocaleString('id-ID');
recalcGrandTotalDraft();
}
function recalcGrandTotalDraft() {
var total = 0;
var lines = [];
document.querySelectorAll('#body-draft tr').forEach(function(tr) {
var harga = Number(tr.dataset.harga) || 0;
var qty = Number((tr.querySelector('.dr-qty') || {}).value) || 0;
total += harga * qty;
if (qty > 0) {
var nama = String(tr.dataset.nama || '').trim();
if (nama) lines.push(nama + ' ' + qty);
}
});
var box = document.getElementById('grand-total-draft');
if (box) box.innerText = 'Rp ' + total.toLocaleString('id-ID');
var picked = document.getElementById('draft-picked');
if (picked) picked.value = lines.join('\n');
}
function renderRingkasan() {
var box = document.getElementById('ringkasan-content');
box.innerHTML = '';
var base = ringkasanOverride || dataMaster;
var isRange = !!ringkasanOverrideRange;
var dateLabel = isRange ? (ringkasanOverrideRange.from + ' s/d ' + ringkasanOverrideRange.to) : (selectedDate || getTodayStr());
var monthRange = getMonthRange(selectedDate || getTodayStr());
var rkFrom = localStorage.getItem('fuku_rk_from') || monthRange.from;
var rkTo = localStorage.getItem('fuku_rk_to') || monthRange.to;
var filterHtml = '<div style="background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:12px; margin-bottom:12px;">' +
'<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">' +
'<div style="font-weight:900; color:#0f172a; font-size:12px; flex:1; min-width:160px;">Filter Ringkasan: <span style="color:#64748b; font-weight:800;">' + dateLabel + '</span></div>' +
'<input id="rk-from" type="date" value="' + rkFrom + '" style="flex:1; min-width:150px;" onchange="localStorage.setItem(\'fuku_rk_from\', this.value)">' +
'<input id="rk-to" type="date" value="' + rkTo + '" style="flex:1; min-width:150px;" onchange="localStorage.setItem(\'fuku_rk_to\', this.value)">' +
'<button class="btn btn-secondary" style="flex:1; min-width:120px;" onclick="applyRingkasanFilter()">LIHAT</button>' +
'<button class="btn btn-secondary" style="flex:1; min-width:120px;" onclick="clearRingkasanFilter()">HARIAN</button>' +
'<button class="btn btn-primary" style="flex:1; min-width:140px;" onclick="saveAllRingkasanChanges()">SIMPAN SEMUA</button>' +
'</div>' +
'</div>';
var stokHtml = '<h3 style="border-bottom: 2px solid #e11d48; padding-bottom: 5px; font-size:13px;">📦 RINGKASAN STOK</h3>';
var locations = ['Lantai Atas', 'Lantai Bawah', 'Showcase'];
var hasStok = false;
locations.forEach(function(loc) {
var items = (base.stok || []).filter(function(s) {
return s.lokasi === loc && (s.stokAwal > 0 || s.restock > 0 || s.terpakai > 0);
});
if (!items.length) return;
if (isRange) items.sort(function(a, b) { return String(a.tanggal || '').localeCompare(String(b.tanggal || '')) || String(a.menu || '').localeCompare(String(b.menu || '')); });
hasStok = true;
var grouped = {};
items.forEach(function(it) {
var t = isRange ? String(it.tanggal || '') : (selectedDate || getTodayStr());
if (!grouped[t]) grouped[t] = [];
grouped[t].push(it);
});
var dates = Object.keys(grouped).sort();
stokHtml += '<div style="margin-bottom: 12px;">' +
'<b style="color: #64748b; font-size: 11px;">' + loc.toUpperCase() + '</b>';
dates.forEach(function(tgl) {
var list = grouped[tgl] || [];
var open = (isRange && ringkasanOverrideRange && String(ringkasanOverrideRange.to) === String(tgl)) ? ' open' : '';
if (isRange) {
stokHtml += '<details class="rk-day"' + open + '><summary><span>' + tgl + '</span><span style="color:#64748b; font-weight:800; font-size:11px;">' + list.length + ' item</span></summary>';
}
stokHtml += '<div class="table-container" style="margin-top:6px;">' +
'<table class="rk-table">' +
'<tr><th>Item</th><th>Sisa</th><th></th></tr>';
list.forEach(function(it) {
var menuObj = (base.menu || []).find(function(m) { return m.nama === it.menu; });
var isLow = it.sisa <= (menuObj ? menuObj.minStok : 0) && it.sisa > 0;
var style = isLow ? ' style="background:#fee2e2;"' : '';
stokHtml += '<tr' + style + ' data-tanggal="' + tgl + '" data-lokasi="' + String(loc) + '" data-menu="' + String(it.menu || '') + '">' +
'<td style="font-weight:800;">' + String(it.menu || '') + '</td>' +
'<td><input type="number" class="rk-st-sisa" value="' + (Number(it.sisa) || 0) + '" readonly></td>' +
'<td class="rk-actions">' +
'<button class="btn btn-secondary rk-mini" onclick="toggleRingkasanEdit(this)">EDIT</button> ' +
'<button class="btn btn-secondary rk-mini" onclick="saveStokFromRingkasanRow(this)">SIMPAN</button> ' +
'<button class="btn btn-danger rk-mini" onclick="deleteStokFromRingkasanRow(this)">HAPUS</button>' +
'</td>' +
'</tr>' +
'<tr class="rk-edit-row" style="display:none">' +
'<td colspan="3" class="rk-editbox">' +
'<div class="rk-editgrid-3">' +
'<div><div style="font-weight:800; font-size:11px; color:#64748b; margin-bottom:4px;">Awal</div><input type="number" class="rk-st-awal" value="' + (Number(it.stokAwal) || 0) + '" oninput="markRingkasanDirty(this); recalcRingkasanStokRow(this)"></div>' +
'<div><div style="font-weight:800; font-size:11px; color:#64748b; margin-bottom:4px;">Re-stock</div><input type="number" class="rk-st-restock" value="' + (Number(it.restock) || 0) + '" oninput="markRingkasanDirty(this); recalcRingkasanStokRow(this)" style="color:#2563eb; font-weight:bold;"></div>' +
'<div><div style="font-weight:800; font-size:11px; color:#64748b; margin-bottom:4px;">Pakai</div><input type="number" class="rk-st-pakai" value="' + (Number(it.terpakai) || 0) + '" oninput="markRingkasanDirty(this); recalcRingkasanStokRow(this)"></div>' +
'</div>' +
'</td>' +
'</tr>';
});
stokHtml += '</table></div>';
if (isRange) stokHtml += '</details>';
});
stokHtml += '</div>';
});
if (!hasStok) stokHtml += '<p style="color: #aaa; font-style: italic; font-size:11px;">Belum ada data stok.</p>';
var belanjaHtml = '<h3 style="border-bottom: 2px solid #e11d48; padding-bottom: 5px; margin-top: 25px; font-size:13px;">🛒 RINGKASAN BELANJA</h3>';
var categories = ['MODAL', 'BAWAH', 'DAPUR', 'MAKAN'];
var hasBelanja = false;
var grandTotal = 0;
var chartData = { labels: [], values: [] };
categories.forEach(function(cat) {
var items = (base.belanja || []).filter(function(b) { return b.kategori === cat && (Number(b.total) || 0) > 0; });
if (!items.length) return;
if (isRange) items.sort(function(a, b) { return String(a.tanggal || '').localeCompare(String(b.tanggal || '')) || String(a.nama || '').localeCompare(String(b.nama || '')); });
hasBelanja = true;
var catTotal = items.reduce(function(a, b) { return a + (Number(b.total) || 0); }, 0);
grandTotal += catTotal;
chartData.labels.push(cat);
chartData.values.push(catTotal);
var grouped = {};
items.forEach(function(it) {
var t = isRange ? String(it.tanggal || '') : (selectedDate || getTodayStr());
if (!grouped[t]) grouped[t] = [];
grouped[t].push(it);
});
var dates = Object.keys(grouped).sort();
belanjaHtml += '<div style="margin-bottom: 12px;">' +
'<b style="color: #64748b; font-size: 11px;">BELANJA ' + cat + '</b>';
dates.forEach(function(tgl) {
var list = grouped[tgl] || [];
var totalTgl = list.reduce(function(a, b) { return a + (Number(b.total) || 0); }, 0);
var open = (isRange && ringkasanOverrideRange && String(ringkasanOverrideRange.to) === String(tgl)) ? ' open' : '';
if (isRange) {
belanjaHtml += '<details class="rk-day"' + open + '><summary><span>' + tgl + '</span><span style="color:#64748b; font-weight:800; font-size:11px;">Rp ' + totalTgl.toLocaleString('id-ID') + '</span></summary>';
}
belanjaHtml += '<div class="table-container" style="margin-top:6px;">' +
'<table class="rk-table">' +
'<tr><th>Barang</th><th>Qty</th><th>Total</th><th></th></tr>';
list.forEach(function(it) {
var total = Number(it.total) || 0;
belanjaHtml += '<tr data-id="' + String(it.id || '') + '" data-tanggal="' + tgl + '" data-kategori="' + String(cat) + '" data-nama="' + String(it.nama || '') + '">' +
'<td style="font-weight:800;">' + String(it.nama || '') + '</td>' +
'<td><input type="number" class="rk-bl-qty" value="' + (Number(it.qty) || 0) + '" oninput="markRingkasanDirty(this); recalcRingkasanBelanjaRow(this)"></td>' +
'<td><input type="text" class="rk-bl-total" value="' + (total ? formatRupiahValue(total) : '') + '" data-raw="' + total + '" readonly></td>' +
'<td class="rk-actions">' +
'<button class="btn btn-secondary rk-mini" onclick="toggleRingkasanEdit(this)">EDIT</button> ' +
'<button class="btn btn-secondary rk-mini" onclick="saveBelanjaFromRingkasanRow(this)">SIMPAN</button> ' +
'<button class="btn btn-danger rk-mini" onclick="deleteBelanjaFromRingkasanRow(this)">HAPUS</button>' +
'</td>' +
'</tr>' +
'<tr class="rk-edit-row" style="display:none">' +
'<td colspan="4" class="rk-editbox">' +
'<div class="rk-editgrid">' +
'<div><div style="font-weight:800; font-size:11px; color:#64748b; margin-bottom:4px;">Harga</div><input type="text" class="rk-bl-harga" value="' + ((Number(it.harga) || 0) ? formatRupiahValue(it.harga) : '') + '" inputmode="numeric" oninput="markRingkasanDirty(this); onCurrencyInput(this); recalcRingkasanBelanjaRow(this)"></div>' +
'<div><div style="font-weight:800; font-size:11px; color:#64748b; margin-bottom:4px;">Catatan</div><input type="text" class="rk-bl-cat" value="' + String(it.catatan || '') + '" oninput="markRingkasanDirty(this)"></div>' +
'</div>' +
'</td>' +
'</tr>';
});
belanjaHtml += '</table></div>';
if (isRange) belanjaHtml += '</details>';
});
belanjaHtml += '</div>';
});
var opTotal = (base.operasional || []).reduce(function(a, b) { return a + (Number(b.total) || 0); }, 0);
if (opTotal > 0) {
grandTotal += opTotal;
chartData.labels.push('OPERASIONAL');
chartData.values.push(opTotal);
}
var pgTotal = (base.pegawai || []).reduce(function(a, b) { return a + (Number(b.nilai) || 0); }, 0);
if (pgTotal > 0 && currentUser === 'admin') {
grandTotal += pgTotal;
chartData.labels.push('PEGAWAI');
chartData.values.push(pgTotal);
}
if (!hasBelanja && opTotal === 0 && pgTotal === 0) {
belanjaHtml += '<p style="color: #aaa; font-style: italic; font-size:11px;">Belum ada data pengeluaran.</p>';
document.getElementById('chart-container').style.display = 'none';
} else {
document.getElementById('chart-container').style.display = 'block';
belanjaHtml += '<div style="text-align: right; font-weight: 900; font-size: 14px; margin-top: 10px; color: #e11d48;">' +
'TOTAL PENGELUARAN: Rp ' + grandTotal.toLocaleString('id-ID') +
'</div>';
setTimeout(function() { renderChart(chartData); }, 100);
}
box.innerHTML = filterHtml + stokHtml + belanjaHtml;
}
function applyRingkasanFilter() {
var from = (document.getElementById('rk-from') && document.getElementById('rk-from').value) || '';
var to = (document.getElementById('rk-to') && document.getElementById('rk-to').value) || '';
if (!from || !to) { alert('Pilih tanggal awal dan akhir.'); return; }
localStorage.setItem('fuku_rk_from', from);
localStorage.setItem('fuku_rk_to', to);
if (from === to) {
ringkasanOverride = null;
ringkasanOverrideRange = null;
currentTab = 'ringkasan';
loadData(from);
return;
}
loadRingkasanPeriode(from, to);
}
function clearRingkasanFilter() {
ringkasanOverride = null;
ringkasanOverrideRange = null;
localStorage.removeItem('fuku_rk_from');
localStorage.removeItem('fuku_rk_to');
renderRingkasan();
}
function loadRingkasanPeriode(from, to) {
showLoading(true, 'Mengambil Ringkasan...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
ringkasanOverride = res || null;
ringkasanOverrideRange = res && res.from && res.to ? { from: String(res.from), to: String(res.to) } : { from: from, to: to };
renderRingkasan();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal mengambil ringkasan.\n' + (err && err.message ? err.message : err));
}).getRingkasanPeriode(from, to);
}
function refreshRingkasanAfterMutation() {
if (ringkasanOverrideRange && ringkasanOverrideRange.from && ringkasanOverrideRange.to) {
loadRingkasanPeriode(ringkasanOverrideRange.from, ringkasanOverrideRange.to);
return;
}
currentTab = 'ringkasan';
loadData(selectedDate || getTodayStr());
}
function toggleRingkasanEdit(btn) {
var tr = btn.closest('tr');
if (!tr) return;
var edit = tr.nextElementSibling;
if (!edit || !edit.classList.contains('rk-edit-row')) return;
edit.style.display = (edit.style.display === 'none' || !edit.style.display) ? 'table-row' : 'none';
}
function markRingkasanDirty(el) {
var tr = el.closest('tr');
if (!tr) return;
var dataRow = tr.dataset && (tr.dataset.nama || tr.dataset.menu) ? tr : tr.previousElementSibling;
if (!dataRow) return;
dataRow.dataset.dirty = '1';
dataRow.classList.add('rk-dirty');
}
function recalcRingkasanBelanjaRow(el) {
var tr = el.closest('tr');
if (!tr) return;
var dataRow = tr.dataset && (tr.dataset.nama || tr.dataset.menu) ? tr : tr.previousElementSibling;
if (!dataRow) return;
var editRow = dataRow.nextElementSibling;
var qtyEl = dataRow.querySelector('.rk-bl-qty');
var hargaEl = editRow && editRow.querySelector ? editRow.querySelector('.rk-bl-harga') : null;
var totalEl = dataRow.querySelector('.rk-bl-total');
if (!qtyEl || !totalEl) return;
var qty = Number(qtyEl.value) || 0;
var harga = hargaEl ? parseRupiahValue(hargaEl.value) : 0;
var total = harga * qty;
totalEl.dataset.raw = String(total);
totalEl.value = total ? formatRupiahValue(total) : '';
}
function saveAllRingkasanChanges() {
var container = document.getElementById('ringkasan-content');
if (!container) return;
var dirtyRows = Array.from(container.querySelectorAll('tr.rk-dirty'));
if (!dirtyRows.length) { alert('Tidak ada perubahan untuk disimpan.'); return; }
var belanjaUpdates = [];
var stokUpdates = [];
dirtyRows.forEach(function(dataRow) {
if (dataRow.querySelector('.rk-bl-qty')) {
var editRow = dataRow.nextElementSibling;
var id = String(dataRow.dataset.id || '').trim();
if (!id) return;
var qty = Number((dataRow.querySelector('.rk-bl-qty') || {}).value) || 0;
var harga = parseRupiahValue((editRow && editRow.querySelector('.rk-bl-harga')) ? editRow.querySelector('.rk-bl-harga').value : '');
var total = Number((dataRow.querySelector('.rk-bl-total') || {}).dataset ? dataRow.querySelector('.rk-bl-total').dataset.raw : 0) || (harga * qty);
var catatan = (editRow && editRow.querySelector('.rk-bl-cat')) ? editRow.querySelector('.rk-bl-cat').value : '';
belanjaUpdates.push({ id: id, harga: harga, qty: qty, total: total, catatan: catatan });
return;
}
if (dataRow.querySelector('.rk-st-sisa')) {
var editRow = dataRow.nextElementSibling;
var tgl = String(dataRow.dataset.tanggal || '').trim() || (selectedDate || getTodayStr());
var loc = String(dataRow.dataset.lokasi || '').trim();
var menu = String(dataRow.dataset.menu || '').trim();
var awal = editRow && editRow.querySelector('.rk-st-awal') ? Number(editRow.querySelector('.rk-st-awal').value) || 0 : 0;
var restock = editRow && editRow.querySelector('.rk-st-restock') ? Number(editRow.querySelector('.rk-st-restock').value) || 0 : 0;
var pakai = editRow && editRow.querySelector('.rk-st-pakai') ? Number(editRow.querySelector('.rk-st-pakai').value) || 0 : 0;
stokUpdates.push({ tanggal: tgl, lokasi: loc, menu: menu, stokAwal: awal, restock: restock, terpakai: pakai });
}
});
if (!belanjaUpdates.length && !stokUpdates.length) { alert('Tidak ada perubahan valid untuk disimpan.'); return; }
showLoading(true, 'Menyimpan Semua...');
google.script.run.withSuccessHandler(function() {
if (!stokUpdates.length) {
showLoading(false);
refreshRingkasanAfterMutation();
return;
}
google.script.run.withSuccessHandler(function() {
showLoading(false);
refreshRingkasanAfterMutation();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan stok.\n' + (err && err.message ? err.message : err));
}).bulkUpdateStokItems(stokUpdates, currentUser);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan belanja.\n' + (err && err.message ? err.message : err));
}).bulkUpdateBelanjaById(belanjaUpdates, currentUser);
}
function saveBelanjaFromRingkasanRow(btn) {
var tr = btn.closest('tr');
if (!tr) return;
var dataRow = tr.dataset && (tr.dataset.nama || tr.dataset.menu) ? tr : tr.previousElementSibling;
if (!dataRow) return;
var editRow = dataRow.nextElementSibling;
var id = String(dataRow.dataset.id || '').trim();
var tgl = String(dataRow.dataset.tanggal || '').trim();
var kat = String(dataRow.dataset.kategori || '').trim();
var nama = String(dataRow.dataset.nama || '').trim();
var qty = Number((dataRow.querySelector('.rk-bl-qty') || {}).value) || 0;
var harga = parseRupiahValue((editRow && editRow.querySelector('.rk-bl-harga') ? editRow.querySelector('.rk-bl-harga').value : ''));
var total = Number((dataRow.querySelector('.rk-bl-total') || {}).dataset ? dataRow.querySelector('.rk-bl-total').dataset.raw : 0) || (harga * qty);
var catatan = editRow && editRow.querySelector('.rk-bl-cat') ? editRow.querySelector('.rk-bl-cat').value : '';
showLoading(true, 'Menyimpan...');
if (id) {
google.script.run.withSuccessHandler(function() {
showLoading(false);
refreshRingkasanAfterMutation();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan belanja.\n' + (err && err.message ? err.message : err));
}).updateBelanjaById(id, { harga: harga, qty: qty, total: total, catatan: catatan }, currentUser);
return;
}
google.script.run.withSuccessHandler(function() {
showLoading(false);
refreshRingkasanAfterMutation();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan belanja.\n' + (err && err.message ? err.message : err));
}).updateBelanjaItem(tgl, kat, nama, harga, qty, total, catatan, currentUser);
}
function deleteBelanjaFromRingkasanRow(btn) {
var tr = btn.closest('tr');
if (!tr) return;
var dataRow = tr.dataset && (tr.dataset.nama || tr.dataset.menu) ? tr : tr.previousElementSibling;
if (!dataRow) return;
var id = String(dataRow.dataset.id || '').trim();
var tgl = String(dataRow.dataset.tanggal || '').trim();
var kat = String(dataRow.dataset.kategori || '').trim();
var nama = String(dataRow.dataset.nama || '').trim();
if (!confirm('Hapus "' + nama + '"?')) return;
showLoading(true, 'Menghapus...');
if (id) {
google.script.run.withSuccessHandler(function() {
showLoading(false);
refreshRingkasanAfterMutation();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal hapus belanja.\n' + (err && err.message ? err.message : err));
}).deleteBelanjaById(id);
return;
}
google.script.run.withSuccessHandler(function() {
showLoading(false);
refreshRingkasanAfterMutation();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal hapus belanja.\n' + (err && err.message ? err.message : err));
}).deleteBelanjaItem(tgl, kat, nama);
}
function recalcRingkasanStokRow(el) {
var tr = el.closest('tr');
if (!tr) return;
var dataRow = tr.dataset && (tr.dataset.menu || tr.dataset.nama) ? tr : tr.previousElementSibling;
if (!dataRow) return;
var editRow = dataRow.nextElementSibling;
var awal = editRow && editRow.querySelector('.rk-st-awal') ? Number(editRow.querySelector('.rk-st-awal').value) || 0 : 0;
var restock = editRow && editRow.querySelector('.rk-st-restock') ? Number(editRow.querySelector('.rk-st-restock').value) || 0 : 0;
var pakai = editRow && editRow.querySelector('.rk-st-pakai') ? Number(editRow.querySelector('.rk-st-pakai').value) || 0 : 0;
var sisa = Math.max(0, (awal + restock) - pakai);
var sisaEl = dataRow.querySelector('.rk-st-sisa');
if (sisaEl) sisaEl.value = sisa;
}
function saveStokFromRingkasanRow(btn) {
var tr = btn.closest('tr');
if (!tr) return;
var dataRow = tr.dataset && (tr.dataset.menu || tr.dataset.nama) ? tr : tr.previousElementSibling;
if (!dataRow) return;
var editRow = dataRow.nextElementSibling;
var tgl = String(dataRow.dataset.tanggal || '').trim() || (selectedDate || getTodayStr());
var loc = String(dataRow.dataset.lokasi || '').trim();
var menu = String(dataRow.dataset.menu || '').trim();
var awal = editRow && editRow.querySelector('.rk-st-awal') ? Number(editRow.querySelector('.rk-st-awal').value) || 0 : 0;
var restock = editRow && editRow.querySelector('.rk-st-restock') ? Number(editRow.querySelector('.rk-st-restock').value) || 0 : 0;
var pakai = editRow && editRow.querySelector('.rk-st-pakai') ? Number(editRow.querySelector('.rk-st-pakai').value) || 0 : 0;
showLoading(true, 'Menyimpan...');
google.script.run.withSuccessHandler(function() {
showLoading(false);
refreshRingkasanAfterMutation();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal simpan stok.\n' + (err && err.message ? err.message : err));
}).updateStokItem(tgl, loc, menu, awal, restock, pakai, currentUser);
}
function deleteStokFromRingkasanRow(btn) {
var tr = btn.closest('tr');
if (!tr) return;
var dataRow = tr.dataset && (tr.dataset.menu || tr.dataset.nama) ? tr : tr.previousElementSibling;
if (!dataRow) return;
var tgl = String(dataRow.dataset.tanggal || '').trim() || (selectedDate || getTodayStr());
var loc = String(dataRow.dataset.lokasi || '').trim();
var menu = String(dataRow.dataset.menu || '').trim();
if (!confirm('Hapus "' + menu + '"?')) return;
showLoading(true, 'Menghapus...');
google.script.run.withSuccessHandler(function() {
showLoading(false);
refreshRingkasanAfterMutation();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal hapus stok.\n' + (err && err.message ? err.message : err));
}).deleteStokItem(tgl, loc, menu);
}
var myChart = null;
function renderChart(data) {
var ctx = document.getElementById('expenseChart').getContext('2d');
if (myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'pie',
data: {
labels: data.labels,
datasets: [{
data: data.values,
backgroundColor: ['#e11d48', '#111', '#64748b', '#16a34a', '#2563eb', '#f59e0b']
}]
},
options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 10 } } } } }
});
}
function kirimRingkasanWA() {
var now = new Date();
var useDate = selectedDate || getTodayStr();
var dateStr = useDate;
var text = '🔥 *LAPORAN HARIAN FUKU* 🔥\n';
text += '📅 ' + dateStr + '\n';
text += '👤 User: ' + currentUser.toUpperCase() + '\n';
text += '------------------------------------------\n\n';
// Stok
var lowStok = dataMaster.stok.filter(s => {
var menuObj = dataMaster.menu.find(m => m.nama === s.menu);
var min = menuObj ? menuObj.minStok : 0;
return s.sisa <= min && s.sisa > 0;
});
if (lowStok.length > 0) {
text += '⚠️ *STOK MENIPIS:*\n';
lowStok.forEach(s => { text += '- ' + s.menu + ': *' + s.sisa + '*\n'; });
text += '\n';
}
// Belanja
text += '🛒 *RINGKASAN PENGELUARAN:*\n';
var grandTotal = 0;
['MODAL', 'BAWAH', 'DAPUR', 'MAKAN'].forEach(cat => {
var items = dataMaster.belanja.filter(b => b.kategori === cat && b.total > 0);
if (items.length > 0) {
var catTotal = items.reduce((a, b) => a + b.total, 0);
text += '\n🔹 *' + cat + '*: Rp ' + catTotal.toLocaleString('id-ID') + '\n';
items.forEach(it => { text += ' - ' + it.nama + ' (' + it.qty + ')\n'; });
grandTotal += catTotal;
}
});
var opTotal = dataMaster.operasional.reduce((a, b) => a + (b.total || 0), 0);
if (opTotal > 0) {
text += '\n🔹 *OPERASIONAL*: Rp ' + opTotal.toLocaleString('id-ID') + '\n';
dataMaster.operasional.forEach(o => { text += ' - ' + o.nama + '\n'; });
grandTotal += opTotal;
}
if (currentUser === 'admin') {
var pgTotal = dataMaster.pegawai.reduce((a, b) => a + (b.nilai || 0), 0);
if (pgTotal > 0) {
text += '\n🔹 *PEGAWAI*: Rp ' + pgTotal.toLocaleString('id-ID') + '\n';
dataMaster.pegawai.forEach(p => { text += ' - ' + p.nama + ' (' + p.kategori + ')\n'; });
grandTotal += pgTotal;
}
}
text += '\n💰 *TOTAL SEMUA: Rp ' + grandTotal.toLocaleString('id-ID') + '*\n';
text += '\n_Laporan otomatis Fuku Stok & Belanja_';
window.open('https://wa.me/?text=' + encodeURIComponent(text), '_blank');
}
function loadData(dateStr) {
showLoading(true, 'Sinkronisasi Data...');
var done = false;
var timeoutId = setTimeout(function() {
if (done) return;
done = true;
showLoading(false);
alert('Sinkronisasi terlalu lama (timeout).\n\nCoba:\n- Refresh halaman\n- Pastikan internet stabil\n- Deploy ulang versi terbaru Stok & Belanja');
}, 25000);
google.script.run.withSuccessHandler(function(res) {
if (done) return;
done = true;
clearTimeout(timeoutId);
try {
if (!res) {
loadDataFallback(dateStr || (selectedDate || getTodayStr()));
return;
}
if (typeof res === 'string') throw new Error(res);
if (!res.menu && res.menuList) res.menu = res.menuList;
if (!res.stok && res.stokData) res.stok = res.stokData;
if (!res.belanja && res.belanjaData) res.belanja = res.belanjaData;
if (!res.operasional && res.operasionalData) res.operasional = res.operasionalData;
if (!res.pegawai && res.pegawaiData) res.pegawai = res.pegawaiData;
if (res.ok === false && res.error) {
alert('Server error:\n' + String(res.error));
}
if (!res.menu || !res.stok || !res.belanja) {
throw new Error('Data dari server tidak lengkap.');
}
dataMaster = {
menu: Array.isArray(res.menu) ? res.menu : [],
stok: Array.isArray(res.stok) ? res.stok : [],
belanja: Array.isArray(res.belanja) ? res.belanja : [],
operasional: Array.isArray(res.operasional) ? res.operasional : [],
pegawai: Array.isArray(res.pegawai) ? res.pegawai : [],
pegawaiList: Array.isArray(res.pegawaiList) ? res.pegawaiList : [],
tanggal: res.tanggal || (dateStr || (selectedDate || getTodayStr())),
ok: res.ok !== false
};
if (dataMaster.pegawaiList && dataMaster.pegawaiList.length) {
listPegawai = dataMaster.pegawaiList
.filter(p => String((p && p.status) || 'Aktif').toLowerCase() !== 'nonaktif')
.map(p => String(p.nama || '').trim())
.filter(Boolean);
}
if (res && res.tanggal) selectedDate = String(res.tanggal);
var dp = document.getElementById('date-picker');
if (dp && selectedDate) dp.value = selectedDate;
initRekapPeriodeInputs();
switchTab(currentTab || 'stok_atas');
} catch (e) {
console.error(e);
alert('Gagal memproses data.\n' + (e && e.message ? e.message : e));
} finally {
showLoading(false);
}
}).withFailureHandler(function(err) {
if (done) return;
done = true;
clearTimeout(timeoutId);
showLoading(false);
alert('Gagal sinkronisasi data.\n' + (err && err.message ? err.message : err));
}).getInitialData(dateStr || (selectedDate || getTodayStr()));
}
function loadDataFallback(dateStr) {
var useDate = dateStr || (selectedDate || getTodayStr());
showLoading(true, 'Sinkronisasi Data...');
var out = { tanggal: useDate, menu: [], stok: [], belanja: [], operasional: [], pegawai: [], pegawaiList: [] };
var pending = 6;
var failed = false;
function doneOne() {
pending--;
if (pending !== 0 || failed) return;
dataMaster = {
menu: out.menu,
stok: out.stok,
belanja: out.belanja,
operasional: out.operasional,
pegawai: out.pegawai,
pegawaiList: out.pegawaiList,
tanggal: out.tanggal,
ok: true
};
selectedDate = out.tanggal;
var dp = document.getElementById('date-picker');
if (dp && selectedDate) dp.value = selectedDate;
if (dataMaster.pegawaiList && dataMaster.pegawaiList.length) {
listPegawai = dataMaster.pegawaiList
.filter(p => String((p && p.status) || 'Aktif').toLowerCase() !== 'nonaktif')
.map(p => String(p.nama || '').trim())
.filter(Boolean);
}
initRekapPeriodeInputs();
showLoading(false);
setTimeout(function() { switchTab(currentTab || 'stok_atas'); }, 0);
}
function fail(label, err) {
if (failed) return;
failed = true;
showLoading(false);
var msg = 'Gagal sinkronisasi (' + label + ').\n' + (err && err.message ? err.message : err);
if (google && google.script && google.script.run && typeof google.script.run.getDebugInfo === 'function') {
google.script.run.withSuccessHandler(function(info) {
alert(msg + '\n\nDebug:\n' + String(info || '(kosong)'));
}).withFailureHandler(function() {
alert(msg);
}).getDebugInfo();
} else {
alert(msg);
}
}
google.script.run.withSuccessHandler(function(menu) { out.menu = Array.isArray(menu) ? menu : []; doneOne(); })
.withFailureHandler(function(err) { fail('menu', err); }).getMenuListExtended();
google.script.run.withSuccessHandler(function(stok) { out.stok = Array.isArray(stok) ? stok : []; doneOne(); })
.withFailureHandler(function(err) { fail('stok', err); }).getStokData(useDate, false);
google.script.run.withSuccessHandler(function(belanja) { out.belanja = Array.isArray(belanja) ? belanja : []; doneOne(); })
.withFailureHandler(function(err) { fail('belanja', err); }).getBelanjaData(useDate);
google.script.run.withSuccessHandler(function(operasional) { out.operasional = Array.isArray(operasional) ? operasional : []; doneOne(); })
.withFailureHandler(function(err) { fail('operasional', err); }).getOperasionalData(useDate);
google.script.run.withSuccessHandler(function(pegawaiList) { out.pegawaiList = Array.isArray(pegawaiList) ? pegawaiList : []; doneOne(); })
.withFailureHandler(function(err) { fail('pegawai list', err); }).getPegawaiList();
google.script.run.withSuccessHandler(function(pegawai) { out.pegawai = Array.isArray(pegawai) ? pegawai : []; doneOne(); })
.withFailureHandler(function(err) { fail('pegawai', err); }).getPegawaiData(useDate);
}
function renderStokTable() {
var body = document.getElementById('body-stok');
body.innerHTML = '';
var lokasi = currentTab === 'stok_atas' ? 'Lantai Atas' : (currentTab === 'stok_bawah' ? 'Lantai Bawah' : 'Showcase');
document.getElementById('stok-title').innerText = '📦 Stok ' + lokasi;
document.getElementById('th-foto').style.display = (currentTab === 'stok_showcase' ? 'table-cell' : 'none');
document.getElementById('btn-import-porsi').style.display = (currentTab === 'stok_showcase' ? 'inline-flex' : 'none');
var saved = dataMaster.stok.filter(s => s.lokasi === lokasi);
var defaults = defaultStok[currentTab] || [];
defaults.forEach(function(nama) {
var s = saved.find(x => x.menu === nama) || { stokAwal: 0, restock: 0, terpakai: 0, sisa: 0, foto: '' };
tambahBarisStok(nama, s);
});
saved.forEach(function(s) {
if (defaults.indexOf(s.menu) === -1) tambahBarisStok(s.menu, s, false);
});
for(var i=0; i<2; i++) tambahBarisStok('', { stokAwal: 0, restock: 0, terpakai: 0, sisa: 0, foto: '' });
if (currentTab === 'stok_showcase') loadShowcaseFotos();
}
function loadShowcaseFotos() {
var useDate = selectedDate || getTodayStr();
showLoading(true, 'Memuat foto...');
google.script.run.withSuccessHandler(function(map) {
try {
var fotoMap = map || {};
document.querySelectorAll('#body-stok tr').forEach(function(tr) {
var nmEl = tr.querySelector('.st-nama');
var ta = tr.querySelector('.st-foto');
var cont = tr.querySelector('.photo-preview-container');
if (!nmEl || !ta || !cont) return;
var nama = String(nmEl.value || '').trim();
if (!nama) return;
var foto = fotoMap[nama];
if (foto == null) return;
ta.value = String(foto || '');
cont.innerHTML = '';
var photos = String(foto || '').split('|').filter(function(x) { return x; });
photos.forEach(function(p) {
var img = document.createElement('img');
img.src = p;
img.className = 'photo-preview';
img.onclick = function() { viewPhoto(p); };
cont.appendChild(img);
});
});
} finally {
showLoading(false);
}
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal memuat foto showcase.\n' + (err && err.message ? err.message : err));
}).getStokFotoMap(useDate, 'Showcase');
}
function tambahBarisStok(nama, data, isDefault = true) {
var body = document.getElementById('body-stok');
var tr = document.createElement('tr');
var i = body.children.length;
var s = data || { stokAwal: 0, restock: 0, terpakai: 0, sisa: 0, foto: '' };
var menuObj = dataMaster.menu.find(m => m.nama === nama);
var min = menuObj ? menuObj.minStok : 0;
var isLow = s.sisa <= min && s.sisa > 0;
if (isLow) tr.style.background = '#fee2e2';
var isReadOnly = (nama && isDefault) ? 'readonly style="background:transparent; border:none; font-weight:800;"' : '';
var thresholdText = min > 0 ? '<div style="font-size:9px; color:#94a3b8;">Min: ' + min + '</div>' : '';
var photoHtml = '';
if (currentTab === 'stok_showcase') {
var photos = (s.foto || '').split('|').filter(x => x);
var previews = photos.map(p => '<img src="' + p + '" class="photo-preview" onclick="viewPhoto(\'' + p + '\')">').join('');
photoHtml = '<td><div class="photo-btn" onclick="this.nextElementSibling.click()">📷</div>' +
'<input type="file" accept="image/*" style="display:none" onchange="handlePhoto(this)" multiple>' +
'<div class="photo-preview-container">' + previews + '</div>' +
'<textarea class="st-foto" style="display:none">' + (s.foto || '') + '</textarea></td>';
}
tr.innerHTML = '<td class="row-num">' + (i + 1) + '</td>' +
'<td><input type="text" class="st-nama" value="' + (nama || '') + '" ' + isReadOnly + '>' + thresholdText + '</td>' +
'<td><input type="number" class="st-awal" value="' + s.stokAwal + '" oninput="recalcRowStok(this)"></td>' +
'<td><input type="number" class="st-restock" value="' + (s.restock || 0) + '" oninput="recalcRowStok(this)" style="color:#2563eb; font-weight:bold;"></td>' +
'<td><input type="number" class="st-pakai" value="' + s.terpakai + '" oninput="recalcRowStok(this)"></td>' +
'<td><input type="number" class="st-sisa" value="' + s.sisa + '" readonly></td>' +
photoHtml +
'<td>' + (nama ? '' : '<button class="btn btn-danger" onclick="this.closest(\'tr\').remove()">🗑️</button>') + '</td>';
body.appendChild(tr);
}
function recalcRowStok(el) {
var tr = el.closest('tr');
var awal = Number(tr.querySelector('.st-awal').value) || 0;
var restock = Number(tr.querySelector('.st-restock').value) || 0;
var pakai = Number(tr.querySelector('.st-pakai').value) || 0;
var sisa = Math.max(0, (awal + restock) - pakai);
tr.querySelector('.st-sisa').value = sisa;
var nama = tr.querySelector('.st-nama').value;
var menuObj = dataMaster.menu.find(m => m.nama === nama);
var min = menuObj ? menuObj.minStok : 0;
tr.style.background = (sisa <= min && sisa > 0) ? '#fee2e2' : 'transparent';
}
function parseCsvLineSafe(line) {
var parts = [];
var current = '';
var inQuotes = false;
for (var i = 0; i < line.length; i++) {
var ch = line[i];
if (ch === '"') {
if (inQuotes && line[i + 1] === '"') {
current += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if ((ch === ',' || ch === ';') && !inQuotes) {
parts.push(current);
current = '';
} else {
current += ch;
}
}
parts.push(current);
return parts.map(function(p) { return String(p).trim().replace(/^"|"$/g, ''); });
}
function normalizeKey(s) {
return String(s || '').trim().toLowerCase().replace(/\s+/g, ' ');
}
function importPorsiTerjualCSV() {
if (currentTab !== 'stok_showcase') { alert('Buka tab SHOWCASE terlebih dahulu.'); return; }
var input = document.createElement('input');
input.type = 'file';
input.accept = '.csv';
input.onchange = function(e) {
var file = e.target.files && e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(re) {
var text = String(re.target.result || '');
var lines = text.split(/\r?\n/).map(function(l) { return String(l || '').trim(); }).filter(function(l) { return l; });
if (lines.length < 2) { alert('CSV kosong / format tidak valid.'); return; }
var header = parseCsvLineSafe(lines[0]).map(function(h) { return normalizeKey(h); });
var idxItem = -1;
var idxQty = -1;
header.forEach(function(h, i) {
if (idxItem === -1 && (h === 'item' || h === 'menu' || h === 'nama')) idxItem = i;
if (idxQty === -1 && (h === 'qty' || h === 'porsi' || h === 'pakai')) idxQty = i;
});
var totals = {};
for (var i = 1; i < lines.length; i++) {
var row = parseCsvLineSafe(lines[i]);
if (!row.length) continue;
var item = '';
var qtyStr = '';
if (idxItem > -1 && idxQty > -1) {
item = row[idxItem] || '';
qtyStr = row[idxQty] || '';
} else if (row.length >= 2) {
item = row[0] || '';
qtyStr = row[1] || '';
} else {
continue;
}
var qty = Number(String(qtyStr).replace(/,/g, '').trim()) || 0;
var key = normalizeKey(item);
if (!key || qty <= 0) continue;
totals[key] = (totals[key] || 0) + qty;
}
var keys = Object.keys(totals);
if (!keys.length) { alert('Tidak ada data Qty yang bisa diproses dari CSV.'); return; }
var rowMap = {};
document.querySelectorAll('#body-stok tr').forEach(function(tr) {
var nm = tr.querySelector('.st-nama');
var pk = tr.querySelector('.st-pakai');
if (!nm || !pk) return;
var k = normalizeKey(nm.value);
if (k) rowMap[k] = pk;
});
var matched = 0;
keys.forEach(function(k) { if (rowMap[k]) matched++; });
if (!matched) { alert('Tidak ada item CSV yang cocok dengan item Showcase.'); return; }
if (!confirm('Akan mengisi kolom Pakai untuk ' + matched + ' item Showcase (overwrite). Lanjutkan?')) return;
keys.forEach(function(k) {
var elPakai = rowMap[k];
if (!elPakai) return;
elPakai.value = totals[k];
recalcRowStok(elPakai);
});
alert('Import selesai. Terisi: ' + matched + ' item.');
};
reader.readAsText(file);
};
input.click();
}
function renderBelanjaTable() {
var body = document.getElementById('body-belanja');
body.innerHTML = '';
var kategori = currentTab.replace('belanja_', '').replace('_', ' ').toUpperCase();
document.getElementById('belanja-title').innerText = '🛒 Belanja ' + kategori;
var syncBtn = document.getElementById('btn-sync-belanja');
var syncAllBtn = document.getElementById('btn-sync-belanja-all');
if (syncBtn) syncBtn.style.display = (currentTab === 'belanja_modal' || currentTab === 'belanja_bawah') ? 'inline-flex' : 'none';
if (syncAllBtn) syncAllBtn.style.display = (currentTab === 'belanja_modal' || currentTab === 'belanja_bawah') ? 'inline-flex' : 'none';
var saved = dataMaster.belanja.filter(b => b.kategori === kategori);
var defaults = defaultBelanja[currentTab] || [];
var defaultKeyMap = {};
defaults.forEach(function(n) { defaultKeyMap[normalizeKey_(n)] = n; });
defaults.forEach(function(nama) {
var key = normalizeKey_(nama);
var match = saved.find(function(x) { return x && normalizeKey_(x.nama) === key; }) || null;
var b = match ? Object.assign({}, match, { nama: nama }) : { nama: nama, harga: 0, qty: 1, total: 0, catatan: '' };
tambahBarisBelanja(b);
});
saved.forEach(function(it) {
if (!it || !it.nama) return;
if (defaultKeyMap[normalizeKey_(it.nama)]) return;
tambahBarisBelanja(it);
});
var emptyRows = (currentTab === 'belanja_makan') ? 5 : 3;
for (var i = 0; i < emptyRows; i++) tambahBarisBelanja();
recalcGrandTotalBelanja();
}
function tambahBarisBelanja(data) {
var body = document.getElementById('body-belanja');
var tr = document.createElement('tr');
var b = data || { nama: '', harga: 0, qty: 1, total: 0, catatan: '' };
if (b && b.id) tr.dataset.id = String(b.id);
tr.innerHTML = '<td class="row-num">' + (body.children.length + 1) + '</td>' +
'<td><input type="text" class="bl-nama" value="' + b.nama + '" onblur="normalizeBelanjaNameInput(this)"></td>' +
'<td><input type="text" class="bl-harga" value="' + (b.harga ? formatRupiahValue(b.harga) : '') + '" inputmode="numeric" oninput="onCurrencyInput(this); recalcRowBelanja(this)"></td>' +
'<td><input type="number" class="bl-qty" value="' + b.qty + '" oninput="recalcRowBelanja(this)"></td>' +
'<td><input type="text" class="bl-total" value="' + (b.total ? formatRupiahValue(b.total) : '') + '" data-raw="' + (Number(b.total) || 0) + '" inputmode="numeric" oninput="onCurrencyInput(this); recalcRowBelanja(this)"></td>' +
'<td><input type="text" class="bl-cat" value="' + b.catatan + '"></td>' +
'<td><button class="btn btn-danger" onclick="this.closest(\'tr\').remove(); recalcGrandTotalBelanja();">🗑️</button></td>';
body.appendChild(tr);
}
function recalcRowBelanja(el) {
var tr = el.closest('tr');
var hargaEl = tr.querySelector('.bl-harga');
var qtyEl = tr.querySelector('.bl-qty');
var totalEl = tr.querySelector('.bl-total');
var qty = Number(qtyEl.value) || 0;
if (el && el.classList && el.classList.contains('bl-total')) {
var total = parseRupiahValue(totalEl.value);
if (qty <= 0) { qty = 1; qtyEl.value = 1; }
var harga2 = qty > 0 ? Math.round(total / qty) : total;
totalEl.dataset.raw = String(total);
totalEl.value = total ? formatRupiahValue(total) : '';
hargaEl.dataset.raw = String(harga2);
hargaEl.value = harga2 ? formatRupiahValue(harga2) : '';
} else {
var harga = parseRupiahValue(hargaEl.value);
var total2 = harga * qty;
totalEl.dataset.raw = String(total2);
totalEl.value = total2 ? formatRupiahValue(total2) : '';
}
recalcGrandTotalBelanja();
}
function recalcGrandTotalBelanja() {
var total = 0;
document.querySelectorAll('.bl-total').forEach(el => total += Number(el.dataset.raw) || parseRupiahValue(el.value) || 0);
document.getElementById('grand-total-belanja').innerText = 'Rp ' + total.toLocaleString('id-ID');
}
// OPERASIONAL
function renderOperasionalTable() {
var body = document.getElementById('body-operasional');
body.innerHTML = '';
dataMaster.operasional.forEach(o => tambahBarisOperasional(o));
for (var i = 0; i < 3; i++) tambahBarisOperasional();
recalcGrandTotalOperasional();
}
function tambahBarisOperasional(o) {
var body = document.getElementById('body-operasional');
var tr = document.createElement('tr');
var d = o || { nama: '', harga: 0, qty: 1, total: 0, catatan: '' };
tr.innerHTML = '<td class="row-num">' + (body.children.length + 1) + '</td>' +
'<td><input type="text" class="op-nama" value="' + d.nama + '" placeholder="Sewa, Listrik, dll..."></td>' +
'<td><input type="text" class="op-harga" value="' + (d.harga ? formatRupiahValue(d.harga) : '') + '" inputmode="numeric" oninput="onCurrencyInput(this); recalcRowOperasional(this)"></td>' +
'<td><input type="number" class="op-qty" value="' + (Number(d.qty) || 1) + '" oninput="recalcRowOperasional(this)"></td>' +
'<td><input type="text" class="op-total" value="' + (d.total ? formatRupiahValue(d.total) : '') + '" inputmode="numeric" oninput="onCurrencyInput(this); recalcRowOperasional(this)"></td>' +
'<td><input type="text" class="op-cat" value="' + d.catatan + '"></td>' +
'<td><button class="btn btn-danger" onclick="this.closest(\'tr\').remove(); recalcGrandTotalOperasional()">🗑️</button></td>';
body.appendChild(tr);
}
function recalcRowOperasional(el) {
var tr = el.closest('tr');
var hargaEl = tr.querySelector('.op-harga');
var qtyEl = tr.querySelector('.op-qty');
var totalEl = tr.querySelector('.op-total');
var qty = Number(qtyEl.value) || 0;
if (el && el.classList && el.classList.contains('op-total')) {
var total = parseRupiahValue(totalEl.value);
if (qty <= 0) { qty = 1; qtyEl.value = 1; }
var harga2 = qty > 0 ? Math.round(total / qty) : total;
totalEl.value = total ? formatRupiahValue(total) : '';
hargaEl.value = harga2 ? formatRupiahValue(harga2) : '';
} else {
var harga = parseRupiahValue(hargaEl.value);
if (qty <= 0) qty = 1;
var total2 = harga * qty;
totalEl.value = total2 ? formatRupiahValue(total2) : '';
}
recalcGrandTotalOperasional();
}
function recalcGrandTotalOperasional() {
var total = 0;
document.querySelectorAll('.op-total').forEach(el => { total += parseRupiahValue(el.value); });
var box = document.getElementById('grand-total-operasional');
if (box) box.innerText = 'Rp ' + total.toLocaleString('id-ID');
}
function simpanSemuaOperasional() {
var rows = [];
document.querySelectorAll('#body-operasional tr').forEach(tr => {
var nama = tr.querySelector('.op-nama').value;
if (!nama) return;
rows.push({
nama: nama,
harga: parseRupiahValue(tr.querySelector('.op-harga').value),
qty: Number(tr.querySelector('.op-qty').value) || 1,
total: parseRupiahValue(tr.querySelector('.op-total').value),
catatan: tr.querySelector('.op-cat').value
});
});
showLoading(true, 'Menyimpan...');
google.script.run.withSuccessHandler(function(res) {
dataMaster.operasional = res;
showLoading(false);
recalcGrandTotalOperasional();
alert('Data Operasional Berhasil Disimpan!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan operasional.\n' + (err && err.message ? err.message : err));
}).saveBulkOperasional(rows, currentUser, selectedDate || getTodayStr());
}
function lihatRekapOperasionalPeriode() {
var from = (document.getElementById('op-rekap-from') && document.getElementById('op-rekap-from').value) || '';
var to = (document.getElementById('op-rekap-to') && document.getElementById('op-rekap-to').value) || '';
if (!from || !to) { alert('Pilih tanggal awal dan akhir.'); return; }
showLoading(true, 'Mengambil Rekap...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
renderRekapOperasional(res);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal mengambil rekap operasional.\n' + (err && err.message ? err.message : err));
}).getOperasionalRekapPeriode(from, to);
}
function renderRekapOperasional(res) {
var totalBox = document.getElementById('op-rekap-total');
if (totalBox) totalBox.innerText = 'Rp ' + ((res && res.total) ? Number(res.total).toLocaleString('id-ID') : '0');
var body = document.getElementById('body-operasional-rekap');
if (!body) return;
body.innerHTML = '';
var rows = (res && res.perItem) ? res.perItem : [];
if (!rows.length) {
body.innerHTML = '<tr><td colspan="5" style="color:#94a3b8; font-style:italic;">Tidak ada data pada periode ini.</td></tr>';
return;
}
rows.forEach(function(r, idx) {
var tr = document.createElement('tr');
tr.innerHTML = '<td class="row-num">' + (idx + 1) + '</td>' +
'<td>' + String(r.nama || '') + '</td>' +
'<td style="text-align:right; font-weight:800;">' + (Number(r.harga) || 0).toLocaleString('id-ID') + '</td>' +
'<td style="text-align:right; font-weight:800;">' + (Number(r.qty) || 0) + '</td>' +
'<td style="text-align:right; font-weight:900; color:#e11d48;">' + (Number(r.total) || 0).toLocaleString('id-ID') + '</td>';
body.appendChild(tr);
});
}
// PEGAWAI
function renderPegawaiTable() {
renderPegawaiListTable();
var body = document.getElementById('body-pegawai');
body.innerHTML = '';
dataMaster.pegawai.forEach(p => tambahBarisPegawai(p));
for (var i = 0; i < 2; i++) tambahBarisPegawai();
recalcGrandTotalPegawai();
}
function renderPegawaiListTable() {
var body = document.getElementById('body-pegawai-list');
if (!body) return;
body.innerHTML = '';
var rows = (dataMaster.pegawaiList || []).slice();
rows.forEach(r => tambahBarisPegawaiList(r));
if (!rows.length) {
for (var i = 0; i < 3; i++) tambahBarisPegawaiList();
}
}
function tambahBarisPegawai(p) {
var body = document.getElementById('body-pegawai');
var tr = document.createElement('tr');
var d = p || { nama: '', nilai: 0, kategori: 'Gaji', catatan: '' };
if (d && d.id) tr.dataset.id = String(d.id);
var nameOptions = listPegawai.map(n => '<option value="' + n + '" ' + (d.nama === n ? 'selected' : '') + '>' + n + '</option>').join('');
tr.innerHTML = '<td class="row-num">' + (body.children.length + 1) + '</td>' +
'<td><select class="pg-nama" onchange="recalcGrandTotalPegawai()"><option value="">-- Pilih --</option>' + nameOptions + '</select></td>' +
'<td><input type="text" class="pg-nilai" value="' + (d.nilai ? formatRupiahValue(d.nilai) : '') + '" inputmode="numeric" oninput="onCurrencyInput(this); recalcGrandTotalPegawai()"></td>' +
'<td><select class="pg-kat" onchange="recalcGrandTotalPegawai()"><option value="Gaji" ' + (d.kategori === 'Gaji' ? 'selected' : '') + '>Gaji</option><option value="Kasbon" ' + (d.kategori === 'Kasbon' ? 'selected' : '') + '>Kasbon</option></select></td>' +
'<td><input type="text" class="pg-cat" value="' + d.catatan + '"></td>' +
'<td><button class="btn btn-danger" onclick="removePegawaiRow(this)">🗑️</button></td>';
body.appendChild(tr);
}
function removePegawaiRow(btn) {
var tr = btn && btn.closest ? btn.closest('tr') : null;
if (!tr) return;
var id = String(tr.dataset.id || '').trim();
if (!id) {
tr.remove();
recalcGrandTotalPegawai();
return;
}
if (!confirm('Hapus data pegawai ini dari sheet?')) return;
showLoading(true, 'Menghapus...');
google.script.run.withSuccessHandler(function(res) {
dataMaster.pegawai = Array.isArray(res) ? res : [];
showLoading(false);
renderPegawaiTable();
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menghapus pegawai.\n' + (err && err.message ? err.message : err));
}).deletePegawaiById(id, selectedDate || getTodayStr());
}
function recalcGrandTotalPegawai() {
var total = 0;
var totalGaji = 0;
var totalKasbon = 0;
document.querySelectorAll('#body-pegawai tr').forEach(tr => {
var nama = (tr.querySelector('.pg-nama') && tr.querySelector('.pg-nama').value) || '';
if (!nama) return;
var nilai = parseRupiahValue(tr.querySelector('.pg-nilai').value);
var kat = (tr.querySelector('.pg-kat') && tr.querySelector('.pg-kat').value) || '';
total += nilai;
if (kat === 'Gaji') totalGaji += nilai;
if (kat === 'Kasbon') totalKasbon += nilai;
});
var grand = document.getElementById('grand-total-pegawai');
if (grand) grand.innerText = 'Rp ' + total.toLocaleString('id-ID');
var gajiBox = document.getElementById('total-gaji-pegawai');
if (gajiBox) gajiBox.innerText = 'Rp ' + totalGaji.toLocaleString('id-ID');
var kasbonBox = document.getElementById('total-kasbon-pegawai');
if (kasbonBox) kasbonBox.innerText = 'Rp ' + totalKasbon.toLocaleString('id-ID');
}
function simpanSemuaPegawai() {
var rows = [];
document.querySelectorAll('#body-pegawai tr').forEach(tr => {
var nama = tr.querySelector('.pg-nama').value;
if (nama) rows.push({ id: tr.dataset.id || '', nama: nama, nilai: parseRupiahValue(tr.querySelector('.pg-nilai').value), kategori: tr.querySelector('.pg-kat').value, catatan: tr.querySelector('.pg-cat').value });
});
showLoading(true, 'Menyimpan...');
google.script.run.withSuccessHandler(function(res) {
dataMaster.pegawai = res;
showLoading(false);
recalcGrandTotalPegawai();
alert('Data Pegawai Berhasil Disimpan!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan pegawai.\n' + (err && err.message ? err.message : err));
}).saveBulkPegawai(rows, currentUser, selectedDate || getTodayStr());
}
function lihatRekapPegawaiPeriode() {
var from = (document.getElementById('pg-rekap-from') && document.getElementById('pg-rekap-from').value) || '';
var to = (document.getElementById('pg-rekap-to') && document.getElementById('pg-rekap-to').value) || '';
if (!from || !to) { alert('Pilih tanggal awal dan akhir.'); return; }
showLoading(true, 'Mengambil Rekap...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
renderRekapPegawai(res);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal mengambil rekap pegawai.\n' + (err && err.message ? err.message : err));
}).getPegawaiRekapPeriode(from, to);
}
function renderRekapPegawai(res) {
var gaji = (res && res.totals && res.totals.gaji) ? Number(res.totals.gaji) : 0;
var kasbon = (res && res.totals && res.totals.kasbon) ? Number(res.totals.kasbon) : 0;
var total = (res && res.totals && res.totals.total) ? Number(res.totals.total) : 0;
var gajiBox = document.getElementById('pg-rekap-gaji');
if (gajiBox) gajiBox.innerText = 'Rp ' + gaji.toLocaleString('id-ID');
var kasbonBox = document.getElementById('pg-rekap-kasbon');
if (kasbonBox) kasbonBox.innerText = 'Rp ' + kasbon.toLocaleString('id-ID');
var totalBox = document.getElementById('pg-rekap-total');
if (totalBox) totalBox.innerText = 'Rp ' + total.toLocaleString('id-ID');
var body = document.getElementById('body-pegawai-rekap');
if (!body) return;
body.innerHTML = '';
var rows = (res && res.perPegawai) ? res.perPegawai : [];
if (!rows.length) {
body.innerHTML = '<tr><td colspan="5" style="color:#94a3b8; font-style:italic;">Tidak ada data pada periode ini.</td></tr>';
return;
}
rows.forEach(function(r, idx) {
var tr = document.createElement('tr');
tr.innerHTML = '<td class="row-num">' + (idx + 1) + '</td>' +
'<td>' + String(r.nama || '') + '</td>' +
'<td style="text-align:right; font-weight:800;">' + (Number(r.gaji) || 0).toLocaleString('id-ID') + '</td>' +
'<td style="text-align:right; font-weight:800;">' + (Number(r.kasbon) || 0).toLocaleString('id-ID') + '</td>' +
'<td style="text-align:right; font-weight:900; color:#e11d48;">' + (Number(r.total) || 0).toLocaleString('id-ID') + '</td>';
body.appendChild(tr);
});
}
function tambahBarisPegawaiList(row) {
var body = document.getElementById('body-pegawai-list');
if (!body) return;
var tr = document.createElement('tr');
var d = row || { id: '', nama: '', status: 'Aktif', catatan: '' };
if (d && d.id) tr.dataset.id = String(d.id);
tr.innerHTML = '<td class="row-num">' + (body.children.length + 1) + '</td>' +
'<td><input type="text" class="pl-nama" value="' + (d.nama || '') + '" placeholder="Nama pegawai"></td>' +
'<td><select class="pl-status"><option value="Aktif" ' + (String(d.status || 'Aktif') === 'Aktif' ? 'selected' : '') + '>Aktif</option><option value="Nonaktif" ' + (String(d.status || '') === 'Nonaktif' ? 'selected' : '') + '>Nonaktif</option></select></td>' +
'<td><input type="text" class="pl-cat" value="' + (d.catatan || '') + '"></td>' +
'<td><button class="btn btn-danger" onclick="this.closest(\'tr\').remove(); renumberPegawaiList()">🗑️</button></td>';
body.appendChild(tr);
renumberPegawaiList();
}
function renumberPegawaiList() {
var body = document.getElementById('body-pegawai-list');
if (!body) return;
Array.from(body.children).forEach((tr, idx) => {
var td = tr.querySelector('.row-num');
if (td) td.innerText = String(idx + 1);
});
}
function simpanDaftarPegawai() {
var rows = [];
document.querySelectorAll('#body-pegawai-list tr').forEach(tr => {
var nama = String(tr.querySelector('.pl-nama').value || '').trim();
if (!nama) return;
rows.push({
id: tr.dataset.id || '',
nama: nama,
status: tr.querySelector('.pl-status').value,
catatan: tr.querySelector('.pl-cat').value
});
});
showLoading(true, 'Menyimpan...');
google.script.run.withSuccessHandler(function(res) {
dataMaster.pegawaiList = res || [];
listPegawai = (dataMaster.pegawaiList || [])
.filter(p => String((p && p.status) || 'Aktif').toLowerCase() !== 'nonaktif')
.map(p => String(p.nama || '').trim())
.filter(Boolean);
showLoading(false);
renderPegawaiTable();
alert('Daftar Pegawai Berhasil Disimpan!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan daftar pegawai.\n' + (err && err.message ? err.message : err));
}).savePegawaiList(rows, currentUser);
}
function simpanSemuaStok() {
var rows = [];
var lokasi = currentTab === 'stok_atas' ? 'Lantai Atas' : (currentTab === 'stok_bawah' ? 'Lantai Bawah' : 'Showcase');
document.querySelectorAll('#body-stok tr').forEach(tr => {
var nama = tr.querySelector('.st-nama').value;
if (nama) rows.push({ menu: nama, stokAwal: tr.querySelector('.st-awal').value, restock: tr.querySelector('.st-restock').value, terpakai: tr.querySelector('.st-pakai').value, sisa: tr.querySelector('.st-sisa').value, lokasi: lokasi, foto: tr.querySelector('.st-foto') ? tr.querySelector('.st-foto').value : '' });
});
showLoading(true, 'Menyimpan Stok...');
google.script.run.withSuccessHandler(function(res) {
dataMaster.stok = res;
showLoading(false);
alert('Stok ' + lokasi + ' Disimpan!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan stok.\n' + (err && err.message ? err.message : err));
}).saveBulkStok(rows, currentUser, selectedDate || getTodayStr());
}
function simpanSemuaBelanja() {
var rows = [];
var kategori = currentTab.replace('belanja_', '').replace('_', ' ').toUpperCase();
var defaults = defaultBelanja[currentTab] || [];
document.querySelectorAll('#body-belanja tr').forEach(tr => {
var namaEl = tr.querySelector('.bl-nama');
var nama = canonicalizeFromDefaults_((namaEl && namaEl.value) || '', defaults);
if (namaEl) namaEl.value = nama;
if (nama) rows.push({ id: tr.dataset.id || '', nama: nama, kategori: kategori, harga: parseRupiahValue(tr.querySelector('.bl-harga').value), qty: tr.querySelector('.bl-qty').value, total: Number(tr.querySelector('.bl-total').dataset.raw) || 0, catatan: tr.querySelector('.bl-cat').value });
});
showLoading(true, 'Menyimpan Belanja...');
google.script.run.withSuccessHandler(function(res) {
dataMaster.belanja = res;
showLoading(false);
alert('Belanja ' + kategori + ' Disimpan!');
}).withFailureHandler(function(err) {
showLoading(false);
alert('Gagal menyimpan belanja.\n' + (err && err.message ? err.message : err));
}).saveBulkBelanja(rows, currentUser, selectedDate || getTodayStr());
}
function syncBelanjaToRekap() {
var kategori = currentTab.replace('belanja_', '').replace('_', ' ').toUpperCase();
if (!(currentTab === 'belanja_modal' || currentTab === 'belanja_bawah')) {
alert('Sync hanya untuk Belanja MODAL & BAWAH.');
return;
}
showLoading(true, 'Sync ke RekapTransaksi...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || res.ok === false) {
alert('Sync gagal.\n' + (res && res.error ? res.error : 'Unknown'));
return;
}
alert('Sync sukses.\nInserted: ' + (Number(res.inserted) || 0) + '\nUpdated: ' + (Number(res.updated) || 0) + '\nSkipped: ' + (Number(res.skipped) || 0));
}).withFailureHandler(function(err) {
showLoading(false);
alert('Sync gagal.\n' + (err && err.message ? err.message : err));
}).syncBelanjaToRekap(selectedDate || getTodayStr(), kategori, currentUser);
}
function syncBelanjaToRekapAll() {
var kategori = currentTab.replace('belanja_', '').replace('_', ' ').toUpperCase();
if (!(currentTab === 'belanja_modal' || currentTab === 'belanja_bawah')) {
alert('Sync hanya untuk Belanja MODAL & BAWAH.');
return;
}
if (!confirm('Sync SEMUA tanggal untuk kategori ' + kategori + ' ke RekapTransaksi?')) return;
showLoading(true, 'Sync semua tanggal...');
google.script.run.withSuccessHandler(function(res) {
showLoading(false);
if (!res || res.ok === false) {
alert('Sync gagal.\n' + (res && res.error ? res.error : 'Unknown'));
return;
}
alert(
'Sync semua selesai.\n' +
'Tanggal terdeteksi: ' + (Number(res.dateCount) || 0) + '\n' +
'Total item: ' + (Number(res.total) || 0) + '\n' +
'Inserted: ' + (Number(res.inserted) || 0) + '\n' +
'Updated: ' + (Number(res.updated) || 0) + '\n' +
'Skipped: ' + (Number(res.skipped) || 0)
);
}).withFailureHandler(function(err) {
showLoading(false);
alert('Sync gagal.\n' + (err && err.message ? err.message : err));
}).syncBelanjaToRekapAll(kategori, currentUser);
}
function handlePhoto(input) {
var files = input.files;
if (!files.length) return;
var container = input.nextElementSibling;
var textArea = container.nextElementSibling;
var currentPhotos = textArea.value ? textArea.value.split('|') : [];
showLoading(true, 'Mengompres...');
var processed = 0;
Array.from(files).forEach(file => {
var reader = new FileReader();
reader.onload = e => {
var img = new Image();
img.onload = () => {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var scale = Math.sqrt(300000 / file.size);
if (scale > 1) scale = 1;
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
var base64 = canvas.toDataURL('image/jpeg', 0.7);
currentPhotos.push(base64);
var previewImg = document.createElement('img');
previewImg.src = base64;
previewImg.className = 'photo-preview';
previewImg.onclick = () => viewPhoto(base64);
container.appendChild(previewImg);
processed++;
if (processed === files.length) { textArea.value = currentPhotos.join('|'); showLoading(false); }
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
function viewPhoto(src) { var win = window.open(); win.document.write('<img src="' + src + '" style="max-width:100%">'); }
</script>
</body>
</html>