Skip to content

Instantly share code, notes, and snippets.

@tsaikienhung06-cmd
Last active January 25, 2026 14:56
Show Gist options
  • Select an option

  • Save tsaikienhung06-cmd/6d46da5b6f72c7803f262e796670ac60 to your computer and use it in GitHub Desktop.

Select an option

Save tsaikienhung06-cmd/6d46da5b6f72c7803f262e796670ac60 to your computer and use it in GitHub Desktop.
SmartBook AI - Financial Reporting
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SmartBook AI - Easy Financial Reporting (MASB Style)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<!-- PDF Generation Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<!-- Charting Library -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: 'Inter', sans-serif; background-color: #f7f7f9; }
.report-table td, .report-table th { padding: 0.5rem 1rem; }
.report-card {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
/* Styles for the navigation tabs */
.tab-button {
padding: 0.5rem 1rem;
border-bottom: 2px solid transparent;
font-weight: 600;
color: #6b7280; /* gray-500 */
transition: all 0.15s ease-in-out;
}
.tab-button.active {
border-color: #4f46e5; /* indigo-600 */
color: #4f46e5; /* indigo-600 */
background-color: #f7f7f9;
}
/* Dashboard Card Styles */
.metric-card {
background-color: white;
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
border: 1px solid #e5e7eb;
}
/* Language modal styles */
.language-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
z-index: 1000;
align-items: center;
justify-content: center;
}
.language-modal.show {
display: flex;
}
</style>
</head>
<body>
<div id="app" class="min-h-screen p-4 sm:p-8">
<!-- Header -->
<header class="mb-8 border-b pb-4 no-print flex justify-between items-start sm:items-center">
<div class="flex-grow">
<h1 class="text-3xl font-bold text-gray-800">SmartBook <span class="text-indigo-600">AI</span></h1>
<p id="app-subtitle" class="text-sm text-gray-500"></p>
<div id="auth-status" class="text-xs text-gray-400 mt-2">Ready to record transactions</div>
</div>
<!-- Language Selector -->
<div class="flex flex-col items-end space-y-1 ml-4">
<label for="language-select" class="text-xs font-medium text-gray-700">Language / Bahasa:</label>
<select id="language-select" class="rounded-md border-gray-300 text-sm shadow-sm p-1.5 border focus:ring-indigo-500 focus:border-indigo-500">
<option value="en">English</option>
<option value="ml">Bahasa Melayu</option>
</select>
</div>
</header>
<div class="lg:flex lg:space-x-8">
<!-- 1. Transaction Input Form -->
<div class="lg:w-1/3 mb-8 lg:mb-0 no-print">
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h2 id="form-title" class="text-xl font-semibold mb-4 text-indigo-600"></h2>
<form id="transaction-form" class="space-y-4">
<div>
<label for="date" id="label-date" class="block text-sm font-medium text-gray-700"></label>
<input type="date" id="date" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="description" id="label-description" class="block text-sm font-medium text-gray-700"></label>
<input type="text" id="description" placeholder="e.g., Sale to Customer A" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="category" id="label-category" class="block text-sm font-medium text-gray-700"></label>
<select id="category" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">
<!-- Options populated by JS -->
</select>
</div>
<div>
<label for="amount" id="label-amount" class="block text-sm font-medium text-gray-700"></label>
<input type="number" id="amount" step="0.01" min="0.01" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">
</div>
<button type="submit" id="transaction-button" class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150">
</button>
<button type="button" id="cancel-edit-button" class="w-full mt-2 py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-100 transition duration-150 hidden">
</button>
<div id="status-message" class="text-sm mt-2 text-center hidden"></div>
</form>
</div>
</div>
<!-- 2. Reports Viewer (Tabbed Interface) -->
<div class="lg:w-2/3">
<!-- Navigation Tabs & Month Picker -->
<div id="view-controls" class="no-print bg-white p-4 rounded-xl shadow-lg mb-4">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4">
<h2 id="report-viewer-title" class="text-2xl font-semibold text-gray-800 mb-2 sm:mb-0"></h2>
<div class="flex items-center space-x-2">
<label for="report-month" id="label-reporting-period" class="text-sm font-medium text-gray-700 whitespace-nowrap"></label>
<input type="month" id="report-month" class="rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
<!-- Tabs (Buttons) -->
<div class="flex space-x-2 border-b border-gray-200 -mb-4 overflow-x-auto">
<button id="tab-transactions" class="tab-button"></button>
<button id="tab-sopl" class="tab-button"></button>
<button id="tab-sofp" class="tab-button"></button>
<button id="tab-socf" class="tab-button"></button>
<button id="tab-dashboard" class="tab-button active"></button>
</div>
</div>
<!-- All Viewable Content Containers -->
<div id="content-views">
<!-- Dashboard View -->
<div id="view-dashboard" class="report-view">
<div id="dashboard-placeholder" class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 text-center text-gray-500">
Loading Dashboard...
</div>
</div>
<!-- SOPL View -->
<div id="view-sopl" class="report-view hidden">
<div id="sopl-placeholder" class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 text-center text-gray-500">
</div>
</div>
<!-- SOFP View -->
<div id="view-sofp" class="report-view hidden">
<div id="sofp-placeholder" class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 text-center text-gray-500">
</div>
</div>
<!-- SOCF View -->
<div id="view-socf" class="report-view hidden">
<div id="socf-placeholder" class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 text-center text-gray-500">
</div>
</div>
<!-- Transaction List View -->
<div id="view-transactions" class="report-view hidden">
<div class="mt-4 pt-4 border-t">
<h3 id="transactions-header" class="text-xl font-semibold mb-4 text-gray-800"></h3>
<div id="transactions-list-container" class="bg-white rounded-xl shadow-lg overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th id="th-date" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th id="th-description" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th id="th-category" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th id="th-amount" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th id="th-actions" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody id="transactions-list" class="bg-white divide-y divide-gray-200">
<tr><td colspan="5" class="px-6 py-4 text-center text-gray-400">Loading transactions...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Language Selection Modal -->
<div id="language-modal" class="language-modal">
<div class="bg-white p-8 rounded-xl shadow-2xl max-w-sm w-full text-center">
<h2 class="text-2xl font-bold text-indigo-600 mb-4">Choose Language / Pilih Bahasa</h2>
<p class="text-gray-600 mb-6">Select your preferred language to start using SmartBook AI.</p>
<div class="space-y-4">
<button id="btn-english" class="w-full py-3 px-4 border border-transparent rounded-lg shadow-sm text-lg font-semibold text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150">
English
</button>
<button id="btn-malay" class="w-full py-3 px-4 border border-transparent rounded-lg shadow-sm text-lg font-semibold text-gray-700 bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400 transition duration-150">
Bahasa Melayu
</button>
</div>
</div>
</div>
<script>
// --- GLOBAL VARIABLES ---
let allTransactions = [];
let editingTransactionId = null;
let currentView = 'dashboard';
let chartInstance = null;
let currentLanguage = 'en';
// --- LANGUAGE PACKS ---
const L_EN = {
title: "SmartBook AI",
subtitle: "MASB Simplified Financial Reporting (Cash Basis).",
form_title: "Record Transaction",
label_date: "Date",
label_description: "Description",
label_category: "Category (Simple)",
label_amount: "Amount (RM)",
record_button: "Record Transaction",
update_button: "Update Transaction",
cancel_button: "Cancel Edit",
report_viewer_title: "Financial Reports",
label_reporting_period: "Reporting Period:",
tab_transactions: "History",
tab_sopl: "P/L (SOPL)",
tab_sofp: "Position (SOFP)",
tab_socf: "Cash Flow (SOCF)",
tab_dashboard: "Dashboard",
th_date: "Date",
th_description: "Description",
th_category: "Category",
th_amount: "Amount (RM)",
th_actions: "Actions",
edit: "Edit",
delete: "Delete",
list_header: "Transaction History (Audit Trail)",
no_trans: "No transactions recorded yet.",
no_data_msg: "No transactions recorded for this period.",
no_month_msg: "Please select a reporting period.",
loading: "Loading transactions...",
status_saving: "Saving...",
status_updating: "Updating...",
status_success: "Transaction recorded successfully!",
status_update_success: "Transaction updated successfully!",
status_delete_success: "Transaction deleted successfully.",
status_edit_id: (id) => `Editing transaction ID: ${id}`,
sopl_title: "Statement of Profit or Loss (SOPL)",
sofp_title: "Statement of Financial Position (SOFP)",
socf_title: "Statement of Cash Flow (SOCF)",
sopl_placeholder: "Select a period and record transactions to view the Statement of Profit or Loss.",
sofp_placeholder: "The Statement of Financial Position will appear here.",
socf_placeholder: "The Statement of Cash Flow will appear here.",
// Categories
cat_select: "Select a category",
cat_group_income: "Income & Financing Inflows",
cat_sales: "Sales Revenue (Service/Goods)",
cat_interest: "Interest Received",
cat_capital: "Owner/Investor Capital",
cat_loan: "Bank Loan / Financing",
cat_group_expense: "Operating Expenses",
cat_rent: "Rent Expense",
cat_utilities: "Utilities (Electric, Water)",
cat_wages: "Wages & Salaries",
cat_supplies: "Supplies & Consumables",
cat_other_op: "Other Operating Expense",
cat_group_investment: "Investment & Drawings",
cat_equipment: "Equipment Purchase (Asset)",
cat_drawings: "Owner Drawings / Withdrawal",
// Report Line Items
"Revenue": "Revenue",
"Other Income": "Other Income",
"Operating Expenses": "Operating Expenses",
"Cash & Bank Balance": "Cash & Bank Balance",
"Equipment": "Equipment",
"Owner's Capital": "Owner's Capital",
"Loan Payable": "Loan Payable",
"Owner's Drawings": "Owner's Drawings",
"Sales Revenue": "Sales Revenue",
"Interest Income": "Interest Income",
"Rent Expense": "Rent Expense",
"Utilities Expense": "Utilities Expense",
"Wages & Salaries Expense": "Wages & Salaries Expense",
"Supplies Expense": "Supplies Expense",
"Other Operating Expense": "Other Operating Expense",
// Report Sections
report_revenue: "REVENUE",
report_expenses: "EXPENSES",
report_total_revenue: "TOTAL REVENUE",
report_total_expenses: "TOTAL EXPENSES",
report_net_profit: "NET PROFIT / (LOSS)",
report_assets: "ASSETS",
report_liabilities_equity: "LIABILITIES & EQUITY",
report_current_assets: "Current Assets",
report_non_current_assets: "Non-Current Assets",
report_cash: "Cash & Bank Balance",
report_total_assets: "TOTAL ASSETS",
report_liabilities: "Liabilities",
report_total_liabilities: "Total Liabilities",
report_equity: "Equity",
report_opening_capital: "Opening Capital + Drawings/Injection",
report_retained_earnings: "Retained Earnings / Net Profit",
report_closing_equity: "CLOSING EQUITY",
report_total_l_e: "TOTAL LIABILITIES & EQUITY",
report_balance_check: (check, diff) => `Accounting Equation Status: ${check} (Difference: ${formatCurrency(diff)})`,
report_balanced: "Balanced",
report_unbalanced: "UNBALANCED!",
// SOCF
cf_operating: "CASH FLOW FROM OPERATING ACTIVITIES",
cf_investing: "CASH FLOW FROM INVESTING ACTIVITIES",
cf_financing: "CASH FLOW FROM FINANCING ACTIVITIES",
cf_net_op: "Net Cash from Operating Activities",
cf_net_inv: "Net Cash from Investing Activities",
cf_net_fin: "Net Cash from Financing Activities",
cf_net_change: "Net Increase / (Decrease) in Cash",
cf_ending_cash: "ENDING CASH BALANCE",
// Dashboard
dashboard_title: "Key Business Health Metrics (Cumulative)",
dashboard_trend_title: "Monthly Performance Trend",
dashboard_trend_subtitle: "This chart shows how your income (Revenue) compares to your spending (Expenses) over time, and the resulting profit (Net Profit).",
metric_profitability: "1. Profitability Score",
metric_profitability_desc: "The percentage of every RM1 of sales that turns into profit. Aim for high values!",
metric_safety: "2. Financial Safety Score",
metric_safety_desc_inf: "No debt recorded, indicating very high financial security.",
metric_safety_desc_safe: (ratio) => `Your assets can cover debts ${ratio.toFixed(1)} times. Score > 1 is safe.`,
metric_safety_desc_risk: "Your debts are higher than your assets. Take immediate action.",
metric_asset_efficiency: "3. Asset Efficiency",
metric_asset_efficiency_desc: "How much profit you generate for every RM1 worth of company assets (equipment, cash, etc.).",
// Chart Labels
chart_income: 'Total Income (Revenue)',
chart_spending: 'Total Spending (Expenses)',
chart_profit: 'Net Profit',
chart_trend_title: 'Income, Spending & Profit Trend',
chart_y_title: 'Amount (RM)',
// PDF
pdf_download: "Download PDF",
pdf_generating: (type) => `Generating PDF for ${type}... Please wait.`,
pdf_success: (name) => `'${name}' downloaded successfully.`,
pdf_error: (msg) => `Error generating PDF: ${msg}. Check console for details.`,
};
const L_ML = {
title: "SmartBook AI",
subtitle: "Pelaporan Kewangan Ringkas MASB (Asas Tunai).",
form_title: "Rekod Transaksi",
label_date: "Tarikh",
label_description: "Huraian",
label_category: "Kategori (Ringkas)",
label_amount: "Jumlah (RM)",
record_button: "Rekod Transaksi",
update_button: "Kemaskini Transaksi",
cancel_button: "Batal Suntingan",
report_viewer_title: "Laporan Kewangan",
label_reporting_period: "Tempoh Pelaporan:",
tab_transactions: "Sejarah",
tab_sopl: "U/R (SOPL)",
tab_sofp: "Kedudukan (SOFP)",
tab_socf: "Aliran Tunai (SOCF)",
tab_dashboard: "Papan Pemuka",
th_date: "Tarikh",
th_description: "Huraian",
th_category: "Kategori",
th_amount: "Jumlah (RM)",
th_actions: "Tindakan",
edit: "Sunting",
delete: "Padam",
list_header: "Sejarah Transaksi (Jejak Audit)",
no_trans: "Tiada transaksi direkodkan lagi.",
no_data_msg: "Tiada data transaksi direkodkan untuk tempoh ini.",
no_month_msg: "Sila pilih tempoh pelaporan.",
loading: "Memuatkan transaksi...",
status_saving: "Menyimpan...",
status_updating: "Mengemaskini...",
status_success: "Transaksi berjaya direkodkan!",
status_update_success: "Transaksi berjaya dikemaskini!",
status_delete_success: "Transaksi berjaya dipadam.",
status_edit_id: (id) => `Menyunting ID transaksi: ${id}`,
sopl_title: "Penyata Untung Rugi (SOPL)",
sofp_title: "Penyata Kedudukan Kewangan (SOFP)",
socf_title: "Penyata Aliran Tunai (SOCF)",
sopl_placeholder: "Pilih tempoh dan rekod transaksi untuk melihat Penyata Untung Rugi.",
sofp_placeholder: "Penyata Kedudukan Kewangan akan dipaparkan di sini.",
socf_placeholder: "Penyata Aliran Tunai akan dipaparkan di sini.",
// Categories
cat_select: "Pilih kategori",
cat_group_income: "Aliran Masuk Pendapatan & Pembiayaan",
cat_sales: "Hasil Jualan (Perkhidmatan/Barangan)",
cat_interest: "Faedah Diterima",
cat_capital: "Modal Pemilik/Pelabur",
cat_loan: "Pinjaman Bank / Pembiayaan",
cat_group_expense: "Perbelanjaan Operasi",
cat_rent: "Sewa Dibayar",
cat_utilities: "Utiliti (Elektrik, Air)",
cat_wages: "Gaji & Upah",
cat_supplies: "Bekalan & Bahan Habis Guna",
cat_other_op: "Perbelanjaan Operasi Lain",
cat_group_investment: "Pelaburan & Pengeluaran",
cat_equipment: "Pembelian Peralatan (Aset)",
cat_drawings: "Pengeluaran Pemilik",
// Report Line Items
"Revenue": "Hasil",
"Other Income": "Pendapatan Lain",
"Operating Expenses": "Perbelanjaan Operasi",
"Cash & Bank Balance": "Tunai & Baki Bank",
"Equipment": "Peralatan",
"Owner's Capital": "Modal Pemilik",
"Loan Payable": "Pinjaman Belum Bayar",
"Owner's Drawings": "Pengeluaran Pemilik",
"Sales Revenue": "Hasil Jualan",
"Interest Income": "Pendapatan Faedah",
"Rent Expense": "Belanja Sewa",
"Utilities Expense": "Belanja Utiliti",
"Wages & Salaries Expense": "Belanja Gaji & Upah",
"Supplies Expense": "Belanja Bekalan",
"Other Operating Expense": "Belanja Operasi Lain",
// Report Sections
report_revenue: "HASIL",
report_expenses: "PERBELANJAAN",
report_total_revenue: "JUMLAH HASIL",
report_total_expenses: "JUMLAH PERBELANJAAN",
report_net_profit: "UNTUNG / (RUGI) BERSIH",
report_assets: "ASET",
report_liabilities_equity: "LIABILITI & EKUITI",
report_current_assets: "Aset Semasa",
report_non_current_assets: "Aset Bukan Semasa",
report_cash: "Tunai & Baki Bank",
report_total_assets: "JUMLAH ASET",
report_liabilities: "Liabiliti",
report_total_liabilities: "Jumlah Liabiliti",
report_equity: "Ekuiti",
report_opening_capital: "Modal Permulaan + Pengeluaran/Suntikan",
report_retained_earnings: "Untung Terkumpul / Untung Bersih",
report_closing_equity: "EKUITI PENUTUPAN",
report_total_l_e: "JUMLAH LIABILITI & EKUITI",
report_balance_check: (check, diff) => `Status Persamaan Perakaunan: ${check} (Perbezaan: ${formatCurrency(diff)})`,
report_balanced: "Seimbang",
report_unbalanced: "TIDAK SEIMBANG!",
// SOCF
cf_operating: "ALIRAN TUNAI DARIPADA AKTIVITI OPERASI",
cf_investing: "ALIRAN TUNAI DARIPADA AKTIVITI PELABURAN",
cf_financing: "ALIRAN TUNAI DARIPADA AKTIVITI PEMBIAYAAN",
cf_net_op: "Tunai Bersih daripada Aktiviti Operasi",
cf_net_inv: "Tunai Bersih daripada Aktiviti Pelaburan",
cf_net_fin: "Tunai Bersih daripada Aktiviti Pembiayaan",
cf_net_change: "Kenaikan / (Penurunan) Bersih dalam Tunai",
cf_ending_cash: "BAKI TUNAI AKHIR",
// Dashboard
dashboard_title: "Metrik Kesihatan Perniagaan Utama (Kumulatif)",
dashboard_trend_title: "Trend Prestasi Bulanan",
dashboard_trend_subtitle: "Carta ini menunjukkan perbandingan pendapatan (Hasil) anda dengan perbelanjaan (Belanja) dari semasa ke semasa, dan untung yang terhasil (Untung Bersih).",
metric_profitability: "1. Skor Keuntungan",
metric_profitability_desc: "Peratusan bagi setiap RM1 jualan yang bertukar menjadi untung. Sasarkan nilai yang tinggi!",
metric_safety: "2. Skor Keselamatan Kewangan",
metric_safety_desc_inf: "Tiada hutang direkodkan, menunjukkan keselamatan kewangan yang sangat tinggi.",
metric_safety_desc_safe: (ratio) => `Aset anda boleh menampung hutang ${ratio.toFixed(1)} kali ganda. Skor > 1 adalah selamat.`,
metric_safety_desc_risk: "Hutang anda lebih tinggi daripada aset anda. Ambil tindakan segera.",
metric_asset_efficiency: "3. Kecekapan Aset",
metric_asset_efficiency_desc: "Berapa banyak untung yang anda jana untuk setiap RM1 nilai aset syarikat (peralatan, tunai, dsb.).",
// Chart Labels
chart_income: 'Jumlah Pendapatan (Hasil)',
chart_spending: 'Jumlah Perbelanjaan (Belanja)',
chart_profit: 'Untung Bersih',
chart_trend_title: 'Trend Pendapatan, Perbelanjaan & Untung',
chart_y_title: 'Jumlah (RM)',
// PDF
pdf_download: "Muat Turun PDF",
pdf_generating: (type) => `Menjana PDF untuk ${type}... Sila tunggu.`,
pdf_success: (name) => `'${name}' berjaya dimuat turun.`,
pdf_error: (msg) => `Ralat menjana PDF: ${msg}. Semak konsol untuk butiran.`,
};
// --- ACCOUNTING CONFIG ---
const ACCOUNT_MAP = {
"Sales Revenue": { account: "Sales Revenue", report: "SOPL", lineItem: "Revenue", effect: 1, flow: "Operating" },
"Interest Received": { account: "Interest Income", report: "SOPL", lineItem: "Other Income", effect: 1, flow: "Operating" },
"Rent Expense": { account: "Rent Expense", report: "SOPL", lineItem: "Operating Expenses", effect: -1, flow: "Operating" },
"Utilities Expense": { account: "Utilities Expense", report: "SOPL", lineItem: "Operating Expenses", effect: -1, flow: "Operating" },
"Wages & Salaries": { account: "Wages & Salaries Expense", report: "SOPL", lineItem: "Operating Expenses", effect: -1, flow: "Operating" },
"Supplies & Consumables": { account: "Supplies Expense", report: "SOPL", lineItem: "Operating Expenses", effect: -1, flow: "Operating" },
"Other Operating Expense": { account: "Other Operating Expense", report: "SOPL", lineItem: "Operating Expenses", effect: -1, flow: "Operating" },
"Equipment Purchase": { account: "Equipment", report: "SOFP", lineItem: "Non-current Assets", effect: 1, flow: "Investing" },
"Capital Injection": { account: "Owner's Capital", report: "SOFP", lineItem: "Equity", effect: 1, flow: "Financing" },
"Loan Received": { account: "Loan Payable", report: "SOFP", lineItem: "Non-current Liabilities", effect: 1, flow: "Financing" },
"Drawings": { account: "Owner's Drawings", report: "SOFP", lineItem: "Equity (Reduction)", effect: -1, flow: "Financing" },
};
const CATEGORY_DISPLAY_MAP = {
"Sales Revenue": () => L.cat_sales,
"Interest Received": () => L.cat_interest,
"Rent Expense": () => L.cat_rent,
"Utilities Expense": () => L.cat_utilities,
"Wages & Salaries": () => L.cat_wages,
"Supplies & Consumables": () => L.cat_supplies,
"Other Operating Expense": () => L.cat_other_op,
"Equipment Purchase": () => L.cat_equipment,
"Capital Injection": () => L.cat_capital,
"Loan Received": () => L.cat_loan,
"Drawings": () => L.cat_drawings,
};
// --- UTILITY FUNCTIONS ---
let L = L_EN;
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-MY', { style: 'currency', currency: 'MYR' }).format(amount || 0);
};
const formatPercentage = (value) => {
return new Intl.NumberFormat('en-US', { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1 }).format(value || 0);
};
function showStatusMessage(message, color) {
const statusMessage = document.getElementById('status-message');
statusMessage.textContent = message;
statusMessage.className = `text-sm mt-2 text-center text-${color}-600 block`;
if (color !== 'indigo') {
setTimeout(() => statusMessage.className = 'hidden', 4000);
}
}
function formatMonthYear(yyyyMm) {
if (!yyyyMm) return '';
const [year, month] = yyyyMm.split('-');
const date = new Date(year, month - 1, 1);
const locale = currentLanguage === 'ml' ? 'ms-MY' : 'en-US';
return date.toLocaleDateString(locale, { year: 'numeric', month: 'long' });
}
// --- LOCAL STORAGE ---
function loadTransactions() {
const stored = localStorage.getItem('smartbook_transactions');
allTransactions = stored ? JSON.parse(stored) : [];
renderTransactionList();
updateReportsView();
}
function saveTransactions() {
localStorage.setItem('smartbook_transactions', JSON.stringify(allTransactions));
}
// --- CRUD OPERATIONS ---
window.editTransaction = function(transaction) {
editingTransactionId = transaction.id;
document.getElementById('date').value = transaction.date;
document.getElementById('description').value = transaction.description;
document.getElementById('category').value = transaction.category;
document.getElementById('amount').value = transaction.amount;
document.getElementById('transaction-button').textContent = L.update_button;
document.getElementById('cancel-edit-button').classList.remove('hidden');
showStatusMessage(L.status_edit_id(editingTransactionId), 'indigo');
};
window.resetForm = function() {
editingTransactionId = null;
document.getElementById('transaction-form').reset();
const today = new Date().toISOString().split('T')[0];
document.getElementById('date').value = today;
document.getElementById('transaction-button').textContent = L.record_button;
document.getElementById('cancel-edit-button').classList.add('hidden');
document.getElementById('status-message').className = 'hidden';
};
window.deleteTransaction = function(id) {
if (confirm('Are you sure you want to delete this transaction?')) {
allTransactions = allTransactions.filter(t => t.id !== id);
saveTransactions();
loadTransactions();
showStatusMessage(L.status_delete_success, 'green');
if (editingTransactionId === id) resetForm();
}
};
function saveTransaction(event) {
event.preventDefault();
const form = event.target;
const transactionData = {
id: editingTransactionId || Date.now().toString(),
date: form.date.value,
description: form.description.value,
category: form.category.value,
amount: parseFloat(form.amount.value),
};
showStatusMessage(editingTransactionId ? L.status_updating : L.status_saving, 'indigo');
if (editingTransactionId) {
const index = allTransactions.findIndex(t => t.id === editingTransactionId);
if (index !== -1) {
allTransactions[index] = transactionData;
showStatusMessage(L.status_update_success, 'green');
}
} else {
allTransactions.push(transactionData);
showStatusMessage(L.status_success, 'green');
}
saveTransactions();
loadTransactions();
resetForm();
}
// --- FINANCIAL CALCULATIONS ---
function calculateReportData(transactions) {
const data = {
sopl: {},
sofp: { assets: {}, liabilities: {}, equity: {} },
socf: { operating: {}, investing: {}, financing: {} },
netProfit: 0,
};
let currentCash = 0;
transactions.forEach(t => {
const map = ACCOUNT_MAP[t.category];
if (!map) return;
const amount = parseFloat(t.amount);
// SOPL
if (map.report === 'SOPL') {
const line = map.lineItem;
const value = amount * map.effect;
data.sopl[line] = (data.sopl[line] || 0) + value;
data.netProfit += value;
}
// SOFP
if (map.report === 'SOFP') {
const account = map.account;
if (map.lineItem.includes("Assets")) {
data.sofp.assets[account] = (data.sofp.assets[account] || 0) + amount;
} else if (map.lineItem.includes("Liabilities")) {
data.sofp.liabilities[account] = (data.sofp.liabilities[account] || 0) + amount;
} else if (map.lineItem.includes("Equity")) {
data.sofp.equity[account] = (data.sofp.equity[account] || 0) + (amount * map.effect);
}
}
// SOCF & Cash
let flowAmount = amount;
if (map.effect === -1 || map.account === 'Equipment') {
flowAmount = -amount;
} else if (map.account === 'Owner\'s Drawings') {
flowAmount = -amount;
}
if (map.flow) {
const account = map.account;
data.socf[map.flow.toLowerCase()][account] = (data.socf[map.flow.toLowerCase()][account] || 0) + flowAmount;
}
// Cash balance
if (map.effect === 1 && map.report !== 'SOFP') {
currentCash += amount;
} else if (map.effect === -1) {
currentCash -= amount;
} else if (map.account === 'Equipment') {
currentCash -= amount;
} else if (map.account === 'Owner\'s Capital' || map.account === 'Loan Payable') {
currentCash += amount;
}
});
data.sofp.assets['Cash & Bank Balance'] = currentCash;
return data;
}
function calculateMonthlyReports() {
const monthlyDataMap = {};
allTransactions.forEach(t => {
const monthKey = t.date.substring(0, 7);
const map = ACCOUNT_MAP[t.category];
if (!map || map.report !== 'SOPL') return;
if (!monthlyDataMap[monthKey]) {
monthlyDataMap[monthKey] = { netProfit: 0, revenue: 0, expenses: 0 };
}
const value = parseFloat(t.amount) * map.effect;
monthlyDataMap[monthKey].netProfit += value;
if (value > 0) {
monthlyDataMap[monthKey].revenue += value;
} else {
monthlyDataMap[monthKey].expenses += value;
}
});
const monthlyKeys = Object.keys(monthlyDataMap).sort();
return {
labels: monthlyKeys.map(key => formatMonthYear(key)),
revenue: monthlyKeys.map(key => monthlyDataMap[key].revenue),
expenses: monthlyKeys.map(key => monthlyDataMap[key].expenses * -1),
netProfit: monthlyKeys.map(key => monthlyDataMap[key].netProfit)
};
}
function calculateKeyRatios(cumulativeData) {
const sopl = cumulativeData.sopl;
const sofp = cumulativeData.sofp;
let totalRevenue = (sopl["Revenue"] || 0) + (sopl["Other Income"] || 0);
const totalAssets = Object.values(sofp.assets).reduce((sum, val) => sum + val, 0);
const totalLiabilities = Object.values(sofp.liabilities).reduce((sum, val) => sum + val, 0);
return {
'Profitability Score': totalRevenue > 0 ? cumulativeData.netProfit / totalRevenue : 0,
'Financial Safety Score': totalLiabilities > 0 ? totalAssets / totalLiabilities : (totalAssets > 0 ? Infinity : 0),
'Asset Efficiency': totalAssets > 0 ? cumulativeData.netProfit / totalAssets : 0
};
}
// --- RENDERING FUNCTIONS ---
function renderTransactionList() {
const listBody = document.getElementById('transactions-list');
listBody.innerHTML = '';
const sorted = [...allTransactions].sort((a, b) => new Date(b.date) - new Date(a.date));
if (sorted.length === 0) {
listBody.innerHTML = `<tr><td colspan="5" class="px-6 py-4 text-center text-gray-400">${L.no_trans}</td></tr>`;
return;
}
sorted.forEach((t, index) => {
const translatedCategory = CATEGORY_DISPLAY_MAP[t.category] ? CATEGORY_DISPLAY_MAP[t.category]() : t.category;
const transactionJson = JSON.stringify({
id: t.id,
date: t.date,
description: t.description,
category: t.category,
amount: t.amount
}).replace(/"/g, '&quot;');
const row = `
<tr class="${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${t.date}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${t.description}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${translatedCategory}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-right text-gray-900">${formatCurrency(t.amount)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-center">
<button onclick="editTransaction(${transactionJson})" class="text-indigo-600 hover:text-indigo-800 font-medium mr-2">${L.edit}</button>
<button onclick="deleteTransaction('${t.id}')" class="text-red-600 hover:text-red-800 font-medium">${L.delete}</button>
</td>
</tr>
`;
listBody.innerHTML += row;
});
}
function renderReport(title, periodTitle, contentHtml, reportId) {
const reportType = reportId.split('-')[0].toUpperCase();
const periodText = reportType === 'SOPL'
? (currentLanguage === 'ml' ? `Untuk Bulan ${periodTitle}` : `For the Month of ${periodTitle}`)
: (currentLanguage === 'ml' ? `Sehingga ${periodTitle}` : `As of ${periodTitle}`);
return `
<div id="${reportId}" class="report-card bg-white p-6 rounded-xl shadow-xl border border-gray-100">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-2xl font-bold text-gray-800">${title}</h3>
<p class="text-sm text-gray-500">${periodText}</p>
</div>
<div class="print-button-container no-print">
<button onclick="generatePDF('${reportId}', '${periodTitle.replace(/'/g, "\\'")}', '${reportType}')" class="py-1 px-3 border border-green-600 rounded-md shadow-sm text-xs font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition duration-150">
${L.pdf_download}
</button>
</div>
</div>
<table class="w-full report-table text-gray-700">
${contentHtml}
</table>
</div>
`;
}
function renderSOPL(soplData, netProfit, periodTitle, reportId) {
let rows = '';
let totalRevenue = 0;
let totalExpenses = 0;
rows += `<tr><td colspan="3" class="pt-4 pb-2 font-semibold text-lg text-indigo-700">${L.report_revenue}</td></tr>`;
for (const line in soplData) {
if (soplData[line] > 0) {
rows += `<tr><td>${L[line]}</td><td></td><td class="text-right">${formatCurrency(soplData[line])}</td></tr>`;
totalRevenue += soplData[line];
}
}
rows += `<tr class="border-t border-b-2 font-bold bg-indigo-50"><td colspan="2">${L.report_total_revenue}</td><td class="text-right">${formatCurrency(totalRevenue)}</td></tr>`;
rows += `<tr><td colspan="3" class="pt-6 pb-2 font-semibold text-lg text-red-700">${L.report_expenses}</td></tr>`;
for (const line in soplData) {
if (soplData[line] < 0) {
rows += `<tr><td>${L[line]}</td><td class="text-right">${formatCurrency(soplData[line] * -1)}</td><td></td></tr>`;
totalExpenses += soplData[line];
}
}
rows += `<tr class="border-t border-b-2 font-bold bg-red-50"><td colspan="2">${L.report_total_expenses}</td><td class="text-right">(${formatCurrency(totalExpenses * -1)})</td></tr>`;
rows += `<tr class="font-extrabold text-lg ${netProfit >= 0 ? 'text-green-700' : 'text-red-700'} border-t-4 border-b-4 mt-4 bg-gray-200">
<td colspan="2">${L.report_net_profit}</td>
<td class="text-right">${formatCurrency(netProfit)}</td>
</tr>`;
return renderReport(L.sopl_title, periodTitle, `<tbody>${rows}</tbody>`, reportId);
}
function renderSOFP(assets, liabilities, netProfit, equityAccounts, periodTitle, reportId) {
let assetRows = `<tr><td colspan="3" class="pt-4 pb-2 font-semibold text-lg text-indigo-700">${L.report_assets}</td></tr>`;
let totalAssets = 0;
assetRows += `<tr><td colspan="3" class="pt-2 font-medium text-gray-600">${L.report_current_assets}</td></tr>`;
const cashKey = 'Cash & Bank Balance';
const cash = assets[cashKey] || 0;
assetRows += `<tr><td>${L[cashKey]}</td><td></td><td class="text-right">${formatCurrency(cash)}</td></tr>`;
totalAssets += cash;
assetRows += `<tr><td colspan="3" class="pt-4 font-medium text-gray-600">${L.report_non_current_assets}</td></tr>`;
for (const account in assets) {
if (account !== cashKey) {
assetRows += `<tr><td>${L[account]}</td><td></td><td class="text-right">${formatCurrency(assets[account])}</td></tr>`;
totalAssets += assets[account];
}
}
assetRows += `<tr class="border-t-2 border-b-2 font-bold bg-indigo-50"><td colspan="2">${L.report_total_assets}</td><td class="text-right">${formatCurrency(totalAssets)}</td></tr>`;
let liabilityEquityRows = `<tr><td colspan="3" class="pt-6 pb-2 font-semibold text-lg text-red-700">${L.report_liabilities_equity}</td></tr>`;
liabilityEquityRows += `<tr><td colspan="3" class="pt-2 font-medium text-gray-600">${L.report_liabilities}</td></tr>`;
let totalLiabilities = 0;
for (const account in liabilities) {
liabilityEquityRows += `<tr><td>${L[account]}</td><td></td><td class="text-right">${formatCurrency(liabilities[account])}</td></tr>`;
totalLiabilities += liabilities[account];
}
liabilityEquityRows += `<tr class="border-t font-medium text-sm"><td colspan="2">${L.report_total_liabilities}</td><td class="text-right">${formatCurrency(totalLiabilities)}</td></tr>`;
let totalEquityAccounts = Object.values(equityAccounts).reduce((sum, val) => sum + val, 0);
const totalEquity = totalEquityAccounts + netProfit;
liabilityEquityRows += `<tr><td colspan="3" class="pt-4 font-medium text-gray-600">${L.report_equity}</td></tr>`;
for (const account in equityAccounts) {
const amount = equityAccounts[account];
const display = amount >= 0 ? formatCurrency(amount) : `(${formatCurrency(amount * -1)})`;
liabilityEquityRows += `<tr><td>${L[account]}</td><td></td><td class="text-right">${display}</td></tr>`;
}
liabilityEquityRows += `<tr><td>${L.report_retained_earnings}</td><td></td><td class="text-right">${formatCurrency(netProfit)}</td></tr>`;
liabilityEquityRows += `<tr class="font-extrabold text-lg text-gray-700 border-t-2">
<td colspan="2">${L.report_closing_equity}</td>
<td class="text-right">${formatCurrency(totalEquity)}</td>
</tr>`;
const totalLAndE = totalLiabilities + totalEquity;
liabilityEquityRows += `<tr class="font-extrabold text-lg text-gray-700 border-t-4 border-b-4 mt-4 bg-gray-200">
<td colspan="2">${L.report_total_l_e}</td>
<td class="text-right">${formatCurrency(totalLAndE)}</td>
</tr>`;
const diff = Math.abs(totalAssets - totalLAndE);
const check = diff < 0.01 ? L.report_balanced : L.report_unbalanced;
const checkColor = diff < 0.01 ? 'text-green-600' : 'text-red-600';
liabilityEquityRows += `<tr><td colspan="3" class="text-center pt-3 text-sm font-semibold ${checkColor}">${L.report_balance_check(check, diff)}</td></tr>`;
return renderReport(L.sofp_title, periodTitle, `<tbody>${assetRows}<tr><td colspan="3"><hr class="my-4"></td></tr>${liabilityEquityRows}</tbody>`, reportId);
}
function renderSOCF(socfData, endingCashBalance, periodTitle, reportId) {
let rows = '';
rows += `<tr><td colspan="3" class="pt-4 pb-2 font-semibold text-lg text-blue-700">${L.cf_operating}</td></tr>`;
let totalOperating = 0;
for (const account in socfData.operating) {
const amount = socfData.operating[account];
const display = amount >= 0 ? formatCurrency(amount) : `(${formatCurrency(amount * -1)})`;
rows += `<tr class="${amount >= 0 ? '' : 'text-red-600'}"><td>${L[account]}</td><td></td><td class="text-right">${display}</td></tr>`;
totalOperating += amount;
}
rows += `<tr class="border-t-2 font-bold bg-blue-50"><td colspan="2">${L.cf_net_op}</td><td class="text-right">${formatCurrency(totalOperating)}</td></tr>`;
rows += `<tr><td colspan="3" class="pt-6 pb-2 font-semibold text-lg text-blue-700">${L.cf_investing}</td></tr>`;
let totalInvesting = 0;
for (const account in socfData.investing) {
const amount = socfData.investing[account];
const display = amount >= 0 ? formatCurrency(amount) : `(${formatCurrency(amount * -1)})`;
rows += `<tr class="${amount >= 0 ? '' : 'text-red-600'}"><td>${L[account]}</td><td></td><td class="text-right">${display}</td></tr>`;
totalInvesting += amount;
}
rows += `<tr class="border-t-2 font-bold bg-blue-50"><td colspan="2">${L.cf_net_inv}</td><td class="text-right">${formatCurrency(totalInvesting)}</td></tr>`;
rows += `<tr><td colspan="3" class="pt-6 pb-2 font-semibold text-lg text-blue-700">${L.cf_financing}</td></tr>`;
let totalFinancing = 0;
for (const account in socfData.financing) {
const amount = socfData.financing[account];
const display = amount >= 0 ? formatCurrency(amount) : `(${formatCurrency(amount * -1)})`;
rows += `<tr class="${amount >= 0 ? '' : 'text-red-600'}"><td>${L[account]}</td><td></td><td class="text-right">${display}</td></tr>`;
totalFinancing += amount;
}
rows += `<tr class="border-t-2 font-bold bg-blue-50"><td colspan="2">${L.cf_net_fin}</td><td class="text-right">${formatCurrency(totalFinancing)}</td></tr>`;
const netChangeInCash = totalOperating + totalInvesting + totalFinancing;
rows += '<tr><td colspan="3" class="pt-6"></td></tr>';
rows += `<tr class="font-bold border-t-2">
<td colspan="2">${L.cf_net_change}</td>
<td class="text-right">${formatCurrency(netChangeInCash)}</td>
</tr>`;
rows += `<tr class="font-extrabold text-lg text-gray-700 border-t-4 border-b-4 mt-4 bg-gray-200">
<td colspan="2">${L.cf_ending_cash}</td>
<td class="text-right">${formatCurrency(endingCashBalance)}</td>
</tr>`;
return renderReport(L.socf_title, periodTitle, `<tbody>${rows}</tbody>`, reportId);
}
function renderDashboard(monthlyData, ratios, periodTitle) {
const dashboardView = document.getElementById('view-dashboard');
let ratioCards = '';
// Profitability Score
const profitabilityScore = formatPercentage(ratios['Profitability Score']);
const profitColor = ratios['Profitability Score'] >= 0.1 ? 'text-green-600' : (ratios['Profitability Score'] > 0 ? 'text-yellow-600' : 'text-red-600');
ratioCards += `
<div class="metric-card border-indigo-200">
<p class="text-sm font-medium text-gray-500">${L.metric_profitability}</p>
<p class="text-3xl font-extrabold ${profitColor} mt-1">${profitabilityScore}</p>
<p class="text-xs text-gray-400 mt-2">${L.metric_profitability_desc}</p>
</div>
`;
// Financial Safety Score
const safetyRatio = ratios['Financial Safety Score'];
const safetyScoreText = safetyRatio === Infinity ? L.report_balanced : safetyRatio.toFixed(1) + ':1';
const safetyScoreColor = safetyRatio === Infinity || safetyRatio >= 2 ? 'text-green-600' : (safetyRatio >= 1 ? 'text-yellow-600' : 'text-red-600');
let safetyExplanation;
if (safetyRatio === Infinity) {
safetyExplanation = L.metric_safety_desc_inf;
} else if (safetyRatio >= 1) {
safetyExplanation = L.metric_safety_desc_safe(safetyRatio);
} else {
safetyExplanation = L.metric_safety_desc_risk;
}
ratioCards += `
<div class="metric-card border-green-200">
<p class="text-sm font-medium text-gray-500">${L.metric_safety}</p>
<p class="text-3xl font-extrabold ${safetyScoreColor} mt-1">${safetyScoreText}</p>
<p class="text-xs text-gray-400 mt-2">${safetyExplanation}</p>
</div>
`;
// Asset Efficiency
const assetEfficiency = formatPercentage(ratios['Asset Efficiency']);
const assetColor = ratios['Asset Efficiency'] >= 0 ? 'text-blue-600' : 'text-red-600';
ratioCards += `
<div class="metric-card border-blue-200">
<p class="text-sm font-medium text-gray-500">${L.metric_asset_efficiency}</p>
<p class="text-3xl font-extrabold ${assetColor} mt-1">${assetEfficiency}</p>
<p class="text-xs text-gray-400 mt-2">${L.metric_asset_efficiency_desc}</p>
</div>
`;
dashboardView.innerHTML = `
<div id="dashboard-content">
<h3 class="text-xl font-semibold mb-4 text-gray-800">${L.dashboard_title}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">${ratioCards}</div>
<h3 class="text-xl font-semibold mb-4 text-gray-800">${L.dashboard_trend_title}</h3>
<p class="text-sm text-gray-500 mb-4">${L.dashboard_trend_subtitle}</p>
<div class="report-card">
<div class="p-4">
<canvas id="revenueExpenseChart"></canvas>
</div>
</div>
</div>
`;
const chartCtx = document.getElementById('revenueExpenseChart');
if (chartCtx) drawChart(chartCtx, monthlyData);
}
function drawChart(ctx, monthlyData) {
if (chartInstance) chartInstance.destroy();
chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: monthlyData.labels,
datasets: [
{
label: L.chart_income,
data: monthlyData.revenue,
backgroundColor: 'rgba(79, 70, 229, 0.7)',
borderColor: 'rgb(79, 70, 229)',
borderWidth: 1
},
{
label: L.chart_spending,
data: monthlyData.expenses,
backgroundColor: 'rgba(239, 68, 68, 0.7)',
borderColor: 'rgb(239, 68, 68)',
borderWidth: 1
},
{
type: 'line',
label: L.chart_profit,
data: monthlyData.netProfit,
backgroundColor: 'rgba(16, 185, 129, 0.9)',
borderColor: 'rgb(16, 185, 129)',
borderWidth: 2,
fill: false
}
]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: L.chart_y_title }
}
},
plugins: {
title: { display: true, text: L.chart_trend_title }
}
}
});
}
// --- VIEW MANAGEMENT ---
function showView(viewId) {
currentView = viewId;
document.querySelectorAll('.report-view').forEach(v => v.classList.add('hidden'));
document.querySelectorAll('.tab-button').forEach(b => b.classList.remove('active'));
document.getElementById(`view-${viewId}`).classList.remove('hidden');
document.getElementById(`tab-${viewId}`).classList.add('active');
if (viewId !== 'transactions') updateReportsView();
}
function updateReportsView() {
const reportMonth = document.getElementById('report-month').value;
if (!reportMonth && currentView !== 'dashboard') {
const noMonthMsg = `<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 text-center text-gray-500">${L.no_month_msg}</div>`;
document.getElementById('view-sopl').innerHTML = noMonthMsg;
document.getElementById('view-sofp').innerHTML = noMonthMsg;
document.getElementById('view-socf').innerHTML = noMonthMsg;
return;
}
const monthlyData = calculateMonthlyReports();
const periodTitle = formatMonthYear(reportMonth);
const monthlyTransactions = allTransactions.filter(t => t.date.substring(0, 7) === reportMonth);
const cumulativeTransactions = allTransactions.filter(t => t.date.localeCompare(reportMonth + '-31') <= 0);
const cumulativeData = calculateReportData(cumulativeTransactions);
const ratios = calculateKeyRatios(cumulativeData);
renderDashboard(monthlyData, ratios, periodTitle);
if (monthlyTransactions.length === 0 && cumulativeTransactions.length === 0 && reportMonth) {
const noDataMsg = `<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 text-center text-gray-500">${L.no_data_msg}</div>`;
if (currentView !== 'dashboard') {
document.getElementById('view-sopl').innerHTML = noDataMsg;
document.getElementById('view-sofp').innerHTML = noDataMsg;
document.getElementById('view-socf').innerHTML = noDataMsg;
}
return;
}
const monthlyReportData = calculateReportData(monthlyTransactions);
document.getElementById('view-sopl').innerHTML = renderSOPL(monthlyReportData.sopl, monthlyReportData.netProfit, periodTitle, 'sopl-report');
document.getElementById('view-sofp').innerHTML = renderSOFP(cumulativeData.sofp.assets, cumulativeData.sofp.liabilities, cumulativeData.netProfit, cumulativeData.sofp.equity, periodTitle, 'sofp-report');
document.getElementById('view-socf').innerHTML = renderSOCF(cumulativeData.socf, cumulativeData.sofp.assets['Cash & Bank Balance'] || 0, periodTitle, 'socf-report');
}
// --- PDF GENERATION ---
window.generatePDF = function(reportId, periodTitle, reportType) {
const reportElement = document.getElementById(reportId);
if (!reportElement) return;
const printBtn = reportElement.querySelector('.print-button-container');
if (printBtn) printBtn.style.display = 'none';
showStatusMessage(L.pdf_generating(reportType), 'indigo');
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
html2canvas(reportElement, { scale: 2, logging: false, useCORS: true }).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const pdfWidth = doc.internal.pageSize.getWidth();
const pdfHeight = doc.internal.pageSize.getHeight();
const imgHeight = canvas.height * pdfWidth / canvas.width;
let heightLeft = imgHeight;
let position = 0;
doc.addImage(imgData, 'PNG', 0, position, pdfWidth, imgHeight);
heightLeft -= pdfHeight;
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
doc.addPage();
doc.addImage(imgData, 'PNG', 0, position, pdfWidth, imgHeight);
heightLeft -= pdfHeight;
}
const fileName = `${reportType}-${periodTitle.replace(/\s/g, '-')}.pdf`;
doc.save(fileName);
if (printBtn) printBtn.style.display = 'block';
showStatusMessage(L.pdf_success(fileName), 'green');
}).catch(error => {
console.error("Error generating PDF:", error);
showStatusMessage(L.pdf_error(error.message), 'red');
if (printBtn) printBtn.style.display = 'block';
});
};
// --- LANGUAGE MANAGEMENT ---
window.setLanguage = function(lang) {
currentLanguage = lang;
L = lang === 'ml' ? L_ML : L_EN;
localStorage.setItem('smartbook_language', currentLanguage);
document.getElementById('language-select').value = currentLanguage;
initializeUI();
renderTransactionList();
updateReportsView();
showView(currentView);
};
function showLanguageModal() {
document.getElementById('language-modal').classList.add('show');
}
function hideLanguageModal() {
document.getElementById('language-modal').classList.remove('show');
}
// --- INITIALIZATION ---
function initializeUI() {
document.getElementById('app-subtitle').textContent = L.subtitle;
document.getElementById('form-title').textContent = L.form_title;
document.getElementById('label-date').textContent = L.label_date;
document.getElementById('label-description').textContent = L.label_description;
document.getElementById('label-category').textContent = L.label_category;
document.getElementById('label-amount').textContent = L.label_amount;
document.getElementById('transaction-button').textContent = L.record_button;
document.getElementById('cancel-edit-button').textContent = L.cancel_button;
const categorySelect = document.getElementById('category');
const selectedCategory = categorySelect.value;
categorySelect.innerHTML = `
<option value="" disabled selected>${L.cat_select}</option>
<optgroup label="${L.cat_group_income}">
<option value="Sales Revenue">${L.cat_sales}</option>
<option value="Interest Received">${L.cat_interest}</option>
<option value="Capital Injection">${L.cat_capital}</option>
<option value="Loan Received">${L.cat_loan}</option>
</optgroup>
<optgroup label="${L.cat_group_expense}">
<option value="Rent Expense">${L.cat_rent}</option>
<option value="Utilities Expense">${L.cat_utilities}</option>
<option value="Wages & Salaries">${L.cat_wages}</option>
<option value="Supplies & Consumables">${L.cat_supplies}</option>
<option value="Other Operating Expense">${L.cat_other_op}</option>
</optgroup>
<optgroup label="${L.cat_group_investment}">
<option value="Equipment Purchase">${L.cat_equipment}</option>
<option value="Drawings">${L.cat_drawings}</option>
</optgroup>
`;
if (selectedCategory) categorySelect.value = selectedCategory;
document.getElementById('report-viewer-title').textContent = L.report_viewer_title;
document.getElementById('label-reporting-period').textContent = L.label_reporting_period;
document.getElementById('tab-transactions').textContent = L.tab_transactions;
document.getElementById('tab-sopl').textContent = L.tab_sopl;
document.getElementById('tab-sofp').textContent = L.tab_sofp;
document.getElementById('tab-socf').textContent = L.tab_socf;
document.getElementById('tab-dashboard').textContent = L.tab_dashboard;
document.getElementById('transactions-header').textContent = L.list_header;
document.getElementById('th-date').textContent = L.th_date;
document.getElementById('th-description').textContent = L.th_description;
document.getElementById('th-category').textContent = L.th_category;
document.getElementById('th-amount').textContent = L.th_amount;
document.getElementById('th-actions').textContent = L.th_actions;
document.getElementById('dashboard-placeholder').textContent = L.loading;
document.getElementById('sopl-placeholder').textContent = L.sopl_placeholder;
document.getElementById('sofp-placeholder').textContent = L.sofp_placeholder;
document.getElementById('socf-placeholder').textContent = L.socf_placeholder;
const today = new Date().toISOString().split('T')[0];
document.getElementById('date').value = today;
if (!document.getElementById('report-month').value) {
const currentMonth = new Date().toISOString().substring(0, 7);
document.getElementById('report-month').value = currentMonth;
}
}
// --- EVENT LISTENERS ---
function initializeApp() {
const storedLanguage = localStorage.getItem('smartbook_language');
if (storedLanguage) {
setLanguage(storedLanguage);
} else {
showLanguageModal();
}
loadTransactions();
// Event Listeners
document.getElementById('report-month').addEventListener('change', updateReportsView);
document.getElementById('transaction-form').addEventListener('submit', saveTransaction);
document.getElementById('cancel-edit-button').addEventListener('click', resetForm);
document.getElementById('language-select').addEventListener('change', function() {
setLanguage(this.value);
});
document.getElementById('btn-english').addEventListener('click', function() {
setLanguage('en');
hideLanguageModal();
});
document.getElementById('btn-malay').addEventListener('click', function() {
setLanguage('ml');
hideLanguageModal();
});
// Tab buttons
document.getElementById('tab-transactions').addEventListener('click', () => showView('transactions'));
document.getElementById('tab-sopl').addEventListener('click', () => showView('sopl'));
document.getElementById('tab-sofp').addEventListener('click', () => showView('sofp'));
document.getElementById('tab-socf').addEventListener('click', () => showView('socf'));
document.getElementById('tab-dashboard').addEventListener('click', () => showView('dashboard'));
showView('dashboard');
}
// Start the app
document.addEventListener('DOMContentLoaded', initializeApp);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment