Last active
January 25, 2026 14:13
-
-
Save tsaikienhung06-cmd/c72342ce75899377037535dfdc29f313 to your computer and use it in GitHub Desktop.
SmartBook AI - Financial Reporting System
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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, '"')})" | |
| 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