<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pangsit Mie Tetangga - POS & Finance</title>
</head>
<body>
<!-- <!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pangsit Mie Tetangga - POS & Finance</title>
<!-- Tailwind -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- html2canvas -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
50: '#fff8ef',
100: '#ffedd8',
200: '#ffd5ad',
300: '#ffb978',
400: '#ff9745',
500: '#f47c20',
600: '#dc6410',
700: '#b94c0d',
800: '#963d10',
900: '#77350f'
}
},
boxShadow: {
soft: '0 4px 20px rgba(30,41,59,.06)',
card: '0 2px 12px rgba(30,41,59,.05)'
}
}
}
}
</script>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #faf9f7;
color: #172033;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button, input, select, textarea {
font: inherit;
}
button {
-webkit-tap-highlight-color: transparent;
}
.sidebar {
width: 270px;
transition: .25s ease;
}
.main-area {
margin-left: 270px;
transition: .25s ease;
}
@media(max-width: 1024px) {
.sidebar {
transform: translateX(-100%);
position: fixed;
z-index: 100;
height: 100vh;
}
.sidebar.open {
transform: translateX(0);
}
.main-area {
margin-left: 0;
}
}
@media(max-width: 640px) {
.desktop-only {
display: none !important;
}
.mobile-bottom-nav {
display: flex;
}
.main-content {
padding-bottom: 95px !important;
}
}
@media(min-width: 641px) {
.mobile-bottom-nav {
display: none;
}
}
.card {
background: white;
border: 1px solid #ece9e4;
border-radius: 14px;
box-shadow: 0 2px 12px rgba(30,41,59,.035);
}
.stat-card {
transition: .2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(30,41,59,.07);
}
.food-icon {
width: 46px;
height: 46px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.food-icon.orange { background: #fff0dd; color: #e66d16; }
.food-icon.green { background: #e5f8e8; color: #159447; }
.food-icon.red { background: #ffe7e7; color: #dc3f3f; }
.food-icon.purple { background: #f1e8ff; color: #7137c9; }
.food-icon.blue { background: #e8f1ff; color: #2563c7; }
.menu-card { transition: .18s ease; }
.menu-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 28px rgba(30,41,59,.08);
}
.menu-img {
height: 145px;
width: 100%;
object-fit: cover;
}
.qty-control {
display: flex;
align-items: center;
gap: 8px;
}
.qty-btn {
width: 32px;
height: 32px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
background: #fff3e5;
color: #d7630c;
transition: .15s;
}
.qty-btn:hover { background: #ffe1c0; }
.floating-cart { animation: slideUp .25s ease; }
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-backdrop {
background: rgba(15,23,42,.42);
backdrop-filter: blur(3px);
}
.tab-active {
background: #fff0dc;
color: #c9570a;
}
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
.receipt {
width: 320px;
background: white;
padding: 22px;
color: #111827;
font-family: Arial, sans-serif;
}
.receipt-line {
border-top: 1px dashed #aaa;
margin: 12px 0;
}
@media(max-width: 640px) {
.modal-container {
width: 100%;
max-height: 94vh;
border-radius: 22px 22px 0 0;
align-self: flex-end;
}
}
.toast { animation: toastIn .25s ease; }
@keyframes toastIn {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
</style>
</head>
<body>
<div id="app">
<!-- MOBILE OVERLAY -->
<div id="sidebarOverlay" class="fixed inset-0 bg-black/30 z-[90] hidden lg:hidden" onclick="toggleSidebar()"></div>
<!-- SIDEBAR -->
<aside id="sidebar" class="sidebar fixed left-0 top-0 bottom-0 bg-white border-r border-stone-200 flex flex-col z-[100]">
<div class="px-6 py-5 border-b border-stone-100">
<div class="flex items-center gap-3">
<div class="w-14 h-14 rounded-2xl bg-brand-50 flex items-center justify-center">
<i data-lucide="soup" class="w-9 h-9 text-brand-600"></i>
</div>
<div>
<div class="font-extrabold text-xl leading-tight text-stone-900">Pangsit Mie</div>
<div class="font-semibold italic text-brand-600 text-xl leading-tight">Tetangga</div>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto px-4 py-5">
<div class="mb-7">
<div class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 px-3 mb-2">Utama</div>
<button onclick="navigate('dashboard')" data-nav="dashboard" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium">
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
Dashboard
</button>
</div>
<div class="mb-7">
<div class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 px-3 mb-2">POS & Transaksi</div>
<button onclick="navigate('pos')" data-nav="pos" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="soup" class="w-5 h-5"></i>
Kasir (POS)
</button>
<button onclick="navigate('history')" data-nav="history" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="receipt-text" class="w-5 h-5"></i>
Riwayat Transaksi
</button>
<button onclick="navigate('reports')" data-nav="reports" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="chart-column" class="w-5 h-5"></i>
Laporan
</button>
</div>
<div class="mb-7">
<div class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 px-3 mb-2">Keuangan</div>
<button onclick="navigate('accounts')" data-nav="accounts" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="wallet-cards" class="w-5 h-5"></i>
Rekening & Saldo
</button>
<button onclick="navigate('transactions')" data-nav="transactions" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="arrow-right-left" class="w-5 h-5"></i>
Transaksi
</button>
<button onclick="navigate('finance')" data-nav="finance" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="chart-no-axes-combined" class="w-5 h-5"></i>
Laporan Keuangan
</button>
</div>
<div class="mb-7">
<div class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 px-3 mb-2">Manajemen</div>
<button onclick="navigate('menus')" data-nav="menus" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="utensils" class="w-5 h-5"></i>
Menu
</button>
<button onclick="navigate('users')" data-nav="users" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="user-round" class="w-5 h-5"></i>
Akun
</button>
<button onclick="navigate('brand')" data-nav="brand" class="nav-item w-full flex items-center gap-3 px-3 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-stone-50">
<i data-lucide="store" class="w-5 h-5"></i>
Profil Brand
</button>
</div>
</div>
<div class="p-4 border-t border-stone-100">
<div class="rounded-xl border border-stone-200 p-3 flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-brand-100 text-brand-700 flex items-center justify-center">
<i data-lucide="user-round" class="w-5 h-5"></i>
</div>
<div class="flex-1 min-w-0">
<div class="font-semibold text-sm truncate" id="currentUserRoleName">Admin Owner</div>
<div class="text-xs text-slate-500" id="currentUserRoleTag">Owner</div>
</div>
<i data-lucide="shield-check" class="w-4 h-4 text-emerald-600"></i>
</div>
<button onclick="logoutUser()" class="w-full mt-3 flex items-center justify-center gap-2 px-3 py-2.5 rounded-xl border border-red-200 text-red-600 bg-red-50 hover:bg-red-100 text-sm font-semibold transition">
<i data-lucide="log-out" class="w-4 h-4"></i>
Logout / Keluar
</button>
</div>
</aside>
<!-- MAIN AREA -->
<main class="main-area min-h-screen">
<header class="h-[74px] bg-white border-b border-stone-200 flex items-center px-5 lg:px-7 sticky top-0 z-40">
<button onclick="toggleSidebar()" class="lg:hidden mr-4 p-2 rounded-lg hover:bg-stone-100">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
<div class="flex-1"></div>
<div class="flex items-center gap-3 sm:gap-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-brand-50 text-brand-600 flex items-center justify-center">
<i data-lucide="store" class="w-5 h-5"></i>
</div>
<div class="hidden sm:block">
<div class="text-sm font-semibold">Pangsit Mie Tetangga</div>
<div class="flex items-center gap-1 text-xs text-emerald-600">
<span class="w-2 h-2 bg-emerald-500 rounded-full"></span>
Online
</div>
</div>
</div>
</div>
</header>
<div id="content" class="main-content p-5 lg:p-7"></div>
</main>
<!-- MOBILE BOTTOM NAV -->
<nav class="mobile-bottom-nav fixed bottom-0 left-0 right-0 bg-white border-t border-stone-200 z-[80] px-2 py-2 justify-around">
<button onclick="navigate('dashboard')" class="flex flex-col items-center gap-1 p-2 text-slate-500">
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
<span class="text-[10px]">Home</span>
</button>
<button onclick="navigate('pos')" class="flex flex-col items-center gap-1 p-2 text-slate-500">
<i data-lucide="soup" class="w-5 h-5"></i>
<span class="text-[10px]">Kasir</span>
</button>
<button onclick="navigate('transactions')" class="flex flex-col items-center gap-1 p-2 text-slate-500">
<i data-lucide="arrow-right-left" class="w-5 h-5"></i>
<span class="text-[10px]">Transaksi</span>
</button>
<button onclick="navigate('history')" class="flex flex-col items-center gap-1 p-2 text-slate-500">
<i data-lucide="receipt-text" class="w-5 h-5"></i>
<span class="text-[10px]">Riwayat</span>
</button>
</nav>
</div>
<div id="modalRoot"></div>
<div id="toastContainer" class="fixed right-4 bottom-5 z-[300] space-y-2"></div>
<script>
/* =========================================================
DATABASE DEMO
========================================================= */
const defaultMenus = [
{ id: 1, name: "Pangsit Mie Original", category: "Makanan", price: 16000, image: "https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=600&q=80" },
{ id: 2, name: "Pangsit Kuah", category: "Makanan", price: 12000, image: "https://images.unsplash.com/photo-1585032226651-759b368d7246?auto=format&fit=crop&w=600&q=80" },
{ id: 3, name: "Dimsum", category: "Makanan", price: 11000, image: "https://images.unsplash.com/photo-1496116218417-1a781b1c416c?auto=format&fit=crop&w=600&q=80" },
{ id: 4, name: "Es Jeruk", category: "Minuman", price: 10000, image: "https://images.unsplash.com/photo-1621263764928-df1444c5e859?auto=format&fit=crop&w=600&q=80" },
{ id: 5, name: "Teh Manis", category: "Minuman", price: 8000, image: "https://images.unsplash.com/photo-1556679343-c7306c1976bc?auto=format&fit=crop&w=600&q=80" },
{ id: 6, name: "Es Teh Lemon", category: "Minuman", price: 10000, image: "https://images.unsplash.com/photo-1556679343-c7306c1976bc?auto=format&fit=crop&w=600&q=80" },
{ id: 7, name: "Extra Pangsit", category: "Extra", price: 5000, image: "https://images.unsplash.com/photo-1496116218417-1a781b1c416c?auto=format&fit=crop&w=600&q=80" },
{ id: 8, name: "Extra Bakso", category: "Extra", price: 5000, image: "https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=600&q=80" }
];
const defaultAccounts = [
{ id: 1, name: "Cash (Tunai)", type: "Cash", balance: 320000 },
{ id: 2, name: "BCA", type: "Bank", balance: 250000 },
{ id: 3, name: "BRI", type: "Bank", balance: 150000 },
{ id: 4, name: "QRIS", type: "Digital", balance: 100000 },
{ id: 5, name: "ShopeePay", type: "Digital", balance: 50000 }
];
const defaultUsers = [
{ id: 1, name: "Admin Owner", username: "owner", password: "owner123", role: "Owner" },
{ id: 2, name: "Kasir Toko", username: "kasir", password: "kasir123", role: "Kasir" }
];
const defaultTransactions = [
{ id: 1001, type: "Pemasukan", amount: 500000, account: "BCA", note: "Modal tambahan", date: "2026-09-28T10:24:00", status: "Berhasil" },
{ id: 1002, type: "Penjualan", amount: 85000, account: "QRIS", note: "Pangsit Mie x2, Es Jeruk x1", date: "2026-09-28T09:45:00", status: "Berhasil", items: [{ name: "Pangsit Mie Original", qty: 2, price: 16000 }, { name: "Es Jeruk", qty: 1, price: 10000 }] },
{ id: 1003, type: "Pengeluaran Usaha", amount: 275000, account: "Cash (Tunai)", note: "Bahan baku", date: "2026-09-28T08:30:00", status: "Berhasil" },
{ id: 1004, type: "Pengeluaran Pribadi", amount: 100000, account: "Cash (Tunai)", note: "Bensin", date: "2026-09-28T07:15:00", status: "Berhasil" }
];
let menus = JSON.parse(localStorage.getItem("pmt_menus")) || defaultMenus;
let accounts = JSON.parse(localStorage.getItem("pmt_accounts")) || defaultAccounts;
let transactions = JSON.parse(localStorage.getItem("pmt_transactions")) || defaultTransactions;
let users = JSON.parse(localStorage.getItem("pmt_users")) || defaultUsers;
let cart = [];
let currentPage = "dashboard";
let currentPOSCategory = "Semua";
let currentUserRole = localStorage.getItem("pmt_current_role") || "Owner";
let currentUserName = localStorage.getItem("pmt_current_name") || (currentUserRole === "Owner" ? "Admin Owner" : "Kasir Toko");
let currentUserId = Number(localStorage.getItem("pmt_current_user_id")) || users.find(user => user.name === currentUserName && user.role === currentUserRole)?.id || null;
/* =========================================================
SESSION / LOGIN
========================================================= */
function isLoggedIn() {
return localStorage.getItem("pmt_logged_in") === "1";
}
function updateCurrentUserUI() {
const name = document.getElementById("currentUserRoleName");
const role = document.getElementById("currentUserRoleTag");
if (name) name.textContent = currentUserName;
if (role) role.textContent = currentUserRole;
}
function loginAs(event) {
event.preventDefault();
const username = document.getElementById("loginUsername").value.trim().toLowerCase();
const password = document.getElementById("loginPassword").value;
const user = users.find(item => item.username.toLowerCase() === username && item.password === password);
if (!user) {
showToast("Username atau password salah.", "error");
return;
}
currentUserId = user.id;
currentUserRole = user.role;
currentUserName = user.name;
localStorage.setItem("pmt_logged_in", "1");
localStorage.setItem("pmt_current_user_id", String(user.id));
localStorage.setItem("pmt_current_role", currentUserRole);
localStorage.setItem("pmt_current_name", currentUserName);
document.getElementById("loginRoot")?.remove();
render();
showToast(`Masuk sebagai ${currentUserName}`);
}
function logoutUser() {
if (!confirm("Yakin ingin keluar dari aplikasi?")) return;
localStorage.removeItem("pmt_logged_in");
localStorage.removeItem("pmt_current_user_id");
localStorage.removeItem("pmt_current_role");
localStorage.removeItem("pmt_current_name");
currentUserId = null;
currentUserRole = "Owner";
currentUserName = "Admin Owner";
document.getElementById("modalRoot").innerHTML = "";
showLoginScreen();
}
function showLoginScreen() {
let root = document.getElementById("loginRoot");
if (!root) {
root = document.createElement("div");
root.id = "loginRoot";
root.className = "fixed inset-0 z-[500] bg-slate-950/95 flex items-center justify-center p-4";
document.body.appendChild(root);
}
root.innerHTML = `
<div class="w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="p-7 bg-gradient-to-br from-brand-50 to-white border-b border-stone-100">
<div class="w-14 h-14 rounded-2xl bg-brand-500 text-white flex items-center justify-center mb-4">${icon("soup",28)}</div>
<h1 class="text-2xl font-extrabold text-slate-900">Pangsit Mie Tetangga</h1>
<p class="text-sm text-slate-500 mt-1">Masuk dengan username dan password.</p>
</div>
<form onsubmit="loginAs(event)" class="p-6 space-y-4">
<div>
<label for="loginUsername" class="block text-sm font-semibold mb-1">Username</label>
<input id="loginUsername" required autocomplete="username" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200">
</div>
<div>
<label for="loginPassword" class="block text-sm font-semibold mb-1">Password</label>
<input id="loginPassword" type="password" required autocomplete="current-password" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200">
</div>
<button type="submit" class="w-full bg-brand-500 hover:bg-brand-600 text-white py-3 rounded-xl font-bold">Masuk</button>
<p class="text-[11px] text-slate-400 text-center">Akun bawaan demo: owner / owner123 · kasir / kasir123. Data akun tersimpan lokal dan bukan untuk autentikasi produksi.</p>
</form>
</div>`;
lucide.createIcons();
}
/* =========================================================
UTILITIES & APPS SCRIPT SYNC LOGIC
========================================================= */
let sharedSaveInFlight = false;
let sharedSavePending = false;
let sharedSyncFailed = false;
let sharedUpdatedAt = "";
function persistLocalData() {
localStorage.setItem("pmt_menus", JSON.stringify(menus));
localStorage.setItem("pmt_accounts", JSON.stringify(accounts));
localStorage.setItem("pmt_transactions", JSON.stringify(transactions));
localStorage.setItem("pmt_users", JSON.stringify(users));
}
function sharedDataSnapshot() {
return {
menus: menus,
accounts: accounts,
transactions: transactions,
users: users
};
}
function saveData() {
persistLocalData();
if (!window.google || !google.script || !google.script.run) return;
sharedSavePending = true;
flushSharedSave();
}
function flushSharedSave() {
if (sharedSaveInFlight || !sharedSavePending || !google.script?.run) return;
sharedSaveInFlight = true;
sharedSavePending = false;
google.script.run
.withSuccessHandler(result => {
sharedSaveInFlight = false;
sharedSyncFailed = false;
sharedUpdatedAt = result?.updatedAt || sharedUpdatedAt;
if (sharedSavePending) flushSharedSave();
})
.withFailureHandler(error => {
sharedSaveInFlight = false;
sharedSyncFailed = true;
showToast(`Gagal menyimpan sinkronisasi: ${error?.message || "Periksa koneksi"}`, "error");
})
.saveSharedData(sharedDataSnapshot());
}
function applySharedData(data) {
if (Array.isArray(data.menus)) menus = data.menus;
if (Array.isArray(data.accounts)) accounts = data.accounts;
if (Array.isArray(data.transactions)) transactions = data.transactions;
if (Array.isArray(data.users)) users = data.users;
sharedUpdatedAt = data.updatedAt || "";
persistLocalData();
}
function startApp() {
const finish = () => {
if (isLoggedIn()) render();
else { render(); showLoginScreen(); }
};
if (!window.google || !google.script || !google.script.run) {
finish();
return;
}
google.script.run
.withSuccessHandler(data => {
if (data?.initialized) applySharedData(data);
else saveData();
finish();
window.setInterval(refreshSharedData, 15000);
})
.withFailureHandler(error => {
finish();
showToast(`Data server belum dapat dimuat: ${error?.message || "Periksa konfigurasi Apps Script"}`, "error");
})
.getSharedData();
}
function refreshSharedData() {
if (sharedSaveInFlight || sharedSavePending || sharedSyncFailed || !google.script?.run) return;
google.script.run
.withSuccessHandler(data => {
if (!data?.initialized || !data.updatedAt || data.updatedAt === sharedUpdatedAt) return;
applySharedData(data);
render();
showToast("Data diperbarui dari perangkat lain.", "info");
})
.withFailureHandler(() => {})
.getSharedData();
}
function rupiah(value) {
value = Number(value) || 0;
return "Rp " + value.toLocaleString("id-ID");
}
function numberFormat(value) {
value = Number(value) || 0;
return value.toLocaleString("id-ID");
}
function escapeHTML(str) {
return String(str ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function formatDate(date) {
return new Date(date).toLocaleDateString("id-ID", { day: "2-digit", month: "short", year: "numeric" });
}
function formatDateTime(date) {
return new Date(date).toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" });
}
function getTotalBalance() {
return accounts.reduce((sum, account) => sum + Number(account.balance || 0), 0);
}
function getTransactions(type = null) {
if (!type) return transactions;
return transactions.filter(t => t.type === type);
}
function totalTransaction(type) {
return getTransactions(type).reduce((sum, t) => sum + Number(t.amount || 0), 0);
}
function showToast(message, type = "success") {
const container = document.getElementById("toastContainer");
const color = type === "success" ? "emerald" : type === "error" ? "red" : "amber";
const div = document.createElement("div");
div.className = `toast bg-white border border-${color}-200 shadow-xl rounded-xl px-4 py-3 flex items-center gap-3`;
div.innerHTML = `
<div class="w-8 h-8 rounded-full bg-${color}-50 text-${color}-600 flex items-center justify-center">
<i data-lucide="${type === "success" ? "check" : type === "error" ? "x" : "info"}" class="w-4 h-4"></i>
</div>
<div class="text-sm font-medium text-slate-700">${escapeHTML(message)}</div>
`;
container.appendChild(div);
lucide.createIcons();
setTimeout(() => div.remove(), 3200);
}
/* =========================================================
APPS SCRIPT SYNC FUNCTIONALITY
========================================================= */
/* Data is persisted through google.script.run in saveData(). */
/*
async function syncToAppSheet(action, transactionData) {
if (appSheetConfig.appScriptUrl) {
const row = {
ID: String(transactionData.id),
Type: transactionData.type || "",
Amount: Number(transactionData.amount || 0),
Account: transactionData.account || "",
Note: transactionData.note || "",
Customer: transactionData.customer || "",
WhatsApp: transactionData.whatsapp || "",
Date: transactionData.date || new Date().toISOString(),
Status: transactionData.status || "Berhasil"
};
try {
const response = await fetch(appSheetConfig.appScriptUrl, { method: "POST", headers: { "Content-Type": "text/plain;charset=utf-8" }, body: JSON.stringify({ action: action, row: row }) });
const result = await response.json();
if (response.ok && result.success) return { success: true };
return { success: false, message: result.message || result.response || "Apps Script gagal." };
} catch (err) { return { success: false, message: err.message }; }
}
if (!appSheetConfig.appId || !appSheetConfig.accessKey) return { success: false, message: "Isi Apps Script URL atau App ID + Access Key AppSheet." };
const endpoint = `https://api.appsheet.com/api/v2/apps/${encodeURIComponent(appSheetConfig.appId)}/tables/${encodeURIComponent(appSheetConfig.tableName || "Transactions")}/Action`;
const rowPayload = { ID: String(transactionData.id), Type: transactionData.type || "", Amount: Number(transactionData.amount || 0), Account: transactionData.account || "", Note: transactionData.note || "", Customer: transactionData.customer || "", WhatsApp: transactionData.whatsapp || "", Date: transactionData.date || new Date().toISOString(), Status: transactionData.status || "Berhasil" };
try {
const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", "ApplicationAccessKey": appSheetConfig.accessKey }, body: JSON.stringify({ Action: action, Properties: { Locale: "id-ID", Timezone: "Asia/Jakarta" }, Rows: [rowPayload] }) });
if (response.ok) return { success: true };
return { success: false, message: await response.text() };
} catch (err) { return { success: false, message: err.message }; }
}
async function triggerSyncWithFeedback(action, data, successLabel) {
if (appSheetConfig.autoSync && (appSheetConfig.appScriptUrl || (appSheetConfig.appId && appSheetConfig.accessKey))) {
showToast("Mengirim sync ke AppSheet...", "info");
const res = await syncToAppSheet(action, data);
if (res.success) {
showToast(`[AppSheet] ${successLabel} berhasil disinkronkan!`, "success");
} else {
showToast(`Gagal Sync AppSheet: ${res.message || "Periksa API Key"}`, "error");
}
}
}
function openAppSheetModal() {
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-center justify-center p-4">
<div class="bg-white rounded-2xl w-full max-w-lg overflow-hidden shadow-2xl">
<div class="p-5 border-b border-stone-100 flex items-center justify-between bg-stone-50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-brand-100 text-brand-700 flex items-center justify-center font-bold">
<i data-lucide="cloud" class="w-5 h-5"></i>
</div>
<div>
<h2 class="text-lg font-bold text-slate-900">Pengaturan Sync AppSheet</h2>
<p class="text-xs text-slate-500">Hubungkan data transaksi ke spreadsheet AppSheet Anda</p>
</div>
</div>
<button onclick="closeModal()" class="p-2 rounded-xl hover:bg-stone-200 text-stone-500">
${icon("x", 20)}
</button>
</div>
<form onsubmit="saveAppSheetConfig(event)" class="p-5 space-y-4">
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-600 mb-1">Apps Script Web App URL (disarankan)</label>
<input id="cfgAppScriptUrl" value="${escapeHTML(appSheetConfig.appScriptUrl || "")}" placeholder="https://script.google.com/macros/s/.../exec" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm font-mono focus:ring-2 focus:ring-brand-200 outline-none">
<span class="text-[11px] text-slate-400">Frontend → Apps Script → AppSheet. API key tidak perlu disimpan di browser.</span>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-600 mb-1">
App ID AppSheet (fallback langsung)
</label>
<input id="cfgAppId" value="${escapeHTML(appSheetConfig.appId)}" placeholder="Contoh: 1a2b3c4d-5e6f-7g8h-9i0j-1234567890ab" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm font-mono focus:ring-2 focus:ring-brand-200 outline-none">
<span class="text-[11px] text-slate-400">Dapatkan di AppSheet -> Users -> Integrations -> API Access</span>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-600 mb-1">
Application Access Key / API Key
</label>
<input id="cfgAccessKey" type="password" value="${escapeHTML(appSheetConfig.accessKey)}" placeholder="V2-xxxx-xxxx-xxxx" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm font-mono focus:ring-2 focus:ring-brand-200 outline-none">
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-600 mb-1">
Nama Tabel Target
</label>
<input id="cfgTableName" value="${escapeHTML(appSheetConfig.tableName || "Transactions")}" placeholder="Transactions" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-brand-200 outline-none">
</div>
<div class="flex items-center justify-between p-3 bg-brand-50 rounded-xl">
<div>
<div class="text-sm font-semibold text-brand-900">Otomatis Sync (Auto-Sync)</div>
<div class="text-xs text-brand-700">Kirim data ke AppSheet saat buat, edit, & hapus.</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input id="cfgAutoSync" type="checkbox" ${appSheetConfig.autoSync ? "checked" : ""} class="sr-only peer">
<div class="w-11 h-6 bg-stone-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-stone-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-600"></div>
</label>
</div>
<div class="pt-2 flex flex-col sm:flex-row gap-2">
<button type="submit" class="flex-1 bg-brand-500 hover:bg-brand-600 text-white font-bold py-3 rounded-xl text-sm transition">
Simpan Pengaturan
</button>
<button type="button" onclick="testSyncAllToAppSheet()" class="px-4 py-3 border border-brand-200 text-brand-700 bg-brand-50 hover:bg-brand-100 font-semibold rounded-xl text-sm flex items-center justify-center gap-2">
${icon("refresh-cw", 16)}
Sync Semua Data
</button>
</div>
</form>
</div>
</div>
`;
lucide.createIcons();
}
function saveAppSheetConfig(event) {
event.preventDefault();
appSheetConfig.appScriptUrl = document.getElementById("cfgAppScriptUrl")?.value.trim() || "";
appSheetConfig.appId = document.getElementById("cfgAppId").value.trim();
appSheetConfig.accessKey = document.getElementById("cfgAccessKey").value.trim();
appSheetConfig.tableName = document.getElementById("cfgTableName").value.trim() || "Transactions";
appSheetConfig.autoSync = document.getElementById("cfgAutoSync").checked;
saveData();
closeModal();
showToast("Pengaturan AppSheet berhasil disimpan!");
}
async function testSyncAllToAppSheet() {
if (!appSheetConfig.appScriptUrl && (!appSheetConfig.appId || !appSheetConfig.accessKey)) {
showToast("Isi Apps Script URL atau App ID & API Access Key terlebih dahulu!", "error");
return;
}
showToast("Mengirim seluruh data ke AppSheet...", "info");
let successCount = 0;
for (const t of transactions) {
const res = await syncToAppSheet("Add", t);
if (res.success) successCount++;
}
showToast(`Proses selesai! ${successCount} dari ${transactions.length} transaksi tersinkron.`, "success");
}
*/
/* =========================================================
NAVIGATION & RENDER LOGIC
========================================================= */
function toggleSidebar() {
const sidebar = document.getElementById("sidebar");
const overlay = document.getElementById("sidebarOverlay");
sidebar.classList.toggle("open");
overlay.classList.toggle("hidden", !sidebar.classList.contains("open"));
}
function navigate(page) {
if (page === "users" && currentUserRole !== "Owner") {
showToast("Hanya Owner yang dapat mengelola akun.", "error");
page = "dashboard";
}
currentPage = page;
if (window.innerWidth < 1024 && document.getElementById("sidebar").classList.contains("open")) {
toggleSidebar();
}
render();
}
function updateNav() {
document.querySelectorAll(".nav-item").forEach(btn => {
btn.classList.remove("bg-brand-50", "text-brand-700");
btn.classList.add("text-slate-600");
});
const active = document.querySelector(`[data-nav="${currentPage}"]`);
if (active) {
active.classList.remove("text-slate-600");
active.classList.add("bg-brand-50", "text-brand-700");
}
}
function icon(name, size = "18") {
return `<i data-lucide="${name}" style="width:${size}px;height:${size}px"></i>`;
}
/* =========================================================
DASHBOARD PAGE
========================================================= */
function dashboardPage() {
const sales = totalTransaction("Penjualan");
const income = totalTransaction("Pemasukan");
const businessExpense = totalTransaction("Pengeluaran Usaha");
const personalExpense = totalTransaction("Pengeluaran Pribadi");
return `
<div class="max-w-[1500px] mx-auto">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6">
<div>
<div class="flex items-center gap-2">
<h1 class="text-2xl lg:text-[27px] font-bold text-slate-900">
Selamat datang, ${escapeHTML(currentUserName)}
</h1>
</div>
<p class="text-slate-500 mt-1">Kelola bisnis kuliner Anda dengan mudah dan efisien.</p>
</div>
<div class="flex items-center gap-3">
<button onclick="openTransactionModal('Pemasukan')" class="bg-brand-500 hover:bg-brand-600 text-white px-4 py-3 rounded-xl flex items-center justify-center gap-2 font-semibold shadow-sm">
${icon("plus",17)}
Tambah Transaksi
</button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-5">
${statCard("Penjualan Hari Ini", rupiah(sales), "soup", "orange", "+12%", "dari kemarin")}
${statCard("Pemasukan Lain", rupiah(income), "coins", "green", "+100%", "dari kemarin")}
${statCard("Pengeluaran Usaha", rupiah(businessExpense), "shopping-basket", "red", "-8%", "dari kemarin")}
${statCard("Pengeluaran Pribadi", rupiah(personalExpense), "wallet", "purple", "-5%", "dari kemarin")}
</div>
<div class="card bg-gradient-to-r from-[#fff8ef] to-[#fff0db] border-brand-200 p-5 lg:p-6 mb-5">
<div class="flex flex-col lg:flex-row lg:items-center gap-5">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-full bg-brand-500 text-white flex items-center justify-center shadow-sm">
${icon("wallet",26)}
</div>
<div>
<div class="text-sm text-brand-800 font-semibold">Saldo Bersih Total</div>
<div class="text-2xl lg:text-3xl font-extrabold text-slate-950">${rupiah(getTotalBalance())}</div>
</div>
</div>
<div class="hidden lg:block h-12 w-px bg-brand-200"></div>
<div>
<div class="text-xs text-slate-500">Total Saldo Rekening Terhubung</div>
<div class="font-bold text-lg">${rupiah(getTotalBalance())}</div>
</div>
<div class="lg:ml-auto">
<button onclick="navigate('accounts')" class="flex items-center gap-2 text-sm font-semibold text-brand-700">
Lihat Rekening ${icon("chevron-right",16)}
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-[1fr_330px] gap-5">
<div class="space-y-5">
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
${icon("soup",19)}
<h2 class="font-bold text-lg">Penjualan Menu Terlaris</h2>
</div>
<button onclick="navigate('reports')" class="text-sm text-brand-600 font-semibold">Lihat Semua</button>
</div>
${renderBestSeller()}
</div>
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="font-bold text-lg">Transaksi Terbaru</h2>
<button onclick="navigate('history')" class="text-sm text-brand-600 font-semibold">Lihat Semua</button>
</div>
${renderRecentTransactions()}
</div>
</div>
<div class="space-y-5">
<div class="card p-5">
<h2 class="font-bold text-lg mb-4">Tambah Transaksi Cepat</h2>
${quickTransaction("Pemasukan", "Modal, transfer, dll", "coins", "green")}
${quickTransaction("Penjualan", "Transaksi kasir", "soup", "orange")}
${quickTransaction("Pengeluaran Usaha", "Bahan baku, listrik, gas", "shopping-basket", "red")}
${quickTransaction("Pengeluaran Pribadi", "Prive, bensin, dll", "wallet", "purple")}
</div>
<div class="card p-5">
<div class="flex items-center justify-between mb-3">
<h2 class="font-bold">Rekening & Saldo</h2>
<button onclick="navigate('accounts')" class="text-xs text-brand-600 font-semibold">Lihat Semua</button>
</div>
${accounts.slice(0,5).map(accountRow).join("")}
</div>
</div>
</div>
</div>
`;
}
function statCard(title, amount, iconName, color, trend, subtitle) {
return `
<div class="card stat-card p-5">
<div class="flex items-center gap-3">
<div class="food-icon ${color}">${icon(iconName,23)}</div>
<div class="min-w-0">
<div class="text-xs sm:text-sm text-slate-500">${title}</div>
<div class="font-extrabold text-xl mt-1 truncate">${amount}</div>
</div>
</div>
<div class="mt-4 text-xs">
<span class="${trend.includes('-') ? 'text-red-500' : 'text-emerald-600'} font-semibold">${trend}</span>
<span class="text-slate-400">${subtitle}</span>
</div>
</div>
`;
}
function quickTransaction(type, description, iconName, color) {
const colors = {
green: "bg-emerald-50 border-emerald-100 text-emerald-700",
orange: "bg-brand-50 border-brand-100 text-brand-700",
red: "bg-red-50 border-red-100 text-red-700",
purple: "bg-purple-50 border-purple-100 text-purple-700"
};
return `
<button onclick="openTransactionModal('${type}')" class="w-full flex items-center gap-3 p-3 rounded-xl border ${colors[color]} mb-3 text-left hover:shadow-sm transition">
<div class="w-10 h-10 rounded-full bg-white/70 flex items-center justify-center">${icon(iconName,20)}</div>
<div class="flex-1">
<div class="font-semibold text-sm">${type}</div>
<div class="text-xs opacity-70">${description}</div>
</div>
${icon("chevron-right",17)}
</button>
`;
}
function accountRow(account) {
let iconName = account.type === "Bank" ? "landmark" : account.type === "Digital" ? "qr-code" : "wallet-cards";
return `
<div class="flex items-center gap-3 py-3 border-b border-stone-100 last:border-0">
<div class="w-9 h-9 rounded-full bg-slate-50 flex items-center justify-center text-slate-600">${icon(iconName,17)}</div>
<div class="flex-1 text-sm">${escapeHTML(account.name)}</div>
<div class="text-sm font-semibold">${rupiah(account.balance)}</div>
</div>
`;
}
function calculateBestSeller() {
const result = {};
transactions.filter(t => t.type === "Penjualan").forEach(t => {
(t.items || []).forEach(item => {
if (!result[item.name]) {
result[item.name] = { name: item.name, qty: 0, total: 0 };
}
result[item.name].qty += Number(item.qty);
result[item.name].total += Number(item.qty) * Number(item.price);
});
});
return Object.values(result).sort((a,b) => b.qty - a.qty);
}
function renderBestSeller() {
let data = calculateBestSeller();
if (!data.length) {
data = menus.slice(0,5).map((m,i) => ({
name: m.name,
qty: [28,18,12,10,8][i] || 3,
total: m.price * ([28,18,12,10,8][i] || 3)
}));
}
return data.slice(0,5).map((item,index) => {
const menu = menus.find(m => m.name === item.name);
return `
<div class="flex items-center gap-3 py-3 border-b border-stone-100 last:border-0">
<img src="${menu?.image || ''}" class="w-12 h-12 rounded-xl object-cover bg-stone-100" onerror="this.style.display='none'">
<div class="flex-1">
<div class="font-semibold text-sm">${escapeHTML(item.name)}</div>
<div class="text-xs text-slate-500">${item.qty} terjual</div>
</div>
${index === 0 ? `<span class="hidden sm:block text-xs text-brand-600 font-semibold">Best Seller</span>` : ""}
<div class="font-bold text-sm">${rupiah(item.total)}</div>
</div>
`;
}).join("");
}
function transactionIcon(type) {
const map = {
"Pemasukan": ["coins","green"],
"Penjualan": ["soup","orange"],
"Pengeluaran Usaha": ["shopping-basket","red"],
"Pengeluaran Pribadi": ["wallet","purple"]
};
return map[type] || ["receipt","blue"];
}
function renderRecentTransactions() {
return transactions.slice().sort((a,b) => new Date(b.date) - new Date(a.date)).slice(0,5).map(t => {
const [iconName,color] = transactionIcon(t.type);
const positive = t.type === "Pemasukan" || t.type === "Penjualan";
return `
<div class="flex items-center gap-3 py-3 border-b border-stone-100 last:border-0">
<div class="food-icon ${color}" style="width:38px;height:38px">${icon(iconName,18)}</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold truncate">
${escapeHTML(t.type)}
<span class="font-normal text-slate-500">- ${escapeHTML(t.note)}</span>
</div>
<div class="text-xs text-slate-400">${formatDateTime(t.date)}</div>
</div>
<div class="text-right">
<div class="font-semibold text-sm ${positive ? 'text-emerald-600' : 'text-red-500'}">
${positive ? '+' : '-'} ${rupiah(t.amount)}
</div>
<div class="text-[10px] text-emerald-600">Berhasil</div>
</div>
</div>
`;
}).join("");
}
/* =========================================================
POS PAGE
========================================================= */
function posPage() {
const filtered = currentPOSCategory === "Semua" ? menus : menus.filter(m => m.category === currentPOSCategory);
const cartCount = cart.reduce((sum,item) => sum + item.qty, 0);
const cartTotal = cart.reduce((sum,item) => sum + item.qty * item.price, 0);
return `
<div class="max-w-[1500px] mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5">
<div>
<h1 class="text-2xl font-bold">Kasir (POS)</h1>
<p class="text-sm text-slate-500 mt-1">Pilih menu untuk membuat pesanan baru.</p>
</div>
<div class="relative">
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">${icon("search",17)}</div>
<input id="menuSearch" oninput="searchPOS()" placeholder="Cari menu..." class="pl-9 pr-4 py-3 w-full sm:w-64 border border-stone-200 rounded-xl outline-none focus:ring-2 focus:ring-brand-200 text-sm">
</div>
</div>
<div class="flex gap-2 overflow-x-auto scrollbar-hide mb-5">
${["Semua","Makanan","Minuman","Extra"].map(cat => `
<button onclick="setPOSCategory('${cat}')" class="px-5 py-2.5 rounded-xl text-sm font-semibold whitespace-nowrap ${currentPOSCategory === cat ? 'tab-active' : 'bg-white border border-stone-200 text-slate-500'}">
${cat}
</button>
`).join("")}
</div>
<div id="posMenuGrid" class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4">
${filtered.map(menuCard).join("")}
</div>
${cartCount > 0 ? `
<button onclick="openCart()" class="floating-cart fixed bottom-5 left-4 right-4 sm:left-auto sm:right-6 sm:w-[370px] z-[70] bg-slate-900 text-white rounded-2xl shadow-2xl p-4 flex items-center gap-3">
<div class="w-11 h-11 rounded-xl bg-brand-500 flex items-center justify-center">${icon("shopping-bag",21)}</div>
<div class="flex-1 text-left">
<div class="font-bold">${cartCount} item</div>
<div class="text-xs text-slate-300">Tap untuk checkout</div>
</div>
<div class="font-bold">${rupiah(cartTotal)}</div>
${icon("chevron-right",18)}
</button>
` : ""}
</div>
`;
}
function menuCard(menu) {
const item = cart.find(c => c.id === menu.id);
const qty = item?.qty || 0;
return `
<div class="menu-card card overflow-hidden" data-menu-name="${escapeHTML(menu.name.toLowerCase())}">
<div class="relative">
<img src="${menu.image}" class="menu-img" onerror="this.src='https://placehold.co/600x400/fff0dd/dc6410?text=Menu'">
<span class="absolute top-2 left-2 bg-white/90 backdrop-blur text-xs font-semibold px-2.5 py-1 rounded-full">${menu.category}</span>
</div>
<div class="p-4">
<div class="font-bold text-sm min-h-[40px]">${escapeHTML(menu.name)}</div>
<div class="text-brand-600 font-extrabold mt-1">${rupiah(menu.price)}</div>
<div class="mt-3">
${qty === 0 ? `
<button onclick="addToCart(${menu.id})" class="w-full bg-brand-50 hover:bg-brand-100 text-brand-700 py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2">
${icon("plus",16)} Tambah
</button>
` : `
<div class="flex items-center justify-between bg-brand-50 rounded-xl p-1">
<button onclick="changeQty(${menu.id}, -1)" class="qty-btn">${icon("minus",15)}</button>
<span class="font-bold">${qty}</span>
<button onclick="changeQty(${menu.id}, 1)" class="qty-btn">${icon("plus",15)}</button>
</div>
`}
</div>
</div>
</div>
`;
}
function setPOSCategory(category) {
currentPOSCategory = category;
render();
}
function searchPOS() {
const keyword = document.getElementById("menuSearch")?.value.toLowerCase() || "";
document.querySelectorAll("#posMenuGrid > div").forEach(card => {
const name = card.dataset.menuName || "";
card.style.display = name.includes(keyword) ? "" : "none";
});
}
function addToCart(id) {
const menu = menus.find(m => m.id === id);
if (!menu) return;
const existing = cart.find(item => item.id === id);
if (existing) {
existing.qty++;
} else {
cart.push({ ...menu, qty: 1 });
}
render();
}
function changeQty(id, amount) {
const item = cart.find(i => i.id === id);
if (!item) return;
item.qty += amount;
if (item.qty <= 0) {
cart = cart.filter(i => i.id !== id);
}
render();
}
function openCart() {
const total = cart.reduce((sum,item) => sum + item.qty * item.price, 0);
const count = cart.reduce((sum,item) => sum + item.qty, 0);
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-end sm:items-center justify-center">
<div class="modal-container bg-white w-full sm:max-w-lg max-h-[92vh] overflow-y-auto rounded-t-3xl sm:rounded-2xl">
<div class="p-5 border-b border-stone-100 flex items-center justify-between">
<div>
<h2 class="text-xl font-bold">Pesanan Aktif</h2>
<p class="text-sm text-slate-500">${count} item dipilih</p>
</div>
<button onclick="closeModal()" class="p-2 rounded-xl hover:bg-stone-100">${icon("x",20)}</button>
</div>
<div class="p-5">
${cart.map(cartRow).join("")}
<div class="border-t border-dashed border-stone-300 mt-4 pt-4">
<div class="flex justify-between text-sm">
<span class="text-slate-500">Total</span>
<span class="text-2xl font-extrabold">${rupiah(total)}</span>
</div>
</div>
<button onclick="openCheckout()" class="w-full mt-5 bg-brand-500 hover:bg-brand-600 text-white py-3.5 rounded-xl font-bold">
Lanjut Checkout
</button>
</div>
</div>
</div>
`;
lucide.createIcons();
}
function cartRow(item) {
return `
<div class="flex items-center gap-3 py-3 border-b border-stone-100">
<img src="${item.image}" class="w-14 h-14 rounded-xl object-cover">
<div class="flex-1">
<div class="font-semibold text-sm">${escapeHTML(item.name)}</div>
<div class="text-xs text-slate-500">${rupiah(item.price)}</div>
</div>
<div class="qty-control">
<button onclick="changeQtyInModal(${item.id},-1)" class="qty-btn">${icon("minus",14)}</button>
<span class="font-bold w-5 text-center">${item.qty}</span>
<button onclick="changeQtyInModal(${item.id},1)" class="qty-btn">${icon("plus",14)}</button>
</div>
</div>
`;
}
function changeQtyInModal(id, amount) {
changeQty(id, amount);
openCart();
}
function openCheckout() {
const total = cart.reduce((sum,item) => sum + item.qty * item.price, 0);
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-end sm:items-center justify-center">
<div class="modal-container bg-white w-full sm:max-w-lg max-h-[94vh] overflow-y-auto rounded-t-3xl sm:rounded-2xl">
<div class="p-5 border-b border-stone-100 flex items-center gap-3">
<button onclick="openCart()" class="p-2 rounded-xl hover:bg-stone-100">${icon("arrow-left",19)}</button>
<div>
<h2 class="text-xl font-bold">Checkout</h2>
<p class="text-sm text-slate-500">Selesaikan pembayaran</p>
</div>
</div>
<form onsubmit="completeSale(event)" class="p-5 space-y-4">
<div>
<label class="block text-sm font-semibold mb-1">Nama Pelanggan</label>
<input id="customerName" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200" placeholder="Contoh: Budi">
</div>
<div>
<label class="block text-sm font-semibold mb-1">Nomor WhatsApp</label>
<input id="customerWA" type="tel" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200" placeholder="08xxxxxxxxxx">
</div>
<div>
<label class="block text-sm font-semibold mb-1">Pembayaran ke Rekening</label>
<select id="paymentAccount" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200">
${accounts.map(a => `<option value="${escapeHTML(a.name)}">${escapeHTML(a.name)}</option>`).join("")}
</select>
</div>
<div>
<label class="block text-sm font-semibold mb-1">Uang Diterima</label>
<div class="relative">
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-semibold">Rp</span>
<input id="checkoutAmountReceived" required inputmode="numeric" oninput="formatAmountInput(this); updateCheckoutChange()" class="w-full border border-stone-200 rounded-xl pl-12 pr-4 py-3 text-lg font-bold outline-none focus:ring-2 focus:ring-brand-200" placeholder="0">
</div>
</div>
<div class="bg-brand-50 rounded-2xl p-4">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">Total Pembayaran</span>
<span class="font-extrabold text-xl text-brand-700">${rupiah(total)}</span>
</div>
<div class="flex justify-between items-center mt-3 pt-3 border-t border-brand-100">
<span class="text-sm font-medium">Kembalian</span>
<span id="checkoutChange" class="font-bold text-sm text-slate-500">Masukkan uang diterima</span>
</div>
</div>
<button class="w-full bg-brand-500 hover:bg-brand-600 text-white py-3.5 rounded-xl font-bold transition">
Selesaikan Pembayaran
</button>
</form>
</div>
</div>
`;
lucide.createIcons();
}
function updateCheckoutChange() {
const total = cart.reduce((sum, item) => sum + item.qty * item.price, 0);
const input = document.getElementById("checkoutAmountReceived");
const display = document.getElementById("checkoutChange");
if (!input || !display) return;
const received = Number(input.value.replace(/\./g, "")) || 0;
if (!received) {
display.textContent = "Masukkan uang diterima";
display.className = "font-bold text-sm text-slate-500";
} else if (received < total) {
display.textContent = `Kurang ${rupiah(total - received)}`;
display.className = "font-bold text-sm text-red-600";
} else {
display.textContent = rupiah(received - total);
display.className = "font-bold text-sm text-emerald-700";
}
}
function completeSale(event) {
event.preventDefault();
if (!cart.length) {
showToast("Keranjang masih kosong", "error");
return;
}
const customer = document.getElementById("customerName").value.trim();
const wa = document.getElementById("customerWA").value.trim();
const account = document.getElementById("paymentAccount").value;
const total = cart.reduce((sum,item) => sum + item.qty * item.price, 0);
const received = Number(document.getElementById("checkoutAmountReceived").value.replace(/\./g, "")) || 0;
if (received < total) {
showToast("Uang diterima kurang dari total pembayaran", "error");
return;
}
const change = received - total;
const itemText = cart.map(item => `${item.name} x${item.qty}`).join(", ");
const transaction = {
id: Date.now(),
type: "Penjualan",
amount: total,
amountReceived: received,
change,
account,
note: `Pemasukan dari penjualan "${itemText}"`,
customer,
whatsapp: wa,
items: cart.map(item => ({ name: item.name, qty: item.qty, price: item.price })),
date: new Date().toISOString(),
status: "Berhasil"
};
transactions.unshift(transaction);
const acc = accounts.find(a => a.name === account);
if (acc) {
acc.balance = Number(acc.balance) + total;
}
saveData();
cart = [];
closeModal();
showToast("Transaksi penjualan berhasil disimpan");
render();
setTimeout(() => { openReceipt(transaction); }, 250);
}
/* =========================================================
TRANSACTION MODAL (ADD & EDIT KHUSUS OWNER)
========================================================= */
function openTransactionModal(type = "Pemasukan") {
if (currentUserRole !== "Owner") { showToast("Akses transaksi manual hanya untuk Owner.", "error"); return; }
const types = [
{ name: "Pemasukan", icon: "coins", color: "green" },
{ name: "Pengeluaran Usaha", icon: "shopping-basket", color: "red" },
{ name: "Pengeluaran Pribadi", icon: "wallet", color: "purple" }
];
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-end sm:items-center justify-center">
<div class="modal-container bg-white w-full sm:max-w-xl max-h-[94vh] overflow-y-auto rounded-t-3xl sm:rounded-2xl">
<div class="p-5 border-b border-stone-100 flex items-center justify-between">
<div>
<h2 class="text-xl font-bold">Tambah Transaksi</h2>
<p class="text-sm text-slate-500">Catat arus kas bisnis</p>
</div>
<button onclick="closeModal()" class="p-2 rounded-xl hover:bg-stone-100">${icon("x",20)}</button>
</div>
<form onsubmit="saveManualTransaction(event)" class="p-5 space-y-4">
<div>
<label class="block text-sm font-semibold mb-2">Jenis Transaksi</label>
<div class="grid sm:grid-cols-3 gap-2">
${types.map(t => `
<button type="button" onclick="selectTransactionType('${t.name}')" id="type-${t.name.replaceAll(' ','-')}" class="transaction-type-btn p-3 rounded-xl border text-left ${t.name === type ? 'border-brand-300 bg-brand-50' : 'border-stone-200'}">
<div class="flex items-center gap-2">
<div class="food-icon ${t.color}" style="width:32px;height:32px">${icon(t.icon,15)}</div>
<span class="font-semibold text-xs">${t.name}</span>
</div>
</button>
`).join("")}
</div>
<input type="hidden" id="transactionType" value="${type}">
</div>
<div>
<label class="block text-sm font-semibold mb-1">Nominal</label>
<div class="relative">
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-semibold">Rp</span>
<input id="transactionAmount" required inputmode="numeric" oninput="formatAmountInput(this)" class="w-full border border-stone-200 rounded-xl pl-12 pr-4 py-3 text-lg font-bold outline-none focus:ring-2 focus:ring-brand-200" placeholder="0">
</div>
</div>
<div>
<label class="block text-sm font-semibold mb-1">Sumber Saldo / Rekening</label>
<select id="transactionAccount" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200">
${accounts.map(a => `<option value="${escapeHTML(a.name)}">${escapeHTML(a.name)}</option>`).join("")}
</select>
</div>
<div>
<label class="block text-sm font-semibold mb-1">Keterangan</label>
<textarea id="transactionNote" required rows="2" class="w-full border border-stone-200 rounded-xl px-4 py-3 resize-none outline-none focus:ring-2 focus:ring-brand-200" placeholder="Modal tambahan, beli bahan baku, bensin..."></textarea>
</div>
<button type="submit" class="w-full bg-brand-500 hover:bg-brand-600 text-white py-3.5 rounded-xl font-bold transition">
Simpan Transaksi
</button>
</form>
</div>
</div>
`;
lucide.createIcons();
}
function selectTransactionType(type) {
document.getElementById("transactionType").value = type;
document.querySelectorAll(".transaction-type-btn").forEach(btn => {
btn.classList.remove("border-brand-300", "bg-brand-50");
btn.classList.add("border-stone-200");
});
const selected = document.getElementById("type-" + type.replaceAll(" ", "-"));
if (selected) {
selected.classList.remove("border-stone-200");
selected.classList.add("border-brand-300", "bg-brand-50");
}
}
function formatAmountInput(input) {
let value = input.value.replace(/\D/g,"");
if (!value) { input.value = ""; return; }
input.value = Number(value).toLocaleString("id-ID");
}
function saveManualTransaction(event) {
event.preventDefault();
const type = document.getElementById("transactionType").value;
const amountText = document.getElementById("transactionAmount").value;
const amount = Number(amountText.replace(/\./g,""));
const accountName = document.getElementById("transactionAccount").value;
const note = document.getElementById("transactionNote").value.trim();
if (!amount || amount <= 0) { showToast("Nominal belum diisi", "error"); return; }
if (!note) { showToast("Keterangan wajib diisi", "error"); return; }
const transaction = {
id: Date.now(),
type,
amount,
account: accountName,
note,
date: new Date().toISOString(),
status: "Berhasil"
};
transactions.unshift(transaction);
const account = accounts.find(a => a.name === accountName);
if (account) {
if (type === "Pemasukan") {
account.balance = Number(account.balance) + amount;
} else {
account.balance = Number(account.balance) - amount;
}
}
saveData();
closeModal();
showToast(`${type} berhasil disimpan`);
render();
}
/* =========================================================
FITUR EDIT RIWAYAT TRANSAKSI (KHUSUS OWNER)
========================================================= */
function openEditTransactionModal(id) {
if (currentUserRole !== "Owner") {
showToast("Hanya akun Owner yang dapat mengedit transaksi!", "error");
return;
}
const t = transactions.find(item => item.id === id);
if (!t) {
showToast("Transaksi tidak ditemukan", "error");
return;
}
const isoDateStr = new Date(t.date).toISOString().slice(0, 16);
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-end sm:items-center justify-center">
<div class="modal-container bg-white w-full sm:max-w-xl max-h-[94vh] overflow-y-auto rounded-t-3xl sm:rounded-2xl">
<div class="p-5 border-b border-stone-100 flex items-center justify-between bg-amber-50/50">
<div class="flex items-center gap-2">
<div class="w-9 h-9 rounded-xl bg-amber-100 text-amber-800 flex items-center justify-center font-bold">
${icon("pencil", 18)}
</div>
<div>
<h2 class="text-lg font-bold text-slate-900">Edit Riwayat Transaksi (Owner)</h2>
<p class="text-xs text-slate-500">ID Transaksi: #${t.id}</p>
</div>
</div>
<button onclick="closeModal()" class="p-2 rounded-xl hover:bg-stone-100">${icon("x",20)}</button>
</div>
<form onsubmit="updateTransaction(event, ${t.id})" class="p-5 space-y-4">
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Jenis Transaksi</label>
<select id="editType" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm font-semibold outline-none focus:ring-2 focus:ring-brand-200">
<option value="Penjualan" ${t.type === "Penjualan" ? "selected" : ""}>Penjualan</option>
<option value="Pemasukan" ${t.type === "Pemasukan" ? "selected" : ""}>Pemasukan</option>
<option value="Pengeluaran Usaha" ${t.type === "Pengeluaran Usaha" ? "selected" : ""}>Pengeluaran Usaha</option>
<option value="Pengeluaran Pribadi" ${t.type === "Pengeluaran Pribadi" ? "selected" : ""}>Pengeluaran Pribadi</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Nominal (Rp)</label>
<input id="editAmount" required inputmode="numeric" oninput="formatAmountInput(this)" value="${numberFormat(t.amount)}" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-lg font-bold outline-none focus:ring-2 focus:ring-brand-200">
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Rekening / Sumber Saldo</label>
<select id="editAccount" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-brand-200">
${accounts.map(a => `<option value="${escapeHTML(a.name)}" ${a.name === t.account ? "selected" : ""}>${escapeHTML(a.name)}</option>`).join("")}
</select>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Waktu / Tanggal Transaksi</label>
<input id="editDate" type="datetime-local" value="${isoDateStr}" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-brand-200">
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Catatan / Keterangan</label>
<textarea id="editNote" required rows="3" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-brand-200 resize-none">${escapeHTML(t.note)}</textarea>
</div>
<div class="bg-amber-50 border border-amber-200 rounded-xl p-3 text-xs text-amber-800 flex items-start gap-2">
${icon("info", 16)}
<span>Mengedit transaksi akan otomatis memperbarui saldo pada rekening yang bersangkutan.</span>
</div>
<div class="pt-2 flex gap-3">
<button type="button" onclick="closeModal()" class="flex-1 border border-stone-200 hover:bg-stone-50 py-3 rounded-xl font-semibold text-sm">
Batal
</button>
<button type="submit" class="flex-1 bg-brand-500 hover:bg-brand-600 text-white py-3 rounded-xl font-bold text-sm transition">
Simpan Perubahan
</button>
</div>
</form>
</div>
</div>
`;
lucide.createIcons();
}
function updateTransaction(event, id) {
event.preventDefault();
const index = transactions.findIndex(t => t.id === id);
if (index === -1) return;
const oldTrans = transactions[index];
const newType = document.getElementById("editType").value;
const newAmount = Number(document.getElementById("editAmount").value.replace(/\./g, ""));
const newAccountName = document.getElementById("editAccount").value;
const newDate = document.getElementById("editDate").value ? new Date(document.getElementById("editDate").value).toISOString() : oldTrans.date;
const newNote = document.getElementById("editNote").value.trim();
if (!newAmount || newAmount <= 0) {
showToast("Nominal transaksi harus lebih dari 0", "error");
return;
}
// 1. REVERT OLD TRANSACTION IMPACT ON ACCOUNT BALANCE
const oldAcc = accounts.find(a => a.name === oldTrans.account);
if (oldAcc) {
if (oldTrans.type === "Pemasukan" || oldTrans.type === "Penjualan") {
oldAcc.balance -= Number(oldTrans.amount);
} else {
oldAcc.balance += Number(oldTrans.amount);
}
}
// 2. APPLY NEW TRANSACTION IMPACT ON ACCOUNT BALANCE
const newAcc = accounts.find(a => a.name === newAccountName);
if (newAcc) {
if (newType === "Pemasukan" || newType === "Penjualan") {
newAcc.balance += newAmount;
} else {
newAcc.balance -= newAmount;
}
}
// 3. UPDATE TRANSACTION OBJECT
const updatedTransaction = {
...oldTrans,
type: newType,
amount: newAmount,
account: newAccountName,
date: newDate,
note: newNote
};
transactions[index] = updatedTransaction;
saveData();
closeModal();
showToast("Transaksi berhasil diperbarui & saldo telah disesuaikan!");
render();
}
/* =========================================================
HISTORY PAGE
========================================================= */
function historyPage() {
return `
<div class="max-w-[1500px] mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5">
<div>
<h1 class="text-2xl font-bold">Riwayat Transaksi</h1>
<p class="text-sm text-slate-500">Semua transaksi bisnis terdaftar.</p>
</div>
<div class="flex items-center gap-2">
<button onclick="openTransactionModal()" class="bg-brand-500 text-white px-4 py-3 rounded-xl font-semibold flex items-center justify-center gap-2 text-sm">
${icon("plus",17)}
Tambah Transaksi
</button>
</div>
</div>
<div class="card overflow-hidden">
<div class="p-4 border-b border-stone-100 flex flex-col md:flex-row gap-3">
<input id="historySearch" oninput="filterHistory()" placeholder="Cari transaksi..." class="flex-1 border border-stone-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-brand-200">
<select id="historyType" onchange="filterHistory()" class="border border-stone-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-brand-200">
<option value="">Semua Jenis</option>
<option>Penjualan</option>
<option>Pemasukan</option>
<option>Pengeluaran Usaha</option>
<option>Pengeluaran Pribadi</option>
</select>
</div>
<div id="historyTable">
${historyRows(transactions)}
</div>
</div>
</div>
`;
}
function historyRows(data) {
if (!data.length) {
return `
<div class="p-12 text-center text-slate-400">
${icon("receipt",35)}
<div class="mt-3 text-sm">Belum ada transaksi.</div>
</div>
`;
}
return `
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-stone-50 text-slate-500 border-b border-stone-100">
<tr>
<th class="text-left px-5 py-3 font-semibold">Transaksi</th>
<th class="text-left px-5 py-3 font-semibold">Rekening</th>
<th class="text-right px-5 py-3 font-semibold">Nominal</th>
<th class="text-left px-5 py-3 font-semibold">Waktu</th>
<th class="text-right px-5 py-3 font-semibold">Aksi (Owner)</th>
</tr>
</thead>
<tbody>
${data.map(t => {
const positive = t.type === "Pemasukan" || t.type === "Penjualan";
const [ic,col] = transactionIcon(t.type);
return `
<tr class="border-t border-stone-100 hover:bg-stone-50/80 transition">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="food-icon ${col}" style="width:38px;height:38px">
${icon(ic,18)}
</div>
<div>
<div class="font-semibold text-slate-900">${escapeHTML(t.type)}</div>
<div class="text-xs text-slate-500 max-w-[320px] truncate">${escapeHTML(t.note)}</div>
</div>
</div>
</td>
<td class="px-5 py-4 text-slate-600 font-medium">${escapeHTML(t.account || "-")}</td>
<td class="px-5 py-4 text-right font-bold ${positive ? 'text-emerald-600' : 'text-red-500'}">
${positive ? "+" : "-"} ${rupiah(t.amount)}
</td>
<td class="px-5 py-4 text-slate-500 whitespace-nowrap text-xs">${formatDateTime(t.date)}</td>
<td class="px-5 py-4 text-right">
<div class="flex items-center justify-end gap-1">
<button onclick="openEditTransactionModal(${t.id})" title="Edit Transaksi (Owner)" class="p-2 rounded-lg text-amber-600 hover:bg-amber-50 transition">
${icon("pencil",16)}
</button>
<button onclick="deleteTransaction(${t.id})" title="Hapus Transaksi" class="p-2 rounded-lg text-red-500 hover:bg-red-50 transition">
${icon("trash-2",16)}
</button>
</div>
</td>
</tr>
`;
}).join("")}
</tbody>
</table>
</div>
`;
}
function filterHistory() {
const keyword = document.getElementById("historySearch")?.value.toLowerCase() || "";
const type = document.getElementById("historyType")?.value || "";
const filtered = transactions.filter(t => {
const text = `${t.type} ${t.note} ${t.account}`.toLowerCase();
return text.includes(keyword) && (!type || t.type === type);
});
document.getElementById("historyTable").innerHTML = historyRows(filtered);
lucide.createIcons();
}
function deleteTransaction(id) {
if (!confirm("Hapus transaksi ini? Saldo rekening akan otomatis disesuaikan.")) return;
const index = transactions.findIndex(t => t.id === id);
if (index === -1) return;
const t = transactions[index];
const account = accounts.find(a => a.name === t.account);
if (account) {
if (t.type === "Pemasukan" || t.type === "Penjualan") {
account.balance -= Number(t.amount);
} else {
account.balance += Number(t.amount);
}
}
transactions.splice(index,1);
saveData();
showToast("Transaksi berhasil dihapus");
render();
}
/* =========================================================
OTHER PAGES (ACCOUNTS, MENUS, REPORTS, FINANCE, USERS, BRAND)
========================================================= */
function accountsPage() {
return `
<div class="max-w-[1200px] mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5">
<div>
<h1 class="text-2xl font-bold">Rekening & Saldo</h1>
<p class="text-sm text-slate-500 mt-1">Kelola sumber saldo usaha.</p>
</div>
<button onclick="openAccountModal()" class="bg-brand-500 text-white px-4 py-3 rounded-xl font-semibold flex items-center justify-center gap-2">
${icon("plus",17)} Tambah Rekening
</button>
</div>
<div class="card bg-gradient-to-r from-[#fff7eb] to-[#ffecd3] p-6 mb-5">
<div class="text-sm text-brand-800">Total Saldo Tergabung</div>
<div class="text-3xl font-extrabold mt-1">${rupiah(getTotalBalance())}</div>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
${accounts.map(account => `
<div class="card p-5">
<div class="flex items-center justify-between">
<div class="food-icon blue">
${icon(account.type === "Bank" ? "landmark" : account.type === "Digital" ? "qr-code" : "wallet", 21)}
</div>
<div class="flex gap-1">
<button onclick="openAccountModal(${account.id})" class="p-2 rounded-lg hover:bg-stone-100">${icon("pencil",15)}</button>
<button onclick="deleteAccount(${account.id})" class="p-2 rounded-lg hover:bg-red-50 text-red-500">${icon("trash-2",15)}</button>
</div>
</div>
<div class="mt-5">
<div class="text-xs text-slate-500 uppercase tracking-wider font-semibold">${escapeHTML(account.type)}</div>
<div class="font-bold text-lg text-slate-900">${escapeHTML(account.name)}</div>
<div class="text-2xl font-extrabold mt-2 text-brand-700">${rupiah(account.balance)}</div>
</div>
</div>
`).join("")}
</div>
</div>
`;
}
function openAccountModal(id = null) {
const account = accounts.find(a => a.id === id);
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-end sm:items-center justify-center">
<div class="modal-container bg-white w-full sm:max-w-md rounded-t-3xl sm:rounded-2xl">
<div class="p-5 border-b border-stone-100 flex justify-between items-center">
<h2 class="text-xl font-bold">${account ? "Edit Rekening" : "Tambah Rekening"}</h2>
<button onclick="closeModal()">${icon("x",20)}</button>
</div>
<form onsubmit="saveAccount(event, ${id || "null"})" class="p-5 space-y-4">
<div>
<label class="block text-sm font-semibold mb-1">Nama Rekening</label>
<input id="accountName" required value="${escapeHTML(account?.name || "")}" placeholder="Contoh: BCA / Kas" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-brand-200">
</div>
<div>
<label class="block text-sm font-semibold mb-1">Tipe Rekening</label>
<select id="accountType" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-brand-200">
${["Cash","Bank","Digital"].map(type => `
<option ${account?.type === type ? "selected" : ""}>${type}</option>
`).join("")}
</select>
</div>
<div>
<label class="block text-sm font-semibold mb-1">Saldo Awal (Rp)</label>
<input id="accountBalance" required inputmode="numeric" oninput="formatAmountInput(this)" value="${account ? numberFormat(account.balance) : ""}" placeholder="0" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-brand-200">
</div>
<button class="w-full bg-brand-500 hover:bg-brand-600 text-white py-3.5 rounded-xl font-bold transition">
Simpan Rekening
</button>
</form>
</div>
</div>
`;
lucide.createIcons();
}
function saveAccount(event, id) {
event.preventDefault();
const name = document.getElementById("accountName").value.trim();
const type = document.getElementById("accountType").value;
const balance = Number(document.getElementById("accountBalance").value.replace(/\./g,""));
if (id) {
const account = accounts.find(a => a.id === id);
account.name = name;
account.type = type;
account.balance = balance;
} else {
accounts.push({ id: Date.now(), name, type, balance });
}
saveData();
closeModal();
showToast("Rekening berhasil disimpan");
render();
}
function deleteAccount(id) {
if (accounts.length <= 1) { showToast("Minimal harus ada satu rekening!", "error"); return; }
if (!confirm("Hapus rekening ini?")) return;
accounts = accounts.filter(a => a.id !== id);
saveData();
showToast("Rekening berhasil dihapus");
render();
}
function menusPage() {
return `
<div class="max-w-[1400px] mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5">
<div>
<h1 class="text-2xl font-bold">Manajemen Menu</h1>
<p class="text-sm text-slate-500">Kelola menu makanan, minuman, dan harga.</p>
</div>
<button onclick="openMenuModal()" class="bg-brand-500 text-white px-4 py-3 rounded-xl font-semibold flex items-center justify-center gap-2">
${icon("plus",17)} Tambah Menu
</button>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4">
${menus.map(menu => `
<div class="card overflow-hidden">
<img src="${menu.image}" class="w-full h-40 object-cover" onerror="this.src='https://placehold.co/600x400/fff0dd/dc6410?text=Menu'">
<div class="p-4">
<span class="text-xs bg-brand-50 text-brand-700 px-2.5 py-1 rounded-full font-semibold">${menu.category}</span>
<div class="font-bold mt-2 text-slate-900">${escapeHTML(menu.name)}</div>
<div class="text-brand-600 font-extrabold mt-1">${rupiah(menu.price)}</div>
<div class="flex gap-2 mt-4">
<button onclick="openMenuModal(${menu.id})" class="flex-1 border border-stone-200 rounded-xl py-2 text-sm font-semibold hover:bg-stone-50">Edit</button>
<button onclick="deleteMenu(${menu.id})" class="px-3 border border-red-100 text-red-500 rounded-xl hover:bg-red-50">${icon("trash-2",16)}</button>
</div>
</div>
</div>
`).join("")}
</div>
</div>
`;
}
function openMenuModal(id = null) {
const menu = menus.find(m => m.id === id);
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-end sm:items-center justify-center">
<div class="modal-container bg-white w-full sm:max-w-lg rounded-t-3xl sm:rounded-2xl">
<div class="p-5 border-b border-stone-100 flex justify-between items-center">
<h2 class="text-xl font-bold">${menu ? "Edit Menu" : "Tambah Menu"}</h2>
<button onclick="closeModal()">${icon("x",20)}</button>
</div>
<form onsubmit="saveMenu(event,${id || "null"})" class="p-5 space-y-4">
<div>
<label class="block text-sm font-semibold mb-1">Nama Menu</label>
<input id="menuName" required value="${escapeHTML(menu?.name || "")}" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-brand-200">
</div>
<div>
<label class="block text-sm font-semibold mb-1">Kategori</label>
<select id="menuCategory" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-brand-200">
${["Makanan","Minuman","Extra"].map(c => `
<option ${menu?.category === c ? "selected" : ""}>${c}</option>
`).join("")}
</select>
</div>
<div>
<label class="block text-sm font-semibold mb-1">Harga (Rp)</label>
<input id="menuPrice" required inputmode="numeric" oninput="formatAmountInput(this)" value="${menu ? numberFormat(menu.price) : ""}" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-brand-200">
</div>
<div>
<label class="block text-sm font-semibold mb-1">URL Foto Menu</label>
<input id="menuImage" value="${escapeHTML(menu?.image || "")}" placeholder="https://..." class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-brand-200">
</div>
<button class="w-full bg-brand-500 hover:bg-brand-600 text-white py-3.5 rounded-xl font-bold transition">
Simpan Menu
</button>
</form>
</div>
</div>
`;
lucide.createIcons();
}
function saveMenu(event, id) {
event.preventDefault();
const name = document.getElementById("menuName").value.trim();
const category = document.getElementById("menuCategory").value;
const price = Number(document.getElementById("menuPrice").value.replace(/\./g,""));
const image = document.getElementById("menuImage").value.trim();
if (id) {
const menu = menus.find(m => m.id === id);
menu.name = name;
menu.category = category;
menu.price = price;
menu.image = image;
} else {
menus.push({
id: Date.now(),
name,
category,
price,
image: image || "https://placehold.co/600x400/fff0dd/dc6410?text=Menu"
});
}
saveData();
closeModal();
showToast("Menu berhasil disimpan");
render();
}
function deleteMenu(id) {
if (!confirm("Hapus menu ini?")) return;
menus = menus.filter(m => m.id !== id);
saveData();
showToast("Menu berhasil dihapus");
render();
}
function reportsPage() {
const sales = totalTransaction("Penjualan");
const income = totalTransaction("Pemasukan");
const business = totalTransaction("Pengeluaran Usaha");
const personal = totalTransaction("Pengeluaran Pribadi");
const profit = sales + income - business - personal;
return `
<div class="max-w-[1400px] mx-auto">
<div class="mb-5">
<h1 class="text-2xl font-bold">Laporan & Analisis</h1>
<p class="text-sm text-slate-500 mt-1">Analisis penjualan dan performa keuangan bisnis.</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-5">
${reportCard("Total Omzet", sales + income, "emerald")}
${reportCard("Pengeluaran Usaha", business, "red")}
${reportCard("Pengeluaran Pribadi", personal, "purple")}
${reportCard("Keuntungan Bersih", profit, "green")}
</div>
<div class="grid lg:grid-cols-2 gap-5">
<div class="card p-5">
<h2 class="font-bold text-lg mb-4">Penjualan Menu</h2>
${renderBestSeller()}
</div>
<div class="card p-5">
<h2 class="font-bold text-lg mb-4">Ringkasan Keuangan</h2>
<div class="space-y-4">
${financeLine("Penjualan", sales, "emerald")}
${financeLine("Pemasukan Lain", income, "emerald")}
${financeLine("Pengeluaran Usaha", business, "red")}
${financeLine("Pengeluaran Pribadi", personal, "purple")}
<div class="border-t pt-4 flex justify-between">
<span class="font-bold">Net Profit</span>
<span class="font-extrabold text-emerald-600">${rupiah(profit)}</span>
</div>
</div>
</div>
</div>
</div>
`;
}
function reportCard(title, value, color) {
return `
<div class="card p-5">
<div class="text-sm text-slate-500">${title}</div>
<div class="text-2xl font-extrabold mt-2">${rupiah(value)}</div>
<div class="text-xs mt-3 text-${color}-600 font-semibold">Berdasarkan transaksi tersimpan</div>
</div>
`;
}
function financeLine(name, value, color) {
return `
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-${color}-500"></span>
<span class="text-sm">${name}</span>
</div>
<span class="font-bold text-sm">${rupiah(value)}</span>
</div>
`;
}
function financePage() {
return reportsPage();
}
function usersPage() {
if (currentUserRole !== "Owner") return `<div class="card p-6 text-center text-red-600">Hanya Owner yang dapat mengelola akun.</div>`;
return `
<div class="max-w-[1000px] mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5">
<div>
<h1 class="text-2xl font-bold">Manajemen Akun</h1>
<p class="text-sm text-slate-500">Tambah dan kelola akun Owner maupun Kasir.</p>
</div>
<button onclick="openUserModal()" class="bg-brand-500 hover:bg-brand-600 text-white px-4 py-3 rounded-xl font-semibold flex items-center gap-2">${icon("plus",17)} Tambah Akun</button>
</div>
<div class="card overflow-hidden divide-y divide-stone-100">
${users.map(user => `
<div class="p-4 sm:p-5 flex flex-wrap items-center gap-3 sm:gap-4">
<div class="w-11 h-11 rounded-full ${user.role === "Owner" ? "bg-brand-100 text-brand-700" : "bg-blue-50 text-blue-600"} flex items-center justify-center">${icon(user.role === "Owner" ? "shield-check" : "user-round",20)}</div>
<div class="flex-1 min-w-[150px]">
<div class="font-bold">${escapeHTML(user.name)} ${user.id === currentUserId ? '<span class="text-xs text-emerald-600">(Anda)</span>' : ""}</div>
<div class="text-xs text-slate-500">Username: ${escapeHTML(user.username)}</div>
</div>
<span class="px-3 py-1 rounded-full ${user.role === "Owner" ? "bg-brand-50 text-brand-700" : "bg-stone-100 text-slate-600"} text-xs font-bold">${escapeHTML(user.role)}</span>
<div class="flex gap-1">
<button onclick="openUserModal(${user.id})" title="Edit akun" class="p-2 rounded-lg text-amber-600 hover:bg-amber-50">${icon("pencil",16)}</button>
<button onclick="deleteUser(${user.id})" title="Hapus akun" class="p-2 rounded-lg text-red-500 hover:bg-red-50">${icon("trash-2",16)}</button>
</div>
</div>`).join("")}
</div>
<p class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-xl p-3 mt-4">Mode lokal/demo: data akun dan password disimpan di browser. Jangan gunakan password sensitif atau untuk keamanan produksi.</p>
</div>
`;
}
function openUserModal(id = null) {
if (currentUserRole !== "Owner") { showToast("Hanya Owner yang dapat mengelola akun.", "error"); return; }
const user = users.find(item => item.id === id);
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-end sm:items-center justify-center p-4">
<div class="modal-container bg-white w-full sm:max-w-lg rounded-t-3xl sm:rounded-2xl overflow-y-auto">
<div class="p-5 border-b border-stone-100 flex items-center justify-between"><h2 class="text-xl font-bold">${user ? "Edit Akun" : "Tambah Akun"}</h2><button onclick="closeModal()" class="p-2 rounded-xl hover:bg-stone-100">${icon("x",20)}</button></div>
<form onsubmit="saveUser(event, ${id || "null"})" class="p-5 space-y-4">
<div><label class="block text-sm font-semibold mb-1">Nama</label><input id="userName" required maxlength="80" value="${escapeHTML(user?.name || "")}" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200"></div>
<div><label class="block text-sm font-semibold mb-1">Username</label><input id="userUsername" required maxlength="40" autocomplete="off" value="${escapeHTML(user?.username || "")}" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200"></div>
<div><label class="block text-sm font-semibold mb-1">Password ${user ? "(opsional, kosongkan jika tidak diubah)" : ""}</label><input id="userPassword" type="password" ${user ? "" : "required"} minlength="4" autocomplete="new-password" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200" placeholder="${user ? "Biarkan kosong untuk mempertahankan password" : "Minimal 4 karakter"}"></div>
<div><label class="block text-sm font-semibold mb-1">Role</label><select id="userRole" class="w-full border border-stone-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-brand-200"><option value="Owner" ${user?.role === "Owner" ? "selected" : ""}>Owner</option><option value="Kasir" ${user?.role === "Kasir" ? "selected" : ""}>Kasir</option></select></div>
<button type="submit" class="w-full bg-brand-500 hover:bg-brand-600 text-white py-3 rounded-xl font-bold">Simpan Akun</button>
</form>
</div>
</div>`;
lucide.createIcons();
}
function saveUser(event, id) {
event.preventDefault();
if (currentUserRole !== "Owner") { showToast("Hanya Owner yang dapat mengelola akun.", "error"); return; }
const name = document.getElementById("userName").value.trim();
const username = document.getElementById("userUsername").value.trim();
const password = document.getElementById("userPassword").value;
const role = document.getElementById("userRole").value;
const existing = users.find(item => item.id === id);
const duplicate = users.some(item => item.id !== id && item.username.toLowerCase() === username.toLowerCase());
if (duplicate) { showToast("Username sudah digunakan.", "error"); return; }
if (!name || !username || (!existing && password.length < 4) || (password && password.length < 4)) { showToast("Lengkapi data. Password minimal 4 karakter.", "error"); return; }
if (existing && existing.id === currentUserId && role !== "Owner") { showToast("Role akun Owner yang sedang digunakan tidak dapat diturunkan.", "error"); return; }
const ownerCountAfter = users.filter(item => item.role === "Owner" && item.id !== id).length + (role === "Owner" ? 1 : 0);
if (ownerCountAfter < 1) { showToast("Minimal harus ada satu akun Owner.", "error"); return; }
const updated = existing
? { ...existing, name, username, role, password: password || existing.password }
: { id: Date.now(), name, username, password, role };
if (existing) users = users.map(item => item.id === id ? updated : item);
else users.push(updated);
if (existing && existing.id === currentUserId) {
currentUserName = updated.name;
currentUserRole = updated.role;
localStorage.setItem("pmt_current_name", currentUserName);
localStorage.setItem("pmt_current_role", currentUserRole);
}
saveData();
closeModal();
showToast(existing ? "Akun berhasil diperbarui." : "Akun berhasil ditambahkan.");
render();
}
function deleteUser(id) {
if (currentUserRole !== "Owner") { showToast("Hanya Owner yang dapat mengelola akun.", "error"); return; }
const user = users.find(item => item.id === id);
if (!user) return;
if (id === currentUserId) { showToast("Akun yang sedang digunakan tidak dapat dihapus.", "error"); return; }
if (user.role === "Owner" && users.filter(item => item.role === "Owner").length <= 1) { showToast("Minimal harus ada satu akun Owner.", "error"); return; }
if (!confirm(`Hapus akun ${user.name}?`)) return;
users = users.filter(item => item.id !== id);
saveData();
showToast("Akun berhasil dihapus.");
render();
}
function brandPage() {
return `
<div class="max-w-[900px] mx-auto">
<h1 class="text-2xl font-bold">Profil Brand</h1>
<p class="text-sm text-slate-500 mt-1 mb-5">Informasi yang tampil pada struk cetak pelanggan.</p>
<div class="card p-5 space-y-4">
<div>
<label class="block text-sm font-semibold mb-1">Nama Usaha</label>
<input value="Pangsit Mie Tetangga" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none">
</div>
<div>
<label class="block text-sm font-semibold mb-1">Alamat Outlet</label>
<textarea class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none" rows="2">Jl. Contoh No. 123, Malang</textarea>
</div>
<div>
<label class="block text-sm font-semibold mb-1">Nomor WhatsApp / Telepon</label>
<input value="081234567890" class="w-full border border-stone-200 rounded-xl px-4 py-2.5 outline-none">
</div>
<button onclick="showToast('Profil brand berhasil diperbarui')" class="bg-brand-500 hover:bg-brand-600 text-white px-5 py-3 rounded-xl font-bold transition">
Simpan Profil
</button>
</div>
</div>
`;
}
function transactionsPage() {
return `
<div class="max-w-[1100px] mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5">
<div>
<h1 class="text-2xl font-bold">Tambah Transaksi Manual</h1>
<p class="text-sm text-slate-500">Input pemasukan dan pengeluaran kas dengan cepat.</p>
</div>
<button onclick="openTransactionModal()" class="bg-brand-500 text-white px-4 py-3 rounded-xl font-semibold">
+ Tambah Transaksi
</button>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<button onclick="openTransactionModal('Pemasukan')" class="card p-5 text-left hover:shadow-lg transition">
<div class="food-icon green">${icon("coins",23)}</div>
<div class="font-bold mt-4 text-slate-900">Pemasukan</div>
<div class="text-xs text-slate-500 mt-1">Modal, transfer owner, dan arus masuk lainnya.</div>
</button>
<button onclick="openTransactionModal('Pengeluaran Usaha')" class="card p-5 text-left hover:shadow-lg transition">
<div class="food-icon red">${icon("shopping-basket",23)}</div>
<div class="font-bold mt-4 text-slate-900">Pengeluaran Usaha</div>
<div class="text-xs text-slate-500 mt-1">Bahan baku, operasional, listrik, gas, plastik.</div>
</button>
<button onclick="openTransactionModal('Pengeluaran Pribadi')" class="card p-5 text-left hover:shadow-lg transition">
<div class="food-icon purple">${icon("wallet",23)}</div>
<div class="font-bold mt-4 text-slate-900">Pengeluaran Pribadi</div>
<div class="text-xs text-slate-500 mt-1">Prive owner, bensin, dan kebutuhan pribadi.</div>
</button>
</div>
</div>
`;
}
/* =========================================================
RECEIPT & WHATSAPP
========================================================= */
function openReceipt(transaction) {
const items = transaction.items || [];
document.getElementById("modalRoot").innerHTML = `
<div class="fixed inset-0 modal-backdrop z-[200] flex items-center justify-center p-4">
<div class="bg-white rounded-2xl p-5 max-h-[95vh] overflow-y-auto shadow-2xl">
<div class="flex items-center justify-between mb-4 border-b border-stone-100 pb-3">
<h2 class="font-bold text-lg text-slate-900">Struk Pembayaran</h2>
<button onclick="closeModal()" class="p-1.5 rounded-lg hover:bg-stone-100">${icon("x",20)}</button>
</div>
<div id="receiptContent" class="receipt shadow-sm border border-stone-200 rounded-xl">
<div class="text-center">
<div class="font-extrabold text-xl text-slate-900">Pangsit Mie Tetangga</div>
<div class="text-xs text-slate-500">Rasa Enak, Harga Bersahabat</div>
<div class="text-xs mt-1 text-slate-600">Jl. Contoh No. 123, Malang</div>
</div>
<div class="receipt-line"></div>
<div class="text-xs text-slate-600">${formatDateTime(transaction.date)}</div>
${transaction.customer ? `<div class="text-xs mt-1 text-slate-700">Pelanggan: ${escapeHTML(transaction.customer)}</div>` : ""}
<div class="receipt-line"></div>
${items.map(item => `
<div class="flex justify-between text-sm mb-2">
<div class="pr-3 text-slate-800">${escapeHTML(item.name)} x${item.qty}</div>
<div class="font-semibold text-slate-900">${rupiah(item.price * item.qty)}</div>
</div>
`).join("")}
<div class="receipt-line"></div>
<div class="flex justify-between font-bold text-base">
<span>TOTAL</span>
<span>${rupiah(transaction.amount)}</span>
</div>
${transaction.amountReceived != null ? `
<div class="flex justify-between text-xs mt-2">
<span>Uang diterima</span>
<span>${rupiah(transaction.amountReceived)}</span>
</div>
<div class="flex justify-between text-xs mt-1">
<span>Kembalian</span>
<span>${rupiah(transaction.change || 0)}</span>
</div>
` : ""}
<div class="text-center text-xs text-slate-500 mt-5">Terima kasih atas kunjungan Anda!</div>
</div>
<div class="grid grid-cols-3 gap-2 mt-4">
<button onclick="printReceipt()" class="border border-stone-200 rounded-xl py-2.5 text-xs font-semibold flex items-center justify-center gap-1 hover:bg-stone-50">
${icon("printer",15)} Print
</button>
<button onclick="downloadReceipt()" class="border border-stone-200 rounded-xl py-2.5 text-xs font-semibold flex items-center justify-center gap-1 hover:bg-stone-50">
${icon("download",15)} PNG
</button>
<button onclick="sendWhatsApp('${encodeURIComponent(transaction.whatsapp || "")}',${transaction.amount})" class="bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl py-2.5 text-xs font-semibold flex items-center justify-center gap-1">
${icon("message-circle",15)} WhatsApp
</button>
</div>
</div>
</div>
`;
lucide.createIcons();
}
function printReceipt() {
const receipt = document.getElementById("receiptContent");
if (!receipt) return;
const win = window.open("", "_blank", "width=400,height=700");
win.document.write(`
<html>
<head>
<title>Struk Pangsit Mie Tetangga</title>
<style>
body { margin:0; display:flex; justify-center; font-family:Arial, sans-serif; }
.receipt { width:320px; padding:20px; }
.receipt-line { border-top:1px dashed #999; margin:12px 0; }
.flex { display:flex; }
.justify-between { justify-content:space-between; }
</style>
</head>
<body>
<div class="receipt">${receipt.innerHTML}</div>
<script>window.onload = function() { window.print(); }<\/script>
</body>
</html>
`);
win.document.close();
}
async function downloadReceipt() {
const receipt = document.getElementById("receiptContent");
if (!receipt) return;
const canvas = await html2canvas(receipt, { scale: 2, backgroundColor: "#ffffff" });
const link = document.createElement("a");
link.download = `struk-${Date.now()}.png`;
link.href = canvas.toDataURL("image/png");
link.click();
showToast("Struk berhasil didownload sebagai PNG");
}
function sendWhatsApp(encodedNumber) {
let number = decodeURIComponent(encodedNumber || "");
if (!number) { showToast("Nomor WhatsApp pelanggan belum diisi", "error"); return; }
number = number.replace(/\D/g,"");
if (number.startsWith("0")) { number = "62" + number.substring(1); }
const text = `Halo, terima kasih telah berbelanja di Pangsit Mie Tetangga! Pesanan Anda telah kami proses.`;
window.open(`https://wa.me/${number}?text=${encodeURIComponent(text)}`, "_blank");
}
function closeModal() {
document.getElementById("modalRoot").innerHTML = "";
}
/* =========================================================
MAIN RENDER & INITIALIZATION
========================================================= */
function render() {
let html = "";
switch(currentPage) {
case "dashboard": html = dashboardPage(); break;
case "pos": html = posPage(); break;
case "history": html = historyPage(); break;
case "accounts": html = accountsPage(); break;
case "transactions": html = transactionsPage(); break;
case "finance": html = financePage(); break;
case "reports": html = reportsPage(); break;
case "menus": html = menusPage(); break;
case "users": html = usersPage(); break;
case "brand": html = brandPage(); break;
default: html = dashboardPage();
}
document.getElementById("content").innerHTML = html;
updateNav();
lucide.createIcons();
updateCurrentUserUI();
}
// Load shared data from Apps Script before rendering the application.
startApp();
</script>
</body>
</html>
-->
<script>
// ⚠️ GANTI DENGAN URL WEB APP GOOGLE APPS SCRIPT ANDA (BERAKHIRAN /exec)
const GAS_API_URL = "https://script.google.com/macros/s/AKfycbw1Hg_pc6yPYZJvitcMxg3Eg-AS9hhIdSk6iboreZo3XoAOvdZY7GmbS8p7qcIwhEya/exec";
// Fungsi Ambil Data dari Google Sheets via GAS
async function loadDataFromGAS() {
try {
const response = await fetch(GAS_API_URL, {
method: "POST",
body: JSON.stringify({ action: "getSharedData" })
});
const res = await response.json();
if (res.status === "success") {
return res.data;
}
} catch (err) {
console.error("Gagal mengambil data:", err);
}
}
// Fungsi Simpan Data ke Google Sheets via GAS
async function saveDataToGAS(snapshotData) {
try {
const response = await fetch(GAS_API_URL, {
method: "POST",
body: JSON.stringify({
action: "saveSharedData",
payload: snapshotData
})
});
const res = await response.json();
return res;
} catch (err) {
console.error("Gagal menyimpan data:", err);
}
}
</script>
</body>
</html>