1150 lines
55 KiB
HTML
1150 lines
55 KiB
HTML
<!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 (Dari–Sampai)</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 < 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 (Dari–Sampai)</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 (Dari–Sampai)</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 ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"').replace(/'/g,''');
|
||
}
|
||
|
||
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>
|