feat: implement automated double-entry accounting for investments and add transaction details modal

This commit is contained in:
sazzadulalambd
2026-04-26 14:56:12 +06:00
parent ae94ce0427
commit 7457b997ef
16 changed files with 8809 additions and 201 deletions

247
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

File diff suppressed because it is too large Load Diff

View File

@@ -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">

View File

@@ -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}
{(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>
{biker.rating > 0 && (
<p className="text-xs text-amber-600 mt-1 flex items-center gap-1"> {biker.rating} ({biker.totalRatings})</p>
)}
</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

View File

@@ -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>
{(() => {
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>
);
}

View 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

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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 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>
);
}

View 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>
);
}

View File

@@ -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 },

View File

@@ -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[] = [