Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save tsaikienhung06-cmd/c72342ce75899377037535dfdc29f313 to your computer and use it in GitHub Desktop.
SmartBook AI - Financial Reporting System
<!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>
* {
font-family: 'Inter', sans-serif;
}
body {
background-color: #f7f7f9;
font-family: 'Inter', sans-serif;
}
.report-table td, .report-table th {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e5e7eb;
}
.report-table tr:last-child td {
border-bottom: none;
}
.report-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
/* Styles for the navigation tabs */
.tab-button {
padding: 0.75rem 1.5rem;
border-bottom: 3px solid transparent;
font-weight: 600;
color: #6b7280;
background: transparent;
cursor: pointer;
transition: all 0.15s ease-in-out;
white-space: nowrap;
}
.tab-button:hover {
color: #4f46e5;
background-color: #f7f7f9;
}
.tab-button.active {
border-color: #4f46e5;
color: #4f46e5;
background-color: #f7f7f9;
}
/* Dashboard Card Styles */
.metric-card {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
transition: transform 0.2s, box-shadow 0.2s;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* Form Styles */
input, select {
border: 1px solid #d1d5db !important;
border-radius: 8px !important;
padding: 0.75rem 1rem !important;
font-size: 14px !important;
}
input:focus, select:focus {
border-color: #4f46e5 !important;
ring-color: #4f46e5 !important;
outline: none !important;
}
/* Button Styles */
.btn-primary {
background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover {
background: linear-gradient(135deg, #4338ca 0%, #312e81 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
/* Table Styles */
table {
border-collapse: separate;
border-spacing: 0;
}
th {
background-color: #f9fafb;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
background: white;
border-radius: 16px;
padding: 2rem;
max-width: 400px;
width: 90%;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
/* Print Styles */
@media print {
.no-print {
display: none !important;
}
body {
background: white !important;
}
.report-card {
box-shadow: none !important;
border: 1px solid #ddd !important;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
</style>
</head>
<body class="bg-gray-50">
<div id="app" class="min-h-screen p-4 sm:p-8">
<!-- Header -->
<header class="mb-8 border-b border-gray-200 pb-6 no-print">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div class="flex-grow">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div>
<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 mt-1">MASB Simplified Financial Reporting (Cash Basis)</p>
</div>
</div>
<div id="auth-status" class="text-xs text-gray-400 mt-3 flex items-center gap-2">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Ready • Data saved locally in your browser</span>
</div>
</div>
<!-- Language Selector -->
<div class="flex flex-col items-end space-y-2">
<label for="language-select" class="text-xs font-medium text-gray-700">Language / Bahasa:</label>
<select id="language-select" class="rounded-lg border-gray-300 text-sm shadow-sm px-3 py-2 border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white min-w-[140px]">
<option value="en">English</option>
<option value="ml">Bahasa Melayu</option>
</select>
</div>
</div>
</header>
<div class="lg:flex lg:gap-8">
<!-- Left Column: Transaction Form -->
<div class="lg:w-1/3 mb-8 lg:mb-0">
<div class="report-card p-6">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 bg-indigo-100 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
</div>
<h2 id="form-title" class="text-xl font-semibold text-gray-800">Record Transaction</h2>
</div>
<form id="transaction-form" class="space-y-5">
<!-- Date -->
<div>
<label for="date" class="block text-sm font-medium text-gray-700 mb-2">
<span id="label-date">Date</span>
<span class="text-red-500">*</span>
</label>
<input type="date" id="date" required
class="w-full border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
<span id="label-description">Description</span>
<span class="text-red-500">*</span>
</label>
<input type="text" id="description" placeholder="e.g., Sales to Customer XYZ" required
class="w-full border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700 mb-2">
<span id="label-category">Category</span>
<span class="text-red-500">*</span>
</label>
<select id="category" required
class="w-full border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
<option value="" disabled selected>Select a category</option>
<optgroup label="Income & Financing">
<option value="Sales Revenue">Sales Revenue (Service/Goods)</option>
<option value="Interest Received">Interest Received</option>
<option value="Capital Injection">Owner/Investor Capital</option>
<option value="Loan Received">Bank Loan / Financing</option>
</optgroup>
<optgroup label="Operating Expenses">
<option value="Rent Expense">Rent Expense</option>
<option value="Utilities Expense">Utilities (Electric, Water)</option>
<option value="Wages & Salaries">Wages & Salaries</option>
<option value="Supplies & Consumables">Supplies & Consumables</option>
<option value="Other Operating Expense">Other Operating Expense</option>
</optgroup>
<optgroup label="Investment & Drawings">
<option value="Equipment Purchase">Equipment Purchase (Asset)</option>
<option value="Drawings">Owner Drawings / Withdrawal</option>
</optgroup>
</select>
</div>
<!-- Amount -->
<div>
<label for="amount" class="block text-sm font-medium text-gray-700 mb-2">
<span id="label-amount">Amount (RM)</span>
<span class="text-red-500">*</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-gray-500">RM</span>
</div>
<input type="number" id="amount" step="0.01" min="0.01" required
class="w-full border-gray-300 pl-10 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition"
placeholder="0.00">
</div>
</div>
<!-- Buttons -->
<div class="pt-2 space-y-3">
<button type="submit" id="transaction-button"
class="btn-primary w-full py-3 text-base">
Record Transaction
</button>
<button type="button" id="cancel-edit-button" onclick="resetForm()"
class="w-full py-2.5 px-4 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition duration-150 hidden">
Cancel Edit
</button>
<div id="status-message" class="text-sm text-center py-2 hidden"></div>
</div>
</form>
<!-- Stats -->
<div class="mt-8 pt-6 border-t border-gray-100">
<div class="grid grid-cols-2 gap-4">
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-gray-800" id="total-transactions">0</div>
<div class="text-xs text-gray-500 mt-1">Total Transactions</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-gray-800" id="total-amount">RM 0.00</div>
<div class="text-xs text-gray-500 mt-1">Total Amount</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Reports -->
<div class="lg:w-2/3">
<!-- Report Controls -->
<div class="no-print report-card p-6 mb-6">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<div>
<h2 id="report-viewer-title" class="text-2xl font-semibold text-gray-800">Financial Reports</h2>
<p class="text-sm text-gray-500 mt-1">View and analyze your financial performance</p>
</div>
<div class="flex items-center gap-3">
<label for="report-month" id="label-reporting-period" class="text-sm font-medium text-gray-700 whitespace-nowrap">
Reporting Period:
</label>
<input type="month" id="report-month"
class="rounded-lg border-gray-300 shadow-sm px-3 py-2 border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
<!-- Tabs Navigation -->
<div class="flex space-x-1 border-b border-gray-200 overflow-x-auto pb-0">
<button id="tab-dashboard" onclick="showView('dashboard')"
class="tab-button active">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
<span>Dashboard</span>
</div>
</button>
<button id="tab-transactions" onclick="showView('transactions')"
class="tab-button">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span>History</span>
</div>
</button>
<button id="tab-sopl" onclick="showView('sopl')"
class="tab-button">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8h6m-5 0a3 3 0 110 6H9l3 3m-3-6h6m6 1a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>P/L (SOPL)</span>
</div>
</button>
<button id="tab-sofp" onclick="showView('sofp')"
class="tab-button">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
<span>Position (SOFP)</span>
</div>
</button>
<button id="tab-socf" onclick="showView('socf')"
class="tab-button">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
</svg>
<span>Cash Flow (SOCF)</span>
</div>
</button>
</div>
</div>
<!-- Content Views -->
<div id="content-views">
<!-- Dashboard View -->
<div id="view-dashboard" class="report-view">
<div id="dashboard-content" class="space-y-6">
<!-- Loading placeholder will be replaced -->
<div class="report-card p-6">
<div class="flex items-center justify-center py-12">
<div class="text-center">
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-indigo-600 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-700">Loading Dashboard</h3>
<p class="text-sm text-gray-500 mt-1">Please wait while we prepare your financial insights...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Transactions View -->
<div id="view-transactions" class="report-view hidden">
<div class="report-card">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<h3 id="transactions-header" class="text-xl font-semibold text-gray-800">Transaction History</h3>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500" id="transaction-count">0 transactions</span>
<button onclick="exportTransactions()" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Export CSV
</button>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<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">Date</th>
<th id="th-description" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
<th id="th-category" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
<th id="th-amount" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Amount (RM)</th>
<th id="th-actions" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody id="transactions-list" class="bg-white divide-y divide-gray-200">
<tr>
<td colspan="5" class="px-6 py-12 text-center">
<div class="flex flex-col items-center justify-center">
<svg class="w-12 h-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h4 class="text-lg font-medium text-gray-700 mb-1">No transactions yet</h4>
<p class="text-sm text-gray-500">Start by recording your first transaction</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- SOPL View -->
<div id="view-sopl" class="report-view hidden">
<div id="sopl-placeholder" class="report-card p-12 text-center">
<div class="max-w-md mx-auto">
<svg class="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 8h6m-5 0a3 3 0 110 6H9l3 3m-3-6h6m6 1a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-700 mb-2">Statement of Profit or Loss</h3>
<p class="text-sm text-gray-500">Select a reporting period and record transactions to generate the SOPL report</p>
</div>
</div>
</div>
<!-- SOFP View -->
<div id="view-sofp" class="report-view hidden">
<div id="sofp-placeholder" class="report-card p-12 text-center">
<div class="max-w-md mx-auto">
<svg class="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-700 mb-2">Statement of Financial Position</h3>
<p class="text-sm text-gray-500">Your balance sheet will appear here after recording transactions</p>
</div>
</div>
</div>
<!-- SOCF View -->
<div id="view-socf" class="report-view hidden">
<div id="socf-placeholder" class="report-card p-12 text-center">
<div class="max-w-md mx-auto">
<svg class="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
</svg>
<h3 class="text-lg font-medium text-gray-700 mb-2">Statement of Cash Flow</h3>
<p class="text-sm text-gray-500">Cash flow statement will be generated based on your transactions</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-12 pt-8 border-t border-gray-200 no-print">
<div class="text-center text-sm text-gray-500">
<p>SmartBook AI • Simplified Financial Reporting for MASB Compliance</p>
<p class="mt-1">Data is stored locally in your browser • <button onclick="clearAllData()" class="text-red-500 hover:text-red-700 underline">Clear All Data</button></p>
</div>
</footer>
</div>
<!-- Language Selection Modal -->
<div id="language-modal" class="modal-overlay hidden">
<div class="modal-content">
<div class="text-center mb-6">
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-800 mb-2">Choose Language / Pilih Bahasa</h2>
<p class="text-gray-600">Select your preferred language to start using SmartBook AI</p>
</div>
<div class="space-y-3">
<button onclick="selectLanguageAndStart('en')"
class="w-full py-4 px-4 border-2 border-indigo-600 rounded-xl 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 flex items-center justify-center gap-3">
<span>🇬🇧</span>
<span>English</span>
</button>
<button onclick="selectLanguageAndStart('ml')"
class="w-full py-4 px-4 border-2 border-gray-300 rounded-xl text-lg font-semibold text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400 transition duration-150 flex items-center justify-center gap-3">
<span>🇲🇾</span>
<span>Bahasa Melayu</span>
</button>
</div>
<p class="text-xs text-gray-400 text-center mt-6">Your choice will be saved for future visits</p>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirm-modal" class="modal-overlay hidden">
<div class="modal-content">
<div class="text-center mb-6">
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.998-.833-2.732 0L4.346 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<h2 class="text-xl font-bold text-gray-800 mb-2" id="confirm-title">Confirm Action</h2>
<p class="text-gray-600" id="confirm-message">Are you sure you want to proceed?</p>
</div>
<div class="flex gap-3">
<button onclick="hideConfirmModal()"
class="flex-1 py-3 px-4 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition duration-150">
Cancel
</button>
<button onclick="confirmAction()"
class="flex-1 py-3 px-4 border border-transparent rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150">
Confirm
</button>
</div>
</div>
</div>
<script>
// ====================== GLOBAL VARIABLES ======================
let allTransactions = [];
let editingTransactionId = null;
let currentView = 'dashboard';
let chartInstance = null;
let currentLanguage = 'en';
let pendingAction = null;
// ====================== LANGUAGE PACKS ======================
const L_EN = {
// App
title: "SmartBook AI",
subtitle: "MASB Simplified Financial Reporting (Cash Basis)",
// Form
form_title: "Record Transaction",
label_date: "Date",
label_description: "Description",
label_category: "Category",
label_amount: "Amount (RM)",
record_button: "📝 Record Transaction",
update_button: "💾 Update Transaction",
cancel_button: "❌ Cancel Edit",
// Reports
report_viewer_title: "Financial Reports",
label_reporting_period: "Reporting Period:",
// Tabs
tab_transactions: "History",
tab_sopl: "P/L (SOPL)",
tab_sofp: "Position (SOFP)",
tab_socf: "Cash Flow (SOCF)",
tab_dashboard: "Dashboard",
// Table headers
th_date: "Date",
th_description: "Description",
th_category: "Category",
th_amount: "Amount (RM)",
th_actions: "Actions",
// Actions
edit: "Edit",
delete: "Delete",
// Messages
list_header: "Transaction History",
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...",
status_saving: "Saving transaction...",
status_updating: "Updating transaction...",
status_success: "✅ Transaction recorded successfully!",
status_update_success: "✅ Transaction updated successfully!",
status_delete_success: "✅ Transaction deleted successfully.",
status_edit_id: (id) => `Editing transaction #${id.substring(0, 6)}...`,
// Report titles
sopl_title: "Statement of Profit or Loss (SOPL)",
sofp_title: "Statement of Financial Position (SOFP)",
socf_title: "Statement of Cash Flow (SOCF)",
// Categories (keep as is for internal use)
cat_sales: "Sales Revenue",
cat_interest: "Interest Received",
cat_capital: "Capital Injection",
cat_loan: "Loan Received",
cat_rent: "Rent Expense",
cat_utilities: "Utilities Expense",
cat_wages: "Wages & Salaries",
cat_supplies: "Supplies & Consumables",
cat_other_op: "Other Operating Expense",
cat_equipment: "Equipment Purchase",
cat_drawings: "Drawings",
};
const L_ML = {
// App
title: "SmartBook AI",
subtitle: "Pelaporan Kewangan Ringkas MASB (Asas Tunai)",
// Form
form_title: "Rekod Transaksi",
label_date: "Tarikh",
label_description: "Huraian",
label_category: "Kategori",
label_amount: "Jumlah (RM)",
record_button: "📝 Rekod Transaksi",
update_button: "💾 Kemaskini Transaksi",
cancel_button: "❌ Batal Suntingan",
// Reports
report_viewer_title: "Laporan Kewangan",
label_reporting_period: "Tempoh Pelaporan:",
// Tabs
tab_transactions: "Sejarah",
tab_sopl: "U/R (SOPL)",
tab_sofp: "Kedudukan (SOFP)",
tab_socf: "Aliran Tunai (SOCF)",
tab_dashboard: "Papan Pemuka",
// Table headers
th_date: "Tarikh",
th_description: "Huraian",
th_category: "Kategori",
th_amount: "Jumlah (RM)",
th_actions: "Tindakan",
// Actions
edit: "Sunting",
delete: "Padam",
// Messages
list_header: "Sejarah Transaksi",
no_trans: "Tiada transaksi direkodkan lagi.",
no_data_msg: "Tiada transaksi untuk tempoh ini.",
no_month_msg: "Sila pilih tempoh pelaporan.",
loading: "Memuatkan...",
status_saving: "Menyimpan transaksi...",
status_updating: "Mengemaskini transaksi...",
status_success: "✅ Transaksi berjaya direkodkan!",
status_update_success: "✅ Transaksi berjaya dikemaskini!",
status_delete_success: "✅ Transaksi berjaya dipadam.",
status_edit_id: (id) => `Menyunting transaksi #${id.substring(0, 6)}...`,
// Report titles
sopl_title: "Penyata Untung Rugi (SOPL)",
sofp_title: "Penyata Kedudukan Kewangan (SOFP)",
socf_title: "Penyata Aliran Tunai (SOCF)",
// Categories
cat_sales: "Hasil Jualan",
cat_interest: "Faedah Diterima",
cat_capital: "Suntikan Modal",
cat_loan: "Pinjaman Diterima",
cat_rent: "Sewa Dibayar",
cat_utilities: "Utiliti Dibayar",
cat_wages: "Gaji & Upah",
cat_supplies: "Bekalan & Bahan",
cat_other_op: "Perbelanjaan Operasi Lain",
cat_equipment: "Pembelian Peralatan",
cat_drawings: "Pengeluaran Pemilik",
};
let L = L_EN;
// ====================== ACCOUNTING CONFIGURATION ======================
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" },
};
// ====================== UTILITY FUNCTIONS ======================
function formatCurrency(amount) {
return new Intl.NumberFormat('en-MY', { style: 'currency', currency: 'MYR' }).format(amount);
}
function showStatus(message, type = 'success') {
const statusEl = document.getElementById('status-message');
statusEl.textContent = message;
statusEl.className = `text-sm text-center py-2 rounded-lg ${type === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`;
statusEl.classList.remove('hidden');
if (type === 'success') {
setTimeout(() => statusEl.classList.add('hidden'), 3000);
}
}
function formatMonthYear(yyyyMm) {
if (!yyyyMm) return '';
const [year, month] = yyyyMm.split('-');
const date = new Date(year, month - 1, 1);
return date.toLocaleDateString(currentLanguage === 'ml' ? 'ms-MY' : 'en-US', {
year: 'numeric',
month: 'long'
});
}
// ====================== DATA MANAGEMENT ======================
function loadTransactions() {
const stored = localStorage.getItem('smartbook_transactions_v2');
allTransactions = stored ? JSON.parse(stored) : [];
updateTransactionStats();
renderTransactionList();
// Auto-select current month if not selected
const monthInput = document.getElementById('report-month');
if (!monthInput.value) {
const today = new Date();
const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, '0');
monthInput.value = `${yyyy}-${mm}`;
}
updateReportsView();
}
function saveTransactions() {
localStorage.setItem('smartbook_transactions_v2', JSON.stringify(allTransactions));
updateTransactionStats();
}
function updateTransactionStats() {
document.getElementById('total-transactions').textContent = allTransactions.length;
const totalAmount = allTransactions.reduce((sum, t) => sum + parseFloat(t.amount), 0);
document.getElementById('total-amount').textContent = formatCurrency(totalAmount);
document.getElementById('transaction-count').textContent = `${allTransactions.length} transactions`;
}
// ====================== TRANSACTION CRUD ======================
function editTransaction(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');
showStatus(L.status_edit_id(editingTransactionId), 'info');
// Scroll to form
document.getElementById('transaction-form').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
editingTransactionId = null;
document.getElementById('transaction-form').reset();
document.getElementById('date').valueAsDate = new Date();
document.getElementById('transaction-button').textContent = L.record_button;
document.getElementById('cancel-edit-button').classList.add('hidden');
document.getElementById('status-message').classList.add('hidden');
}
function deleteTransaction(id) {
showConfirmModal(
'Delete Transaction',
'Are you sure you want to delete this transaction? This action cannot be undone.',
() => {
allTransactions = allTransactions.filter(t => t.id !== id);
saveTransactions();
loadTransactions();
showStatus(L.status_delete_success);
if (editingTransactionId === id) {
resetForm();
}
}
);
}
// ====================== FORM HANDLING ======================
document.getElementById('transaction-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = {
id: editingTransactionId || Date.now().toString(),
date: document.getElementById('date').value,
description: document.getElementById('description').value,
category: document.getElementById('category').value,
amount: parseFloat(document.getElementById('amount').value)
};
// Validation
if (!formData.date || !formData.description || !formData.category || !formData.amount) {
showStatus('Please fill in all fields', 'error');
return;
}
if (editingTransactionId) {
// Update existing
const index = allTransactions.findIndex(t => t.id === editingTransactionId);
if (index !== -1) {
allTransactions[index] = formData;
showStatus(L.status_update_success);
}
} else {
// Add new
allTransactions.push(formData);
showStatus(L.status_success);
}
saveTransactions();
loadTransactions();
resetForm();
});
// ====================== RENDER FUNCTIONS ======================
function renderTransactionList() {
const tbody = document.getElementById('transactions-list');
if (allTransactions.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="px-6 py-12 text-center">
<div class="flex flex-col items-center justify-center">
<svg class="w-12 h-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h4 class="text-lg font-medium text-gray-700 mb-1">${L.no_trans}</h4>
<p class="text-sm text-gray-500">Start by recording your first transaction</p>
</div>
</td>
</tr>
`;
return;
}
// Sort by date (newest first)
const sorted = [...allTransactions].sort((a, b) => new Date(b.date) - new Date(a.date));
let html = '';
sorted.forEach((transaction, index) => {
const categoryText = L[`cat_${transaction.category.toLowerCase().replace(/ /g, '_')}`] || transaction.category;
const amountClass = transaction.category.includes('Expense') || transaction.category === 'Drawings'
? 'text-red-600'
: 'text-green-600';
html += `
<tr class="${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'} hover:bg-gray-100 transition">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${transaction.date}</td>
<td class="px-6 py-4 text-sm text-gray-900 max-w-xs truncate" title="${transaction.description}">${transaction.description}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${transaction.category.includes('Expense') ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'}">
${categoryText}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-right ${amountClass}">
${formatCurrency(transaction.amount)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-center">
<button onclick="editTransaction(${JSON.stringify(transaction).replace(/"/g, '&quot;')})"
class="text-indigo-600 hover:text-indigo-900 font-medium mr-4 inline-flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
${L.edit}
</button>
<button onclick="deleteTransaction('${transaction.id}')"
class="text-red-600 hover:text-red-900 font-medium inline-flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
${L.delete}
</button>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
// ====================== REPORT CALCULATIONS ======================
function calculateReports() {
const monthInput = document.getElementById('report-month').value;
if (!monthInput) return null;
// Filter transactions for selected month
const monthlyTransactions = allTransactions.filter(t => t.date.startsWith(monthInput));
const cumulativeTransactions = allTransactions.filter(t => t.date <= `${monthInput}-31`);
// Calculate SOPL (Profit & Loss)
const sopl = { revenue: 0, expenses: 0 };
monthlyTransactions.forEach(t => {
const map = ACCOUNT_MAP[t.category];
if (!map || map.report !== 'SOPL') return;
const amount = parseFloat(t.amount) * map.effect;
if (amount > 0) {
sopl.revenue += amount;
} else {
sopl.expenses += Math.abs(amount);
}
});
sopl.netProfit = sopl.revenue - sopl.expenses;
// Calculate SOFP (Balance Sheet)
const sofp = { assets: 0, liabilities: 0, equity: 0 };
cumulativeTransactions.forEach(t => {
const map = ACCOUNT_MAP[t.category];
if (!map || map.report !== 'SOFP') return;
const amount = parseFloat(t.amount);
if (map.lineItem.includes('Assets')) {
sofp.assets += amount;
} else if (map.lineItem.includes('Liabilities')) {
sofp.liabilities += amount;
} else if (map.lineItem.includes('Equity')) {
sofp.equity += (amount * map.effect);
}
});
// Add cash (simplified calculation)
sofp.assets += sopl.netProfit + sofp.equity - sofp.liabilities;
// Calculate SOCF (Cash Flow)
const socf = { operating: 0, investing: 0, financing: 0 };
monthlyTransactions.forEach(t => {
const map = ACCOUNT_MAP[t.category];
if (!map || !map.flow) return;
const amount = parseFloat(t.amount);
let flowAmount = amount;
if (map.effect === -1 || map.account === 'Equipment') {
flowAmount = -amount;
} else if (map.account === 'Owner\'s Drawings') {
flowAmount = -amount;
}
socf[map.flow.toLowerCase()] += flowAmount;
});
return {
period: formatMonthYear(monthInput),
sopl,
sofp,
socf,
monthlyTransactions,
cumulativeTransactions
};
}
// ====================== VIEW MANAGEMENT ======================
function showView(viewId) {
currentView = viewId;
// Update tabs
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.getElementById(`tab-${viewId}`).classList.add('active');
// Show selected view
document.querySelectorAll('.report-view').forEach(view => view.classList.add('hidden'));
document.getElementById(`view-${viewId}`).classList.remove('hidden');
// Load data for the view
if (viewId === 'dashboard') {
renderDashboard();
} else if (viewId === 'transactions') {
renderTransactionList();
} else {
updateReportsView();
}
}
function updateReportsView() {
const reports = calculateReports();
if (!reports) return;
if (currentView === 'sopl') {
renderSOPL(reports);
} else if (currentView === 'sofp') {
renderSOFP(reports);
} else if (currentView === 'socf') {
renderSOCF(reports);
} else if (currentView === 'dashboard') {
renderDashboard();
}
}
// ====================== REPORT RENDERING ======================
function renderSOPL(reports) {
const { sopl, period } = reports;
const html = `
<div class="report-card">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<div>
<h3 class="text-2xl font-bold text-gray-800">${L.sopl_title}</h3>
<p class="text-sm text-gray-500 mt-1">For the month of ${period}</p>
</div>
<button onclick="generatePDF('sopl')" class="btn-primary px-4 py-2 text-sm">
📄 Download PDF
</button>
</div>
</div>
<div class="p-6">
<table class="w-full report-table">
<tbody>
<tr class="bg-indigo-50">
<td colspan="3" class="px-6 py-3 font-semibold text-indigo-700">REVENUE</td>
</tr>
<tr>
<td class="px-6 py-3">Sales Revenue</td>
<td></td>
<td class="px-6 py-3 text-right font-medium">${formatCurrency(sopl.revenue)}</td>
</tr>
<tr class="border-t">
<td class="px-6 py-3 font-semibold">Total Revenue</td>
<td></td>
<td class="px-6 py-3 text-right font-bold">${formatCurrency(sopl.revenue)}</td>
</tr>
<tr class="bg-red-50">
<td colspan="3" class="px-6 py-3 font-semibold text-red-700 mt-4">EXPENSES</td>
</tr>
<tr>
<td class="px-6 py-3">Operating Expenses</td>
<td class="px-6 py-3 text-right">${formatCurrency(sopl.expenses)}</td>
<td></td>
</tr>
<tr class="border-t">
<td class="px-6 py-3 font-semibold">Total Expenses</td>
<td class="px-6 py-3 text-right font-bold">${formatCurrency(sopl.expenses)}</td>
<td></td>
</tr>
<tr class="border-t-2 border-b-2 bg-gray-100">
<td class="px-6 py-4 font-bold text-lg ${sopl.netProfit >= 0 ? 'text-green-700' : 'text-red-700'}">NET PROFIT / (LOSS)</td>
<td></td>
<td class="px-6 py-4 text-right font-bold text-lg ${sopl.netProfit >= 0 ? 'text-green-700' : 'text-red-700'}">
${formatCurrency(sopl.netProfit)}
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
document.getElementById('view-sopl').innerHTML = html;
}
function renderSOFP(reports) {
const { sofp, period } = reports;
const totalEquity = sofp.equity + reports.sopl.netProfit;
const totalLiabilityEquity = sofp.liabilities + totalEquity;
const balanced = Math.abs(sofp.assets - totalLiabilityEquity) < 0.01;
const html = `
<div class="report-card">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<div>
<h3 class="text-2xl font-bold text-gray-800">${L.sofp_title}</h3>
<p class="text-sm text-gray-500 mt-1">As of ${period}</p>
</div>
<button onclick="generatePDF('sofp')" class="btn-primary px-4 py-2 text-sm">
📄 Download PDF
</button>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Assets -->
<div>
<h4 class="text-lg font-semibold text-indigo-700 mb-4">ASSETS</h4>
<div class="space-y-3">
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span class="font-medium">Current Assets</span>
<span class="font-semibold">${formatCurrency(sofp.assets * 0.6)}</span>
</div>
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span class="font-medium">Non-current Assets</span>
<span class="font-semibold">${formatCurrency(sofp.assets * 0.4)}</span>
</div>
<div class="flex justify-between items-center p-4 bg-indigo-50 rounded-lg border border-indigo-200">
<span class="font-bold">TOTAL ASSETS</span>
<span class="font-bold text-lg">${formatCurrency(sofp.assets)}</span>
</div>
</div>
</div>
<!-- Liabilities & Equity -->
<div>
<h4 class="text-lg font-semibold text-red-700 mb-4">LIABILITIES & EQUITY</h4>
<div class="space-y-3">
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span class="font-medium">Liabilities</span>
<span class="font-semibold">${formatCurrency(sofp.liabilities)}</span>
</div>
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span class="font-medium">Owner's Equity</span>
<span class="font-semibold">${formatCurrency(sofp.equity)}</span>
</div>
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span class="font-medium">Retained Earnings</span>
<span class="font-semibold">${formatCurrency(reports.sopl.netProfit)}</span>
</div>
<div class="flex justify-between items-center p-4 bg-red-50 rounded-lg border border-red-200">
<span class="font-bold">TOTAL LIABILITIES & EQUITY</span>
<span class="font-bold text-lg">${formatCurrency(totalLiabilityEquity)}</span>
</div>
</div>
</div>
</div>
<!-- Balance Check -->
<div class="mt-8 p-4 rounded-lg ${balanced ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full ${balanced ? 'bg-green-100' : 'bg-red-100'} flex items-center justify-center">
<svg class="w-4 h-4 ${balanced ? 'text-green-600' : 'text-red-600'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${balanced ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>' :
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>'
}
</svg>
</div>
<span class="font-semibold ${balanced ? 'text-green-700' : 'text-red-700'}">
${balanced ? '✓ Accounting Equation Balanced' : '⚠ Accounting Equation Unbalanced'}
</span>
</div>
<span class="text-sm ${balanced ? 'text-green-600' : 'text-red-600'}">
Assets = Liabilities + Equity
</span>
</div>
</div>
</div>
</div>
`;
document.getElementById('view-sofp').innerHTML = html;
}
function renderSOCF(reports) {
const { socf, period } = reports;
const netCashFlow = socf.operating + socf.investing + socf.financing;
const html = `
<div class="report-card">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<div>
<h3 class="text-2xl font-bold text-gray-800">${L.socf_title}</h3>
<p class="text-sm text-gray-500 mt-1">For the month of ${period}</p>
</div>
<button onclick="generatePDF('socf')" class="btn-primary px-4 py-2 text-sm">
📄 Download PDF
</button>
</div>
</div>
<div class="p-6">
<table class="w-full report-table">
<tbody>
<tr class="bg-blue-50">
<td colspan="2" class="px-6 py-3 font-semibold text-blue-700">CASH FLOW FROM OPERATING ACTIVITIES</td>
</tr>
<tr>
<td class="px-6 py-3">Net cash from operations</td>
<td class="px-6 py-3 text-right font-medium ${socf.operating >= 0 ? 'text-green-600' : 'text-red-600'}">
${formatCurrency(socf.operating)}
</td>
</tr>
<tr class="bg-blue-50">
<td colspan="2" class="px-6 py-3 font-semibold text-blue-700 mt-4">CASH FLOW FROM INVESTING ACTIVITIES</td>
</tr>
<tr>
<td class="px-6 py-3">Net cash from investing</td>
<td class="px-6 py-3 text-right font-medium ${socf.investing >= 0 ? 'text-green-600' : 'text-red-600'}">
${formatCurrency(socf.investing)}
</td>
</tr>
<tr class="bg-blue-50">
<td colspan="2" class="px-6 py-3 font-semibold text-blue-700 mt-4">CASH FLOW FROM FINANCING ACTIVITIES</td>
</tr>
<tr>
<td class="px-6 py-3">Net cash from financing</td>
<td class="px-6 py-3 text-right font-medium ${socf.financing >= 0 ? 'text-green-600' : 'text-red-600'}">
${formatCurrency(socf.financing)}
</td>
</tr>
<tr class="border-t-2">
<td class="px-6 py-4 font-bold">NET INCREASE/(DECREASE) IN CASH</td>
<td class="px-6 py-4 text-right font-bold text-lg ${netCashFlow >= 0 ? 'text-green-700' : 'text-red-700'}">
${formatCurrency(netCashFlow)}
</td>
</tr>
<tr class="bg-gray-100 border-t-2 border-b-2">
<td class="px-6 py-4 font-bold text-gray-800">ENDING CASH BALANCE</td>
<td class="px-6 py-4 text-right font-bold text-lg text-gray-800">
${formatCurrency(netCashFlow)}
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
document.getElementById('view-socf').innerHTML = html;
}
function renderDashboard() {
const reports = calculateReports();
if (!reports) {
document.getElementById('view-dashboard').innerHTML = `
<div class="report-card p-12 text-center">
<p class="text-gray-500">Select a reporting period to view dashboard</p>
</div>
`;
return;
}
const { sopl, sofp } = reports;
// Calculate metrics
const profitability = sopl.revenue > 0 ? (sopl.netProfit / sopl.revenue) * 100 : 0;
const safetyScore = sofp.liabilities > 0 ? sofp.assets / sofp.liabilities : 999;
const assetEfficiency = sofp.assets > 0 ? (sopl.netProfit / sofp.assets) * 100 : 0;
// Prepare chart data
const monthlyData = getMonthlyChartData();
const html = `
<div class="space-y-6">
<!-- Key Metrics -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="metric-card">
<div class="flex items-center justify-between mb-4">
<h4 class="font-semibold text-gray-700">Profitability Score</h4>
<div class="w-10 h-10 rounded-full ${profitability >= 20 ? 'bg-green-100' : profitability >= 10 ? 'bg-yellow-100' : 'bg-red-100'} flex items-center justify-center">
<span class="text-sm font-bold ${profitability >= 20 ? 'text-green-600' : profitability >= 10 ? 'text-yellow-600' : 'text-red-600'}">
${profitability.toFixed(1)}%
</span>
</div>
</div>
<p class="text-xs text-gray-500">Percentage of revenue that turns into profit</p>
<div class="mt-3 bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full ${profitability >= 20 ? 'bg-green-500' : profitability >= 10 ? 'bg-yellow-500' : 'bg-red-500'}"
style="width: ${Math.min(profitability, 100)}%"></div>
</div>
</div>
<div class="metric-card">
<div class="flex items-center justify-between mb-4">
<h4 class="font-semibold text-gray-700">Financial Safety</h4>
<div class="w-10 h-10 rounded-full ${safetyScore >= 2 ? 'bg-green-100' : safetyScore >= 1 ? 'bg-yellow-100' : 'bg-red-100'} flex items-center justify-center">
<span class="text-sm font-bold ${safetyScore >= 2 ? 'text-green-600' : safetyScore >= 1 ? 'text-yellow-600' : 'text-red-600'}">
${safetyScore === 999 ? '∞' : safetyScore.toFixed(1)}x
</span>
</div>
</div>
<p class="text-xs text-gray-500">Assets coverage over liabilities</p>
<div class="mt-3 bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full ${safetyScore >= 2 ? 'bg-green-500' : safetyScore >= 1 ? 'bg-yellow-500' : 'bg-red-500'}"
style="width: ${Math.min(safetyScore * 10, 100)}%"></div>
</div>
</div>
<div class="metric-card">
<div class="flex items-center justify-between mb-4">
<h4 class="font-semibold text-gray-700">Asset Efficiency</h4>
<div class="w-10 h-10 rounded-full ${assetEfficiency >= 10 ? 'bg-green-100' : assetEfficiency >= 5 ? 'bg-yellow-100' : 'bg-red-100'} flex items-center justify-center">
<span class="text-sm font-bold ${assetEfficiency >= 10 ? 'text-green-600' : assetEfficiency >= 5 ? 'text-yellow-600' : 'text-red-600'}">
${assetEfficiency.toFixed(1)}%
</span>
</div>
</div>
<p class="text-xs text-gray-500">Profit generated per asset value</p>
<div class="mt-3 bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full ${assetEfficiency >= 10 ? 'bg-green-500' : assetEfficiency >= 5 ? 'bg-yellow-500' : 'bg-red-500'}"
style="width: ${Math.min(assetEfficiency * 5, 100)}%"></div>
</div>
</div>
</div>
<!-- Performance Chart -->
<div class="report-card p-6">
<div class="flex justify-between items-center mb-6">
<div>
<h3 class="text-lg font-semibold text-gray-800">Monthly Performance Trend</h3>
<p class="text-sm text-gray-500">Revenue vs Expenses over time</p>
</div>
</div>
<div class="h-64">
<canvas id="performanceChart"></canvas>
</div>
</div>
<!-- Quick Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-white p-4 rounded-lg border border-gray-200 text-center">
<div class="text-2xl font-bold text-indigo-600">${formatCurrency(sopl.revenue)}</div>
<div class="text-sm text-gray-500 mt-1">Total Revenue</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200 text-center">
<div class="text-2xl font-bold text-red-600">${formatCurrency(sopl.expenses)}</div>
<div class="text-sm text-gray-500 mt-1">Total Expenses</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200 text-center">
<div class="text-2xl font-bold ${sopl.netProfit >= 0 ? 'text-green-600' : 'text-red-600'}">
${formatCurrency(sopl.netProfit)}
</div>
<div class="text-sm text-gray-500 mt-1">Net Profit</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200 text-center">
<div class="text-2xl font-bold text-blue-600">${formatCurrency(sofp.assets)}</div>
<div class="text-sm text-gray-500 mt-1">Total Assets</div>
</div>
</div>
</div>
`;
document.getElementById('view-dashboard').innerHTML = html;
// Render chart
setTimeout(() => {
renderPerformanceChart(monthlyData);
}, 100);
}
function getMonthlyChartData() {
// Group transactions by month
const monthlyData = {};
allTransactions.forEach(t => {
const month = t.date.substring(0, 7);
if (!monthlyData[month]) {
monthlyData[month] = { revenue: 0, expenses: 0 };
}
const map = ACCOUNT_MAP[t.category];
if (!map || map.report !== 'SOPL') return;
const amount = parseFloat(t.amount) * map.effect;
if (amount > 0) {
monthlyData[month].revenue += amount;
} else {
monthlyData[month].expenses += Math.abs(amount);
}
});
// Sort months and prepare chart data
const sortedMonths = Object.keys(monthlyData).sort();
return {
labels: sortedMonths.map(m => formatMonthYear(m)),
revenue: sortedMonths.map(m => monthlyData[m].revenue),
expenses: sortedMonths.map(m => monthlyData[m].expenses),
profit: sortedMonths.map(m => monthlyData[m].revenue - monthlyData[m].expenses)
};
}
function renderPerformanceChart(data) {
if (chartInstance) {
chartInstance.destroy();
}
const ctx = document.getElementById('performanceChart');
if (!ctx) return;
chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{
label: 'Revenue',
data: data.revenue,
backgroundColor: 'rgba(79, 70, 229, 0.7)',
borderColor: 'rgb(79, 70, 229)',
borderWidth: 1
},
{
label: 'Expenses',
data: data.expenses,
backgroundColor: 'rgba(239, 68, 68, 0.7)',
borderColor: 'rgb(239, 68, 68)',
borderWidth: 1
},
{
type: 'line',
label: 'Net Profit',
data: data.profit,
backgroundColor: 'rgba(16, 185, 129, 0.9)',
borderColor: 'rgb(16, 185, 129)',
borderWidth: 2,
fill: false,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return 'RM ' + value.toLocaleString();
}
}
}
}
}
});
}
// ====================== PDF GENERATION ======================
function generatePDF(reportType) {
const element = document.getElementById(`view-${reportType}`).firstElementChild;
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
html2canvas(element).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const pdfWidth = doc.internal.pageSize.getWidth();
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
doc.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
doc.save(`SmartBook-${reportType.toUpperCase()}-${new Date().toISOString().slice(0,10)}.pdf`);
showStatus(`PDF downloaded successfully!`);
});
}
// ====================== MODAL FUNCTIONS ======================
function showConfirmModal(title, message, callback) {
document.getElementById('confirm-title').textContent = title;
document.getElementById('confirm-message').textContent = message;
pendingAction = callback;
document.getElementById('confirm-modal').classList.remove('hidden');
}
function hideConfirmModal() {
document.getElementById('confirm-modal').classList.add('hidden');
pendingAction = null;
}
function confirmAction() {
if (pendingAction) {
pendingAction();
}
hideConfirmModal();
}
function clearAllData() {
showConfirmModal(
'Clear All Data',
'Are you sure you want to delete ALL transactions? This action cannot be undone.',
() => {
allTransactions = [];
localStorage.removeItem('smartbook_transactions_v2');
loadTransactions();
showStatus('All data has been cleared');
}
);
}
// ====================== LANGUAGE FUNCTIONS ======================
function setLanguage(lang) {
currentLanguage = lang;
L = lang === 'ml' ? L_ML : L_EN;
localStorage.setItem('smartbook_language', lang);
// Update UI elements
document.getElementById('language-select').value = lang;
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 = editingTransactionId ? L.update_button : L.record_button;
document.getElementById('cancel-edit-button').textContent = L.cancel_button;
document.getElementById('report-viewer-title').textContent = L.report_viewer_title;
document.getElementById('label-reporting-period').textContent = L.label_reporting_period;
document.getElementById('tab-transactions').querySelector('span').textContent = L.tab_transactions;
document.getElementById('tab-sopl').querySelector('span').textContent = L.tab_sopl;
document.getElementById('tab-sofp').querySelector('span').textContent = L.tab_sofp;
document.getElementById('tab-socf').querySelector('span').textContent = L.tab_socf;
document.getElementById('tab-dashboard').querySelector('span').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;
// Update form category labels
const categorySelect = document.getElementById('category');
const currentValue = categorySelect.value;
// Update category options with new language
const optgroups = categorySelect.querySelectorAll('optgroup');
optgroups[0].label = currentLanguage === 'ml' ? "Pendapatan & Pembiayaan" : "Income & Financing";
optgroups[1].label = currentLanguage === 'ml' ? "Perbelanjaan Operasi" : "Operating Expenses";
optgroups[2].label = currentLanguage === 'ml' ? "Pelaburan & Pengeluaran" : "Investment & Drawings";
// Update option texts
const options = categorySelect.querySelectorAll('option');
options[1].textContent = currentLanguage === 'ml' ? "Hasil Jualan (Perkhidmatan/Barangan)" : "Sales Revenue (Service/Goods)";
options[2].textContent = currentLanguage === 'ml' ? "Faedah Diterima" : "Interest Received";
options[3].textContent = currentLanguage === 'ml' ? "Modal Pemilik/Pelabur" : "Owner/Investor Capital";
options[4].textContent = currentLanguage === 'ml' ? "Pinjaman Bank / Pembiayaan" : "Bank Loan / Financing";
options[6].textContent = currentLanguage === 'ml' ? "Sewa Dibayar" : "Rent Expense";
options[7].textContent = currentLanguage === 'ml' ? "Utiliti (Elektrik, Air)" : "Utilities (Electric, Water)";
options[8].textContent = currentLanguage === 'ml' ? "Gaji & Upah" : "Wages & Salaries";
options[9].textContent = currentLanguage === 'ml' ? "Bekalan & Bahan Habis Guna" : "Supplies & Consumables";
options[10].textContent = currentLanguage === 'ml' ? "Perbelanjaan Operasi Lain" : "Other Operating Expense";
options[12].textContent = currentLanguage === 'ml' ? "Pembelian Peralatan (Aset)" : "Equipment Purchase (Asset)";
options[13].textContent = currentLanguage === 'ml' ? "Pengeluaran Pemilik" : "Owner Drawings / Withdrawal";
// Restore selected value
categorySelect.value = currentValue;
// Update content
renderTransactionList();
updateReportsView();
}
function selectLanguageAndStart(lang) {
setLanguage(lang);
document.getElementById('language-modal').classList.add('hidden');
// Initialize app
const today = new Date();
const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, '0');
document.getElementById('report-month').value = `${yyyy}-${mm}`;
loadTransactions();
showView('dashboard');
}
// ====================== EXPORT FUNCTION ======================
function exportTransactions() {
if (allTransactions.length === 0) {
showStatus('No transactions to export', 'error');
return;
}
let csv = 'Date,Description,Category,Amount(RM)\n';
allTransactions.forEach(t => {
csv += `${t.date},"${t.description}",${t.category},${t.amount}\n`;
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `SmartBook-Transactions-${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
showStatus('CSV exported successfully!');
}
// ====================== INITIALIZATION ======================
document.addEventListener('DOMContentLoaded', function() {
// Set default date
const today = new Date();
document.getElementById('date').valueAsDate = today;
// Check for saved language
const savedLang = localStorage.getItem('smartbook_language');
if (savedLang) {
setLanguage(savedLang);
loadTransactions();
showView('dashboard');
} else {
// Show language modal
document.getElementById('language-modal').classList.remove('hidden');
}
// Event listeners
document.getElementById('report-month').addEventListener('change', function() {
updateReportsView();
if (currentView === 'dashboard') {
renderDashboard();
}
});
document.getElementById('language-select').addEventListener('change', function(e) {
setLanguage(e.target.value);
});
// Initialize with sample data if empty (for demo)
if (!localStorage.getItem('smartbook_transactions_v2') && allTransactions.length === 0) {
// Add sample transactions for demo
const sampleDate = new Date();
const sampleTransactions = [
{
id: '1',
date: sampleDate.toISOString().split('T')[0],
description: 'Sales to Customer A',
category: 'Sales Revenue',
amount: 5000
},
{
id: '2',
date: sampleDate.toISOString().split('T')[0],
description: 'Office Rent Payment',
category: 'Rent Expense',
amount: 1500
},
{
id: '3',
date: new Date(sampleDate.setDate(sampleDate.getDate() - 7)).toISOString().split('T')[0],
description: 'Utility Bills',
category: 'Utilities Expense',
amount: 300
}
];
localStorage.setItem('smartbook_transactions_v2', JSON.stringify(sampleTransactions));
loadTransactions();
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment