diff --git a/package-lock.json b/package-lock.json index cec640f..97df709 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "jaiben-ui", "version": "0.1.0", "dependencies": { + "date-fns": "^4.1.0", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", "lucide-react": "^1.8.0", "next": "16.2.4", "react": "19.2.4", @@ -229,6 +232,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1570,6 +1582,19 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -1590,6 +1615,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", @@ -2447,6 +2479,16 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.20", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", @@ -2597,6 +2639,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2654,6 +2716,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2669,6 +2743,16 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2737,6 +2821,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2821,6 +2915,16 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3521,6 +3625,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3531,6 +3646,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3916,6 +4037,20 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3968,6 +4103,12 @@ "node": ">= 0.4" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4492,6 +4633,32 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz", + "integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2 || ^3 || ^4" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -5281,6 +5448,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5321,6 +5494,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5432,6 +5612,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5483,6 +5673,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -5559,6 +5756,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5876,6 +6083,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6075,6 +6292,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", @@ -6096,6 +6323,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6433,6 +6670,16 @@ "punycode": "^2.1.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 5e649f2..a8b2269 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "lint": "eslint" }, "dependencies": { + "date-fns": "^4.1.0", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", "lucide-react": "^1.8.0", "next": "16.2.4", "react": "19.2.4", diff --git a/src/app/admin/accounting/page.tsx b/src/app/admin/accounting/page.tsx new file mode 100644 index 0000000..784860e --- /dev/null +++ b/src/app/admin/accounting/page.tsx @@ -0,0 +1,1319 @@ +'use client'; + +import { useState, useMemo, useEffect, useRef } from 'react'; +import { + Plus, Search, Download, Eye, Edit, Trash2, X, TrendingUp, TrendingDown, + DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building, + ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List, + Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet, + Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike +} from 'lucide-react'; + +export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense'; + +export type TransactionType = + | 'deposit' + | 'rent_income' + | 'investor_funding' + | 'investor_withdraw' + | 'salary' + | 'rent_expense' + | 'utility' + | 'maintenance' + | 'bike_purchase' + | 'bike_sale' + | 'other_income' + | 'other_expense'; + +export interface ChartOfAccount { + id: string; + code: string; + name: string; + type: AccountType; + parentId?: string; + isActive: boolean; + balance: number; +} + +export interface JournalEntry { + id: string; + date: string; + reference: string; + description: string; + entries: { + accountId: string; + accountCode: string; + accountName: string; + debit: number; + credit: number; + }[]; + isPosted: boolean; + isAuto: boolean; + sourceType?: TransactionType; + createdAt: string; + createdBy: string; +} + +export interface AccountingTransaction { + id: string; + date: string; + type: TransactionType; + amount: number; + description: string; + beneficiary?: string; + beneficiaryPhone?: string; + paymentMethod: 'cash' | 'bank' | 'mobile'; + reference?: string; + notes?: string; + bikeId?: string; + bikeDetails?: string; + createdAt: string; + createdBy: string; +} + +const defaultAccounts: ChartOfAccount[] = [ + { id: 'ASSET-001', code: '1000', name: 'Assets', type: 'asset', isActive: true, balance: 0 }, + { id: 'ASSET-101', code: '1100', name: 'Cash in Hand', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 85000 }, + { id: 'ASSET-102', code: '1200', name: 'Bank - City Bank', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 125000 }, + { id: 'ASSET-103', code: '1300', name: 'bKash Business', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 28000 }, + { id: 'ASSET-104', code: '1400', name: 'Accounts Receivable', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 15000 }, + { id: 'ASSET-104A', code: '1410', name: 'Cheque Receivable', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 0 }, + { id: 'ASSET-105', code: '1500', name: 'Bikes & Equipment', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 2500000 }, + { id: 'ASSET-106', code: '1600', name: 'Bike Inventory', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 0 }, + + { id: 'LIAB-001', code: '2000', name: 'Liabilities', type: 'liability', isActive: true, balance: 0 }, + { id: 'LIAB-201', code: '2100', name: 'Accounts Payable', type: 'liability', parentId: 'LIAB-001', isActive: true, balance: 5000 }, + { id: 'LIAB-202', code: '2200', name: 'Investor Liabilities', type: 'liability', parentId: 'LIAB-001', isActive: true, balance: 500000 }, + { id: 'LIAB-203', code: '2300', name: 'Bike Supplier Payable', type: 'liability', parentId: 'LIAB-001', isActive: true, balance: 0 }, + + { id: 'EQUITY-001', code: '3000', name: 'Equity', type: 'equity', isActive: true, balance: 0 }, + { id: 'EQUITY-301', code: '3100', name: 'Owner Equity', type: 'equity', parentId: 'EQUITY-001', isActive: true, balance: 2000000 }, + { id: 'EQUITY-302', code: '3200', name: 'Retained Earnings', type: 'equity', parentId: 'EQUITY-001', isActive: true, balance: 198000 }, + + { id: 'INC-001', code: '4000', name: 'Income', type: 'income', isActive: true, balance: 0 }, + { id: 'INC-401', code: '4100', name: 'Bike Deposit Income', type: 'income', parentId: 'INC-001', isActive: true, balance: 50000 }, + { id: 'INC-402', code: '4200', name: 'Rental Income', type: 'income', parentId: 'INC-001', isActive: true, balance: 185000 }, + { id: 'INC-403', code: '4300', name: 'Investor Funding', type: 'income', parentId: 'INC-001', isActive: true, balance: 500000 }, + { id: 'INC-404', code: '4400', name: 'Bike Sales Income', type: 'income', parentId: 'INC-001', isActive: true, balance: 0 }, + { id: 'INC-405', code: '4500', name: 'Other Income', type: 'income', parentId: 'INC-001', isActive: true, balance: 0 }, + + { id: 'EXP-001', code: '5000', name: 'Expenses', type: 'expense', isActive: true, balance: 0 }, + { id: 'EXP-501', code: '5100', name: 'Salary Expense', type: 'expense', parentId: 'EXP-001', isActive: true, balance: 25000 }, + { id: 'EXP-502', code: '5200', name: 'Rent Expense', type: 'expense', parentId: 'EXP-001', isActive: true, balance: 35000 }, + { id: 'EXP-503', code: '5300', name: 'Utility Expense', type: 'expense', parentId: 'EXP-001', isActive: true, balance: 8500 }, + { id: 'EXP-504', code: '5400', name: 'Maintenance Expense', type: 'expense', parentId: 'EXP-001', isActive: true, balance: 2500 }, + { id: 'EXP-505', code: '5500', name: 'Investor Withdrawal', type: 'expense', parentId: 'EXP-001', isActive: true, balance: 15000 }, + { id: 'EXP-506', code: '5600', name: 'Bike Cost of Sales', type: 'expense', parentId: 'EXP-001', isActive: true, balance: 0 }, + { id: 'EXP-507', code: '5700', name: 'Other Expense', type: 'expense', parentId: 'EXP-001', isActive: true, balance: 0 }, +]; + +const mockTransactions: AccountingTransaction[] = [ + { id: 'TXN-001', date: '2024-03-20', type: 'deposit', amount: 5000, description: 'Bike deposit from Rahim Ahmed', beneficiary: 'Rahim Ahmed', beneficiaryPhone: '01712345678', paymentMethod: 'cash', reference: 'DEP-001', createdAt: '2024-03-20T10:30:00', createdBy: 'Admin' }, + { id: 'TXN-002', date: '2024-03-20', type: 'rent_income', amount: 1200, description: 'Daily rental - Rahim Ahmed', beneficiary: 'Rahim Ahmed', paymentMethod: 'mobile', reference: 'REN-002', createdAt: '2024-03-20T14:00:00', createdBy: 'Admin' }, + { id: 'TXN-003', date: '2024-03-19', type: 'investor_funding', amount: 50000, description: 'Investment from Mohammad Islam', beneficiary: 'Mohammad Islam', beneficiaryPhone: '01987654321', paymentMethod: 'bank', reference: 'INV-001', createdAt: '2024-03-19T09:00:00', createdBy: 'Admin' }, + { id: 'TXN-004', date: '2024-03-19', type: 'salary', amount: 25000, description: 'Monthly salary - March 2024', beneficiary: 'Staff Team', paymentMethod: 'bank', reference: 'SAL-003', createdAt: '2024-03-19T16:00:00', createdBy: 'Admin' }, + { id: 'TXN-005', date: '2024-03-18', type: 'rent_expense', amount: 35000, description: 'Office rent - March 2024', beneficiary: 'Landlord', paymentMethod: 'bank', reference: 'RNT-001', createdAt: '2024-03-18T10:00:00', createdBy: 'Admin' }, + { id: 'TXN-006', date: '2024-03-18', type: 'utility', amount: 8500, description: 'Electricity bill - March 2024', beneficiary: 'DESCO', paymentMethod: 'mobile', reference: 'UTL-001', createdAt: '2024-03-18T11:30:00', createdBy: 'Admin' }, + { id: 'TXN-007', date: '2024-03-17', type: 'investor_withdraw', amount: 15000, description: 'Monthly profit withdrawal', beneficiary: 'Mohammad Islam', beneficiaryPhone: '01987654321', paymentMethod: 'bank', reference: 'WTH-002', createdAt: '2024-03-17T15:00:00', createdBy: 'Admin' }, + { id: 'TXN-008', date: '2024-03-17', type: 'maintenance', amount: 2500, description: 'Bike maintenance - Battery', beneficiary: 'Service Center', paymentMethod: 'cash', reference: 'MNT-001', createdAt: '2024-03-17T13:00:00', createdBy: 'Admin' }, + { id: 'TXN-009', date: '2024-03-15', type: 'bike_purchase', amount: 150000, description: 'Purchased 5 EVs from supplier', beneficiary: 'Green EV Bangladesh', beneficiaryPhone: '02-1234567', paymentMethod: 'bank', reference: 'PUR-001', bikeDetails: '5x Yadea DT3', createdAt: '2024-03-15T09:00:00', createdBy: 'Admin' }, + { id: 'TXN-010', date: '2024-03-10', type: 'bike_sale', amount: 45000, description: 'Sold EV to customer', beneficiary: 'John Doe', beneficiaryPhone: '01812345678', paymentMethod: 'bank', reference: 'SAL-001', bikeDetails: 'Dhaka Metro Cha-5679', createdAt: '2024-03-10T11:00:00', createdBy: 'Admin' }, +]; + +const mockJournalEntries: JournalEntry[] = [ + { + id: 'JE-001', date: '2024-03-20', reference: 'DEP-001', description: 'Bike deposit from Rahim Ahmed', + entries: [ + { accountId: 'ASSET-101', accountCode: '1100', accountName: 'Cash in Hand', debit: 5000, credit: 0 }, + { accountId: 'INC-401', accountCode: '4100', accountName: 'Bike Deposit Income', debit: 0, credit: 5000 }, + ], + isPosted: true, isAuto: false, sourceType: 'deposit', createdAt: '2024-03-20T10:30:00', createdBy: 'Admin', + }, + { + id: 'JE-002', date: '2024-03-20', reference: 'REN-002', description: 'Daily rental income', + entries: [ + { accountId: 'ASSET-103', accountCode: '1300', accountName: 'bKash Business', debit: 1200, credit: 0 }, + { accountId: 'INC-402', accountCode: '4200', accountName: 'Rental Income', debit: 0, credit: 1200 }, + ], + isPosted: true, isAuto: true, sourceType: 'rent_income', createdAt: '2024-03-20T14:00:00', createdBy: 'System', + }, + { + id: 'JE-003', date: '2024-03-19', reference: 'INV-001', description: 'Investment received', + entries: [ + { accountId: 'ASSET-102', accountCode: '1200', accountName: 'Bank - City Bank', debit: 50000, credit: 0 }, + { accountId: 'LIAB-202', accountCode: '2200', accountName: 'Investor Liabilities', debit: 0, credit: 50000 }, + ], + isPosted: true, isAuto: true, sourceType: 'investor_funding', createdAt: '2024-03-19T09:00:00', createdBy: 'System', + }, + { + id: 'JE-004', date: '2024-03-19', reference: 'SAL-003', description: 'Monthly salary payment', + entries: [ + { accountId: 'EXP-501', accountCode: '5100', accountName: 'Salary Expense', debit: 25000, credit: 0 }, + { accountId: 'ASSET-102', accountCode: '1200', accountName: 'Bank - City Bank', debit: 0, credit: 25000 }, + ], + isPosted: true, isAuto: false, sourceType: 'salary', createdAt: '2024-03-19T16:00:00', createdBy: 'Admin', + }, + { + id: 'JE-005', date: '2024-03-15', reference: 'PUR-001', description: 'Purchased 5 EVs', + entries: [ + { accountId: 'ASSET-105', accountCode: '1500', accountName: 'Bikes & Equipment', debit: 150000, credit: 0 }, + { accountId: 'ASSET-102', accountCode: '1200', accountName: 'Bank - City Bank', debit: 0, credit: 150000 }, + ], + isPosted: true, isAuto: true, sourceType: 'bike_purchase', createdAt: '2024-03-15T09:00:00', createdBy: 'System', + }, + { + id: 'JE-006', date: '2024-03-10', reference: 'SAL-001', description: 'Sold EV to customer', + entries: [ + { accountId: 'ASSET-102', accountCode: '1200', accountName: 'Bank - City Bank', debit: 45000, credit: 0 }, + { accountId: 'INC-404', accountCode: '4400', accountName: 'Bike Sales Income', debit: 0, credit: 45000 }, + ], + isPosted: true, isAuto: true, sourceType: 'bike_sale', createdAt: '2024-03-10T11:00:00', createdBy: 'System', + }, + { + id: 'JE-007', date: '2024-03-17', reference: 'WTH-002', description: 'Investor profit withdrawal', + entries: [ + { accountId: 'LIAB-202', accountCode: '2200', accountName: 'Investor Liabilities', debit: 15000, credit: 0 }, + { accountId: 'ASSET-102', accountCode: '1200', accountName: 'Bank - City Bank', debit: 0, credit: 15000 }, + ], + isPosted: true, isAuto: true, sourceType: 'investor_withdraw', createdAt: '2024-03-17T15:00:00', createdBy: 'System', + }, +]; + +const accountTypeConfig: Record = { + asset: { label: 'Asset', color: 'text-blue-600', bg: 'bg-blue-50' }, + liability: { label: 'Liability', color: 'text-red-600', bg: 'bg-red-50' }, + equity: { label: 'Equity', color: 'text-purple-600', bg: 'bg-purple-50' }, + income: { label: 'Income', color: 'text-green-600', bg: 'bg-green-50' }, + expense: { label: 'Expense', color: 'text-amber-600', bg: 'bg-amber-50' }, +}; + +const transactionTypeConfig: Record = { + deposit: { label: 'Bike Deposit', category: 'income', icon: Banknote }, + rent_income: { label: 'Rental Income', category: 'income', icon: Tag }, + investor_funding: { label: 'Investor Funding', category: 'income', icon: Users }, + investor_withdraw: { label: 'Investor Withdraw', category: 'expense', icon: Users }, + salary: { label: 'Salary Payment', category: 'expense', icon: Users }, + rent_expense: { label: 'Office Rent', category: 'expense', icon: Home }, + utility: { label: 'Utility Bills', category: 'expense', icon: Building }, + maintenance: { label: 'Maintenance', category: 'expense', icon: Wrench }, + bike_purchase: { label: 'Buy Bike', category: 'asset', icon: ShoppingCart }, + bike_sale: { label: 'Sell Bike', category: 'income', icon: Bike }, + other_income: { label: 'Other Income', category: 'income', icon: DollarSign }, + other_expense: { label: 'Other Expense', category: 'expense', icon: DollarSign }, +}; + +function generateAutoJournalEntries(type: TransactionType, amount: number, description: string, isManualEntry: boolean = false): JournalEntry['entries'] { + if (isManualEntry) return []; + + const config: Partial> = { + rent_income: { debitAccount: 'ASSET-103', debitCode: '1300', creditAccount: 'INC-402', creditCode: '4200' }, + investor_funding: { debitAccount: 'ASSET-102', debitCode: '1200', creditAccount: 'LIAB-202', creditCode: '2200' }, + investor_withdraw: { debitAccount: 'LIAB-202', debitCode: '2200', creditAccount: 'ASSET-102', creditCode: '1200' }, + bike_purchase: { debitAccount: 'ASSET-105', debitCode: '1500', creditAccount: 'ASSET-102', creditCode: '1200' }, + bike_sale: { debitAccount: 'ASSET-102', debitCode: '1200', creditAccount: 'INC-404', creditCode: '4400' }, + }; + + const c = config[type]; + if (!c) return []; + + return [ + { accountId: c.debitAccount, accountCode: c.debitCode, accountName: '', debit: amount, credit: 0 }, + { accountId: c.creditAccount, accountCode: c.creditCode, accountName: '', debit: 0, credit: amount }, + ]; +} + +export default function AccountingPage() { + const [activeTab, setActiveTab] = useState<'dashboard' | 'transactions' | 'journal' | 'ledger' | 'accounts'>('dashboard'); + const [transactions, setTransactions] = useState(mockTransactions); + const [accounts] = useState(defaultAccounts); + const [journalEntries, setJournalEntries] = useState(mockJournalEntries); + const [searchQuery, setSearchQuery] = useState(''); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [timePeriod, setTimePeriod] = useState<'this_month' | 'last_month' | 'last_3_months'>('this_month'); + const [showModal, setShowModal] = useState(false); + const [editingTransaction, setEditingTransaction] = useState(null); + const [viewingTransaction, setViewingTransaction] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + + const getDateRange = (period: 'this_month' | 'last_month' | 'last_3_months') => { + const now = new Date(); + let from: Date, to: Date = new Date(now); + let label: string; + + if (period === 'this_month') { + from = new Date(now.getFullYear(), now.getMonth(), 1); + to = new Date(now.getFullYear(), now.getMonth() + 1, 0); + label = now.toLocaleString('en-US', { month: 'long', year: 'numeric' }); + } else if (period === 'last_month') { + from = new Date(now.getFullYear(), now.getMonth() - 1, 1); + to = new Date(now.getFullYear(), now.getMonth(), 0); + label = from.toLocaleString('en-US', { month: 'long', year: 'numeric' }); + } else { + from = new Date(now.getFullYear(), now.getMonth() - 2, 1); + to = new Date(now.getFullYear(), now.getMonth() + 1, 0); + const startLabel = from.toLocaleString('en-US', { month: 'short', year: 'numeric' }); + const endLabel = to.toLocaleString('en-US', { month: 'short', year: 'numeric' }); + label = `${startLabel} - ${endLabel}`; + } + + return { + from: from.toISOString().split('T')[0], + to: to.toISOString().split('T')[0], + label: label + }; + }; + + const periodInfo = getDateRange(timePeriod); + + const filteredTransactions = useMemo(() => { + return transactions.filter(t => { + const matchesSearch = !searchQuery || + t.description.toLowerCase().includes(searchQuery.toLowerCase()) || + t.id.toLowerCase().includes(searchQuery.toLowerCase()) || + t.beneficiary?.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesDate = (!dateFrom || t.date >= dateFrom) && (!dateTo || t.date <= dateTo); + return matchesSearch && matchesDate; + }).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + }, [transactions, searchQuery, dateFrom, dateTo]); + + const paginatedTransactions = useMemo(() => { + const start = (currentPage - 1) * itemsPerPage; + return filteredTransactions.slice(start, start + itemsPerPage); + }, [filteredTransactions, currentPage]); + + const totalPages = Math.ceil(filteredTransactions.length / itemsPerPage); + + const stats = useMemo(() => { + const periodRange = getDateRange(timePeriod); + const transactionData = transactions.filter(t => t.date >= periodRange.from && t.date <= periodRange.to); + const income = transactionData.filter(t => ['deposit', 'rent_income', 'investor_funding', 'bike_sale', 'other_income'].includes(t.type)).reduce((sum, t) => sum + t.amount, 0); + const expense = transactionData.filter(t => ['investor_withdraw', 'salary', 'rent_expense', 'utility', 'maintenance', 'bike_purchase', 'other_expense'].includes(t.type)).reduce((sum, t) => sum + t.amount, 0); + const assets = accounts.filter(a => a.type === 'asset').reduce((sum, a) => sum + a.balance, 0); + const liabilities = accounts.filter(a => a.type === 'liability').reduce((sum, a) => sum + a.balance, 0); + const equity = accounts.filter(a => a.type === 'equity').reduce((sum, a) => sum + a.balance, 0); + return { income, expense, netProfit: income - expense, assets, liabilities, equity: equity + (income - expense), transactionCount: transactionData.length }; + }, [accounts, transactions, timePeriod]); + + const handleSaveTransaction = (data: Partial, isManual: boolean = false) => { + const isAuto = !isManual && ['rent_income', 'investor_funding', 'investor_withdraw', 'bike_purchase', 'bike_sale'].includes(data.type || ''); + const autoEntries = isAuto ? generateAutoJournalEntries(data.type!, data.amount!, data.description!, isManual) : []; + + const accMap = Object.fromEntries(accounts.map(a => [a.id, a.name])); + autoEntries.forEach(e => { e.accountName = accMap[e.accountId] || e.accountName; }); + + const newEntry: JournalEntry = { + id: `JE-${String(Date.now()).slice(-6)}`, + date: data.date || new Date().toISOString().split('T')[0], + reference: data.reference || `${data.type?.toUpperCase()}-${Date.now()}`, + description: data.description || '', + entries: autoEntries.length > 0 ? autoEntries : [ + { accountId: 'ASSET-101', accountCode: '1100', accountName: 'Cash in Hand', debit: data.amount || 0, credit: 0 }, + { accountId: 'EXP-507', accountCode: '5700', accountName: 'Other Expense', debit: 0, credit: data.amount || 0 }, + ], + isPosted: true, + isAuto: isAuto, + sourceType: data.type, + createdAt: new Date().toISOString(), + createdBy: isAuto ? 'System' : 'Admin', + }; + + if (editingTransaction) { + setTransactions(prev => prev.map(t => t.id === editingTransaction.id ? { ...t, ...data } as AccountingTransaction : t)); + } else { + const newTransaction: AccountingTransaction = { + id: `TXN-${String(Date.now()).slice(-6)}`, + date: data.date || new Date().toISOString().split('T')[0], + type: data.type || 'other_expense', + amount: data.amount || 0, + description: data.description || '', + beneficiary: data.beneficiary, + beneficiaryPhone: data.beneficiaryPhone, + paymentMethod: data.paymentMethod || 'cash', + reference: data.reference, + notes: data.notes, + bikeDetails: data.bikeDetails, + createdAt: new Date().toISOString(), + createdBy: 'Admin', + }; + setTransactions(prev => [newTransaction, ...prev]); + setJournalEntries(prev => [...prev, newEntry]); + } + setShowModal(false); + setEditingTransaction(null); + }; + + const handleDeleteTransaction = (id: string) => { + if (confirm('Are you sure you want to delete this transaction?')) { + const txn = transactions.find(t => t.id === id); + if (txn) { + setTransactions(prev => prev.filter(t => t.id !== id)); + setJournalEntries(prev => prev.filter(j => j.reference !== txn.reference)); + } + } + }; + + const handleExportPDF = () => { + import('jspdf').then(jsPDF => { + const doc = new jsPDF.default(); + + doc.setFontSize(18); + doc.setTextColor(6, 95, 70); + doc.text('JAIBEN Mobility Ltd', 20, 20); + + doc.setFontSize(12); + doc.setTextColor(0); + const title = activeTab === 'transactions' ? 'Transactions Report' : + activeTab === 'journal' ? 'Journal Entries Report' : + activeTab === 'ledger' ? 'General Ledger Report' : 'Accounting Report'; + doc.text(title, 20, 30); + + doc.setFontSize(9); + doc.setTextColor(100); + doc.text(`Date: ${new Date().toLocaleDateString()}`, 20, 38); + if (dateFrom || dateTo) { + doc.text(`Period: ${dateFrom || 'Start'} to ${dateTo || 'Now'}`, 20, 45); + } + + doc.setFontSize(9); + doc.setTextColor(150); + doc.text('Generated from JAIBEN Accounting System', 20, 280); + + doc.save(`accounting-${activeTab}-${new Date().toISOString().split('T')[0]}.pdf`); + }); + }; + + const handleExportExcel = () => { + let csv = ''; + if (activeTab === 'transactions') { + csv = 'ID,Date,Type,Amount,Beneficiary,Payment Method,Description\n'; + filteredTransactions.forEach(t => { + csv += `${t.id},${t.type},${t.amount},${t.beneficiary || ''},${t.paymentMethod},${t.description}\n`; + }); + } else if (activeTab === 'journal') { + csv = 'Date,Reference,Account,Debit,Credit\n'; + journalEntries.forEach(j => { + j.entries.forEach(e => { + csv += `${j.date},${j.reference},${e.accountName},${e.debit},${e.credit}\n`; + }); + }); + } + + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `accounting-${activeTab}-${new Date().toISOString().split('T')[0]}.csv`; + a.click(); + }; + + const handlePrint = () => { + window.print(); + }; + + const tabs = [ + { id: 'dashboard', label: 'Dashboard', icon: PieChart }, + { id: 'transactions', label: 'Transactions', icon: Receipt }, + { id: 'journal', label: 'Journal', icon: BookOpen }, + { id: 'ledger', label: 'Ledger', icon: List }, + { id: 'accounts', label: 'Chart of Accounts', icon: Calculator }, + ]; + + return ( +
+
+
+

Accounting

+

Double-entry bookkeeping with auto-journals

+
+
+ +
+
+ +
+
+ {tabs.map(tab => { + const Icon = tab.icon; + return ( + + ); + })} +
+
+ + {(activeTab === 'transactions' || activeTab === 'journal' || activeTab === 'ledger') && ( +
+
+ + { setSearchQuery(e.target.value); setCurrentPage(1); }} + className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20" + /> +
+
+ From: + { setDateFrom(e.target.value); setCurrentPage(1); }} + className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20" + /> +
+
+ To: + { setDateTo(e.target.value); setCurrentPage(1); }} + className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20" + /> +
+
+ + + +
+
+ )} + + {activeTab === 'dashboard' && ( + <> +
+
+

Financial Overview

+

{periodInfo.label}

+
+
+ {[ + { id: 'this_month', label: 'This Month' }, + { id: 'last_month', label: 'Last Month' }, + { id: 'last_3_months', label: '3 Months' }, + ].map((period) => ( + + ))} +
+
+ + + )} + {activeTab === 'transactions' && ( + { setEditingTransaction(t); setShowModal(true); }} + onDelete={handleDeleteTransaction} + /> + )} + {activeTab === 'journal' && } + {activeTab === 'ledger' && } + {activeTab === 'accounts' && } + + { setShowModal(false); setEditingTransaction(null); }} + onSave={handleSaveTransaction} + transaction={editingTransaction} + accounts={accounts} + /> + + setViewingTransaction(null)} + transaction={viewingTransaction} + accounts={accounts} + /> +
+ ); +} + +function DashboardView({ stats, accounts, monthLabel = 'This Month' }: { stats: any; accounts: ChartOfAccount[]; monthLabel?: string }) { + const currentMonth = new Date().toLocaleString('en-US', { month: 'long', year: 'numeric' }); + + return ( +
+
+ + + + + +
+ +
+
+
+
+
+

Accounting Equation

+
+

A = L + E

+
+
+
+
+
+ +
+
+

Assets

+

<1000> Code Series

+
+
+

৳{stats.assets.toLocaleString()}

+
+ +
+
+ = +
+
+ +
+
+
+ +
+
+

Liabilities

+

<2000> Code Series

+
+
+

৳{stats.liabilities.toLocaleString()}

+
+ +
+
+ + +
+
+ +
+
+
+ +
+
+

Equity

+

<3000> Code Series

+
+
+

৳{stats.equity.toLocaleString()}

+
+ +
+

Balance Check:

+ + {stats.assets === stats.liabilities + stats.equity ? '✓ Balanced' : '⚠ Check Required'} + +
+
+
+ +
+
+
+
+

Auto-Journal Sources

+
+

System-generated entries

+
+
+ {[ + { label: 'Rental Income', desc: 'When bike is rented via payment', auto: true }, + { label: 'Investor Funding', desc: 'When investor adds money', auto: true }, + { label: 'Investor Withdraw', desc: 'When investor withdraws profit', auto: true }, + { label: 'Bike Purchase', desc: 'When bikes are purchased', auto: true }, + { label: 'Bike Sales', desc: 'When bikes are sold', auto: true }, + { label: 'Maintenance Expense', desc: 'When damage/repair is paid', auto: true }, + { label: 'Manual Entry', desc: 'User creates manually', auto: false }, + ].map((item, i) => ( +
+
+ {item.auto ? : } +
+

{item.label}

+

{item.desc}

+
+
+ + {item.auto ? 'Auto' : 'Manual'} + +
+ ))} +
+
+
+
+ ); +} + +function StatCard({ label, value, icon: Icon, color }: { label: string; value: number; icon: React.ElementType; color: string }) { + const colors: Record = { green: 'bg-green-50 text-green-600', red: 'bg-red-50 text-red-600', blue: 'bg-blue-50 text-blue-600', purple: 'bg-purple-50 text-purple-600', slate: 'bg-slate-100 text-slate-600' }; + const showCurrency = label !== 'Transactions'; + return ( +
+
+
+ +
+
+

{showCurrency ? `৳${value.toLocaleString()}` : value.toLocaleString()}

+

{label}

+
+ ); +} + +function TransactionsView({ transactions, totalPages, currentPage, onPageChange, onView, onEdit, onDelete }: any) { + return ( +
+
+ + + + + + + + + + + + + {transactions.map((txn: AccountingTransaction) => ( + + + + + + + + + ))} + +
IDDateTypeBeneficiaryAmountActions
{txn.id}{txn.date} + {txn.type.replace('_', ' ')} + {txn.beneficiary || '-'} + ৳{txn.amount.toLocaleString()} + +
+ + + +
+
+
+ + {totalPages > 1 && ( +
+

Page {currentPage} of {totalPages}

+
+ + +
+
+ )} +
+ ); +} + +function JournalView({ entries, dateFrom, dateTo }: { entries: JournalEntry[]; dateFrom: string; dateTo: string }) { + const filtered = entries.filter(e => (!dateFrom || e.date >= dateFrom) && (!dateTo || e.date <= dateTo)); + + return ( +
+
+

Journal Entries

+

{filtered.length} entries found

+
+
+ + + + + + + + + + + + + {filtered.map((entry) => ( + <> + {entry.entries.map((line, idx) => ( + + {idx === 0 && ( + <> + + + + )} + + + + {idx === 0 && ( + + )} + + ))} + + ))} + +
DateRefAccountDebitCreditType
+ {entry.date} + + {entry.reference} + + {line.accountName} + ({line.accountCode}) + + {line.debit > 0 && ৳{line.debit.toLocaleString()}} + + {line.credit > 0 && ৳{line.credit.toLocaleString()}} + + + {entry.isAuto ? 'Auto' : 'Manual'} + +
+
+
+ ); +} + +function LedgerView({ accounts, journalEntries, dateFrom, dateTo }: { accounts: ChartOfAccount[]; journalEntries: JournalEntry[]; dateFrom: string; dateTo: string }) { + const filteredEntries = journalEntries.filter(e => (!dateFrom || e.date >= dateFrom) && (!dateTo || e.date <= dateTo)); + const ledgerAccounts = accounts.filter(a => a.isActive && a.parentId); + + return ( +
+ {ledgerAccounts.map(account => { + const entries = filteredEntries.flatMap(je => + je.entries.filter(e => e.accountId === account.id).map(e => ({ + date: je.date, + reference: je.reference, + debit: e.debit, + credit: e.credit, + isAuto: je.isAuto + })) + ); + + let balance = 0; + if (account.type === 'asset' || account.type === 'expense') { + balance = entries.reduce((sum, e) => sum + e.debit - e.credit, 0); + } else { + balance = entries.reduce((sum, e) => sum + e.credit - e.debit, 0); + } + + return ( +
+
+
+ {account.code} + {account.name} + + {accountTypeConfig[account.type].label} + +
+ ৳{balance.toLocaleString()} +
+ {entries.length > 0 && ( + + + + + + + + + + + + {entries.slice(0, 10).map((entry, idx) => { + let runningBalance = 0; + if (account.type === 'asset' || account.type === 'expense') { + runningBalance = entries.slice(0, idx + 1).reduce((sum, e) => sum + e.debit - e.credit, 0); + } else { + runningBalance = entries.slice(0, idx + 1).reduce((sum, e) => sum + e.credit - e.debit, 0); + } + return ( + + + + + + + + ); + })} + +
DateRefDebitCreditBalance
{entry.date}{entry.reference}{entry.debit > 0 ? `৳${entry.debit}` : '-'}{entry.credit > 0 ? `৳${entry.credit}` : '-'}৳{runningBalance}
+ )} +
+ ); + })} +
+ ); +} + +function AccountsView({ accounts }: { accounts: ChartOfAccount[] }) { + const accountTypes: AccountType[] = ['asset', 'liability', 'equity', 'income', 'expense']; + + return ( +
+ {accountTypes.map(type => { + const typeAccounts = accounts.filter(a => a.type === type && a.isActive); + const parentAccounts = typeAccounts.filter(a => !a.parentId); + + return ( +
+
+
+ + {accountTypeConfig[type].label} + + {typeAccounts.length} Accounts +
+ ৳{typeAccounts.reduce((sum, a) => sum + a.balance, 0).toLocaleString()} +
+
+ {parentAccounts.map(account => { + const children = typeAccounts.filter(a => a.parentId === account.id); + return ( +
+
+
+ {account.code} + {account.name} +
+ ৳{account.balance.toLocaleString()} +
+ {children.map(child => ( +
+
+ {child.code} + {child.name} +
+ ৳{child.balance.toLocaleString()} +
+ ))} +
+ ); + })} +
+
+ ); + })} +
+ ); +} + +function TransactionModal({ isOpen, onClose, onSave, transaction, accounts }: { isOpen: boolean; onClose: () => void; onSave: (data: Partial, isManual?: boolean) => void; transaction: AccountingTransaction | null; accounts: ChartOfAccount[] }) { + const [formData, setFormData] = useState>({ + date: new Date().toISOString().split('T')[0], + type: 'rent_income', + amount: 0, + description: '', + beneficiary: '', + beneficiaryPhone: '', + paymentMethod: 'cash', + reference: '', + notes: '', + bikeDetails: '', + }); + + useEffect(() => { + if (transaction) { + setFormData(transaction); + } else { + setFormData({ + date: new Date().toISOString().split('T')[0], + type: 'rent_income', + amount: 0, + description: '', + beneficiary: '', + beneficiaryPhone: '', + paymentMethod: 'cash', + reference: '', + notes: '', + bikeDetails: '', + }); + } + }, [transaction, isOpen]); + + const getPaymentAccount = (method: string) => { + switch (method) { + case 'bank': return { code: '1200', name: 'Bank - City Bank' }; + case 'cash': return { code: '1100', name: 'Cash in Hand' }; + case 'mobile': return { code: '1300', name: 'bKash Business' }; + case 'cheque': return { code: '1410', name: 'Cheque Receivable' }; + default: return { code: '1100', name: 'Cash in Hand' }; + } + }; + + const getTransactionAccounts = (type: TransactionType, amount: number, method: string) => { + const payment = getPaymentAccount(method); + + const journalMap: Record = { + deposit: { debit: payment, credit: { code: '4100', name: 'Bike Deposit Income' } }, + rent_income: { debit: payment, credit: { code: '4200', name: 'Rental Income' } }, + investor_funding: { debit: payment, credit: { code: '2200', name: 'Investor Liabilities' } }, + investor_withdraw: { debit: { code: '2200', name: 'Investor Liabilities' }, credit: payment }, + salary: { debit: { code: '5100', name: 'Salary Expense' }, credit: payment }, + rent_expense: { debit: { code: '5200', name: 'Rent Expense' }, credit: payment }, + utility: { debit: { code: '5300', name: 'Utility Expense' }, credit: payment }, + maintenance: { debit: { code: '5400', name: 'Maintenance Expense' }, credit: payment }, + bike_purchase: { debit: { code: '1500', name: 'Bikes & Equipment' }, credit: payment }, + bike_sale: { debit: payment, credit: { code: '4400', name: 'Bike Sales Income' } }, + other_income: { debit: payment, credit: { code: '4500', name: 'Other Income' } }, + other_expense: { debit: { code: '5700', name: 'Other Expense' }, credit: payment }, + }; + + const entry = journalMap[type] || { debit: payment, credit: { code: '5700', name: 'Other Expense' } }; + return entry; + }; + + const typeConfig = transactionTypeConfig[formData.type || 'rent_income']; + const journalEntry = getTransactionAccounts(formData.type || 'rent_income', formData.amount || 0, formData.paymentMethod || 'cash'); + + return ( +
+
+
+
+
+

+ {transaction ? 'Edit Transaction' : 'New Transaction'} +

+

Create manual journal entry

+
+ +
+ +
{ e.preventDefault(); onSave(formData, true); }} className="flex-1 overflow-y-auto p-6 space-y-5"> +
+
+ + setFormData({ ...formData, date: e.target.value })} + className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm bg-white" + required + /> +
+
+ + +
+
+ +
+
+ + setFormData({ ...formData, amount: Number(e.target.value) })} + className="w-full px-3 py-2.5 border border-green-200 rounded-lg text-sm bg-white font-bold text-green-700" + required + min="0" + placeholder="0" + /> +
+
+ + +
+
+ +
+ + setFormData({ ...formData, description: e.target.value })} + className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm" + required + placeholder="Enter transaction description" + /> +
+ +
+
+ + setFormData({ ...formData, beneficiary: e.target.value })} + className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm" + placeholder="Name of person or organization" + /> +
+
+ + setFormData({ ...formData, beneficiaryPhone: e.target.value })} + className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm" + placeholder="Contact number" + /> +
+
+ +
+
+ + setFormData({ ...formData, reference: e.target.value })} + className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm" + placeholder="Auto-generated if empty" + /> +
+
+ + setFormData({ ...formData, bikeDetails: e.target.value })} + className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm" + placeholder="Bike model, plate number, etc." + /> +
+
+ +
+ + +
+ +
+

+ Rented Bikes History +

+
+ + + + + + + + + + + + + + + {rentedBikesHistory.map(rental => ( + + + + + + + + + + + ))} + +
BikePlatePlanStart DateEnd DateDaysTotal RentStatus
+
+ +
+

{rental.bikeName}

+

{rental.bikeId}

+
+
+
{rental.plate}{rental.plan}{rental.startDate}{rental.endDate || '-'}{rental.totalDays}৳{rental.totalRent} + + {rental.status === 'active' && } + {rental.status === 'returned' && } + {rental.status === 'active' ? 'Active' : 'Returned'} + +
+
+
+
+ )} + {activeTab === 'stats' && (
diff --git a/src/app/admin/bikers/page.tsx b/src/app/admin/bikers/page.tsx index dbf0b60..cfe87a4 100644 --- a/src/app/admin/bikers/page.tsx +++ b/src/app/admin/bikers/page.tsx @@ -136,6 +136,10 @@ interface Biker { lastRideAt?: string; firstRideAt?: string; joinedFrom: string; + + pendingRent?: number; + pendingRentDays?: number; + lastRentPaidAt?: string; } const mockBikers: Biker[] = [ @@ -191,6 +195,9 @@ const mockBikers: Biker[] = [ lastRideAt: '2024-03-21', firstRideAt: '2024-01-15', joinedFrom: 'App Store', + pendingRent: 0, + pendingRentDays: 0, + lastRentPaidAt: '2024-03-21', }, { id: 'B002', @@ -323,6 +330,9 @@ const mockBikers: Biker[] = [ insuranceExpiry: '2024-12-01', tags: ['top_rider', 'vip'], joinedFrom: 'Facebook', + pendingRent: 150, + pendingRentDays: 3, + lastRentPaidAt: '2024-03-18', }, { id: 'B005', @@ -411,7 +421,10 @@ export default function BikersPage() { biker.gpsDeviceId?.toLowerCase().includes(searchQuery.toLowerCase()); const matchesStatus = statusFilter === 'all' || biker.status === statusFilter; const matchesKyc = kycFilter === 'all' || biker.kycStatus === kycFilter; - return matchesSearch && matchesStatus && matchesKyc; + const matchesRent = (statusFilter === 'rent_pending') + ? (biker.pendingRent && biker.pendingRent > 0) + : true; + return matchesSearch && matchesStatus && matchesKyc && matchesRent; }); const sortedBikers = [...filteredBikers].sort((a, b) => { @@ -587,6 +600,7 @@ export default function BikersPage() { + setSelectedBikerId(e.target.value)} + className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm" + > + + {availableBikers.filter(b => !assignedBikers.some(ab => ab.id === b.id)).map(b => ( + + ))} + + +
+ ) : ( +

Maximum bikers assigned for this plan

+ )} +
+ + {assignedBikers.length > 0 && ( +
+ {assignedBikers.map((biker, idx) => ( +
+
+
+
+ +
+
+

{biker.name}

+

{biker.phone}

+

{biker.email}

+
+
+ +
+
+
+ + { + const updated = [...assignedBikers]; + updated[idx].dailyRate = Number(e.target.value); + setAssignedBikers(updated); + }} + className="w-full px-2 py-1 border border-slate-200 rounded text-sm" + /> +
+
+ +

{biker.license}

+
+
+
+ + +
+
+ ))} +
+ )} + + {assignedBikers.length > 0 && ( +
+
+ Total Daily Rate: + + ৳{assignedBikers.reduce((sum, b) => sum + b.dailyRate, 0)}/day + +
+
+ )} +
+ +
+ + +
+
+
+ )} +
+ ); +} + function InvestorTab({ bike }: { bike: Bike }) { return (
@@ -635,10 +1235,16 @@ function InvestorTab({ bike }: { bike: Bike }) {
-
+

{bike.investorName || 'Investor'}

ID: {bike.investorId}

+ + View Investor +
@@ -657,8 +1263,8 @@ function InvestorTab({ bike }: { bike: Bike }) {

ROI

- {bike.purchasePrice && bike.totalEarnings - ? ((bike.totalEarnings / bike.purchasePrice) * 100).toFixed(1) + {bike.purchasePrice && bike.totalEarnings + ? ((bike.totalEarnings / bike.purchasePrice) * 100).toFixed(1) : 0}%

@@ -686,4 +1292,267 @@ function InvestorTab({ bike }: { bike: Bike }) {
); +} + +function DamageModal({ bike, damage, onClose, onSave }: { bike: Bike; damage: DamageRecord | null; onClose: () => void; onSave: (damage: DamageRecord) => void }) { + const [formData, setFormData] = useState({ + id: damage?.id || `DMG${Date.now()}`, + date: damage?.date || new Date().toISOString().split('T')[0], + type: damage?.type || 'accident', + description: damage?.description || '', + reportedBy: damage?.reportedBy || '', + reportedAt: damage?.reportedAt || new Date().toISOString().replace('T', ' ').slice(0, 16), + estimatedCost: damage?.estimatedCost || 0, + actualCost: damage?.actualCost || 0, + status: damage?.status || 'reported', + }); + + const damageTypes = [ + { value: 'accident', label: 'Accident' }, + { value: 'theft', label: 'Theft' }, + { value: 'natural', label: 'Natural Disaster' }, + { value: 'wear_tear', label: 'Wear & Tear' }, + { value: 'other', label: 'Other' }, + ]; + + const statusOptions = [ + { value: 'reported', label: 'Reported' }, + { value: 'under_repair', label: 'Under Repair' }, + { value: 'repaired', label: 'Repaired' }, + { value: 'claim_rejected', label: 'Claim Rejected' }, + ]; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave(formData as DamageRecord); + }; + + return ( +
+
+
+

{damage ? 'Edit Damage Record' : 'Add Damage Record'}

+ +
+ +
+ + setFormData({ ...formData, date: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + required + /> +
+
+ + +
+
+ + +

This message will be sent via SMS to {selectedRequest.phone}

+
+
+
+ + +
+
+ + )} + + {showDetailsModal && selectedRequest && ( +
+
+
+

Request Details - {selectedRequest.id}

+ +
+
+
+
+

Name

+

{selectedRequest.name}

+
+
+

Phone

+

{selectedRequest.phone}

+
+
+

Email

+

{selectedRequest.email}

+
+
+

Location

+

{selectedRequest.location}

+
+
+ + {selectedRequest.type === 'biker' && ( +
+

+ Bike Details +

+
+
+
+ + +
+
+ + { + const updated = requests.map(r => + r.id === selectedRequest.id + ? { ...r, evAllocation: { ...r.evAllocation!, bikeModel: e.target.value } } + : r + ); + setRequests(updated); + }} + /> +
+
+
+
+ )} + +
+

+ Documents +

+
+ {selectedRequest.requiredDocuments.map((doc, idx) => ( +
+
+ {doc.status === 'pending' ? ( + + ) : doc.status === 'uploaded' ? ( + + ) : doc.status === 'approved' ? ( + + ) : ( + + )} +
+ {doc.name} + {doc.uploadedAt && {doc.uploadedAt}} +
+
+
+ {doc.status === 'pending' && ( + + Waiting for upload + + )} + {(doc.status === 'uploaded' || doc.status === 'approved') && ( + + )} + {(doc.status === 'uploaded' || doc.status === 'approved') && ( + + )} + {doc.status === 'rejected' && ( + + Rejected + + )} + {doc.status === 'approved' && ( + + Approved + + )} + {doc.status === 'rejected' && doc.rejectedReason && ( +
Reason: {doc.rejectedReason}
+ )} +
+
+ ))} +
+
+ + {selectedRequest.type === 'biker' && selectedRequest.bikeRequested && ( +
+

+ Bike Images (When Taking Bike) +

+
+ {['Front', 'Back', 'Left Side', 'Right Side'].map((view) => ( +
+ + {view} + Upload +
+ ))} +
+ +
+ )} + + {selectedRequest.messageHistory.length > 0 && ( +
+

Message History

+
+ {selectedRequest.messageHistory.map((msg, idx) => ( +
+
+ + {msg.from === 'admin' ? 'Admin' : 'User'} + + {msg.date} +
+

{msg.message}

+
+ ))} +
+
+ )} + + {selectedRequest.notes.length > 0 && ( +
+

Notes

+
+ {selectedRequest.notes.map((note, idx) => ( +
+

{note}

+
+ ))} +
+
+ )} +
+
+ {selectedRequest.status !== 'approved' && selectedRequest.status !== 'rejected' && ( + <> + {selectedRequest.type === 'biker' && ( + + )} + {selectedRequest.type === 'investor' && ( + + )} + {selectedRequest.type === 'shop' && ( + + )} + + )} + +
+
+
+ )} + + {showActionModal && selectedRequest && ( +
+
+
+

+ {actionType === 'approve' ? 'Approve Request' : actionType === 'reject' ? 'Reject Request' : 'Request Documents'} +

+
+
+

+ {actionType === 'approve' && `Are you sure you want to approve ${selectedRequest.name} as a ${selectedRequest.type}?`} + {actionType === 'reject' && `Are you sure you want to reject ${selectedRequest.name}'s request?`} + {actionType === 'documents' && `Send a message to ${selectedRequest.name} requesting required documents.`} +

+
+

Name: {selectedRequest.name}

+

Phone: {selectedRequest.phone}

+

Type: {selectedRequest.type}

+
+
+
+ + +
+
+
+ )} + + setShowNewApplicationModal(false)} + onSave={(data) => { + const newRequest: Request = { + id: `REQ${String(Date.now()).slice(-6)}`, + applicationSource: data.applicationSource || 'walkin', + sourceDetails: data.sourceDetails || '', + name: data.name || '', + phone: data.phone || '', + email: data.email || '', + type: data.type || 'biker', + location: data.location || '', + address: data.address || '', + status: 'pending', + verificationStage: 'application', + submittedAt: new Date().toISOString().split('T')[0], + requiredDocuments: data.type === 'biker' ? [ + { id: 'doc1', name: 'NID Front', status: 'pending' }, + { id: 'doc2', name: 'NID Back', status: 'pending' }, + { id: 'doc3', name: 'Driving License', status: 'pending' }, + { id: 'doc4', name: 'Profile Photo', status: 'pending' }, + ] : data.type === 'investor' ? [ + { id: 'doc1', name: 'NID', status: 'pending' }, + { id: 'doc2', name: 'TIN Certificate', status: 'pending' }, + { id: 'doc3', name: 'Bank Statement', status: 'pending' }, + ] : [ + { id: 'doc1', name: 'NID', status: 'pending' }, + { id: 'doc2', name: 'Trade License', status: 'pending' }, + { id: 'doc3', name: 'Shop Photo', status: 'pending' }, + ], + riderPlan: data.riderPlan, + employmentInfo: data.employmentInfo, + nomineeDetails: data.nomineeDetails, + securityDeposit: data.securityDeposit, + advancePayment: data.advancePayment, + paymentMethod: data.paymentMethod, + bikeRequested: data.bikeRequested, + scheduleDate: data.scheduleDate, + notes: [], + messageHistory: [], + }; + setRequests([newRequest, ...requests]); + setShowNewApplicationModal(false); + alert('Application created successfully!'); + }} + /> + + ); +} + +function NewApplicationModal({ isOpen, onClose, onSave }: { isOpen: boolean; onClose: () => void; onSave: (data: Partial) => void }) { + const [formData, setFormData] = useState>({ + applicationSource: 'walkin', + sourceDetails: '', + name: '', + phone: '', + email: '', + type: 'biker', + location: '', + address: '', + riderPlan: 'daily_rent', + nomineeDetails: { name: '', phone: '', relationship: '', nid: '' }, + employmentInfo: { company: '', dailyEarning: 0, whyEV: '', experience: '' }, + securityDeposit: 0, + advancePayment: 0, + paymentMethod: 'cash', + }); + + const [step, setStep] = useState(1); + + const updateField = (field: string, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const updateNested = (parent: string, field: string, value: any) => { + setFormData(prev => ({ + ...prev, + [parent]: { ...(prev as any)[parent], [field]: value } + })); + }; + + if (!isOpen) return null; + + return ( +
+
+
+
+

New Application

+

Step {step} of 5

+
+ +
+ +
+
+ {[ + { n: 1, l: 'Basic Info' }, + { n: 2, l: 'Documents' }, + { n: 3, l: 'Employment' }, + { n: 4, l: 'Plan' }, + { n: 5, l: 'Nominee' }, + ].map(s => ( +
= s.n ? 'bg-accent' : 'bg-slate-200'}`} /> + ))} +
+
+ +
+ {step === 1 && ( +
+
+

+ Basic Information +

+
+
+ +
-

{kyc.userName}

-

- {kyc.phone} -

+ +
-
-
-
-

- Submitted: {kyc.submittedAt} -

-

- Dhaka, Bangladesh -

-

- NID: 1234567890 -

+
+ + updateField('name', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + placeholder="Enter full name" + /> +
+
+ + updateField('phone', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + placeholder="01XXXXXXXXX" + /> +
+
+ + updateField('email', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + placeholder="email@example.com" + /> +
+
+ + updateField('location', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + placeholder="Area, Dhaka" + /> +
+
+ + updateField('address', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + placeholder="Complete address" + />
-
-
- - {kyc.status} - - {kyc.status === 'pending' && ( -
- - - -
- )}
- ))} + )} + + {step === 2 && ( +
+
+

+ Required Documents +

+

Documents to be collected from the applicant

+
+ {formData.type === 'biker' && [ + { n: 'NID (Front)', f: 'nid_front' }, + { n: 'NID (Back)', f: 'nid_back' }, + { n: 'Driving License', f: 'driving_license' }, + { n: 'Profile Photo', f: 'profile_photo' }, + { n: 'Bank Account Card', f: 'bank_card' }, + { n: 'Mobile Wallet Info', f: 'wallet_info' }, + ].map(doc => ( +
+ {doc.n} + + Pending + +
+ ))} + {formData.type === 'investor' && [ + { n: 'NID Copy', f: 'nid' }, + { n: 'TIN Certificate', f: 'tin' }, + { n: 'Bank Statement (3 months)', f: 'bank_stmt' }, + { n: 'Photo', f: 'photo' }, + ].map(doc => ( +
+ {doc.n} + + Pending + +
+ ))} + {formData.type === 'shop' && [ + { n: 'Trade License', f: 'trade_license' }, + { n: 'Shop Owner NID', f: 'owner_nid' }, + { n: 'Shop Photos', f: 'shop_photos' }, + { n: 'Electricity Bill', f: 'electricity' }, + ].map(doc => ( +
+ {doc.n} + + Pending + +
+ ))} +
+
+
+ )} + + {step === 3 && formData.type === 'biker' && ( +
+
+

+ Employment Information +

+
+
+ + updateNested('employmentInfo', 'company', e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + placeholder="e.g., Foodpanda, ssl, Pathao" + /> +
+
+ + updateNested('employmentInfo', 'dailyEarning', Number(e.target.value))} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" + placeholder="0" + /> +
+
+ +