2115 lines
106 KiB
HTML
2115 lines
106 KiB
HTML
<!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 > 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>
|