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

1150 lines
55 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard Laporan Fuku</title>
<script src="/local-preview-bridge.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f0f2f5; margin: 0; color: #333; }
.header { background: #111; color: white; padding: 20px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.1); position: sticky; top: 0; z-index: 100; }
.container { max-width: 1200px; margin: 20px auto; padding: 0 20px; }
.nav-tabs { display: flex; justify-content: center; gap: 10px; margin-bottom: 25px; }
.tab-btn { padding: 10px 25px; border-radius: 25px; border: none; background: #ddd; color: #666; font-weight: 700; cursor: pointer; transition: 0.2s; }
.tab-btn.active { background: #e11d48; color: white; }
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin-bottom: 30px; }
.summary-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border: 1px solid #eee; transition: transform 0.2s; }
.summary-card:hover { transform: translateY(-5px); }
.summary-card .label { font-size: 13px; color: #666; font-weight: 600; margin-bottom: 10px; text-transform: uppercase; }
.summary-card .value { font-size: 22px; font-weight: 800; color: #e11d48; }
.summary-card.profit .value { color: #10b981; }
.charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 20px; margin-bottom: 30px; }
@media (max-width: 600px) { .charts-grid { grid-template-columns: 1fr; } }
.chart-box { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border: 1px solid #eee; }
.chart-title { font-size: 16px; font-weight: 700; margin-bottom: 20px; color: #111; border-bottom: 2px solid #f0f2f5; padding-bottom: 10px; }
.list-box { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border: 1px solid #eee; }
.item-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f0f2f5; }
.item-row:last-child { border-bottom: none; }
.item-name { font-weight: 600; font-size: 14px; }
.item-val { font-weight: 800; color: #e11d48; font-size: 14px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
#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; }
.spinner { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #e11d48; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.btn-sync { background: #e11d48; color: white; border: none; padding: 8px 15px; border-radius: 8px; font-weight: 700; cursor: pointer; font-size: 12px; }
.btn-ghost { background: transparent; color: #fff; border: 1px solid rgba(255,255,255,0.35); padding: 8px 12px; border-radius: 8px; font-weight: 800; cursor: pointer; font-size: 12px; }
.filter-row { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: end; }
.filter-row.two { grid-template-columns: 1fr 1fr auto; }
.filter-field { min-width: 200px; }
.filter-field .lbl { font-size: 11px; font-weight: 800; color: #666; margin-bottom: 6px; }
.filter-input { width: 100%; height: 40px; padding: 0 12px; border: 1px solid #ddd; border-radius: 10px; font-size: 14px; box-sizing: border-box; }
.filter-btn { height: 40px; width: 110px; padding: 0 14px; border-radius: 10px; display: flex; align-items: center; justify-content: center; }
@media (max-width: 640px) {
.filter-row, .filter-row.two { grid-template-columns: 1fr; }
.filter-btn { width: 100%; }
.filter-field { min-width: 0; }
}
.home-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.home-card { width: 100%; max-width: 520px; background: #fff; border: 1px solid #eee; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.06); padding: 22px; }
.home-title { font-weight: 900; font-size: 18px; color: #111; text-align: center; }
.home-sub { font-size: 12px; color: #64748b; text-align: center; margin-top: 6px; }
.home-actions { display: grid; gap: 12px; margin-top: 18px; }
.home-btn { display: block; width: 100%; padding: 14px 14px; border-radius: 12px; border: none; font-weight: 900; font-size: 14px; cursor: pointer; }
.home-btn.primary { background: #e11d48; color: #fff; }
.home-btn.dark { background: #111; color: #fff; }
.home-btn.light { background: #f1f5f9; color: #111; border: 1px solid #e2e8f0; }
.home-btn a { color: inherit; text-decoration: none; display: block; width: 100%; }
.muted { color:#94a3b8; font-size:12px; }
.mini-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 20px; }
.mini-card { background:#fff; border:1px solid #eee; border-radius:12px; padding:14px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.mini-card .k { font-size: 11px; color:#666; font-weight:800; text-transform:uppercase; }
.mini-card .v { margin-top:8px; font-size:20px; font-weight:900; color:#111; }
.mini-card .v.red { color:#e11d48; }
.mini-card .v.orange { color:#f59e0b; }
.history-date { background:#f8fafc; border:1px solid #e2e8f0; border-radius:10px; padding:10px 12px; font-weight:900; color:#111; display:flex; justify-content:space-between; align-items:center; }
.history-card { background:#fff; border:1px solid #eee; border-radius:12px; padding:12px; margin-top:10px; cursor:pointer; }
.history-card .top { display:flex; justify-content:space-between; gap:10px; }
.history-card .id { font-weight:900; color:#111; font-size:14px; }
.history-card .amt { font-weight:900; color:#e11d48; }
.history-card .sub { font-size:12px; color:#64748b; margin-top:4px; display:flex; justify-content:space-between; gap:10px; }
.btn-load { width:100%; padding:14px; border:none; border-radius:12px; font-weight:900; background:#e11d48; color:#fff; cursor:pointer; }
.modal { position:fixed; inset:0; background:rgba(0,0,0,0.55); display:none; align-items:center; justify-content:center; padding:20px; z-index:3000; }
.modal .box { width:100%; max-width:560px; background:#fff; border-radius:16px; border:1px solid #eee; box-shadow: 0 10px 30px rgba(0,0,0,0.2); padding:16px; }
.modal .hdr { display:flex; justify-content:space-between; align-items:center; gap:10px; }
.modal .ttl { font-weight:900; color:#111; }
.modal .close { border:none; background:#111; color:#fff; border-radius:10px; padding:8px 12px; font-weight:900; cursor:pointer; }
</style>
</head>
<body>
<div id="home" class="home-wrap" style="display:flex;">
<div class="home-card">
<div class="home-title">RekapTransaksi</div>
<div class="home-sub">Pilih halaman yang ingin dibuka</div>
<div class="home-actions">
<button class="home-btn primary" onclick="authGo('dashboard')">Dashboard</button>
<button class="home-btn dark" onclick="window.open('https://s.id/POSFuku', '_blank', 'noopener,noreferrer')">Kasir</button>
<button class="home-btn light" onclick="window.open('https://s.id/StokFuku', '_blank', 'noopener,noreferrer')">Stok</button>
</div>
</div>
</div>
<div id="loading" style="display:none;">
<div class="spinner"></div>
<div id="loading-text" style="font-weight: 800; color: #e11d48;">MEMUAT...</div>
</div>
<div id="dashboard-root" style="display:none;">
<div class="header">
<div style="display:flex; justify-content:space-between; align-items:center; max-width:1200px; margin:0 auto; padding:0 20px;">
<div style="text-align:left;">
<div style="font-size: 20px; font-weight: 900;">DASHBOARD FUKU</div>
<div style="font-size: 12px; opacity: 0.8;">Restaurant Analytics</div>
</div>
<div style="display:flex; gap:10px; align-items:center;">
<button class="btn-ghost" onclick="goHome()">LOGOUT</button>
<button class="btn-sync" onclick="loadData()">SYNC DATA</button>
</div>
</div>
</div>
<div class="container">
<div class="nav-tabs">
<button class="tab-btn active" onclick="switchTab('sales', this)">PENJUALAN</button>
<button class="tab-btn" onclick="switchTab('expense', this)">BELANJA</button>
<button class="tab-btn" onclick="switchTab('rekap', this)">REKAP</button>
<button class="tab-btn" onclick="switchTab('calendar', this)">KALENDER</button>
<button class="tab-btn" onclick="switchTab('upload', this)">UPLOAD</button>
</div>
<!-- Tab Penjualan -->
<div id="tab-sales" class="tab-content active">
<div class="list-box" style="margin-bottom:20px;">
<div class="chart-title">Filter Penjualan (DariSampai)</div>
<div class="filter-row two">
<div class="filter-field">
<div class="lbl">Dari</div>
<input id="sales-from" type="date" class="filter-input">
</div>
<div class="filter-field">
<div class="lbl">Sampai</div>
<input id="sales-to" type="date" class="filter-input">
</div>
<button class="btn-sync filter-btn" onclick="loadSalesRange()">TAMPILKAN</button>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="label">Total Omzet</div>
<div id="sum-sales" class="value">Rp 0</div>
</div>
<div class="summary-card profit">
<div class="label">Net Profit</div>
<div id="sum-profit" class="value">Rp 0</div>
</div>
<div class="summary-card">
<div class="label">Total Transaksi</div>
<div id="sum-trans" class="value">0</div>
</div>
<div class="summary-card">
<div class="label">Rata-rata / Transaksi</div>
<div id="sum-avg" class="value">Rp 0</div>
</div>
</div>
<div class="mini-grid">
<div class="mini-card">
<div class="k">Transaksi &lt; 100rb</div>
<div id="sum-bucket-lt100" class="v red">0</div>
</div>
<div class="mini-card">
<div class="k">Transaksi ≥ 100rb</div>
<div id="sum-bucket-gte100" class="v">0</div>
</div>
<div class="mini-card">
<div class="k">Transaksi ≥ 200rb</div>
<div id="sum-bucket-gte200" class="v">0</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-box">
<div class="chart-title">Tren Penjualan Harian</div>
<canvas id="dailyChart"></canvas>
</div>
<div class="chart-box">
<div class="chart-title">Metode Pembayaran</div>
<canvas id="methodChart"></canvas>
</div>
</div>
<div class="charts-grid">
<div class="chart-box">
<div class="chart-title">Tren Jam Transaksi</div>
<canvas id="hourlyChart"></canvas>
</div>
<div class="list-box">
<div class="chart-title">Rata-rata Konsumsi Menu / Hari (Top 10)</div>
<div id="avg-items-list"></div>
</div>
</div>
<div class="list-box">
<div class="chart-title">10 Produk Terlaris</div>
<div id="top-items-list"></div>
</div>
</div>
<!-- Tab Belanja -->
<div id="tab-expense" class="tab-content">
<div class="list-box" style="margin-bottom:20px;">
<div class="chart-title">Filter Belanja (DariSampai)</div>
<div class="filter-row two">
<div class="filter-field">
<div class="lbl">Dari</div>
<input id="exp-from" type="date" class="filter-input">
</div>
<div class="filter-field">
<div class="lbl">Sampai</div>
<input id="exp-to" type="date" class="filter-input">
</div>
<button class="btn-sync filter-btn" style="background:#f59e0b;" onclick="loadExpenseRange()">TAMPILKAN</button>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="label">Total Belanja</div>
<div id="sum-expense" class="value" style="color:#f59e0b;">Rp 0</div>
</div>
<div class="summary-card">
<div class="label">Belanja Terbesar</div>
<div id="sum-max-expense" class="value" style="color:#f59e0b;">Rp 0</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-box">
<div class="chart-title">Tren Pengeluaran Harian</div>
<canvas id="expenseDailyChart"></canvas>
</div>
<div class="chart-box">
<div class="chart-title">Kategori Pengeluaran</div>
<canvas id="expenseCatChart"></canvas>
</div>
</div>
<div class="list-box">
<div class="chart-title">10 Pengeluaran Terbesar</div>
<div id="top-expense-list"></div>
</div>
</div>
<!-- Tab Upload -->
<div id="tab-upload" class="tab-content">
<div class="summary-grid">
<div class="summary-card">
<div class="label">Upload Data Transaksi (CSV)</div>
<div style="margin-top:10px;">
<input type="file" id="upload-trans-csv" accept=".csv" style="padding:10px; border:1px dashed #ccc; margin-bottom:10px;">
<button class="btn-sync" style="width:100%;" onclick="processUpload('trans')">IMPORT TRANSAKSI</button>
</div>
<p style="font-size:10px; color:#999; margin-top:10px;">Format: ID, Meja, Status, Nama, WA, Items_JSON, Total, Metode</p>
</div>
<div class="summary-card">
<div class="label">Upload Data Belanja (CSV)</div>
<div style="margin-top:10px;">
<input type="file" id="upload-belanja-csv" accept=".csv" style="padding:10px; border:1px dashed #ccc; margin-bottom:10px;">
<button class="btn-sync" style="width:100%; background:#f59e0b;" onclick="processUpload('belanja')">IMPORT BELANJA</button>
</div>
<p style="font-size:10px; color:#999; margin-top:10px;">Format: Tanggal, Nama, Kategori, Qty, Harga, Total</p>
</div>
</div>
</div>
<!-- Tab Rekap -->
<div id="tab-rekap" class="tab-content">
<div class="list-box" style="margin-bottom:20px;">
<div class="chart-title">Filter Rekap & Riwayat (DariSampai)</div>
<div class="filter-row two">
<div class="filter-field">
<div class="lbl">Dari</div>
<input id="rk-from" type="date" class="filter-input">
</div>
<div class="filter-field">
<div class="lbl">Sampai</div>
<input id="rk-to" type="date" class="filter-input">
</div>
<button class="btn-sync filter-btn" onclick="loadRekapRange()">TAMPILKAN</button>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="label">Periode</div>
<div id="rk-period" class="value" style="font-size:16px; color:#111;">-</div>
</div>
<div class="summary-card">
<div class="label">Total Nota</div>
<div id="rk-nota" class="value">0</div>
</div>
<div class="summary-card">
<div class="label">Total Penjualan</div>
<div id="rk-sales" class="value">Rp 0</div>
</div>
<div class="summary-card">
<div class="label">Total Belanja</div>
<div id="rk-expense" class="value" style="color:#f59e0b;">Rp 0</div>
</div>
<div class="summary-card profit">
<div class="label">Net Profit</div>
<div id="rk-profit" class="value">Rp 0</div>
</div>
</div>
<div class="charts-grid">
<div class="list-box">
<div class="chart-title">Penjualan per Metode</div>
<div id="rk-methods"></div>
</div>
<div class="list-box">
<div class="chart-title">Porsi Terjual</div>
<div id="rk-portions"></div>
</div>
</div>
<div class="list-box" style="margin-top:20px;">
<div class="chart-title">Riwayat Transaksi</div>
<div class="filter-row" style="margin-top:10px;">
<div class="filter-field" style="min-width:0;">
<div class="lbl">Cari (ID / Meja / Nama / WA)</div>
<input id="his-q" class="filter-input" placeholder="contoh: Meja-3 atau an-123">
</div>
<button class="btn-sync filter-btn" onclick="loadHistory(true)">CARI</button>
</div>
<div id="his-list" style="margin-top:12px;"></div>
<div id="his-more-wrap" style="margin-top:12px; display:none;">
<button class="btn-load" onclick="loadHistory(false)">MUAT LEBIH BANYAK</button>
</div>
</div>
</div>
<!-- Tab Kalender -->
<div id="tab-calendar" class="tab-content">
<div class="list-box" style="margin-bottom:20px;">
<div class="chart-title">Kalender & Catatan Penting</div>
<div class="filter-row two">
<div class="filter-field">
<div class="lbl">Tanggal</div>
<input id="cal-date" type="date" class="filter-input" onchange="updateOffInfo()">
</div>
<div class="filter-field">
<div class="lbl">Libur Toko (tentatif)</div>
<div id="cal-off-info" class="filter-input" style="display:flex; align-items:center; background:#fff; font-weight:900; color:#111;"></div>
</div>
<button class="btn-sync filter-btn" onclick="loadCalendarNotes()">TAMPILKAN</button>
</div>
<div style="margin-top:12px; display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
<div class="mini-card">
<div class="k">Terakhir Libur</div>
<div id="cal-last-off" class="v">2026-04-06</div>
</div>
<div class="mini-card">
<div class="k">Libur Berikutnya</div>
<div id="cal-next-off" class="v red">-</div>
</div>
</div>
</div>
<div class="list-box" style="margin-bottom:20px;">
<div class="chart-title">Tambah Catatan</div>
<div class="filter-row two">
<div class="filter-field">
<div class="lbl">Judul</div>
<input id="cal-title" class="filter-input" placeholder="Contoh: Maintenance, Promo, Tutup cepat">
</div>
<div class="filter-field">
<div class="lbl">User</div>
<input id="cal-user" class="filter-input" placeholder="admin / ridho / ...">
</div>
<button class="btn-sync filter-btn" onclick="saveCalendarNote()">SIMPAN</button>
</div>
<div style="margin-top:10px;">
<div class="lbl" style="font-size:11px; font-weight:800; color:#666; margin-bottom:6px;">Catatan</div>
<textarea id="cal-note" class="filter-input" style="height:120px; padding:10px;" placeholder="Tulis info penting untuk tanggal ini..."></textarea>
<input id="cal-id" type="hidden">
</div>
</div>
<div class="list-box">
<div class="chart-title">Daftar Catatan</div>
<div id="cal-list"></div>
</div>
</div>
</div>
</div>
<div id="modal" class="modal" onclick="closeModal(event)">
<div class="box">
<div class="hdr">
<div id="modal-title" class="ttl">Rincian</div>
<button class="close" onclick="closeModal()">TUTUP</button>
</div>
<div id="modal-body" style="margin-top:12px;"></div>
</div>
</div>
<script>
function goDashboard() {
document.getElementById('home').style.display = 'none';
document.getElementById('dashboard-root').style.display = 'block';
var url = new URL(window.location.href);
url.searchParams.set('view', 'dashboard');
try { history.replaceState(null, '', url.toString()); } catch(e) {}
loadData();
}
function goHome() {
document.getElementById('dashboard-root').style.display = 'none';
document.getElementById('home').style.display = 'flex';
var url = new URL(window.location.href);
url.searchParams.delete('view');
try { history.replaceState(null, '', url.toString()); } catch(e) {}
}
function authGo(action) {
var u = prompt('Login ' + action.toUpperCase() + '\n\nUsername:');
if (u === null) return;
u = String(u || '').trim();
if (!u) { alert('Username tidak boleh kosong.'); return; }
var p = prompt('Password:');
if (p === null) return;
var loadingEl = document.getElementById('loading');
if (loadingEl) {
loadingEl.style.display = 'flex';
var txt = document.getElementById('loading-text');
if (txt) txt.innerText = 'LOGIN...';
}
google.script.run.withSuccessHandler(function(res) {
if (loadingEl) loadingEl.style.display = 'none';
if (!res || !res.ok) {
alert('Login gagal.');
return;
}
if (action === 'dashboard') {
goDashboard();
} else if (action === 'kasir') {
window.open('https://s.id/POSFuku', '_blank', 'noopener,noreferrer');
} else if (action === 'stok') {
window.open('https://s.id/StokFuku', '_blank', 'noopener,noreferrer');
}
}).withFailureHandler(function(err) {
if (loadingEl) loadingEl.style.display = 'none';
alert('Gagal login: ' + (err && err.message ? err.message : err));
}).rekapLogin(action, u, p);
}
window.onload = function() {
var v = new URLSearchParams(window.location.search).get('view');
var isDash = v === 'dashboard';
document.getElementById('home').style.display = isDash ? 'none' : 'flex';
document.getElementById('dashboard-root').style.display = isDash ? 'block' : 'none';
if (isDash) loadData();
};
var dailyChart, methodChart, expenseDailyChart, expenseCatChart, hourlyChart;
var rekapLoadedOnce = false;
var expenseLoadedOnce = false;
var historyOffset = 0;
var historyCache = {};
function switchTab(tab, el) {
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
if (el) el.classList.add('active');
if (tab === 'rekap' && !rekapLoadedOnce) {
initRekapInputs();
loadRekapRange();
rekapLoadedOnce = true;
}
if (tab === 'expense' && !expenseLoadedOnce) {
initRekapInputs();
loadExpenseRange();
expenseLoadedOnce = true;
}
if (tab === 'calendar') {
initCalendarTab();
}
}
var CAL_LAST_OFF = '2026-04-06';
function initCalendarTab() {
var d = document.getElementById('cal-date');
if (d && !d.value) {
var saved = '';
try { saved = localStorage.getItem('rk_cal_date') || ''; } catch(e) {}
d.value = saved || getTodayStr();
}
var u = document.getElementById('cal-user');
if (u && !u.value) u.value = 'admin';
updateOffInfo();
loadCalendarNotes();
}
function parseDateYmd(s) {
if (!s || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
return new Date(s + 'T00:00:00');
}
function formatYmd(dt) {
var y = dt.getFullYear();
var m = String(dt.getMonth() + 1).padStart(2, '0');
var d = String(dt.getDate()).padStart(2, '0');
return y + '-' + m + '-' + d;
}
function getNextOffDate(fromDateStr) {
var base = parseDateYmd(CAL_LAST_OFF);
if (!base) return null;
var from = parseDateYmd(fromDateStr || getTodayStr());
if (!from) return null;
var diffDays = Math.floor((from.getTime() - base.getTime()) / 86400000);
if (diffDays < 0) diffDays = 0;
var steps = Math.floor(diffDays / 14);
var next = new Date(base.getTime());
next.setDate(next.getDate() + (steps * 14));
while (next.getTime() < from.getTime()) next.setDate(next.getDate() + 14);
return next;
}
function isOffDate(dateStr) {
var base = parseDateYmd(CAL_LAST_OFF);
var d = parseDateYmd(dateStr);
if (!base || !d) return false;
var diff = Math.floor((d.getTime() - base.getTime()) / 86400000);
return diff >= 0 && diff % 14 === 0;
}
function updateOffInfo() {
var dateStr = (document.getElementById('cal-date') && document.getElementById('cal-date').value) || getTodayStr();
var lastEl = document.getElementById('cal-last-off');
if (lastEl) lastEl.innerText = CAL_LAST_OFF;
var next = getNextOffDate(dateStr);
var nextStr = next ? formatYmd(next) : '-';
var nextEl = document.getElementById('cal-next-off');
if (nextEl) nextEl.innerText = nextStr;
var info = document.getElementById('cal-off-info');
if (info) info.innerText = isOffDate(dateStr) ? 'LIBUR (tentatif)' : 'BUKA (tentatif)';
try { localStorage.setItem('rk_cal_date', dateStr); } catch(e) {}
}
function loadCalendarNotes() {
var dateStr = (document.getElementById('cal-date') && document.getElementById('cal-date').value) || getTodayStr();
updateOffInfo();
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(list) {
document.getElementById('loading').style.display = 'none';
renderCalendarNotes(Array.isArray(list) ? list : []);
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal memuat catatan kalender: ' + (err && err.message ? err.message : err));
}).getCalendarNotesByDate(dateStr);
}
function renderCalendarNotes(list) {
var box = document.getElementById('cal-list');
if (!box) return;
if (!list.length) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:10px 0;">Belum ada catatan.</div>';
return;
}
var html = '';
list.forEach(function(n) {
html += '<div class="history-card" style="cursor:default;">' +
'<div class="top"><div class="id">' + esc(n.judul || '(Tanpa Judul)') + '</div><div class="amt" style="color:#111; font-size:12px;">' + esc((n.timestamp || '').slice(0, 16)) + '</div></div>' +
'<div class="sub" style="justify-content:flex-start; gap:8px;"><span style="font-weight:900; color:#64748b;">' + esc(n.user || '-') + '</span><span style="color:#94a3b8;">' + esc(n.tanggal || '') + '</span></div>' +
'<div style="margin-top:8px; font-size:13px; white-space:pre-wrap; color:#111;">' + esc(n.catatan || '') + '</div>' +
'<div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">' +
'<button class="btn-sync" style="background:#111;" onclick="editCalendarNote(' + "'" + String(n.id || '') + "'" + ')">EDIT</button>' +
'<button class="btn-sync" style="background:#ef4444;" onclick="deleteCalendarNote(' + "'" + String(n.id || '') + "'" + ')">HAPUS</button>' +
'</div>' +
'</div>';
});
box.innerHTML = html;
}
function editCalendarNote(id) {
var dateStr = (document.getElementById('cal-date') && document.getElementById('cal-date').value) || getTodayStr();
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(list) {
document.getElementById('loading').style.display = 'none';
var items = Array.isArray(list) ? list : [];
var n = items.find(function(x) { return String(x.id) === String(id); });
if (!n) { alert('Catatan tidak ditemukan.'); return; }
document.getElementById('cal-id').value = n.id || '';
document.getElementById('cal-title').value = n.judul || '';
document.getElementById('cal-note').value = n.catatan || '';
document.getElementById('cal-user').value = n.user || (document.getElementById('cal-user').value || '');
window.scrollTo({ top: 0, behavior: 'smooth' });
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal ambil catatan: ' + (err && err.message ? err.message : err));
}).getCalendarNotesByDate(dateStr);
}
function saveCalendarNote() {
var dateStr = (document.getElementById('cal-date') && document.getElementById('cal-date').value) || getTodayStr();
var id = (document.getElementById('cal-id') && document.getElementById('cal-id').value) || '';
var judul = (document.getElementById('cal-title') && document.getElementById('cal-title').value) || '';
var catatan = (document.getElementById('cal-note') && document.getElementById('cal-note').value) || '';
var user = (document.getElementById('cal-user') && document.getElementById('cal-user').value) || '';
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(list) {
document.getElementById('loading').style.display = 'none';
document.getElementById('cal-id').value = '';
document.getElementById('cal-title').value = '';
document.getElementById('cal-note').value = '';
renderCalendarNotes(Array.isArray(list) ? list : []);
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal simpan catatan: ' + (err && err.message ? err.message : err));
}).saveCalendarNote({ id: id, tanggal: dateStr, judul: judul, catatan: catatan, user: user }, user);
}
function deleteCalendarNote(id) {
if (!confirm('Hapus catatan ini?')) return;
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function() {
document.getElementById('loading').style.display = 'none';
loadCalendarNotes();
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal hapus catatan: ' + (err && err.message ? err.message : err));
}).deleteCalendarNote(id);
}
function processUpload(type) {
var fileInput = document.getElementById('upload-' + type + '-csv');
var file = fileInput.files[0];
if (!file) { alert('Pilih file CSV terlebih dahulu.'); return; }
var reader = new FileReader();
reader.onload = function(e) {
var content = e.target.result;
var lines = content.split('\n');
if (lines.length < 2) { alert('File CSV kosong atau tidak valid.'); return; }
var data = [];
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var parts = parseCsvLine(line);
// Langsung gunakan semua kolom dari CSV
if (type === 'trans' && parts.length >= 1) {
data.push(parts);
} else if (type === 'belanja' && parts.length >= 1) {
data.push(parts);
}
}
if (data.length > 0) {
if (confirm('Import ' + data.length + ' data ' + type + '?')) {
document.getElementById('loading').style.display = 'flex';
var serverFunc = type === 'trans' ? 'importTransactions' : 'importBelanja';
google.script.run.withSuccessHandler(function(res) {
alert('Berhasil mengimpor ' + res.count + ' data.');
loadData();
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal impor: ' + err.message);
})[serverFunc](data);
}
}
};
reader.readAsText(file);
}
function parseCsvLine(line) {
var parts = [];
var current = '';
var inQuotes = false;
for (var j = 0; j < line.length; j++) {
var char = line[j];
if (char === '"') {
if (inQuotes && line[j+1] === '"') { current += '"'; j++; }
else { inQuotes = !inQuotes; }
} else if (char === ',' && !inQuotes) {
parts.push(current.trim());
current = '';
} else { current += char; }
}
parts.push(current.trim());
return parts.map(p => p.replace(/^"|"$/g, ''));
}
function fmtRp(n) {
return 'Rp ' + Number(n).toLocaleString('id-ID');
}
function loadData() {
initRekapInputs();
loadSalesRange();
}
function getTodayStr() {
return new Date().toISOString().split('T')[0];
}
function shiftDateStr(dayOffset) {
var d = new Date();
d.setDate(d.getDate() + Number(dayOffset || 0));
return d.toISOString().split('T')[0];
}
function initRekapInputs() {
var today = getTodayStr();
var f = document.getElementById('rk-from');
var t = document.getElementById('rk-to');
if (f && !f.value) f.value = shiftDateStr(-6);
if (t && !t.value) t.value = today;
var sf = document.getElementById('sales-from');
var st = document.getElementById('sales-to');
if (sf && !sf.value) sf.value = shiftDateStr(-6);
if (st && !st.value) st.value = today;
var ef = document.getElementById('exp-from');
var et = document.getElementById('exp-to');
if (ef && !ef.value) ef.value = shiftDateStr(-6);
if (et && !et.value) et.value = today;
}
function loadRekapDate() {
var el = document.getElementById('rk-date');
if (!el) { loadRekapRange(); return; }
var d = el.value || getTodayStr();
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(res) {
document.getElementById('loading').style.display = 'none';
renderRekap(res);
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal memuat rekap: ' + err.message);
}).getRekapByDate(d);
}
function loadRekapRange() {
var f = document.getElementById('rk-from').value || getTodayStr();
var t = document.getElementById('rk-to').value || getTodayStr();
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(res) {
document.getElementById('loading').style.display = 'none';
renderRekap(res);
loadHistory(true);
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal memuat rekap: ' + err.message);
}).getRekapByRange(f, t);
}
function renderRekap(res) {
if (!res) return;
var period = res.isRange ? (res.startDate + ' s/d ' + res.endDate) : res.startDate;
document.getElementById('rk-period').innerText = period;
document.getElementById('rk-nota').innerText = Number((res.summary && res.summary.totalNota) || 0);
document.getElementById('rk-sales').innerText = fmtRp((res.summary && res.summary.totalSales) || 0);
document.getElementById('rk-expense').innerText = fmtRp((res.summary && res.summary.totalBelanja) || 0);
document.getElementById('rk-profit').innerText = fmtRp((res.summary && res.summary.netProfit) || 0);
var boxMethods = document.getElementById('rk-methods');
boxMethods.innerHTML = '';
var order = ['Tunai', 'QRIS', 'Debit', 'Credit', 'Transfer'];
var methods = res.methods || {};
order.forEach(function(k) {
var v = Number(methods[k] || 0);
boxMethods.innerHTML += '<div class="item-row"><span class="item-name">' + k + '</span><span class="item-val">' + fmtRp(v) + '</span></div>';
});
Object.keys(methods).forEach(function(k) {
if (order.indexOf(k) > -1) return;
var v = Number(methods[k] || 0);
boxMethods.innerHTML += '<div class="item-row"><span class="item-name">' + k + '</span><span class="item-val">' + fmtRp(v) + '</span></div>';
});
var boxPortions = document.getElementById('rk-portions');
boxPortions.innerHTML = '';
var list = Array.isArray(res.portions) ? res.portions : [];
if (!list.length) {
boxPortions.innerHTML = '<div style="font-size:12px; color:#999; text-align:center; padding:10px 0;">Tidak ada data porsi.</div>';
} else {
renderGroupedPorsiToBox(boxPortions, list.map(function(x){ return { nama: x.nama, qty: x.qty }; }));
}
}
function updateDashboard(res) {
// Sales Summary
document.getElementById('sum-sales').innerText = fmtRp(res.summary.totalSales);
document.getElementById('sum-profit').innerText = fmtRp(res.summary.netProfit);
document.getElementById('sum-trans').innerText = res.summary.totalTransactions;
document.getElementById('sum-avg').innerText = fmtRp(res.summary.avgTransaction);
// Expense Summary
document.getElementById('sum-expense').innerText = fmtRp(res.summary.totalBelanja);
var maxExp = res.topBelanja.length > 0 ? res.topBelanja[0].total : 0;
document.getElementById('sum-max-expense').innerText = fmtRp(maxExp);
// Top Items
var itemList = document.getElementById('top-items-list');
itemList.innerHTML = '';
renderGroupedPorsiToBox(itemList, (res.topItems || []).map(function(it){ return { nama: it[0], qty: it[1] }; }));
// Top Expense
var expList = document.getElementById('top-expense-list');
expList.innerHTML = '';
res.topBelanja.forEach(function(it) {
expList.innerHTML += '<div class="item-row"><div class="item-info"><div class="item-name">' + it.nama + '</div><div style="font-size:10px; color:#999;">' + it.tgl + ' | ' + it.kat + '</div></div><span class="item-val" style="color:#f59e0b;">' + fmtRp(it.total) + '</span></div>';
});
// Charts
renderSalesCharts(res);
renderExpenseCharts(res);
}
function loadSalesRange() {
var f = document.getElementById('sales-from').value || shiftDateStr(-6);
var t = document.getElementById('sales-to').value || getTodayStr();
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(res) {
document.getElementById('loading').style.display = 'none';
if (!res || res.error) { alert(res && res.error ? res.error : 'Gagal'); return; }
renderSalesFromBundle(res);
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal memuat penjualan: ' + err.message);
}).getDashboardBundleByRange(f, t);
}
function renderSalesFromBundle(bundle) {
var sales = bundle.sales || {};
var exp = bundle.expense || {};
document.getElementById('sum-sales').innerText = fmtRp((sales.summary && sales.summary.totalSales) || 0);
document.getElementById('sum-trans').innerText = Number((sales.summary && sales.summary.totalTransactions) || 0);
document.getElementById('sum-avg').innerText = fmtRp((sales.summary && sales.summary.avgTransaction) || 0);
document.getElementById('sum-profit').innerText = fmtRp(Number(bundle.profit || 0));
var b = sales.buckets || {};
document.getElementById('sum-bucket-lt100').innerText = Number(b.lt100 || 0);
document.getElementById('sum-bucket-gte100').innerText = Number(b.gte100 || 0);
document.getElementById('sum-bucket-gte200').innerText = Number(b.gte200 || 0);
var itemList = document.getElementById('top-items-list');
itemList.innerHTML = '';
renderGroupedPorsiToBox(itemList, (sales.topItems || []).map(function(it){ return { nama: it[0], qty: it[1] }; }));
var avgBox = document.getElementById('avg-items-list');
avgBox.innerHTML = '';
(sales.avgMenuPerDay || []).forEach(function(it) {
avgBox.innerHTML += '<div class="item-row"><span class="item-name">' + it.nama + '</span><span class="item-val">' + (Number(it.avg || 0).toFixed(1)) + ' / hari</span></div>';
});
if (!(sales.avgMenuPerDay || []).length) avgBox.innerHTML = '<div class="muted" style="text-align:center; padding:10px 0;">Tidak ada data.</div>';
renderSalesChartsFromSales(sales);
renderHourlyChart(sales.hourlyCounts || []);
document.getElementById('sum-expense').innerText = fmtRp((exp.summary && exp.summary.totalBelanja) || 0);
var maxExp = (exp.summary && exp.summary.maxBelanja) || 0;
document.getElementById('sum-max-expense').innerText = fmtRp(maxExp);
renderExpenseChartsFromExpense(exp);
renderTopExpense(exp.topBelanja || []);
}
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function(c) {
return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]);
});
}
function normalizePorsiName(name) {
var s = String(name || '').trim();
var isArrow = /^->\s*/.test(s);
var base = s.replace(/^->\s*/, '').trim();
return { raw: s, base: base, isArrow: isArrow };
}
function groupPorsiList(list) {
var grouped = {};
(list || []).forEach(function(it) {
if (!it) return;
var qty = Number(it.qty) || 0;
if (!qty) return;
var info = normalizePorsiName(it.nama);
var base = info.base || info.raw;
if (!grouped[base]) grouped[base] = { base: base, total: 0, arrow: 0, normal: 0 };
grouped[base].total += qty;
if (info.isArrow) grouped[base].arrow += qty;
else grouped[base].normal += qty;
});
return Object.keys(grouped).map(function(k){ return grouped[k]; })
.sort(function(a,b){ return (b.total||0)-(a.total||0); });
}
function renderGroupedPorsiToBox(box, list) {
if (!box) return;
var groups = groupPorsiList(list);
if (!groups.length) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:10px 0;">Tidak ada data.</div>';
return;
}
var html = '';
groups.forEach(function(g) {
html += '<div class="item-row" style="font-weight:900;"><span class="item-name">' + esc(g.base) + '</span><span class="item-val">' + (Number(g.total)||0) + ' Porsi</span></div>';
if ((g.arrow||0) > 0 && (g.normal||0) > 0) {
html += '<div class="item-row" style="padding-left:12px; opacity:0.8;"><span class="item-name">-> ' + esc(g.base) + '</span><span class="item-val">' + (Number(g.arrow)||0) + '</span></div>';
html += '<div class="item-row" style="padding-left:12px; opacity:0.8;"><span class="item-name">' + esc(g.base) + '</span><span class="item-val">' + (Number(g.normal)||0) + '</span></div>';
}
});
box.innerHTML = html;
}
function loadExpenseRange() {
var f = document.getElementById('exp-from').value || shiftDateStr(-6);
var t = document.getElementById('exp-to').value || getTodayStr();
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(exp) {
document.getElementById('loading').style.display = 'none';
if (!exp || exp.error) { alert(exp && exp.error ? exp.error : 'Gagal'); return; }
document.getElementById('sum-expense').innerText = fmtRp((exp.summary && exp.summary.totalBelanja) || 0);
document.getElementById('sum-max-expense').innerText = fmtRp((exp.summary && exp.summary.maxBelanja) || 0);
renderExpenseChartsFromExpense(exp);
renderTopExpense(exp.topBelanja || []);
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal memuat belanja: ' + err.message);
}).getExpenseAnalyticsByRange(f, t);
}
function renderSalesChartsFromSales(sales) {
var daily = sales.dailySales || [];
var labels = daily.map(d => d.date);
var salesData = daily.map(d => d.sales);
if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(document.getElementById('dailyChart'), {
type: 'line',
data: { labels: labels, datasets: [{ label: 'Penjualan', data: salesData, borderColor: '#e11d48', backgroundColor: 'rgba(225,29,72,0.15)', tension: 0.3, fill: true }] },
options: { responsive: true, plugins: { legend: { display: false } } }
});
var methods = sales.methods || {};
var methodLabels = Object.keys(methods);
var methodData = methodLabels.map(k => methods[k]);
if (methodChart) methodChart.destroy();
methodChart = new Chart(document.getElementById('methodChart'), {
type: 'doughnut',
data: { labels: methodLabels, datasets: [{ data: methodData, backgroundColor: ['#e11d48','#10b981','#6366f1','#f59e0b','#111'] }] },
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
});
}
function renderHourlyChart(counts) {
var labels = [];
var data = [];
for (var i = 0; i < 24; i++) { labels.push(String(i).padStart(2, '0') + ':00'); data.push(Number(counts[i] || 0)); }
if (hourlyChart) hourlyChart.destroy();
hourlyChart = new Chart(document.getElementById('hourlyChart'), {
type: 'bar',
data: { labels: labels, datasets: [{ label: 'Jumlah Transaksi', data: data, backgroundColor: 'rgba(225,29,72,0.6)' }] },
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } }
});
}
function renderExpenseChartsFromExpense(exp) {
var daily = exp.dailyBelanja || [];
var labels = daily.map(d => d.date);
var expData = daily.map(d => d.total);
if (expenseDailyChart) expenseDailyChart.destroy();
expenseDailyChart = new Chart(document.getElementById('expenseDailyChart'), {
type: 'line',
data: { labels: labels, datasets: [{ label: 'Belanja', data: expData, borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.15)', tension: 0.3, fill: true }] },
options: { responsive: true, plugins: { legend: { display: false } } }
});
var cats = exp.kategoriBelanja || {};
var catLabels = Object.keys(cats);
var catData = catLabels.map(k => cats[k]);
if (expenseCatChart) expenseCatChart.destroy();
expenseCatChart = new Chart(document.getElementById('expenseCatChart'), {
type: 'pie',
data: { labels: catLabels, datasets: [{ data: catData, backgroundColor: ['#f59e0b','#e11d48','#10b981','#6366f1','#111','#94a3b8'] }] },
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
});
}
function renderTopExpense(list) {
var expList = document.getElementById('top-expense-list');
expList.innerHTML = '';
(list || []).forEach(function(it) {
expList.innerHTML += '<div class="item-row"><div class="item-info"><div class="item-name">' + it.nama + '</div><div style="font-size:10px; color:#999;">' + it.tgl + ' | ' + it.kat + '</div></div><span class="item-val" style="color:#f59e0b;">' + fmtRp(it.total) + '</span></div>';
});
if (!(list || []).length) expList.innerHTML = '<div class="muted" style="text-align:center; padding:10px 0;">Tidak ada data.</div>';
}
function loadHistory(reset) {
var f = document.getElementById('rk-from').value || shiftDateStr(-6);
var t = document.getElementById('rk-to').value || getTodayStr();
var q = String(document.getElementById('his-q').value || '').trim();
if (reset) {
historyOffset = 0;
historyCache = {};
document.getElementById('his-list').innerHTML = '';
document.getElementById('his-more-wrap').style.display = 'none';
}
document.getElementById('loading').style.display = 'flex';
google.script.run.withSuccessHandler(function(res) {
document.getElementById('loading').style.display = 'none';
if (!res || res.error) { alert(res && res.error ? res.error : 'Gagal'); return; }
historyOffset = Number(res.nextOffset || 0);
renderHistoryList(res.items || []);
document.getElementById('his-more-wrap').style.display = res.hasMore ? 'block' : 'none';
}).withFailureHandler(function(err) {
document.getElementById('loading').style.display = 'none';
alert('Gagal memuat riwayat: ' + err.message);
}).getTransaksiHistoryPage(f, t, q, historyOffset, 50);
}
function renderHistoryList(items) {
var box = document.getElementById('his-list');
if (!box) return;
if (!items.length && !box.innerHTML) {
box.innerHTML = '<div class="muted" style="text-align:center; padding:10px 0;">Tidak ada transaksi.</div>';
return;
}
items.forEach(function(t) {
historyCache[String(t.id)] = t;
var dateKey = String(t.tgl || '');
var dateBlockId = 'his-date-' + dateKey.replace(/[^0-9]/g, '');
var dateBlock = document.getElementById(dateBlockId);
if (!dateBlock) {
var wrap = document.createElement('div');
wrap.id = dateBlockId;
wrap.innerHTML = '<div class="history-date"><span>' + dateKey + '</span><span class="muted"> </span></div><div class="history-items"></div>';
box.appendChild(wrap);
dateBlock = wrap;
}
var itemsBox = dateBlock.querySelector('.history-items');
var el = document.createElement('div');
el.className = 'history-card';
el.onclick = function() { openTransDetail(t.id); };
el.innerHTML = '<div class="top"><div class="id">' + t.id + '</div><div class="amt">' + fmtRp(t.total || 0) + '</div></div>' +
'<div class="sub"><span>Meja ' + (t.meja || '-') + ' | ' + (t.nama || '-') + '</span><span>' + (t.metodeBayar || '-') + '</span></div>';
itemsBox.appendChild(el);
});
}
function openTransDetail(id) {
var t = historyCache[String(id)];
if (!t) return;
document.getElementById('modal-title').innerText = 'Rincian ' + t.id;
var items = Array.isArray(t.items) ? t.items : [];
var itemsHtml = items.map(function(it) {
var nm = String(it.nama || '');
var qty = Number(it.qty || 0);
var harga = Number(it.harga || 0);
return '<div class="item-row"><span class="item-name">' + nm + ' x' + qty + '</span><span class="item-val">' + fmtRp(qty * harga) + '</span></div>';
}).join('');
if (!itemsHtml) itemsHtml = '<div class="muted" style="text-align:center; padding:10px 0;">Tidak ada item.</div>';
var body = '<div class="muted" style="margin-bottom:10px;">' + (t.tgl || '') + ' • ' + (t.timestamp || '') + '</div>' +
'<div class="item-row"><span class="item-name">Status</span><span class="item-val">' + (t.status || '-') + '</span></div>' +
'<div class="item-row"><span class="item-name">Metode</span><span class="item-val">' + (t.metodeBayar || '-') + '</span></div>' +
'<div class="item-row"><span class="item-name">Total</span><span class="item-val">' + fmtRp(t.total || 0) + '</span></div>' +
'<div style="margin-top:12px; font-weight:900;">Items</div>' + itemsHtml;
if (t.catatan) body += '<div style="margin-top:12px; font-weight:900;">Catatan</div><div class="muted" style="white-space:pre-wrap;">' + escapeHtml(String(t.catatan || '')) + '</div>';
document.getElementById('modal-body').innerHTML = body;
document.getElementById('modal').style.display = 'flex';
}
function escapeHtml(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');
}
function closeModal(e) {
if (e && e.target && e.target.classList && !e.target.classList.contains('modal')) return;
document.getElementById('modal').style.display = 'none';
}
function renderSalesCharts(res) {
var ctx1 = document.getElementById('dailyChart').getContext('2d');
if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(ctx1, {
type: 'line',
data: {
labels: res.daily.map(d => d.date),
datasets: [{
label: 'Penjualan',
data: res.daily.map(d => d.sales),
borderColor: '#e11d48',
backgroundColor: 'rgba(225, 29, 72, 0.1)',
fill: true,
tension: 0.3
}]
},
options: { responsive: true, plugins: { legend: { display: false } } }
});
var ctx2 = document.getElementById('methodChart').getContext('2d');
if (methodChart) methodChart.destroy();
methodChart = new Chart(ctx2, {
type: 'doughnut',
data: {
labels: Object.keys(res.methods),
datasets: [{ data: Object.values(res.methods), backgroundColor: ['#e11d48', '#2563eb', '#10b981', '#f59e0b', '#6366f1'] }]
},
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
});
}
function renderExpenseCharts(res) {
var ctx1 = document.getElementById('expenseDailyChart').getContext('2d');
if (expenseDailyChart) expenseDailyChart.destroy();
expenseDailyChart = new Chart(ctx1, {
type: 'bar',
data: {
labels: res.daily.map(d => d.date),
datasets: [{
label: 'Belanja',
data: res.daily.map(d => d.expense),
backgroundColor: '#f59e0b'
}]
},
options: { responsive: true, plugins: { legend: { display: false } } }
});
var ctx2 = document.getElementById('expenseCatChart').getContext('2d');
if (expenseCatChart) expenseCatChart.destroy();
expenseCatChart = new Chart(ctx2, {
type: 'pie',
data: {
labels: Object.keys(res.kategoriBelanja),
datasets: [{ data: Object.values(res.kategoriBelanja), backgroundColor: ['#f59e0b', '#10b981', '#ef4444', '#3b82f6', '#8b5cf6'] }]
},
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
});
}
</script>
</body>
</html>