feat: implement automated double-entry accounting for investments and add transaction details modal
This commit is contained in:
247
package-lock.json
generated
247
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
1319
src/app/admin/accounting/page.tsx
Normal file
1319
src/app/admin/accounting/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ import {
|
||||
User, Phone, Mail, MapPin, Calendar, Heart, Briefcase, Car, Navigation,
|
||||
FileText, Clock, TrendingUp, CreditCard, Shield, Award, Star, Activity,
|
||||
Eye, Edit, Trash2, ArrowLeft, PhoneCall, MessageCircle, CheckCircle, XCircle,
|
||||
AlertTriangle, DollarSign, Wallet, Bike as BikeIcon, Wrench, Ban, MoreHorizontal, Copy,
|
||||
ExternalLink, Download, Upload, Bell, MessageSquare, Send, RefreshCcw
|
||||
AlertTriangle, DollarSign as DollarSignIcon, Wallet, Bike as BikeIcon, Wrench, Ban, MoreHorizontal, Copy,
|
||||
ExternalLink, Download, Upload, Bell, MessageSquare, Send, RefreshCcw, Image
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DrivingLicense {
|
||||
@@ -168,12 +168,31 @@ export default function BikerDetailPage({ params }: PageProps) {
|
||||
{ id: 'personal', label: 'Personal', icon: User },
|
||||
{ id: 'license', label: 'License & GPS', icon: Car },
|
||||
{ id: 'documents', label: 'Documents', icon: FileText },
|
||||
{ id: 'reviews', label: 'Reviews', icon: MessageCircle },
|
||||
{ id: 'stats', label: 'Statistics', icon: TrendingUp },
|
||||
{ id: 'rent', label: 'Rent History', icon: DollarSignIcon },
|
||||
{ id: 'bike', label: 'Bike', icon: BikeIcon },
|
||||
// { id: 'reviews', label: 'Reviews', icon: MessageCircle },
|
||||
// { id: 'stats', label: 'Statistics', icon: TrendingUp },
|
||||
{ id: 'account', label: 'Account', icon: CreditCard },
|
||||
{ id: 'activity', label: 'Activity', icon: Activity },
|
||||
];
|
||||
|
||||
const rentHistory = [
|
||||
{ id: 'R001', date: '2024-03-21', amount: 50, status: 'paid', bikeId: 'EV001', plan: 'Single' },
|
||||
{ id: 'R002', date: '2024-03-20', amount: 50, status: 'paid', bikeId: 'EV001', plan: 'Single' },
|
||||
{ id: 'R003', date: '2024-03-19', amount: 50, status: 'paid', bikeId: 'EV001', plan: 'Single' },
|
||||
{ id: 'R004', date: '2024-03-18', amount: 50, status: 'paid', bikeId: 'EV001', plan: 'Single' },
|
||||
{ id: 'R005', date: '2024-03-17', amount: 50, status: 'paid', bikeId: 'EV001', plan: 'Single' },
|
||||
{ id: 'R006', date: '2024-03-16', amount: 50, status: 'paid', bikeId: 'EV001', plan: 'Single' },
|
||||
{ id: 'R007', date: '2024-03-15', amount: 50, status: 'paid', bikeId: 'EV001', plan: 'Single' },
|
||||
{ id: 'R008', date: '2024-03-14', amount: 0, status: 'pending', bikeId: 'EV001', plan: 'Single' },
|
||||
];
|
||||
|
||||
const rentedBikesHistory = [
|
||||
{ id: 'RB001', bikeId: 'EV001', bikeName: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9012', startDate: '2024-03-14', endDate: null, status: 'active', plan: 'Single', dailyRate: 50, totalDays: 8, totalRent: 400 },
|
||||
{ id: 'RB002', bikeId: 'EV003', bikeName: 'Yadea DT3', plate: 'Dhaka Metro Cha-5678', startDate: '2024-02-01', endDate: '2024-03-13', status: 'returned', plan: 'Rent-to-Own', dailyRate: 45, totalDays: 42, totalRent: 1890 },
|
||||
{ id: 'RB003', bikeId: 'EV005', bikeName: 'Etron ET50', plate: 'Dhaka Metro Cha-1234', startDate: '2024-01-10', endDate: '2024-01-31', status: 'returned', plan: 'Shared', dailyRate: 60, totalDays: 22, totalRent: 1320 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6 min-h-screen">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
@@ -389,6 +408,249 @@ export default function BikerDetailPage({ params }: PageProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'rent' && (
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<DollarSignIcon className="w-5 h-5 text-accent" /> Daily Rent History
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button className="px-3 py-1.5 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 flex items-center gap-1">
|
||||
<Download className="w-4 h-4" /> Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<p className="text-xs text-green-600 font-medium">Total Paid</p>
|
||||
<p className="text-lg font-bold text-green-700">৳{rentHistory.filter(r => r.status === 'paid').reduce((sum, r) => sum + r.amount, 0)}</p>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<p className="text-xs text-red-600 font-medium">Pending</p>
|
||||
<p className="text-lg font-bold text-red-700">৳{rentHistory.filter(r => r.status === 'pending').reduce((sum, r) => sum + r.amount, 0)}</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<p className="text-xs text-blue-600 font-medium">Total Days</p>
|
||||
<p className="text-lg font-bold text-blue-700">{rentHistory.length}</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3">
|
||||
<p className="text-xs text-slate-600 font-medium">Current Plan</p>
|
||||
<p className="text-lg font-bold text-slate-700">{biker.membershipType}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Bike ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Plan</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Amount</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{rentHistory.map(rent => (
|
||||
<tr key={rent.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rent.date}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rent.bikeId}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rent.plan}</td>
|
||||
<td className="px-4 py-3 text-sm font-semibold text-slate-700">৳{rent.amount}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${rent.status === 'paid'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-amber-100 text-amber-700'
|
||||
}`}>
|
||||
{rent.status === 'paid' && <CheckCircle className="w-3 h-3" />}
|
||||
{rent.status === 'pending' && <Clock className="w-3 h-3" />}
|
||||
{rent.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{rent.status === 'pending' ? (
|
||||
<button className="px-3 py-1.5 bg-accent text-white text-xs rounded-lg hover:bg-accent-dark">
|
||||
Collect
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-slate-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'bike' && (
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<BikeIcon className="w-5 h-5 text-accent" /> Rented Bikes Details
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button className="px-3 py-1.5 bg-accent text-white text-sm rounded-lg hover:bg-accent-dark flex items-center gap-1">
|
||||
<Edit className="w-4 h-4" /> Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-lg bg-slate-100 flex items-center justify-center">
|
||||
<BikeIcon className="w-8 h-8 text-slate-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-700">{biker.currentBike || 'No Bike Assigned'}</p>
|
||||
<p className="text-sm text-slate-500">Plate: {biker.bikePlate || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-slate-700 mb-4">Mileage</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm text-slate-500 block mb-1">Current Odometer (km)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
placeholder="Enter current reading"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-500 block mb-1">Last Updated</label>
|
||||
<p className="text-sm text-slate-700">{biker.lastRideAt || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-500 block mb-1">Total Distance</label>
|
||||
<p className="text-lg font-bold text-blue-600">{biker.totalDistance.toLocaleString()} km</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-slate-700 mb-4">Battery Health</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm text-slate-500 block mb-1">Battery Percentage</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="number"
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
placeholder="0-100"
|
||||
/>
|
||||
<span className="text-slate-500">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-500 block mb-1">Health Status</label>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
|
||||
<CheckCircle className="w-4 h-4" /> Good
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-500 block mb-1">Estimated Range</label>
|
||||
<p className="text-lg font-bold text-green-600">85 km</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-6">
|
||||
<h4 className="font-medium text-slate-700 mb-4">Bike Images</h4>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
|
||||
<Image className="w-8 h-8 text-slate-400 mb-2" />
|
||||
<span className="text-xs text-slate-500">Front</span>
|
||||
</div>
|
||||
<div className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
|
||||
<Image className="w-8 h-8 text-slate-400 mb-2" />
|
||||
<span className="text-xs text-slate-500">Back</span>
|
||||
</div>
|
||||
<div className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
|
||||
<Image className="w-8 h-8 text-slate-400 mb-2" />
|
||||
<span className="text-xs text-slate-500">Left Side</span>
|
||||
</div>
|
||||
<div className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
|
||||
<Image className="w-8 h-8 text-slate-400 mb-2" />
|
||||
<span className="text-xs text-slate-500">Right Side</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="mt-4 px-4 py-2 border border-dashed border-slate-300 rounded-lg text-sm text-slate-500 hover:border-accent hover:text-accent w-full">
|
||||
+ Upload Images
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-slate-700 mb-4">Notes</h4>
|
||||
<textarea
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
rows={4}
|
||||
placeholder="Add notes about the bike condition, issues, etc..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h4 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-accent" /> Rented Bikes History
|
||||
</h4>
|
||||
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Bike</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Plate</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Plan</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Start Date</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">End Date</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Days</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Total Rent</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{rentedBikesHistory.map(rental => (
|
||||
<tr key={rental.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<BikeIcon className="w-4 h-4 text-slate-400" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700">{rental.bikeName}</p>
|
||||
<p className="text-xs text-slate-400">{rental.bikeId}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rental.plate}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rental.plan}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rental.startDate}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rental.endDate || '-'}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{rental.totalDays}</td>
|
||||
<td className="px-4 py-3 text-sm font-semibold text-green-600">৳{rental.totalRent}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${rental.status === 'active'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{rental.status === 'active' && <Activity className="w-3 h-3" />}
|
||||
{rental.status === 'returned' && <CheckCircle className="w-3 h-3" />}
|
||||
{rental.status === 'active' ? 'Active' : 'Returned'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'stats' && (
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
|
||||
@@ -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() {
|
||||
<option value="pending">Pending</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="blocked">Blocked</option>
|
||||
<option value="rent_pending" className="text-amber-600">Rent Pending</option>
|
||||
</select>
|
||||
<select
|
||||
value={kycFilter}
|
||||
@@ -626,7 +640,7 @@ export default function BikersPage() {
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Financial</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Member</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">KYC</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rent Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -678,18 +692,27 @@ export default function BikersPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[biker.status]}`}>
|
||||
{biker.status === 'active' && <Activity className="w-3 h-3" />}
|
||||
{biker.status === 'pending' && <Clock className="w-3 h-3" />}
|
||||
{biker.status === 'blocked' && <Ban className="w-3 h-3" />}
|
||||
{biker.status}
|
||||
</span>
|
||||
{biker.rating > 0 && (
|
||||
<p className="text-xs text-amber-600 mt-1 flex items-center gap-1">★ {biker.rating} ({biker.totalRatings})</p>
|
||||
{(biker.pendingRent && biker.pendingRent > 0) ? (
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700">
|
||||
<AlertTriangle className="w-3 h-3" /> Pending
|
||||
</span>
|
||||
<p className="text-xs text-amber-600 mt-1">৳{biker.pendingRent} ({biker.pendingRentDays}d)</p>
|
||||
</div>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
|
||||
<CheckCircle className="w-3 h-3" /> Clear
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<a href={`tel:${biker.phone}`} className="p-2 hover:bg-green-50 rounded-lg" title="Call">
|
||||
<PhoneCall className="w-4 h-4 text-green-600" />
|
||||
</a>
|
||||
<a href={`sms:${biker.phone}`} className="p-2 hover:bg-blue-50 rounded-lg" title="Message">
|
||||
<MessageCircle className="w-4 h-4 text-blue-500" />
|
||||
</a>
|
||||
<button onClick={() => handleViewDetails(biker)} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
|
||||
<Eye className="w-4 h-4 text-blue-500" />
|
||||
</button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import type { Investor } from '@/data/mockData';
|
||||
import {
|
||||
ArrowLeft, Wallet, TrendingUp, Banknote, Calendar, Phone, Mail, MapPin, Edit, Trash2, Plus, X, Bike,
|
||||
User, FileText, CreditCard, DollarSign, Clock, ChevronDown, ExternalLink, Download, Upload,
|
||||
AlertTriangle, Shield, Star, CheckCircle, XCircle, Search, Filter
|
||||
AlertTriangle, Shield, Star, CheckCircle, XCircle, Search, Filter, BookOpen, ArrowRight, Printer
|
||||
} from 'lucide-react';
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
@@ -54,6 +54,10 @@ export default function InvestorDetailPage() {
|
||||
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
|
||||
const [selectedBikeId, setSelectedBikeId] = useState('');
|
||||
const [showCreateInvestmentModal, setShowCreateInvestmentModal] = useState(false);
|
||||
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
||||
const [showJournalModal, setShowJournalModal] = useState(false);
|
||||
const [selectedInvoice, setSelectedInvoice] = useState<any>(null);
|
||||
const [investorJournals, setInvestorJournals] = useState<any[]>([]);
|
||||
const [showBankModal, setShowBankModal] = useState(false);
|
||||
const [showMobileBankingModal, setShowMobileBankingModal] = useState(false);
|
||||
const [showTaxModal, setShowTaxModal] = useState(false);
|
||||
@@ -101,8 +105,40 @@ export default function InvestorDetailPage() {
|
||||
};
|
||||
|
||||
const handleCreateInvestment = () => {
|
||||
const invId = `ip${Date.now()}`;
|
||||
const transactionId = `INV/${new Date().getFullYear()}/${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`;
|
||||
const invId = `INV-${Date.now()}`;
|
||||
const year = new Date().getFullYear();
|
||||
const transactionRef = newInvestment.transactionReference || `INV/${year}/${String(investor.investments.length + 1).padStart(4, '0')}`;
|
||||
|
||||
const getDebitAccount = (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: '1200', name: 'Bank - City Bank' };
|
||||
}
|
||||
};
|
||||
|
||||
const debitAccount = getDebitAccount(newInvestment.paymentMethod);
|
||||
|
||||
const journalEntry = {
|
||||
entryId: `JE-${Date.now()}`,
|
||||
date: newInvestment.startDate,
|
||||
reference: transactionRef,
|
||||
description: `${investor.name} - ${newInvestment.planName}`,
|
||||
entries: [
|
||||
{ accountCode: debitAccount.code, accountName: debitAccount.name, debit: newInvestment.totalInvestment, credit: 0 },
|
||||
{ accountCode: '2200', accountName: 'Investor Liabilities', debit: 0, credit: newInvestment.totalInvestment },
|
||||
],
|
||||
isAuto: true,
|
||||
sourceType: 'investor_funding',
|
||||
createdAt: new Date().toISOString(),
|
||||
type: 'investment',
|
||||
amount: newInvestment.totalInvestment,
|
||||
paymentMethod: newInvestment.paymentMethod
|
||||
};
|
||||
|
||||
setInvestorJournals([journalEntry, ...investorJournals]);
|
||||
|
||||
console.log('Creating Investment:', {
|
||||
id: invId,
|
||||
@@ -110,23 +146,26 @@ export default function InvestorDetailPage() {
|
||||
...newInvestment,
|
||||
actualEarnings: 0,
|
||||
status: 'active' as const,
|
||||
transactionId: transactionId,
|
||||
transactionId: transactionRef,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log('Accounting Entry:', {
|
||||
entryId: `AC-${Date.now()}`,
|
||||
type: 'investment_created',
|
||||
investorId: investor.id,
|
||||
investmentId: invId,
|
||||
amount: newInvestment.totalInvestment,
|
||||
debitAccount: 'Investment Asset - Investor',
|
||||
creditAccount: newInvestment.paymentMethod === 'bank' ? 'Bank Account' : 'Cash Account',
|
||||
transactionRef: transactionId,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
alert(`Investment created successfully!
|
||||
|
||||
alert(`Investment created successfully!\n\nInvestment ID: ${invId}\nTransaction Ref: ${transactionId}\nAmount: ৳${newInvestment.totalInvestment.toLocaleString()}\n\nAccounting entries have been logged.`);
|
||||
Investor: ${investor.name}
|
||||
Investment ID: ${invId}
|
||||
Transaction Ref: ${transactionRef}
|
||||
Amount: ৳${newInvestment.totalInvestment.toLocaleString()}
|
||||
|
||||
Accounting Entry Created (Auto-Journal):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Date: ${newInvestment.startDate}
|
||||
Ref: ${transactionRef}
|
||||
Description: ${investor.name} - ${newInvestment.planName}
|
||||
|
||||
Debit (Dr): ${debitAccount.name} ৳${newInvestment.totalInvestment.toLocaleString()}
|
||||
Credit (Cr): Investor Liability ৳${newInvestment.totalInvestment.toLocaleString()}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||
setShowCreateInvestmentModal(false);
|
||||
setNewInvestment({
|
||||
planName: '',
|
||||
@@ -745,7 +784,59 @@ export default function InvestorDetailPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700">{tx.description}</p>
|
||||
<p className="text-xs text-slate-400">{tx.createdAt} {tx.referenceNumber && `• Ref: ${tx.referenceNumber}`}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs text-slate-400">{tx.createdAt}</p>
|
||||
{tx.referenceNumber && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const getAccountingInfo = (type: string, amount: number) => {
|
||||
if (type === 'investment') {
|
||||
return {
|
||||
debitAccount: 'Bank - City Bank (1200)',
|
||||
creditAccount: 'Investor Liabilities (2200)',
|
||||
amount: amount
|
||||
};
|
||||
} else if (type === 'earning' || type === 'bike_earning' || type === 'investment_return') {
|
||||
return {
|
||||
debitAccount: 'Investor Liabilities (2200)',
|
||||
creditAccount: 'Rental Income (4200)',
|
||||
amount: amount
|
||||
};
|
||||
} else if (type === 'withdrawal') {
|
||||
return {
|
||||
debitAccount: 'Investor Liabilities (2200)',
|
||||
creditAccount: 'Bank - City Bank (1200)',
|
||||
amount: amount
|
||||
};
|
||||
}
|
||||
return {
|
||||
debitAccount: 'N/A',
|
||||
creditAccount: 'N/A',
|
||||
amount: amount
|
||||
};
|
||||
};
|
||||
const accounting = getAccountingInfo(tx.type, tx.amount);
|
||||
setSelectedInvoice({
|
||||
reference: tx.referenceNumber,
|
||||
date: tx.createdAt,
|
||||
type: tx.type,
|
||||
amount: tx.amount,
|
||||
description: tx.description,
|
||||
status: tx.status,
|
||||
investorName: investor.name,
|
||||
investorId: investor.id,
|
||||
debitAccount: accounting.debitAccount,
|
||||
creditAccount: accounting.creditAccount
|
||||
});
|
||||
setShowInvoiceModal(true);
|
||||
}}
|
||||
className="text-xs text-investor hover:underline flex items-center gap-1"
|
||||
>
|
||||
• Ref: {tx.referenceNumber}
|
||||
<BookOpen className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@@ -1034,14 +1125,52 @@ export default function InvestorDetailPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h4 className="font-semibold text-blue-800 mb-2">Accounting Entries to be Created:</h4>
|
||||
<div className="text-sm text-blue-700 space-y-1">
|
||||
<p>• Debit: Investment Asset - Investor ({newInvestment.totalInvestment ? `৳${newInvestment.totalInvestment.toLocaleString()}` : '৳0'})</p>
|
||||
<p>• Credit: {newInvestment.paymentMethod === 'bank' ? 'Bank Account' : newInvestment.paymentMethod === 'mobile' ? 'Mobile Wallet' : newInvestment.paymentMethod === 'cash' ? 'Cash Account' : 'Cheque Receivable'} ({newInvestment.totalInvestment ? `৳${newInvestment.totalInvestment.toLocaleString()}` : '৳0'})</p>
|
||||
<p>• Transaction ID will be auto-generated</p>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const getDebitAccountDisplay = (method: string) => {
|
||||
switch (method) {
|
||||
case 'bank': return 'Bank - City Bank (1200)';
|
||||
case 'cash': return 'Cash in Hand (1100)';
|
||||
case 'mobile': return 'bKash Business (1300)';
|
||||
case 'cheque': return 'Cheque Receivable (1410)';
|
||||
default: return 'Bank - City Bank (1200)';
|
||||
}
|
||||
};
|
||||
const debitDisplay = getDebitAccountDisplay(newInvestment.paymentMethod);
|
||||
|
||||
return (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
<h4 className="font-semibold text-green-800">Auto-Journal Entry</h4>
|
||||
</div>
|
||||
<div className="text-sm text-green-700 space-y-2">
|
||||
<p className="text-xs text-green-600 uppercase font-medium mb-2">Double EntryAccounting</p>
|
||||
<div className="flex items-center justify-between bg-white rounded p-2 border border-green-100">
|
||||
<div>
|
||||
<p className="font-medium">Debit (Dr)</p>
|
||||
<p className="text-xs text-green-600">{debitDisplay}</p>
|
||||
</div>
|
||||
<p className="font-bold text-green-700">৳{newInvestment.totalInvestment.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<div className="w-6 h-6 rounded-full bg-green-200 flex items-center justify-center">
|
||||
<span className="text-green-600 text-xs">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white rounded p-2 border border-green-100">
|
||||
<div>
|
||||
<p className="font-medium">Credit (Cr)</p>
|
||||
<p className="text-xs text-green-600">Investor Liabilities (2200)</p>
|
||||
</div>
|
||||
<p className="font-bold text-green-700">৳{newInvestment.totalInvestment.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="mt-3 pt-2 border-t border-green-200">
|
||||
<p className="text-xs text-green-600">Transaction Ref: Auto Generate</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
|
||||
@@ -1212,6 +1341,202 @@ export default function InvestorDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showJournalModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold text-slate-800">Journal Records</h2>
|
||||
<button onClick={() => setShowJournalModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-5 overflow-y-auto flex-1">
|
||||
{investorJournals.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{investorJournals.map((journal, idx) => (
|
||||
<div key={idx} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-slate-500">{journal.reference}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700">Auto</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{journal.date}</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between bg-white p-2 rounded border border-slate-200">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Debit (Dr)</p>
|
||||
<p className="text-xs text-slate-500">Bank - City Bank (1200)</p>
|
||||
</div>
|
||||
<p className="font-bold text-green-600">৳{journal.amount.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-0.5">
|
||||
<ArrowRight className="w-4 h-4 text-slate-400 rotate-90" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white p-2 rounded border border-slate-200">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Credit (Cr)</p>
|
||||
<p className="text-xs text-slate-500">Investor Liabilities (2200)</p>
|
||||
</div>
|
||||
<p className="font-bold text-red-600">৳{journal.amount.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-slate-200 text-xs text-slate-500">
|
||||
{journal.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-slate-400">
|
||||
<BookOpen className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No journal records yet</p>
|
||||
<p className="text-sm">Create an investment to see auto-journal entries</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showInvoiceModal && selectedInvoice && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold text-slate-800">Invoice</h2>
|
||||
<button onClick={() => setShowInvoiceModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6" id="invoice-content">
|
||||
<div className="text-center border-b border-slate-200 pb-4 mb-4">
|
||||
<h1 className="text-xl font-extrabold text-investor">JAIBEN Mobility Ltd</h1>
|
||||
<p className="text-xs text-slate-500">EV Rental & Investment Company</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">Invoice No</p>
|
||||
<p className="text-sm font-medium">{selectedInvoice.reference}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-slate-500">Date</p>
|
||||
<p className="text-sm font-medium">{selectedInvoice.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-xs text-slate-500">Investor</p>
|
||||
<p className="text-sm font-medium">{selectedInvoice.investorName}</p>
|
||||
<p className="text-xs text-slate-400">{selectedInvoice.investorId}</p>
|
||||
</div>
|
||||
|
||||
<table className="w-full mb-4">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-2 text-xs text-slate-500">Description</th>
|
||||
<th className="text-right py-2 text-xs text-slate-500">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-slate-100">
|
||||
<td className="py-3 text-sm">{selectedInvoice.description}</td>
|
||||
<td className="py-3 text-sm text-right font-medium">৳{selectedInvoice.amount.toLocaleString()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td className="py-2 text-sm font-medium">Total</td>
|
||||
<td className="py-2 text-sm font-bold text-right">৳{selectedInvoice.amount.toLocaleString()}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-xs text-slate-500">Status</span>
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
selectedInvoice.status === 'completed' ? 'bg-green-100 text-green-700' :
|
||||
selectedInvoice.status === 'pending' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{selectedInvoice.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-slate-200">
|
||||
<p className="text-xs text-slate-500 mb-2">Double Entry Accounting (Journal)</p>
|
||||
<div className="bg-slate-50 rounded-lg p-3 border border-slate-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium">Debit (Dr)</p>
|
||||
<p className="text-xs text-slate-500">{selectedInvoice.debitAccount || 'N/A'}</p>
|
||||
</div>
|
||||
<p className="text-sm font-bold">৳{selectedInvoice.amount.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<ArrowRight className="w-4 h-4 text-slate-400 rotate-90" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-medium">Credit (Cr)</p>
|
||||
<p className="text-xs text-slate-500">{selectedInvoice.creditAccount || 'N/A'}</p>
|
||||
</div>
|
||||
<p className="text-sm font-bold">৳{selectedInvoice.amount.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-slate-200 text-center">
|
||||
<p className="text-xs text-slate-400">Thank you for your investment!</p>
|
||||
<p className="text-xs text-slate-400">Generated on {new Date().toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 border-t border-slate-100 flex justify-between">
|
||||
<button
|
||||
onClick={() => window.print()}
|
||||
className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark flex items-center gap-2"
|
||||
>
|
||||
<Printer className="w-4 h-4" /> Print
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
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);
|
||||
doc.text('Invoice: ' + selectedInvoice.reference, 20, 30);
|
||||
doc.text('Date: ' + selectedInvoice.date, 20, 38);
|
||||
doc.text('Investor: ' + selectedInvoice.investorName, 20, 46);
|
||||
doc.text('Description: ' + selectedInvoice.description, 20, 54);
|
||||
doc.text('Amount: ৳' + selectedInvoice.amount.toLocaleString(), 20, 64);
|
||||
doc.text('Status: ' + selectedInvoice.status, 20, 72);
|
||||
if (selectedInvoice.debitAccount) {
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(100);
|
||||
doc.text('Double Entry Accounting:', 20, 85);
|
||||
doc.text('Dr: ' + selectedInvoice.debitAccount + ' ৳' + selectedInvoice.amount.toLocaleString(), 20, 93);
|
||||
doc.text('Cr: ' + selectedInvoice.creditAccount + ' ৳' + selectedInvoice.amount.toLocaleString(), 20, 101);
|
||||
}
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(150);
|
||||
doc.text('Thank you for your investment!', 20, 115);
|
||||
doc.save(`invoice-${selectedInvoice.reference}.pdf`);
|
||||
});
|
||||
}}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2"
|
||||
>
|
||||
<Download className="w-4 h-4" /> PDF
|
||||
</button>
|
||||
<button onClick={() => setShowInvoiceModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
695
src/app/admin/kyc/[id]/page.tsx
Normal file
695
src/app/admin/kyc/[id]/page.tsx
Normal file
@@ -0,0 +1,695 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import {
|
||||
Shield, Check, Clock, Bike, User, Phone,
|
||||
MapPin, FileText, Image, DollarSign, Wrench, Battery,
|
||||
CheckCircle, XCircle, ArrowLeft, Save, Printer, Send,
|
||||
MessageSquare, Edit, UserCheck, Wallet, Store, Globe, Calendar, Briefcase, Plus, Upload
|
||||
} from 'lucide-react';
|
||||
|
||||
type ApplicationSource = 'app' | 'web' | 'walkin' | 'referral';
|
||||
type KYCType = 'biker' | 'investor' | 'shop' | 'merchant' | 'general';
|
||||
type RiderPlan = 'daily_rent' | 'weekly_rent' | 'monthly_rent' | 'rent_to_own' | 'share_ev';
|
||||
type VerificationStage = 'application' | 'document_collection' | 'risk_check' | 'plan_selection' | 'payment' | 'agreement' | 'allocated' | 'active';
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'pending' | 'uploaded' | 'approved' | 'rejected';
|
||||
imageUrl?: string;
|
||||
rejectedReason?: string;
|
||||
uploadedAt?: string;
|
||||
}
|
||||
|
||||
interface Request {
|
||||
id: string;
|
||||
applicationSource: ApplicationSource;
|
||||
sourceDetails?: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
type: KYCType;
|
||||
status: 'pending' | 'documents_needed' | 'under_review' | 'risk_check' | 'approved' | 'rejected';
|
||||
verificationStage: VerificationStage;
|
||||
submittedAt: string;
|
||||
location: string;
|
||||
address: string;
|
||||
requiredDocuments: Document[];
|
||||
riderPlan?: RiderPlan;
|
||||
nomineeDetails?: { name: string; phone: string; relationship: string; nid: string };
|
||||
employmentInfo?: { company: string; dailyEarning: number; whyEV: string; experience: string };
|
||||
riskCheck?: { nidVerified: boolean; nomineeNidVerified: boolean; deliveryPlatformStatus: string; paymentReliability: string; notes: string };
|
||||
agreement?: { dailyRentObligation: number; latePenalty: number; signedAt?: string };
|
||||
evAllocation?: { evId: string; bikeModel: string; batteryId: string; hubLocation: string; gpsActivated: boolean };
|
||||
securityDeposit?: number;
|
||||
advancePayment?: number;
|
||||
paymentMethod?: 'bank' | 'wallet' | 'cash';
|
||||
bikeRequested?: string;
|
||||
scheduleDate?: string;
|
||||
notes: string[];
|
||||
messageHistory: { date: string; message: string; from: 'admin' | 'user' }[];
|
||||
}
|
||||
|
||||
const mockRequests: Request[] = [
|
||||
{
|
||||
id: 'REQ001',
|
||||
applicationSource: 'app',
|
||||
name: 'Rahim Ahmed',
|
||||
phone: '01712345678',
|
||||
email: 'rahim@email.com',
|
||||
type: 'biker',
|
||||
status: 'pending',
|
||||
verificationStage: 'application',
|
||||
submittedAt: '2024-03-20',
|
||||
location: 'Gulshan, Dhaka',
|
||||
address: 'House 12, Road 5, Gulshan 1',
|
||||
requiredDocuments: [
|
||||
{ id: 'd1', name: 'NID Front', status: 'uploaded', uploadedAt: '2024-03-20' },
|
||||
{ id: 'd2', name: 'NID Back', status: 'uploaded', uploadedAt: '2024-03-20' },
|
||||
{ id: 'd3', name: 'Driving License', status: 'pending' },
|
||||
{ id: 'd4', name: 'Profile Photo', status: 'uploaded', uploadedAt: '2024-03-20' },
|
||||
],
|
||||
riderPlan: 'daily_rent',
|
||||
employmentInfo: { company: 'Foodpanda', dailyEarning: 2500, whyEV: 'Low maintenance, good for delivery', experience: '3 years bike riding' },
|
||||
nomineeDetails: { name: 'Fatema', phone: '01712345699', relationship: 'Wife', nid: '1234567890123' },
|
||||
securityDeposit: 5000,
|
||||
advancePayment: 500,
|
||||
bikeRequested: 'AIMA Lightning',
|
||||
notes: ['Downloaded app and applied through mobile'],
|
||||
messageHistory: [],
|
||||
},
|
||||
{
|
||||
id: 'REQ002',
|
||||
applicationSource: 'walkin',
|
||||
sourceDetails: 'Gulshan Hub',
|
||||
name: 'Karim Hasan',
|
||||
phone: '01712345679',
|
||||
email: 'karim@email.com',
|
||||
type: 'investor',
|
||||
status: 'documents_needed',
|
||||
verificationStage: 'document_collection',
|
||||
submittedAt: '2024-03-19',
|
||||
location: 'Banani, Dhaka',
|
||||
address: 'Flat 3B, House 22, Banani',
|
||||
requiredDocuments: [
|
||||
{ id: 'd5', name: 'NID', status: 'uploaded', uploadedAt: '2024-03-19' },
|
||||
{ id: 'd6', name: 'TIN Certificate', status: 'pending' },
|
||||
{ id: 'd7', name: 'Bank Statement', status: 'pending' },
|
||||
],
|
||||
notes: ['Walked in at Gulshan office - referred by current biker'],
|
||||
messageHistory: [],
|
||||
},
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: 'bg-amber-100 text-amber-700',
|
||||
documents_needed: 'bg-orange-100 text-orange-700',
|
||||
under_review: 'bg-blue-100 text-blue-700',
|
||||
risk_check: 'bg-purple-100 text-purple-700',
|
||||
approved: 'bg-green-100 text-green-700',
|
||||
rejected: 'bg-red-100 text-red-700',
|
||||
};
|
||||
|
||||
const stageLabels: Record<string, string> = {
|
||||
application: 'Application',
|
||||
document_collection: 'Documents',
|
||||
risk_check: 'Risk Check',
|
||||
plan_selection: 'Plan Selection',
|
||||
payment: 'Payment',
|
||||
agreement: 'Agreement',
|
||||
allocated: 'EV Allocated',
|
||||
active: 'Active',
|
||||
};
|
||||
|
||||
const sourceLabels: Record<string, string> = {
|
||||
app: 'Mobile App',
|
||||
web: 'Website',
|
||||
walkin: 'Walk-in',
|
||||
referral: 'Referral',
|
||||
};
|
||||
|
||||
const planLabels: Record<string, string> = {
|
||||
daily_rent: 'Daily Rent',
|
||||
weekly_rent: 'Weekly Rent',
|
||||
monthly_rent: 'Monthly Rent',
|
||||
rent_to_own: 'Rent-to-Own',
|
||||
share_ev: 'Share EV',
|
||||
};
|
||||
|
||||
const typeIcons: Record<string, any> = {
|
||||
biker: Bike,
|
||||
investor: DollarSign,
|
||||
shop: Store,
|
||||
merchant: User,
|
||||
};
|
||||
|
||||
export default function KYCDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
const [request, setRequest] = useState<Request | null>(null);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editForm, setEditForm] = useState<Partial<Request>>({});
|
||||
const [showMessageModal, setShowMessageModal] = useState(false);
|
||||
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
||||
const [newNoteText, setNewNoteText] = useState('');
|
||||
const [newMessageText, setNewMessageText] = useState('');
|
||||
const [showAddDocModal, setShowAddDocModal] = useState(false);
|
||||
const [newDocName, setNewDocName] = useState('');
|
||||
const [showUploadDocModal, setShowUploadDocModal] = useState(false);
|
||||
const [uploadDocId, setUploadDocId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const found = mockRequests.find(r => r.id === id);
|
||||
if (found) {
|
||||
setRequest(found);
|
||||
setEditForm(found);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (!request) {
|
||||
return (
|
||||
<div className="p-6 flex items-center justify-center min-h-[50vh]">
|
||||
<div className="text-center">
|
||||
<Shield className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-500">Request not found</p>
|
||||
<button
|
||||
onClick={() => router.push('/admin/kyc')}
|
||||
className="mt-4 px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
||||
>
|
||||
Back to KYC
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
setRequest(prev => prev ? { ...prev, ...editForm } : null);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleAddNote = () => {
|
||||
if (!request || !newNoteText.trim()) return;
|
||||
setRequest(prev => prev ? { ...prev, notes: [...prev.notes, newNoteText] } : null);
|
||||
setNewNoteText('');
|
||||
setShowAddNoteModal(false);
|
||||
};
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!request || !newMessageText.trim()) return;
|
||||
setRequest(prev => prev ? {
|
||||
...prev,
|
||||
messageHistory: [...prev.messageHistory, { date: new Date().toISOString().split('T')[0], message: newMessageText, from: 'admin' as const }]
|
||||
} : null);
|
||||
setNewMessageText('');
|
||||
setShowMessageModal(false);
|
||||
};
|
||||
|
||||
const handleAddDocument = () => {
|
||||
if (!request || !newDocName.trim()) return;
|
||||
setRequest(prev => prev ? {
|
||||
...prev,
|
||||
requiredDocuments: [...prev.requiredDocuments, { id: `doc-${Date.now()}`, name: newDocName, status: 'pending' as const }]
|
||||
} : null);
|
||||
setNewDocName('');
|
||||
setShowAddDocModal(false);
|
||||
};
|
||||
|
||||
const handleApproveDocument = (docId: string) => {
|
||||
if (!request) return;
|
||||
setRequest(prev => prev ? {
|
||||
...prev,
|
||||
requiredDocuments: prev.requiredDocuments.map(doc =>
|
||||
doc.id === docId ? { ...doc, status: 'approved' as const } : doc
|
||||
)
|
||||
} : null);
|
||||
};
|
||||
|
||||
const handleRejectDocument = (docId: string) => {
|
||||
const reason = prompt('Enter rejection reason:');
|
||||
if (reason) {
|
||||
setRequest(prev => prev ? {
|
||||
...prev,
|
||||
requiredDocuments: prev.requiredDocuments.map(doc =>
|
||||
doc.id === docId ? { ...doc, status: 'rejected' as const, rejectedReason: reason } : doc
|
||||
)
|
||||
} : null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadDocument = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || !uploadDocId || !request) return;
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
setRequest(prev => prev ? {
|
||||
...prev,
|
||||
requiredDocuments: prev.requiredDocuments.map(doc =>
|
||||
doc.id === uploadDocId ? { ...doc, status: 'uploaded' as const, imageUrl, uploadedAt: new Date().toISOString() } : doc
|
||||
)
|
||||
} : null);
|
||||
setShowUploadDocModal(false);
|
||||
};
|
||||
|
||||
const openUploadModal = (docId: string) => {
|
||||
setUploadDocId(docId);
|
||||
setShowUploadDocModal(true);
|
||||
};
|
||||
|
||||
const TypeIcon = typeIcons[request.type];
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
||||
<button
|
||||
onClick={() => router.push('/admin/kyc')}
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" /> Back to KYC
|
||||
</button>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-extrabold text-slate-800">{request.id}</h1>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[request.status]}`}>
|
||||
{request.status.replace('_', ' ')}
|
||||
</span>
|
||||
{request.type === 'biker' && request.verificationStage && (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-cyan-100 text-cyan-700">
|
||||
{stageLabels[request.verificationStage]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-500 mt-1">{request.name} • {request.submittedAt}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{editMode ? (
|
||||
<>
|
||||
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
||||
<Save className="w-4 h-4" /> Save
|
||||
</button>
|
||||
<button onClick={() => { setEditForm(request); setEditMode(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{request.type === 'biker' && request.status !== 'approved' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Approve this request and create biker profile?')) {
|
||||
setRequest(prev => prev ? { ...prev, status: 'approved', verificationStage: 'active' } : null);
|
||||
alert('Biker created successfully!');
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
|
||||
>
|
||||
<Bike className="w-4 h-4" /> Make Biker
|
||||
</button>
|
||||
)}
|
||||
{request.type === 'investor' && request.status !== 'approved' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Approve this request and create investor profile?')) {
|
||||
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
|
||||
alert('Investor created successfully!');
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
|
||||
>
|
||||
<DollarSign className="w-4 h-4" /> Make Investor
|
||||
</button>
|
||||
)}
|
||||
{request.type === 'shop' && request.status !== 'approved' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Approve this request and create shop profile?')) {
|
||||
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
|
||||
alert('Shop created successfully!');
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
|
||||
>
|
||||
<Store className="w-4 h-4" /> Make Shop
|
||||
</button>
|
||||
)}
|
||||
{request.type === 'merchant' && request.status !== 'approved' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Approve this request and create merchant profile?')) {
|
||||
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
|
||||
alert('Merchant created successfully!');
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-orange-600 text-white rounded-lg text-sm hover:bg-orange-700 flex items-center gap-2"
|
||||
>
|
||||
<User className="w-4 h-4" /> Make Merchant
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
||||
<Edit className="w-4 h-4" /> Edit
|
||||
</button>
|
||||
<button onClick={() => setShowAddNoteModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4" /> Note
|
||||
</button>
|
||||
<button onClick={() => setShowMessageModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
||||
<Send className="w-4 h-4" /> Message
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
|
||||
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
|
||||
<User className="w-5 h-5" /> Personal Info
|
||||
</h3>
|
||||
{editMode ? (
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.name || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||
placeholder="Full Name"
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.phone || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
|
||||
placeholder="Phone"
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
value={editForm.email || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
|
||||
placeholder="Email"
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.location || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, location: e.target.value })}
|
||||
placeholder="Location"
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Name</span><span className="text-sm font-medium text-blue-800">{request.name}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Phone</span><span className="text-sm font-medium text-blue-800">{request.phone}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Email</span><span className="text-sm font-medium text-blue-800">{request.email || '-'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Location</span><span className="text-sm font-medium text-blue-800">{request.location}</span></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
|
||||
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
|
||||
<Globe className="w-5 h-5" /> Source
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-green-600">Source</span><span className="text-sm font-medium text-green-800">{sourceLabels[request.applicationSource]}</span></div>
|
||||
{request.sourceDetails && <div className="flex justify-between"><span className="text-sm text-green-600">Details</span><span className="text-sm font-medium text-green-800">{request.sourceDetails}</span></div>}
|
||||
<div className="flex justify-between"><span className="text-sm text-green-600">Type</span><span className="text-sm font-medium text-green-800 capitalize flex items-center gap-1"><TypeIcon className="w-4 h-4" /> {request.type}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.type === 'biker' && (
|
||||
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
||||
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5" /> Plan Selection
|
||||
</h3>
|
||||
{editMode ? (
|
||||
<div className="space-y-3">
|
||||
<select
|
||||
value={editForm.riderPlan || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, riderPlan: e.target.value as RiderPlan })}
|
||||
className="w-full px-3 py-2 border border-purple-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Select Plan</option>
|
||||
<option value="daily_rent">Daily Rent</option>
|
||||
<option value="weekly_rent">Weekly Rent</option>
|
||||
<option value="monthly_rent">Monthly Rent</option>
|
||||
<option value="rent_to_own">Rent-to-Own</option>
|
||||
<option value="share_ev">Share EV</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.bikeRequested || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, bikeRequested: e.target.value })}
|
||||
placeholder="Bike Model"
|
||||
className="w-full px-3 py-2 border border-purple-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-purple-600">Plan</span><span className="text-sm font-medium text-purple-800">{request.riderPlan ? planLabels[request.riderPlan] : '-'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-purple-600">Bike</span><span className="text-sm font-medium text-purple-800">{request.bikeRequested || '-'}</span></div>
|
||||
{request.scheduleDate && <div className="flex justify-between"><span className="text-sm text-purple-600">Schedule</span><span className="text-sm font-medium text-purple-800">{request.scheduleDate}</span></div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{request.type === 'biker' && request.nomineeDetails && (
|
||||
<div className="bg-pink-50 p-4 rounded-xl border border-pink-100">
|
||||
<h3 className="font-semibold text-pink-800 mb-3 flex items-center gap-2">
|
||||
<User className="w-5 h-5" /> Nominee Details
|
||||
</h3>
|
||||
{editMode ? (
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.nomineeDetails?.name || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, nomineeDetails: { ...editForm.nomineeDetails!, name: e.target.value } })}
|
||||
placeholder="Nominee Name"
|
||||
className="w-full px-3 py-2 border border-pink-200 rounded-lg text-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.nomineeDetails?.phone || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, nomineeDetails: { ...editForm.nomineeDetails!, phone: e.target.value } })}
|
||||
placeholder="Phone"
|
||||
className="w-full px-3 py-2 border border-pink-200 rounded-lg text-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.nomineeDetails?.relationship || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, nomineeDetails: { ...editForm.nomineeDetails!, relationship: e.target.value } })}
|
||||
placeholder="Relationship"
|
||||
className="w-full px-3 py-2 border border-pink-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-pink-600">Name</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.name}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-pink-600">Phone</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.phone}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-pink-600">Relationship</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.relationship}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-pink-600">NID</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.nid}</span></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold text-amber-800 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" /> Documents ({request.requiredDocuments.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowAddDocModal(true)}
|
||||
className="text-xs px-2 py-1 bg-white rounded border border-amber-200 text-amber-700 hover:bg-amber-100 flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-3 h-3" /> Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{request.requiredDocuments.map((doc) => (
|
||||
<div key={doc.id} className="flex items-center justify-between p-2 bg-white rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
{doc.status === 'pending' && <Clock className="w-4 h-4 text-amber-400" />}
|
||||
{doc.status === 'uploaded' && <FileText className="w-4 h-4 text-blue-400" />}
|
||||
{doc.status === 'approved' && <CheckCircle className="w-4 h-4 text-green-500" />}
|
||||
{doc.status === 'rejected' && <XCircle className="w-4 h-4 text-red-500" />}
|
||||
<span className="text-sm text-slate-700">{doc.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{doc.status === 'pending' && (
|
||||
<button onClick={() => openUploadModal(doc.id)} className="p-1 bg-amber-100 text-amber-600 rounded hover:bg-amber-200" title="Upload"><Upload className="w-4 h-4" /></button>
|
||||
)}
|
||||
{(doc.status === 'uploaded' || doc.status === 'approved') && doc.imageUrl && (
|
||||
<button onClick={() => window.open(doc.imageUrl, '_blank')} className="p-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200" title="View"><Image className="w-4 h-4" /></button>
|
||||
)}
|
||||
{doc.status === 'uploaded' && (
|
||||
<>
|
||||
<button onClick={() => handleApproveDocument(doc.id)} className="p-1 bg-green-100 text-green-600 rounded hover:bg-green-200" title="Approve"><CheckCircle className="w-4 h-4" /></button>
|
||||
<button onClick={() => handleRejectDocument(doc.id)} className="p-1 bg-red-100 text-red-600 rounded hover:bg-red-200" title="Reject"><XCircle className="w-4 h-4" /></button>
|
||||
</>
|
||||
)}
|
||||
{doc.status === 'approved' && <span className="text-xs px-2 py-1 bg-green-100 text-green-700 rounded-full">Approved</span>}
|
||||
{doc.status === 'rejected' && <span className="text-xs px-2 py-1 bg-red-100 text-red-700 rounded-full">Rejected</span>}
|
||||
{doc.status === 'pending' && <span className="text-xs px-2 py-1 bg-amber-100 text-amber-700 rounded-full">Pending</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.type === 'biker' && request.employmentInfo && (
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
||||
<Briefcase className="w-5 h-5" /> Employment
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-slate-600">Company</span><span className="text-sm font-medium text-slate-800">{request.employmentInfo.company}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-slate-600">Daily Earning</span><span className="text-sm font-medium text-slate-800">৳{request.employmentInfo.dailyEarning}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-slate-600">Experience</span><span className="text-sm font-medium text-slate-800">{request.employmentInfo.experience}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5" /> Notes ({request.notes.length})
|
||||
</h3>
|
||||
{request.notes.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{request.notes.map((note, idx) => (
|
||||
<div key={idx} className="text-sm text-slate-600 p-2 bg-white rounded-lg">• {note}</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-400">No notes yet</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-50 p-4 rounded-xl border border-indigo-100">
|
||||
<h3 className="font-semibold text-indigo-800 mb-3 flex items-center gap-2">
|
||||
<Send className="w-5 h-5" /> Messages ({request.messageHistory.length})
|
||||
</h3>
|
||||
{request.messageHistory.length > 0 ? (
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{request.messageHistory.map((msg, idx) => (
|
||||
<div key={idx} className={`text-sm p-2 rounded-lg ${msg.from === 'admin' ? 'bg-blue-100 text-blue-800' : 'bg-white text-slate-600'}`}>
|
||||
<span className="font-medium">{msg.from === 'admin' ? 'Admin' : 'User'}:</span> {msg.message}
|
||||
<span className="text-xs text-slate-400 ml-2">{msg.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-indigo-400">No messages yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showMessageModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800">Send Message</h3>
|
||||
<button onClick={() => setShowMessageModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<textarea
|
||||
value={newMessageText}
|
||||
onChange={(e) => setNewMessageText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
rows={4}
|
||||
placeholder="Type message to send to user..."
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowMessageModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
<button onClick={handleSendMessage} disabled={!newMessageText.trim()} className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddNoteModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800">Add Note</h3>
|
||||
<button onClick={() => setShowAddNoteModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<textarea
|
||||
value={newNoteText}
|
||||
onChange={(e) => setNewNoteText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
rows={4}
|
||||
placeholder="Enter note..."
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowAddNoteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
<button onClick={handleAddNote} disabled={!newNoteText.trim()} className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddDocModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800">Request New Document</h3>
|
||||
<button onClick={() => setShowAddDocModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<input
|
||||
type="text"
|
||||
value={newDocName}
|
||||
onChange={(e) => setNewDocName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
placeholder="Document name..."
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowAddDocModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
<button onClick={handleAddDocument} disabled={!newDocName.trim()} className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50">Add Document</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showUploadDocModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800">Upload Document</h3>
|
||||
<button onClick={() => setShowUploadDocModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,.pdf"
|
||||
onChange={handleUploadDocument}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm file:mr-4 file:px-4 file:py-2 file:bg-accent file:text-white file:rounded-lg file:border-0 cursor-pointer"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-2">Supported: JPG, PNG, PDF</p>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowUploadDocModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
941
src/app/admin/maintenance/[id]/page.tsx
Normal file
941
src/app/admin/maintenance/[id]/page.tsx
Normal file
@@ -0,0 +1,941 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import {
|
||||
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
|
||||
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
|
||||
CheckCircle, XCircle, ChevronLeft, Save, Printer, Send, QrCode,
|
||||
Wallet, Building, Edit, MessageSquare, Calendar, ArrowLeft
|
||||
} from 'lucide-react';
|
||||
|
||||
type TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense';
|
||||
|
||||
type MaintenanceType = 'damage' | 'repair' | 'service' | 'battery_swap' | 'inspection' | 'other';
|
||||
type DamageSeverity = 'critical' | 'major' | 'minor' | 'cosmetic';
|
||||
type MaintenanceStatus = 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
|
||||
type PaymentStatus = 'pending' | 'approved' | 'paid' | 'rejected';
|
||||
|
||||
interface MaintenanceRecord {
|
||||
id: string;
|
||||
date: string;
|
||||
type: MaintenanceType;
|
||||
severity: DamageSeverity;
|
||||
status: MaintenanceStatus;
|
||||
paymentStatus: PaymentStatus;
|
||||
bikeId: string;
|
||||
bikeModel: string;
|
||||
bikePlate: string;
|
||||
batteryId?: string;
|
||||
reporterId: string;
|
||||
reporterName: string;
|
||||
reporterPhone: string;
|
||||
reporterRole: 'biker' | 'staff' | 'hub';
|
||||
description: string;
|
||||
location: string;
|
||||
estimatedCost: number;
|
||||
actualCost?: number;
|
||||
partsUsed?: string[];
|
||||
images: { id: string; name: string; url: string; uploadedAt: string }[];
|
||||
assignedTo?: string;
|
||||
notes: string[];
|
||||
resolvedAt?: string;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
const mockMaintenance: MaintenanceRecord[] = [
|
||||
{
|
||||
id: 'MNT-001',
|
||||
date: '2024-03-21',
|
||||
type: 'damage',
|
||||
severity: 'major',
|
||||
status: 'in_progress',
|
||||
paymentStatus: 'approved',
|
||||
bikeId: 'EV-004',
|
||||
bikeModel: 'AIMA Lightning',
|
||||
bikePlate: 'Dhaka Metro Cha-5679',
|
||||
batteryId: 'BAT-044',
|
||||
reporterId: 'BIKER-004',
|
||||
reporterName: 'Sofiq Rahman',
|
||||
reporterPhone: '01712345681',
|
||||
reporterRole: 'biker',
|
||||
description: 'Front fender damaged in accident at Gulshan signal',
|
||||
location: 'Gulshan, Dhaka',
|
||||
estimatedCost: 3500,
|
||||
actualCost: 3200,
|
||||
partsUsed: ['Front fender', 'Mounting brackets'],
|
||||
images: [
|
||||
{ id: 'img1', name: 'Damage Front', url: '', uploadedAt: '2024-03-21' },
|
||||
{ id: 'img2', name: 'Damage Side', url: '', uploadedAt: '2024-03-21' },
|
||||
],
|
||||
assignedTo: 'Service Center A',
|
||||
notes: ['Parts ordered from supplier'],
|
||||
createdAt: '2024-03-21T10:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
{
|
||||
id: 'MNT-002',
|
||||
date: '2024-03-20',
|
||||
type: 'service',
|
||||
severity: 'minor',
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
bikeId: 'EV-002',
|
||||
bikeModel: 'Yadea DT3',
|
||||
bikePlate: 'Dhaka Metro Ba-1234',
|
||||
batteryId: 'BAT-021',
|
||||
reporterId: 'BIKER-002',
|
||||
reporterName: 'Karim Hasan',
|
||||
reporterPhone: '01712345679',
|
||||
reporterRole: 'biker',
|
||||
description: 'Routine service - brake adjustment and chain lubrication',
|
||||
location: 'Banani Hub',
|
||||
estimatedCost: 500,
|
||||
actualCost: 450,
|
||||
images: [],
|
||||
notes: ['Service completed'],
|
||||
resolvedAt: '2024-03-20T14:00:00',
|
||||
createdAt: '2024-03-20T08:00:00',
|
||||
createdBy: 'Hub Staff',
|
||||
},
|
||||
{
|
||||
id: 'MNT-003',
|
||||
date: '2024-03-19',
|
||||
type: 'battery_swap',
|
||||
severity: 'minor',
|
||||
status: 'completed',
|
||||
paymentStatus: 'pending',
|
||||
bikeId: 'EV-007',
|
||||
bikeModel: 'Etron ET50',
|
||||
bikePlate: 'Dhaka Metro Ca-8901',
|
||||
reporterId: 'BIKER-007',
|
||||
reporterName: 'Jamal',
|
||||
reporterPhone: '01712345687',
|
||||
reporterRole: 'biker',
|
||||
description: 'Battery not holding charge properly - need replacement',
|
||||
location: 'Dhanmondi, Dhaka',
|
||||
estimatedCost: 0,
|
||||
images: [],
|
||||
notes: ['Battery replaced under warranty'],
|
||||
resolvedAt: '2024-03-19T16:00:00',
|
||||
createdAt: '2024-03-19T12:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
{
|
||||
id: 'MNT-004',
|
||||
date: '2024-03-18',
|
||||
type: 'repair',
|
||||
severity: 'critical',
|
||||
status: 'in_progress',
|
||||
paymentStatus: 'pending',
|
||||
bikeId: 'EV-010',
|
||||
bikeModel: 'TVS iQube',
|
||||
bikePlate: 'Dhaka Metro Da-4567',
|
||||
reporterId: 'BIKER-010',
|
||||
reporterName: 'Ripon',
|
||||
reporterPhone: '01712345690',
|
||||
reporterRole: 'biker',
|
||||
description: 'Motor issue - bike not moving properly',
|
||||
location: 'Mirpur, Dhaka',
|
||||
estimatedCost: 8000,
|
||||
images: [
|
||||
{ id: 'img3', name: 'Motor Damage', url: '', uploadedAt: '2024-03-18' },
|
||||
],
|
||||
assignedTo: 'Authorized Service Center',
|
||||
notes: ['Motor needs replacement - ordered', 'Waiting for parts'],
|
||||
createdAt: '2024-03-18T09:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
{
|
||||
id: 'MNT-005',
|
||||
date: '2024-03-17',
|
||||
type: 'inspection',
|
||||
severity: 'minor',
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
bikeId: 'EV-001',
|
||||
bikeModel: 'AIMA Lightning',
|
||||
bikePlate: 'Dhaka Metro Aa-1111',
|
||||
reporterId: 'Hub-01',
|
||||
reporterName: 'Gulshan Hub',
|
||||
reporterPhone: '02-1234567',
|
||||
reporterRole: 'hub',
|
||||
description: 'Monthly inspection completed',
|
||||
location: 'Gulshan Hub',
|
||||
estimatedCost: 300,
|
||||
actualCost: 250,
|
||||
images: [],
|
||||
notes: ['All checks passed'],
|
||||
resolvedAt: '2024-03-17T15:00:00',
|
||||
createdAt: '2024-03-17T10:00:00',
|
||||
createdBy: 'Hub Staff',
|
||||
},
|
||||
{
|
||||
id: 'MNT-006',
|
||||
date: '2024-03-15',
|
||||
type: 'damage',
|
||||
severity: 'cosmetic',
|
||||
status: 'completed',
|
||||
paymentStatus: 'rejected',
|
||||
bikeId: 'EV-005',
|
||||
bikeModel: 'Yadea DT3',
|
||||
bikePlate: 'Dhaka Metro Ba-5678',
|
||||
reporterId: 'BIKER-005',
|
||||
reporterName: 'Rahim',
|
||||
reporterPhone: '01712345685',
|
||||
reporterRole: 'biker',
|
||||
description: 'Minor scratch on mirror - customer dropped bike slowly',
|
||||
location: 'Uttara, Dhaka',
|
||||
estimatedCost: 500,
|
||||
images: [],
|
||||
notes: ['Denied - user responsibility'],
|
||||
createdAt: '2024-03-15T14:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
reported: 'bg-amber-100 text-amber-700',
|
||||
in_progress: 'bg-blue-100 text-blue-700',
|
||||
parts_ordered: 'bg-purple-100 text-purple-700',
|
||||
completed: 'bg-green-100 text-green-700',
|
||||
cancelled: 'bg-red-100 text-red-700',
|
||||
};
|
||||
|
||||
const severityColors: Record<string, string> = {
|
||||
critical: 'bg-red-100 text-red-700',
|
||||
major: 'bg-orange-100 text-orange-700',
|
||||
minor: 'bg-amber-100 text-amber-700',
|
||||
cosmetic: 'bg-slate-100 text-slate-700',
|
||||
};
|
||||
|
||||
const paymentColors: Record<string, string> = {
|
||||
pending: 'bg-amber-100 text-amber-700',
|
||||
approved: 'bg-blue-100 text-blue-700',
|
||||
paid: 'bg-green-100 text-green-700',
|
||||
rejected: 'bg-red-100 text-red-700',
|
||||
};
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
damage: 'Damage',
|
||||
repair: 'Repair',
|
||||
service: 'Service',
|
||||
battery_swap: 'Battery Swap',
|
||||
inspection: 'Inspection',
|
||||
other: 'Other',
|
||||
};
|
||||
|
||||
export default function MaintenanceDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
const [record, setRecord] = useState<MaintenanceRecord | null>(null);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [showCompleteModal, setShowCompleteModal] = useState(false);
|
||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
||||
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
||||
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
|
||||
const [newNoteText, setNewNoteText] = useState('');
|
||||
const [actualCost, setActualCost] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const found = mockMaintenance.find(r => r.id === id);
|
||||
if (found) {
|
||||
setRecord(found);
|
||||
setEditForm(found);
|
||||
setActualCost(found.actualCost?.toString() || found.estimatedCost.toString());
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (!record) {
|
||||
return (
|
||||
<div className="p-6 flex items-center justify-center min-h-[50vh]">
|
||||
<div className="text-center">
|
||||
<Wrench className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-500">Record not found</p>
|
||||
<button
|
||||
onClick={() => router.push('/admin/maintenance')}
|
||||
className="mt-4 px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
||||
>
|
||||
Back to Maintenance
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
setRecord(prev => prev ? { ...prev, ...editForm } : null);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
if (!record) return;
|
||||
const cost = parseInt(actualCost) || record.estimatedCost;
|
||||
setRecord(prev => prev ? {
|
||||
...prev,
|
||||
status: 'completed',
|
||||
resolvedAt: new Date().toISOString().split('T')[0],
|
||||
actualCost: cost
|
||||
} : null);
|
||||
setShowCompleteModal(false);
|
||||
};
|
||||
|
||||
const handlePayment = (source: 'bank' | 'cash' | 'biker') => {
|
||||
if (!record) return;
|
||||
const cost = record.actualCost || record.estimatedCost;
|
||||
|
||||
setRecord(prev => prev ? { ...prev, paymentStatus: 'paid' } : null);
|
||||
setShowPaymentModal(false);
|
||||
setShowInvoiceModal(true);
|
||||
};
|
||||
|
||||
const handleGenerateInvoice = () => {
|
||||
if (!record) return;
|
||||
import('jspdf').then(jsPDF => {
|
||||
const doc = new jsPDF.default();
|
||||
const cost = record.actualCost || record.estimatedCost;
|
||||
const qrData = `INV-${record.id}|${record.bikePlate}|${record.type}|${cost}|${new Date().toISOString().split('T')[0]}`;
|
||||
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(6, 95, 70);
|
||||
doc.text('JAIBEN Mobility Ltd', 20, 20);
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(0);
|
||||
doc.text('Maintenance Invoice', 20, 32);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(100);
|
||||
doc.text(`Invoice No: INV-${record.id}`, 20, 42);
|
||||
doc.text(`Date: ${record.date}`, 20, 48);
|
||||
doc.text(`Issue Type: ${typeLabels[record.type]}`, 20, 54);
|
||||
doc.text(`Severity: ${record.severity}`, 20, 60);
|
||||
doc.text(`Status: ${record.status}`, 20, 66);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(0);
|
||||
doc.text('Bike Details', 20, 80);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Bike ID: ${record.bikeId}`, 20, 86);
|
||||
doc.text(`Model: ${record.bikeModel}`, 20, 92);
|
||||
doc.text(`License Plate: ${record.bikePlate}`, 20, 98);
|
||||
if (record.batteryId) doc.text(`Battery ID: ${record.batteryId}`, 20, 104);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text('Reporter', 20, 118);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Name: ${record.reporterName}`, 20, 124);
|
||||
doc.text(`Phone: ${record.reporterPhone}`, 20, 130);
|
||||
doc.text(`Role: ${record.reporterRole}`, 20, 136);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text('Description', 20, 150);
|
||||
doc.setFontSize(10);
|
||||
const descLines = doc.splitTextToSize(record.description, 170);
|
||||
doc.text(descLines, 20, 156);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text('Service Details', 120, 80);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Location: ${record.location}`, 120, 86);
|
||||
if (record.assignedTo) doc.text(`Assigned: ${record.assignedTo}`, 120, 92);
|
||||
if (record.resolvedAt) doc.text(`Resolved: ${record.resolvedAt}`, 120, 98);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text('Cost Breakdown', 20, 175);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Estimated Cost: ৳${record.estimatedCost}`, 20, 181);
|
||||
if (record.actualCost) doc.text(`Actual Cost: ৳${record.actualCost}`, 20, 187);
|
||||
if (record.partsUsed && record.partsUsed.length > 0) {
|
||||
doc.text(`Parts: ${record.partsUsed.join(', ')}`, 20, 193);
|
||||
}
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(6, 95, 70);
|
||||
doc.text(`Total: ৳${cost}`, 20, 205);
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(150);
|
||||
doc.text('Generated from JAIBEN Maintenance System', 20, 280);
|
||||
doc.text(`QR: ${qrData}`, 20, 286);
|
||||
|
||||
doc.save(`maintenance-invoice-${record.id}.pdf`);
|
||||
});
|
||||
setShowInvoiceModal(false);
|
||||
};
|
||||
|
||||
const handleAddNote = () => {
|
||||
if (!record || !newNoteText.trim()) return;
|
||||
setRecord(prev => prev ? { ...prev, notes: [...prev.notes, newNoteText] } : null);
|
||||
setNewNoteText('');
|
||||
setShowAddNoteModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
||||
<button
|
||||
onClick={() => router.push('/admin/maintenance')}
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" /> Back to Maintenance
|
||||
</button>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-extrabold text-slate-800">{record.id}</h1>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${severityColors[record.severity]}`}>
|
||||
{record.severity}
|
||||
</span>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[record.status]}`}>
|
||||
{record.status.replace('_', ' ')}
|
||||
</span>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${paymentColors[record.paymentStatus]}`}>
|
||||
{record.paymentStatus}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-500 mt-1">{typeLabels[record.type]} • {record.date}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{editMode ? (
|
||||
<>
|
||||
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
||||
<Save className="w-4 h-4" /> Save
|
||||
</button>
|
||||
<button onClick={() => setEditMode(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
||||
<Edit className="w-4 h-4" /> Edit
|
||||
</button>
|
||||
<button onClick={() => setShowAddNoteModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4" /> Note
|
||||
</button>
|
||||
{record.status !== 'completed' && (
|
||||
<button
|
||||
onClick={() => setShowCompleteModal(true)}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
|
||||
>
|
||||
<Check className="w-4 h-4" /> Complete
|
||||
</button>
|
||||
)}
|
||||
{record.status === 'completed' && record.paymentStatus !== 'paid' && (
|
||||
<button
|
||||
onClick={() => setShowPaymentModal(true)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
|
||||
>
|
||||
<DollarSign className="w-4 h-4" /> Payment
|
||||
</button>
|
||||
)}
|
||||
{record.paymentStatus === 'paid' && (
|
||||
<button
|
||||
onClick={() => setShowInvoiceModal(true)}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
|
||||
>
|
||||
<Printer className="w-4 h-4" /> Invoice
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
|
||||
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
|
||||
<Bike className="w-5 h-5" /> Bike Information
|
||||
</h3>
|
||||
{editMode ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-blue-700 block mb-1">Bike ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.bikeId || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, bikeId: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-blue-700 block mb-1">License Plate</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.bikePlate || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, bikePlate: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-blue-700 block mb-1">Model</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.bikeModel || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, bikeModel: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-blue-700 block mb-1">Battery ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.batteryId || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, batteryId: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-blue-600">Bike ID</span>
|
||||
<span className="text-sm font-medium text-blue-800">{record.bikeId}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-blue-600">Model</span>
|
||||
<span className="text-sm font-medium text-blue-800">{record.bikeModel}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-blue-600">License Plate</span>
|
||||
<span className="text-sm font-medium text-blue-800">{record.bikePlate}</span>
|
||||
</div>
|
||||
{record.batteryId && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-blue-600">Battery ID</span>
|
||||
<span className="text-sm font-medium text-blue-800">{record.batteryId}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
|
||||
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
|
||||
<User className="w-5 h-5" /> Reporter
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-green-600">Name</span>
|
||||
<span className="text-sm font-medium text-green-800">{record.reporterName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-green-600">Phone</span>
|
||||
<span className="text-sm font-medium text-green-800">{record.reporterPhone}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-green-600">Role</span>
|
||||
<span className="text-sm font-medium text-green-800 capitalize">{record.reporterRole}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
|
||||
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" /> Description
|
||||
</h3>
|
||||
{editMode ? (
|
||||
<textarea
|
||||
value={editForm.description || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-amber-700 mb-3">{record.description}</p>
|
||||
<div className="flex items-center gap-2 text-xs text-amber-600">
|
||||
<MapPin className="w-3 h-3" /> {record.location}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
||||
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" /> Cost Details
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-white p-3 rounded-lg">
|
||||
<p className="text-xs text-purple-600">Estimated</p>
|
||||
{editMode ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.estimatedCost || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, estimatedCost: parseInt(e.target.value) })}
|
||||
className="w-full px-2 py-1 border border-purple-200 rounded text-sm"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-lg font-bold text-purple-800">৳{record.estimatedCost}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-white p-3 rounded-lg">
|
||||
<p className="text-xs text-purple-600">Actual</p>
|
||||
<p className="text-lg font-bold text-purple-800">৳{record.actualCost || record.estimatedCost}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
|
||||
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
|
||||
<User className="w-5 h-5" /> Assigned To
|
||||
</h3>
|
||||
{editMode ? (
|
||||
<select
|
||||
value={editForm.assignedTo || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, assignedTo: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-cyan-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Select Service Center</option>
|
||||
<option value="Service Center A">Service Center A</option>
|
||||
<option value="Service Center B">Service Center B</option>
|
||||
<option value="Authorized Service Center">Authorized Service Center</option>
|
||||
<option value="Gulshan Hub">Gulshan Hub</option>
|
||||
<option value="Banani Hub">Banani Hub</option>
|
||||
<option value="Dhanmondi Hub">Dhanmondi Hub</option>
|
||||
</select>
|
||||
) : (
|
||||
<p className="text-sm text-cyan-700">{record.assignedTo || 'Not assigned'}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
|
||||
<Wrench className="w-5 h-5" /> Parts Used
|
||||
</h3>
|
||||
{editMode && (
|
||||
<select
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
const currentParts = editForm.partsUsed || [];
|
||||
if (!currentParts.includes(e.target.value)) {
|
||||
setEditForm({ ...editForm, partsUsed: [...currentParts, e.target.value] });
|
||||
}
|
||||
e.target.value = '';
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 text-xs border border-orange-200 rounded"
|
||||
>
|
||||
<option value="">+ Add Part</option>
|
||||
<option value="Front fender">Front fender</option>
|
||||
<option value="Rear fender">Rear fender</option>
|
||||
<option value="Mirror">Mirror</option>
|
||||
<option value="Headlight">Headlight</option>
|
||||
<option value="Tail light">Tail light</option>
|
||||
<option value="Brake pad">Brake pad</option>
|
||||
<option value="Brake shoe">Brake shoe</option>
|
||||
<option value="Chain">Chain</option>
|
||||
<option value="Battery">Battery</option>
|
||||
<option value="Motor">Motor</option>
|
||||
<option value="Controller">Controller</option>
|
||||
<option value="Throttle">Throttle</option>
|
||||
<option value="Lever">Lever</option>
|
||||
<option value="Stand">Stand</option>
|
||||
<option value="Seat">Seat</option>
|
||||
<option value="Tyre">Tyre</option>
|
||||
<option value="Tube">Tube</option>
|
||||
<option value="Mounting brackets">Mounting brackets</option>
|
||||
<option value="Bolt set">Bolt set</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(editMode ? editForm.partsUsed : record.partsUsed)?.map((part, idx) => (
|
||||
<span key={idx} className="px-3 py-1 bg-white rounded-full text-sm text-orange-700 border border-orange-200 flex items-center gap-1">
|
||||
{part}
|
||||
{editMode && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const updated = [...(editForm.partsUsed || [])];
|
||||
updated.splice(idx, 1);
|
||||
setEditForm({ ...editForm, partsUsed: updated });
|
||||
}}
|
||||
className="ml-1 text-orange-400 hover:text-red-500"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{(editMode ? editForm.partsUsed : record.partsUsed)?.length === 0 && (
|
||||
<p className="text-sm text-orange-400">No parts added</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-50 p-4 rounded-xl border border-indigo-100">
|
||||
<h3 className="font-semibold text-indigo-800 mb-3 flex items-center gap-2">
|
||||
<Image className="w-5 h-5" /> Images ({(editMode ? editForm.images : record.images)?.length})
|
||||
</h3>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{(editMode ? editForm.images : record.images)?.map((img) => (
|
||||
<div key={img.id} className="relative aspect-square bg-white rounded-lg flex flex-col items-center justify-center border border-indigo-100">
|
||||
<Image className="w-8 h-8 text-indigo-400" />
|
||||
<span className="text-xs text-indigo-500 mt-1 text-center">{img.name}</span>
|
||||
{editMode && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const updated = (editForm.images || []).filter((i: any) => i.id !== img.id);
|
||||
setEditForm({ ...editForm, images: updated });
|
||||
}}
|
||||
className="absolute top-1 right-1 w-5 h-5 bg-red-100 rounded-full text-red-500 text-xs flex items-center justify-center"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{editMode && (
|
||||
<label className="aspect-square bg-white rounded-lg flex flex-col items-center justify-center border border-dashed border-indigo-200 cursor-pointer hover:bg-indigo-50">
|
||||
<Image className="w-8 h-8 text-indigo-400" />
|
||||
<span className="text-xs text-indigo-500 mt-1">+ Add</span>
|
||||
<input type="file" className="hidden" accept="image/*" />
|
||||
</label>
|
||||
)}
|
||||
{(editMode ? editForm.images : record.images)?.length === 0 && !editMode && (
|
||||
<p className="text-sm text-indigo-400 col-span-4 text-center py-4">No images</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5" /> Notes ({(editMode ? editForm.notes : record.notes)?.length})
|
||||
</h3>
|
||||
{editMode && (
|
||||
<div className="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
id="newNoteInput"
|
||||
placeholder="Add a note..."
|
||||
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
if (input.value.trim()) {
|
||||
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const input = document.getElementById('newNoteInput') as HTMLInputElement;
|
||||
if (input?.value.trim()) {
|
||||
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
||||
input.value = '';
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{(editMode ? editForm.notes : record.notes)?.map((note, idx) => (
|
||||
<div key={idx} className="text-sm text-slate-600 p-2 bg-white rounded-lg">
|
||||
• {note}
|
||||
</div>
|
||||
))}
|
||||
{(editMode ? editForm.notes : record.notes)?.length === 0 && (
|
||||
<p className="text-sm text-slate-400">No notes yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCompleteModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800">Complete Maintenance</h3>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-green-700">Enter actual cost to complete this record</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-2 block">Actual Cost (৳)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={actualCost}
|
||||
onChange={(e) => setActualCost(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-lg font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
Estimated: ৳{record.estimatedCost}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowCompleteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg flex items-center gap-2"
|
||||
>
|
||||
<Check className="w-4 h-4" /> Mark Complete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPaymentModal && record && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div className="p-4 border-b border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800">Process Payment - {record.id}</h3>
|
||||
<p className="text-sm text-slate-500">Amount: ৳{record.actualCost || record.estimatedCost}</p>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
<p className="text-sm text-slate-600 mb-2">Select payment method:</p>
|
||||
<button
|
||||
onClick={() => handlePayment('bank')}
|
||||
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Building className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-slate-800">Bank Transfer</p>
|
||||
<p className="text-xs text-slate-500">Debit Bank (1200) → Credit Maintenance (5400)</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePayment('cash')}
|
||||
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-green-500 hover:bg-green-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
||||
<Wallet className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-slate-800">Cash</p>
|
||||
<p className="text-xs text-slate-500">Debit Cash (1100) → Credit Maintenance (5400)</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePayment('biker')}
|
||||
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-purple-500 hover:bg-purple-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-slate-800">Biker Wallet</p>
|
||||
<p className="text-xs text-slate-500">Deduct from rider wallet</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end">
|
||||
<button onClick={() => setShowPaymentModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showInvoiceModal && record && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-800">Invoice Generated!</h3>
|
||||
</div>
|
||||
<div className="p-6 text-center">
|
||||
<div className="w-20 h-20 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-slate-800 mb-2">Payment Complete</p>
|
||||
<p className="text-slate-600 mb-4">Invoice INV-{record.id} is ready</p>
|
||||
|
||||
<div className="bg-slate-50 p-4 rounded-lg text-left mb-4">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-slate-500">Maintenance ID</span>
|
||||
<span className="text-sm font-medium">{record.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-slate-500">Bike</span>
|
||||
<span className="text-sm font-medium">{record.bikePlate}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-slate-500">Amount Paid</span>
|
||||
<span className="text-sm font-bold text-green-600">৳{record.actualCost || record.estimatedCost}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<div className="w-24 h-24 bg-white border-2 border-slate-200 rounded-lg flex items-center justify-center">
|
||||
<QrCode className="w-16 h-16 text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">Scan QR for verification</p>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-between">
|
||||
<button
|
||||
onClick={() => setShowInvoiceModal(false)}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerateInvoice}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg flex items-center gap-2"
|
||||
>
|
||||
<Printer className="w-4 h-4" /> Download PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddNoteModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800">Add Note</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<textarea
|
||||
value={newNoteText}
|
||||
onChange={(e) => setNewNoteText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
rows={4}
|
||||
placeholder="Enter note..."
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowAddNoteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
<button
|
||||
onClick={handleAddNote}
|
||||
disabled={!newNoteText.trim()}
|
||||
className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Save Note
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
958
src/app/admin/maintenance/page.tsx
Normal file
958
src/app/admin/maintenance/page.tsx
Normal file
@@ -0,0 +1,958 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
|
||||
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
|
||||
CheckCircle, XCircle, ChevronDown, ChevronUp, Download, Eye, Edit,
|
||||
MessageSquare, Filter, Calendar, Save, Printer, Send
|
||||
} from 'lucide-react';
|
||||
|
||||
type TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense';
|
||||
|
||||
type MaintenanceType = 'damage' | 'repair' | 'service' | 'battery_swap' | 'inspection' | 'other';
|
||||
type DamageSeverity = 'critical' | 'major' | 'minor' | 'cosmetic';
|
||||
type MaintenanceStatus = 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
|
||||
type PaymentStatus = 'pending' | 'approved' | 'paid' | 'rejected';
|
||||
|
||||
interface MaintenanceRecord {
|
||||
id: string;
|
||||
date: string;
|
||||
type: MaintenanceType;
|
||||
severity: DamageSeverity;
|
||||
status: MaintenanceStatus;
|
||||
paymentStatus: PaymentStatus;
|
||||
|
||||
bikeId: string;
|
||||
bikeModel: string;
|
||||
bikePlate: string;
|
||||
batteryId?: string;
|
||||
|
||||
reporterId: string;
|
||||
reporterName: string;
|
||||
reporterPhone: string;
|
||||
reporterRole: 'biker' | 'staff' | 'hub';
|
||||
|
||||
description: string;
|
||||
location: string;
|
||||
|
||||
estimatedCost: number;
|
||||
actualCost?: number;
|
||||
partsUsed?: string[];
|
||||
|
||||
images: { id: string; name: string; url: string; uploadedAt: string }[];
|
||||
bills?: { id: string; name: string; url: string }[];
|
||||
|
||||
assignedTo?: string;
|
||||
notes: string[];
|
||||
resolvedAt?: string;
|
||||
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
const mockMaintenance: MaintenanceRecord[] = [
|
||||
{
|
||||
id: 'MNT-001',
|
||||
date: '2024-03-21',
|
||||
type: 'damage',
|
||||
severity: 'major',
|
||||
status: 'in_progress',
|
||||
paymentStatus: 'approved',
|
||||
bikeId: 'EV-004',
|
||||
bikeModel: 'AIMA Lightning',
|
||||
bikePlate: 'Dhaka Metro Cha-5679',
|
||||
batteryId: 'BAT-044',
|
||||
reporterId: 'BIKER-004',
|
||||
reporterName: 'Sofiq Rahman',
|
||||
reporterPhone: '01712345681',
|
||||
reporterRole: 'biker',
|
||||
description: 'Front fender damaged in accident at Gulshan signal',
|
||||
location: 'Gulshan, Dhaka',
|
||||
estimatedCost: 3500,
|
||||
actualCost: 3200,
|
||||
partsUsed: ['Front fender', 'Mounting brackets'],
|
||||
images: [
|
||||
{ id: 'img1', name: 'Damage Front', url: '', uploadedAt: '2024-03-21' },
|
||||
{ id: 'img2', name: 'Damage Side', url: '', uploadedAt: '2024-03-21' },
|
||||
],
|
||||
assignedTo: 'Service Center A',
|
||||
notes: ['Parts ordered from supplier'],
|
||||
createdAt: '2024-03-21T10:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
{
|
||||
id: 'MNT-002',
|
||||
date: '2024-03-20',
|
||||
type: 'service',
|
||||
severity: 'minor',
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
bikeId: 'EV-002',
|
||||
bikeModel: 'Yadea DT3',
|
||||
bikePlate: 'Dhaka Metro Ba-1234',
|
||||
batteryId: 'BAT-021',
|
||||
reporterId: 'BIKER-002',
|
||||
reporterName: 'Karim Hasan',
|
||||
reporterPhone: '01712345679',
|
||||
reporterRole: 'biker',
|
||||
description: 'Routine service - brake adjustment and chain lubrication',
|
||||
location: 'Banani Hub',
|
||||
estimatedCost: 500,
|
||||
actualCost: 450,
|
||||
images: [],
|
||||
notes: ['Service completed'],
|
||||
resolvedAt: '2024-03-20T14:00:00',
|
||||
createdAt: '2024-03-20T08:00:00',
|
||||
createdBy: 'Hub Staff',
|
||||
},
|
||||
{
|
||||
id: 'MNT-003',
|
||||
date: '2024-03-19',
|
||||
type: 'battery_swap',
|
||||
severity: 'minor',
|
||||
status: 'completed',
|
||||
paymentStatus: 'pending',
|
||||
bikeId: 'EV-007',
|
||||
bikeModel: 'Etron ET50',
|
||||
bikePlate: 'Dhaka Metro Ca-8901',
|
||||
reporterId: 'BIKER-007',
|
||||
reporterName: 'Jamal',
|
||||
reporterPhone: '01712345687',
|
||||
reporterRole: 'biker',
|
||||
description: 'Battery not holding charge properly - need replacement',
|
||||
location: 'Dhanmondi, Dhaka',
|
||||
estimatedCost: 0,
|
||||
images: [],
|
||||
notes: ['Battery replaced under warranty'],
|
||||
resolvedAt: '2024-03-19T16:00:00',
|
||||
createdAt: '2024-03-19T12:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
{
|
||||
id: 'MNT-004',
|
||||
date: '2024-03-18',
|
||||
type: 'repair',
|
||||
severity: 'critical',
|
||||
status: 'parts_ordered',
|
||||
paymentStatus: 'pending',
|
||||
bikeId: 'EV-010',
|
||||
bikeModel: 'TVS iQube',
|
||||
bikePlate: 'Dhaka Metro Da-4567',
|
||||
reporterId: 'BIKER-010',
|
||||
reporterName: 'Ripon',
|
||||
reporterPhone: '01712345690',
|
||||
reporterRole: 'biker',
|
||||
description: 'Motor issue - bike not moving properly',
|
||||
location: 'Mirpur, Dhaka',
|
||||
estimatedCost: 8000,
|
||||
images: [
|
||||
{ id: 'img3', name: 'Motor Damage', url: '', uploadedAt: '2024-03-18' },
|
||||
],
|
||||
assignedTo: 'Authorized Service Center',
|
||||
notes: ['Motor needs replacement - ordered', 'Waiting for parts'],
|
||||
createdAt: '2024-03-18T09:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
{
|
||||
id: 'MNT-005',
|
||||
date: '2024-03-17',
|
||||
type: 'inspection',
|
||||
severity: 'minor',
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
bikeId: 'EV-001',
|
||||
bikeModel: 'AIMA Lightning',
|
||||
bikePlate: 'Dhaka Metro Aa-1111',
|
||||
reporterId: 'Hub-01',
|
||||
reporterName: 'Gulshan Hub',
|
||||
reporterPhone: '02-1234567',
|
||||
reporterRole: 'hub',
|
||||
description: 'Monthly inspection completed',
|
||||
location: 'Gulshan Hub',
|
||||
estimatedCost: 300,
|
||||
actualCost: 250,
|
||||
images: [],
|
||||
notes: ['All checks passed'],
|
||||
resolvedAt: '2024-03-17T15:00:00',
|
||||
createdAt: '2024-03-17T10:00:00',
|
||||
createdBy: 'Hub Staff',
|
||||
},
|
||||
{
|
||||
id: 'MNT-006',
|
||||
date: '2024-03-15',
|
||||
type: 'damage',
|
||||
severity: 'cosmetic',
|
||||
status: 'completed',
|
||||
paymentStatus: 'rejected',
|
||||
bikeId: 'EV-005',
|
||||
bikeModel: 'Yadea DT3',
|
||||
bikePlate: 'Dhaka Metro Ba-5678',
|
||||
reporterId: 'BIKER-005',
|
||||
reporterName: 'Rahim',
|
||||
reporterPhone: '01712345685',
|
||||
reporterRole: 'biker',
|
||||
description: 'Minor scratch on mirror - customer dropped bike slowly',
|
||||
location: 'Uttara, Dhaka',
|
||||
estimatedCost: 500,
|
||||
images: [],
|
||||
notes: ['Denied - user responsibility'],
|
||||
createdAt: '2024-03-15T14:00:00',
|
||||
createdBy: 'Admin',
|
||||
},
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
reported: 'bg-amber-100 text-amber-700',
|
||||
in_progress: 'bg-blue-100 text-blue-700',
|
||||
parts_ordered: 'bg-purple-100 text-purple-700',
|
||||
completed: 'bg-green-100 text-green-700',
|
||||
cancelled: 'bg-red-100 text-red-700',
|
||||
};
|
||||
|
||||
const severityColors: Record<string, string> = {
|
||||
critical: 'bg-red-100 text-red-700',
|
||||
major: 'bg-orange-100 text-orange-700',
|
||||
minor: 'bg-amber-100 text-amber-700',
|
||||
cosmetic: 'bg-slate-100 text-slate-700',
|
||||
};
|
||||
|
||||
const paymentColors: Record<string, string> = {
|
||||
pending: 'bg-amber-100 text-amber-700',
|
||||
approved: 'bg-blue-100 text-blue-700',
|
||||
paid: 'bg-green-100 text-green-700',
|
||||
rejected: 'bg-red-100 text-red-700',
|
||||
};
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
damage: 'Damage',
|
||||
repair: 'Repair',
|
||||
service: 'Service',
|
||||
battery_swap: 'Battery Swap',
|
||||
inspection: 'Inspection',
|
||||
other: 'Other',
|
||||
};
|
||||
|
||||
const typeIcons: Record<string, any> = {
|
||||
damage: AlertTriangle,
|
||||
repair: Wrench,
|
||||
service: Wrench,
|
||||
battery_swap: Battery,
|
||||
inspection: Search,
|
||||
other: FileText,
|
||||
};
|
||||
|
||||
export default function MaintenancePage() {
|
||||
const [activeTab, setActiveTab] = useState<'all' | MaintenanceType>('all');
|
||||
const [records, setRecords] = useState<MaintenanceRecord[]>(mockMaintenance);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [selectedRecord, setSelectedRecord] = useState<MaintenanceRecord | null>(null);
|
||||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
||||
const [showNewModal, setShowNewModal] = useState(false);
|
||||
const [showAddExpenseModal, setShowAddExpenseModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||
const [expandedNotes, setExpandedNotes] = useState<string[]>([]);
|
||||
const [newNoteText, setNewNoteText] = useState('');
|
||||
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
|
||||
|
||||
const filteredRecords = records.filter(r => {
|
||||
const matchesTab = activeTab === 'all' || r.type === activeTab;
|
||||
const matchesSearch = !searchQuery ||
|
||||
r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
r.id.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
|
||||
return matchesTab && matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
critical: records.filter(r => r.severity === 'critical' && r.status !== 'completed').length,
|
||||
inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length,
|
||||
completed: records.filter(r => r.status === 'completed').length,
|
||||
pendingPayment: records.filter(r => r.paymentStatus === 'pending' && r.status === 'completed').length,
|
||||
totalCost: records.reduce((sum, r) => sum + (r.actualCost || r.estimatedCost), 0),
|
||||
};
|
||||
|
||||
const toggleNotes = (id: string) => {
|
||||
setExpandedNotes(prev =>
|
||||
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddNote = () => {
|
||||
if (!selectedRecord || !newNoteText.trim()) return;
|
||||
setRecords(prev => prev.map(r =>
|
||||
r.id === selectedRecord.id
|
||||
? { ...r, notes: [...r.notes, newNoteText] }
|
||||
: r
|
||||
));
|
||||
setNewNoteText('');
|
||||
setShowAddNoteModal(false);
|
||||
};
|
||||
|
||||
const handleCompleteRecord = () => {
|
||||
if (!selectedRecord) return;
|
||||
setRecords(prev => prev.map(r =>
|
||||
r.id === selectedRecord.id
|
||||
? { ...r, status: 'completed', resolvedAt: new Date().toISOString().split('T')[0] }
|
||||
: r
|
||||
));
|
||||
alert(`Record ${selectedRecord.id} marked as completed!`);
|
||||
setShowDetailsModal(false);
|
||||
};
|
||||
|
||||
const handlePayment = (source: 'biker' | 'company') => {
|
||||
if (!selectedRecord) return;
|
||||
const cost = selectedRecord.actualCost || selectedRecord.estimatedCost;
|
||||
|
||||
setRecords(prev => prev.map(r =>
|
||||
r.id === selectedRecord.id
|
||||
? { ...r, paymentStatus: 'paid' }
|
||||
: r
|
||||
));
|
||||
|
||||
const journalEntry = {
|
||||
id: `MNT-JE-${Date.now()}`,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
reference: selectedRecord.id,
|
||||
description: `${selectedRecord.type === 'damage' ? 'Damage' : 'Maintenance'} - ${selectedRecord.bikePlate} - ${source === 'biker' ? 'Biker' : 'Company'} paid`,
|
||||
entries: source === 'biker' ? [
|
||||
{ accountId: 'ASSET-101', accountCode: '1100', accountName: 'Cash in Hand', debit: cost, credit: 0 },
|
||||
{ accountId: 'EXP-504', accountCode: '5400', accountName: 'Maintenance Expense', debit: 0, credit: cost },
|
||||
] : [
|
||||
{ accountId: 'ASSET-102', accountCode: '1200', accountName: 'Bank - City Bank', debit: cost, credit: 0 },
|
||||
{ accountId: 'EXP-504', accountCode: '5400', accountName: 'Maintenance Expense', debit: 0, credit: cost },
|
||||
],
|
||||
isPosted: true,
|
||||
isAuto: true,
|
||||
sourceType: 'maintenance' as TransactionType,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: 'System',
|
||||
};
|
||||
|
||||
alert(`Payment processed!\n\nAmount: ৳${cost}\nSource: ${source === 'biker' ? 'Biker Wallet' : 'Company Account'}\n\nAuto-journal entry created: ${journalEntry.reference}\n\nDebit: ${source === 'biker' ? 'Cash in Hand (1100)' : 'Bank - City Bank (1200)'}\nCredit: Maintenance Expense (5400)`);
|
||||
setShowPaymentModal(false);
|
||||
setShowDetailsModal(false);
|
||||
};
|
||||
|
||||
const handleGenerateInvoice = () => {
|
||||
if (!selectedRecord) return;
|
||||
import('jspdf').then(jsPDF => {
|
||||
const doc = new jsPDF.default();
|
||||
const cost = selectedRecord.actualCost || selectedRecord.estimatedCost;
|
||||
|
||||
doc.setFontSize(18);
|
||||
doc.setTextColor(6, 95, 70);
|
||||
doc.text('JAIBEN Mobility Ltd', 20, 20);
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(0);
|
||||
doc.text('Maintenance Invoice', 20, 32);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(100);
|
||||
doc.text(`Invoice No: INV-${selectedRecord.id}`, 20, 42);
|
||||
doc.text(`Date: ${selectedRecord.date}`, 20, 48);
|
||||
doc.text(`Issue Type: ${selectedRecord.type}`, 20, 54);
|
||||
doc.text(`Severity: ${selectedRecord.severity}`, 20, 60);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(0);
|
||||
doc.text('Bike Details', 20, 72);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Bike ID: ${selectedRecord.bikeId}`, 20, 78);
|
||||
doc.text(`Model: ${selectedRecord.bikeModel}`, 20, 84);
|
||||
doc.text(`License Plate: ${selectedRecord.bikePlate}`, 20, 90);
|
||||
if (selectedRecord.batteryId) doc.text(`Battery ID: ${selectedRecord.batteryId}`, 20, 96);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text('Description', 20, 108);
|
||||
doc.setFontSize(10);
|
||||
const descLines = doc.splitTextToSize(selectedRecord.description, 170);
|
||||
doc.text(descLines, 20, 114);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text('Cost Breakdown', 20, 130);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Estimated Cost: ৳${selectedRecord.estimatedCost}`, 20, 136);
|
||||
if (selectedRecord.actualCost) doc.text(`Actual Cost: ৳${selectedRecord.actualCost}`, 20, 142);
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(6, 95, 70);
|
||||
doc.text(`Total: ৳${cost}`, 20, 152);
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(150);
|
||||
doc.text('Generated from JAIBEN Maintenance System', 20, 280);
|
||||
|
||||
doc.save(`maintenance-invoice-${selectedRecord.id}.pdf`);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Damage & Maintenance</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">Track bike damage, repairs, and service records</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowNewModal(true)}
|
||||
className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-medium hover:bg-accent-dark flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Report Issue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-extrabold text-slate-800">{stats.critical}</p>
|
||||
<p className="text-sm text-slate-500">Critical</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
||||
<Wrench className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-extrabold text-slate-800">{stats.inProgress}</p>
|
||||
<p className="text-sm text-slate-500">In Progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center">
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p>
|
||||
<p className="text-sm text-slate-500">Completed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center">
|
||||
<DollarSign className="w-6 h-6 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-extrabold text-slate-800">{stats.pendingPayment}</p>
|
||||
<p className="text-sm text-slate-500">Pending Payment</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center">
|
||||
<DollarSign className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-extrabold text-slate-800">৳{stats.totalCost.toLocaleString()}</p>
|
||||
<p className="text-sm text-slate-500">Total Cost</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6">
|
||||
<div className="p-4 border-b border-slate-100">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${activeTab === 'all' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('damage')}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'damage' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" /> Damage
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('repair')}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'repair' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||
>
|
||||
<Wrench className="w-4 h-4" /> Repair
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('service')}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'service' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||
>
|
||||
<Wrench className="w-4 h-4" /> Service
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('battery_swap')}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'battery_swap' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||
>
|
||||
<Battery className="w-4 h-4" /> Battery
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by bike ID, plate, reporter..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full lg:w-64 pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="reported">Reported</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="parts_ordered">Parts Ordered</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-50">
|
||||
{filteredRecords.map(record => {
|
||||
const TypeIcon = typeIcons[record.type];
|
||||
return (
|
||||
<Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-5 hover:bg-slate-50 transition-colors">
|
||||
<div className="flex flex-col lg:flex-row lg:items-start gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center">
|
||||
<TypeIcon className="w-6 h-6 text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold text-slate-800">{record.id}</p>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${severityColors[record.severity]}`}>
|
||||
{record.severity}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 flex items-center gap-2">
|
||||
<Bike className="w-3 h-3" /> {record.bikeModel} ({record.bikePlate})
|
||||
<span className="text-slate-300">|</span>
|
||||
<User className="w-3 h-3" /> {record.reporterName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-slate-700">{record.description}</p>
|
||||
<div className="flex flex-wrap gap-4 text-sm text-slate-500 mt-1">
|
||||
<p className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" /> {record.date}
|
||||
</p>
|
||||
<p className="flex items-center gap-1">
|
||||
<MapPin className="w-3 h-3" /> {record.location}
|
||||
</p>
|
||||
{record.images.length > 0 && (
|
||||
<p className="flex items-center gap-1 text-blue-600">
|
||||
<Image className="w-3 h-3" /> {record.images.length} photos
|
||||
</p>
|
||||
)}
|
||||
{record.notes.length > 0 && (
|
||||
<button
|
||||
onClick={() => toggleNotes(record.id)}
|
||||
className="flex items-center gap-1 text-purple-600"
|
||||
>
|
||||
<MessageSquare className="w-3 h-3" /> {record.notes.length} notes
|
||||
{expandedNotes.includes(record.id) ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expandedNotes.includes(record.id) && record.notes.length > 0 && (
|
||||
<div className="mt-2 p-3 bg-purple-50 rounded-lg space-y-1">
|
||||
{record.notes.map((note, idx) => (
|
||||
<p key={idx} className="text-xs text-slate-700">• {note}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-slate-700">৳{record.actualCost || record.estimatedCost}</p>
|
||||
<p className="text-xs text-slate-500">{record.paymentStatus === 'paid' ? 'Paid' : record.paymentStatus === 'approved' ? 'Approved' : 'Payment ' + record.paymentStatus}</p>
|
||||
</div>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[record.status]}`}>
|
||||
{record.status === 'reported' && <Clock className="w-3 h-3" />}
|
||||
{record.status === 'in_progress' && <Wrench className="w-3 h-3" />}
|
||||
{record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />}
|
||||
{record.status === 'completed' && <CheckCircle className="w-3 h-3" />}
|
||||
{record.status === 'cancelled' && <XCircle className="w-3 h-3" />}
|
||||
{record.status.replace('_', ' ')}
|
||||
</span>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); setSelectedRecord(record); setShowDetailsModal(true); }}
|
||||
className="p-2 hover:bg-slate-100 rounded-lg text-slate-500"
|
||||
title="View Details"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{filteredRecords.length === 0 && (
|
||||
<div className="p-12 text-center">
|
||||
<Wrench className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-500">No records found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDetailsModal && selectedRecord && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-800">Maintenance Details - {selectedRecord.id}</h3>
|
||||
<p className="text-xs text-slate-500">{selectedRecord.date}</p>
|
||||
</div>
|
||||
<button onClick={() => setShowDetailsModal(false)} className="p-1 hover:bg-slate-100 rounded">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-y-auto max-h-[70vh]">
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="text-xs text-slate-500">Type / Severity</p>
|
||||
<p className="font-semibold text-slate-800 capitalize">{selectedRecord.type} / {selectedRecord.severity}</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="text-xs text-slate-500">Status</p>
|
||||
<span className={`inline-flex items-center gap-1 text-sm font-medium px-2 py-1 rounded-full ${statusColors[selectedRecord.status]}`}>
|
||||
{selectedRecord.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="text-xs text-slate-500">Bike</p>
|
||||
<p className="font-semibold text-slate-800">{selectedRecord.bikeModel}</p>
|
||||
<p className="text-sm text-slate-600">{selectedRecord.bikePlate} (ID: {selectedRecord.bikeId})</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="text-xs text-slate-500">Battery</p>
|
||||
<p className="font-semibold text-slate-800">{selectedRecord.batteryId || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h4 className="font-semibold text-slate-700 mb-3">Reported By</h4>
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="font-medium text-slate-800">{selectedRecord.reporterName}</p>
|
||||
<p className="text-sm text-slate-600">{selectedRecord.reporterPhone}</p>
|
||||
<p className="text-xs text-slate-500 capitalize">{selectedRecord.reporterRole}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h4 className="font-semibold text-slate-700 mb-3">Description</h4>
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="text-slate-700">{selectedRecord.description}</p>
|
||||
<p className="text-sm text-slate-500 mt-2 flex items-center gap-1">
|
||||
<MapPin className="w-3 h-3" /> {selectedRecord.location}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||
<p className="text-xs text-green-700">Estimated Cost</p>
|
||||
<p className="text-xl font-bold text-green-700">৳{selectedRecord.estimatedCost}</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<p className="text-xs text-blue-700">Actual Cost</p>
|
||||
<p className="text-xl font-bold text-blue-700">৳{selectedRecord.actualCost || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedRecord.partsUsed && selectedRecord.partsUsed.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h4 className="font-semibold text-slate-700 mb-3">Parts Used</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedRecord.partsUsed.map((part, idx) => (
|
||||
<span key={idx} className="px-3 py-1 bg-slate-100 rounded-full text-sm text-slate-600">
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedRecord.images.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h4 className="font-semibold text-slate-700 mb-3">Images ({selectedRecord.images.length})</h4>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{selectedRecord.images.map((img) => (
|
||||
<div key={img.id} className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
|
||||
<Image className="w-8 h-8 text-slate-400" />
|
||||
<span className="text-xs text-slate-500 mt-1">{img.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedRecord.notes.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h4 className="font-semibold text-slate-700 mb-3">Notes</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedRecord.notes.map((note, idx) => (
|
||||
<div key={idx} className="p-3 bg-purple-50 rounded-lg">
|
||||
<p className="text-sm text-slate-700">{note}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => { setShowDetailsModal(false); setShowEditModal(true); }}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2"
|
||||
>
|
||||
<Edit className="w-4 h-4" /> Edit Record
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowDetailsModal(false); setShowAddNoteModal(true); }}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" /> Add Note
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedRecord.status !== 'completed' && (
|
||||
<button
|
||||
onClick={handleCompleteRecord}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
|
||||
>
|
||||
<Check className="w-4 h-4" /> Mark Complete
|
||||
</button>
|
||||
)}
|
||||
{selectedRecord.status === 'completed' && selectedRecord.paymentStatus !== 'paid' && (
|
||||
<button
|
||||
onClick={() => { setShowDetailsModal(false); setShowPaymentModal(true); }}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
|
||||
>
|
||||
<DollarSign className="w-4 h-4" /> Process Payment
|
||||
</button>
|
||||
)}
|
||||
{selectedRecord.paymentStatus === 'paid' && (
|
||||
<button
|
||||
onClick={handleGenerateInvoice}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
|
||||
>
|
||||
<Printer className="w-4 h-4" /> Generate Invoice
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setShowDetailsModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNewModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-800">Report New Issue</h3>
|
||||
<button onClick={() => setShowNewModal(false)} className="p-1 hover:bg-slate-100 rounded">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-y-auto max-h-[70vh] space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Issue Type *</label>
|
||||
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
||||
<option value="damage">Damage</option>
|
||||
<option value="repair">Repair</option>
|
||||
<option value="service">Service</option>
|
||||
<option value="battery_swap">Battery Swap</option>
|
||||
<option value="inspection">Inspection</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label>
|
||||
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
||||
<option value="critical">Critical</option>
|
||||
<option value="major">Major</option>
|
||||
<option value="minor">Minor</option>
|
||||
<option value="cosmetic">Cosmetic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Bike ID *</label>
|
||||
<input type="text" placeholder="EV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID</label>
|
||||
<input type="text" placeholder="BAT-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Description *</label>
|
||||
<textarea rows={3} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Describe the issue..." />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Location *</label>
|
||||
<input type="text" placeholder="Where did the issue occur?" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Estimated Cost</label>
|
||||
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Upload Images</label>
|
||||
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center">
|
||||
<Image className="w-8 h-8 text-slate-300 mx-auto mb-2" />
|
||||
<p className="text-sm text-slate-500">Drag and drop or click to upload</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowNewModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={() => { setShowNewModal(false); alert('Issue reported successfully!'); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark">
|
||||
Submit Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddNoteModal && selectedRecord && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-800">Add Note - {selectedRecord.id}</h3>
|
||||
<button onClick={() => setShowAddNoteModal(false)} className="p-1 hover:bg-slate-100 rounded">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-2 block">New Note</label>
|
||||
<textarea
|
||||
value={newNoteText}
|
||||
onChange={(e) => setNewNoteText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
rows={4}
|
||||
placeholder="Enter note..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowAddNoteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddNote}
|
||||
disabled={!newNoteText.trim()}
|
||||
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
|
||||
>
|
||||
<Save className="w-4 h-4 inline mr-1" /> Save Note
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPaymentModal && selectedRecord && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-800">Process Payment - {selectedRecord.id}</h3>
|
||||
<button onClick={() => setShowPaymentModal(false)} className="p-1 hover:bg-slate-100 rounded">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-slate-600">Amount to Pay</p>
|
||||
<p className="text-2xl font-bold text-slate-800">৳{selectedRecord.actualCost || selectedRecord.estimatedCost}</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-slate-600">Bike</p>
|
||||
<p className="font-medium text-slate-800">{selectedRecord.bikeModel} ({selectedRecord.bikePlate})</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-2 block">Payment Source</label>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => handlePayment('company')}
|
||||
className="w-full p-4 border-2 border-slate-200 rounded-lg text-left hover:border-accent hover:bg-accent-light/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<DollarSign className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-800">Company Account</p>
|
||||
<p className="text-sm text-slate-500">Pay from company bank/cash</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePayment('biker')}
|
||||
className="w-full p-4 border-2 border-slate-200 rounded-lg text-left hover:border-accent hover:bg-accent-light/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-800">Biker Wallet</p>
|
||||
<p className="text-sm text-slate-500">Deduct from rider's wallet</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowPaymentModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
686
src/app/admin/rentals/[id]/page.tsx
Normal file
686
src/app/admin/rentals/[id]/page.tsx
Normal file
@@ -0,0 +1,686 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import {
|
||||
ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle,
|
||||
Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin,
|
||||
Phone, MessageCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
type RentalStatus = 'active' | 'pending' | 'completed' | 'disputed' | 'cancelled' | 'locked';
|
||||
type RentalType = 'single' | 'shared' | 'rent-to-own';
|
||||
|
||||
interface BikeImage {
|
||||
id: string;
|
||||
type: 'front' | 'back' | 'left' | 'right';
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface Note {
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Rental {
|
||||
id: string;
|
||||
bikeId: string;
|
||||
userId: string;
|
||||
type: RentalType;
|
||||
status: RentalStatus;
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
deposit: number;
|
||||
dailyRate: number;
|
||||
totalPaid: number;
|
||||
dueRental?: number;
|
||||
lockedAt?: string;
|
||||
lockedReason?: string;
|
||||
hubId?: string;
|
||||
hubName?: string;
|
||||
}
|
||||
|
||||
const mockRentals: Rental[] = [
|
||||
{
|
||||
id: 'RNT-001',
|
||||
bikeId: 'BIKE-001',
|
||||
userId: 'USR-001',
|
||||
type: 'single',
|
||||
status: 'active',
|
||||
startDate: '2024-01-15',
|
||||
deposit: 5000,
|
||||
dailyRate: 300,
|
||||
totalPaid: 81900,
|
||||
dueRental: 0,
|
||||
hubId: 'HUB-001',
|
||||
hubName: 'Gulshan Hub'
|
||||
},
|
||||
{
|
||||
id: 'RNT-002',
|
||||
bikeId: 'BIKE-002',
|
||||
userId: 'USR-002',
|
||||
type: 'shared',
|
||||
status: 'pending',
|
||||
startDate: '2024-02-01',
|
||||
deposit: 3000,
|
||||
dailyRate: 200,
|
||||
totalPaid: 2000,
|
||||
dueRental: 0,
|
||||
hubId: 'HUB-002',
|
||||
hubName: 'Banani Hub'
|
||||
},
|
||||
{
|
||||
id: 'RNT-003',
|
||||
bikeId: 'BIKE-003',
|
||||
userId: 'USR-003',
|
||||
type: 'rent-to-own',
|
||||
status: 'completed',
|
||||
startDate: '2023-06-01',
|
||||
endDate: '2023-12-01',
|
||||
deposit: 10000,
|
||||
dailyRate: 500,
|
||||
totalPaid: 150000,
|
||||
hubId: 'HUB-001',
|
||||
hubName: 'Gulshan Hub'
|
||||
}
|
||||
];
|
||||
|
||||
const mockBikes: Record<string, {
|
||||
id: string;
|
||||
model: string;
|
||||
plate: string;
|
||||
status: string;
|
||||
odometer: number;
|
||||
batteryHealth: number;
|
||||
images: BikeImage[];
|
||||
}> = {
|
||||
'BIKE-001': {
|
||||
id: 'BIKE-001',
|
||||
model: 'AIMA Lightning',
|
||||
plate: 'Dhaka Metro Cha-9012',
|
||||
status: 'active',
|
||||
odometer: 3510,
|
||||
batteryHealth: 85,
|
||||
images: [
|
||||
{ id: 'img1', type: 'front', url: '' },
|
||||
{ id: 'img2', type: 'back', url: '' },
|
||||
{ id: 'img3', type: 'left', url: '' },
|
||||
{ id: 'img4', type: 'right', url: '' }
|
||||
]
|
||||
},
|
||||
'BIKE-002': {
|
||||
id: 'BIKE-002',
|
||||
model: 'Yadea DT3',
|
||||
plate: 'Dhaka Metro Ba-5521',
|
||||
status: 'active',
|
||||
odometer: 2100,
|
||||
batteryHealth: 92,
|
||||
images: []
|
||||
}
|
||||
};
|
||||
|
||||
const mockUsers: Record<string, {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
walletBalance: number;
|
||||
membership: string;
|
||||
joinedFrom: string;
|
||||
kycStatus: 'verified' | 'pending' | 'rejected';
|
||||
insurance: 'active' | 'expired' | 'none';
|
||||
insuranceExpiry?: string;
|
||||
}> = {
|
||||
'USR-001': {
|
||||
id: 'USR-001',
|
||||
name: 'Rahim Ahmed',
|
||||
phone: '+8801712345678',
|
||||
email: 'rahim@example.com',
|
||||
walletBalance: 2100,
|
||||
membership: 'vip',
|
||||
joinedFrom: 'Facebook',
|
||||
kycStatus: 'verified',
|
||||
insurance: 'active',
|
||||
insuranceExpiry: '2024-12-01'
|
||||
},
|
||||
'USR-002': {
|
||||
id: 'USR-002',
|
||||
name: 'Karim Hasan',
|
||||
phone: '+8801812345678',
|
||||
email: 'karim@example.com',
|
||||
walletBalance: 500,
|
||||
membership: 'standard',
|
||||
joinedFrom: 'Referral',
|
||||
kycStatus: 'pending',
|
||||
insurance: 'none'
|
||||
}
|
||||
};
|
||||
|
||||
const mockHubs = [
|
||||
{ id: 'HUB-001', name: 'Gulshan Hub', address: 'Gulshan 1, Dhaka' },
|
||||
{ id: 'HUB-002', name: 'Banani Hub', address: 'Banani, Dhaka' },
|
||||
{ id: 'HUB-003', name: 'Uttara Hub', address: 'Uttara, Dhaka' }
|
||||
];
|
||||
|
||||
export default function RentalDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
const [rental, setRental] = useState<Rental | null>(null);
|
||||
const [user, setUser] = useState<typeof mockUsers['USR-001'] | null>(null);
|
||||
const [bike, setBike] = useState<typeof mockBikes['BIKE-001'] | null>(null);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [notes, setNotes] = useState<Note[]>([
|
||||
{ id: 'n1', text: 'Initial rental started. Bike in good condition.', createdAt: '2024-01-15' },
|
||||
{ id: 'n2', text: 'Battery replaced on 2024-01-20.', createdAt: '2024-01-20' }
|
||||
]);
|
||||
const [newNote, setNewNote] = useState('');
|
||||
const [editForm, setEditForm] = useState<Partial<Rental>>({});
|
||||
const [showLockModal, setShowLockModal] = useState(false);
|
||||
const [lockReason, setLockReason] = useState('');
|
||||
const [dueAmount, setDueAmount] = useState(0);
|
||||
const [showDueModal, setShowDueModal] = useState(false);
|
||||
const [showImageModal, setShowImageModal] = useState(false);
|
||||
const [uploadImageType, setUploadImageType] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const found = mockRentals.find(r => r.id === id);
|
||||
if (found) {
|
||||
setRental(found);
|
||||
setEditForm(found);
|
||||
setUser(mockUsers[found.userId as keyof typeof mockUsers] || null);
|
||||
setBike(mockBikes[found.bikeId as keyof typeof mockBikes] || mockBikes['BIKE-001']);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (!rental) {
|
||||
return (
|
||||
<div className="p-6 flex items-center justify-center min-h-[50vh]">
|
||||
<div className="text-center">
|
||||
<Bike className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-500">Rental not found</p>
|
||||
<button
|
||||
onClick={() => router.push('/admin/rentals')}
|
||||
className="mt-4 px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
||||
>
|
||||
Back to Rentals
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
setRental(prev => prev ? { ...prev, ...editForm } : null);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleLockRental = () => {
|
||||
if (!lockReason.trim()) return;
|
||||
setRental(prev => prev ? { ...prev, status: 'locked', lockedAt: new Date().toISOString(), lockedReason: lockReason } : null);
|
||||
setShowLockModal(false);
|
||||
setLockReason('');
|
||||
};
|
||||
|
||||
const handleUnlockRental = () => {
|
||||
setRental(prev => prev ? { ...prev, status: 'active', lockedAt: undefined, lockedReason: undefined } : null);
|
||||
};
|
||||
|
||||
const handleCancelRental = () => {
|
||||
if (confirm('Are you sure you want to cancel this rental? This action cannot be undone.')) {
|
||||
setRental(prev => prev ? { ...prev, status: 'cancelled', endDate: new Date().toISOString() } : null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddDue = () => {
|
||||
setRental(prev => prev ? { ...prev, dueRental: (prev.dueRental || 0) + dueAmount } : null);
|
||||
setShowDueModal(false);
|
||||
setDueAmount(0);
|
||||
};
|
||||
|
||||
const handleAddNote = () => {
|
||||
if (!newNote.trim()) return;
|
||||
setNotes(prev => [...prev, { id: `n${Date.now()}`, text: newNote, createdAt: new Date().toISOString().split('T')[0] }]);
|
||||
setNewNote('');
|
||||
};
|
||||
|
||||
const handleUpdateOdometer = (value: number) => {
|
||||
if (bike) setBike(prev => prev ? { ...prev, odometer: value } : null);
|
||||
};
|
||||
|
||||
const handleUpdateBattery = (value: number) => {
|
||||
if (bike) setBike(prev => prev ? { ...prev, batteryHealth: value } : null);
|
||||
};
|
||||
|
||||
const handleUploadImage = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || !bike) return;
|
||||
const url = URL.createObjectURL(file);
|
||||
setBike(prev => prev ? {
|
||||
...prev,
|
||||
images: prev.images.map(img => img.type === uploadImageType ? { ...img, url } : img)
|
||||
} : null);
|
||||
setShowImageModal(false);
|
||||
};
|
||||
|
||||
const statusColors = {
|
||||
active: 'bg-green-100 text-green-700',
|
||||
pending: 'bg-amber-100 text-amber-700',
|
||||
completed: 'bg-blue-100 text-blue-700',
|
||||
disputed: 'bg-red-100 text-red-700',
|
||||
cancelled: 'bg-slate-100 text-slate-700',
|
||||
locked: 'bg-red-100 text-red-700'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
||||
<button
|
||||
onClick={() => router.push('/admin/rentals')}
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" /> Back to Rentals
|
||||
</button>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-extrabold text-slate-800">{rental.id}</h1>
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[rental.status]}`}>
|
||||
{rental.status}
|
||||
</span>
|
||||
{rental.status === 'locked' && (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-red-100 text-red-700">
|
||||
<Lock className="w-3 h-3" /> Locked
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-500 mt-1">Started {rental.startDate} • From {rental.hubName || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{editMode ? (
|
||||
<>
|
||||
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
||||
<Save className="w-4 h-4" /> Save
|
||||
</button>
|
||||
<button onClick={() => { setEditForm(rental); setEditMode(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
||||
<Edit className="w-4 h-4" /> Edit
|
||||
</button>
|
||||
{rental.status === 'active' && (
|
||||
<>
|
||||
<button onClick={() => setShowDueModal(true)} className="px-4 py-2 bg-amber-600 text-white rounded-lg text-sm hover:bg-amber-700 flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4" /> Add Due
|
||||
</button>
|
||||
{rental.status === 'active' ? (
|
||||
<button onClick={() => setShowLockModal(true)} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700 flex items-center gap-2">
|
||||
<Lock className="w-4 h-4" /> Lock Rental
|
||||
</button>
|
||||
) : rental.status === 'locked' ? (
|
||||
<button onClick={handleUnlockRental} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
||||
<Unlock className="w-4 h-4" /> Unlock Rental
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{rental.status !== 'cancelled' && rental.status !== 'completed' && (
|
||||
<button onClick={handleCancelRental} className="px-4 py-2 bg-slate-600 text-white rounded-lg text-sm hover:bg-slate-700 flex items-center gap-2">
|
||||
<XCircle className="w-4 h-4" /> Cancel Rental
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
|
||||
<p className="text-sm text-blue-600">Total Spent</p>
|
||||
<p className="text-xl font-bold text-blue-800">৳{rental.totalPaid.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
|
||||
<p className="text-sm text-green-600">Wallet Balance</p>
|
||||
<p className="text-xl font-bold text-green-800">৳{user?.walletBalance.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
||||
<p className="text-sm text-purple-600">Deposit Paid</p>
|
||||
<p className="text-xl font-bold text-purple-800">৳{rental.deposit.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
|
||||
<p className="text-sm text-amber-600">Due Rental</p>
|
||||
<p className="text-xl font-bold text-amber-800">৳{(rental.dueRental || 0).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
|
||||
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
|
||||
<Bike className="w-5 h-5" /> Rented Bike Details
|
||||
</h3>
|
||||
{bike && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Model</span><span className="text-sm font-medium text-blue-800">{bike.model}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Plate</span><span className="text-sm font-medium text-blue-800">{bike.plate}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Type</span><span className="text-sm font-medium text-blue-800 capitalize">{rental.type}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Daily Rate</span><span className="text-sm font-medium text-blue-800">৳{rental.dailyRate}/day</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
|
||||
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
|
||||
<Gauge className="w-5 h-5" /> Mileage Tracking
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-amber-600">Current Odometer (km)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bike?.odometer || 0}
|
||||
onChange={(e) => handleUpdateOdometer(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm mt-1"
|
||||
disabled={rental.status !== 'active'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-amber-600">Total Distance</label>
|
||||
<p className="text-lg font-semibold text-amber-800">{(bike?.odometer || 0).toLocaleString()} km</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
|
||||
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
|
||||
<Battery className="w-5 h-5" /> Battery Health
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-green-600">Battery Percentage (0-100%)</label>
|
||||
<input
|
||||
type="number"
|
||||
max={100}
|
||||
min={0}
|
||||
value={bike?.batteryHealth || 0}
|
||||
onChange={(e) => handleUpdateBattery(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-green-200 rounded-lg text-sm mt-1"
|
||||
disabled={rental.status !== 'active'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-green-600">Health Status</label>
|
||||
<p className={`text-lg font-semibold ${(bike?.batteryHealth || 0) > 70 ? 'text-green-700' : (bike?.batteryHealth || 0) > 40 ? 'text-amber-700' : 'text-red-700'}`}>
|
||||
{(bike?.batteryHealth || 0) > 70 ? 'Good' : (bike?.batteryHealth || 0) > 40 ? 'Fair' : 'Poor'}
|
||||
</p>
|
||||
<p className="text-xs text-green-600">Estimated Range: {Math.round((bike?.batteryHealth || 0) * 1)} km</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold text-purple-800 flex items-center gap-2">
|
||||
<Image className="w-5 h-5" /> Bike Images
|
||||
</h3>
|
||||
{rental.status === 'active' && (
|
||||
<button
|
||||
onClick={() => setShowImageModal(true)}
|
||||
className="text-xs px-2 py-1 bg-white rounded border border-purple-200 text-purple-700 hover:bg-purple-100 flex items-center gap-1"
|
||||
>
|
||||
<Upload className="w-3 h-3" /> Upload
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{['front', 'back', 'left', 'right'].map(type => {
|
||||
const img = bike?.images.find(i => i.type === type);
|
||||
return (
|
||||
<div key={type} className="aspect-video bg-white rounded-lg border border-purple-200 flex items-center justify-center overflow-hidden">
|
||||
{img?.url ? (
|
||||
<img src={img.url} alt={type} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="text-center p-2">
|
||||
<Image className="w-8 h-8 text-purple-300 mx-auto" />
|
||||
<p className="text-xs text-purple-500 capitalize mt-1">{type}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5" /> Notes ({notes.length})
|
||||
</h3>
|
||||
{notes.length > 0 ? (
|
||||
<div className="space-y-2 mb-3">
|
||||
{notes.map(note => (
|
||||
<div key={note.id} className="bg-white p-3 rounded-lg">
|
||||
<p className="text-sm text-slate-700">{note.text}</p>
|
||||
<p className="text-xs text-slate-400 mt-1">{note.createdAt}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 mb-3">No notes yet.</p>
|
||||
)}
|
||||
{rental.status === 'active' && (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newNote}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
placeholder="Add notes about the bike condition, issues, etc..."
|
||||
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
/>
|
||||
<button onClick={handleAddNote} disabled={!newNote.trim()} className="px-4 py-2 bg-accent text-white rounded-lg text-sm disabled:opacity-50">
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
|
||||
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
|
||||
<User className="w-5 h-5" /> User Info
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Name</span><span className="text-sm font-medium text-blue-800">{user?.name}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Phone</span><span className="text-sm font-medium text-blue-800">{user?.phone}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-blue-600">Email</span><span className="text-sm font-medium text-blue-800">{user?.email || '-'}</span></div>
|
||||
{user && (
|
||||
<div className="flex gap-2 mt-2 pt-2 border-t border-blue-100">
|
||||
<a href={`tel:${user.phone}`} className="flex-1 py-2 bg-green-500 text-white rounded-lg text-sm text-center hover:bg-green-600 flex items-center justify-center gap-2">
|
||||
<Phone className="w-4 h-4" /> Call
|
||||
</a>
|
||||
<a href={`sms:${user.phone}`} className="flex-1 py-2 bg-blue-500 text-white rounded-lg text-sm text-center hover:bg-blue-600 flex items-center justify-center gap-2">
|
||||
<MessageCircle className="w-4 h-4" /> Message
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
||||
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||
<Shield className="w-5 h-5" /> Membership & Insurance
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-purple-600">Membership</span><span className="text-sm font-medium text-purple-800 uppercase">{user?.membership}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-purple-600">KYC Status</span><span className="text-sm font-medium text-purple-800 capitalize">{user?.kycStatus}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-purple-600">Insurance</span><span className="text-sm font-medium text-purple-800 capitalize">{user?.insurance}</span></div>
|
||||
{user?.insuranceExpiry && <div className="flex justify-between"><span className="text-sm text-purple-600">Insurance Expiry</span><span className="text-sm font-medium text-purple-800">{user.insuranceExpiry}</span></div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
|
||||
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5" /> Hub Info
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-green-600">Hub</span><span className="text-sm font-medium text-green-800">{rental.hubName || '-'}</span></div>
|
||||
{editMode ? (
|
||||
<select
|
||||
value={editForm.hubId || ''}
|
||||
onChange={(e) => {
|
||||
const hub = mockHubs.find(h => h.id === e.target.value);
|
||||
setEditForm({ ...editForm, hubId: e.target.value, hubName: hub?.name });
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-green-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Select Hub</option>
|
||||
{mockHubs.map(hub => (
|
||||
<option key={hub.id} value={hub.id}>{hub.name}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="flex justify-between"><span className="text-sm text-green-600">Joined From</span><span className="text-sm font-medium text-green-800">{user?.joinedFrom || '-'}</span></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rental.status === 'locked' && (
|
||||
<div className="bg-red-50 p-4 rounded-xl border border-red-100">
|
||||
<h3 className="font-semibold text-red-800 mb-3 flex items-center gap-2">
|
||||
<Lock className="w-5 h-5" /> Locked Info
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between"><span className="text-sm text-red-600">Locked At</span><span className="text-sm font-medium text-red-800">{rental.lockedAt?.split('T')[0]}</span></div>
|
||||
<div className="flex justify-between"><span className="text-sm text-red-600">Reason</span><span className="text-sm font-medium text-red-800">{rental.lockedReason}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showLockModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<Lock className="w-5 h-5" /> Lock Rental
|
||||
</h3>
|
||||
<button onClick={() => setShowLockModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<label className="text-sm text-slate-600">Reason for locking</label>
|
||||
<textarea
|
||||
value={lockReason}
|
||||
onChange={(e) => setLockReason(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
rows={3}
|
||||
placeholder="Enter reason..."
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowLockModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
<button onClick={handleLockRental} disabled={!lockReason.trim()} className="px-4 py-2 bg-red-600 text-white rounded-lg disabled:opacity-50">Lock Rental</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDueModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" /> Add Due Rental
|
||||
</h3>
|
||||
<button onClick={() => setShowDueModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<label className="text-sm text-slate-600">Due Amount (৳)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={dueAmount}
|
||||
onChange={(e) => setDueAmount(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
placeholder="Enter amount..."
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-2">Current Due: ৳{rental.dueRental || 0}</p>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowDueModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
<button onClick={handleAddDue} disabled={dueAmount <= 0} className="px-4 py-2 bg-amber-600 text-white rounded-lg disabled:opacity-50">Add Due</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showImageModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800">Upload Bike Image</h3>
|
||||
<button onClick={() => setShowImageModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<label className="text-sm text-slate-600">Select Image Type</label>
|
||||
<select
|
||||
value={uploadImageType}
|
||||
onChange={(e) => setUploadImageType(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value="">Select type...</option>
|
||||
<option value="front">Front</option>
|
||||
<option value="back">Back</option>
|
||||
<option value="left">Left Side</option>
|
||||
<option value="right">Right Side</option>
|
||||
</select>
|
||||
{uploadImageType && (
|
||||
<div className="mt-4">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleUploadImage}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm file:mr-4 file:px-4 file:py-2 file:bg-accent file:text-white file:rounded-lg file:border-0 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setShowImageModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Gauge({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Battery({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="1" y="6" width="18" height="12" rx="2" />
|
||||
<line x1="23" y1="10" x2="23" y2="14" />
|
||||
<line x1="7" y1="10" x2="7" y2="14" />
|
||||
<line x1="11" y1="10" x2="11" y2="14" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,270 @@
|
||||
import { FileText, Search, Filter, Bike, User, Calendar, DollarSign, Clock, MoreVertical, Eye } from 'lucide-react';
|
||||
import { rentals, bikes, users } from '@/data/mockData';
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FileText, Search, Filter, Bike, User, Calendar, DollarSign, Clock, MoreVertical, Eye, Plus, Phone, MessageCircle, X, CreditCard, Wallet, Building, Download, Printer } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
type RentalStatus = 'active' | 'pending' | 'completed' | 'disputed' | 'cancelled' | 'locked';
|
||||
type RentalType = 'single' | 'shared' | 'rent-to-own';
|
||||
|
||||
interface Rental {
|
||||
id: string;
|
||||
bikeId: string;
|
||||
userId: string;
|
||||
type: RentalType;
|
||||
status: RentalStatus;
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
contractMonths?: number;
|
||||
deposit: number;
|
||||
dailyRate: number;
|
||||
totalPaid: number;
|
||||
dueRental?: number;
|
||||
lockedAt?: string;
|
||||
lockedReason?: string;
|
||||
hubId?: string;
|
||||
hubName?: string;
|
||||
}
|
||||
|
||||
const mockBikes = [
|
||||
{ id: 'BIKE-001', model: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9012', status: 'available' },
|
||||
{ id: 'BIKE-002', model: 'Yadea DT3', plate: 'Dhaka Metro Ba-5521', status: 'available' },
|
||||
{ id: 'BIKE-003', model: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', status: 'available' },
|
||||
{ id: 'BIKE-004', model: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9013', status: 'rented' },
|
||||
{ id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'available' },
|
||||
];
|
||||
|
||||
const mockUsers = [
|
||||
{ id: 'USR-001', name: 'Rahim Ahmed', phone: '+8801712345678', membership: 'vip' },
|
||||
{ id: 'USR-002', name: 'Karim Hasan', phone: '+8801812345678', membership: 'standard' },
|
||||
{ id: 'USR-003', name: 'Jamal Uddin', phone: '+8801912345678', membership: 'premium' },
|
||||
{ id: 'USR-004', name: 'Rafiq Islam', phone: '+8801512345678', membership: 'standard' },
|
||||
];
|
||||
|
||||
const mockHubs = [
|
||||
{ id: 'HUB-001', name: 'Gulshan Hub' },
|
||||
{ id: 'HUB-002', name: 'Banani Hub' },
|
||||
{ id: 'HUB-003', name: 'Uttara Hub' },
|
||||
{ id: 'HUB-004', name: 'Mirpur Hub' },
|
||||
];
|
||||
|
||||
const mockRentals: Rental[] = [
|
||||
{
|
||||
id: 'RNT-001',
|
||||
bikeId: 'BIKE-001',
|
||||
userId: 'USR-001',
|
||||
type: 'single',
|
||||
status: 'active',
|
||||
startDate: '2024-01-15',
|
||||
deposit: 5000,
|
||||
dailyRate: 300,
|
||||
totalPaid: 81900,
|
||||
dueRental: 0,
|
||||
hubId: 'HUB-001',
|
||||
hubName: 'Gulshan Hub'
|
||||
},
|
||||
{
|
||||
id: 'RNT-002',
|
||||
bikeId: 'BIKE-002',
|
||||
userId: 'USR-002',
|
||||
type: 'single',
|
||||
status: 'pending',
|
||||
startDate: '2024-02-10',
|
||||
deposit: 0,
|
||||
dailyRate: 150,
|
||||
totalPaid: 150,
|
||||
dueRental: 0,
|
||||
hubId: 'HUB-002',
|
||||
hubName: 'Banani Hub'
|
||||
},
|
||||
{
|
||||
id: 'RNT-003',
|
||||
bikeId: 'BIKE-003',
|
||||
userId: 'USR-003',
|
||||
type: 'rent-to-own',
|
||||
status: 'completed',
|
||||
startDate: '2023-06-01',
|
||||
endDate: '2023-12-01',
|
||||
deposit: 10000,
|
||||
dailyRate: 500,
|
||||
totalPaid: 150000,
|
||||
hubId: 'HUB-001',
|
||||
hubName: 'Gulshan Hub'
|
||||
}
|
||||
];
|
||||
|
||||
export default function RentalsPage() {
|
||||
const [rentals, setRentals] = useState<Rental[]>(mockRentals);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
|
||||
const [newRental, setNewRental] = useState({
|
||||
bikeId: '',
|
||||
userId: '',
|
||||
type: 'single' as RentalType,
|
||||
startDate: new Date().toISOString().split('T')[0],
|
||||
contractMonths: 0,
|
||||
deposit: 0,
|
||||
dailyRate: 150,
|
||||
hubId: '',
|
||||
paymentMethod: 'cash' as 'cash' | 'bank' | 'biker_wallet',
|
||||
});
|
||||
|
||||
const [showJournalPreview, setShowJournalPreview] = useState(false);
|
||||
|
||||
const filteredRentals = rentals.filter(r => {
|
||||
if (statusFilter !== 'all' && r.status !== statusFilter) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
active: rentals.filter(r => r.status === 'active').length,
|
||||
pending: rentals.filter(r => r.status === 'pending').length,
|
||||
completed: rentals.filter(r => r.status === 'completed').length,
|
||||
disputed: rentals.filter(r => r.status === 'disputed').length,
|
||||
};
|
||||
|
||||
const handleCreateRental = () => {
|
||||
if (!newRental.bikeId || !newRental.userId || !newRental.hubId) return;
|
||||
setShowJournalPreview(true);
|
||||
};
|
||||
|
||||
const confirmCreateRental = () => {
|
||||
const bike = mockBikes.find(b => b.id === newRental.bikeId);
|
||||
const user = mockUsers.find(u => u.id === newRental.userId);
|
||||
const hub = mockHubs.find(h => h.id === newRental.hubId);
|
||||
|
||||
const rental: Rental = {
|
||||
id: `RNT-${String(rentals.length + 1).padStart(3, '0')}`,
|
||||
bikeId: newRental.bikeId,
|
||||
userId: newRental.userId,
|
||||
type: newRental.type,
|
||||
status: 'pending',
|
||||
startDate: newRental.startDate,
|
||||
endDate: newRental.contractMonths > 0
|
||||
? new Date(new Date(newRental.startDate).setMonth(new Date(newRental.startDate).getMonth() + newRental.contractMonths)).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
contractMonths: newRental.contractMonths || undefined,
|
||||
deposit: newRental.deposit,
|
||||
dailyRate: newRental.dailyRate,
|
||||
totalPaid: newRental.deposit,
|
||||
dueRental: 0,
|
||||
hubId: newRental.hubId,
|
||||
hubName: hub?.name,
|
||||
};
|
||||
|
||||
setRentals([...rentals, rental]);
|
||||
setShowJournalPreview(false);
|
||||
setShowCreateModal(false);
|
||||
setNewRental({
|
||||
bikeId: '',
|
||||
userId: '',
|
||||
type: 'single',
|
||||
startDate: new Date().toISOString().split('T')[0],
|
||||
contractMonths: 0,
|
||||
deposit: 0,
|
||||
dailyRate: 150,
|
||||
hubId: '',
|
||||
paymentMethod: 'cash',
|
||||
});
|
||||
};
|
||||
|
||||
const generateInvoice = () => {
|
||||
if (newRental.deposit <= 0) return;
|
||||
|
||||
const rentalId = `RNT-${String(rentals.length + 1).padStart(3, '0')}`;
|
||||
const bike = mockBikes.find(b => b.id === newRental.bikeId);
|
||||
const user = mockUsers.find(u => u.id === newRental.userId);
|
||||
const hub = mockHubs.find(h => h.id === newRental.hubId);
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||
const contractMonths = newRental.contractMonths;
|
||||
const endDate = contractMonths > 0
|
||||
? new Date(new Date(newRental.startDate).setMonth(new Date(newRental.startDate).getMonth() + contractMonths)).toISOString().split('T')[0]
|
||||
: '';
|
||||
|
||||
import('jspdf').then((jsPDF) => {
|
||||
const doc = new jsPDF.default();
|
||||
|
||||
doc.setFillColor(6, 95, 70);
|
||||
doc.rect(0, 0, 220, 40, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(24);
|
||||
doc.text('JAIBEN Mobility Ltd', 20, 20);
|
||||
doc.setFontSize(12);
|
||||
doc.text('Payment Receipt', 20, 30);
|
||||
|
||||
doc.setTextColor(6, 95, 70);
|
||||
doc.setFontSize(18);
|
||||
doc.text('OFFICIAL RECEIPT', 120, 25);
|
||||
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Receipt No: ${rentalId}-DEP`, 20, 55);
|
||||
doc.text(`Date: ${date} ${time}`, 20, 62);
|
||||
|
||||
doc.line(20, 70, 190, 70);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text('Payment Details', 20, 82);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Rental ID: ${rentalId}`, 20, 90);
|
||||
doc.text(`Bike: ${bike?.model} (${bike?.plate})`, 20, 97);
|
||||
doc.text(`Customer: ${user?.name}`, 20, 104);
|
||||
doc.text(`Phone: ${user?.phone}`, 20, 111);
|
||||
doc.text(`Hub: ${hub?.name}`, 20, 118);
|
||||
doc.text(`Rental Type: ${newRental.type}`, 20, 125);
|
||||
doc.text(`Start Date: ${newRental.startDate}`, 20, 132);
|
||||
if (contractMonths > 0) {
|
||||
doc.text(`Contract: ${contractMonths} Months`, 20, 139);
|
||||
doc.text(`End Date: ${endDate}`, 20, 146);
|
||||
doc.text(`Daily Rate: ৳${newRental.dailyRate}/day`, 20, 156);
|
||||
} else {
|
||||
doc.text(`Daily Rate: ৳${newRental.dailyRate}/day`, 20, 139);
|
||||
}
|
||||
|
||||
doc.line(20, 165, 190, 165);
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.text('Amount Details', 20, 175);
|
||||
doc.setFillColor(240, 240, 240);
|
||||
doc.rect(20, 180, 170, 20, 'F');
|
||||
doc.setFontSize(10);
|
||||
doc.text('Deposit', 30, 192);
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(6, 95, 70);
|
||||
doc.text(`৳${newRental.deposit}`, 150, 192);
|
||||
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Payment: ${newRental.paymentMethod === 'cash' ? 'Cash' : newRental.paymentMethod === 'bank' ? 'Bank' : 'Wallet'}`, 20, 210);
|
||||
|
||||
const qrData = `JAIBEN|${rentalId}|${date}|${user?.name}|${bike?.model}|${newRental.deposit}|${newRental.startDate}|${contractMonths}`;
|
||||
const publicUrl = `https://jaiben.app/rental/${rentalId}`;
|
||||
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(100);
|
||||
doc.text('Generated from JAIBEN Rental System', 20, 250);
|
||||
doc.text(`QR: ${qrData.substring(0, 35)}...`, 20, 258);
|
||||
doc.setTextColor(6, 95, 70);
|
||||
doc.text(`URL: ${publicUrl}`, 20, 266);
|
||||
|
||||
doc.save(`deposit-receipt-${rentalId}.pdf`);
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: RentalStatus) => {
|
||||
const styles = {
|
||||
active: 'bg-green-100 text-green-700',
|
||||
pending: 'bg-amber-100 text-amber-700',
|
||||
completed: 'bg-blue-100 text-blue-700',
|
||||
disputed: 'bg-red-100 text-red-700',
|
||||
cancelled: 'bg-slate-100 text-slate-700',
|
||||
locked: 'bg-red-100 text-red-700',
|
||||
};
|
||||
return styles[status];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
||||
@@ -10,25 +273,48 @@ export default function RentalsPage() {
|
||||
<p className="text-sm text-slate-500 mt-1">View and manage all rental transactions</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
|
||||
<Filter className="w-4 h-4" /> Filter
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="disputed">Disputed</option>
|
||||
<option value="locked">Locked</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> New Rental
|
||||
</button>
|
||||
<button className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark">
|
||||
<button className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50">
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
{['Active', 'Pending', 'Completed', 'Disputed'].map(status => {
|
||||
const count = rentals.filter(r => r.status === status.toLowerCase()).length;
|
||||
return (
|
||||
<div key={status} className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-2xl font-extrabold text-slate-800">{count}</p>
|
||||
<p className="text-sm text-slate-500">{status} Rentals</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-2xl font-extrabold text-green-600">{stats.active}</p>
|
||||
<p className="text-sm text-slate-500">Active Rentals</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-2xl font-extrabold text-amber-600">{stats.pending}</p>
|
||||
<p className="text-sm text-slate-500">Pending</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-2xl font-extrabold text-blue-600">{stats.completed}</p>
|
||||
<p className="text-sm text-slate-500">Completed</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
|
||||
<p className="text-2xl font-extrabold text-red-600">{stats.disputed}</p>
|
||||
<p className="text-sm text-slate-500">Disputed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
||||
@@ -41,15 +327,18 @@ export default function RentalsPage() {
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">User</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hub</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Deposit</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Daily</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Paid</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{rentals.map(rental => {
|
||||
const bike = bikes.find(b => b.id === rental.bikeId);
|
||||
{filteredRentals.map(rental => {
|
||||
const bike = mockBikes.find(b => b.id === rental.bikeId);
|
||||
const user = mockUsers.find(u => u.id === rental.userId);
|
||||
return (
|
||||
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
@@ -58,13 +347,19 @@ export default function RentalsPage() {
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bike className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm text-slate-600">{bike?.model}</span>
|
||||
<div>
|
||||
<span className="text-sm text-slate-600">{bike?.model || rental.bikeId}</span>
|
||||
<p className="text-xs text-slate-400">{bike?.plate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm text-slate-600">{rental.userId}</span>
|
||||
<div>
|
||||
<span className="text-sm text-slate-600">{user?.name || rental.userId}</span>
|
||||
<p className="text-xs text-slate-400">{user?.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -75,26 +370,39 @@ export default function RentalsPage() {
|
||||
<Calendar className="w-3 h-3" /> {rental.startDate}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-slate-600">{rental.hubName || '-'}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm font-medium text-slate-700">৳{rental.deposit}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm font-semibold text-green-600">৳{rental.totalPaid}</span>
|
||||
<span className="text-sm text-slate-600">৳{rental.dailyRate}/d</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${
|
||||
rental.status === 'active' ? 'bg-green-100 text-green-700' :
|
||||
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
|
||||
rental.status === 'completed' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
<span className="text-sm font-semibold text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${getStatusBadge(rental.status)}`}>
|
||||
{rental.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button className="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<Eye className="w-4 h-4 text-slate-400" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<Link href={`/admin/rentals/${rental.id}`} className="p-1.5 hover:bg-slate-100 rounded-lg" title="View">
|
||||
<Eye className="w-4 h-4 text-slate-500" />
|
||||
</Link>
|
||||
{user && (
|
||||
<>
|
||||
<a href={`tel:${user.phone}`} className="p-1.5 hover:bg-green-100 rounded-lg" title="Call">
|
||||
<Phone className="w-4 h-4 text-green-500" />
|
||||
</a>
|
||||
<a href={`sms:${user.phone}`} className="p-1.5 hover:bg-blue-100 rounded-lg" title="Message">
|
||||
<MessageCircle className="w-4 h-4 text-blue-500" />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -103,6 +411,281 @@ export default function RentalsPage() {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800">Create New Rental</h3>
|
||||
<button onClick={() => setShowCreateModal(false)} className="text-slate-400 hover:text-slate-600">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Select User</label>
|
||||
<select
|
||||
value={newRental.userId}
|
||||
onChange={(e) => setNewRental({ ...newRental, userId: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value="">Select User...</option>
|
||||
{mockUsers.map(user => (
|
||||
<option key={user.id} value={user.id}>{user.name} ({user.phone})</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Select Bike</label>
|
||||
<select
|
||||
value={newRental.bikeId}
|
||||
onChange={(e) => setNewRental({ ...newRental, bikeId: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value="">Select Bike...</option>
|
||||
{mockBikes.filter(b => b.status === 'available').map(bike => (
|
||||
<option key={bike.id} value={bike.id}>{bike.model} - {bike.plate}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Rental Type</label>
|
||||
<select
|
||||
value={newRental.type}
|
||||
onChange={(e) => setNewRental({ ...newRental, type: e.target.value as RentalType })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value="single">Single</option>
|
||||
<option value="shared">Shared</option>
|
||||
<option value="rent-to-own">Rent to Own</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={newRental.startDate}
|
||||
onChange={(e) => setNewRental({ ...newRental, startDate: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Contract Months</label>
|
||||
<select
|
||||
value={newRental.contractMonths}
|
||||
onChange={(e) => setNewRental({ ...newRental, contractMonths: Number(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value={0}>Daily Basis</option>
|
||||
<option value={3}>3 Months</option>
|
||||
<option value={6}>6 Months</option>
|
||||
<option value={12}>12 Months</option>
|
||||
<option value={18}>18 Months</option>
|
||||
<option value={24}>24 Months</option>
|
||||
</select>
|
||||
{newRental.contractMonths > 0 && (
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
End Date: {new Date(new Date(newRental.startDate).setMonth(new Date(newRental.startDate).getMonth() + newRental.contractMonths)).toISOString().split('T')[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Daily Rate (৳)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newRental.dailyRate}
|
||||
onChange={(e) => setNewRental({ ...newRental, dailyRate: Number(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Deposit (৳)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newRental.deposit}
|
||||
onChange={(e) => setNewRental({ ...newRental, deposit: Number(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Hub</label>
|
||||
<select
|
||||
value={newRental.hubId}
|
||||
onChange={(e) => setNewRental({ ...newRental, hubId: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||||
>
|
||||
<option value="">Select Hub...</option>
|
||||
{mockHubs.map(hub => (
|
||||
<option key={hub.id} value={hub.id}>{hub.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{newRental.deposit > 0 && (
|
||||
<div>
|
||||
<label className="text-sm text-slate-600">Deposit Payment Method</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNewRental({ ...newRental, paymentMethod: 'cash' })}
|
||||
className={`flex-1 py-2 px-3 rounded-lg text-sm flex items-center justify-center gap-2 border ${
|
||||
newRental.paymentMethod === 'cash'
|
||||
? 'bg-green-100 border-green-300 text-green-700'
|
||||
: 'border-slate-200 text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<Wallet className="w-4 h-4" /> Cash
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNewRental({ ...newRental, paymentMethod: 'bank' })}
|
||||
className={`flex-1 py-2 px-3 rounded-lg text-sm flex items-center justify-center gap-2 border ${
|
||||
newRental.paymentMethod === 'bank'
|
||||
? 'bg-blue-100 border-blue-300 text-blue-700'
|
||||
: 'border-slate-200 text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<Building className="w-4 h-4" /> Bank
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNewRental({ ...newRental, paymentMethod: 'biker_wallet' })}
|
||||
className={`flex-1 py-2 px-3 rounded-lg text-sm flex items-center justify-center gap-2 border ${
|
||||
newRental.paymentMethod === 'biker_wallet'
|
||||
? 'bg-purple-100 border-purple-300 text-purple-700'
|
||||
: 'border-slate-200 text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<CreditCard className="w-4 h-4" /> Wallet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateRental}
|
||||
disabled={!newRental.bikeId || !newRental.userId || !newRental.hubId}
|
||||
className="px-4 py-2 bg-accent text-white rounded-lg text-sm disabled:opacity-50"
|
||||
>
|
||||
Review & Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showJournalPreview && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div className="p-4 border-b border-slate-100">
|
||||
<h3 className="font-semibold text-slate-800">Journal Entry Preview</h3>
|
||||
<p className="text-sm text-slate-500">Auto-generated journal for rental deposit</p>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="bg-slate-50 p-3 rounded-lg">
|
||||
<p className="text-sm font-medium text-slate-700 mb-2">Rental: {newRental.type.toUpperCase()}</p>
|
||||
<p className="text-xs text-slate-500">Date: {new Date().toISOString().split('T')[0]}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700 mb-2">Journal Entries</p>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-100">
|
||||
<tr>
|
||||
<th className="px-2 py-1 text-left">Account</th>
|
||||
<th className="px-2 py-1 text-right">Debit (৳)</th>
|
||||
<th className="px-2 py-1 text-right">Credit (৳)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{newRental.deposit > 0 && (
|
||||
<>
|
||||
<tr>
|
||||
<td className="px-2 py-2">
|
||||
{newRental.paymentMethod === 'cash' && '1000 - Cash'}
|
||||
{newRental.paymentMethod === 'bank' && '1100 - Bank'}
|
||||
{newRental.paymentMethod === 'biker_wallet' && '1200 - Biker Wallet'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right font-medium">{newRental.deposit}</td>
|
||||
<td className="px-2 py-2 text-right">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-2 py-2">2100 - Deposit Received</td>
|
||||
<td className="px-2 py-2 text-right">-</td>
|
||||
<td className="px-2 py-2 text-right font-medium">{newRental.deposit}</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
{newRental.deposit === 0 && (
|
||||
<tr>
|
||||
<td className="px-2 py-2 text-center text-slate-500" colSpan={3}>
|
||||
No deposit amount - Journal not required
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
{newRental.deposit > 0 && (
|
||||
<tfoot className="bg-slate-100">
|
||||
<tr>
|
||||
<td className="px-2 py-1 font-medium">Total</td>
|
||||
<td className="px-2 py-1 text-right font-medium">{newRental.deposit}</td>
|
||||
<td className="px-2 py-1 text-right font-medium">{newRental.deposit}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-3 rounded-lg">
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>Bike:</strong> {mockBikes.find(b => b.id === newRental.bikeId)?.model} {mockBikes.find(b => b.id === newRental.bikeId)?.plate}
|
||||
</p>
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>User:</strong> {mockUsers.find(u => u.id === newRental.userId)?.name} ({mockUsers.find(u => u.id === newRental.userId)?.phone})
|
||||
</p>
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>Hub:</strong> {mockHubs.find(h => h.id === newRental.hubId)?.name}
|
||||
</p>
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>Type:</strong> {newRental.type} | <strong>Contract:</strong> {newRental.contractMonths ? `${newRental.contractMonths} Months` : 'Daily Basis'}
|
||||
</p>
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>Daily Rate:</strong> ৳{newRental.dailyRate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-between">
|
||||
<button
|
||||
onClick={generateInvoice}
|
||||
disabled={newRental.deposit <= 0}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
<Printer className="w-4 h-4" /> Print Invoice
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowJournalPreview(false)}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmCreateRental}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700"
|
||||
>
|
||||
Confirm & Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
250
src/app/rental/[id]/page.tsx
Normal file
250
src/app/rental/[id]/page.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Bike, User, Calendar, DollarSign, MapPin, Shield, Clock, CheckCircle } from 'lucide-react';
|
||||
|
||||
type RentalStatus = 'active' | 'pending' | 'completed' | 'disputed' | 'cancelled' | 'locked';
|
||||
type RentalType = 'single' | 'shared' | 'rent-to-own';
|
||||
|
||||
interface Rental {
|
||||
id: string;
|
||||
bikeId: string;
|
||||
userId: string;
|
||||
type: RentalType;
|
||||
status: RentalStatus;
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
contractMonths?: number;
|
||||
deposit: number;
|
||||
dailyRate: number;
|
||||
totalPaid: number;
|
||||
dueRental?: number;
|
||||
hubId?: string;
|
||||
hubName?: string;
|
||||
}
|
||||
|
||||
interface BikeInfo {
|
||||
id: string;
|
||||
model: string;
|
||||
plate: string;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
membership: string;
|
||||
}
|
||||
|
||||
const mockRentals: Rental[] = [
|
||||
{
|
||||
id: 'RNT-001',
|
||||
bikeId: 'BIKE-001',
|
||||
userId: 'USR-001',
|
||||
type: 'single',
|
||||
status: 'active',
|
||||
startDate: '2024-01-15',
|
||||
contractMonths: 6,
|
||||
endDate: '2024-07-15',
|
||||
deposit: 5000,
|
||||
dailyRate: 300,
|
||||
totalPaid: 81900,
|
||||
dueRental: 0,
|
||||
hubId: 'HUB-001',
|
||||
hubName: 'Gulshan Hub'
|
||||
},
|
||||
{
|
||||
id: 'RNT-002',
|
||||
bikeId: 'BIKE-002',
|
||||
userId: 'USR-002',
|
||||
type: 'single',
|
||||
status: 'pending',
|
||||
startDate: '2024-02-10',
|
||||
contractMonths: 12,
|
||||
endDate: '2025-02-10',
|
||||
deposit: 3000,
|
||||
dailyRate: 150,
|
||||
totalPaid: 3000,
|
||||
hubId: 'HUB-002',
|
||||
hubName: 'Banani Hub'
|
||||
}
|
||||
];
|
||||
|
||||
const mockBikes: Record<string, BikeInfo> = {
|
||||
'BIKE-001': { id: 'BIKE-001', model: 'AIMA Lightning', plate: 'Dhaka Metro Cha-9012' },
|
||||
'BIKE-002': { id: 'BIKE-002', model: 'Yadea DT3', plate: 'Dhaka Metro Ba-5521' },
|
||||
'BIKE-003': { id: 'BIKE-003', model: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234' },
|
||||
};
|
||||
|
||||
const mockUsers: Record<string, UserInfo> = {
|
||||
'USR-001': { id: 'USR-001', name: 'Rahim Ahmed', phone: '+8801712345678', membership: 'vip' },
|
||||
'USR-002': { id: 'USR-002', name: 'Karim Hasan', phone: '+8801812345678', membership: 'standard' },
|
||||
'USR-003': { id: 'USR-003', name: 'Jamal Uddin', phone: '+8801912345678', membership: 'premium' },
|
||||
};
|
||||
|
||||
export default function PublicRentalPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const [rental, setRental] = useState<Rental | null>(null);
|
||||
const [bike, setBike] = useState<BikeInfo | null>(null);
|
||||
const [user, setUser] = useState<UserInfo | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
const found = mockRentals.find(r => r.id === id);
|
||||
if (found) {
|
||||
setRental(found);
|
||||
setBike(mockBikes[found.bikeId] || null);
|
||||
setUser(mockUsers[found.userId] || null);
|
||||
}
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (!rental) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-100 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Clock className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-slate-800 mb-2">Rental Not Found</h1>
|
||||
<p className="text-slate-500">This rental ID is not valid or has been removed.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
active: 'bg-green-500',
|
||||
pending: 'bg-amber-500',
|
||||
completed: 'bg-blue-500',
|
||||
disputed: 'bg-red-500',
|
||||
cancelled: 'bg-slate-400',
|
||||
locked: 'bg-red-600',
|
||||
};
|
||||
|
||||
const isExpired = rental.endDate && new Date(rental.endDate) < new Date();
|
||||
const isActive = rental.status === 'active' && !isExpired;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 p-4">
|
||||
<div className="max-w-lg mx-auto">
|
||||
<div className="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div className={`${statusColors[rental.status]} text-white p-6 text-center`}>
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
<span className="text-lg font-semibold uppercase">{rental.status}</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">RENTAL ID: {rental.id}</p>
|
||||
{isExpired && <p className="text-sm mt-2 opacity-80">EXPIRED</p>}
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="bg-blue-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Bike className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-blue-600">Bike</p>
|
||||
<p className="font-semibold text-blue-800">{bike?.model}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-blue-600">Plate</span>
|
||||
<span className="font-medium text-blue-800">{bike?.plate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-purple-600">Renter</p>
|
||||
<p className="font-semibold text-purple-800">{user?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-purple-600">Phone</span>
|
||||
<span className="font-medium text-purple-800">{user?.phone}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm mt-1">
|
||||
<span className="text-purple-600">Membership</span>
|
||||
<span className="font-medium text-purple-800 uppercase">{user?.membership}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<Calendar className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-green-600">Contract Period</p>
|
||||
<p className="font-semibold text-green-800">
|
||||
{rental.contractMonths ? `${rental.contractMonths} Months` : 'Daily Basis'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-600">Start Date</span>
|
||||
<span className="font-medium text-green-800">{rental.startDate}</span>
|
||||
</div>
|
||||
{rental.endDate && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-600">End Date</span>
|
||||
<span className="font-medium text-green-800">{rental.endDate}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm mt-2 pt-2 border-t border-green-200">
|
||||
<span className="text-green-600 font-medium">Daily Rate</span>
|
||||
<span className="font-bold text-green-700">৳{rental.dailyRate}/day</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center">
|
||||
<DollarSign className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-amber-600">Payment</p>
|
||||
<p className="font-semibold text-amber-800">Total Paid: ৳{rental.totalPaid.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-600">Deposit Paid</span>
|
||||
<span className="font-medium text-amber-800">৳{rental.deposit.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-600">Due</span>
|
||||
<span className="font-medium text-amber-800">৳{(rental.dueRental || 0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-slate-200 rounded-lg flex items-center justify-center">
|
||||
<MapPin className="w-5 h-5 text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-slate-600">Hub</p>
|
||||
<p className="font-semibold text-slate-800">{rental.hubName || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 text-center">
|
||||
<p className="text-xs text-slate-400">Verified by JAIBEN Mobility Ltd</p>
|
||||
<p className="text-xs text-slate-400">Generated from JAIBEN Rental System</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,16 +20,20 @@ import {
|
||||
Shield,
|
||||
Truck,
|
||||
ChevronDown,
|
||||
LogOut
|
||||
LogOut,
|
||||
Calculator,
|
||||
Wrench
|
||||
} from 'lucide-react';
|
||||
|
||||
const adminNavItems = [
|
||||
{ label: 'Dashboard', href: '/admin', icon: BarChart3 },
|
||||
{ label: 'KYC Requests', href: '/admin/kyc', icon: Shield },
|
||||
{ label: 'Rentals', href: '/admin/rentals', icon: FileText },
|
||||
{ label: 'Bikers', href: '/admin/bikers', icon: Users },
|
||||
{ label: 'Investors', href: '/admin/investors', icon: Wallet },
|
||||
{ label: 'Fleet Management', href: '/admin/fleet', icon: Bike },
|
||||
{ label: 'KYC Requests', href: '/admin/kyc', icon: Shield },
|
||||
{ label: 'Rentals', href: '/admin/rentals', icon: FileText },
|
||||
{ label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench },
|
||||
{ label: 'Accounting', href: '/admin/accounting', icon: Calculator },
|
||||
{ label: 'Revenue', href: '/admin/revenue', icon: CreditCard },
|
||||
{ label: 'Reports', href: '/admin/reports', icon: BarChart3 },
|
||||
{ label: 'Geofences', href: '/admin/geofence', icon: MapPin },
|
||||
@@ -63,8 +67,8 @@ export default function Sidebar() {
|
||||
const isShop = pathname.startsWith('/shop');
|
||||
|
||||
const navItems = isAdmin ? adminNavItems :
|
||||
isInvestor ? investorNavItems :
|
||||
isShop ? shopNavItems : bikerNavItems;
|
||||
isInvestor ? investorNavItems :
|
||||
isShop ? shopNavItems : bikerNavItems;
|
||||
|
||||
const toggleMenu = (label: string) => {
|
||||
setExpandedMenu(expandedMenu === label ? null : label);
|
||||
|
||||
@@ -24,6 +24,21 @@ export interface Bike {
|
||||
purchaseDate?: string;
|
||||
currentRent?: number;
|
||||
totalEarnings?: number;
|
||||
assignmentHistory?: BikeAssignment[];
|
||||
}
|
||||
|
||||
export interface BikeAssignment {
|
||||
id: string;
|
||||
bikeId: string;
|
||||
bikerId: string;
|
||||
bikerName: string;
|
||||
assignedAt: string;
|
||||
assignedBy: string;
|
||||
unassignedAt?: string;
|
||||
unassignedBy?: string;
|
||||
reason?: string;
|
||||
status: 'active' | 'completed';
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface Rental {
|
||||
@@ -154,16 +169,62 @@ export const users: User[] = [
|
||||
];
|
||||
|
||||
export const bikes: Bike[] = [
|
||||
{ id: 'b1', model: 'Etron ET50', brand: 'Etron', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: ' Dhaka Metro Cha-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'u1', investorId: 'inv1', purchasePrice: 85000, purchaseDate: '2024-01-15', currentRent: 350, totalEarnings: 14250 },
|
||||
{ id: 'b2', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5678', status: 'available', batteryLevel: 95, location: 'Banani', investorId: 'inv1', purchasePrice: 65000, purchaseDate: '2024-01-20', currentRent: 0, totalEarnings: 12750 },
|
||||
{ id: 'b3', model: 'AIMA Lightning', brand: 'AIMA', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-9012', status: 'rented', batteryLevel: 62, location: 'Uttara', assignedTo: 'u2', investorId: 'inv2', purchasePrice: 95000, purchaseDate: '2023-11-05', currentRent: 450, totalEarnings: 22500 },
|
||||
{ id: 'b4', model: 'TVS iQube', brand: 'TVS', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-3456', status: 'maintenance', batteryLevel: 45, location: 'Workshop', investorId: 'inv2', purchasePrice: 72000, purchaseDate: '2023-12-01', currentRent: 0, totalEarnings: 8500 },
|
||||
{ id: 'b5', model: 'Bajaj Chetak', brand: 'Bajaj', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-7890', status: 'available', batteryLevel: 100, location: 'Dhanmondi', investorId: 'inv2', purchasePrice: 68000, purchaseDate: '2023-11-10', currentRent: 0, totalEarnings: 20000 },
|
||||
{ id: 'b6', model: 'Hero Photon', brand: 'Hero', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-1111', status: 'rented', batteryLevel: 88, location: 'Mirpur', investorId: 'inv2', purchasePrice: 55000, purchaseDate: '2024-02-01', currentRent: 300, totalEarnings: 4500 },
|
||||
{ id: 'b7', model: 'Okinawa Praise', brand: 'Okinawa', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-2222', status: 'available', batteryLevel: 92, location: 'Gulshan 2', investorId: 'inv3', purchasePrice: 75000, purchaseDate: '2024-02-10', currentRent: 0, totalEarnings: 9500 },
|
||||
{ id: 'b8', model: 'Bajaj Chetak', brand: 'Bajaj', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-3333', status: 'available', batteryLevel: 100, location: 'Dhanmondi', purchasePrice: 70000, purchaseDate: '2024-03-01', currentRent: 0, totalEarnings: 0 },
|
||||
{ id: 'b9', model: 'TVS iQube', brand: 'TVS', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-4444', status: 'rented', batteryLevel: 75, location: 'Banani', purchasePrice: 75000, purchaseDate: '2024-03-05', currentRent: 380, totalEarnings: 1140 },
|
||||
{ id: 'b10', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5555', status: 'available', batteryLevel: 90, location: 'Uttara', purchasePrice: 68000, purchaseDate: '2024-03-10', currentRent: 0, totalEarnings: 0 },
|
||||
{
|
||||
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'u1', investorId: 'inv1', purchasePrice: 85000, purchaseDate: '2024-01-15', currentRent: 350, totalEarnings: 14250,
|
||||
assignmentHistory: [
|
||||
{ id: 'ash1', bikeId: 'EV001', bikerId: 'u3', bikerName: 'Rahim Khan', assignedAt: '2024-01-20 10:30:00', assignedBy: 'admin1', unassignedAt: '2024-02-15 14:20:00', unassignedBy: 'admin1', reason: 'Bike transfer to another biker', status: 'completed', notes: 'Initial assignment' },
|
||||
{ id: 'ash2', bikeId: 'EV001', bikerId: 'u1', bikerName: 'Karim Ahmed', assignedAt: '2024-02-15 15:00:00', assignedBy: 'admin1', status: 'active', notes: 'Reassigned after maintenance' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5678', status: 'available', batteryLevel: 95, location: 'Banani', investorId: 'inv1', purchasePrice: 65000, purchaseDate: '2024-01-20', currentRent: 0, totalEarnings: 12750,
|
||||
assignmentHistory: [
|
||||
{ id: 'ash3', bikeId: 'EV002', bikerId: 'u1', bikerName: 'Karim Ahmed', assignedAt: '2024-01-25 09:00:00', assignedBy: 'admin1', unassignedAt: '2024-03-01 11:30:00', unassignedBy: 'admin1', reason: 'Rental completed', status: 'completed', notes: 'First rental period' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV003', model: 'AIMA Lightning', brand: 'AIMA', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-9012', status: 'rented', batteryLevel: 62, location: 'Uttara', assignedTo: 'u2', investorId: 'inv2', purchasePrice: 95000, purchaseDate: '2023-11-05', currentRent: 450, totalEarnings: 22500,
|
||||
assignmentHistory: [
|
||||
{ id: 'ash4', bikeId: 'EV003', bikerId: 'u4', bikerName: 'Jamal Hossain', assignedAt: '2023-11-10 08:00:00', assignedBy: 'admin1', unassignedAt: '2024-01-05 16:00:00', unassignedBy: 'admin1', reason: 'Bike returned for maintenance', status: 'completed' },
|
||||
{ id: 'ash5', bikeId: 'EV003', bikerId: 'u2', bikerName: 'Sofiq Rahman', assignedAt: '2024-01-10 10:00:00', assignedBy: 'admin1', status: 'active', notes: 'Rent-to-own agreement' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV004', model: 'TVS iQube', brand: 'TVS', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-3456', status: 'maintenance', batteryLevel: 45, location: 'Workshop', investorId: 'inv2', purchasePrice: 72000, purchaseDate: '2023-12-01', currentRent: 0, totalEarnings: 8500,
|
||||
assignmentHistory: [
|
||||
{ id: 'ash6', bikeId: 'EV004', bikerId: 'u5', bikerName: 'Ripon Mia', assignedAt: '2023-12-05 12:00:00', assignedBy: 'admin1', unassignedAt: '2024-03-10 09:00:00', unassignedBy: 'admin1', reason: 'Battery replacement - under maintenance', status: 'completed', notes: 'Battery damaged, sent to workshop' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV005', model: 'Bajaj Chetak', brand: 'Bajaj', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-7890', status: 'available', batteryLevel: 100, location: 'Dhanmondi', investorId: 'inv2', purchasePrice: 68000, purchaseDate: '2023-11-10', currentRent: 0, totalEarnings: 20000,
|
||||
assignmentHistory: [
|
||||
{ id: 'ash7', bikeId: 'EV005', bikerId: 'u6', bikerName: 'Mizanur Rahman', assignedAt: '2023-11-15 14:00:00', assignedBy: 'admin1', unassignedAt: '2024-02-20 10:30:00', unassignedBy: 'admin1', reason: 'Biker requested return', status: 'completed', notes: 'Returned in good condition' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV006', model: 'Hero Photon', brand: 'Hero', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-1111', status: 'rented', batteryLevel: 88, location: 'Mirpur', investorId: 'inv2', purchasePrice: 55000, purchaseDate: '2024-02-01', currentRent: 300, totalEarnings: 4500,
|
||||
assignmentHistory: [
|
||||
{ id: 'ash8', bikeId: 'EV006', bikerId: 'u7', bikerName: 'Alamin Hossain', assignedAt: '2024-02-05 11:00:00', assignedBy: 'admin1', status: 'active' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV007', model: 'Okinawa Praise', brand: 'Okinawa', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-2222', status: 'available', batteryLevel: 92, location: 'Gulshan 2', investorId: 'inv3', purchasePrice: 75000, purchaseDate: '2024-02-10', currentRent: 0, totalEarnings: 9500,
|
||||
assignmentHistory: []
|
||||
},
|
||||
{
|
||||
id: 'EV008', model: 'Bajaj Chetak', brand: 'Bajaj', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-3333', status: 'available', batteryLevel: 100, location: 'Dhanmondi', purchasePrice: 70000, purchaseDate: '2024-03-01', currentRent: 0, totalEarnings: 0,
|
||||
assignmentHistory: []
|
||||
},
|
||||
{
|
||||
id: 'EV009', model: 'TVS iQube', brand: 'TVS', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-4444', status: 'rented', batteryLevel: 75, location: 'Banani', purchasePrice: 75000, purchaseDate: '2024-03-05', currentRent: 380, totalEarnings: 1140,
|
||||
assignmentHistory: [
|
||||
{ id: 'ash9', bikeId: 'EV009', bikerId: 'u8', bikerName: 'Babul Akter', assignedAt: '2024-03-08 09:30:00', assignedBy: 'admin1', status: 'active' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EV010', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5555', status: 'available', batteryLevel: 90, location: 'Uttara', purchasePrice: 68000, purchaseDate: '2024-03-10', currentRent: 0, totalEarnings: 0,
|
||||
assignmentHistory: []
|
||||
},
|
||||
];
|
||||
|
||||
export const rentals: Rental[] = [
|
||||
|
||||
Reference in New Issue
Block a user