Compare commits

...

69 Commits

Author SHA1 Message Date
sazzadulalambd
3603f2191c feat: expand RBAC system with granular permissions and update role definitions and UI configuration 2026-05-21 20:22:32 +06:00
sazzadulalambd
c85b609797 feat: add employee management module with roster, search, and filtering to hub details page 2026-05-21 20:16:42 +06:00
sazzadulalambd
916eec0f72 feat: implement automated battery rental invoicing and journal entry tracking with print support 2026-05-21 12:01:23 +06:00
sazzadulalambd
b83325b8e3 refactor: overhaul project structure, update configuration, and improve consistency across admin and investor dashboard components. 2026-05-21 11:40:03 +06:00
sazzadulalambd
7332f85512 refactor: clean up catch block formatting and comment out version display in Sidebar 2026-05-20 14:58:38 +06:00
sazzadulalambd
989221f953 feat: add service center management module with CRUD functionality and status filtering 2026-05-20 14:48:29 +06:00
sazzadulalambd
8669da78d6 refactor: adjust UI state management and OCR locking logic in maintenance page 2026-05-20 14:10:18 +06:00
sazzadulalambd
bb561e493b feat: add AI OCR processing state and conditional UI locking for maintenance records 2026-05-20 13:26:25 +06:00
sazzadulalambd
3933141140 feat: add withdraw category to notifications and improve UI sidebar layout 2026-05-19 21:02:57 +06:00
sazzadulalambd
bcb319ca71 refactor: rename FOCO to FICO model and enhance notification system with new templates, metadata, and category filtering. 2026-05-19 20:53:50 +06:00
sazzadulalambd
9442e64a86 update 2026-05-19 20:45:10 +06:00
sazzadulalambd
233327e488 fix: update notification titles and messages to reflect damage alerts for specific bikes 2026-05-19 20:29:10 +06:00
sazzadulalambd
08958a8722 refactor: update bike detail links to point to admin fleet route instead of public bike view 2026-05-19 20:27:26 +06:00
sazzadulalambd
16c299ae7f feat: add battery and bike maintenance history pages and update navigation links to include source tracking 2026-05-19 20:25:01 +06:00
sazzadulalambd
9126d3dfa2 feat: implement advanced rental transaction history table with filtering, sorting, and pagination in fleet and battery views 2026-05-19 20:01:36 +06:00
sazzadulalambd
8ae1c8316b feat: implement navigation to rental history page for individual assets and create associated detail view route 2026-05-19 19:35:27 +06:00
sazzadulalambd
123ba98c9e feat: add asset management modals for assigning bikes and batteries and confirming unassignments 2026-05-19 19:27:03 +06:00
sazzadulalambd
b1dd4b0683 feat: add animated bike icon to assign bike modal header and fix indentation formatting 2026-05-19 19:10:12 +06:00
sazzadulalambd
7ced7f8ed4 refactor: remove register bike and battery assignment buttons from investor detail page 2026-05-19 19:02:15 +06:00
sazzadulalambd
646068dbe3 refactor: dynamic display of asset details and investment data based on investment type 2026-05-19 18:55:50 +06:00
sazzadulalambd
f0d92f31ff feat: implement dynamic payment tracking with local storage and update transaction UI for battery investments 2026-05-19 18:22:06 +06:00
sazzadulalambd
cd6d6e4386 feat: implement persistent local storage state for batteries, bikes, and investors with dynamic patching logic 2026-05-19 18:11:35 +06:00
sazzadulalambd
2a891df398 feat: add active batteries stat to investor profile and update grid layout for payment selection 2026-05-19 17:33:09 +06:00
sazzadulalambd
be137d65df feat: add battery management functionality with assignment, registration, and investment tracking to investor dashboard 2026-05-19 17:25:32 +06:00
sazzadulalambd
623500d845 feat: implement battery asset management and assignment functionality for investors 2026-05-19 16:46:39 +06:00
sazzadulalambd
04423603c0 refactor: replace detail button with Link for battery navigation and static label 2026-05-19 16:37:08 +06:00
sazzadulalambd
2645aeca30 feat: add investor co-ownership tracking and management fields to battery details view 2026-05-19 16:35:11 +06:00
sazzadulalambd
c0ae111c8c feat: replace monthly rent with daily rent and deposit fields in battery schema and UI 2026-05-19 16:33:08 +06:00
sazzadulalambd
c6a9fd960e refactor: rename investment plans to EV investment plans across UI components 2026-05-19 16:20:31 +06:00
sazzadulalambd
5d1a5454c0 feat: implement battery investment settings tab and configuration component 2026-05-19 16:17:20 +06:00
sazzadulalambd
3297daf124 refactor: simplify payment workflow by removing amount input and update sidebar profile navigation 2026-05-19 16:00:27 +06:00
sazzadulalambd
3edcfbc654 feat: comment out new investment entry points in dashboard and plans pages 2026-05-19 15:12:27 +06:00
sazzadulalambd
5aded5bdc6 feat: implement multi-role dashboard state with hub management, ticketing, and accounting features 2026-05-17 23:50:11 +06:00
sazzadulalambd
f8a745ad42 feat: enhance admin dashboard with real-time telematics, audit logs, and interactive management tools 2026-05-17 23:45:38 +06:00
sazzadulalambd
6870ca6b0f feat: add real-time notification count to sidebar and implement dedicated admin notification management dashboard 2026-05-17 23:26:10 +06:00
sazzadulalambd
aaf91255bb feat: add hub selection to battery form and update data schema 2026-05-17 20:35:10 +06:00
sazzadulalambd
9370b71b25 feat: integrate battery selection and sync battery level from mock data in fleet management form 2026-05-17 20:32:54 +06:00
sazzadulalambd
a4ff86b953 feat: add battery management tab with support for viewing, adding, editing, and deleting batteries in hub dashboard 2026-05-17 20:24:47 +06:00
sazzadulalambd
89300a457e feat: add withdrawal management tab and request processing functionality to accounting page 2026-05-17 20:13:33 +06:00
sazzadulalambd
8f445857a9 feat: enhance maintenance details page with issue history navigation and responsive layout improvements 2026-05-17 19:06:24 +06:00
sazzadulalambd
440a87f0b5 feat: add category-based filtering and enhanced dashboard stats for damage and maintenance records 2026-05-17 15:19:31 +06:00
sazzadulalambd
fb1eff4931 feat: implement maintenance invoice creation flow with dynamic pricing, quantity editing, and PDF breakdown reporting 2026-05-17 00:45:04 +06:00
sazzadulalambd
0274e9a90b feat: enhance maintenance records with structured part management, service cost tracking, and dynamic invoice calculation 2026-05-17 00:23:08 +06:00
sazzadulalambd
48fd93fea8 feat: implement real-time rental map dashboard and integrate location tracking updates into admin modules 2026-05-16 22:34:44 +06:00
sazzadulalambd
1feab1fa23 feat: add live rental tracking map view with GPS coordinates and real-time refresh 2026-05-16 21:47:01 +06:00
sazzadulalambd
4c25990e70 feat: add support for battery swapping status and bike image gallery management in fleet details 2026-05-16 20:54:17 +06:00
sazzadulalambd
de9499b567 feat: enhance bike overview with detailed battery tracking and rental subscription data 2026-05-16 20:44:19 +06:00
sazzadulalambd
ec487f6d27 feat: add hub tracking to damage and maintenance records with selection UI 2026-05-16 20:19:23 +06:00
sazzadulalambd
36b12772b7 feat: include hub information in battery damage and maintenance records 2026-05-16 20:00:28 +06:00
sazzadulalambd
4b1ff96db2 feat: implement PWA install prompt banner for investor dashboard 2026-05-16 19:46:43 +06:00
sazzadulalambd
bd18c265ca feat: add damage and maintenance history tracking with CRUD functionality to battery detail page 2026-05-16 19:33:28 +06:00
sazzadulalambd
ce4bbfaf81 fix: include batteryRentPending in rental interface and sanitize pending rent calculation 2026-05-16 15:28:33 +06:00
sazzadulalambd
adbcded611 feat: add battery rent support and integrate adjusted battery costs into rental billing calculations 2026-05-16 15:09:19 +06:00
sazzadulalambd
21c408f828 feat: integrate battery selection and rental billing calculations into rental details view 2026-05-16 15:09:12 +06:00
sazzadulalambd
1882cfbb91 refactor: remove battery manual entry fields in biker profile and add battery rental history table to rental details 2026-05-16 14:06:33 +06:00
sazzadulalambd
41530a4691 feat: add manual BMS data refresh functionality and expand battery purchase form with accounting fields 2026-05-16 12:36:00 +06:00
sazzadulalambd
e932e6b817 refactor: remove RefreshCw icon and unused BMS refresh functionality from battery detail page 2026-05-16 12:20:18 +06:00
sazzadulalambd
b39f3981fc feat: implement battery editing functionality with modal and state management 2026-05-16 12:19:54 +06:00
sazzadulalambd
f5cd411a05 feat: implement battery management module with list and detail views 2026-05-16 12:10:49 +06:00
sazzadulalambd
62b8d567bd refactor: improve responsive layout and add payment history section to investment details page 2026-05-16 10:45:06 +06:00
sazzadulalambd
d8e82cef19 feat: add payment tracking and manual payment submission to investment details and configure standalone deployment mode 2026-05-16 10:20:12 +06:00
sazzadulalambd
5e59909e8e feat: add payment modal and due amount functionality to investment details page 2026-05-15 22:37:42 +06:00
sazzadulalambd
78a51ca60a feat: replace ROI metric with total bike count and add item counts to investment detail tabs 2026-05-15 19:18:07 +06:00
sazzadulalambd
18b29d535b feat: update investment details page with mock data, navigation enhancements, and refined layout 2026-05-15 18:49:26 +06:00
sazzadulalambd
e25bfa91a5 feat: add notifications page and update navigation to redirect notifications to a dedicated route 2026-05-15 18:25:29 +06:00
sazzadulalambd
ad6d5e26ad feat: implement sidebar notification drawer and update investor page padding layouts 2026-05-15 18:07:15 +06:00
sazzadulalambd
845ae91d64 feat: add InvestorNotification component and integrate it across investor dashboard pages 2026-05-15 17:56:16 +06:00
sazzadulalambd
bb1d4628ee feat: implement radio-based payment selection UI and rename paymentOption to paymentType 2026-05-15 12:25:24 +06:00
sazzadulalambd
cab01a25ac feat: add partial payment support for investments and update mock data fields 2026-05-15 03:10:37 +06:00
50 changed files with 21440 additions and 5479 deletions

3
.gitignore vendored
View File

@@ -50,3 +50,6 @@ next-env.d.ts
**/docs **/docs
**/.docs **/.docs
**/deploy.zip
**/deploy

1
JML Submodule

Submodule JML added at 7332f85512

20
deploy.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Build the project
echo "Building project..."
npm run build
# Create a deployment folder
echo "Preparing deployment files..."
mkdir -p deploy
cp -r .next deploy/
cp -r public deploy/
cp server.js deploy/
cp package.json deploy/
cp next.config.ts deploy/
# Optional: Zip the files
echo "Zipping deployment files..."
cd deploy && zip -r ../deploy.zip . && cd ..
echo "Done! Upload 'deploy.zip' to your cPanel directory and follow the guide."

View File

@@ -8,6 +8,7 @@ const withPWA = withPWAInit({
} as any); } as any);
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone',
images: { images: {
remotePatterns: [ remotePatterns: [
{ {

19
server.js Normal file
View File

@@ -0,0 +1,19 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const port = process.env.PORT || 3000
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
handle(req, res, parsedUrl)
}).listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})

View File

@@ -6,7 +6,8 @@ import {
DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building, DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building,
ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List, ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List,
Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet, Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet,
Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike,
Clock, Check, CheckCircle
} from 'lucide-react'; } from 'lucide-react';
export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense'; export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense';
@@ -71,6 +72,20 @@ export interface AccountingTransaction {
createdBy: string; createdBy: string;
} }
export interface WithdrawRequest {
id: string;
investorId: string;
investorName: string;
phone: string;
amount: number;
requestDate: string;
status: 'pending' | 'approved' | 'completed' | 'rejected';
bankName: string;
accountNo: string;
processedDate?: string;
paymentMethod?: string;
}
const defaultAccounts: ChartOfAccount[] = [ const defaultAccounts: ChartOfAccount[] = [
{ id: 'ASSET-001', code: '1000', name: 'Assets', type: 'asset', isActive: true, balance: 0 }, { id: 'ASSET-001', code: '1000', name: 'Assets', type: 'asset', isActive: true, balance: 0 },
{ id: 'ASSET-101', code: '1100', name: 'Cash in Hand', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 85000 }, { id: 'ASSET-101', code: '1100', name: 'Cash in Hand', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 85000 },
@@ -223,7 +238,7 @@ function generateAutoJournalEntries(type: TransactionType, amount: number, descr
} }
export default function AccountingPage() { export default function AccountingPage() {
const [activeTab, setActiveTab] = useState<'dashboard' | 'transactions' | 'journal' | 'ledger' | 'accounts'>('dashboard'); const [activeTab, setActiveTab] = useState<'dashboard' | 'transactions' | 'journal' | 'ledger' | 'accounts' | 'withdraw'>('dashboard');
const [transactions, setTransactions] = useState(mockTransactions); const [transactions, setTransactions] = useState(mockTransactions);
const [accounts] = useState(defaultAccounts); const [accounts] = useState(defaultAccounts);
const [journalEntries, setJournalEntries] = useState(mockJournalEntries); const [journalEntries, setJournalEntries] = useState(mockJournalEntries);
@@ -234,6 +249,14 @@ export default function AccountingPage() {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [editingTransaction, setEditingTransaction] = useState<AccountingTransaction | null>(null); const [editingTransaction, setEditingTransaction] = useState<AccountingTransaction | null>(null);
const [viewingTransaction, setViewingTransaction] = useState<AccountingTransaction | null>(null); const [viewingTransaction, setViewingTransaction] = useState<AccountingTransaction | null>(null);
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [payNowModal, setPayNowModal] = useState<WithdrawRequest | null>(null);
const [paymentForm, setPaymentForm] = useState({ method: 'bank', reference: '', notes: '', date: new Date().toISOString().split('T')[0] });
const [withdrawRequests, setWithdrawRequests] = useState<WithdrawRequest[]>([
{ id: 'WDR-001', investorId: 'INV-001', investorName: 'Mohammad Islam', phone: '01987654321', amount: 15000, requestDate: '2024-03-20', status: 'pending', bankName: 'City Bank', accountNo: '1234567890' },
{ id: 'WDR-002', investorId: 'INV-002', investorName: 'Rahima Begum', phone: '01876543210', amount: 25000, requestDate: '2024-03-18', status: 'approved', bankName: 'DBBL', accountNo: '9876543210', processedDate: '2024-03-19', paymentMethod: 'bank' },
{ id: 'WDR-003', investorId: 'INV-003', investorName: 'Ahmed Hassan', phone: '01765432109', amount: 8000, requestDate: '2024-03-15', status: 'completed', bankName: 'bKash', accountNo: '01765432109', processedDate: '2024-03-16', paymentMethod: 'mobile' },
]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10; const itemsPerPage = 10;
@@ -417,7 +440,8 @@ export default function AccountingPage() {
{ id: 'transactions', label: 'Transactions', icon: Receipt }, { id: 'transactions', label: 'Transactions', icon: Receipt },
{ id: 'journal', label: 'Journal', icon: BookOpen }, { id: 'journal', label: 'Journal', icon: BookOpen },
{ id: 'ledger', label: 'Ledger', icon: List }, { id: 'ledger', label: 'Ledger', icon: List },
{ id: 'accounts', label: 'Chart of Accounts', icon: Calculator }, { id: 'accounts', label: 'Accounts', icon: Calculator },
{ id: 'withdraw', label: 'Withdraw', icon: ArrowDownLeft },
]; ];
return ( return (
@@ -453,6 +477,11 @@ export default function AccountingPage() {
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
{tab.label} {tab.label}
{tab.id === 'withdraw' && withdrawRequests.filter(w => w.status === 'pending').length > 0 && (
<span className="ml-1 px-1.5 py-0.5 text-xs font-medium bg-orange-100 text-orange-700 rounded-full">
{withdrawRequests.filter(w => w.status === 'pending').length}
</span>
)}
</button> </button>
); );
})} })}
@@ -547,6 +576,289 @@ export default function AccountingPage() {
{activeTab === 'ledger' && <LedgerView accounts={accounts} journalEntries={journalEntries} dateFrom={dateFrom} dateTo={dateTo} />} {activeTab === 'ledger' && <LedgerView accounts={accounts} journalEntries={journalEntries} dateFrom={dateFrom} dateTo={dateTo} />}
{activeTab === 'accounts' && <AccountsView accounts={accounts} />} {activeTab === 'accounts' && <AccountsView accounts={accounts} />}
{activeTab === 'withdraw' && (
<div className="space-y-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-slate-800">Withdraw Management</h2>
<p className="text-sm text-slate-500">Process investor withdrawal requests</p>
</div>
<button
onClick={() => setShowWithdrawModal(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
<Plus className="w-5 h-5" />
<span>New Withdraw Request</span>
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Clock className="w-5 h-5 text-orange-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'pending').length}</p>
<p className="text-sm text-slate-500">Pending</p>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-xl p-4 border border-blue-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
<Check className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'approved').length}</p>
<p className="text-sm text-slate-500">Approved</p>
</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'completed').length}</p>
<p className="text-sm text-slate-500">Completed</p>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<TrendingDown className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.reduce((sum, w) => sum + w.amount, 0).toLocaleString()}</p>
<p className="text-sm text-slate-500">Total Amount</p>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th className="px-4 py-3 text-left font-semibold text-slate-600">ID</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Investor</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Phone</th>
<th className="px-4 py-3 text-right font-semibold text-slate-600">Amount</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Bank/Method</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Request Date</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Status</th>
<th className="px-4 py-3 text-center font-semibold text-slate-600">Actions</th>
</tr>
</thead>
<tbody>
{withdrawRequests.map((req) => (
<tr key={req.id} className="border-b border-slate-50 hover:bg-slate-50">
<td className="px-4 py-3 font-medium text-slate-800">{req.id}</td>
<td className="px-4 py-3">
<div>
<p className="font-medium text-slate-800">{req.investorName}</p>
<p className="text-xs text-slate-500">{req.investorId}</p>
</div>
</td>
<td className="px-4 py-3 text-slate-600">{req.phone}</td>
<td className="px-4 py-3 text-right font-bold text-slate-800">{req.amount.toLocaleString()}</td>
<td className="px-4 py-3">
<p className="text-slate-600">{req.bankName}</p>
<p className="text-xs text-slate-400">{req.accountNo}</p>
</td>
<td className="px-4 py-3 text-slate-600">{req.requestDate}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${
req.status === 'pending' ? 'bg-orange-100 text-orange-700' :
req.status === 'approved' ? 'bg-blue-100 text-blue-700' :
req.status === 'completed' ? 'bg-green-100 text-green-700' :
'bg-red-100 text-red-700'
}`}>
{req.status === 'pending' && <Clock className="w-3 h-3" />}
{req.status === 'approved' && <Check className="w-3 h-3" />}
{req.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{req.status}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-center gap-1">
{req.status === 'pending' && (
<>
<button
onClick={() => setWithdrawRequests(prev => prev.map(w => w.id === req.id ? { ...w, status: 'approved', processedDate: new Date().toISOString().split('T')[0] } : w))}
className="p-1.5 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200"
title="Approve"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={() => setWithdrawRequests(prev => prev.map(w => w.id === req.id ? { ...w, status: 'rejected' } : w))}
className="p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200"
title="Reject"
>
<X className="w-4 h-4" />
</button>
</>
)}
{req.status === 'approved' && (
<button
onClick={() => { setPayNowModal(req); setPaymentForm({ method: 'bank', reference: `PAY-${req.id}`, notes: '', date: new Date().toISOString().split('T')[0] }); }}
className="px-3 py-1.5 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 flex items-center gap-1"
>
<DollarSign className="w-3 h-3" /> Pay Now
</button>
)}
{req.status === 'completed' && (
<div className="flex items-center gap-1">
<span className="text-xs text-green-600 font-medium">Paid</span>
<button onClick={() => window.print()} className="p-1 text-slate-400 hover:text-blue-600" title="Print Invoice">
<Printer className="w-4 h-4" />
</button>
</div>
)}
{req.status === 'rejected' && (
<span className="text-xs text-red-600 font-medium">Rejected</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{showWithdrawModal && (
<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 justify-between items-center">
<h3 className="font-semibold text-slate-800">New Withdraw Request</h3>
<button onClick={() => setShowWithdrawModal(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 className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investor ID</label>
<input type="text" placeholder="INV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone</label>
<input type="tel" placeholder="01XXXXXXXXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investor Name</label>
<input type="text" placeholder="Enter name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Amount ()</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-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Name</label>
<input type="text" placeholder="Bank name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Number</label>
<input type="text" placeholder="Account number" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setShowWithdrawModal(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={() => setShowWithdrawModal(false)} className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">Submit Request</button>
</div>
</div>
</div>
)}
</div>
)}
{payNowModal && (
<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">Process Payment</h3>
<button onClick={() => setPayNowModal(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Paying to</p>
<p className="font-medium text-slate-800">{payNowModal.investorName}</p>
<p className="text-sm text-slate-600">{payNowModal.bankName} - {payNowModal.accountNo}</p>
</div>
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Amount</p>
<p className="text-xl font-bold text-blue-600">{payNowModal.amount.toLocaleString()}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Date</label>
<input type="date" value={paymentForm.date} onChange={(e) => setPaymentForm(p => ({ ...p, date: e.target.value }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select value={paymentForm.method} onChange={(e) => setPaymentForm(p => ({ ...p, method: e.target.value }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Reference No.</label>
<input type="text" value={paymentForm.reference} onChange={(e) => setPaymentForm(p => ({ ...p, reference: e.target.value }))} placeholder="e.g. TRX-123456" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Notes (Optional)</label>
<textarea value={paymentForm.notes} onChange={(e) => setPaymentForm(p => ({ ...p, notes: e.target.value }))} placeholder="Add any notes..." className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} />
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setPayNowModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={() => {
const newTransaction: AccountingTransaction = {
id: `TXN-${Date.now()}`,
date: paymentForm.date,
type: 'investor_withdraw',
amount: payNowModal.amount,
description: `Investor Withdrawal - ${payNowModal.investorName}`,
beneficiary: payNowModal.investorName,
beneficiaryPhone: payNowModal.phone,
paymentMethod: paymentForm.method as 'cash' | 'bank' | 'mobile',
reference: paymentForm.reference,
notes: paymentForm.notes,
createdAt: new Date().toISOString(),
createdBy: 'Admin'
};
setTransactions(prev => [newTransaction, ...prev]);
setWithdrawRequests(prev => prev.map(w => w.id === payNowModal.id ? { ...w, status: 'completed' as const, processedDate: paymentForm.date, paymentMethod: paymentForm.method } : w));
setPayNowModal(null);
}} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
<Banknote className="w-4 h-4" /> Complete Payment
</button>
</div>
</div>
</div>
)}
<TransactionModal <TransactionModal
isOpen={showModal} isOpen={showModal}
onClose={() => { setShowModal(false); setEditingTransaction(null); }} onClose={() => { setShowModal(false); setEditingTransaction(null); }}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -752,7 +752,7 @@ function SectionCard({ title, icon: Icon, children, headerBg = 'bg-slate-50', ed
{editKey && setEditingSection ? ( {editKey && setEditingSection ? (
editingSection !== editKey ? ( editingSection !== editKey ? (
<button onClick={() => { setEditingSection(editKey); onEdit?.(); }} className="p-1.5 hover:bg-white rounded-lg transition-colors"> <button onClick={() => { setEditingSection(editKey); onEdit?.(); }} className="p-1.5 hover:bg-white rounded-lg transition-colors">
<Edit className="w-4 h-4 text-slate-500" /> {/* <Edit className="w-4 h-4 text-slate-500" /> */}
</button> </button>
) : ( ) : (
<div className="flex gap-1"> <div className="flex gap-1">
@@ -862,7 +862,7 @@ export default function BikerDetailPage() {
<ArrowLeft className="w-4 h-4" /> Back to Bikers <ArrowLeft className="w-4 h-4" /> Back to Bikers
</button> </button>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-4"> <div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-8">
<div className="p-4 lg:p-6 flex flex-col lg:flex-row lg:items-center gap-4"> <div className="p-4 lg:p-6 flex flex-col lg:flex-row lg:items-center gap-4">
<div className="relative group"> <div className="relative group">
{biker.profileImage ? ( {biker.profileImage ? (
@@ -2040,7 +2040,6 @@ export default function BikerDetailPage() {
<> <>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<p className="text-xs font-semibold text-slate-500">Editing: {biker.bikes.batteries[editForm.editingIndex]?.name}</p> <p className="text-xs font-semibold text-slate-500">Editing: {biker.bikes.batteries[editForm.editingIndex]?.name}</p>
<button onClick={() => setEditForm({ ...editForm, editingIndex: undefined })} className="text-xs text-blue-600 hover:underline">+ Add New</button>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
@@ -2083,60 +2082,14 @@ export default function BikerDetailPage() {
</> </>
) : ( ) : (
<> <>
<div className="flex items-center justify-between mb-2"> <div className="p-3 bg-slate-50 rounded-lg border border-slate-200">
<p className="text-xs font-semibold text-slate-500">Add New Battery</p> <p className="text-sm text-slate-600">Click edit button on existing batteries to update them.</p>
{biker.bikes.batteries.length > 0 && (
<button onClick={() => { const bat = biker.bikes.batteries[0]; setEditForm({ batId: bat.id, batName: bat.name, batPercent: bat.percent, batStatus: bat.status, batLocation: bat.location, batSwappedAt: bat.swappedAt, batOdometer: bat.odometer, editingIndex: 0 }); }} className="text-xs text-blue-600 hover:underline">Edit existing</button>
)}
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID <span className="text-red-500">*</span></label>
<input type="text" value={editForm.batId || ''} onChange={(e) => setEditForm({ ...editForm, batId: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-xs" placeholder="BAT-DH-004" />
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery Name</label>
<input type="text" value={editForm.batName || ''} onChange={(e) => setEditForm({ ...editForm, batName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-xs" placeholder="Battery D" />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Charge %</label>
<input type="number" value={editForm.batPercent || ''} onChange={(e) => setEditForm({ ...editForm, batPercent: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-xs" placeholder="0-100" />
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Status</label>
<select value={editForm.batStatus || 'available'} onChange={(e) => setEditForm({ ...editForm, batStatus: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-xs bg-white">
<option value="active">Active</option>
<option value="available">Available</option>
<option value="charging">Charging</option>
</select>
</div>
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Location</label>
<input type="text" value={editForm.batLocation || ''} onChange={(e) => setEditForm({ ...editForm, batLocation: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-xs" placeholder="Swap Station" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Swapped At</label>
<input type="text" value={editForm.batSwappedAt || ''} onChange={(e) => setEditForm({ ...editForm, batSwappedAt: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-xs" placeholder="YYYY-MM-DD HH:MM" />
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Odometer (KM)</label>
<input type="number" value={editForm.batOdometer || ''} onChange={(e) => setEditForm({ ...editForm, batOdometer: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-xs" />
</div>
</div> </div>
</> </>
)} )}
</> </>
) : ( ) : (
<> <>
<div className="flex justify-end mb-2">
<button onClick={() => { setEditingSection('batteryHistory'); setEditForm({ editingIndex: undefined }); }} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-xs font-medium hover:bg-blue-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Battery
</button>
</div>
{biker.bikes.batteries.map((bat, idx) => ( {biker.bikes.batteries.map((bat, idx) => (
<div key={bat.id} className={`p-4 rounded-lg border ${bat.status === 'active' ? 'bg-green-50 border-green-200' : bat.status === 'charging' ? 'bg-amber-50 border-amber-200' : 'bg-slate-50 border-slate-200'}`}> <div key={bat.id} className={`p-4 rounded-lg border ${bat.status === 'active' ? 'bg-green-50 border-green-200' : bat.status === 'charging' ? 'bg-amber-50 border-amber-200' : 'bg-slate-50 border-slate-200'}`}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@@ -9,7 +9,8 @@ import {
GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet, GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet,
MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3, MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3,
History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus, History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus,
AlertCircle, Image as ImageIcon AlertCircle, Image as ImageIcon, Camera, Search, ArrowUpDown, ChevronLeft,
ChevronRight, RefreshCw, CheckCircle2, XCircle
} from 'lucide-react'; } from 'lucide-react';
interface GPSDevice { interface GPSDevice {
@@ -74,6 +75,8 @@ interface DamageRecord {
estimatedCost?: number; estimatedCost?: number;
actualCost?: number; actualCost?: number;
status: 'reported' | 'under_repair' | 'repaired' | 'claim_rejected'; status: 'reported' | 'under_repair' | 'repaired' | 'claim_rejected';
hubId?: string;
hubName?: string;
images?: string[]; images?: string[];
billImage?: string; billImage?: string;
resolvedAt?: string; resolvedAt?: string;
@@ -89,9 +92,26 @@ interface MaintenanceRecord {
cost: number; cost: number;
nextDueDate?: string; nextDueDate?: string;
status: 'scheduled' | 'in_progress' | 'completed'; status: 'scheduled' | 'in_progress' | 'completed';
hubId?: string;
hubName?: string;
notes?: string; notes?: string;
} }
interface BatteryHistory {
id: string;
batteryId: string;
brand: string;
model: string;
serialNumber: string;
assignedDate: string;
returnedDate?: string;
swappedToBatteryId?: string;
status: 'active' | 'returned' | 'swapped';
socStart: number;
socEnd?: number;
monthlyRent?: number;
}
interface Bike { interface Bike {
id: string; id: string;
model: string; model: string;
@@ -100,10 +120,19 @@ interface Bike {
plateNumber: string; plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired'; status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number; batteryLevel: number;
location?: string; // deprecated - use hubId/hubName currentBatteryId?: string;
currentBatteryBrand?: string;
currentBatteryModel?: string;
location?: string;
hubId?: string; hubId?: string;
hubName?: string; hubName?: string;
assignedTo?: string; assignedTo?: string;
renterPhone?: string;
renterNid?: string;
rentalStartDate?: string;
subscriptionType?: 'daily' | 'weekly' | 'monthly';
weeklyRent?: number;
monthlyRent?: number;
investorId?: string; investorId?: string;
investorName?: string; investorName?: string;
purchaseDate?: string; purchaseDate?: string;
@@ -124,11 +153,18 @@ interface Bike {
assignmentHistory?: BikeAssignment[]; assignmentHistory?: BikeAssignment[];
damageHistory?: DamageRecord[]; damageHistory?: DamageRecord[];
maintenanceHistory?: MaintenanceRecord[]; maintenanceHistory?: MaintenanceRecord[];
batteryHistory?: BatteryHistory[];
bikeImages?: {
front?: string;
back?: string;
left?: string;
right?: string;
};
} }
const mockBikes: Bike[] = [ const mockBikes: Bike[] = [
{ {
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15', id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, currentBatteryId: 'BAT-001', currentBatteryBrand: 'EVE Energy', currentBatteryModel: 'Li-Ion 60V50Ah', location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', renterPhone: '01712345678', renterNid: '1234567890', rentalStartDate: '2024-03-01', subscriptionType: 'weekly', weeklyRent: 2400, hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15',
gpsDevice: { id: 'GPS001', phone: '01712345601', imei: '861234567890123', lastActive: '2024-03-21 14:30', signal: 85, battery: 72 }, gpsDevice: { id: 'GPS001', phone: '01712345601', imei: '861234567890123', lastActive: '2024-03-21 14:30', signal: 85, battery: 72 },
documents: [ documents: [
{ type: 'registration', number: 'REG-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true }, { type: 'registration', number: 'REG-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true },
@@ -149,14 +185,36 @@ const mockBikes: Bike[] = [
{ id: 'A005', action: 'Insurance Renewed', details: 'Insurance renewed for 1 year', date: '2024-01-15', by: 'Admin' }, { id: 'A005', action: 'Insurance Renewed', details: 'Insurance renewed for 1 year', date: '2024-01-15', by: 'Admin' },
], ],
damageHistory: [ damageHistory: [
{ id: 'DMG001', date: '2024-02-10', type: 'accident', description: 'Minor collision at Mirpur intersection', reportedBy: 'Jamal Khan', reportedAt: '2024-02-10 14:30', estimatedCost: 5000, actualCost: 4500, status: 'repaired', resolvedAt: '2024-02-15' }, { id: 'DMG001', date: '2024-02-10', type: 'accident', description: 'Minor collision at Mirpur intersection', reportedBy: 'Jamal Khan', reportedAt: '2024-02-10 14:30', estimatedCost: 5000, actualCost: 4500, status: 'repaired', resolvedAt: '2024-02-15', hubId: 'HUB-001', hubName: 'Gulshan Hub' },
{ id: 'DMG002', date: '2024-03-15', type: 'wear_tear', description: 'Front tire wear - replaced', reportedBy: 'Rahim Ahmed', reportedAt: '2024-03-15 09:00', estimatedCost: 2500, actualCost: 2200, status: 'repaired', resolvedAt: '2024-03-16' }, { id: 'DMG002', date: '2024-03-15', type: 'wear_tear', description: 'Front tire wear - replaced', reportedBy: 'Rahim Ahmed', reportedAt: '2024-03-15 09:00', estimatedCost: 2500, actualCost: 2200, status: 'repaired', resolvedAt: '2024-03-16', hubId: 'HUB-002', hubName: 'Banani Hub' },
], ],
maintenanceHistory: [ maintenanceHistory: [
{ id: 'MNT001', date: '2024-03-01', type: 'routine', description: 'Full service - oil change, brake check, tire rotation', performedBy: 'Service Center', cost: 1500, nextDueDate: '2024-04-01', status: 'completed' }, { id: 'MNT001', date: '2024-03-01', type: 'routine', description: 'Full service - oil change, brake check, tire rotation', performedBy: 'Service Center', cost: 1500, nextDueDate: '2024-04-01', status: 'completed', hubId: 'HUB-001', hubName: 'Gulshan Hub' },
{ id: 'MNT002', date: '2024-02-15', type: 'battery', description: 'Battery health check and terminal cleaning', performedBy: 'Service Center', cost: 500, nextDueDate: '2024-05-15', status: 'completed' }, { id: 'MNT002', date: '2024-02-15', type: 'battery', description: 'Battery health check and terminal cleaning', performedBy: 'Service Center', cost: 500, nextDueDate: '2024-05-15', status: 'completed', hubId: 'HUB-003', hubName: 'Uttara Hub' },
{ id: 'MNT003', date: '2024-01-20', type: 'tire', description: 'Tire pressure check and inflation', performedBy: 'Service Center', cost: 300, nextDueDate: '2024-04-20', status: 'completed' }, { id: 'MNT003', date: '2024-01-20', type: 'tire', description: 'Tire pressure check and inflation', performedBy: 'Service Center', cost: 300, nextDueDate: '2024-04-20', status: 'completed', hubId: 'HUB-004', hubName: 'Mirpur Hub' },
] ],
batteryHistory: [
{ id: 'BH001', batteryId: 'BAT-001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2024-00001', assignedDate: '2024-03-15', status: 'active', socStart: 85, monthlyRent: 1500 },
{ id: 'BH002', batteryId: 'BAT-005', brand: 'Samsung SDI', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2023-00045', assignedDate: '2024-01-10', returnedDate: '2024-03-14', status: 'returned', socStart: 80, socEnd: 45, monthlyRent: 1200 },
{ id: 'BH003', batteryId: 'BAT-008', brand: 'LG Chem', model: 'Li-Ion 48V40Ah', serialNumber: 'SN-2023-00012', assignedDate: '2023-11-05', returnedDate: '2024-01-09', status: 'swapped', socStart: 90, socEnd: 60, monthlyRent: 1000 },
{ id: 'BH004', batteryId: 'BAT-003', brand: 'Panasonic', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2023-00078', assignedDate: '2023-08-20', returnedDate: '2023-11-04', status: 'swapped', socStart: 88, socEnd: 55, monthlyRent: 1500 },
{ id: 'BH005', batteryId: 'BAT-002', brand: 'Sony', model: 'Li-Ion 48V35Ah', serialNumber: 'SN-2023-00034', assignedDate: '2023-05-15', returnedDate: '2023-08-19', status: 'returned', socStart: 75, socEnd: 40, monthlyRent: 900 },
{ id: 'BH006', batteryId: 'BAT-012', brand: 'BYD', model: 'LiFePO4 60V40Ah', serialNumber: 'SN-2023-00056', assignedDate: '2024-04-20', returnedDate: '2024-06-15', status: 'returned', socStart: 92, socEnd: 35, monthlyRent: 1300 },
{ id: 'BH007', batteryId: 'BAT-015', brand: 'CATL', model: 'Li-Ion 72V50Ah', serialNumber: 'SN-2024-00089', assignedDate: '2024-06-20', status: 'active', socStart: 88, monthlyRent: 1800 },
{ id: 'BH008', batteryId: 'BAT-009', brand: 'Tongsheng', model: 'Li-Ion 48V45Ah', serialNumber: 'SN-2023-00023', assignedDate: '2023-02-10', returnedDate: '2023-05-14', status: 'returned', socStart: 82, socEnd: 50, monthlyRent: 1100 },
{ id: 'BH009', batteryId: 'BAT-011', brand: 'Binek', model: 'Li-Ion 60V48Ah', serialNumber: 'SN-2023-00067', assignedDate: '2024-02-01', returnedDate: '2024-04-18', status: 'returned', socStart: 78, socEnd: 42, monthlyRent: 1400 },
{ id: 'BH010', batteryId: 'BAT-007', brand: 'Kexin', model: 'Li-Ion 48V36Ah', serialNumber: 'SN-2022-00045', assignedDate: '2022-12-05', returnedDate: '2023-02-08', status: 'returned', socStart: 85, socEnd: 55, monthlyRent: 850 },
{ id: 'BH011', batteryId: 'BAT-004', brand: 'Faraday', model: 'LiFePO4 48V42Ah', serialNumber: 'SN-2022-00089', assignedDate: '2022-09-15', returnedDate: '2022-12-04', status: 'returned', socStart: 90, socEnd: 48, monthlyRent: 1200 },
{ id: 'BH012', batteryId: 'BAT-006', brand: 'Reliance', model: 'Lead Acid 48V32Ah', serialNumber: 'SN-2022-00034', assignedDate: '2022-06-20', returnedDate: '2022-09-14', status: 'returned', socStart: 95, socEnd: 30, monthlyRent: 600 },
{ id: 'BH013', batteryId: 'BAT-020', brand: 'Maxell', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2024-00123', assignedDate: '2024-07-10', swappedToBatteryId: 'BAT-025', status: 'swapped', socStart: 75, socEnd: 65, monthlyRent: 1400 },
{ id: 'BH014', batteryId: 'BAT-018', brand: 'Nikola', model: 'LiFePO4 48V40Ah', serialNumber: 'SN-2023-00078', assignedDate: '2024-05-05', swappedToBatteryId: 'BAT-022', status: 'swapped', socStart: 82, socEnd: 55, monthlyRent: 1150 },
],
bikeImages: {
front: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
back: 'https://images.unsplash.com/photo-1591637333184-19aa84de3fbd?w=400&h=300&fit=crop',
left: 'https://images.unsplash.com/photo-1622185135505-2d795043906a?w=400&h=300&fit=crop',
right: 'https://images.unsplash.com/photo-1609630875171-b1321377ee53?w=400&h=300&fit=crop',
}
}, },
{ {
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01', id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01',
@@ -258,6 +316,13 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
{ value: 'other', label: 'Other' }, { value: 'other', label: 'Other' },
]; ];
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 handleAddDamage = (damage: DamageRecord) => { const handleAddDamage = (damage: DamageRecord) => {
setBikes(bikes.map(b => { setBikes(bikes.map(b => {
if (b.id === bike.id) { if (b.id === bike.id) {
@@ -415,6 +480,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<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">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Hub</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Reported By</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Reported By</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Est. Cost</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Est. Cost</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actual Cost</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actual Cost</th>
@@ -430,14 +496,15 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<span className="text-sm text-slate-700 capitalize">{damage.type.replace('_', ' ')}</span> <span className="text-sm text-slate-700 capitalize">{damage.type.replace('_', ' ')}</span>
</td> </td>
<td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{damage.description}</td> <td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{damage.description}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.hubName || '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.reportedBy}</td> <td className="px-4 py-3 text-sm text-slate-600">{damage.reportedBy}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.estimatedCost || 0}</td> <td className="px-4 py-3 text-sm text-slate-600">{damage.estimatedCost || 0}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{damage.actualCost || '-'}</td> <td className="px-4 py-3 text-sm font-medium text-slate-700">{damage.actualCost || '-'}</td>
<td className="px-4 py-3"> <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 ${damage.status === 'repaired' ? 'bg-green-100 text-green-700' : <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${damage.status === 'repaired' ? 'bg-green-100 text-green-700' :
damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' : damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' :
damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' : damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' :
'bg-slate-100 text-slate-700' 'bg-slate-100 text-slate-700'
}`}> }`}>
{damage.status.replace('_', ' ')} {damage.status.replace('_', ' ')}
</span> </span>
@@ -498,6 +565,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<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">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Hub</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Performed By</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Performed By</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Cost</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Cost</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Next Due</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Next Due</th>
@@ -513,13 +581,14 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<span className="text-sm text-slate-700 capitalize">{maintenance.type}</span> <span className="text-sm text-slate-700 capitalize">{maintenance.type}</span>
</td> </td>
<td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{maintenance.description}</td> <td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{maintenance.description}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.hubName || '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.performedBy}</td> <td className="px-4 py-3 text-sm text-slate-600">{maintenance.performedBy}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{maintenance.cost}</td> <td className="px-4 py-3 text-sm font-medium text-slate-700">{maintenance.cost}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td> <td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td>
<td className="px-4 py-3"> <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 ${maintenance.status === 'completed' ? 'bg-green-100 text-green-700' : <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${maintenance.status === 'completed' ? 'bg-green-100 text-green-700' :
maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' : maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-700' 'bg-slate-100 text-slate-700'
}`}> }`}>
{maintenance.status.replace('_', ' ')} {maintenance.status.replace('_', ' ')}
</span> </span>
@@ -591,6 +660,15 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
} }
function OverviewTab({ bike }: { bike: Bike }) { function OverviewTab({ bike }: { bike: Bike }) {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;
const batteryHistory = bike.batteryHistory || [];
const totalPages = Math.ceil(batteryHistory.length / itemsPerPage);
const paginatedHistory = batteryHistory.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
@@ -614,20 +692,38 @@ function OverviewTab({ bike }: { bike: Bike }) {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Plate Number</p> <p className="text-xs text-slate-500">Bike</p>
<p className="font-semibold text-slate-700">{bike.plateNumber}</p> <p className="font-semibold text-slate-700">{bike.brand} {bike.model}</p>
<p className="text-xs text-slate-500 mt-1">{bike.plateNumber}</p>
</div> </div>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Battery</p> <p className="text-xs text-slate-500">Battery</p>
<p className={`font-semibold ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p> <p className={`font-semibold text-lg ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
{bike.currentBatteryId && (
<p className="text-xs font-medium text-slate-700 mt-1">{bike.currentBatteryId}</p>
)}
{bike.currentBatteryBrand && (
<p className="text-xs text-slate-500">{bike.currentBatteryBrand} {bike.currentBatteryModel}</p>
)}
</div> </div>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Current Renter</p> <p className="text-xs text-slate-500">Current Renter</p>
<p className="font-semibold text-slate-700">{bike.assignedTo || 'Available'}</p> <p className="font-semibold text-slate-700">{bike.assignedTo || 'Available'}</p>
{bike.renterPhone && <p className="text-xs text-slate-500 mt-1">{bike.renterPhone}</p>}
{bike.rentalStartDate && <p className="text-xs text-slate-400 mt-1">Since: {bike.rentalStartDate}</p>}
</div> </div>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Daily Rate</p> <p className="text-xs text-slate-500">Subscription</p>
<p className="font-semibold text-green-600">{bike.currentRent || 0}</p> <p className="font-semibold text-slate-700 capitalize">{bike.subscriptionType || 'Daily'}</p>
{bike.subscriptionType === 'weekly' && bike.weeklyRent && (
<p className="text-xs text-green-600 mt-1">{bike.weeklyRent}/week</p>
)}
{bike.subscriptionType === 'monthly' && bike.monthlyRent && (
<p className="text-xs text-green-600 mt-1">{bike.monthlyRent}/month</p>
)}
{bike.subscriptionType === 'daily' && (
<p className="text-xs text-green-600 mt-1">{bike.currentRent || 0}/day</p>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -681,6 +777,91 @@ function OverviewTab({ bike }: { bike: Bike }) {
</div> </div>
)} )}
</div> </div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Battery History</h3>
{batteryHistory.length > 0 ? (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery ID</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Brand/Model</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned Date</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned/Swapped</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Start SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">End SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedHistory.map(bh => (
<tr key={bh.id} className="hover:bg-slate-50">
<td className="px-3 py-2 text-sm text-slate-700 font-medium">{bh.batteryId}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.brand} {bh.model}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.assignedDate}</td>
<td className="px-3 py-2 text-sm text-slate-600">
{bh.status === 'swapped' ? (
<>
<span className="text-blue-600 block">Swapped to {bh.swappedToBatteryId}</span>
<span className="text-blue-600 text-[10px] block opacity-75">{bh.returnedDate}</span>
</>
) : bh.returnedDate || '-'}
</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socStart}%</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socEnd ? `${bh.socEnd}%` : '-'}</td>
<td className="px-3 py-2 text-sm text-green-600">{bh.monthlyRent}</td>
<td className="px-3 py-2">
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bh.status === 'active' ? 'bg-green-100 text-green-700' : bh.status === 'swapped' ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'}`}>
{bh.status === 'active' ? 'Active' : bh.status === 'swapped' ? 'Swapped' : 'Returned'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-100">
<p className="text-sm text-slate-500">
Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, batteryHistory.length)} of {batteryHistory.length} batteries
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-1 text-sm rounded-lg ${currentPage === page ? 'bg-accent text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</>
) : (
<div className="text-center py-8 text-slate-500">
No battery history found
</div>
)}
</div>
</div> </div>
); );
} }
@@ -768,9 +949,50 @@ function GPSTab({ bike }: { bike: Bike }) {
function DocumentsTab({ bike }: { bike: Bike }) { function DocumentsTab({ bike }: { bike: Bike }) {
const docs = bike.documents || []; const docs = bike.documents || [];
const [images, setImages] = useState(bike.bikeImages || { front: '', back: '', left: '', right: '' });
const [uploading, setUploading] = useState<string | null>(null);
const handleImageUpload = (view: 'front' | 'back' | 'left' | 'right', e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploading(view);
const reader = new FileReader();
reader.onloadend = () => {
setImages(prev => ({ ...prev, [view]: reader.result as string }));
setUploading(null);
};
reader.readAsDataURL(file);
}
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Images</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{(['front', 'back', 'left', 'right'] as const).map(view => (
<div key={view} className="space-y-2">
<p className="text-xs font-medium text-slate-500 capitalize">{view} View</p>
<div className="relative aspect-video bg-slate-100 rounded-lg overflow-hidden border border-slate-200">
{images[view] ? (
<img src={images[view]} alt={`${view} view`} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center">
<Camera className="w-8 h-8 text-slate-300" />
</div>
)}
<label className="absolute inset-0 cursor-pointer hover:bg-black/20 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<span className="bg-white px-3 py-1 rounded-full text-xs font-medium text-slate-700 shadow">
{uploading === view ? 'Uploading...' : 'Upload'}
</span>
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(view, e)} />
</label>
</div>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3> <h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3>
{docs.length === 0 ? ( {docs.length === 0 ? (
@@ -827,93 +1049,395 @@ function DocumentsTab({ bike }: { bike: Bike }) {
} }
function RentalTab({ bike }: { bike: Bike }) { function RentalTab({ bike }: { bike: Bike }) {
const history = bike.rentalHistory || []; // Generate highly realistic rent transaction history
const [transactions] = useState<any[]>(() => {
const list: any[] = [];
const riders = [bike.assignedTo || 'Karim Ahmed', 'Sajib Islam', 'Nayeem Chowdhury', 'Rakib Hasan', 'Kamal Hossain'];
const methods = ['bKash', 'Nagad', 'Rocket', 'Bank Transfer'];
const getRateDisplay = (type: string, rate: number) => { const days = 25;
switch (type) { const baseDate = new Date();
case 'single': return `৳${rate}/day`;
case 'shared': return `৳${rate / 2}+${rate / 2} (2 person)`; for (let i = 0; i < days; i++) {
case 'rent-to-own': return `৳${rate}/day`; const date = new Date();
default: return `৳${rate}`; date.setDate(baseDate.getDate() - i);
const dateString = date.toISOString().split('T')[0];
const riderIndex = (i) % riders.length;
const methodIndex = (i + 1) % methods.length;
// status distribution
let status: 'paid' | 'pending' | 'failed' = 'paid';
if (i === 1) status = 'pending';
else if (i === 5) status = 'failed';
const amount = bike.currentRent || 350;
list.push({
id: `TX-BK-${10200 + i}`,
date: dateString,
riderName: riders[riderIndex],
duration: '1 Day',
amount: amount,
status: status,
payoutMethod: methods[methodIndex]
});
} }
return list;
});
// Filter & Sorting State
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'amount' | 'rider'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 8;
// Handler functions
const handleSort = (field: 'date' | 'amount' | 'rider') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
}; };
// Filter Logic
const filteredTransactions = transactions.filter(tx => {
if (statusFilter !== 'all' && tx.status !== statusFilter) return false;
if (searchQuery && !tx.riderName.toLowerCase().includes(searchQuery.toLowerCase()) && !tx.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(tx.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(tx.date) > new Date(dateTo)) return false;
return true;
});
// Sort Logic
const sortedTransactions = [...filteredTransactions].sort((a, b) => {
let comparison = 0;
if (sortBy === 'date') {
comparison = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'amount') {
comparison = a.amount - b.amount;
} else if (sortBy === 'rider') {
comparison = a.riderName.localeCompare(b.riderName);
}
return sortOrder === 'desc' ? -comparison : comparison;
});
// Pagination
const totalPages = Math.ceil(sortedTransactions.length / pageSize);
const paginatedTransactions = sortedTransactions.slice((page - 1) * pageSize, page * pageSize);
const statusConfig: Record<string, { label: string; bg: string; color: string; icon: any }> = {
paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle2 },
pending: { label: 'Pending', bg: 'bg-amber-100', color: 'text-amber-700', icon: Clock },
failed: { label: 'Failed', bg: 'bg-red-100', color: 'text-red-700', icon: XCircle },
};
const totalCollected = filteredTransactions
.filter(t => t.status === 'paid')
.reduce((sum, t) => sum + t.amount, 0);
const pendingAmount = filteredTransactions
.filter(t => t.status === 'pending')
.reduce((sum, t) => sum + t.amount, 0);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100"> {/* Dynamic Rental Metrics - Sleek and Responsive */}
<h3 className="font-semibold text-slate-700 mb-3">Rental History</h3> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{history.length === 0 ? ( <div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-center py-8"> <div className="flex items-center gap-3">
<History className="w-12 h-12 text-slate-300 mx-auto mb-4" /> <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<p className="text-sm text-slate-500">No rental history yet.</p> <DollarSign className="w-5 h-5 text-green-600" />
</div>
) : (
<div className="space-y-3">
{history.map(rental => (
<div key={rental.id} className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-start justify-between mb-2">
<div>
<p className="font-medium text-slate-700">{rental.bikerName}</p>
<p className="text-xs text-slate-500">ID: {rental.id}</p>
</div>
<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 === 'completed' ? 'bg-blue-100 text-blue-700' :
'bg-red-100 text-red-700'
}`}>
{rental.status}
</span>
</div>
<div className="flex flex-wrap gap-3 text-xs">
<span className="bg-slate-100 px-2 py-1 rounded text-slate-600">
{rental.type === 'single' ? 'Single (350/day)' :
rental.type === 'shared' ? 'Shared (60/day)' :
'Rent-to-Own (450/day)'}
</span>
<span className="text-slate-500">
{rental.startDate} {rental.endDate && `to ${rental.endDate}`}
</span>
</div>
<div className="flex justify-between mt-2 pt-2 border-t border-slate-100">
<span className="text-xs text-slate-500">{rental.rideCount} rides</span>
<span className="text-sm font-semibold text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Rental Rates Info</h3>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
<span className="text-xs font-bold text-green-600">1</span>
</div>
<span className="font-medium text-slate-700">Single</span>
</div> </div>
<span className="font-semibold text-green-600">৳350/day</span> <div>
</div> <p className="text-xs text-slate-500">Total Collected</p>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"> <p className="text-lg font-bold text-green-600">৳{totalCollected.toLocaleString()}</p>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-xs font-bold text-blue-600">2</span>
</div>
<span className="font-medium text-slate-700">Shared (2 Person)</span>
</div> </div>
<span className="font-semibold text-green-600">৳60/day (৳30+৳30)</span>
</div>
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-xs font-bold text-purple-600">3</span>
</div>
<span className="font-medium text-slate-700">Rent-to-Own</span>
</div>
<span className="font-semibold text-green-600">৳450/day</span>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Zap className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Active Rate</p>
<p className="text-lg font-bold text-slate-800">৳{bike.currentRent || 350}/day</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending Amount</p>
<p className="text-lg font-bold text-amber-600">৳{pendingAmount.toLocaleString()}</p>
</div>
</div>
</div>
</div>
{/* Main Table Container */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Filters */}
<div className="p-4 border-b border-slate-100 bg-slate-50/20">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<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 rider or ref..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
/>
</div>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2.5 py-1.5 text-xs text-red-500 hover:bg-red-50 rounded font-semibold transition-colors"
>
Clear
</button>
)}
<button
onClick={() => {
setSearchQuery('');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setSortBy('date');
setSortOrder('desc');
setPage(1);
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
</div>
</div>
</div>
{/* Desktop Table View */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '' : ''}</span>}
</div>
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Transaction ID
</th>
<th
onClick={() => handleSort('rider')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
<div className="flex items-center gap-1">
Rider {sortBy === 'rider' && <span className="text-accent">{sortOrder === 'asc' ? '' : ''}</span>}
</div>
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Duration
</th>
<th
onClick={() => handleSort('amount')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
>
<div className="flex items-center gap-1 justify-end">
Amount {sortBy === 'amount' && <span className="text-accent">{sortOrder === 'asc' ? '' : ''}</span>}
</div>
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Method
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<tr key={tx.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 text-sm text-slate-800 font-medium">
{tx.date}
</td>
<td className="px-4 py-3 text-xs font-mono font-semibold text-slate-400">
{tx.id}
</td>
<td className="px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400" />
<span className="font-semibold text-slate-700">{tx.riderName}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-500 font-medium">
{tx.duration}
</td>
<td className="px-4 py-3 text-sm font-bold text-slate-800 text-right">
৳{tx.amount.toLocaleString()}
</td>
<td className="px-4 py-3 text-sm text-slate-600 font-medium capitalize">
{tx.payoutMethod}
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold ${status.bg} ${status.color}`}>
<StatusIcon className="w-3.5 h-3.5" />
{status.label}
</span>
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={7} className="px-4 py-12 text-center text-slate-400">
<AlertCircle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p className="text-sm font-semibold">No rental transactions found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Mobile View */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<div key={tx.id} className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex items-start justify-between gap-3 mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400 shrink-0" />
<p className="text-sm font-semibold text-slate-800 truncate">{tx.riderName}</p>
</div>
<p className="text-xs text-slate-400 ml-6">Ref: {tx.id}</p>
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">৳{tx.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold mt-1 ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div>
</div>
<div className="flex items-center justify-between ml-6 text-xs text-slate-400">
<span>{tx.date}</span>
<span className="capitalize">{tx.payoutMethod}</span>
</div>
</div>
);
})
) : (
<div className="p-8 text-center text-slate-500">
<AlertCircle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div>
)}
</div>
{/* Paginated Footer */}
{sortedTransactions.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedTransactions.length)} of {sortedTransactions.length} records
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronLeft className="w-4 h-4 text-slate-600" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${
page === pageNum
? 'bg-accent text-white'
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronRight className="w-4 h-4 text-slate-600" />
</button>
</div>
</div>
)}
</div> </div>
</div> </div>
); );
@@ -1305,8 +1829,17 @@ function DamageModal({ bike, damage, onClose, onSave }: { bike: Bike; damage: Da
estimatedCost: damage?.estimatedCost || 0, estimatedCost: damage?.estimatedCost || 0,
actualCost: damage?.actualCost || 0, actualCost: damage?.actualCost || 0,
status: damage?.status || 'reported', status: damage?.status || 'reported',
hubId: damage?.hubId || '',
hubName: damage?.hubName || '',
}); });
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 damageTypes = [ const damageTypes = [
{ value: 'accident', label: 'Accident' }, { value: 'accident', label: 'Accident' },
{ value: 'theft', label: 'Theft' }, { value: 'theft', label: 'Theft' },
@@ -1379,6 +1912,19 @@ function DamageModal({ bike, damage, onClose, onSave }: { bike: Bike; damage: Da
required required
/> />
</div> </div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Hub</label>
<select
value={formData.hubId}
onChange={(e) => setFormData({ ...formData, hubId: e.target.value, hubName: mockHubs.find(h => h.id === e.target.value)?.name || '' })}
className="w-full px-3 py-2 border border-slate-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>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Estimated Cost (৳)</label> <label className="text-sm font-medium text-slate-600 mb-1 block">Estimated Cost (৳)</label>
@@ -1435,8 +1981,17 @@ function MaintenanceModal({ bike, maintenance, onClose, onSave }: { bike: Bike;
cost: maintenance?.cost || 0, cost: maintenance?.cost || 0,
nextDueDate: maintenance?.nextDueDate || '', nextDueDate: maintenance?.nextDueDate || '',
status: maintenance?.status || 'completed', status: maintenance?.status || 'completed',
hubId: maintenance?.hubId || '',
hubName: maintenance?.hubName || '',
}); });
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 maintenanceTypes = [ const maintenanceTypes = [
{ value: 'routine', label: 'Routine Service' }, { value: 'routine', label: 'Routine Service' },
{ value: 'battery', label: 'Battery' }, { value: 'battery', label: 'Battery' },
@@ -1510,6 +2065,19 @@ function MaintenanceModal({ bike, maintenance, onClose, onSave }: { bike: Bike;
required required
/> />
</div> </div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Hub</label>
<select
value={formData.hubId}
onChange={(e) => setFormData({ ...formData, hubId: e.target.value, hubName: mockHubs.find(h => h.id === e.target.value)?.name || '' })}
className="w-full px-3 py-2 border border-slate-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>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Cost (৳)</label> <label className="text-sm font-medium text-slate-600 mb-1 block">Cost (৳)</label>

View File

@@ -17,6 +17,7 @@ interface Bike {
plateNumber: string; plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired'; status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number; batteryLevel: number;
batteryId?: string;
location?: string; // deprecated - use hubId/hubName location?: string; // deprecated - use hubId/hubName
hubId?: string; hubId?: string;
hubName?: string; hubName?: string;
@@ -56,6 +57,15 @@ const hubs = [
{ id: 'HUB-004', name: 'Mirpur Hub' }, { id: 'HUB-004', name: 'Mirpur Hub' },
]; ];
const mockBatteries = [
{ id: 'BAT-001', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-001', chargeLevel: 95 },
{ id: 'BAT-002', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-002', chargeLevel: 75 },
{ id: 'BAT-003', brand: 'Lithium', model: '60V/40Ah', serialNumber: 'LTH-2024-003', chargeLevel: 45 },
{ id: 'BAT-004', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-004', chargeLevel: 88 },
{ id: 'BAT-005', brand: 'Lithium', model: '48V/25Ah', serialNumber: 'LTH-2024-005', chargeLevel: 62 },
{ id: 'BAT-006', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-006', chargeLevel: 100 },
];
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
available: 'bg-green-100 text-green-700', available: 'bg-green-100 text-green-700',
rented: 'bg-blue-100 text-blue-700', rented: 'bg-blue-100 text-blue-700',
@@ -159,9 +169,9 @@ export default function FleetPage() {
<p className="font-semibold text-slate-700">{selectedMapBike.model}</p> <p className="font-semibold text-slate-700">{selectedMapBike.model}</p>
<p className="text-xs text-slate-500">{selectedMapBike.brand} {selectedMapBike.id}</p> <p className="text-xs text-slate-500">{selectedMapBike.brand} {selectedMapBike.id}</p>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${selectedMapBike.status === 'available' ? 'bg-green-100 text-green-700' : <span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${selectedMapBike.status === 'available' ? 'bg-green-100 text-green-700' :
selectedMapBike.status === 'rented' ? 'bg-blue-100 text-blue-700' : selectedMapBike.status === 'rented' ? 'bg-blue-100 text-blue-700' :
selectedMapBike.status === 'maintenance' ? 'bg-amber-100 text-amber-700' : selectedMapBike.status === 'maintenance' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-500' 'bg-slate-100 text-slate-500'
}`}> }`}>
{selectedMapBike.status} {selectedMapBike.status}
</span> </span>
@@ -182,7 +192,7 @@ export default function FleetPage() {
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-slate-500">Battery</span> <span className="text-slate-500">Battery</span>
<span className={`font-medium ${selectedMapBike.batteryLevel > 50 ? 'text-green-600' : <span className={`font-medium ${selectedMapBike.batteryLevel > 50 ? 'text-green-600' :
selectedMapBike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600' selectedMapBike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'
}`}>{selectedMapBike.batteryLevel}%</span> }`}>{selectedMapBike.batteryLevel}%</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
@@ -488,6 +498,7 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
plateNumber: '', plateNumber: '',
status: 'available', status: 'available',
batteryLevel: 100, batteryLevel: 100,
batteryId: '',
location: '', // deprecated location: '', // deprecated
hubId: '', hubId: '',
hubName: '', hubName: '',
@@ -563,14 +574,27 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Battery Level (%)</label> <label className="block text-sm font-medium text-slate-700 mb-1">Battery</label>
<input <select
type="number" value={formData.batteryId || ''}
value={formData.batteryLevel} onChange={(e) => {
onChange={(e) => handleChange('batteryLevel', parseInt(e.target.value))} const battery = mockBatteries.find(b => b.id === e.target.value);
handleChange('batteryId', e.target.value);
if (battery) {
handleChange('batteryLevel', battery.chargeLevel);
}
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/> >
<option value="">Select Battery...</option>
{mockBatteries.map(battery => (
<option key={battery.id} value={battery.id}>
{battery.brand} {battery.model} - {battery.serialNumber} ({battery.chargeLevel}%)
</option>
))}
</select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Hub *</label> <label className="block text-sm font-medium text-slate-700 mb-1">Hub *</label>
<select <select
@@ -588,6 +612,16 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
))} ))}
</select> </select>
</div> </div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Battery Level (%)</label>
<input
type="number"
value={formData.batteryLevel}
onChange={(e) => handleChange('batteryLevel', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label> <label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label>
<input <input
@@ -812,9 +846,9 @@ function FleetMap({ bikes, onSelectBike, selectedBike, large }: { bikes: Bike[];
<path <path
d={large ? "M0,0 L-3,-6 A3.5,3.5 0 1,1 3,-6 L0,0 Z" : "M0,0 L-2,-4 A2.5,2.5 0 1,1 2,-4 L0,0 Z"} d={large ? "M0,0 L-3,-6 A3.5,3.5 0 1,1 3,-6 L0,0 Z" : "M0,0 L-2,-4 A2.5,2.5 0 1,1 2,-4 L0,0 Z"}
className={`${data.bikes[0].status === 'available' ? 'fill-green-500' : className={`${data.bikes[0].status === 'available' ? 'fill-green-500' :
data.bikes[0].status === 'rented' ? 'fill-blue-500' : data.bikes[0].status === 'rented' ? 'fill-blue-500' :
data.bikes[0].status === 'maintenance' ? 'fill-amber-500' : data.bikes[0].status === 'maintenance' ? 'fill-amber-500' :
'fill-slate-400' 'fill-slate-400'
}`} }`}
filter="url(#shadow)" filter="url(#shadow)"
/> />

View File

@@ -4,7 +4,8 @@ import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { import {
ArrowLeft, MapPin, Phone, Clock, Bike, Plus, X, Edit, Save, Trash2, ArrowLeft, MapPin, Phone, Clock, Bike, Plus, X, Edit, Save, Trash2,
Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle, Battery,
Mail, Calendar, Briefcase, Users, Search, UserPlus
} from 'lucide-react'; } from 'lucide-react';
interface Hub { interface Hub {
@@ -29,6 +30,17 @@ interface BikeInfo {
status: 'available' | 'rented' | 'maintenance'; status: 'available' | 'rented' | 'maintenance';
} }
interface BatteryInfo {
id: string;
brand: string;
model: string;
serialNumber: string;
status: 'available' | 'charging' | 'deployed' | 'maintenance';
chargeLevel: number;
assignedBike?: string;
assignedDate?: string;
}
const mockHub: Hub = { const mockHub: Hub = {
id: 'HUB-001', id: 'HUB-001',
name: 'JAIBEN Head Office', name: 'JAIBEN Head Office',
@@ -52,6 +64,15 @@ const mockHubBikes: BikeInfo[] = [
{ id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'maintenance' }, { id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'maintenance' },
]; ];
const mockHubBatteries: BatteryInfo[] = [
{ id: 'BAT-001', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-001', status: 'available', chargeLevel: 95 },
{ id: 'BAT-002', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-002', status: 'charging', chargeLevel: 75 },
{ id: 'BAT-003', brand: 'Lithium', model: '60V/40Ah', serialNumber: 'LTH-2024-003', status: 'deployed', chargeLevel: 45, assignedBike: 'BIKE-002', assignedDate: '2024-03-15' },
{ id: 'BAT-004', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-004', status: 'available', chargeLevel: 88 },
{ id: 'BAT-005', brand: 'Lithium', model: '48V/25Ah', serialNumber: 'LTH-2024-005', status: 'maintenance', chargeLevel: 0 },
{ id: 'BAT-006', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-006', status: 'deployed', chargeLevel: 62, assignedBike: 'BIKE-004', assignedDate: '2024-03-18' },
];
interface RentalInfo { interface RentalInfo {
id: string; id: string;
userName: string; userName: string;
@@ -70,6 +91,70 @@ const mockHubRentals: RentalInfo[] = [
{ id: 'RNT-003', userName: 'Jamal Uddin', bike: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', startDate: '2024-02-01', type: 'shared', status: 'pending', dailyRate: 150, totalPaid: 450 }, { id: 'RNT-003', userName: 'Jamal Uddin', bike: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', startDate: '2024-02-01', type: 'shared', status: 'pending', dailyRate: 150, totalPaid: 450 },
]; ];
interface Employee {
id: string;
name: string;
role: 'Manager' | 'Accountant' | 'Staff' | 'Technician' | 'Support';
email: string;
phone: string;
status: 'Active' | 'On Leave' | 'Inactive';
joiningDate: string;
shift: 'Morning' | 'Evening' | 'Night' | 'Full-time';
}
const mockHubEmployees: Employee[] = [
{
id: 'EMP-001',
name: 'Arif Rahman',
role: 'Manager',
email: 'arif.rahman@jaiben.com',
phone: '+8801711223344',
status: 'Active',
joiningDate: '2023-01-10',
shift: 'Full-time',
},
{
id: 'EMP-002',
name: 'Tasmia Chowdhury',
role: 'Accountant',
email: 'tasmia.c@jaiben.com',
phone: '+8801722334455',
status: 'Active',
joiningDate: '2023-03-15',
shift: 'Morning',
},
{
id: 'EMP-003',
name: 'Kamrul Islam',
role: 'Staff',
email: 'kamrul.i@jaiben.com',
phone: '+8801733445566',
status: 'Active',
joiningDate: '2023-06-20',
shift: 'Evening',
},
{
id: 'EMP-004',
name: 'Mizanur Rahman',
role: 'Technician',
email: 'mizan.r@jaiben.com',
phone: '+8801744556677',
status: 'Active',
joiningDate: '2023-08-01',
shift: 'Morning',
},
{
id: 'EMP-005',
name: 'Sujon Ali',
role: 'Support',
email: 'sujon.a@jaiben.com',
phone: '+8801755667788',
status: 'On Leave',
joiningDate: '2023-11-15',
shift: 'Night',
},
];
export default function HubDetailPage() { export default function HubDetailPage() {
const params = useParams(); const params = useParams();
const router = useRouter(); const router = useRouter();
@@ -77,10 +162,36 @@ export default function HubDetailPage() {
const [hub, setHub] = useState<Hub>(mockHub); const [hub, setHub] = useState<Hub>(mockHub);
const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes); const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes);
const [batteries, setBatteries] = useState<BatteryInfo[]>(mockHubBatteries);
const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals); const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals);
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [editForm, setEditForm] = useState(hub); const [editForm, setEditForm] = useState(hub);
const [activeTab, setActiveTab] = useState<'overview' | 'bikes' | 'rentals'>('overview'); const [activeTab, setActiveTab] = useState<'overview' | 'employees' | 'bikes' | 'batteries' | 'rentals'>('overview');
const [employees, setEmployees] = useState<Employee[]>(mockHubEmployees);
const [employeeSearch, setEmployeeSearch] = useState('');
const [roleFilter, setRoleFilter] = useState<string>('All');
const [statusFilter, setStatusFilter] = useState<string>('All');
const [addEmployeeModal, setAddEmployeeModal] = useState(false);
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
const [employeeForm, setEmployeeForm] = useState<Omit<Employee, 'id'>>({
name: '',
role: 'Staff',
email: '',
phone: '',
status: 'Active',
joiningDate: new Date().toISOString().split('T')[0],
shift: 'Full-time'
});
const [deleteEmployeeModal, setDeleteEmployeeModal] = useState<Employee | null>(null);
const [assignModal, setAssignModal] = useState<BatteryInfo | null>(null);
const [selectedBike, setSelectedBike] = useState('');
const [addBikeModal, setAddBikeModal] = useState(false);
const [addBatteryModal, setAddBatteryModal] = useState(false);
const [editingBike, setEditingBike] = useState<BikeInfo | null>(null);
const [editingBattery, setEditingBattery] = useState<BatteryInfo | null>(null);
const [bikeForm, setBikeForm] = useState<{ model: string; plate: string; status: 'available' | 'rented' | 'maintenance' }>({ model: '', plate: '', status: 'available' });
const [batteryForm, setBatteryForm] = useState<{ brand: string; model: string; serialNumber: string; chargeLevel: number; status: 'available' | 'charging' | 'deployed' | 'maintenance' }>({ brand: '', model: '', serialNumber: '', chargeLevel: 100, status: 'available' });
const [deleteModal, setDeleteModal] = useState<{ type: 'bike' | 'battery'; item: BikeInfo | BatteryInfo } | null>(null);
const handleSaveEdit = () => { const handleSaveEdit = () => {
setHub(editForm); setHub(editForm);
@@ -155,26 +266,44 @@ export default function HubDetailPage() {
<button <button
onClick={() => setActiveTab('overview')} onClick={() => setActiveTab('overview')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview' className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview'
? 'border-accent text-accent' ? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 hover:text-slate-700'
}`} }`}
> >
Overview Overview
</button> </button>
<button
onClick={() => setActiveTab('employees')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'employees'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Employees ({employees.length})
</button>
<button <button
onClick={() => setActiveTab('bikes')} onClick={() => setActiveTab('bikes')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes' className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes'
? 'border-accent text-accent' ? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 hover:text-slate-700'
}`} }`}
> >
Bikes ({bikes.length}) Bikes ({bikes.length})
</button> </button>
<button
onClick={() => setActiveTab('batteries')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'batteries'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Batteries ({batteries.length})
</button>
<button <button
onClick={() => setActiveTab('rentals')} onClick={() => setActiveTab('rentals')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals' className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals'
? 'border-accent text-accent' ? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 hover:text-slate-700'
}`} }`}
> >
Rentals ({rentals.length}) Rentals ({rentals.length})
@@ -337,11 +466,199 @@ export default function HubDetailPage() {
</div> </div>
)} )}
{activeTab === 'employees' && (
<div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<h3 className="font-bold text-slate-800 text-lg">Hub Employees ({employees.length})</h3>
<p className="text-sm text-slate-500 mt-0.5">Manage and track hub operational personnel and roles</p>
</div>
<button
onClick={() => {
setEditingEmployee(null);
setEmployeeForm({
name: '',
role: 'Staff',
email: '',
phone: '',
status: 'Active',
joiningDate: new Date().toISOString().split('T')[0],
shift: 'Full-time'
});
setAddEmployeeModal(true);
}}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium flex items-center gap-2 hover:opacity-90 transition-all shadow-sm self-start md:self-auto"
>
<UserPlus className="w-4 h-4" /> Add Employee
</button>
</div>
{/* Search & Filter Toolbar */}
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 flex flex-col md:flex-row gap-3 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-3 top-2.5 w-4 h-4 text-slate-400" />
<input
type="text"
value={employeeSearch}
onChange={(e) => setEmployeeSearch(e.target.value)}
placeholder="Search by name, email, phone or ID..."
className="w-full pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div className="flex gap-3">
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="All">All Roles</option>
<option value="Manager">Managers</option>
<option value="Accountant">Accountants</option>
<option value="Staff">Operations Staff</option>
<option value="Technician">Technicians</option>
<option value="Support">Support Staff</option>
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="All">All Statuses</option>
<option value="Active">Active / On Duty</option>
<option value="On Leave">On Leave</option>
<option value="Inactive">Inactive</option>
</select>
</div>
</div>
{/* Roster Cards Grid */}
{employees.filter(emp => {
const matchesSearch =
emp.name.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.email.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.phone.includes(employeeSearch) ||
emp.id.toLowerCase().includes(employeeSearch.toLowerCase());
const matchesRole = roleFilter === 'All' || emp.role === roleFilter;
const matchesStatus = statusFilter === 'All' || emp.status === statusFilter;
return matchesSearch && matchesRole && matchesStatus;
}).length === 0 ? (
<div className="text-center py-12 bg-slate-50 rounded-xl border border-slate-100">
<Users className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500 text-sm">No employees match your search or filter criteria.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{employees.filter(emp => {
const matchesSearch =
emp.name.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.email.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.phone.includes(employeeSearch) ||
emp.id.toLowerCase().includes(employeeSearch.toLowerCase());
const matchesRole = roleFilter === 'All' || emp.role === roleFilter;
const matchesStatus = statusFilter === 'All' || emp.status === statusFilter;
return matchesSearch && matchesRole && matchesStatus;
}).map(emp => {
const roleConfig: Record<string, { badge: string; circle: string; text: string }> = {
Manager: { badge: 'bg-emerald-100 text-emerald-800 border-emerald-200', circle: 'bg-emerald-50 text-emerald-600', text: 'text-emerald-700' },
Accountant: { badge: 'bg-blue-100 text-blue-800 border-blue-200', circle: 'bg-blue-50 text-blue-600', text: 'text-blue-700' },
Staff: { badge: 'bg-purple-100 text-purple-800 border-purple-200', circle: 'bg-purple-50 text-purple-600', text: 'text-purple-700' },
Technician: { badge: 'bg-amber-100 text-amber-800 border-amber-200', circle: 'bg-amber-50 text-amber-600', text: 'text-amber-700' },
Support: { badge: 'bg-orange-100 text-orange-800 border-orange-200', circle: 'bg-orange-50 text-orange-600', text: 'text-orange-700' },
};
const style = roleConfig[emp.role] || roleConfig.Staff;
return (
<div key={emp.id} className="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden hover:shadow-md transition-all flex flex-col justify-between">
<div className="p-5">
<div className="flex items-start justify-between gap-3 mb-4">
<div className="flex items-center gap-3">
<div className={`w-12 h-12 rounded-full flex items-center justify-center font-bold text-lg ${style.circle}`}>
{emp.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
</div>
<div>
<h4 className="font-bold text-slate-800 hover:text-accent transition-colors">{emp.name}</h4>
<span className="text-xs text-slate-400 font-mono">{emp.id}</span>
</div>
</div>
<span className={`inline-flex items-center text-xs font-semibold px-2.5 py-0.5 rounded-full border ${style.badge}`}>
{emp.role}
</span>
</div>
<div className="space-y-2.5 my-4 border-t border-b border-slate-50 py-3">
<div className="flex items-center gap-2 text-sm text-slate-600">
<Mail className="w-4 h-4 text-slate-400 flex-shrink-0" />
<a href={`mailto:${emp.email}`} className="truncate hover:text-accent hover:underline">{emp.email}</a>
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Phone className="w-4 h-4 text-slate-400 flex-shrink-0" />
<a href={`tel:${emp.phone}`} className="hover:text-accent hover:underline">{emp.phone}</a>
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Clock className="w-4 h-4 text-slate-400 flex-shrink-0" />
<span>Shift: <span className="font-medium text-slate-700">{emp.shift}</span></span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Calendar className="w-4 h-4 text-slate-400 flex-shrink-0" />
<span>Joined: <span className="font-medium text-slate-700">{emp.joiningDate}</span></span>
</div>
</div>
</div>
<div className="px-5 pb-5 pt-1 border-t border-slate-50 flex items-center justify-between bg-slate-50/50">
<span className={`inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 rounded-full ${
emp.status === 'Active' ? 'bg-green-100 text-green-700' :
emp.status === 'On Leave' ? 'bg-amber-100 text-amber-700' :
'bg-slate-200 text-slate-600'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${
emp.status === 'Active' ? 'bg-green-500' :
emp.status === 'On Leave' ? 'bg-amber-500' :
'bg-slate-500'
}`} />
{emp.status}
</span>
<div className="flex gap-2">
<button
onClick={() => {
setEditingEmployee(emp);
setEmployeeForm({
name: emp.name,
role: emp.role,
email: emp.email,
phone: emp.phone,
status: emp.status,
joiningDate: emp.joiningDate,
shift: emp.shift
});
setAddEmployeeModal(true);
}}
className="px-2.5 py-1.5 text-xs font-semibold text-blue-600 hover:text-blue-700 bg-white border border-blue-100 hover:border-blue-200 rounded-lg hover:shadow-sm transition-all"
>
Edit
</button>
<button
onClick={() => setDeleteEmployeeModal(emp)}
className="px-2.5 py-1.5 text-xs font-semibold text-red-600 hover:text-red-700 bg-white border border-red-100 hover:border-red-200 rounded-lg hover:shadow-sm transition-all"
>
Delete
</button>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
)}
{activeTab === 'bikes' && ( {activeTab === 'bikes' && (
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Bikes ({bikes.length})</h3> <h3 className="font-semibold text-slate-800">Hub Bikes ({bikes.length})</h3>
<button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2"> <button onClick={() => { setBikeForm({ model: '', plate: '', status: 'available' }); setAddBikeModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Bike <Plus className="w-4 h-4" /> Add Bike
</button> </button>
</div> </div>
@@ -351,8 +668,8 @@ export default function HubDetailPage() {
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Bike className="w-5 h-5 text-slate-400" /> <Bike className="w-5 h-5 text-slate-400" />
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'available' ? 'bg-green-100 text-green-700' : <span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'available' ? 'bg-green-100 text-green-700' :
bike.status === 'rented' ? 'bg-amber-100 text-amber-700' : bike.status === 'rented' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700' 'bg-red-100 text-red-700'
}`}> }`}>
{bike.status} {bike.status}
</span> </span>
@@ -360,6 +677,73 @@ export default function HubDetailPage() {
<p className="font-medium text-slate-800">{bike.model}</p> <p className="font-medium text-slate-800">{bike.model}</p>
<p className="text-sm text-slate-500">{bike.plate}</p> <p className="text-sm text-slate-500">{bike.plate}</p>
<p className="text-xs text-slate-400 mt-2">ID: {bike.id}</p> <p className="text-xs text-slate-400 mt-2">ID: {bike.id}</p>
<div className="flex gap-2 mt-3">
<button onClick={() => { setEditingBike(bike); setBikeForm({ model: bike.model, plate: bike.plate, status: bike.status }); setAddBikeModal(true); }} className="flex-1 py-1.5 text-xs font-medium text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">Edit</button>
<button onClick={() => setDeleteModal({ type: 'bike', item: bike })} className="flex-1 py-1.5 text-xs font-medium text-red-600 border border-red-200 rounded-lg hover:bg-red-50">Delete</button>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === 'batteries' && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Batteries ({batteries.length})</h3>
<button onClick={() => { setBatteryForm({ brand: '', model: '', serialNumber: '', chargeLevel: 100, status: 'available' }); setAddBatteryModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Battery
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{batteries.map(battery => (
<div key={battery.id} className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div className="flex items-center justify-between mb-2">
<Battery className="w-5 h-5 text-slate-400" />
<span className={`text-xs font-medium px-2 py-1 rounded-full ${battery.status === 'available' ? 'bg-green-100 text-green-700' :
battery.status === 'charging' ? 'bg-blue-100 text-blue-700' :
battery.status === 'deployed' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{battery.status}
</span>
</div>
<p className="font-medium text-slate-800">{battery.brand} {battery.model}</p>
<p className="text-sm text-slate-500">SN: {battery.serialNumber}</p>
<div className="mt-2">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-slate-500">Charge</span>
<span className={`font-medium ${battery.chargeLevel > 50 ? 'text-green-600' : battery.chargeLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>
{battery.chargeLevel}%
</span>
</div>
<div className="h-1.5 bg-slate-200 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${battery.chargeLevel > 50 ? 'bg-green-500' : battery.chargeLevel > 20 ? 'bg-amber-500' : 'bg-red-500'}`} style={{ width: `${battery.chargeLevel}%` }} />
</div>
</div>
{battery.assignedBike && (
<p className="text-xs text-slate-400 mt-2">Assigned to: {battery.assignedBike}</p>
)}
<div className="mt-2 flex gap-2">
<button
onClick={() => { setAssignModal(battery); setSelectedBike(battery.assignedBike || ''); }}
className="flex-1 py-1.5 text-xs font-medium text-accent border border-accent rounded-lg hover:bg-accent hover:text-white transition-colors"
>
{battery.assignedBike ? 'Reassign' : 'Assign'}
</button>
{battery.assignedBike && (
<button
onClick={() => { setBatteries(prev => prev.map(b => b.id === battery.id ? { ...b, assignedBike: undefined, assignedDate: undefined, status: 'available' as const } : b)); }}
className="py-1.5 px-2 text-xs font-medium text-orange-600 border border-orange-200 rounded-lg hover:bg-orange-50"
>
Unassign
</button>
)}
</div>
<div className="flex gap-2 mt-2">
<button onClick={() => { setEditingBattery(battery); setBatteryForm({ brand: battery.brand, model: battery.model, serialNumber: battery.serialNumber, chargeLevel: battery.chargeLevel, status: battery.status }); setAddBatteryModal(true); }} className="flex-1 py-1.5 text-xs font-medium text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">Edit</button>
<button onClick={() => setDeleteModal({ type: 'battery', item: battery })} className="flex-1 py-1.5 text-xs font-medium text-red-600 border border-red-200 rounded-lg hover:bg-red-50">Delete</button>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -370,9 +754,9 @@ export default function HubDetailPage() {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Rentals ({rentals.length})</h3> <h3 className="font-semibold text-slate-800">Hub Rentals ({rentals.length})</h3>
<button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2"> {/* <button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> New Rental <Plus className="w-4 h-4" /> New Rental
</button> </button> */}
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
@@ -416,11 +800,10 @@ export default function HubDetailPage() {
<span className="text-sm font-medium text-green-600">৳{rental.totalPaid.toLocaleString()}</span> <span className="text-sm font-medium text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${ <span className={`text-xs font-medium px-2.5 py-1 rounded-full ${rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'active' ? 'bg-green-100 text-green-700' : rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' : 'bg-blue-100 text-blue-700'
'bg-blue-100 text-blue-700' }`}>
}`}>
{rental.status} {rental.status}
</span> </span>
</td> </td>
@@ -433,6 +816,382 @@ export default function HubDetailPage() {
)} )}
</div> </div>
</div> </div>
{assignModal && (
<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">Assign Battery to Bike</h3>
<button onClick={() => setAssignModal(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Battery</p>
<p className="font-medium text-slate-800">{assignModal.brand} {assignModal.model}</p>
<p className="text-xs text-slate-500">SN: {assignModal.serialNumber}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
<select
value={selectedBike}
onChange={(e) => setSelectedBike(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">-- Select a bike --</option>
{bikes.filter(b => b.status !== 'maintenance').map(bike => (
<option key={bike.id} value={bike.id}>
{bike.model} - {bike.plate}
</option>
))}
</select>
</div>
{selectedBike && (
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-green-600">Battery will be assigned to:</p>
<p className="font-medium text-green-800">
{bikes.find(b => b.id === selectedBike)?.model} ({bikes.find(b => b.id === selectedBike)?.plate})
</p>
</div>
)}
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setAssignModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
setBatteries(prev => prev.map(b => b.id === assignModal.id ? {
...b,
assignedBike: selectedBike || undefined,
assignedDate: selectedBike ? new Date().toISOString().split('T')[0] : undefined,
status: selectedBike ? 'deployed' as const : 'available' as const
} : b));
setAssignModal(null);
}}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark"
>
{assignModal.assignedBike ? 'Update Assignment' : 'Assign Battery'}
</button>
</div>
</div>
</div>
)}
{addBikeModal && (
<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">{editingBike ? 'Edit Bike' : 'Add New Bike'}</h3>
<button onClick={() => { setAddBikeModal(false); setEditingBike(null); }} 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 font-medium text-slate-600 mb-1 block">Model</label>
<input type="text" value={bikeForm.model} onChange={(e) => setBikeForm(f => ({ ...f, model: e.target.value }))} placeholder="e.g. AIMA Lightning" disabled={!!editingBike} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">License Plate</label>
<input type="text" value={bikeForm.plate} onChange={(e) => setBikeForm(f => ({ ...f, plate: e.target.value }))} placeholder="e.g. Dhaka Metro Cha-1234" disabled={!!editingBike} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={bikeForm.status} onChange={(e) => setBikeForm(f => ({ ...f, status: e.target.value as any }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="available">Available</option>
<option value="rented">Rented</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => { setAddBikeModal(false); setEditingBike(null); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (editingBike) {
setBikes(prev => prev.map(b => b.id === editingBike.id ? { ...b, ...bikeForm } : b));
} else {
const newBike: BikeInfo = { id: `BIKE-${Date.now()}`, ...bikeForm };
setBikes(prev => [...prev, newBike]);
}
setAddBikeModal(false);
setEditingBike(null);
}}
disabled={!bikeForm.model || !bikeForm.plate}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
>
{editingBike ? 'Update Bike' : 'Add Bike'}
</button>
</div>
</div>
</div>
)}
{addBatteryModal && (
<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">{editingBattery ? 'Edit Battery' : 'Add New Battery'}</h3>
<button onClick={() => { setAddBatteryModal(false); setEditingBattery(null); }} 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 font-medium text-slate-600 mb-1 block">Brand</label>
<input type="text" value={batteryForm.brand} onChange={(e) => setBatteryForm(f => ({ ...f, brand: e.target.value }))} placeholder="e.g. Lithium" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Model</label>
<input type="text" value={batteryForm.model} onChange={(e) => setBatteryForm(f => ({ ...f, model: e.target.value }))} placeholder="e.g. 60V/30Ah" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Serial Number</label>
<input type="text" value={batteryForm.serialNumber} onChange={(e) => setBatteryForm(f => ({ ...f, serialNumber: e.target.value }))} placeholder="e.g. LTH-2024-001" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Charge Level (%)</label>
<input type="number" min="0" max="100" value={batteryForm.chargeLevel} onChange={(e) => setBatteryForm(f => ({ ...f, chargeLevel: parseInt(e.target.value) || 0 }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={batteryForm.status} onChange={(e) => setBatteryForm(f => ({ ...f, status: e.target.value as any }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="available">Available</option>
<option value="charging">Charging</option>
<option value="deployed">Deployed</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => { setAddBatteryModal(false); setEditingBattery(null); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (editingBattery) {
setBatteries(prev => prev.map(b => b.id === editingBattery.id ? { ...b, ...batteryForm } : b));
} else {
const newBattery: BatteryInfo = { id: `BAT-${Date.now()}`, ...batteryForm, assignedBike: undefined, assignedDate: undefined };
setBatteries(prev => [...prev, newBattery]);
}
setAddBatteryModal(false);
setEditingBattery(null);
}}
disabled={!batteryForm.brand || !batteryForm.model || !batteryForm.serialNumber}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
>
{editingBattery ? 'Update Battery' : 'Add Battery'}
</button>
</div>
</div>
</div>
)}
{deleteModal && (
<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-sm">
<div className="p-6 text-center">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<h3 className="font-semibold text-slate-800 mb-2">Confirm Delete</h3>
<p className="text-sm text-slate-500">
Are you sure you want to delete this {deleteModal.type === 'bike' ? 'bike' : 'battery'}?
{deleteModal.type === 'bike' && (
<span className="block mt-1 font-medium text-slate-700">{(deleteModal.item as BikeInfo).model} - {(deleteModal.item as BikeInfo).plate}</span>
)}
{deleteModal.type === 'battery' && (
<span className="block mt-1 font-medium text-slate-700">{(deleteModal.item as BatteryInfo).brand} {(deleteModal.item as BatteryInfo).model}</span>
)}
</p>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setDeleteModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (deleteModal.type === 'bike') {
setBikes(prev => prev.filter(b => b.id !== (deleteModal.item as BikeInfo).id));
} else {
setBatteries(prev => prev.filter(b => b.id !== (deleteModal.item as BatteryInfo).id));
}
setDeleteModal(null);
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700"
>
Delete
</button>
</div>
</div>
</div>
)}
{addEmployeeModal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden">
<div className="p-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
<h3 className="font-bold text-slate-800">{editingEmployee ? 'Edit Employee Details' : 'Register New Employee'}</h3>
<button onClick={() => { setAddEmployeeModal(false); setEditingEmployee(null); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4 max-h-[80vh] overflow-y-auto">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Full Name</label>
<input
type="text"
value={employeeForm.name}
onChange={(e) => setEmployeeForm(f => ({ ...f, name: e.target.value }))}
placeholder="e.g. Arif Rahman"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Role</label>
<select
value={employeeForm.role}
onChange={(e) => setEmployeeForm(f => ({ ...f, role: e.target.value as any }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="Manager">Manager</option>
<option value="Accountant">Accountant</option>
<option value="Staff">Operations Staff</option>
<option value="Technician">Technician</option>
<option value="Support">Support Staff</option>
</select>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Shift</label>
<select
value={employeeForm.shift}
onChange={(e) => setEmployeeForm(f => ({ ...f, shift: e.target.value as any }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="Full-time">Full-time</option>
<option value="Morning">Morning</option>
<option value="Evening">Evening</option>
<option value="Night">Night</option>
</select>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Email Address</label>
<input
type="email"
value={employeeForm.email}
onChange={(e) => setEmployeeForm(f => ({ ...f, email: e.target.value }))}
placeholder="e.g. arif.rahman@jaiben.com"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Phone Number</label>
<input
type="text"
value={employeeForm.phone}
onChange={(e) => setEmployeeForm(f => ({ ...f, phone: e.target.value }))}
placeholder="e.g. +8801711223344"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Status</label>
<select
value={employeeForm.status}
onChange={(e) => setEmployeeForm(f => ({ ...f, status: e.target.value as any }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="Active">Active / On Duty</option>
<option value="On Leave">On Leave</option>
<option value="Inactive">Inactive</option>
</select>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Joining Date</label>
<input
type="date"
value={employeeForm.joiningDate}
onChange={(e) => setEmployeeForm(f => ({ ...f, joiningDate: e.target.value }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2 bg-slate-50">
<button
onClick={() => { setAddEmployeeModal(false); setEditingEmployee(null); }}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 transition-all"
>
Cancel
</button>
<button
onClick={() => {
if (editingEmployee) {
setEmployees(prev => prev.map(emp => emp.id === editingEmployee.id ? { ...emp, ...employeeForm } : emp));
} else {
const nextIdNum = employees.length > 0
? Math.max(...employees.map(e => parseInt(e.id.split('-')[1]) || 0)) + 1
: 1;
const nextIdStr = `EMP-${nextIdNum.toString().padStart(3, '0')}`;
const newEmployee: Employee = {
id: nextIdStr,
...employeeForm
};
setEmployees(prev => [...prev, newEmployee]);
}
setAddEmployeeModal(false);
setEditingEmployee(null);
}}
disabled={!employeeForm.name || !employeeForm.email || !employeeForm.phone}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-semibold hover:opacity-90 disabled:opacity-50 transition-all"
>
{editingEmployee ? 'Save Changes' : 'Register Employee'}
</button>
</div>
</div>
</div>
)}
{deleteEmployeeModal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm overflow-hidden">
<div className="p-6 text-center">
<div className="w-12 h-12 bg-red-50 border border-red-200 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<h3 className="font-bold text-slate-800 text-lg mb-2">Remove Employee</h3>
<p className="text-sm text-slate-500">
Are you sure you want to remove <span className="font-semibold text-slate-700">{deleteEmployeeModal.name}</span> from Gulshan Head Office's operational roster?
</p>
<div className="mt-3 bg-slate-50 p-3 rounded-lg border border-slate-100 text-left">
<p className="text-xs text-slate-400 font-mono">ID: {deleteEmployeeModal.id}</p>
<p className="text-xs font-semibold text-slate-700 capitalize mt-1">Role: {deleteEmployeeModal.role}</p>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2 bg-slate-50">
<button
onClick={() => setDeleteEmployeeModal(null)}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 transition-all"
>
Cancel
</button>
<button
onClick={() => {
setEmployees(prev => prev.filter(emp => emp.id !== deleteEmployeeModal.id));
setDeleteEmployeeModal(null);
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-semibold hover:bg-red-700 transition-all"
>
Confirm Delete
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
'use client';
import { useState, useEffect } from 'react';
import { Battery, X } from 'lucide-react';
import toast from 'react-hot-toast';
interface AssignBatteryModalProps {
isOpen: boolean;
onClose: () => void;
investor: any;
batteries: any[];
unassignedBatteries: any[];
preselectedPlanId?: string;
onAssign: (planId: string, batteryIds: string[]) => void;
}
export default function AssignBatteryModal({
isOpen,
onClose,
investor,
batteries,
unassignedBatteries,
preselectedPlanId = '',
onAssign
}: AssignBatteryModalProps) {
const [selectedPlanId, setSelectedPlanId] = useState(preselectedPlanId);
const [selectedBatteryIds, setSelectedBatteryIds] = useState<string[]>([]);
useEffect(() => {
if (isOpen) {
setSelectedPlanId(preselectedPlanId);
setSelectedBatteryIds([]);
}
}, [isOpen, preselectedPlanId]);
if (!isOpen || !investor) return null;
const getPlanTargetAssetCount = (plan: any) => {
if (!plan) return 1;
if (plan.assetType === 'battery' || plan.planName?.toLowerCase().includes('battery')) {
if (plan.id === 'ip3') return 2;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
} else {
if (plan.id === 'ip1' || plan.id === 'ip2') return 1;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
}
};
const selectedPlan = investor.investments?.find((inv: any) => inv.id === selectedPlanId);
const assignedCount = selectedPlan ? batteries.filter(b => b.investmentId === selectedPlan.id).length : 0;
const targetCount = selectedPlan ? getPlanTargetAssetCount(selectedPlan) : 0;
const remainingCapacity = selectedPlan ? Math.max(0, targetCount - assignedCount) : 0;
const handleAssignSubmit = () => {
if (!selectedPlanId) {
toast.error('Please select an investment plan');
return;
}
if (selectedBatteryIds.length === 0) {
toast.error('Please select at least one battery');
return;
}
onAssign(selectedPlanId, selectedBatteryIds);
onClose();
};
return (
<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 overflow-hidden flex flex-col">
<div className="p-5 border-b border-emerald-100 bg-emerald-50 flex items-center justify-between">
<h2 className="text-lg font-bold text-emerald-800 flex items-center gap-2">
<Battery className="w-5 h-5 text-emerald-600 animate-bounce" />
Assign Battery to Partner
</h2>
<button onClick={onClose} className="p-2 hover:bg-emerald-100 rounded-lg text-emerald-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-5 space-y-4 overflow-y-auto max-h-[70vh]">
<div>
<label className="text-sm font-medium text-slate-600 mb-1.5 block">Link to Investment Plan *</label>
<select
value={selectedPlanId}
disabled={!!preselectedPlanId}
onChange={(e) => {
setSelectedPlanId(e.target.value);
setSelectedBatteryIds([]);
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 disabled:bg-slate-50 disabled:text-slate-500"
>
<option value="">Select plan</option>
{investor.investments?.filter((inv: any) => inv.assetType === 'battery' || inv.planName.toLowerCase().includes('battery')).map((inv: any) => {
const curAssigned = batteries.filter(b => b.investmentId === inv.id).length;
const target = getPlanTargetAssetCount(inv);
const rem = Math.max(0, target - curAssigned);
return (
<option key={inv.id} value={inv.id}>
{inv.planName} (Remaining: {rem} / {target} Pack{target !== 1 ? 's' : ''})
</option>
);
})}
</select>
</div>
{selectedPlanId && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-semibold text-slate-700 block">Select Battery Pack(s) *</label>
<span className="text-xs font-bold text-emerald-700 bg-emerald-100 px-2 py-0.5 rounded-full">
Selected: {selectedBatteryIds.length} / {remainingCapacity}
</span>
</div>
{remainingCapacity === 0 ? (
<div className="p-3 text-center text-xs text-amber-700 bg-amber-50 rounded-lg border border-amber-200">
This plan has reached its full capacity of {targetCount} battery pack(s). Unassign some batteries first to assign new ones.
</div>
) : (
<div className="border border-slate-200 rounded-lg max-h-56 overflow-y-auto divide-y divide-slate-100">
{unassignedBatteries.map(bat => {
const isChecked = selectedBatteryIds.includes(bat.id);
const isDisabled = !isChecked && selectedBatteryIds.length >= remainingCapacity;
return (
<label
key={bat.id}
className={`flex items-center justify-between p-3 cursor-pointer text-sm transition-colors ${
isChecked ? 'bg-emerald-50/50' : isDisabled ? 'opacity-50 cursor-not-allowed bg-slate-50' : 'hover:bg-slate-50'
}`}
>
<div className="flex items-center gap-2.5">
<input
type="checkbox"
checked={isChecked}
disabled={isDisabled}
onChange={(e) => {
if (e.target.checked) {
if (selectedBatteryIds.length < remainingCapacity) {
setSelectedBatteryIds([...selectedBatteryIds, bat.id]);
} else {
toast.error(`Cannot select more than ${remainingCapacity} batteries`);
}
} else {
setSelectedBatteryIds(selectedBatteryIds.filter(id => id !== bat.id));
}
}}
className="rounded text-emerald-600 focus:ring-emerald-500 border-slate-300 w-4 h-4"
/>
<div>
<p className="font-semibold text-slate-800">{bat.brand} {bat.model}</p>
<p className="text-xs text-slate-500">SN: {bat.serialNumber}</p>
</div>
</div>
<span className="text-slate-600 font-medium text-xs">{bat.purchasePrice?.toLocaleString() || 0}</span>
</label>
);
})}
{unassignedBatteries.length === 0 && (
<div className="p-4 text-center text-slate-400 text-sm">No unassigned batteries available</div>
)}
</div>
)}
</div>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
<button
onClick={onClose}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 bg-white"
>
Cancel
</button>
<button
onClick={handleAssignSubmit}
disabled={!selectedPlanId || selectedBatteryIds.length === 0}
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-semibold hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-colors"
>
Assign {selectedBatteryIds.length > 0 ? `${selectedBatteryIds.length} Battery/ies` : 'Battery'}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,191 @@
'use client';
import { useState, useEffect } from 'react';
import { Bike, X } from 'lucide-react';
import toast from 'react-hot-toast';
interface AssignBikeModalProps {
isOpen: boolean;
onClose: () => void;
investor: any;
bikes: any[];
preselectedPlanId?: string;
onAssign: (planId: string, bikeIds: string[]) => void;
}
export default function AssignBikeModal({
isOpen,
onClose,
investor,
bikes,
preselectedPlanId = '',
onAssign
}: AssignBikeModalProps) {
const [selectedPlanId, setSelectedPlanId] = useState(preselectedPlanId);
const [selectedBikeIds, setSelectedBikeIds] = useState<string[]>([]);
useEffect(() => {
if (isOpen) {
setSelectedPlanId(preselectedPlanId);
setSelectedBikeIds([]);
}
}, [isOpen, preselectedPlanId]);
if (!isOpen || !investor) return null;
const getPlanTargetAssetCount = (plan: any) => {
if (!plan) return 1;
if (plan.assetType === 'battery' || plan.planName?.toLowerCase().includes('battery')) {
if (plan.id === 'ip3') return 2;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
} else {
if (plan.id === 'ip1' || plan.id === 'ip2') return 1;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
}
};
const selectedPlan = investor.investments?.find((inv: any) => inv.id === selectedPlanId);
const assignedCount = selectedPlan ? bikes.filter(b => b.investmentId === selectedPlan.id).length : 0;
const targetCount = selectedPlan ? getPlanTargetAssetCount(selectedPlan) : 0;
const remainingCapacity = selectedPlan ? Math.max(0, targetCount - assignedCount) : 0;
const availableBikes = bikes.filter(b => !b.investorId && b.status === 'available');
const handleAssignSubmit = () => {
if (!selectedPlanId) {
toast.error('Please select an investment plan');
return;
}
if (selectedBikeIds.length === 0) {
toast.error('Please select at least one bike');
return;
}
onAssign(selectedPlanId, selectedBikeIds);
onClose();
};
return (
<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 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-investor flex items-center gap-2">
<Bike className="w-5 h-5 text-investor animate-bounce" />
Assign Bike to Investor
</h2>
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4 overflow-y-auto max-h-[70vh]">
<div>
<label className="text-sm font-medium text-slate-600 mb-1.5 block">Select Investment Plan *</label>
<select
value={selectedPlanId}
disabled={!!preselectedPlanId}
onChange={(e) => {
setSelectedPlanId(e.target.value);
setSelectedBikeIds([]);
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-investor disabled:bg-slate-50 disabled:text-slate-500"
>
<option value="">Choose an active plan</option>
{investor.investments?.filter((inv: any) => inv.assetType === 'bike' || !inv.assetType || inv.planName.toLowerCase().includes('ev') || inv.planName.toLowerCase().includes('bike')).map((inv: any) => {
const curAssigned = bikes.filter(b => b.investmentId === inv.id).length;
const target = getPlanTargetAssetCount(inv);
const rem = Math.max(0, target - curAssigned);
return (
<option key={inv.id} value={inv.id}>
{inv.planName} (Remaining: {rem} / {target} Bike{target !== 1 ? 's' : ''})
</option>
);
})}
</select>
</div>
{selectedPlanId && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-semibold text-slate-700 block">Select Bike(s) *</label>
<span className="text-xs font-bold text-investor bg-investor/10 px-2 py-0.5 rounded-full">
Selected: {selectedBikeIds.length} / {remainingCapacity}
</span>
</div>
{remainingCapacity === 0 ? (
<div className="p-3 text-center text-xs text-amber-700 bg-amber-50 rounded-lg border border-amber-200">
This plan has reached its full capacity of {targetCount} bike(s). Unassign some bikes first to assign new ones.
</div>
) : (
<div className="border border-slate-200 rounded-lg max-h-48 overflow-y-auto divide-y divide-slate-100">
{availableBikes.map(bike => {
const isChecked = selectedBikeIds.includes(bike.id);
const isDisabled = !isChecked && selectedBikeIds.length >= remainingCapacity;
return (
<label
key={bike.id}
className={`flex items-center justify-between p-3 cursor-pointer text-sm transition-colors ${
isChecked ? 'bg-investor/5' : isDisabled ? 'opacity-50 cursor-not-allowed bg-slate-50' : 'hover:bg-slate-50'
}`}
>
<div className="flex items-center gap-2.5">
<input
type="checkbox"
checked={isChecked}
disabled={isDisabled}
onChange={(e) => {
if (e.target.checked) {
if (selectedBikeIds.length < remainingCapacity) {
setSelectedBikeIds([...selectedBikeIds, bike.id]);
} else {
toast.error(`Cannot select more than ${remainingCapacity} bikes`);
}
} else {
setSelectedBikeIds(selectedBikeIds.filter(id => id !== bike.id));
}
}}
className="rounded text-investor focus:ring-investor border-slate-300 w-4 h-4"
/>
<div>
<p className="font-semibold text-slate-800">{bike.model} {bike.brand}</p>
<p className="text-xs text-slate-500">{bike.plateNumber}</p>
</div>
</div>
<span className="text-slate-600 font-medium text-xs">{bike.purchasePrice?.toLocaleString() || 0}</span>
</label>
);
})}
{availableBikes.length === 0 && (
<div className="p-4 text-center text-slate-400 text-sm">No unassigned available bikes found</div>
)}
</div>
)}
</div>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
<button
onClick={onClose}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 bg-white"
>
Cancel
</button>
<button
onClick={handleAssignSubmit}
disabled={!selectedPlanId || selectedBikeIds.length === 0}
className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-semibold hover:bg-investor-dark disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-colors"
>
Assign {selectedBikeIds.length > 0 ? `${selectedBikeIds.length} Bike(s)` : 'Bike'}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import { AlertTriangle } from 'lucide-react';
interface UnassignConfirmModalProps {
isOpen: boolean;
onClose: () => void;
type: 'bike' | 'battery';
name: string;
details: string;
onConfirm: () => void;
}
export default function UnassignConfirmModal({
isOpen,
onClose,
type,
name,
details,
onConfirm
}: UnassignConfirmModalProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-in fade-in zoom-in-95 duration-200">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-slate-100">
<div className="p-6 text-center">
<div className="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-red-100">
<AlertTriangle className="w-8 h-8 text-red-600 animate-pulse" />
</div>
<h3 className="text-lg font-bold text-slate-900 mb-2">Unassign Confirmation</h3>
<p className="text-sm text-slate-500 mb-6 px-1">
Are you sure you want to unassign {type} <span className="font-semibold text-slate-800">{name}</span> ({details})?
</p>
<div className="flex gap-3 justify-center">
<button
onClick={onClose}
className="px-5 py-2.5 border border-slate-200 text-slate-600 rounded-xl text-sm font-semibold hover:bg-slate-50 transition-colors flex-1"
>
Cancel
</button>
<button
onClick={() => {
onConfirm();
onClose();
}}
className="px-5 py-2.5 bg-red-600 text-white rounded-xl text-sm font-semibold hover:bg-red-700 hover:shadow-lg transition-all flex-1"
>
Unassign
</button>
</div>
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,541 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import {
Wrench, ArrowLeft, Battery, AlertTriangle, Calendar, DollarSign, Clock,
CheckCircle, XCircle, Search, RefreshCw, ChevronLeft, ChevronRight,
Eye, User, FileText, ArrowRight, ShieldAlert, Sparkles
} from 'lucide-react';
interface HistoryRecord {
id: string;
date: string;
type: 'damage' | 'maintenance' | 'service' | 'repair' | 'inspection' | 'battery_swap';
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
status: 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
description: string;
cost: number;
reporter: string;
resolvedAt?: string;
partsUsed?: string[];
}
export default function BatteryMaintenanceHistoryPage({ params }: { params: Promise<{ id: string }> }) {
const router = useRouter();
const searchParams = useSearchParams();
const batteryId = use(params).id;
const fromRecord = searchParams.get('from');
// Realistic mock data for a specific battery's maintenance and damage history
const [historyList] = useState<HistoryRecord[]>(() => {
return [
{
id: 'MNT-003',
date: '2024-03-19',
type: 'battery_swap',
severity: 'minor',
status: 'completed',
description: 'Battery not holding charge properly - swapped under warranty.',
cost: 0,
reporter: 'Jamal (Biker)',
resolvedAt: '2024-03-19',
partsUsed: []
},
{
id: 'MNT-009',
date: '2024-03-05',
type: 'repair',
severity: 'major',
status: 'completed',
description: 'Battery port connector pin replacement & calibration.',
cost: 1200,
reporter: 'Uttara Hub Staff',
resolvedAt: '2024-03-06',
partsUsed: ['Connector Pins', 'Silicone Seals']
},
{
id: 'MNT-015',
date: '2024-02-15',
type: 'service',
severity: 'minor',
status: 'completed',
description: 'Cell rebalancing and firmware upgrade for BMS.',
cost: 800,
reporter: 'Authorized Service Center',
resolvedAt: '2024-02-15',
partsUsed: []
},
{
id: 'MNT-020',
date: '2024-01-22',
type: 'damage',
severity: 'critical',
status: 'completed',
description: 'Cell thermal runaway inspection due to temperature alert.',
cost: 1500,
reporter: 'System Alert',
resolvedAt: '2024-01-24',
partsUsed: ['BMS Module']
},
{
id: 'MNT-025',
date: '2023-12-01',
type: 'inspection',
severity: 'cosmetic',
status: 'completed',
description: 'Outer plastic protective case scratch audit and hub cleanup.',
cost: 0,
reporter: 'Kamal Ahmed (Biker)',
resolvedAt: '2023-12-01',
partsUsed: []
}
];
});
// Client Side Filter & Sorting States
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'cost' | 'severity'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 5;
const handleSort = (field: 'date' | 'cost' | 'severity') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
};
// Logic
const filteredList = historyList.filter(item => {
if (typeFilter !== 'all') {
if (typeFilter === 'damage_all') {
if (item.type !== 'damage') return false;
} else if (typeFilter === 'damage_cosmetic') {
if (item.type !== 'damage' || item.severity !== 'cosmetic') return false;
} else if (typeFilter === 'damage_minor') {
if (item.type !== 'damage' || item.severity !== 'minor') return false;
} else if (typeFilter === 'damage_major') {
if (item.type !== 'damage' || item.severity !== 'major') return false;
} else if (typeFilter === 'damage_critical') {
if (item.type !== 'damage' || item.severity !== 'critical') return false;
} else if (typeFilter === 'maintenance_all') {
if (item.type === 'damage') return false;
} else if (typeFilter === 'maintenance_service') {
if (item.type !== 'service') return false;
} else if (typeFilter === 'maintenance_repair') {
if (item.type !== 'repair') return false;
} else if (typeFilter === 'maintenance_inspection') {
if (item.type !== 'inspection') return false;
} else if (typeFilter === 'maintenance_battery_swap') {
if (item.type !== 'battery_swap') return false;
}
}
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
if (searchQuery && !item.description.toLowerCase().includes(searchQuery.toLowerCase()) && !item.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(item.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(item.date) > new Date(dateTo)) return false;
return true;
});
const sortedList = [...filteredList].sort((a, b) => {
let comp = 0;
if (sortBy === 'date') {
comp = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'cost') {
comp = a.cost - b.cost;
} else if (sortBy === 'severity') {
const ranks = { critical: 4, major: 3, minor: 2, cosmetic: 1 };
comp = ranks[a.severity] - ranks[b.severity];
}
return sortOrder === 'desc' ? -comp : comp;
});
// Pagination
const totalPages = Math.ceil(sortedList.length / pageSize);
const paginatedList = sortedList.slice((page - 1) * pageSize, page * pageSize);
// Status/Severity Badge Colors
const severityColors = {
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 statusColors = {
reported: 'bg-amber-50 text-amber-700 border border-amber-200',
in_progress: 'bg-blue-50 text-blue-700 border border-blue-200',
parts_ordered: 'bg-purple-50 text-purple-700 border border-purple-200',
completed: 'bg-green-50 text-green-700 border border-green-200',
cancelled: 'bg-red-50 text-red-700 border border-red-200',
};
const totalCost = filteredList.reduce((sum, item) => sum + item.cost, 0);
const criticalCount = filteredList.filter(item => item.severity === 'critical' || item.severity === 'major').length;
return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
{/* Back navigation links */}
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => {
if (fromRecord) {
router.push(`/admin/maintenance/${fromRecord}`);
} else {
router.push('/admin/maintenance');
}
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors bg-white shadow-sm"
>
<ArrowLeft className="w-4 h-4 text-slate-600" />
</button>
<div>
<h1 className="text-2xl font-extrabold text-slate-800 flex items-center gap-2">
<Battery className="w-6 h-6 text-green-600" /> Battery History Ledger
</h1>
<p className="text-xs text-slate-500">
Viewing comprehensive damage & maintenance history for Battery <span className="font-semibold text-green-600">{batteryId}</span>
</p>
</div>
</div>
{/* Top Metrics Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<Wrench className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Events</p>
<p className="text-lg font-bold text-slate-800">{filteredList.length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<ShieldAlert className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Major / Critical</p>
<p className="text-lg font-bold text-red-600">{criticalCount}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-emerald-600" />
</div>
<div>
<p className="text-xs text-slate-500">Accumulated Cost</p>
<p className="text-lg font-bold text-emerald-600">{totalCost.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm bg-gradient-to-br from-teal-50 to-green-50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-teal-100 rounded-lg flex items-center justify-center shrink-0">
<Sparkles className="w-5 h-5 text-teal-600" />
</div>
<div>
<p className="text-xs text-teal-500">Health Status</p>
<p className="text-lg font-bold text-emerald-700">Excellent</p>
</div>
</div>
</div>
</div>
{/* Main Ledger Content */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
{/* Advanced Filters */}
<div className="p-4 border-b border-slate-100 bg-slate-50/30">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<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 description or reference..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
/>
</div>
<select
value={typeFilter}
onChange={(e) => { setTypeFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer font-medium"
>
<option value="all">All Types</option>
<optgroup label="Damage Levels">
<option value="damage_all">All Damages</option>
<option value="damage_cosmetic">Damage - Cosmetic</option>
<option value="damage_minor">Damage - Minor</option>
<option value="damage_major">Damage - Major</option>
<option value="damage_critical">Damage - Critical</option>
</optgroup>
<optgroup label="Maintenance Types">
<option value="maintenance_all">All Maintenance</option>
<option value="maintenance_service">Service</option>
<option value="maintenance_repair">Repair</option>
<option value="maintenance_inspection">Inspection</option>
<option value="maintenance_battery_swap">Battery Swap</option>
</optgroup>
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<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 className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo || searchQuery || typeFilter !== 'all' || statusFilter !== 'all') && (
<button
onClick={() => {
setSearchQuery('');
setTypeFilter('all');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setPage(1);
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset Filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
)}
</div>
</div>
</div>
{/* Desktop View */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Reference ID
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Type
</th>
<th
onClick={() => handleSort('severity')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Severity {sortBy === 'severity' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Description
</th>
<th
onClick={() => handleSort('cost')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
>
Cost {sortBy === 'cost' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider text-center">
Action
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
<td className="px-4 py-3.5 text-sm text-slate-600 font-medium">
{item.date}
</td>
<td className="px-4 py-3.5 text-xs font-mono font-semibold text-slate-400">
{item.id}
</td>
<td className="px-4 py-3.5 text-sm text-slate-700 capitalize font-medium">
{item.type.replace('_', ' ')}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-semibold ${severityColors[item.severity]}`}>
{item.severity}
</span>
</td>
<td className="px-4 py-3.5 text-sm text-slate-600 max-w-xs">
<p className="truncate" title={item.description}>{item.description}</p>
{item.partsUsed && item.partsUsed.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{item.partsUsed.map(p => (
<span key={p} className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.2 rounded">
{p}
</span>
))}
</div>
)}
</td>
<td className="px-4 py-3.5 text-sm font-bold text-slate-800 text-right">
{item.cost.toLocaleString()}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</td>
<td className="px-4 py-3.5 text-center">
<Link
href={`/admin/maintenance/${item.id}`}
className="inline-flex p-1.5 hover:bg-slate-100 rounded-lg text-slate-500 hover:text-slate-700 transition-colors"
>
<Eye className="w-4 h-4" />
</Link>
</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-400">
<AlertTriangle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p className="text-sm font-semibold">No maintenance logs found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Mobile View */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<div key={item.id} className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex justify-between items-start mb-2">
<div>
<span className="text-xs text-slate-400 font-mono font-semibold">{item.id}</span>
<h4 className="text-sm font-bold text-slate-800 capitalize mt-0.5">
{item.type.replace('_', ' ')}
</h4>
</div>
<div className="text-right">
<span className="text-sm font-extrabold text-slate-900">{item.cost.toLocaleString()}</span>
<span className={`block text-[10px] font-semibold mt-1 px-1.5 py-0.5 rounded capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</div>
</div>
<p className="text-xs text-slate-500 mb-2">{item.description}</p>
<div className="flex justify-between items-center text-[11px] text-slate-400">
<span>{item.date}</span>
<Link
href={`/admin/maintenance/${item.id}`}
className="text-accent font-semibold hover:underline flex items-center gap-0.5"
>
Details <ArrowRight className="w-3 h-3" />
</Link>
</div>
</div>
))
) : (
<div className="p-8 text-center text-slate-500">
<AlertTriangle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No maintenance logs found</p>
</div>
)}
</div>
{/* Footer Pagination */}
{sortedList.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedList.length)} of {sortedList.length} records
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronLeft className="w-4 h-4 text-slate-600" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${page === pageNum
? 'bg-accent text-white'
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronRight className="w-4 h-4 text-slate-600" />
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,537 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import {
Wrench, ArrowLeft, Bike, AlertTriangle, Calendar, DollarSign, Clock,
CheckCircle, XCircle, Search, RefreshCw, ChevronLeft, ChevronRight,
Eye, User, FileText, ArrowRight, ShieldAlert, Sparkles
} from 'lucide-react';
interface HistoryRecord {
id: string;
date: string;
type: 'damage' | 'maintenance' | 'service' | 'repair' | 'inspection';
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
status: 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
description: string;
cost: number;
reporter: string;
resolvedAt?: string;
partsUsed?: string[];
}
export default function BikeMaintenanceHistoryPage({ params }: { params: Promise<{ id: string }> }) {
const router = useRouter();
const searchParams = useSearchParams();
const bikeId = use(params).id;
const fromRecord = searchParams.get('from');
// Realistic mock data for a specific bike's maintenance and damage history
const [historyList] = useState<HistoryRecord[]>(() => {
return [
{
id: 'MNT-001',
date: '2024-03-21',
type: 'damage',
severity: 'major',
status: 'in_progress',
description: 'Front fender damaged in minor collision at Gulshan signal.',
cost: 3200,
reporter: 'Sofiq Rahman (Biker)',
partsUsed: ['Front fender', 'Mounting brackets']
},
{
id: 'MNT-005',
date: '2024-03-17',
type: 'inspection',
severity: 'minor',
status: 'completed',
description: 'Monthly scheduled routine vehicle inspection.',
cost: 250,
reporter: 'Gulshan Hub Staff',
resolvedAt: '2024-03-17',
partsUsed: []
},
{
id: 'MNT-012',
date: '2024-02-10',
type: 'repair',
severity: 'critical',
status: 'completed',
description: 'Motor controller overheating check & throttle replacement.',
cost: 7500,
reporter: 'System Alert',
resolvedAt: '2024-02-12',
partsUsed: ['Throttle Assembly', 'Controller Fan']
},
{
id: 'MNT-018',
date: '2024-01-15',
type: 'service',
severity: 'minor',
status: 'completed',
description: 'Chain lubrication, brake shoe calibration, and mirror tightening.',
cost: 600,
reporter: 'Kamal Ahmed (Biker)',
resolvedAt: '2024-01-15',
partsUsed: ['Brake Shoe Set']
},
{
id: 'MNT-022',
date: '2023-12-05',
type: 'damage',
severity: 'cosmetic',
status: 'completed',
description: 'Side mirror cracked due to parking slip.',
cost: 800,
reporter: 'Kamal Ahmed (Biker)',
resolvedAt: '2023-12-06',
partsUsed: ['Left Side Mirror']
}
];
});
// Client Side Filter & Sorting States
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'cost' | 'severity'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 5;
const handleSort = (field: 'date' | 'cost' | 'severity') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
};
// Logic
const filteredList = historyList.filter(item => {
if (typeFilter !== 'all') {
if (typeFilter === 'damage_all') {
if (item.type !== 'damage') return false;
} else if (typeFilter === 'damage_cosmetic') {
if (item.type !== 'damage' || item.severity !== 'cosmetic') return false;
} else if (typeFilter === 'damage_minor') {
if (item.type !== 'damage' || item.severity !== 'minor') return false;
} else if (typeFilter === 'damage_major') {
if (item.type !== 'damage' || item.severity !== 'major') return false;
} else if (typeFilter === 'damage_critical') {
if (item.type !== 'damage' || item.severity !== 'critical') return false;
} else if (typeFilter === 'maintenance_all') {
if (item.type === 'damage') return false;
} else if (typeFilter === 'maintenance_service') {
if (item.type !== 'service') return false;
} else if (typeFilter === 'maintenance_repair') {
if (item.type !== 'repair') return false;
} else if (typeFilter === 'maintenance_inspection') {
if (item.type !== 'inspection') return false;
}
}
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
if (searchQuery && !item.description.toLowerCase().includes(searchQuery.toLowerCase()) && !item.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(item.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(item.date) > new Date(dateTo)) return false;
return true;
});
const sortedList = [...filteredList].sort((a, b) => {
let comp = 0;
if (sortBy === 'date') {
comp = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'cost') {
comp = a.cost - b.cost;
} else if (sortBy === 'severity') {
const ranks = { critical: 4, major: 3, minor: 2, cosmetic: 1 };
comp = ranks[a.severity] - ranks[b.severity];
}
return sortOrder === 'desc' ? -comp : comp;
});
// Pagination
const totalPages = Math.ceil(sortedList.length / pageSize);
const paginatedList = sortedList.slice((page - 1) * pageSize, page * pageSize);
// Status/Severity Badge Colors
const severityColors = {
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 statusColors = {
reported: 'bg-amber-50 text-amber-700 border border-amber-200',
in_progress: 'bg-blue-50 text-blue-700 border border-blue-200',
parts_ordered: 'bg-purple-50 text-purple-700 border border-purple-200',
completed: 'bg-green-50 text-green-700 border border-green-200',
cancelled: 'bg-red-50 text-red-700 border border-red-200',
};
const totalCost = filteredList.reduce((sum, item) => sum + item.cost, 0);
const criticalCount = filteredList.filter(item => item.severity === 'critical' || item.severity === 'major').length;
return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
{/* Back navigation links */}
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => {
if (fromRecord) {
router.push(`/admin/maintenance/${fromRecord}`);
} else {
router.push('/admin/maintenance');
}
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors bg-white shadow-sm"
>
<ArrowLeft className="w-4 h-4 text-slate-600" />
</button>
<div>
<h1 className="text-2xl font-extrabold text-slate-800 flex items-center gap-2">
<Bike className="w-6 h-6 text-purple-600" /> History Ledger
</h1>
<p className="text-xs text-slate-500">
Viewing comprehensive damage & maintenance history for Bike <span className="font-semibold text-purple-600">{bikeId}</span>
</p>
</div>
</div>
{/* Top Metrics Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center shrink-0">
<Wrench className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Events</p>
<p className="text-lg font-bold text-slate-800">{filteredList.length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<ShieldAlert className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Major / Critical</p>
<p className="text-lg font-bold text-red-600">{criticalCount}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Accumulated Cost</p>
<p className="text-lg font-bold text-green-600">{totalCost.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm bg-gradient-to-br from-indigo-50 to-purple-50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center shrink-0">
<Sparkles className="w-5 h-5 text-indigo-600" />
</div>
<div>
<p className="text-xs text-indigo-500">Service Status</p>
<p className="text-lg font-bold text-indigo-700">Healthy</p>
</div>
</div>
</div>
</div>
{/* Main Ledger Content */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
{/* Advanced Filters */}
<div className="p-4 border-b border-slate-100 bg-slate-50/30">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<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 description or reference..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
/>
</div>
<select
value={typeFilter}
onChange={(e) => { setTypeFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer font-medium"
>
<option value="all">All Types</option>
<optgroup label="Damage Levels">
<option value="damage_all">All Damages</option>
<option value="damage_cosmetic">Damage - Cosmetic</option>
<option value="damage_minor">Damage - Minor</option>
<option value="damage_major">Damage - Major</option>
<option value="damage_critical">Damage - Critical</option>
</optgroup>
<optgroup label="Maintenance Types">
<option value="maintenance_all">All Maintenance</option>
<option value="maintenance_service">Service</option>
<option value="maintenance_repair">Repair</option>
<option value="maintenance_inspection">Inspection</option>
</optgroup>
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<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 className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo || searchQuery || typeFilter !== 'all' || statusFilter !== 'all') && (
<button
onClick={() => {
setSearchQuery('');
setTypeFilter('all');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setPage(1);
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset Filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
)}
</div>
</div>
</div>
{/* Desktop View */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Reference ID
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Type
</th>
<th
onClick={() => handleSort('severity')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Severity {sortBy === 'severity' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Description
</th>
<th
onClick={() => handleSort('cost')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
>
Cost {sortBy === 'cost' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider text-center">
Action
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
<td className="px-4 py-3.5 text-sm text-slate-600 font-medium">
{item.date}
</td>
<td className="px-4 py-3.5 text-xs font-mono font-semibold text-slate-400">
{item.id}
</td>
<td className="px-4 py-3.5 text-sm text-slate-700 capitalize font-medium">
{item.type.replace('_', ' ')}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-semibold ${severityColors[item.severity]}`}>
{item.severity}
</span>
</td>
<td className="px-4 py-3.5 text-sm text-slate-600 max-w-xs">
<p className="truncate" title={item.description}>{item.description}</p>
{item.partsUsed && item.partsUsed.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{item.partsUsed.map(p => (
<span key={p} className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.2 rounded">
{p}
</span>
))}
</div>
)}
</td>
<td className="px-4 py-3.5 text-sm font-bold text-slate-800 text-right">
{item.cost.toLocaleString()}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</td>
<td className="px-4 py-3.5 text-center">
<Link
href={`/admin/maintenance/${item.id}`}
className="inline-flex p-1.5 hover:bg-slate-100 rounded-lg text-slate-500 hover:text-slate-700 transition-colors"
>
<Eye className="w-4 h-4" />
</Link>
</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-400">
<AlertTriangle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p className="text-sm font-semibold">No maintenance logs found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Mobile View */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<div key={item.id} className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex justify-between items-start mb-2">
<div>
<span className="text-xs text-slate-400 font-mono font-semibold">{item.id}</span>
<h4 className="text-sm font-bold text-slate-800 capitalize mt-0.5">
{item.type.replace('_', ' ')}
</h4>
</div>
<div className="text-right">
<span className="text-sm font-extrabold text-slate-900">{item.cost.toLocaleString()}</span>
<span className={`block text-[10px] font-semibold mt-1 px-1.5 py-0.5 rounded capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</div>
</div>
<p className="text-xs text-slate-500 mb-2">{item.description}</p>
<div className="flex justify-between items-center text-[11px] text-slate-400">
<span>{item.date}</span>
<Link
href={`/admin/maintenance/${item.id}`}
className="text-accent font-semibold hover:underline flex items-center gap-0.5"
>
Details <ArrowRight className="w-3 h-3" />
</Link>
</div>
</div>
))
) : (
<div className="p-8 text-center text-slate-500">
<AlertTriangle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No maintenance logs found</p>
</div>
)}
</div>
{/* Footer Pagination */}
{sortedList.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedList.length)} of {sortedList.length} records
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronLeft className="w-4 h-4 text-slate-600" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${page === pageNum
? 'bg-accent text-white'
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronRight className="w-4 h-4 text-slate-600" />
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,13 +1,14 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { import {
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone, AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key, MapPin, FileText, Image as ImageIcon, DollarSign, Wrench, Battery, Key,
CheckCircle, XCircle, ChevronDown, ChevronUp, Download, Eye, Edit, CheckCircle, XCircle, ChevronDown, ChevronUp, Download, Eye, Edit,
MessageSquare, Filter, Calendar, Save, Printer, Send MessageSquare, Filter, Calendar, Save, Printer, Send, Activity
} from 'lucide-react'; } 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 TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense';
@@ -245,7 +246,9 @@ const typeIcons: Record<string, any> = {
}; };
export default function MaintenancePage() { export default function MaintenancePage() {
const [activeTab, setActiveTab] = useState<'all' | MaintenanceType>('all'); const router = useRouter();
const [mainCategory, setMainCategory] = useState<'damage' | 'maintenance'>('damage');
const [targetType, setTargetType] = useState<'all' | 'battery' | 'fleet'>('all');
const [records, setRecords] = useState<MaintenanceRecord[]>(mockMaintenance); const [records, setRecords] = useState<MaintenanceRecord[]>(mockMaintenance);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
@@ -259,20 +262,45 @@ export default function MaintenancePage() {
const [expandedNotes, setExpandedNotes] = useState<string[]>([]); const [expandedNotes, setExpandedNotes] = useState<string[]>([]);
const [newNoteText, setNewNoteText] = useState(''); const [newNoteText, setNewNoteText] = useState('');
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({}); const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
const [reportType, setReportType] = useState<'damage' | 'maintenance'>('damage');
const [showSuccessModal, setShowSuccessModal] = useState(false);
const filteredRecords = records.filter(r => { const filteredRecords = records.filter(r => {
const matchesTab = activeTab === 'all' || r.type === activeTab; const isDamage = r.type === 'damage';
const matchesCategory = mainCategory === 'damage' ? isDamage : !isDamage;
const matchesTarget = targetType === 'all' ||
(targetType === 'battery' && r.batteryId) ||
(targetType === 'fleet' && r.bikeId && !r.batteryId);
const matchesSearch = !searchQuery || const matchesSearch = !searchQuery ||
r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) || r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) || r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) || r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) || r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.id.toLowerCase().includes(searchQuery.toLowerCase()); r.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
(r.batteryId && r.batteryId.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesStatus = statusFilter === 'all' || r.status === statusFilter; const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
return matchesTab && matchesSearch && matchesStatus; return matchesCategory && matchesTarget && matchesSearch && matchesStatus;
}); });
const damageRecords = records.filter(r => r.type === 'damage');
const maintenanceRecords = records.filter(r => r.type !== 'damage');
const currentMonth = new Date().toISOString().slice(0, 7);
const stats = { const stats = {
damageCount: damageRecords.length,
maintenanceCount: maintenanceRecords.length,
damageThisMonth: damageRecords.filter(r => r.date?.slice(0, 7) === currentMonth).length,
maintenanceThisMonth: maintenanceRecords.filter(r => r.date?.slice(0, 7) === currentMonth).length,
completedThisMonth: records.filter(r => r.status === 'completed' && r.resolvedAt?.slice(0, 7) === currentMonth).length,
batteryDamage: damageRecords.filter(r => r.batteryId).length,
fleetDamage: damageRecords.filter(r => r.bikeId && !r.batteryId).length,
batteryMaintenance: maintenanceRecords.filter(r => r.batteryId).length,
fleetMaintenance: maintenanceRecords.filter(r => r.bikeId && !r.batteryId).length,
upcomingBattery: maintenanceRecords.filter(r => r.batteryId && r.status === 'reported').length,
upcomingFleet: maintenanceRecords.filter(r => r.bikeId && !r.batteryId && r.status === 'reported').length,
ongoingBattery: maintenanceRecords.filter(r => r.batteryId && r.status === 'in_progress').length,
ongoingFleet: maintenanceRecords.filter(r => r.bikeId && !r.batteryId && r.status === 'in_progress').length,
pendingMaintenance: maintenanceRecords.filter(r => r.status === 'reported' || r.status === 'in_progress').length,
completedMaintenance: maintenanceRecords.filter(r => r.status === 'completed').length,
critical: records.filter(r => r.severity === 'critical' && r.status !== 'completed').length, critical: records.filter(r => r.severity === 'critical' && r.status !== 'completed').length,
inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length, inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length,
completed: records.filter(r => r.status === 'completed').length, completed: records.filter(r => r.status === 'completed').length,
@@ -397,7 +425,7 @@ export default function MaintenancePage() {
}; };
return ( return (
<div className="p-4 lg:p-6"> <div className="p-4 lg:p-6 mb-6 lg:mb-0">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6"> <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div> <div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Damage & Maintenance</h1> <h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Damage & Maintenance</h1>
@@ -413,36 +441,45 @@ export default function MaintenancePage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-red-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center"> <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" /> <AlertTriangle className="w-6 h-6 text-red-600" />
</div> </div>
<div> <div className="flex-1">
<p className="text-2xl font-extrabold text-slate-800">{stats.critical}</p> <div className="flex items-baseline justify-between gap-2">
<p className="text-sm text-slate-500">Critical</p> <p className="text-2xl font-extrabold text-slate-800">{stats.damageCount}</p>
<span className="text-xs text-red-600 font-medium bg-red-50 px-2 py-0.5 rounded">{stats.damageThisMonth} this month</span>
</div>
<p className="text-sm text-slate-500">Total Damage</p>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-blue-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center"> <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" /> <Wrench className="w-6 h-6 text-blue-600" />
</div> </div>
<div> <div className="flex-1">
<p className="text-2xl font-extrabold text-slate-800">{stats.inProgress}</p> <div className="flex items-baseline justify-between gap-2">
<p className="text-sm text-slate-500">In Progress</p> <p className="text-2xl font-extrabold text-slate-800">{stats.maintenanceCount}</p>
<span className="text-xs text-blue-600 font-medium bg-blue-50 px-2 py-0.5 rounded">{stats.maintenanceThisMonth} this month</span>
</div>
<p className="text-sm text-slate-500">Total Maintenance</p>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-green-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center"> <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" /> <CheckCircle className="w-6 h-6 text-green-600" />
</div> </div>
<div> <div className="flex-1">
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p> <div className="flex items-baseline justify-between gap-2">
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p>
<span className="text-xs text-green-600 font-medium bg-green-50 px-2 py-0.5 rounded">{stats.completedThisMonth} this month</span>
</div>
<p className="text-sm text-slate-500">Completed</p> <p className="text-sm text-slate-500">Completed</p>
</div> </div>
</div> </div>
@@ -458,7 +495,7 @@ export default function MaintenancePage() {
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> {/* <div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center"> <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" /> <DollarSign className="w-6 h-6 text-purple-600" />
@@ -468,43 +505,145 @@ export default function MaintenancePage() {
<p className="text-sm text-slate-500">Total Cost</p> <p className="text-sm text-slate-500">Total Cost</p>
</div> </div>
</div> </div>
</div> </div> */}
</div> </div>
{mainCategory === 'damage' && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-red-50 rounded-xl p-4 border border-red-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.damageCount}</p>
<p className="text-sm text-slate-500">Total Damage</p>
</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<Battery className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.batteryDamage}</p>
<p className="text-sm text-slate-500">Battery Damage</p>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<Bike className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.fleetDamage}</p>
<p className="text-sm text-slate-500">Fleet Damage</p>
</div>
</div>
</div>
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Clock className="w-5 h-5 text-orange-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.critical}</p>
<p className="text-sm text-slate-500">Critical Damage</p>
</div>
</div>
</div>
</div>
)}
{mainCategory === 'maintenance' && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-blue-50 rounded-xl p-4 border border-blue-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
<Wrench className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.maintenanceCount}</p>
<p className="text-sm text-slate-500">Total Maintenance</p>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<Bike className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.upcomingFleet}</p>
<p className="text-sm text-slate-500">Upcoming Fleet</p>
</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<Battery className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.upcomingBattery}</p>
<p className="text-sm text-slate-500">Upcoming Battery</p>
</div>
</div>
</div>
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Activity className="w-5 h-5 text-orange-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.ongoingBattery + stats.ongoingFleet}</p>
<p className="text-sm text-slate-500">Ongoing Maintenance</p>
</div>
</div>
</div>
</div>
)}
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6"> <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="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center gap-4"> <div className="flex flex-col lg:flex-row lg:items-center gap-4">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<button <div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg">
onClick={() => setActiveTab('all')} <button
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'}`} onClick={() => setMainCategory('damage')}
> className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 ${mainCategory === 'damage' ? 'bg-red-600 text-white shadow-sm' : 'text-slate-600 hover:bg-white hover:shadow-sm'}`}
All >
</button> <AlertTriangle className="w-4 h-4" /> Damage
<button </button>
onClick={() => setActiveTab('damage')} <button
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'}`} onClick={() => setMainCategory('maintenance')}
> className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 ${mainCategory === 'maintenance' ? 'bg-blue-600 text-white shadow-sm' : 'text-slate-600 hover:bg-white hover:shadow-sm'}`}
<AlertTriangle className="w-4 h-4" /> Damage >
</button> <Wrench className="w-4 h-4" /> Maintenance
<button </button>
onClick={() => setActiveTab('repair')} </div>
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'}`} <div className="flex items-center gap-1">
> <button
<Wrench className="w-4 h-4" /> Repair onClick={() => setTargetType('all')}
</button> className={`px-3 py-1.5 rounded-lg text-sm font-medium ${targetType === 'all' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
<button >
onClick={() => setActiveTab('service')} All
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'}`} </button>
> <button
<Wrench className="w-4 h-4" /> Service onClick={() => setTargetType('battery')}
</button> className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${targetType === 'battery' ? 'bg-green-600 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
<button >
onClick={() => setActiveTab('battery_swap')} <Battery className="w-4 h-4" /> Battery
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'}`} </button>
> <button
<Battery className="w-4 h-4" /> Battery onClick={() => setTargetType('fleet')}
</button> className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${targetType === 'fleet' ? 'bg-purple-600 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
>
<Bike className="w-4 h-4" /> Fleet
</button>
</div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="relative"> <div className="relative">
@@ -537,47 +676,59 @@ export default function MaintenancePage() {
{filteredRecords.map(record => { {filteredRecords.map(record => {
const TypeIcon = typeIcons[record.type]; const TypeIcon = typeIcons[record.type];
return ( return (
<Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-5 hover:bg-slate-50 transition-colors"> <Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-4 lg:p-5 hover:bg-slate-50 transition-colors">
<div className="flex flex-col lg:flex-row lg:items-start gap-4"> <div className="flex flex-col lg:flex-row lg:items-start gap-3 lg:gap-4">
<div className="flex items-center gap-4"> <div className="flex items-start gap-3">
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center"> <div className="w-10 h-10 lg:w-12 lg:h-12 rounded-lg lg:rounded-xl bg-slate-100 flex items-center justify-center flex-shrink-0">
<TypeIcon className="w-6 h-6 text-slate-600" /> <TypeIcon className="w-5 h-5 lg:w-6 lg:h-6 text-slate-600" />
</div> </div>
<div> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-1.5 lg:gap-2">
<p className="font-semibold text-slate-800">{record.id}</p> <p className="font-semibold text-slate-800 text-sm lg:text-base">{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.batteryId && (
<span className="inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full bg-green-100 text-green-700">
<Battery className="w-3 h-3" /> Battery
</span>
)}
{!record.batteryId && record.bikeId && (
<span className="inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-700">
<Bike className="w-3 h-3" /> Fleet
</span>
)}
<span className={`inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full ${severityColors[record.severity]}`}>
{record.severity} {record.severity}
</span> </span>
</div> </div>
<p className="text-sm text-slate-500 flex items-center gap-2"> <p className="text-xs lg:text-sm text-slate-500 flex flex-wrap items-center gap-x-1 lg:gap-x-2">
<Bike className="w-3 h-3" /> {record.bikeModel} ({record.bikePlate}) <span className="flex items-center gap-1"><Bike className="w-3 h-3" /> {record.bikeModel}</span>
<span className="text-slate-300">|</span> <span className="hidden sm:inline text-slate-300">|</span>
<User className="w-3 h-3" /> {record.reporterName} <span className="text-xs">{record.bikePlate}</span>
<span className="hidden lg:inline text-slate-300">|</span>
<span className="flex items-center gap-1"><User className="w-3 h-3" /> {record.reporterName}</span>
</p> </p>
</div> </div>
</div> </div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<p className="text-sm text-slate-700">{record.description}</p> <p className="text-sm text-slate-700 line-clamp-2">{record.description}</p>
<div className="flex flex-wrap gap-4 text-sm text-slate-500 mt-1"> <div className="flex flex-wrap gap-2 lg:gap-4 text-xs lg:text-sm text-slate-500 mt-1.5">
<p className="flex items-center gap-1"> <p className="flex items-center gap-1">
<Clock className="w-3 h-3" /> {record.date} <Clock className="w-3 h-3" /> {record.date}
</p> </p>
<p className="flex items-center gap-1"> <p className="flex items-center gap-1 truncate max-w-[100px] lg:max-w-none">
<MapPin className="w-3 h-3" /> {record.location} <MapPin className="w-3 h-3 flex-shrink-0" /> <span className="truncate">{record.location}</span>
</p> </p>
{record.images.length > 0 && ( {record.images.length > 0 && (
<p className="flex items-center gap-1 text-blue-600"> <p className="flex items-center gap-1 text-blue-600">
<Image className="w-3 h-3" /> {record.images.length} photos <ImageIcon className="w-3 h-3" /> {record.images.length}
</p> </p>
)} )}
{record.notes.length > 0 && ( {record.notes.length > 0 && (
<button <button
onClick={() => toggleNotes(record.id)} onClick={(e) => { e.preventDefault(); toggleNotes(record.id); }}
className="flex items-center gap-1 text-purple-600" className="flex items-center gap-1 text-purple-600"
> >
<MessageSquare className="w-3 h-3" /> {record.notes.length} notes <MessageSquare className="w-3 h-3" /> {record.notes.length}
{expandedNotes.includes(record.id) ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />} {expandedNotes.includes(record.id) ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</button> </button>
)} )}
@@ -592,24 +743,24 @@ export default function MaintenancePage() {
)} )}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center justify-between lg:justify-end gap-2 lg:gap-3 mt-2 lg:mt-0">
<div className="text-right"> <div className="text-left lg:text-right">
<p className="text-sm font-medium text-slate-700">{record.actualCost || record.estimatedCost}</p> <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> <p className="text-xs text-slate-500">{record.paymentStatus === 'paid' ? 'Paid' : record.paymentStatus === 'approved' ? 'Approved' : 'Payment ' + record.paymentStatus}</p>
</div> </div>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[record.status]}`}> <div className="flex items-center gap-1.5 lg:gap-2">
{record.status === 'reported' && <Clock className="w-3 h-3" />} <span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${statusColors[record.status]}`}>
{record.status === 'in_progress' && <Wrench className="w-3 h-3" />} {record.status === 'reported' && <Clock className="w-3 h-3" />}
{record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />} {record.status === 'in_progress' && <Wrench className="w-3 h-3" />}
{record.status === 'completed' && <CheckCircle className="w-3 h-3" />} {record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />}
{record.status === 'cancelled' && <XCircle className="w-3 h-3" />} {record.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{record.status.replace('_', ' ')} {record.status === 'cancelled' && <XCircle className="w-3 h-3" />}
</span> <span className="hidden sm:inline">{record.status.replace('_', ' ')}</span>
</span>
<div className="flex gap-1">
<button <button
onClick={(e) => { e.preventDefault(); setSelectedRecord(record); setShowDetailsModal(true); }} onClick={(e) => { e.preventDefault(); router.push(`/admin/maintenance/${record.id}`); }}
className="p-2 hover:bg-slate-100 rounded-lg text-slate-500" className="p-1.5 lg:p-2 hover:bg-slate-100 rounded-lg text-slate-500"
title="View Details" title="View Details"
> >
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
@@ -714,7 +865,7 @@ export default function MaintenancePage() {
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
{selectedRecord.images.map((img) => ( {selectedRecord.images.map((img) => (
<div key={img.id} className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center"> <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" /> <ImageIcon className="w-8 h-8 text-slate-400" />
<span className="text-xs text-slate-500 mt-1">{img.name}</span> <span className="text-xs text-slate-500 mt-1">{img.name}</span>
</div> </div>
))} ))}
@@ -794,53 +945,146 @@ export default function MaintenancePage() {
</button> </button>
</div> </div>
<div className="p-4 overflow-y-auto max-h-[70vh] space-y-4"> <div className="p-4 overflow-y-auto max-h-[70vh] space-y-4">
<div>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Report Type *</label>
<div className="flex gap-2 mb-4">
<button
type="button"
onClick={() => setReportType('damage')}
className={`flex-1 py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-medium transition-colors ${reportType === 'damage' ? 'border-red-500 bg-red-50 text-red-700' : 'border-slate-200 text-slate-600 hover:border-red-300'}`}
>
<AlertTriangle className="w-5 h-5" /> Damage
</button>
<button
type="button"
onClick={() => setReportType('maintenance')}
className={`flex-1 py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-medium transition-colors ${reportType === 'maintenance' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-600 hover:border-blue-300'}`}
>
<Wrench className="w-5 h-5" /> Maintenance
</button>
</div>
</div>
<div>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Select Target *</label>
<div className="grid grid-cols-2 gap-3 mb-4">
<button
type="button"
className="p-4 border-2 rounded-xl flex flex-col items-center gap-2 hover:border-green-500 hover:bg-green-50 transition-colors"
>
<Battery className="w-8 h-8 text-green-600" />
<span className="font-medium text-slate-700">Battery</span>
<span className="text-xs text-slate-500">For battery issues</span>
</button>
<button
type="button"
className="p-4 border-2 rounded-xl flex flex-col items-center gap-2 hover:border-purple-500 hover:bg-purple-50 transition-colors"
>
<Bike className="w-8 h-8 text-purple-600" />
<span className="font-medium text-slate-700">Fleet (Bike)</span>
<span className="text-xs text-slate-500">For bike issues</span>
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Issue Type *</label> <label className="text-xs font-medium text-slate-600 mb-1 block">Category *</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"> <select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="damage">Damage</option> <option value="">Select Category</option>
<option value="repair">Repair</option> {reportType === 'damage' ? (
<option value="service">Service</option> <>
<option value="battery_swap">Battery Swap</option> <option value="damage">Damage</option>
<option value="inspection">Inspection</option> <option value="repair">Repair</option>
<option value="other">Other</option> <option value="accident">Accident</option>
<option value="theft">Theft</option>
<option value="vandalism">Vandalism</option>
</>
) : (
<>
<option value="service">Service</option>
<option value="routine_service">Routine Service</option>
<option value="repair">Repair</option>
<option value="battery_swap">Battery Swap</option>
<option value="inspection">Inspection</option>
</>
)}
</select> </select>
</div> </div>
<div> {reportType === 'damage' ? (
<label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label> <div>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"> <label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label>
<option value="critical">Critical</option> <select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="major">Major</option> <option value="critical">Critical</option>
<option value="minor">Minor</option> <option value="major">Major</option>
<option value="cosmetic">Cosmetic</option> <option value="minor">Minor</option>
</select> <option value="cosmetic">Cosmetic</option>
</div> </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">Status</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
)}
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID</label> <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" /> <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">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>
<div className="grid grid-cols-2 gap-4">
{reportType === 'damage' ? (
<>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Reporter Name *</label>
<input type="text" placeholder="Enter name" 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">Reporter Phone *</label>
<input type="tel" placeholder="01XXXXXXXXX" 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">Performed By</label>
<input type="text" placeholder="Technician name" 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">Next Due Date</label>
<input type="date" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</>
)}
</div> </div>
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Description *</label> <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..." /> <textarea rows={3} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Describe the issue in detail..." />
</div> </div>
<div> <div className="grid grid-cols-2 gap-4">
<label className="text-xs font-medium text-slate-600 mb-1 block">Location *</label> <div>
<input type="text" placeholder="Where did the issue occur?" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" /> <label className="text-xs font-medium text-slate-600 mb-1 block">Location / Hub *</label>
</div> <input type="text" placeholder="e.g., Gulshan Hub" 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> <div>
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" /> <label className="text-xs font-medium text-slate-600 mb-1 block">
{reportType === 'damage' ? 'Estimated Cost (৳)' : 'Service Cost (৳)'}
</label>
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div> </div>
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Upload Images</label> <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"> <div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-accent cursor-pointer">
<Image className="w-8 h-8 text-slate-300 mx-auto mb-2" /> <ImageIcon 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> <p className="text-sm text-slate-500">Click to upload images</p>
<p className="text-xs text-slate-400">JPG, PNG up to 5MB</p>
</div> </div>
</div> </div>
</div> </div>
@@ -848,7 +1092,7 @@ export default function MaintenancePage() {
<button onClick={() => setShowNewModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50"> <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 Cancel
</button> </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"> <button onClick={() => { setShowNewModal(false); setShowSuccessModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark">
Submit Report Submit Report
</button> </button>
</div> </div>
@@ -953,6 +1197,24 @@ export default function MaintenancePage() {
</div> </div>
</div> </div>
)} )}
{showSuccessModal && (
<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 p-6 text-center">
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-xl font-bold text-slate-800 mb-2">Issue Reported Successfully!</h3>
<p className="text-slate-500 mb-6">Your issue has been submitted and will be reviewed shortly.</p>
<button
onClick={() => setShowSuccessModal(false)}
className="px-6 py-2 bg-accent text-white rounded-lg hover:bg-accent-dark"
>
OK
</button>
</div>
</div>
)}
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ import Link from 'next/link';
import { import {
ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle, ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle,
Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin, Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin,
Phone, MessageCircle, Play, Check, X, FileText, Download Phone, MessageCircle, Play, Check, X, FileText, Download, Battery, Printer
} from 'lucide-react'; } from 'lucide-react';
import { import {
canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit, canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit,
@@ -41,6 +41,9 @@ interface Rental {
bikeModel: string; bikeModel: string;
bikePlate: string; bikePlate: string;
bikeBattery: number; bikeBattery: number;
batteryId?: string;
batteryName?: string;
batteryRent?: number;
type: RentalType; type: RentalType;
status: RentalStatus; status: RentalStatus;
startDate: string; startDate: string;
@@ -73,6 +76,43 @@ interface Rental {
activatedAt?: string; activatedAt?: string;
lockHistory?: LockEvent[]; lockHistory?: LockEvent[];
paymentHistory?: PaymentHistory[]; paymentHistory?: PaymentHistory[];
batteryHistory?: BatteryRentalHistory[];
}
interface BatteryRentalHistory {
id: string;
batteryId: string;
batteryName: string;
assignedAt: string;
returnedAt?: string;
monthlyRent: number;
deposit: number;
depositMethod: 'cash' | 'bank' | 'bkash' | 'nagad';
invoiceId: string;
invoiceGeneratedAt: string;
status: 'active' | 'returned';
}
interface Invoice {
id: string;
type: 'ev_rental' | 'battery_rental';
relatedId: string; // rental id or battery history id
amount: number;
deposit?: number;
generatedAt: string;
status: 'paid' | 'unpaid';
description: string;
}
interface JournalEntry {
id: string;
date: string;
description: string;
debit: string;
credit: string;
amount: number;
referenceId: string;
type: 'battery_deposit' | 'battery_rent' | 'ev_deposit' | 'ev_rent';
} }
interface LockEvent { interface LockEvent {
@@ -114,6 +154,7 @@ const mockRentals: Rental[] = [
dailyRate: 150, dailyRate: 150,
weeklyRate: 900, weeklyRate: 900,
monthlyRate: 3500, monthlyRate: 3500,
batteryRent: 1500,
totalPaid: 38500, totalPaid: 38500,
dueRental: 0, dueRental: 0,
pendingRent: 0, pendingRent: 0,
@@ -146,6 +187,10 @@ const mockRentals: Rental[] = [
{ id: 'lh3', action: 'locked', reason: 'Second payment overdue - Day 2 penalty', performedBy: 'System', performedAt: '2024-03-01' }, { id: 'lh3', action: 'locked', reason: 'Second payment overdue - Day 2 penalty', performedBy: 'System', performedAt: '2024-03-01' },
{ id: 'lh4', action: 'unlocked', reason: 'Payment cleared', performedBy: 'Admin Manager', performedAt: '2024-03-02' }, { id: 'lh4', action: 'unlocked', reason: 'Payment cleared', performedBy: 'Admin Manager', performedAt: '2024-03-02' },
], ],
batteryHistory: [
{ id: 'BAT-RENT-001', batteryId: 'BAT-DH-001', batteryName: 'Galaxy 72V 45Ah', assignedAt: '2024-01-16', monthlyRent: 1500, deposit: 3000, depositMethod: 'bkash' as const, invoiceId: 'INV-BAT-001', invoiceGeneratedAt: '2024-01-16', status: 'active' as const },
{ id: 'BAT-RENT-002', batteryId: 'BAT-DH-002', batteryName: 'Titan 72V 50Ah', assignedAt: '2024-02-20', returnedAt: '2024-03-15', monthlyRent: 1800, deposit: 3500, depositMethod: 'cash' as const, invoiceId: 'INV-BAT-002', invoiceGeneratedAt: '2024-02-20', status: 'returned' as const },
],
}, },
{ {
id: 'RNT-002', id: 'RNT-002',
@@ -269,6 +314,23 @@ const mockHubs = [
{ id: 'HUB-004', name: 'Mirpur Hub' }, { id: 'HUB-004', name: 'Mirpur Hub' },
]; ];
interface BatteryOption {
id: string;
brand: string;
model: string;
soc: number;
monthlyRent: number;
status: 'available' | 'in-use';
}
const mockBatteries: BatteryOption[] = [
{ id: 'BAT-DH-001', brand: 'Galaxy', model: '72V 45Ah', soc: 85, monthlyRent: 1500, status: 'available' },
{ id: 'BAT-DH-002', brand: 'Titan', model: '72V 50Ah', soc: 92, monthlyRent: 1800, status: 'available' },
{ id: 'BAT-DH-003', brand: 'PowerMax', model: '60V 40Ah', soc: 78, monthlyRent: 1200, status: 'available' },
{ id: 'BAT-DH-004', brand: 'UltraCell', model: '72V 55Ah', soc: 88, monthlyRent: 2000, status: 'available' },
{ id: 'BAT-DH-005', brand: 'EcoVolt', model: '48V 30Ah', soc: 65, monthlyRent: 800, status: 'available' },
];
const mockDamageHistory = [ const mockDamageHistory = [
{ id: 'DMG-001', date: '2024-02-10', description: 'Minor scratch on left mirror', severity: 'minor', status: 'resolved' }, { id: 'DMG-001', date: '2024-02-10', description: 'Minor scratch on left mirror', severity: 'minor', status: 'resolved' },
{ id: 'DMG-002', date: '2024-03-05', description: 'Front fender dented', severity: 'moderate', status: 'reported' }, { id: 'DMG-002', date: '2024-03-05', description: 'Front fender dented', severity: 'moderate', status: 'reported' },
@@ -316,6 +378,23 @@ export default function RentalDetailPage() {
const [documents, setDocuments] = useState(mockDocuments); const [documents, setDocuments] = useState(mockDocuments);
const [showUploadModal, setShowUploadModal] = useState(false); const [showUploadModal, setShowUploadModal] = useState(false);
const [uploadDocName, setUploadDocName] = useState(''); const [uploadDocName, setUploadDocName] = useState('');
const [showAddBatteryModal, setShowAddBatteryModal] = useState(false);
const [selectedBatteryId, setSelectedBatteryId] = useState('');
const [batteryDeposit, setBatteryDeposit] = useState(0);
const [batteryDepositMethod, setBatteryDepositMethod] = useState<'cash' | 'bank' | 'bkash' | 'nagad'>('cash');
const [showBatteryInvoicePreview, setShowBatteryInvoicePreview] = useState<Invoice | null>(null);
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
const [downloadSuccess, setDownloadSuccess] = useState(false);
const [invoices, setInvoices] = useState<Invoice[]>([
{ id: 'INV-EV-001', type: 'ev_rental', relatedId: 'RNT-001', amount: 3000, deposit: 3000, generatedAt: '2024-01-15', status: 'paid', description: 'EV Rental Deposit — Jamal Uddin (AIMA Lightning)' },
{ id: 'INV-BAT-001', type: 'battery_rental', relatedId: 'BAT-RENT-001', amount: 1500, deposit: 3000, generatedAt: '2024-01-16', status: 'paid', description: 'Battery Rental — Galaxy 72V 45Ah (Deposit + 1st Month)' },
{ id: 'INV-BAT-002', type: 'battery_rental', relatedId: 'BAT-RENT-002', amount: 1800, deposit: 3500, generatedAt: '2024-02-20', status: 'paid', description: 'Battery Rental — Titan 72V 50Ah (Deposit + 1st Month)' },
]);
const [journalEntries, setJournalEntries] = useState<JournalEntry[]>([
{ id: 'JRN-001', date: '2024-01-15', description: 'EV Rental Deposit received from Jamal Uddin', debit: 'Cash / Bank', credit: 'Rental Deposit Liability', amount: 3000, referenceId: 'INV-EV-001', type: 'ev_deposit' },
{ id: 'JRN-002', date: '2024-01-16', description: 'Battery Deposit received — Galaxy 72V 45Ah', debit: 'Cash / Bank', credit: 'Battery Deposit Liability', amount: 3000, referenceId: 'INV-BAT-001', type: 'battery_deposit' },
{ id: 'JRN-003', date: '2024-02-20', description: 'Battery Deposit received — Titan 72V 50Ah', debit: 'Cash / Bank', credit: 'Battery Deposit Liability', amount: 3500, referenceId: 'INV-BAT-002', type: 'battery_deposit' },
]);
const [acceptPermission, setAcceptPermission] = useState(false); const [acceptPermission, setAcceptPermission] = useState(false);
const [rejectPermission, setRejectPermission] = useState(false); const [rejectPermission, setRejectPermission] = useState(false);
@@ -468,6 +547,80 @@ export default function RentalDetailPage() {
setRental(prev => prev ? { ...prev, status: 'active', activatedAt: new Date().toISOString().split('T')[0] } : null); setRental(prev => prev ? { ...prev, status: 'active', activatedAt: new Date().toISOString().split('T')[0] } : null);
}; };
const handleAddBattery = () => {
if (!selectedBatteryId || !rental) return;
const battery = mockBatteries.find(b => b.id === selectedBatteryId);
if (!battery) return;
const today = new Date().toISOString().split('T')[0];
const newInvId = `INV-BAT-${Date.now().toString().slice(-6)}`;
const newBatHistId = `BAT-RENT-${Date.now()}`;
// 1. Create battery rental history entry
const newBatteryHistory: BatteryRentalHistory = {
id: newBatHistId,
batteryId: battery.id,
batteryName: `${battery.brand} ${battery.model}`,
assignedAt: today,
monthlyRent: battery.monthlyRent,
deposit: batteryDeposit,
depositMethod: batteryDepositMethod,
invoiceId: newInvId,
invoiceGeneratedAt: today,
status: 'active',
};
// 2. Generate invoice
const newInvoice: Invoice = {
id: newInvId,
type: 'battery_rental',
relatedId: newBatHistId,
amount: battery.monthlyRent,
deposit: batteryDeposit,
generatedAt: today,
status: 'paid',
description: `Battery Rental — ${battery.brand} ${battery.model} (Deposit ৳${batteryDeposit} + 1st Month ৳${battery.monthlyRent})`,
};
// 3. Auto journal entries
const journalDeposit: JournalEntry = {
id: `JRN-${Date.now()}-DEP`,
date: today,
description: `Battery Deposit received — ${battery.brand} ${battery.model}`,
debit: 'Cash / Bank',
credit: 'Battery Deposit Liability',
amount: batteryDeposit,
referenceId: newInvId,
type: 'battery_deposit',
};
const journalRent: JournalEntry = {
id: `JRN-${Date.now()}-RENT`,
date: today,
description: `1st Month Battery Rent — ${battery.brand} ${battery.model}`,
debit: 'Cash / Bank',
credit: 'Battery Rent Revenue',
amount: battery.monthlyRent,
referenceId: newInvId,
type: 'battery_rent',
};
setInvoices(prev => [...prev, newInvoice]);
setJournalEntries(prev => [...prev, journalDeposit, journalRent]);
setRental(prev => prev ? {
...prev,
batteryId: battery.id,
batteryName: `${battery.brand} ${battery.model}`,
batteryRent: (prev.batteryRent || 0) + battery.monthlyRent,
batteryHistory: [...(prev.batteryHistory || []), newBatteryHistory],
} : null);
setShowBatteryInvoicePreview(newInvoice);
setShowAddBatteryModal(false);
setSelectedBatteryId('');
setBatteryDeposit(0);
setBatteryDepositMethod('cash');
};
const handleAddNote = () => { const handleAddNote = () => {
if (!newNote.trim()) return; if (!newNote.trim()) return;
setNotes(prev => [...prev, { id: `n${Date.now()}`, text: newNote, createdAt: new Date().toISOString().split('T')[0] }]); setNotes(prev => [...prev, { id: `n${Date.now()}`, text: newNote, createdAt: new Date().toISOString().split('T')[0] }]);
@@ -496,6 +649,25 @@ export default function RentalDetailPage() {
setShowSmsModal(false); setShowSmsModal(false);
}; };
const handlePrintInvoiceDirect = (inv: Invoice) => {
setShowBatteryInvoicePreview(inv);
setTimeout(() => {
window.print();
}, 150);
};
const handleSimulatedPDFDownload = (invId: string) => {
setIsDownloadingPDF(true);
setDownloadSuccess(false);
setTimeout(() => {
setIsDownloadingPDF(false);
setDownloadSuccess(true);
setTimeout(() => {
setDownloadSuccess(false);
}, 2000);
}, 1500);
};
const statusBadge = getStatusBadge(rental.status); const statusBadge = getStatusBadge(rental.status);
const typeBadge = getTypeBadge(rental.type); const typeBadge = getTypeBadge(rental.type);
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus); const paymentBadge = getPaymentStatusBadge(rental.paymentStatus);
@@ -516,6 +688,47 @@ export default function RentalDetailPage() {
return ( return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto"> <div className="p-4 lg:p-6 max-w-8xl mx-auto">
{/* Global `@media print` style sheet overrides */}
<style dangerouslySetInnerHTML={{
__html: `
@media print {
/* Hide the screen scrollbar and all default content layout wrapper */
body, html {
background-color: white !important;
color: black !important;
overflow: visible !important;
height: auto !important;
}
/* Hide standard layout wrapper tags */
body * {
visibility: hidden !important;
}
/* Show exclusively our paper canvas sheet */
#printable-invoice-modal, #printable-invoice-modal * {
visibility: visible !important;
}
#printable-invoice-modal {
position: absolute !important;
left: 0 !important;
top: 0 !important;
width: 100% !important;
margin: 0 !important;
padding: 40px !important;
box-shadow: none !important;
border: none !important;
background: white !important;
color: black !important;
}
/* Suppress all interactive controls from printouts */
.no-print {
display: none !important;
}
}
`}} />
<Link href="/admin/rentals" className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4"> <Link href="/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 <ArrowLeft className="w-4 h-4" /> Back to Rentals
</Link> </Link>
@@ -684,12 +897,36 @@ export default function RentalDetailPage() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between"><span className="text-sm text-slate-600">Type</span><span className="text-sm font-medium text-slate-800 capitalize">{rental.subscriptionType}</span></div> <div className="flex justify-between"><span className="text-sm text-slate-600">Type</span><span className="text-sm font-medium text-slate-800 capitalize">{rental.subscriptionType}</span></div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-slate-600">Rate</span> <span className="text-sm text-slate-600">Bike Rate</span>
<span className="text-sm font-medium text-slate-800"> <span className="text-sm font-medium text-slate-800">
{rental.subscriptionType === 'daily' ? rental.dailyRate : rental.subscriptionType === 'weekly' ? rental.weeklyRate : rental.monthlyRate}/ {rental.subscriptionType === 'daily' ? rental.dailyRate : rental.subscriptionType === 'weekly' ? rental.weeklyRate : rental.monthlyRate}/
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'} {rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
</span> </span>
</div> </div>
{rental.batteryRent && rental.batteryRent > 0 && (
<>
<div className="flex justify-between">
<span className="text-sm text-amber-600">Battery Rate</span>
<span className="text-sm font-medium text-amber-700">
{Math.round(rental.batteryRent / (rental.subscriptionType === 'daily' ? 30 : rental.subscriptionType === 'weekly' ? 4 : 1))}/
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
</span>
</div>
<div className="pt-2 mt-2 border-t border-slate-100">
<div className="flex justify-between">
<span className="text-sm font-medium text-emerald-700">Total (Bike + Battery)</span>
<span className="text-sm font-bold text-emerald-700">
{rental.subscriptionType === 'daily'
? rental.dailyRate + Math.round(rental.batteryRent / 30)
: rental.subscriptionType === 'weekly'
? rental.weeklyRate + Math.round(rental.batteryRent / 4)
: rental.monthlyRate + rental.batteryRent}/
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
</span>
</div>
</div>
</>
)}
</div> </div>
</div> </div>
@@ -710,6 +947,448 @@ export default function RentalDetailPage() {
</div> </div>
</div> </div>
{/* Battery Rental History */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-amber-50">
<h3 className="font-semibold text-amber-800 flex items-center gap-2">
<Battery className="w-5 h-5 text-amber-500" /> Battery Rental History
</h3>
<button onClick={() => setShowAddBatteryModal(true)} className="px-3 py-1.5 bg-amber-600 text-white rounded-lg text-xs font-medium hover:bg-amber-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Battery
</button>
</div>
{rental.batteryHistory && rental.batteryHistory.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Deposit</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Invoice</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
<th className="px-3 py-2"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rental.batteryHistory.map(bat => (
<tr key={bat.id} className="hover:bg-slate-50">
<td className="px-3 py-2">
<p className="text-sm font-medium text-slate-800">{bat.batteryName}</p>
<p className="text-xs text-slate-400 font-mono">{bat.batteryId}</p>
</td>
<td className="px-3 py-2 text-sm text-slate-600">{bat.assignedAt}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bat.returnedAt || <span className="text-slate-300"></span>}</td>
<td className="px-3 py-2">
<p className="text-sm font-medium text-purple-700">{bat.deposit?.toLocaleString() ?? '—'}</p>
<p className="text-xs text-slate-400 capitalize">{bat.depositMethod}</p>
</td>
<td className="px-3 py-2 text-sm font-semibold text-emerald-600">{bat.monthlyRent}/mo</td>
<td className="px-3 py-2">
<button
onClick={() => {
const inv = invoices.find(i => i.id === bat.invoiceId);
if (inv) setShowBatteryInvoicePreview(inv);
}}
className="text-xs text-blue-600 hover:underline font-mono flex items-center gap-1"
>
<FileText className="w-3 h-3" />{bat.invoiceId}
</button>
</td>
<td className="px-3 py-2">
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${bat.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'}`}>
{bat.status === 'active' ? 'Active' : 'Returned'}
</span>
</td>
<td className="px-3 py-2">
{bat.status === 'active' && (
<button
onClick={() => {
const today = new Date().toISOString().split('T')[0];
setRental(prev => prev ? {
...prev,
batteryHistory: prev.batteryHistory?.map(b =>
b.id === bat.id ? { ...b, status: 'returned' as const, returnedAt: today } : b
),
} : null);
}}
className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs hover:bg-slate-200"
>
Return
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="px-4 py-6 text-center">
<Battery className="w-8 h-8 text-slate-200 mx-auto mb-2" />
<p className="text-sm text-slate-400">No battery assigned yet.</p>
</div>
)}
{rental.batteryHistory?.some(b => b.status === 'active') && (
<div className="m-3 p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-center justify-between">
<p className="text-sm text-amber-700">
<span className="font-semibold">Active Battery Rent: </span>
{rental.batteryHistory.filter(b => b.status === 'active').reduce((sum, b) => sum + b.monthlyRent, 0).toLocaleString()}/month
</p>
<span className="text-xs text-amber-500">{rental.batteryHistory.filter(b => b.status === 'active').length} active</span>
</div>
)}
</div>
{/* Add Battery Modal */}
{showAddBatteryModal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg">
<div className="p-5 border-b border-slate-100 flex justify-between items-center bg-amber-50 rounded-t-2xl">
<div className="flex items-center gap-2">
<Battery className="w-5 h-5 text-amber-600" />
<h3 className="font-bold text-amber-900">Add Battery to Rental</h3>
</div>
<button onClick={() => { setShowAddBatteryModal(false); setSelectedBatteryId(''); setBatteryDeposit(0); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-5 space-y-4">
{/* Battery Select */}
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Select Battery</label>
<select
value={selectedBatteryId}
onChange={(e) => setSelectedBatteryId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Choose a battery...</option>
{mockBatteries.map(bat => (
<option key={bat.id} value={bat.id}>{bat.brand} {bat.model} SOC: {bat.soc}% {bat.monthlyRent}/month</option>
))}
</select>
</div>
{/* Deposit */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Deposit Amount ()</label>
<input
type="number"
min={0}
value={batteryDeposit}
onChange={(e) => setBatteryDeposit(parseInt(e.target.value) || 0)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="e.g. 3000"
/>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5 block">Payment Method</label>
<select
value={batteryDepositMethod}
onChange={(e) => setBatteryDepositMethod(e.target.value as 'cash' | 'bank' | 'bkash' | 'nagad')}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="cash">Cash</option>
<option value="bank">Bank Transfer</option>
<option value="bkash">bKash</option>
<option value="nagad">Nagad</option>
</select>
</div>
</div>
{/* Rate preview */}
{selectedBatteryId && (() => {
const bat = mockBatteries.find(b => b.id === selectedBatteryId);
if (!bat) return null;
const rate = rental.subscriptionType === 'daily'
? Math.round(bat.monthlyRent / 30)
: rental.subscriptionType === 'weekly'
? Math.round(bat.monthlyRent / 4)
: bat.monthlyRent;
return (
<div className="p-4 bg-amber-50 border border-amber-200 rounded-xl space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-amber-700 font-medium">Monthly Rent</span>
<span className="text-sm font-bold text-amber-800">{bat.monthlyRent.toLocaleString()}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-amber-700 font-medium">Deposit</span>
<span className="text-sm font-bold text-purple-700">{batteryDeposit.toLocaleString()}</span>
</div>
<div className="border-t border-amber-200 pt-2 flex items-center justify-between">
<span className="text-sm text-amber-800 font-semibold">Total Due Today</span>
<span className="text-base font-extrabold text-amber-900">{(batteryDeposit + rate).toLocaleString()}</span>
</div>
<div className="bg-white rounded-lg p-2 border border-amber-100">
<p className="text-xs text-amber-600 font-medium mb-1">Rate by Subscription ({rental.subscriptionType})</p>
<p className="text-sm font-bold text-amber-800">{rate}/{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}</p>
</div>
<div className="text-xs text-amber-500 bg-amber-50 p-2 rounded border border-amber-100">
<p className="font-semibold mb-0.5">📋 Invoice will be auto-generated</p>
<p> Debit: Cash/Bank ({(batteryDeposit + rate).toLocaleString()})</p>
<p> Credit: Battery Deposit Liability ({batteryDeposit.toLocaleString()}) + Battery Rent Revenue ({rate.toLocaleString()})</p>
</div>
</div>
);
})()}
<div className="flex gap-2 pt-2">
<button onClick={() => { setShowAddBatteryModal(false); setSelectedBatteryId(''); setBatteryDeposit(0); }} className="flex-1 py-2.5 px-4 border border-slate-200 text-slate-600 rounded-xl text-sm hover:bg-slate-50">
Cancel
</button>
<button
onClick={handleAddBattery}
disabled={!selectedBatteryId}
className="flex-1 py-2.5 px-4 bg-amber-600 text-white rounded-xl text-sm font-semibold hover:bg-amber-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
<FileText className="w-4 h-4" /> Assign & Generate Invoice
</button>
</div>
</div>
</div>
</div>
)}
{/* Unified Printable Invoice Viewer Modal */}
{showBatteryInvoicePreview && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 overflow-y-auto no-print">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden border border-slate-100 flex flex-col my-8 transform transition-all animate-fadeIn">
{/* Modal Title bar (no-print) */}
<div className={`p-5 border-b border-slate-100 flex justify-between items-center text-white ${showBatteryInvoicePreview.type === 'battery_rental'
? 'bg-gradient-to-r from-amber-500 to-amber-600'
: 'bg-gradient-to-r from-blue-600 to-indigo-600'
}`}>
<div className="flex items-center gap-2">
{showBatteryInvoicePreview.type === 'battery_rental' ? (
<Battery className="w-5 h-5" />
) : (
<Bike className="w-5 h-5" />
)}
<div>
<h3 className="font-bold text-lg">Invoice Details</h3>
<p className="text-xs opacity-90">Manage, print and download client invoice</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-xs font-bold px-3 py-1 rounded-full bg-white/20 border border-white/30 tracking-wide uppercase">
{showBatteryInvoicePreview.status}
</span>
<button onClick={() => setShowBatteryInvoicePreview(null)} className="p-1.5 text-white/80 hover:text-white transition bg-white/10 hover:bg-white/20 rounded-full">
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Physical Invoice Paper (Rendered to PDF/Printer) */}
<div className="p-8 space-y-6 overflow-y-auto max-h-[60vh] bg-slate-50/50" id="printable-invoice-modal">
{/* Invoice Banner & Branding */}
<div className="flex justify-between items-start border-b border-slate-200 pb-6">
<div>
<div className="flex items-center gap-2 text-slate-800 font-extrabold text-xl tracking-tight">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center text-white ${showBatteryInvoicePreview.type === 'battery_rental' ? 'bg-amber-500' : 'bg-blue-600'
}`}>
</div>
<span>JAIBEN <span className="text-xs font-light text-slate-400">by JML Group</span></span>
</div>
<p className="text-xs text-slate-500 mt-2 font-medium">Gulshan Hub, House 12, Road 5</p>
<p className="text-xs text-slate-500">Gulshan-1, Dhaka 1212, Bangladesh</p>
<p className="text-xs text-slate-400">billing@jaiben.com | +880 9612-JAIBEN</p>
</div>
<div className="text-right">
<h2 className="text-2xl font-black text-slate-800 uppercase tracking-widest">INVOICE</h2>
<div className="text-sm font-bold text-slate-700 font-mono mt-1">{showBatteryInvoicePreview.id}</div>
<div className="mt-3 text-xs text-slate-500 space-y-0.5">
<p><span className="font-semibold text-slate-700">Date:</span> {showBatteryInvoicePreview.generatedAt}</p>
<p><span className="font-semibold text-slate-700">Due Date:</span> {showBatteryInvoicePreview.generatedAt}</p>
<p><span className="font-semibold text-slate-700">Status:</span> <span className="text-emerald-600 font-bold uppercase">{showBatteryInvoicePreview.status}</span></p>
</div>
</div>
</div>
{/* Client & Billing Info */}
<div className="grid grid-cols-2 gap-6 bg-white p-4 rounded-xl border border-slate-100">
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Billed To</p>
<p className="text-sm font-bold text-slate-800">{rental.userName}</p>
<p className="text-xs text-slate-600 mt-1">{rental.userPhone}</p>
<p className="text-xs text-slate-500 mt-0.5">Hub: {rental.hubName}</p>
</div>
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Details</p>
<p className="text-xs text-slate-700"><span className="font-semibold text-slate-600">Rental Type:</span> <span className="capitalize">{rental.type}</span></p>
{showBatteryInvoicePreview.type === 'battery_rental' ? (
<p className="text-xs text-slate-700 mt-1"><span className="font-semibold text-slate-600">Product:</span> Battery Rent ({rental.batteryName || 'Galaxy 72V 45Ah'})</p>
) : (
<p className="text-xs text-slate-700 mt-1"><span className="font-semibold text-slate-600">Product:</span> EV Ride Rent ({rental.bikeModel})</p>
)}
<p className="text-xs text-slate-700 mt-1"><span className="font-semibold text-slate-600">Contract ID:</span> <span className="font-mono">{rental.id}</span></p>
</div>
</div>
{/* Invoice Items Table */}
<div className="overflow-hidden border border-slate-200 rounded-xl bg-white">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 border-b border-slate-200">
<th className="p-3 text-xs font-bold text-slate-600 uppercase">Item Description</th>
<th className="p-3 text-xs font-bold text-slate-600 uppercase text-center">Qty</th>
<th className="p-3 text-xs font-bold text-slate-600 uppercase text-right">Price</th>
<th className="p-3 text-xs font-bold text-slate-600 uppercase text-right">Amount</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{/* Deposit Line Item */}
{(showBatteryInvoicePreview.deposit ?? 0) > 0 && (
<tr>
<td className="p-3">
<p className="text-sm font-semibold text-slate-800">
{showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Security Deposit' : 'EV Rental Security Deposit'}
</p>
<p className="text-xs text-slate-400">Refundable security deposit held for hardware safety</p>
</td>
<td className="p-3 text-sm text-slate-600 text-center font-semibold">1</td>
<td className="p-3 text-sm text-slate-700 text-right font-medium">{showBatteryInvoicePreview.deposit?.toLocaleString()}</td>
<td className="p-3 text-sm text-slate-800 text-right font-bold">{showBatteryInvoicePreview.deposit?.toLocaleString()}</td>
</tr>
)}
{/* Rent Line Item */}
{showBatteryInvoicePreview.amount > 0 && (
<tr>
<td className="p-3">
<p className="text-sm font-semibold text-slate-800">
{showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Subscription Service Fee' : 'EV Rent Service Advance'}
</p>
<p className="text-xs text-slate-400">Initial cycle subscription fee ({rental.subscriptionType} billing rate)</p>
</td>
<td className="p-3 text-sm text-slate-600 text-center font-semibold">1</td>
<td className="p-3 text-sm text-slate-700 text-right font-medium">{showBatteryInvoicePreview.amount.toLocaleString()}</td>
<td className="p-3 text-sm text-slate-800 text-right font-bold">{showBatteryInvoicePreview.amount.toLocaleString()}</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Summary and Payment Breakdown */}
<div className="flex flex-col md:flex-row justify-between items-start gap-4 pt-2">
<div className="text-xs text-slate-500 max-w-sm">
<p className="font-bold text-slate-700 mb-1">Terms & Conditions</p>
<p className="leading-relaxed">This invoice confirms receipt of payment. Security deposits are refundable upon lease completion and inspection approval of the hardware. Rent fees are non-refundable.</p>
</div>
<div className="w-full md:w-64 bg-slate-50 border border-slate-200 rounded-xl p-4 space-y-2">
<div className="flex justify-between text-sm text-slate-600">
<span>Subtotal:</span>
<span className="font-semibold">{((showBatteryInvoicePreview.deposit ?? 0) + showBatteryInvoicePreview.amount).toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm text-slate-600">
<span>Tax/VAT (0%):</span>
<span>0</span>
</div>
<div className="border-t border-slate-200 pt-2 flex justify-between text-base font-bold text-slate-800">
<span>Total Paid:</span>
<span className="text-emerald-700">{((showBatteryInvoicePreview.deposit ?? 0) + showBatteryInvoicePreview.amount).toLocaleString()}</span>
</div>
<div className="flex justify-between text-xs text-slate-500 font-medium">
<span>Total Due:</span>
<span>0 (Fully Settled)</span>
</div>
</div>
</div>
{/* Auto Journal Entry / Accounting Ledger (no-print) */}
<div className="bg-emerald-50/50 border border-emerald-100 rounded-xl p-4 space-y-3 no-print">
<div className="flex justify-between items-center">
<p className="text-xs font-bold text-emerald-800 uppercase tracking-wide flex items-center gap-1.5">
System Ledger (Auto Journal Entry)
</p>
<span className="text-[10px] bg-emerald-100 text-emerald-800 font-semibold px-2 py-0.5 rounded-full">Balanced</span>
</div>
<div className="space-y-1.5 divide-y divide-emerald-100/50 text-xs">
<div className="flex justify-between pb-1.5 font-bold text-emerald-900">
<span>Debit Account</span>
<span>Amount</span>
</div>
<div className="flex justify-between py-1.5 text-emerald-800">
<span className="flex items-center gap-1">🟢 Dr: Cash / Bank Account</span>
<span className="font-semibold">{((showBatteryInvoicePreview.deposit ?? 0) + showBatteryInvoicePreview.amount).toLocaleString()}</span>
</div>
<div className="flex justify-between pt-1.5 pb-1 font-bold text-slate-800">
<span>Credit Account(s)</span>
<span>Amount</span>
</div>
{(showBatteryInvoicePreview.deposit ?? 0) > 0 && (
<div className="flex justify-between py-1 text-slate-600">
<span> Cr: {showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Security Deposit Liability' : 'EV Rental Security Deposit Liability'}</span>
<span>{showBatteryInvoicePreview.deposit?.toLocaleString()}</span>
</div>
)}
{showBatteryInvoicePreview.amount > 0 && (
<div className="flex justify-between py-1 text-slate-600">
<span> Cr: {showBatteryInvoicePreview.type === 'battery_rental' ? 'Battery Rental Revenue Account' : 'EV Rental Revenue Account'}</span>
<span>{showBatteryInvoicePreview.amount.toLocaleString()}</span>
</div>
)}
</div>
</div>
</div>
{/* Actions footer (no-print) */}
<div className="p-5 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row gap-2 justify-end no-print">
<button
onClick={() => setShowBatteryInvoicePreview(null)}
className="py-2.5 px-4 border border-slate-200 text-slate-600 rounded-xl text-sm font-semibold hover:bg-slate-100 transition order-2 sm:order-1"
>
Close
</button>
<button
onClick={() => handleSimulatedPDFDownload(showBatteryInvoicePreview.id)}
disabled={isDownloadingPDF || downloadSuccess}
className="py-2.5 px-4 bg-indigo-600 text-white rounded-xl text-sm font-semibold hover:bg-indigo-700 flex items-center justify-center gap-2 transition disabled:opacity-75 order-1 sm:order-2"
>
{isDownloadingPDF ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Generating PDF...
</>
) : downloadSuccess ? (
<>
<CheckCircle className="w-4 h-4 text-white" />
Downloaded!
</>
) : (
<>
<Download className="w-4 h-4" />
Download PDF
</>
)}
</button>
<button
onClick={() => window.print()}
className="py-2.5 px-5 bg-emerald-600 text-white rounded-xl text-sm font-semibold hover:bg-emerald-700 flex items-center justify-center gap-2 transition order-1 sm:order-3"
>
<Printer className="w-4 h-4" />
Print Invoice
</button>
</div>
</div>
</div>
)}
{/* Initial Condition Images */} {/* Initial Condition Images */}
{/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */} {/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */}
{rental.initialImages && ( {rental.initialImages && (
@@ -918,38 +1597,127 @@ export default function RentalDetailPage() {
</div> </div>
</div> </div>
<div className="bg-white p-4 rounded-xl border border-slate-200"> <div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-blue-50">
<h3 className="font-semibold text-slate-700 flex items-center gap-2"> <h3 className="font-semibold text-blue-900 flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-500" /> Rental Documents <FileText className="w-5 h-5 text-blue-500" /> Rental Documents & Invoices
</h3> </h3>
<button onClick={() => setShowUploadModal(true)} className="px-3 py-1.5 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 flex items-center gap-2"> <button onClick={() => setShowUploadModal(true)} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-xs font-medium hover:bg-blue-700 flex items-center gap-1">
<Upload className="w-4 h-4" /> Upload Document <Upload className="w-3 h-3" /> Upload
</button> </button>
</div> </div>
<div className="space-y-2">
{mockDocuments.map(doc => ( {/* EV Rental Invoices */}
<div key={doc.id} className="p-3 bg-slate-50 rounded-lg flex items-center justify-between"> <div className="px-4 pt-4 pb-2">
<div className="flex items-center gap-3"> <p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1"><Bike className="w-3.5 h-3.5" /> EV Rental Invoices</p>
<FileText className="w-5 h-5 text-slate-400" /> <div className="space-y-1.5">
<div> {invoices.filter(inv => inv.type === 'ev_rental').length === 0 && (
<p className="text-sm font-medium text-slate-700">{doc.name}</p> <p className="text-xs text-slate-400 italic py-1">No EV rental invoices.</p>
<p className="text-xs text-slate-500">{doc.uploadedAt}</p> )}
{invoices.filter(inv => inv.type === 'ev_rental').map(inv => (
<div key={inv.id} className="flex items-center justify-between p-2.5 bg-blue-50 border border-blue-100 rounded-lg transition hover:shadow-sm">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<FileText className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="text-sm font-semibold text-slate-800 font-mono">{inv.id}</p>
<p className="text-xs text-slate-500">{inv.description}</p>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-slate-400">{inv.generatedAt}</span>
<button
onClick={() => setShowBatteryInvoicePreview(inv)}
className="text-xs text-emerald-600 hover:text-emerald-700 hover:underline font-semibold flex items-center gap-0.5"
>
view invoice
</button>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-blue-700">{(inv.amount + (inv.deposit ?? 0)).toLocaleString()}</span>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${inv.status === 'paid' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>{inv.status}</span>
<button onClick={() => setShowBatteryInvoicePreview(inv)} className="p-1.5 text-blue-600 hover:bg-blue-100 rounded-lg" title="View Invoice">
<FileText className="w-3.5 h-3.5" />
</button>
<button onClick={() => handlePrintInvoiceDirect(inv)} className="p-1.5 text-slate-600 hover:bg-slate-100 rounded-lg" title="Print Invoice">
<Printer className="w-3.5 h-3.5" />
</button>
</div> </div>
</div> </div>
<button onClick={() => { ))}
const link = document.createElement('a'); </div>
link.href = '#'; </div>
link.download = doc.name;
alert(`Downloading: ${doc.name}`); {/* Battery Rental Invoices */}
}} className="px-3 py-1.5 text-blue-600 hover:bg-blue-50 rounded-lg text-sm flex items-center gap-1"> <div className="px-4 pt-3 pb-4">
<Download className="w-4 h-4" /> Download <p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1"><Battery className="w-3.5 h-3.5" /> Battery Rental Invoices</p>
</button> <div className="space-y-1.5">
<button onClick={() => alert(`Viewing: ${doc.name}`)} className="px-3 py-1.5 text-slate-600 hover:bg-slate-100 rounded-lg text-sm flex items-center gap-1"> {invoices.filter(inv => inv.type === 'battery_rental').length === 0 && (
<FileText className="w-4 h-4" /> View <p className="text-xs text-slate-400 italic py-1">No battery rental invoices.</p>
</button> )}
</div> {invoices.filter(inv => inv.type === 'battery_rental').map(inv => (
))} <div key={inv.id} className="flex items-center justify-between p-2.5 bg-amber-50 border border-amber-100 rounded-lg transition hover:shadow-sm">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center">
<Battery className="w-4 h-4 text-amber-600" />
</div>
<div>
<p className="text-sm font-semibold text-slate-800 font-mono">{inv.id}</p>
<p className="text-xs text-slate-500">{inv.description}</p>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-slate-400">{inv.generatedAt}</span>
<button
onClick={() => setShowBatteryInvoicePreview(inv)}
className="text-xs text-emerald-600 hover:text-emerald-700 hover:underline font-semibold flex items-center gap-0.5"
>
view invoice
</button>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="text-right">
<p className="text-sm font-bold text-amber-700">{((inv.deposit ?? 0) + inv.amount).toLocaleString()}</p>
<p className="text-xs text-slate-400">Dep: {(inv.deposit ?? 0).toLocaleString()} + Rent: {inv.amount.toLocaleString()}</p>
</div>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${inv.status === 'paid' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>{inv.status}</span>
<button onClick={() => setShowBatteryInvoicePreview(inv)} className="p-1.5 text-amber-600 hover:bg-amber-100 rounded-lg" title="View Invoice">
<FileText className="w-3.5 h-3.5" />
</button>
<button onClick={() => handlePrintInvoiceDirect(inv)} className="p-1.5 text-slate-600 hover:bg-slate-100 rounded-lg" title="Print Invoice">
<Printer className="w-3.5 h-3.5" />
</button>
</div>
</div>
))}
</div>
</div>
{/* General Docs */}
<div className="px-4 pb-4 border-t border-slate-100 pt-3">
<p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1"><FileText className="w-3.5 h-3.5" /> Other Documents</p>
<div className="space-y-1.5">
{documents.map(doc => (
<div key={doc.id} className="flex items-center justify-between p-2.5 bg-slate-50 border border-slate-100 rounded-lg">
<div className="flex items-center gap-2.5">
<FileText className="w-4 h-4 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-700">{doc.name}</p>
<p className="text-xs text-slate-400">{doc.uploadedAt}</p>
</div>
</div>
<div className="flex items-center gap-1">
<button onClick={() => alert(`Downloading: ${doc.name}`)} className="px-2.5 py-1.5 text-blue-600 hover:bg-blue-50 rounded-lg text-xs flex items-center gap-1">
<Download className="w-3.5 h-3.5" /> Download
</button>
<button onClick={() => alert(`Viewing: ${doc.name}`)} className="px-2.5 py-1.5 text-slate-600 hover:bg-slate-100 rounded-lg text-xs flex items-center gap-1">
<FileText className="w-3.5 h-3.5" /> View
</button>
</div>
</div>
))}
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,382 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import {
ArrowLeft, Search, Bike, User, MapPin, Battery,
Phone, MessageCircle, X, Navigation, Clock, RefreshCw
} from 'lucide-react';
interface Rental {
id: string;
bikeId: string;
userId: string;
userName: string;
userPhone: string;
bikeModel: string;
bikePlate: string;
bikeBattery: number;
status: 'pending' | 'accepted' | 'active' | 'completed' | 'cancelled' | 'locked';
type: 'single' | 'shared' | 'rent-to-own';
hubId: string;
hubName: string;
location?: {
lat: number;
lng: number;
address?: string;
lastUpdate?: string;
speed?: number;
heading?: number;
};
}
const mockRentals: Rental[] = [
{
id: 'RNT-001',
bikeId: 'BIKE-001',
userId: 'USR-003',
userName: 'Jamal Uddin',
userPhone: '+8801912345678',
bikeModel: 'AIMA Lightning',
bikePlate: 'Dhaka Metro Cha-9012',
bikeBattery: 87,
status: 'active',
type: 'single',
hubId: 'HUB-001',
hubName: 'Gulshan Hub',
location: { lat: 23.7925, lng: 90.4074, address: 'Gulshan 1, Dhaka', lastUpdate: '2024-03-28 14:30:00', speed: 0, heading: 180 },
},
{
id: 'RNT-002',
bikeId: 'BIKE-002',
userId: 'USR-004',
userName: 'Rafiq Islam',
userPhone: '+8801512345678',
bikeModel: 'Yadea DT3',
bikePlate: 'Dhaka Metro Ba-5521',
bikeBattery: 65,
status: 'active',
type: 'shared',
hubId: 'HUB-002',
hubName: 'Banani Hub',
location: { lat: 23.8041, lng: 90.4152, address: 'Banani, Dhaka', lastUpdate: '2024-03-28 14:28:00', speed: 15, heading: 90 },
},
{
id: 'RNT-003',
bikeId: 'BIKE-003',
userId: 'USR-001',
userName: 'Rahim Ahmed',
userPhone: '+8801712345678',
bikeModel: 'AIMA EM5',
bikePlate: 'Dhaka Metro Ko-1234',
bikeBattery: 92,
status: 'active',
type: 'rent-to-own',
hubId: 'HUB-003',
hubName: 'Uttara Hub',
location: { lat: 23.8776, lng: 90.4014, address: 'Uttara Sector 11, Dhaka', lastUpdate: '2024-03-28 14:25:00', speed: 25, heading: 270 },
},
{
id: 'RNT-004',
bikeId: 'BIKE-005',
userId: 'USR-005',
userName: 'Farid Ahmed',
userPhone: '+8801612345678',
bikeModel: 'Yadea G5',
bikePlate: 'Dhaka Metro Ha-5678',
bikeBattery: 45,
status: 'active',
type: 'single',
hubId: 'HUB-004',
hubName: 'Mirpur Hub',
location: { lat: 23.8222, lng: 90.3639, address: 'Mirpur 10, Dhaka', lastUpdate: '2024-03-28 14:20:00', speed: 0, heading: 0 },
},
{
id: 'RNT-005',
bikeId: 'BIKE-001',
userId: 'USR-002',
userName: 'Karim Hasan',
userPhone: '+8801812345678',
bikeModel: 'AIMA Lightning',
bikePlate: 'Dhaka Metro Cha-9012',
bikeBattery: 78,
status: 'pending',
type: 'single',
hubId: 'HUB-001',
hubName: 'Gulshan Hub',
location: { lat: 23.7889, lng: 90.4025, address: 'Dhanmondi 27, Dhaka', lastUpdate: '2024-03-28 14:15:00', speed: 8, heading: 45 },
},
];
const statusColors: Record<string, { bg: string; text: string }> = {
active: { bg: 'bg-green-100', text: 'text-green-700' },
pending: { bg: 'bg-amber-100', text: 'text-amber-700' },
accepted: { bg: 'bg-blue-100', text: 'text-blue-700' },
completed: { bg: 'bg-indigo-100', text: 'text-indigo-700' },
cancelled: { bg: 'bg-slate-100', text: 'text-slate-600' },
locked: { bg: 'bg-red-100', text: 'text-red-700' },
};
export default function RentalMapPage() {
const [rentals, setRentals] = useState<Rental[]>(mockRentals);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [selectedRental, setSelectedRental] = useState<Rental | null>(null);
const [lastRefresh, setLastRefresh] = useState(new Date());
const [liveLocations, setLiveLocations] = useState<Record<string, { lat: number; lng: number; speed: number }>>({});
const filteredRentals = rentals.filter(r => {
const matchesSearch = r.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.userName.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
return matchesSearch && matchesStatus && r.location;
});
const simulateLiveUpdate = () => {
const newLocations: Record<string, { lat: number; lng: number; speed: number }> = {};
rentals.forEach(rental => {
if (rental.location && rental.status === 'active') {
const movement = Math.random() * 0.002 - 0.001;
newLocations[rental.id] = {
lat: rental.location.lat + movement,
lng: rental.location.lng + movement,
speed: Math.floor(Math.random() * 30),
};
}
});
setLiveLocations(newLocations);
setLastRefresh(new Date());
};
useEffect(() => {
const interval = setInterval(simulateLiveUpdate, 5000);
return () => clearInterval(interval);
}, [rentals]);
const getMarkerPosition = (rental: Rental) => {
if (liveLocations[rental.id]) {
return { lat: liveLocations[rental.id].lat, lng: liveLocations[rental.id].lng };
}
return { lat: rental.location?.lat || 0, lng: rental.location?.lng || 0 };
};
return (
<div className="min-h-screen bg-slate-100">
<div className="p-4 bg-white shadow-sm border-b border-slate-200">
<div className="flex items-center gap-3 mb-3">
<Link href="/admin/rentals" className="p-2 hover:bg-slate-100 rounded-lg">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</Link>
<div className="flex-1">
<h1 className="text-xl font-extrabold text-slate-800">Live Rental Map</h1>
</div>
<button
onClick={simulateLiveUpdate}
className="p-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700"
>
<RefreshCw className="w-5 h-5" />
</button>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm text-slate-600"
/>
</div>
<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"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
</div>
</div>
<div className="p-4 space-y-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 relative overflow-hidden h-64 sm:h-80 lg:h-96">
<div className="absolute inset-0 bg-slate-100">
<div className="relative w-full h-full">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<Navigation className="w-16 h-16 text-emerald-400 mx-auto mb-4" />
<p className="text-lg font-semibold text-slate-600">Live Map View</p>
<p className="text-sm text-slate-500 mt-1">
Showing {filteredRentals.length} rentals on map
</p>
<p className="text-xs text-slate-400 mt-2">
Last updated: {lastRefresh.toLocaleTimeString()}
</p>
</div>
</div>
{filteredRentals.map((rental, index) => {
const pos = getMarkerPosition(rental);
const x = ((pos.lng - 90.35) / 0.15) * 100;
const y = ((23.95 - pos.lat) / 0.2) * 100;
return (
<div
key={rental.id}
className={`absolute transform -translate-x-1/2 -translate-y-1/2 cursor-pointer ${selectedRental?.id === rental.id ? 'z-20' : 'z-10'}`}
style={{ left: `${Math.min(95, Math.max(5, x))}%`, top: `${Math.min(90, Math.max(10, y))}%` }}
onClick={() => setSelectedRental(rental)}
>
<div className={`relative w-10 h-10 rounded-full flex items-center justify-center shadow-lg ${rental.status === 'active' ? 'bg-green-500' : rental.status === 'pending' ? 'bg-amber-500' : 'bg-slate-400'}`}>
<Bike className="w-5 h-5 text-white" />
<span className="absolute -top-1 -right-1 w-4 h-4 bg-white rounded-full flex items-center justify-center text-[10px] font-bold text-slate-700">
{index + 1}
</span>
</div>
<div className="absolute top-12 left-1/2 -translate-x-1/2 bg-white px-2 py-1 rounded-lg shadow text-xs font-medium text-slate-700 whitespace-nowrap">
{rental.id}
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden flex flex-col max-h-96">
<div className="p-3 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-700 text-sm">
Rental List ({filteredRentals.length})
</h3>
<div className="flex items-center gap-1 text-xs text-slate-500">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
Live
</div>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{filteredRentals.map((rental, index) => (
<div
key={rental.id}
onClick={() => setSelectedRental(rental)}
className={`p-3 rounded-lg border cursor-pointer transition-all ${selectedRental?.id === rental.id ? 'border-emerald-500 bg-emerald-50' : 'border-slate-200 hover:border-emerald-300'}`}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-emerald-100 flex items-center justify-center text-xs font-bold text-emerald-700">
{index + 1}
</span>
<Link href={`/admin/rentals/${rental.id}`} className="text-sm font-medium text-emerald-600 hover:underline">
{rental.id}
</Link>
</div>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${statusColors[rental.status].bg} ${statusColors[rental.status].text}`}>
{rental.status}
</span>
</div>
<div className="flex items-center gap-2 mb-2">
<Bike className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-600">{rental.bikeModel}</span>
</div>
<div className="flex items-center gap-2 mb-2">
<User className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-600">{rental.userName}</span>
</div>
<div className="flex items-center justify-between text-xs text-slate-500">
<div className="flex items-center gap-1">
<MapPin className="w-3 h-3" />
{rental.location?.address || rental.hubName}
</div>
<div className="flex items-center gap-1">
<Battery className="w-3 h-3" />
{rental.bikeBattery}%
</div>
</div>
{liveLocations[rental.id] && (
<div className="mt-2 pt-2 border-t border-slate-100 flex items-center justify-between text-xs">
<span className="text-slate-400">
Speed: {liveLocations[rental.id].speed} km/h
</span>
<span className="text-slate-400 flex items-center gap-1">
<Clock className="w-3 h-3" /> {rental.location?.lastUpdate}
</span>
</div>
)}
</div>
))}
</div>
</div>
</div>
{selectedRental && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => setSelectedRental(null)}>
<div className="bg-white rounded-xl shadow-xl w-full max-w-md" onClick={e => e.stopPropagation()}>
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Rental Details</h3>
<button onClick={() => setSelectedRental(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="flex items-center justify-between">
<span className="text-lg font-bold text-emerald-600">{selectedRental.id}</span>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${statusColors[selectedRental.status].bg} ${statusColors[selectedRental.status].text}`}>
{selectedRental.status}
</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Bike</p>
<p className="font-medium text-slate-700">{selectedRental.bikeModel}</p>
<p className="text-xs text-slate-400">{selectedRental.bikePlate}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Battery</p>
<p className={`font-medium ${selectedRental.bikeBattery > 50 ? 'text-green-600' : selectedRental.bikeBattery > 20 ? 'text-amber-600' : 'text-red-600'}`}>
{selectedRental.bikeBattery}%
</p>
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Renter</p>
<p className="font-medium text-slate-700">{selectedRental.userName}</p>
<p className="text-sm text-slate-500">{selectedRental.userPhone}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Location</p>
<p className="font-medium text-slate-700">{selectedRental.location?.address}</p>
<p className="text-xs text-slate-400">Lat: {selectedRental.location?.lat.toFixed(4)}, Lng: {selectedRental.location?.lng.toFixed(4)}</p>
{selectedRental.location?.lastUpdate && (
<p className="text-xs text-slate-400 mt-1">Last Update: {selectedRental.location.lastUpdate}</p>
)}
</div>
<div className="flex gap-2">
<a href={`tel:${selectedRental.userPhone}`} className="flex-1 py-2 px-4 bg-emerald-100 text-emerald-700 rounded-lg text-sm font-medium text-center flex items-center justify-center gap-2 hover:bg-emerald-200">
<Phone className="w-4 h-4" /> Call
</a>
<a href={`sms:${selectedRental.userPhone}`} className="flex-1 py-2 px-4 bg-blue-100 text-blue-700 rounded-lg text-sm font-medium text-center flex items-center justify-center gap-2 hover:bg-blue-200">
<MessageCircle className="w-4 h-4" /> SMS
</a>
</div>
<Link href={`/admin/rentals/${selectedRental.id}`} className="block w-full py-2 px-4 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium text-center hover:bg-slate-200">
View Full Details
</Link>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
import { import {
FileText, Search, Filter, Bike, User, Calendar, DollarSign, Clock, MoreVertical, FileText, Search, Filter, Bike, User, Calendar, DollarSign, Clock, MoreVertical,
Eye, Plus, Phone, MessageCircle, X, CreditCard, Wallet, Building, Download, Eye, Plus, Phone, MessageCircle, X, CreditCard, Wallet, Building, Download,
Printer, ChevronLeft, ChevronRight, CheckCircle, AlertTriangle Printer, ChevronLeft, ChevronRight, CheckCircle, AlertTriangle, MapPin
} from 'lucide-react'; } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { import {
@@ -33,6 +33,10 @@ interface Rental {
bikeModel: string; bikeModel: string;
bikePlate: string; bikePlate: string;
bikeBattery: number; bikeBattery: number;
batteryId?: string;
batteryName?: string;
batteryRent?: number;
batteryRentPending?: number;
type: RentalType; type: RentalType;
status: RentalStatus; status: RentalStatus;
startDate: string; startDate: string;
@@ -56,6 +60,11 @@ interface Rental {
lockedReason?: string; lockedReason?: string;
hubId: string; hubId: string;
hubName: string; hubName: string;
location?: {
lat: number;
lng: number;
address?: string;
};
initialImages?: BikeImage[]; initialImages?: BikeImage[];
imagesApproved: boolean; imagesApproved: boolean;
bikerNote?: string; bikerNote?: string;
@@ -63,6 +72,15 @@ interface Rental {
createdAt: string; createdAt: string;
acceptedAt?: string; acceptedAt?: string;
activatedAt?: string; activatedAt?: string;
batteryHistory?: {
id: string;
batteryId: string;
batteryName: string;
assignedAt: string;
returnedAt?: string;
monthlyRent: number;
status: 'active' | 'returned';
}[];
} }
interface Bike { interface Bike {
@@ -103,6 +121,23 @@ const mockHubs = [
{ id: 'HUB-004', name: 'Mirpur Hub' }, { id: 'HUB-004', name: 'Mirpur Hub' },
]; ];
interface Battery {
id: string;
brand: string;
model: string;
soc: number;
monthlyRent: number;
status: 'available' | 'in-use' | 'maintenance';
}
const mockBatteries: Battery[] = [
{ id: 'BAT-DH-001', brand: 'Galaxy', model: '72V 45Ah', soc: 85, monthlyRent: 1500, status: 'available' },
{ id: 'BAT-DH-002', brand: 'Titan', model: '72V 50Ah', soc: 92, monthlyRent: 1800, status: 'available' },
{ id: 'BAT-DH-003', brand: 'PowerMax', model: '60V 40Ah', soc: 78, monthlyRent: 1200, status: 'available' },
{ id: 'BAT-DH-004', brand: 'UltraCell', model: '72V 55Ah', soc: 88, monthlyRent: 2000, status: 'available' },
{ id: 'BAT-DH-005', brand: 'EcoVolt', model: '48V 30Ah', soc: 65, monthlyRent: 800, status: 'available' },
];
const rentalSettings = { const rentalSettings = {
single: { single: {
Premium: { deposit: 5000, contractMonths: [1, 3, 6, 12], dailyRate: 200, weeklyRate: 1200, monthlyRate: 5000 }, Premium: { deposit: 5000, contractMonths: [1, 3, 6, 12], dailyRate: 200, weeklyRate: 1200, monthlyRate: 5000 },
@@ -142,6 +177,7 @@ const mockRentals: Rental[] = [
dailyRate: 150, dailyRate: 150,
weeklyRate: 900, weeklyRate: 900,
monthlyRate: 3500, monthlyRate: 3500,
batteryRent: 1500,
totalPaid: 38500, totalPaid: 38500,
dueRental: 0, dueRental: 0,
pendingRent: 0, pendingRent: 0,
@@ -151,6 +187,7 @@ const mockRentals: Rental[] = [
penaltyAmount: 0, penaltyAmount: 0,
hubId: 'HUB-001', hubId: 'HUB-001',
hubName: 'Gulshan Hub', hubName: 'Gulshan Hub',
location: { lat: 23.7925, lng: 90.4074, address: 'Gulshan 1, Dhaka' },
imagesApproved: true, imagesApproved: true,
createdAt: '2024-01-15', createdAt: '2024-01-15',
acceptedAt: '2024-01-15', acceptedAt: '2024-01-15',
@@ -185,6 +222,7 @@ const mockRentals: Rental[] = [
penaltyAmount: 0, penaltyAmount: 0,
hubId: 'HUB-002', hubId: 'HUB-002',
hubName: 'Banani Hub', hubName: 'Banani Hub',
location: { lat: 23.8041, lng: 90.4152, address: 'Banani, Dhaka' },
imagesApproved: false, imagesApproved: false,
createdAt: '2024-02-10', createdAt: '2024-02-10',
}, },
@@ -243,6 +281,7 @@ const mockRentals: Rental[] = [
dailyRate: 100, dailyRate: 100,
weeklyRate: 600, weeklyRate: 600,
monthlyRate: 2200, monthlyRate: 2200,
batteryRent: 1500,
totalPaid: 2600, totalPaid: 2600,
dueRental: 600, dueRental: 600,
pendingRent: 600, pendingRent: 600,
@@ -304,6 +343,8 @@ export default function RentalsPage() {
subscriptionType: 'daily' | 'weekly' | 'monthly'; subscriptionType: 'daily' | 'weekly' | 'monthly';
contractMonths: number; contractMonths: number;
bikeId: string; bikeId: string;
batteryId: string;
batteryRent: number;
startDate: string; startDate: string;
hubId: string; hubId: string;
depositAmount: number; depositAmount: number;
@@ -316,12 +357,16 @@ export default function RentalsPage() {
subscriptionType: 'daily', subscriptionType: 'daily',
contractMonths: 0, contractMonths: 0,
bikeId: '', bikeId: '',
batteryId: '',
batteryRent: 0,
startDate: new Date().toISOString().split('T')[0], startDate: new Date().toISOString().split('T')[0],
hubId: '', hubId: '',
depositAmount: 0, depositAmount: 0,
depositPaymentMethod: 'cash', depositPaymentMethod: 'cash',
}); });
const availableBatteries = mockBatteries.filter(b => b.status === 'available');
const [showJournalPreview, setShowJournalPreview] = useState(false); const [showJournalPreview, setShowJournalPreview] = useState(false);
useEffect(() => { useEffect(() => {
@@ -439,6 +484,7 @@ export default function RentalsPage() {
const bike = mockBikes.find(b => b.id === newRental.bikeId); const bike = mockBikes.find(b => b.id === newRental.bikeId);
const user = mockUsers.find(u => u.id === newRental.userId); const user = mockUsers.find(u => u.id === newRental.userId);
const hub = mockHubs.find(h => h.id === newRental.hubId); const hub = mockHubs.find(h => h.id === newRental.hubId);
const battery = mockBatteries.find(b => b.id === newRental.batteryId);
const settings = planConditions[newRental.type]?.find(p => p.name === newRental.planConditionName) || planConditions[newRental.type]?.[0]; const settings = planConditions[newRental.type]?.find(p => p.name === newRental.planConditionName) || planConditions[newRental.type]?.[0];
const rental: Rental = { const rental: Rental = {
@@ -450,6 +496,9 @@ export default function RentalsPage() {
bikeModel: bike?.model || '', bikeModel: bike?.model || '',
bikePlate: bike?.plate || '', bikePlate: bike?.plate || '',
bikeBattery: bike?.battery || 0, bikeBattery: bike?.battery || 0,
batteryId: newRental.batteryId || undefined,
batteryName: battery ? `${battery.brand} ${battery.model}` : undefined,
batteryRent: newRental.batteryRent || undefined,
type: newRental.type, type: newRental.type,
status: 'pending', status: 'pending',
startDate: newRental.startDate, startDate: newRental.startDate,
@@ -473,6 +522,14 @@ export default function RentalsPage() {
hubName: hub?.name || '', hubName: hub?.name || '',
imagesApproved: false, imagesApproved: false,
createdAt: new Date().toISOString().split('T')[0], createdAt: new Date().toISOString().split('T')[0],
batteryHistory: newRental.batteryId ? [{
id: `BAT-RENT-${Date.now()}`,
batteryId: newRental.batteryId,
batteryName: battery ? `${battery.brand} ${battery.model}` : '',
assignedAt: new Date().toISOString().split('T')[0],
monthlyRent: newRental.batteryRent,
status: 'active' as const,
}] : undefined,
}; };
setRentals([...rentals, rental]); setRentals([...rentals, rental]);
@@ -486,6 +543,8 @@ export default function RentalsPage() {
subscriptionType: 'daily', subscriptionType: 'daily',
contractMonths: 0, contractMonths: 0,
bikeId: '', bikeId: '',
batteryId: '',
batteryRent: 0,
evModel: '', evModel: '',
startDate: new Date().toISOString().split('T')[0], startDate: new Date().toISOString().split('T')[0],
hubId: '', hubId: '',
@@ -516,6 +575,12 @@ export default function RentalsPage() {
<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 justify-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 justify-center gap-2">
<Download className="w-4 h-4" /> Export <Download className="w-4 h-4" /> Export
</button> </button>
<Link
href="/admin/rentals/map"
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 justify-center gap-2"
>
<MapPin className="w-4 h-4" /> Map View
</Link>
</div> </div>
</div> </div>
@@ -643,23 +708,42 @@ export default function RentalsPage() {
</td> */} </td> */}
<td className="px-4 py-3"> <td className="px-4 py-3">
{(() => { {(() => {
const rent = rental[rental.subscriptionType === 'daily' ? 'dailyRate' : rental.subscriptionType === 'weekly' ? 'weeklyRate' : 'monthlyRate']; const bikeRent = rental[rental.subscriptionType === 'daily' ? 'dailyRate' : rental.subscriptionType === 'weekly' ? 'weeklyRate' : 'monthlyRate'];
const due = rental.pendingRent || 0; const batteryRent = rental.batteryRent || 0;
const batteryRentAdjusted = rental.subscriptionType === 'daily'
? Math.round(batteryRent / 30)
: rental.subscriptionType === 'weekly'
? Math.round(batteryRent / 4)
: batteryRent;
const totalRent = bikeRent + batteryRentAdjusted;
const due = (rental.pendingRent || 0) + (rental.batteryRentPending || 0);
const penalty = rental.penaltyAmount || 0; const penalty = rental.penaltyAmount || 0;
const totalDue = due + penalty; const totalDue = due + penalty;
const periodLabel = rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'wk' : 'mo';
if (totalDue > 0) { if (totalDue > 0) {
return ( return (
<div> <div className="space-y-1">
<span className="text-sm font-medium text-amber-600">{totalDue.toLocaleString()}</span> <span className="text-sm font-medium text-amber-600">{totalDue.toLocaleString()}</span>
<p className="text-xs text-slate-400">{rental.subscriptionType} {rent}/{(rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'wk' : 'mo')}</p> {batteryRent > 0 ? (
{penalty > 0 && <span className="text-xs text-red-500">+{penalty} penalty</span>} <p className="text-xs text-slate-500">
{rental.subscriptionType} {bikeRent}{periodLabel} +Battery: {batteryRentAdjusted}{periodLabel}
</p>
) : (
<p className="text-xs text-red-400">{rental.subscriptionType} {totalRent}{periodLabel} +{penalty} penalty</p>
)}
</div> </div>
); );
} }
return ( return (
<div> <div className="space-y-1">
<span className="text-sm font-medium text-slate-700">{rent.toLocaleString()}</span> <span className="text-sm font-medium text-slate-700">{totalRent.toLocaleString()}</span>
<p className="text-xs text-slate-400 capitalize">{rental.subscriptionType}</p> {batteryRent > 0 ? (
<p className="text-xs text-slate-500">
{rental.subscriptionType} {bikeRent}{periodLabel} +Battery: {batteryRentAdjusted}{periodLabel}
</p>
) : (
<p className="text-xs text-slate-400 capitalize">{rental.subscriptionType}</p>
)}
</div> </div>
); );
})()} })()}
@@ -751,18 +835,39 @@ export default function RentalsPage() {
{rental.depositPaid ? 'Paid' : 'Unpaid'} {rental.depositPaid ? 'Paid' : 'Unpaid'}
</span> </span>
</div> </div>
{totalDue > 0 ? ( {(() => {
<div className="text-right"> const batteryRent = rental.batteryRent || 0;
<span className="text-sm font-medium text-amber-600">{totalDue.toLocaleString()}</span> const batteryRentAdjusted = rental.subscriptionType === 'daily'
<p className="text-xs text-slate-400">{rental.subscriptionType} {rent}/{(rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'wk' : 'mo')}</p> ? Math.round(batteryRent / 30)
{penalty > 0 && <span className="text-xs text-red-500">+{penalty}</span>} : rental.subscriptionType === 'weekly'
</div> ? Math.round(batteryRent / 4)
) : ( : batteryRent;
<div className="text-right"> const totalRent = rent + batteryRentAdjusted;
<span className="text-sm font-medium text-slate-700">{rent.toLocaleString()}</span> const periodLabel = rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'wk' : 'mo';
<p className="text-xs text-slate-400 capitalize">{rental.subscriptionType}</p> return totalDue > 0 ? (
</div> <div className="text-right">
)} <span className="text-sm font-medium text-amber-600">{totalDue.toLocaleString()}</span>
{batteryRent > 0 ? (
<p className="text-xs text-slate-500">
{rental.subscriptionType} {rent}{periodLabel} +Battery: {batteryRentAdjusted}{periodLabel} +{penalty} penalty
</p>
) : (
<p className="text-xs text-slate-400">{rental.subscriptionType} {totalRent}{periodLabel} +{penalty} penalty</p>
)}
</div>
) : (
<div className="text-right">
<span className="text-sm font-medium text-slate-700">{totalRent.toLocaleString()}</span>
{batteryRent > 0 ? (
<p className="text-xs text-slate-500">
{rental.subscriptionType} {rent}{periodLabel} +Battery: {batteryRentAdjusted}{periodLabel}
</p>
) : (
<p className="text-xs text-slate-400 capitalize">{rental.subscriptionType}</p>
)}
</div>
);
})()}
</div> </div>
<div className="flex items-center gap-1.5 pt-1"> <div className="flex items-center gap-1.5 pt-1">
@@ -918,35 +1023,80 @@ export default function RentalsPage() {
<span className="text-xs text-emerald-600 font-medium uppercase tracking-wide">{newRental.planConditionName}</span> <span className="text-xs text-emerald-600 font-medium uppercase tracking-wide">{newRental.planConditionName}</span>
</div> </div>
<p className="text-sm text-slate-600">Deposit: <span className="font-semibold text-slate-800">{selectedPlan?.deposit.toLocaleString()}</span></p> <p className="text-sm text-slate-600">Deposit: <span className="font-semibold text-slate-800">{selectedPlan?.deposit.toLocaleString()}</span></p>
<p className="text-sm text-slate-600"> <div className="text-sm text-slate-600">
Rate: <span className="font-semibold text-slate-800">{newRental.subscriptionType === 'daily' ? selectedPlan?.dailyRate : newRental.subscriptionType === 'weekly' ? selectedPlan?.weeklyRate : selectedPlan?.monthlyRate}</span> <span>Rate: </span>
<span className="text-slate-500">/{newRental.subscriptionType === 'daily' ? 'day' : newRental.subscriptionType === 'weekly' ? 'week' : 'month'}</span> {(() => {
</p> const bikeRate = newRental.subscriptionType === 'daily' ? selectedPlan?.dailyRate : newRental.subscriptionType === 'weekly' ? selectedPlan?.weeklyRate : selectedPlan?.monthlyRate;
const batteryRent = newRental.batteryRent || 0;
const batteryRate = batteryRent > 0 ? (newRental.subscriptionType === 'daily' ? Math.round(batteryRent / 30) : newRental.subscriptionType === 'weekly' ? Math.round(batteryRent / 4) : batteryRent) : 0;
const totalRate = bikeRate + batteryRate;
return (
<span className="font-semibold text-slate-800">
{totalRate.toLocaleString()}/{newRental.subscriptionType === 'daily' ? 'day' : newRental.subscriptionType === 'weekly' ? 'week' : 'month'}
{batteryRent > 0 && <span className="text-xs text-amber-600 ml-1">(Bike: {bikeRate} + Battery: {batteryRate})</span>}
</span>
);
})()}
</div>
</div> </div>
</div> </div>
)} )}
{createStep === 3 && ( {createStep === 3 && (
<div> <div className="space-y-4">
<h4 className="font-medium text-slate-700 mb-3">Step 3: Select Bike</h4> <h4 className="font-medium text-slate-700 mb-3">Step 3: Select Bike & Battery</h4>
<select
value={newRental.bikeId} <div>
onChange={(e) => setNewRental({ ...newRental, bikeId: e.target.value })} <label className="text-sm text-slate-600 mb-1 block">Select Bike</label>
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" <select
> value={newRental.bikeId}
<option value="">Select Bike...</option> onChange={(e) => setNewRental({ ...newRental, bikeId: e.target.value })}
{availableBikes.map(bike => ( className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
<option key={bike.id} value={bike.id}>{bike.model} - {bike.plate}</option> >
))} <option value="">Select Bike...</option>
</select> {availableBikes.map(bike => (
{selectedBike && ( <option key={bike.id} value={bike.id}>{bike.model} - {bike.plate}</option>
<div className="mt-3 flex items-center gap-2"> ))}
<span className="text-sm text-slate-600">Battery:</span> </select>
<span className={`text-sm px-2 py-1 rounded-full ${selectedBike.battery > 70 ? 'bg-green-100 text-green-700' : selectedBike.battery > 40 ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}> {selectedBike && (
{selectedBike.battery}% <div className="mt-2 flex items-center gap-2">
</span> <span className="text-sm text-slate-600">Battery:</span>
</div> <span className={`text-sm px-2 py-1 rounded-full ${selectedBike.battery > 70 ? 'bg-green-100 text-green-700' : selectedBike.battery > 40 ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}>
)} {selectedBike.battery}%
</span>
</div>
)}
</div>
<div>
<label className="text-sm text-slate-600 mb-1 block">Request Battery (Optional)</label>
<p className="text-xs text-slate-500 mb-2">Biker can request for battery. It will be added to their rental.</p>
<select
value={newRental.batteryId}
onChange={(e) => {
const bat = availableBatteries.find(b => b.id === e.target.value);
setNewRental({ ...newRental, batteryId: e.target.value, batteryRent: bat?.monthlyRent || 0 });
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">No Battery - Use Bike's Battery</option>
{availableBatteries.map(bat => (
<option key={bat.id} value={bat.id}>{bat.brand} {bat.model} - SOC: {bat.soc}% - Rent: ৳{bat.monthlyRent}/month</option>
))}
</select>
{newRental.batteryId && (
<div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg space-y-1">
<p className="text-sm text-amber-700">Battery Monthly Rent: <span className="font-bold">৳{newRental.batteryRent}/month</span></p>
<div className="text-xs text-amber-600 pt-1 border-t border-amber-100">
<p>Calculated Rate by Subscription:</p>
<p>• Daily: ৳{Math.round(newRental.batteryRent / 30)}/day (৳{newRental.batteryRent}/30)</p>
<p>• Weekly: ৳{Math.round(newRental.batteryRent / 4)}/week (৳{newRental.batteryRent}/4)</p>
<p>• Monthly (30 days): ৳{newRental.batteryRent}/month</p>
<p>• Monthly (31 days): ৳{newRental.batteryRent}/month</p>
</div>
</div>
)}
</div>
</div> </div>
)} )}
@@ -958,7 +1108,43 @@ export default function RentalsPage() {
<p className="text-sm text-slate-600">Deposit Amount: ৳{selectedPlan?.deposit.toLocaleString()}</p> <p className="text-sm text-slate-600">Deposit Amount: ৳{selectedPlan?.deposit.toLocaleString()}</p>
<p className="text-sm text-slate-600">User: {selectedUser?.name}</p> <p className="text-sm text-slate-600">User: {selectedUser?.name}</p>
<p className="text-sm text-slate-600">Bike: {selectedBike?.model} ({selectedBike?.plate})</p> <p className="text-sm text-slate-600">Bike: {selectedBike?.model} ({selectedBike?.plate})</p>
{newRental.batteryId && (
<p className="text-sm text-slate-600">Battery: {availableBatteries.find(b => b.id === newRental.batteryId)?.brand} {availableBatteries.find(b => b.id === newRental.batteryId)?.model} (Monthly Rent: ৳{newRental.batteryRent})</p>
)}
<p className="text-sm text-slate-600">Hub: {mockHubs.find(h => h.id === newRental.hubId)?.name}</p> <p className="text-sm text-slate-600">Hub: {mockHubs.find(h => h.id === newRental.hubId)?.name}</p>
{newRental.batteryId && (
<div className="mt-3 pt-3 border-t border-slate-200">
<p className="text-sm font-medium text-emerald-700 mb-2">Total Rental Payment (Bike + Battery):</p>
{(() => {
const bikeRate = newRental.subscriptionType === 'daily' ? selectedPlan?.dailyRate : newRental.subscriptionType === 'weekly' ? selectedPlan?.weeklyRate : selectedPlan?.monthlyRate;
const batteryMonthlyRent = newRental.batteryRent || 0;
const dailyRate = bikeRate + Math.round(batteryMonthlyRent / 30);
const weeklyRate = bikeRate + Math.round(batteryMonthlyRent / 4);
const monthly30Rate = bikeRate + batteryMonthlyRent;
const monthly31Rate = bikeRate + batteryMonthlyRent;
return (
<div className="space-y-1 text-xs">
<p className="text-slate-600">
<span className="font-medium">Daily:</span> ৳{dailyRate}/day
<span className="text-amber-600"> (Bike: ৳{bikeRate} + Battery: ৳{Math.round(batteryMonthlyRent / 30)})</span>
</p>
<p className="text-slate-600">
<span className="font-medium">Weekly:</span> ৳{weeklyRate}/week
<span className="text-amber-600"> (Bike: ৳{bikeRate} + Battery: ৳{Math.round(batteryMonthlyRent / 4)})</span>
</p>
<p className="text-slate-600">
<span className="font-medium">Monthly (30 days):</span> ৳{monthly30Rate}/month
<span className="text-amber-600"> (Bike: ৳{bikeRate} + Battery: ৳{batteryMonthlyRent})</span>
</p>
<p className="text-slate-600">
<span className="font-medium">Monthly (31 days):</span> ৳{monthly31Rate}/month
<span className="text-amber-600"> (Bike: ৳{bikeRate} + Battery: ৳{batteryMonthlyRent})</span>
</p>
</div>
);
})()}
</div>
)}
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Shield, Plus, Search, X, Edit, Trash2, Copy, Check, ChevronDown, ChevronRight, BookOpen, FileSearch, Settings, BarChart3, Bike, Users, Briefcase, Truck, Store, BatteryCharging, Building2, Wrench, DollarSign, TrendingUp, UserCog } from 'lucide-react'; import { Shield, Plus, Search, X, Edit, Trash2, Copy, Check, ChevronDown, ChevronRight, BookOpen, FileSearch, Settings, BarChart3, Bike, Users, Briefcase, Truck, Store, BatteryCharging, Building2, Wrench, DollarSign, TrendingUp, UserCog, Bell, MessageSquare } from 'lucide-react';
interface Permission { interface Permission {
key: string; key: string;
@@ -35,11 +35,10 @@ interface Role {
} }
const buildDefaultGroups = (): PermissionGroup[] => [ const buildDefaultGroups = (): PermissionGroup[] => [
{ {
id: 'kyc', id: 'kyc',
title: 'KYC Requests & Verification', title: 'KYC Requests & Verification',
description: 'The Biker user will request from the app. Investor, Swap Station, Merchant will request from the website. Front desk officers (hub/head office) can request for Biker, Investor, Swap Station, Merchant and can upload remaining documents. Admin officers (head office) will approve or reject documents with notes and "make a Biker | Investor | Swap Station | Merchant".', description: 'The Biker user will request from the app. Biker, Investor, Shop, Merchant will request from the website. Front desk officers (hub/head office) can request for Biker, Investor, Shop, Merchant, and can upload remaining documents. Admin managers (head office) will approve or reject documents with notes and "make a Biker | Investor | Shop | Merchant".',
icon: FileSearch, icon: FileSearch,
permissions: [ permissions: [
{ key: 'kyc.request', label: 'KYC Request', enabled: false }, { key: 'kyc.request', label: 'KYC Request', enabled: false },
@@ -62,15 +61,20 @@ const buildDefaultGroups = (): PermissionGroup[] => [
edit: { key: 'settings.kyc_documents_config', label: 'Config', enabled: false }, edit: { key: 'settings.kyc_documents_config', label: 'Config', enabled: false },
}, },
{ {
label: 'Plan Selection with Condition', label: 'Plan Selection with EV Condition',
view: { key: 'settings.plan_selection_with_condition_view', label: 'View', enabled: false }, view: { key: 'settings.plan_selection_with_condition_view', label: 'View', enabled: false },
edit: { key: 'settings.plan_selection_with_condition_config', label: 'Config', enabled: false }, edit: { key: 'settings.plan_selection_with_condition_config', label: 'Config', enabled: false },
}, },
{ {
label: 'Investment Plan', label: 'EV Investment Plan',
view: { key: 'settings.investment_plan_view', label: 'View', enabled: false }, view: { key: 'settings.investment_plan_view', label: 'View', enabled: false },
edit: { key: 'settings.investment_plan_config', label: 'Config', enabled: false }, edit: { key: 'settings.investment_plan_config', label: 'Config', enabled: false },
}, },
{
label: 'Battery Investment Plan',
view: { key: 'settings.battery_investment_plan_view', label: 'View', enabled: false },
edit: { key: 'settings.battery_investment_plan_config', label: 'Config', enabled: false },
},
{ {
label: 'Swap Station Plan', label: 'Swap Station Plan',
view: { key: 'settings.swap_station_plan_view', label: 'View', enabled: false }, view: { key: 'settings.swap_station_plan_view', label: 'View', enabled: false },
@@ -86,13 +90,23 @@ const buildDefaultGroups = (): PermissionGroup[] => [
view: { key: 'settings.company_policy_view', label: 'View', enabled: false }, view: { key: 'settings.company_policy_view', label: 'View', enabled: false },
edit: { key: 'settings.company_policy_config', label: 'Config', enabled: false }, edit: { key: 'settings.company_policy_config', label: 'Config', enabled: false },
}, },
{
label: 'Email & SMS Templates',
view: { key: 'settings.es_templates_view', label: 'View', enabled: false },
edit: { key: 'settings.es_templates_config', label: 'Config', enabled: false },
},
{
label: 'EV Parts',
view: { key: 'settings.ev_parts_view', label: 'View', enabled: false },
edit: { key: 'settings.ev_parts_config', label: 'Config', enabled: false },
},
], ],
permissions: [], permissions: [],
}, },
{ {
id: 'dashboard', id: 'dashboard',
title: 'Dashboard', title: 'Dashboard',
description: 'Access to main dashboard', description: 'Access to main dashboard insights and summary metrics.',
icon: BarChart3, icon: BarChart3,
permissions: [ permissions: [
{ key: 'dashboard.view', label: 'View Dashboard', enabled: false }, { key: 'dashboard.view', label: 'View Dashboard', enabled: false },
@@ -101,133 +115,188 @@ const buildDefaultGroups = (): PermissionGroup[] => [
{ {
id: 'rentals', id: 'rentals',
title: 'Rentals', title: 'Rentals',
description: 'Manage rental operations', description: 'Front desk officers can select user, rental type, contract duration, bike, hub and payment methods to create rentals. Bikers can accept or reject rentals. Managers can edit, cancel, lock, or unlock rentals and view default and penalty fee tracking.',
icon: Bike, icon: Bike,
permissions: [ permissions: [
{ key: 'rentals.view', label: 'View', enabled: false }, { key: 'rental.view', label: 'View Rentals', enabled: false },
{ key: 'rentals.create', label: 'Create', enabled: false }, { key: 'rental.create', label: 'Create Rental', enabled: false },
{ key: 'rentals.edit', label: 'Edit', enabled: false }, { key: 'rental.requset', label: 'Rental Request', enabled: false },
{ key: 'rentals.delete', label: 'Delete', enabled: false }, { key: 'rental.accept', label: 'Accept Rental', enabled: false },
{ key: 'rental.reject', label: 'Reject Rental', enabled: false },
{ key: 'rental.edit', label: 'Edit Rental', enabled: false },
{ key: 'rental.cancel', label: 'Cancel Rental', enabled: false },
{ key: 'rental.image_approve', label: 'Image Approve', enabled: false },
{ key: 'rental.lock', label: 'Lock Rental', enabled: false },
{ key: 'rental.unlock', label: 'Unlock Rental', enabled: false },
] ]
}, },
{ {
id: 'bikers', id: 'bikers',
title: 'Bikers', title: 'Bikers Management',
description: 'Manage bikers', description: 'Handle automatic biker profile creation after KYC approval, basic detail updates, status/membership changes, document uploads/removals, internal notes and activity tracking.',
icon: Users, icon: Users,
permissions: [ permissions: [
{ key: 'bikers.view', label: 'View', enabled: false }, { key: 'biker.view', label: 'View Biker Profile', enabled: false },
{ key: 'bikers.create', label: 'Create', enabled: false }, { key: 'biker.create', label: 'Create Biker Profile', enabled: false },
{ key: 'bikers.edit', label: 'Edit', enabled: false }, { key: 'biker.edit', label: 'Edit Biker Profile', enabled: false },
{ key: 'bikers.delete', label: 'Delete', enabled: false }, { key: 'biker.delete', label: 'Soft Delete / Deactivate Biker', enabled: false },
{ key: 'biker.status_change', label: 'Change Biker Status', enabled: false },
{ key: 'biker.membership_change', label: 'Change Biker Membership', enabled: false },
{ key: 'biker.kyc_view', label: 'View Biker KYC Info', enabled: false },
{ key: 'biker.kyc_update', label: 'Update Biker KYC Info', enabled: false },
{ key: 'biker.activity_view', label: 'View Biker Activity Logs', enabled: false },
{ key: 'biker.document_view', label: 'View Biker Documents', enabled: false },
{ key: 'biker.document_upload', label: 'Upload Biker Documents', enabled: false },
{ key: 'biker.document_delete', label: 'Remove Biker Documents', enabled: false },
{ key: 'biker.rental_history_view', label: 'View Biker Rental History', enabled: false },
{ key: 'biker.payment_history_view', label: 'View Biker Payment History', enabled: false },
{ key: 'biker.wallet_view', label: 'View Biker Wallet Balance', enabled: false },
{ key: 'biker.note_add', label: 'Add Biker Internal Notes', enabled: false },
{ key: 'biker.note_view', label: 'View Biker Internal Notes', enabled: false },
{ key: 'biker.export', label: 'Export Biker Reports & Data', enabled: false },
{ key: 'biker.make_valid_user', label: 'Make Biker Active & Valid', enabled: false },
{ key: 'biker.lock', label: 'Lock Biker Account', enabled: false },
{ key: 'biker.unlock', label: 'Unlock Biker Account', enabled: false },
] ]
}, },
{ {
id: 'investors', id: 'investors',
title: 'Investors', title: 'Investors Management',
description: 'Manage investors', description: 'Manage automatically created profiles, assign EV & Battery Investment plans, oversee bank accounts/tax logs, track daily rental shares, process withdrawal requests and notifications.',
icon: Briefcase, icon: Briefcase,
permissions: [ permissions: [
{ key: 'investors.view', label: 'View', enabled: false }, { key: 'investor.view', label: 'View Investors', enabled: false },
{ key: 'investors.create', label: 'Create', enabled: false }, { key: 'investor.create', label: 'Create Investor Profile', enabled: false },
{ key: 'investors.edit', label: 'Edit', enabled: false }, { key: 'investor.edit', label: 'Edit Investor Details', enabled: false },
{ key: 'investors.delete', label: 'Delete', enabled: false }, { key: 'investor.delete', label: 'Soft Delete Investor', enabled: false },
{ key: 'investor.plan_assign', label: 'Assign Investment Plans', enabled: false },
{ key: 'investor.bank_edit', label: 'Manage Bank & Tax Info', enabled: false },
{ key: 'investor.withdraw_request', label: 'Withdrawal Requests', enabled: false },
{ key: 'investor.document_upload', label: 'Upload Investor Documents', enabled: false },
{ key: 'investor.document_approve', label: 'Approve Investor Documents', enabled: false },
{ key: 'investor.notification_view', label: 'View Investor Notifications', enabled: false },
]
},
{
id: 'battery',
title: 'Battery Management',
description: 'Add new battery assets with pricing, deposit, daily rent, and BMS identifiers. Oversee ownership history, rental transactions, damage logs, maintenance, and data exports.',
icon: BatteryCharging,
permissions: [
{ key: 'battery.view', label: 'View Batteries', enabled: false },
{ key: 'battery.create', label: 'Add Battery & BMS Config', enabled: false },
{ key: 'battery.edit', label: 'Edit Battery details', enabled: false },
{ key: 'battery.delete', label: 'Delete Battery Record', enabled: false },
{ key: 'battery.export', label: 'Export Battery History', enabled: false },
] ]
}, },
{ {
id: 'fleet', id: 'fleet',
title: 'Fleet', title: 'Fleet Management',
description: 'Manage fleet vehicles', description: 'Register new EV bikes, configure GPS details, associate battery packs, view rental transactions, maintenance, damage history, investor ownership logs, and activity trackers.',
icon: Truck, icon: Truck,
permissions: [ permissions: [
{ key: 'fleet.view', label: 'View', enabled: false }, { key: 'fleet.view', label: 'View Fleet Vehicles', enabled: false },
{ key: 'fleet.create', label: 'Create', enabled: false }, { key: 'fleet.create', label: 'Register New Bike', enabled: false },
{ key: 'fleet.edit', label: 'Edit', enabled: false }, { key: 'fleet.edit', label: 'Edit Fleet Vehicle', enabled: false },
{ key: 'fleet.delete', label: 'Delete', enabled: false }, { key: 'fleet.delete', label: 'Soft Delete Bike', enabled: false },
{ key: 'fleet.gps_config', label: 'Configure Vehicle GPS', enabled: false },
{ key: 'fleet.export', label: 'Export Fleet Records', enabled: false },
] ]
}, },
{ {
id: 'merchants', id: 'service_centers',
title: 'Merchants', title: 'Service Centers',
description: 'Manage merchants', description: 'Add and manage service center listings, locations, and capacity. Track fleet service logs, maintenance logs, and uploaded dealer/biker invoices.',
icon: Store, icon: Wrench,
permissions: [ permissions: [
{ key: 'merchants.view', label: 'View', enabled: false }, { key: 'service_center.view', label: 'View Service Centers', enabled: false },
{ key: 'merchants.create', label: 'Create', enabled: false }, { key: 'service_center.create', label: 'Create Service Center', enabled: false },
{ key: 'merchants.edit', label: 'Edit', enabled: false }, { key: 'service_center.edit', label: 'Edit Service Center Details', enabled: false },
{ key: 'merchants.delete', label: 'Delete', enabled: false }, { key: 'service_center.delete', label: 'Remove Service Center', enabled: false },
]
},
{
id: 'swap_stations',
title: 'Swap Stations',
description: 'Manage swap stations',
icon: BatteryCharging,
permissions: [
{ key: 'swap_stations.view', label: 'View', enabled: false },
{ key: 'swap_stations.create', label: 'Create', enabled: false },
{ key: 'swap_stations.edit', label: 'Edit', enabled: false },
{ key: 'swap_stations.delete', label: 'Delete', enabled: false },
]
},
{
id: 'hubs',
title: 'Hubs',
description: 'Manage hubs',
icon: Building2,
permissions: [
{ key: 'hubs.view', label: 'View', enabled: false },
{ key: 'hubs.create', label: 'Create', enabled: false },
{ key: 'hubs.edit', label: 'Edit', enabled: false },
{ key: 'hubs.delete', label: 'Delete', enabled: false },
] ]
}, },
{ {
id: 'maintenance', id: 'maintenance',
title: 'Maintenance', title: 'Damage & Maintenance',
description: 'Manage maintenance requests', description: 'Report damage or technical issues from the biker side, and manage repairs, spare parts inventory, claim notifications, and service schedules on the admin side.',
icon: Wrench, icon: Settings,
permissions: [ permissions: [
{ key: 'maintenance.view', label: 'View', enabled: false }, { key: 'maintenance.view', label: 'View Damage Logs', enabled: false },
{ key: 'maintenance.create', label: 'Create', enabled: false }, { key: 'maintenance.create', label: 'Report Damage/Claim Free Service', enabled: false },
{ key: 'maintenance.edit', label: 'Edit', enabled: false }, { key: 'maintenance.edit', label: 'Update Repair Details', enabled: false },
{ key: 'maintenance.delete', label: 'Delete', enabled: false }, { key: 'maintenance.delete', label: 'Delete Repair Log', enabled: false },
] ]
}, },
{ {
id: 'accounting', id: 'accounting',
title: 'Accounting', title: 'Accounting',
description: 'Manage financial records', description: 'Oversee automatic journals generated by rentals, verify battery rental deposits, maintain general ledgers, and process and verify investor withdrawal requests.',
icon: DollarSign, icon: DollarSign,
permissions: [ permissions: [
{ key: 'accounting.view', label: 'View', enabled: false }, { key: 'accounting.view', label: 'View Financial Ledger', enabled: false },
{ key: 'accounting.create', label: 'Create', enabled: false }, { key: 'accounting.create', label: 'Create Financial Entry', enabled: false },
{ key: 'accounting.edit', label: 'Edit', enabled: false }, { key: 'accounting.edit', label: 'Modify Financial Entry', enabled: false },
{ key: 'accounting.delete', label: 'Delete', enabled: false }, { key: 'accounting.delete', label: 'Void Financial Entry', enabled: false },
{ key: 'accounting.withdraw_process', label: 'Process & Pay Withdrawal', enabled: false },
]
},
{
id: 'hubs',
title: 'Hubs Management',
description: 'Admin can create and manage geographic hubs, assign dedicated Hub Managers, allocate bike/battery inventory, and process hub-specific rentals and KYC checkups.',
icon: Building2,
permissions: [
{ key: 'hub.view', label: 'View Hubs', enabled: false },
{ key: 'hub.create', label: 'Create New Hub', enabled: false },
{ key: 'hub.edit', label: 'Edit Hub Details & Staff', enabled: false },
{ key: 'hub.delete', label: 'Deactivate Hub', enabled: false },
] ]
}, },
{ {
id: 'reports', id: 'reports',
title: 'Reports', title: 'Reports & Analytics',
description: 'View and generate reports', description: 'Generate, schedule, view, and export comprehensive administrative reports for KYC, fleet, investment payouts, rentals, and general ledger operations.',
icon: TrendingUp, icon: TrendingUp,
permissions: [ permissions: [
{ key: 'reports.view', label: 'View', enabled: false }, { key: 'reports.view', label: 'View Reports', enabled: false },
{ key: 'reports.export', label: 'Export', enabled: false }, { key: 'reports.export', label: 'Export Reports (CSV/PDF)', enabled: false },
] ]
}, },
{ {
id: 'users', id: 'users',
title: 'Users', title: 'Users & Staff Management',
description: 'Manage system users', description: 'Admin-only module for managing internal administrative users, hub managers, front desk staff, accountants, and Super Admins.',
icon: UserCog, icon: UserCog,
permissions: [ permissions: [
{ key: 'users.view', label: 'View', enabled: false }, { key: 'users.view', label: 'View System Users', enabled: false },
{ key: 'users.create', label: 'Create', enabled: false }, { key: 'users.create', label: 'Create Staff User', enabled: false },
{ key: 'users.edit', label: 'Edit', enabled: false }, { key: 'users.edit', label: 'Edit Staff details', enabled: false },
{ key: 'users.delete', label: 'Delete', enabled: false }, { key: 'users.delete', label: 'Deactivate Staff User', enabled: false },
] ]
}, },
{
id: 'roles',
title: 'Roles & Access Permissions',
description: 'Super Admin-only module to construct custom administrative roles and configure fine-grained permissions.',
icon: Shield,
permissions: [
{ key: 'roles.view', label: 'View Roles list', enabled: false },
{ key: 'roles.config', label: 'Configure & Update Roles', enabled: false },
]
},
{
id: 'notifications',
title: 'Notifications & Messaging',
description: 'Access system logs, alerts (KYC Verification, Rentals, Vehicle Service, Cabinet, Ledger), compose custom system notifications, broadcast messages, and schedule notifications.',
icon: Bell,
permissions: [
{ key: 'notifications.view', label: 'View Notifications', enabled: false },
{ key: 'messaging.compose', label: 'Compose Message', enabled: false },
{ key: 'messaging.broadcast', label: 'Broadcast message', enabled: false },
{ key: 'messaging.schedule', label: 'Schedule notification', enabled: false },
]
}
]; ];
const mockRoles: Role[] = [ const mockRoles: Role[] = [
@@ -238,7 +307,12 @@ const mockRoles: Role[] = [
isDefault: false, isDefault: false,
permissionGroups: buildDefaultGroups().map(g => ({ permissionGroups: buildDefaultGroups().map(g => ({
...g, ...g,
permissions: g.permissions.map(p => ({ ...p, enabled: true })) permissions: g.permissions.map(p => ({ ...p, enabled: true })),
permissionPairs: g.permissionPairs?.map(p => ({
...p,
view: { ...p.view, enabled: true },
edit: { ...p.edit, enabled: true }
}))
})) }))
}, },
{ {
@@ -251,13 +325,18 @@ const mockRoles: Role[] = [
permissions: g.permissions.map(p => ({ permissions: g.permissions.map(p => ({
...p, ...p,
enabled: !p.key.includes('delete') enabled: !p.key.includes('delete')
})),
permissionPairs: g.permissionPairs?.map(p => ({
...p,
view: { ...p.view, enabled: true },
edit: { ...p.edit, enabled: !p.edit.key.includes('config') && !p.edit.key.includes('delete') }
})) }))
})) }))
}, },
{ {
id: 'ROLE-003', id: 'ROLE-003',
name: 'Front Desk Officer', name: 'Front Desk Officer',
description: 'Hub/head office officer - can request KYC and upload documents', description: 'Hub/head office officer - can request KYC, create rentals and upload documents',
isDefault: false, isDefault: false,
permissionGroups: buildDefaultGroups().map(g => { permissionGroups: buildDefaultGroups().map(g => {
if (g.id === 'kyc') { if (g.id === 'kyc') {
@@ -269,9 +348,55 @@ const mockRoles: Role[] = [
})) }))
}; };
} }
if (g.id === 'rentals') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['rental.view', 'rental.create', 'rental.image_approve'].includes(p.key)
}))
};
}
if (g.id === 'bikers') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['biker.view', 'biker.edit', 'biker.kyc_view', 'biker.kyc_update', 'biker.activity_view', 'biker.document_view', 'biker.document_upload', 'biker.rental_history_view', 'biker.payment_history_view', 'biker.wallet_view', 'biker.note_add', 'biker.note_view'].includes(p.key)
}))
};
}
if (g.id === 'investors') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['investor.view', 'investor.document_upload'].includes(p.key)
}))
};
}
if (g.id === 'settings') {
return {
...g,
permissionPairs: g.permissionPairs?.map(p => ({
...p,
view: { ...p.view, enabled: true },
edit: { ...p.edit, enabled: false }
}))
};
}
if (g.id === 'dashboard') { if (g.id === 'dashboard') {
return { ...g, permissions: g.permissions.map(p => ({ ...p, enabled: true })) }; return { ...g, permissions: g.permissions.map(p => ({ ...p, enabled: true })) };
} }
if (['battery', 'fleet', 'service_centers', 'maintenance', 'accounting', 'hubs', 'reports'].includes(g.id)) {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: p.key.includes('view')
}))
};
}
return g; return g;
}) })
}, },
@@ -319,7 +444,16 @@ const mockRoles: Role[] = [
...g, ...g,
permissions: g.permissions.map(p => ({ permissions: g.permissions.map(p => ({
...p, ...p,
enabled: ['rentals.view', 'rentals.create'].includes(p.key) enabled: ['rental.requset', 'rental.accept', 'rental.reject', 'rental.view'].includes(p.key)
}))
};
}
if (g.id === 'bikers') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['biker.view'].includes(p.key)
})) }))
}; };
} }

View File

@@ -0,0 +1,801 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
ArrowLeft, Building2, Star, Phone, Mail, MapPin, Activity, Wrench,
Battery, Bike, DollarSign, CheckCircle2, Clock, AlertTriangle, Search,
SlidersHorizontal, ArrowUpDown, User, Calendar, Shield, Tag, Plus, Eye,
BarChart3, Percent, ChevronRight, ExternalLink
} from 'lucide-react';
import Link from 'next/link';
import { ServiceCenter } from '../page';
// Interface for Maintenance History Record
interface HistoryRecord {
id: string;
date: string;
assetId: string;
assetType: 'EV Bike' | 'Battery';
serviceType: 'Damage' | 'Repair' | 'Service' | 'Battery Swap' | 'Inspection';
description: string;
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
status: 'completed' | 'in_progress' | 'parts_ordered';
estimatedCost: number;
actualCost: number;
partsUsed: { name: string; qty: number; price: number }[];
laborCost: number;
technician: string;
}
// Generate realistic mock history data based on Center ID/Name
const getMockHistoryData = (centerName: string): HistoryRecord[] => {
const baseHistory: HistoryRecord[] = [
{
id: 'MNT-101',
date: '2024-03-21',
assetId: 'EV-004',
assetType: 'EV Bike',
serviceType: 'Damage',
description: 'Front fender shattered in traffic collision. Replaced brackets and front wheel.',
severity: 'major',
status: 'in_progress',
estimatedCost: 3500,
actualCost: 3200,
partsUsed: [
{ name: 'Front fender', qty: 1, price: 1500 },
{ name: 'Mounting brackets', qty: 2, price: 800 },
{ name: 'Brake pads', qty: 1, price: 600 }
],
laborCost: 1200,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-102',
date: '2024-03-18',
assetId: 'BAT-044',
assetType: 'Battery',
serviceType: 'Battery Swap',
description: 'Internal diagnostic showing rapid voltage degradation. Replaced cells and recalibrated BMS.',
severity: 'critical',
status: 'completed',
estimatedCost: 12000,
actualCost: 11500,
partsUsed: [
{ name: 'Battery 60V cell pack', qty: 1, price: 9500 },
{ name: 'BMS Controller Board', qty: 1, price: 2000 }
],
laborCost: 2500,
technician: 'Kamrul Hasan'
},
{
id: 'MNT-103',
date: '2024-03-15',
assetId: 'EV-012',
assetType: 'EV Bike',
serviceType: 'Service',
description: 'Routine 5,000km periodic maintenance. Calibrated drum brakes and greased chassis bearings.',
severity: 'minor',
status: 'completed',
estimatedCost: 1500,
actualCost: 1450,
partsUsed: [
{ name: 'Brake Cable', qty: 1, price: 250 },
{ name: 'Sprocket kit', qty: 1, price: 450 }
],
laborCost: 750,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-104',
date: '2024-03-10',
assetId: 'BAT-021',
assetType: 'Battery',
serviceType: 'Inspection',
description: 'Thermal warning flag during hyper-charging cycle. Terminals cleaned and thermal gel reapplied.',
severity: 'cosmetic',
status: 'completed',
estimatedCost: 500,
actualCost: 400,
partsUsed: [
{ name: 'Thermal paste', qty: 1, price: 150 }
],
laborCost: 250,
technician: 'Kamrul Hasan'
},
{
id: 'MNT-105',
date: '2024-03-05',
assetId: 'EV-009',
assetType: 'EV Bike',
serviceType: 'Repair',
description: 'Throttle failure reported by delivery driver. Replaced magnetic sensor assembly.',
severity: 'major',
status: 'completed',
estimatedCost: 1800,
actualCost: 2100,
partsUsed: [
{ name: 'Throttle control assembly', qty: 1, price: 800 },
{ name: 'Wiring loom adapter', qty: 1, price: 450 }
],
laborCost: 850,
technician: 'Rafiqul Islam'
},
{
id: 'MNT-106',
date: '2024-02-28',
assetId: 'EV-017',
assetType: 'EV Bike',
serviceType: 'Damage',
description: 'Rear tire blowout due to road debris. Replacement and alignment completed.',
severity: 'minor',
status: 'completed',
estimatedCost: 2800,
actualCost: 2750,
partsUsed: [
{ name: 'Rear Tire tubeless', qty: 1, price: 2200 },
{ name: 'Chain replacement', qty: 1, price: 400 }
],
laborCost: 500,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-107',
date: '2024-02-20',
assetId: 'BAT-089',
assetType: 'Battery',
serviceType: 'Battery Swap',
description: 'Dead module replacement under premium warranty. Replaced sub-assemblies.',
severity: 'critical',
status: 'parts_ordered',
estimatedCost: 15000,
actualCost: 0,
partsUsed: [
{ name: 'Battery 48V cell pack', qty: 1, price: 8000 }
],
laborCost: 1500,
technician: 'Kamrul Hasan'
}
];
// Variations in records based on Center's specialty & size to make data dynamic
if (centerName.includes('Gulshan') || centerName.includes('Center A')) {
return baseHistory;
} else if (centerName.includes('Banani') || centerName.includes('Center B')) {
return baseHistory.filter(h => h.serviceType === 'Battery Swap' || h.serviceType === 'Service' || h.serviceType === 'Inspection').map(h => ({
...h,
id: h.id.replace('10', '20'),
technician: 'Tanvir Rahman'
}));
} else {
// Uttara / Authorized
return baseHistory.filter(h => h.serviceType === 'Inspection' || h.serviceType === 'Repair').map(h => ({
...h,
id: h.id.replace('10', '30'),
technician: 'Arif Chowdhury'
}));
}
};
export default function ServiceCenterDetailsPage() {
const params = useParams();
const router = useRouter();
const id = params.id as string;
const [isMounted, setIsMounted] = useState(false);
const [center, setCenter] = useState<ServiceCenter | null>(null);
const [history, setHistory] = useState<HistoryRecord[]>([]);
// Filtering / Sorting / Search states for history
const [searchQuery, setSearchQuery] = useState('');
const [assetFilter, setAssetFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
const [severityFilter, setSeverityFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [sortBy, setSortBy] = useState<'date-desc' | 'date-asc' | 'cost-desc' | 'cost-asc' | 'urgency-desc' | 'urgency-asc'>('date-desc');
// Map Interactive detail popup state
const [mapPopup, setMapPopup] = useState<string | null>(null);
useEffect(() => {
setIsMounted(true);
// Load Service Centers from localStorage
const stored = localStorage.getItem('jaiben_service_centers');
let foundCenter: ServiceCenter | null = null;
if (stored) {
try {
const centers: ServiceCenter[] = JSON.parse(stored);
foundCenter = centers.find(c => c.id === id) || null;
} catch (e) {}
}
if (foundCenter) {
setCenter(foundCenter);
setHistory(getMockHistoryData(foundCenter.name));
} else {
router.push('/admin/service-centers');
}
}, [id, router]);
if (!isMounted || !center) return null;
// Filter History records
const filteredHistory = history.filter(h => {
const matchesSearch = h.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.assetId.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.technician.toLowerCase().includes(searchQuery.toLowerCase());
const matchesAsset = assetFilter === 'all' || h.assetType === assetFilter;
const matchesType = typeFilter === 'all' || h.serviceType === typeFilter;
const matchesSeverity = severityFilter === 'all' || h.severity === severityFilter;
const matchesStatus = statusFilter === 'all' || h.status === statusFilter;
return matchesSearch && matchesAsset && matchesType && matchesSeverity && matchesStatus;
});
// Sort History records
const severityWeights = { cosmetic: 1, minor: 2, major: 3, critical: 4 };
const sortedHistory = [...filteredHistory].sort((a, b) => {
switch (sortBy) {
case 'date-desc':
return new Date(b.date).getTime() - new Date(a.date).getTime();
case 'date-asc':
return new Date(a.date).getTime() - new Date(b.date).getTime();
case 'cost-desc':
return (b.actualCost || b.estimatedCost) - (a.actualCost || a.estimatedCost);
case 'cost-asc':
return (a.actualCost || a.estimatedCost) - (b.actualCost || b.estimatedCost);
case 'urgency-desc':
return severityWeights[b.severity] - severityWeights[a.severity];
case 'urgency-asc':
return severityWeights[a.severity] - severityWeights[b.severity];
default:
return 0;
}
});
// Financial & Aggregate calculations
const totalRepairs = history.length;
const completedRepairs = history.filter(h => h.status === 'completed');
const totalEstimatedCost = completedRepairs.reduce((sum, h) => sum + h.estimatedCost, 0);
const totalActualCost = completedRepairs.reduce((sum, h) => sum + h.actualCost, 0);
const costVariance = totalActualCost - totalEstimatedCost;
const totalPartsCost = completedRepairs.reduce((sum, h) => sum + h.partsUsed.reduce((s, p) => s + (p.price * p.qty), 0), 0);
const totalLaborCost = completedRepairs.reduce((sum, h) => sum + h.laborCost, 0);
const totalSpend = totalPartsCost + totalLaborCost;
// Aggregate Parts Utilized Log
const partsAggregated: { name: string; totalQty: number; totalCost: number }[] = [];
history.forEach(h => {
h.partsUsed.forEach(part => {
const existing = partsAggregated.find(p => p.name === part.name);
if (existing) {
existing.totalQty += part.qty;
existing.totalCost += part.price * part.qty;
} else {
partsAggregated.push({
name: part.name,
totalQty: part.qty,
totalCost: part.price * part.qty
});
}
});
});
const topParts = partsAggregated.sort((a, b) => b.totalQty - a.totalQty).slice(0, 5);
const statusColors = {
active: 'bg-emerald-100 text-emerald-700',
busy: 'bg-amber-100 text-amber-700',
inactive: 'bg-slate-100 text-slate-700'
};
const severityColors = {
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 statusHistoryColors = {
completed: 'bg-emerald-100 text-emerald-700',
in_progress: 'bg-blue-100 text-blue-700',
parts_ordered: 'bg-purple-100 text-purple-700'
};
return (
<div className="p-4 lg:p-6 mb-6 lg:mb-0 space-y-6 max-w-8xl mx-auto">
{/* Navigation Top - standard layout of other detail profiles */}
<div className="flex items-center justify-between border-b border-slate-100 pb-4 mb-4">
<button
onClick={() => router.push('/admin/service-centers')}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 text-sm font-semibold transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4 text-slate-400" /> Back to Service Centers
</button>
<span className="text-xs font-bold text-slate-400">
Node Registry: {center.id}
</span>
</div>
{/* Main Profile Info Header - rounded-xl alignment matching maintenance page */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6 relative overflow-hidden">
{/* Glow effect decorative */}
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none" />
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-6 relative z-10">
<div className="space-y-4 flex-1">
<div className="flex flex-wrap items-center gap-3">
<div className="w-14 h-14 rounded-xl bg-slate-50 border border-slate-100 flex items-center justify-center flex-shrink-0">
<Building2 className="w-7 h-7 text-slate-600" />
</div>
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800 leading-tight">{center.name}</h1>
<div className="flex flex-wrap items-center gap-2 mt-1">
<span className={`px-2.5 py-0.5 text-xs font-semibold rounded-full border border-transparent ${statusColors[center.status]}`}>
{center.status}
</span>
<div className="flex items-center gap-1 text-yellow-500 font-bold text-sm bg-yellow-50 px-2 py-0.5 rounded border border-yellow-100">
<Star className="w-3.5 h-3.5 fill-yellow-500" />
<span>{center.rating.toFixed(1)}</span>
</div>
</div>
</div>
</div>
{/* Profile items - clean, consistent spacing */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-3 gap-x-6 text-sm text-slate-600 pt-2">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-slate-400" />
<div className="flex flex-wrap items-center gap-x-2">
<span>{center.address}</span>
{center.googleMapLink && (
<a
href={center.googleMapLink}
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline flex items-center gap-0.5 font-bold"
>
Map Link <ExternalLink className="w-3 h-3" />
</a>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Phone className="w-4 h-4 text-slate-400" />
<span>{center.phone}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-slate-400" />
<span>{center.email}</span>
</div>
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-slate-400" />
<span>Staff: <strong className="font-semibold text-slate-800">{center.staffCount} technicians</strong></span>
</div>
<div className="flex items-center gap-2">
<Wrench className="w-4 h-4 text-slate-400" />
<span>Capacity: <strong className="font-semibold text-slate-800">{center.capacity}</strong> total slots</span>
</div>
</div>
{/* Specialization List Header */}
<div className="space-y-1.5 pt-2">
<span className="text-[10px] font-extrabold uppercase text-slate-400 tracking-wider">Node Specializations</span>
<div className="flex flex-wrap gap-1.5">
{center.specialization.map(spec => (
<span key={spec} className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded-lg text-xs font-bold text-slate-600">
{spec}
</span>
))}
</div>
</div>
</div>
{/* Quick Stats Header Summary - simplified matching maintenance specs, occupancy not needed */}
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-5 rounded-xl border border-slate-100 w-full lg:max-w-xs flex-shrink-0">
<div className="space-y-1">
<p className="text-xs font-bold text-slate-400 uppercase tracking-wide">Repairs Logs</p>
<p className="text-2xl font-extrabold text-slate-800">{totalRepairs}</p>
<p className="text-[10px] text-slate-500">{completedRepairs.length} completed</p>
</div>
<div className="space-y-1">
<p className="text-xs font-bold text-slate-400 uppercase tracking-wide">Capacity</p>
<p className="text-2xl font-extrabold text-slate-800">{center.capacity}</p>
<p className="text-[10px] text-slate-500">Service slots registered</p>
</div>
</div>
</div>
</div>
{/* Analytics: Map Mockup & Cost Breakdown Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* INTERACTIVE STYLIZED MAP CONTAINER - aligned clean white rounded-xl styles */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-4 flex flex-col justify-between lg:col-span-1 min-h-[380px]">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg">Node Location Map</h3>
<p className="text-xs text-slate-400">Dhaka city arterial coverage grid mockup</p>
</div>
{/* Map canvas container */}
<div className="flex-1 bg-slate-900 rounded-xl relative overflow-hidden border border-slate-850 shadow-inner flex items-center justify-center min-h-[220px]">
{/* Pulsating target coordinate representing the Center */}
<div
className="absolute w-8 h-8 flex items-center justify-center cursor-pointer group z-20"
style={{ top: '45%', left: '50%', transform: 'translate(-50%, -50%)' }}
onClick={() => setMapPopup(center.name)}
>
<span className="absolute inline-flex h-full w-full rounded-full bg-accent opacity-75 animate-ping" />
<div className="relative w-4 h-4 bg-accent border-2 border-white rounded-full flex items-center justify-center shadow-lg group-hover:scale-125 transition-transform">
<div className="w-1.5 h-1.5 bg-white rounded-full" />
</div>
</div>
{/* Stylized Dhaka grids & landmarks using SVGs */}
<svg className="w-full h-full absolute inset-0 opacity-40 select-none pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="none">
{/* Arterial Highways */}
<line x1="20" y1="0" x2="20" y2="100" stroke="#475569" strokeWidth="0.75" strokeDasharray="2" />
<line x1="50" y1="0" x2="50" y2="100" stroke="#475569" strokeWidth="1.5" />
<line x1="80" y1="0" x2="80" y2="100" stroke="#475569" strokeWidth="0.75" />
<line x1="0" y1="30" x2="100" y2="30" stroke="#475569" strokeWidth="0.75" />
<line x1="0" y1="50" x2="100" y2="50" stroke="#475569" strokeWidth="1.5" />
<line x1="0" y1="80" x2="100" y2="80" stroke="#475569" strokeWidth="0.75" />
{/* Waterway (Gulshan Lake) */}
<path d="M 50,0 Q 52,25 48,50 T 54,100" fill="none" stroke="#1e3a8a" strokeWidth="3" opacity="0.3" />
<path d="M 47,40 Q 70,45 80,42" fill="none" stroke="#1e3a8a" strokeWidth="2.5" opacity="0.3" />
{/* Other hubs mockup dots */}
<circle cx="20" cy="30" r="1.5" fill="#4f46e5" />
<circle cx="80" cy="30" r="1.5" fill="#4f46e5" />
<circle cx="20" cy="80" r="1.5" fill="#4f46e5" />
<circle cx="80" cy="80" r="1.5" fill="#4f46e5" />
</svg>
{/* Scale watermark */}
<div className="absolute bottom-2 left-2 text-[9px] text-slate-500 font-bold bg-slate-950/75 px-1.5 py-0.5 rounded border border-slate-800">
GPS: {center.latitude.toFixed(4)}°N, {center.longitude.toFixed(4)}°E
</div>
{/* Stylized popup when clicked */}
{mapPopup && (
<div className="absolute top-2 right-2 left-2 bg-slate-950/90 border border-slate-800 rounded-lg p-2.5 text-xs text-white z-30 animate-fadeIn space-y-1">
<div className="flex items-center justify-between">
<span className="font-extrabold text-accent">{center.name}</span>
<button onClick={() => setMapPopup(null)} className="text-slate-400 hover:text-white font-bold">×</button>
</div>
<p className="text-[10px] text-slate-400">{center.address}</p>
<div className="flex justify-between pt-1 border-t border-slate-800 text-[9px] text-slate-400 font-bold">
<span>Capacity: {center.capacity} slots</span>
<span>Rating: {center.rating.toFixed(1)}</span>
</div>
</div>
)}
{/* Custom street labels */}
<div className="absolute top-4 left-[53%] text-[8px] font-bold text-slate-600 tracking-widest uppercase origin-center rotate-90 select-none">
Gulshan Lake Road
</div>
<div className="absolute top-[52%] left-4 text-[8px] font-bold text-slate-600 tracking-widest uppercase select-none">
Tejgaon-Gulshan Link Road
</div>
</div>
<div className="text-center text-[10px] font-bold text-slate-400 uppercase tracking-widest pt-1">
📍 Click GPS coordinate node for telemetry details
</div>
</div>
{/* FINANCIAL PERFORMANCE & EXPENSE TRACKING */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6 lg:col-span-2">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg">Financial Performance & Cost Margins</h3>
<p className="text-xs text-slate-400">Aggregated historical metrics from completed maintenance invoices</p>
</div>
{/* Financial details panel */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl flex flex-col justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Estimated Spend</span>
<div className="mt-2">
<p className="text-2xl font-extrabold text-slate-800">{totalEstimatedCost.toLocaleString()}</p>
<p className="text-[10px] text-slate-500 mt-0.5">Budgeted repairs cost</p>
</div>
</div>
<div className="bg-indigo-50/50 border border-indigo-100/50 p-4 rounded-xl flex flex-col justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Actual Invoice Spend</span>
<div className="mt-2">
<p className="text-2xl font-extrabold text-indigo-700">{totalActualCost.toLocaleString()}</p>
<p className="text-[10px] text-indigo-500 mt-0.5">Billed repair totals</p>
</div>
</div>
<div className={`p-4 rounded-xl border flex flex-col justify-between ${costVariance > 0 ? 'bg-rose-50 border-rose-100' : 'bg-emerald-50 border-emerald-100'}`}>
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Cost Variance</span>
<div className="mt-2">
<p className={`text-2xl font-extrabold ${costVariance > 0 ? 'text-rose-700' : 'text-emerald-700'}`}>
{costVariance > 0 ? `+৳${costVariance.toLocaleString()}` : `-৳${Math.abs(costVariance).toLocaleString()}`}
</p>
<p className="text-[10px] text-slate-500 mt-0.5">
{costVariance > 0 ? 'Over budget invoices' : 'Under budget savings!'}
</p>
</div>
</div>
</div>
{/* Parts Used Aggregates & Labor Breakdown */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2">
{/* Margins */}
<div className="space-y-4">
<h4 className="text-xs font-extrabold uppercase text-slate-400 tracking-wider">Expense Margin Distribution</h4>
<div className="space-y-3">
{/* Parts Spend Bar */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs font-bold text-slate-600">
<span className="flex items-center gap-1"><Battery className="w-3.5 h-3.5 text-indigo-500" /> Spare Parts Cost</span>
<span>{totalPartsCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}%)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-indigo-650 h-full rounded-full"
style={{ width: `${totalSpend > 0 ? (totalPartsCost/totalSpend)*100 : 0}%` }}
/>
</div>
</div>
{/* Labor Spend Bar */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs font-bold text-slate-600">
<span className="flex items-center gap-1"><Wrench className="w-3.5 h-3.5 text-emerald-500" /> Labor Costs</span>
<span>{totalLaborCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}%)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-emerald-550 h-full rounded-full"
style={{ width: `${totalSpend > 0 ? (totalLaborCost/totalSpend)*100 : 0}%` }}
/>
</div>
</div>
</div>
{/* General Health Tip */}
<div className="p-3 bg-slate-50 border border-slate-100 rounded-lg text-xs text-slate-500 flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" />
<span>
<strong>Cost Ratio Notice:</strong> This node maintains a healthy parts-to-labor ratio of {totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}:{totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}. Lower labor ratios reflect technician efficiency.
</span>
</div>
</div>
{/* Parts utilized list */}
<div className="space-y-3">
<h4 className="text-xs font-extrabold uppercase text-slate-400 tracking-wider">Top Spare Parts Log</h4>
<div className="divide-y divide-slate-100 border border-slate-100 rounded-xl overflow-hidden bg-slate-50/50">
{topParts.length === 0 ? (
<p className="text-xs text-slate-400 p-4 text-center">No spare parts recorded yet</p>
) : topParts.map(part => (
<div key={part.name} className="p-2.5 flex items-center justify-between text-xs text-slate-600">
<span className="font-semibold text-slate-700">{part.name}</span>
<div className="flex items-center gap-4 text-right">
<span className="font-bold text-slate-500">Qty: {part.totalQty}</span>
<span className="font-extrabold text-slate-800">{part.totalCost.toLocaleString()}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* Interactive History Log - aligned standard filters and table headers */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6">
{/* Section title */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 border-b border-slate-100 pb-4">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg flex items-center gap-2">
<Activity className="w-5 h-5 text-indigo-500" />
<span>Serviced Fleet History Log</span>
</h3>
<p className="text-xs text-slate-400">Integrated audit list for EV bikes and Battery Swap maintenance nodes</p>
</div>
{/* Quick Counter */}
<span className="px-3 py-1 bg-indigo-50 border border-indigo-100/50 rounded-lg text-xs font-extrabold text-indigo-700 self-start lg:self-auto">
{sortedHistory.length} Matches Found
</span>
</div>
{/* Filter Controls Panel */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 bg-slate-50 p-4 rounded-lg border border-slate-100">
{/* Search bar */}
<div className="relative col-span-1 lg:col-span-2">
<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 ID, tech, description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 border border-slate-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all bg-white"
/>
</div>
{/* Filter 1: Asset Type */}
<select
value={assetFilter}
onChange={(e) => setAssetFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="all">All Asset Types</option>
<option value="EV Bike">EV Bike</option>
<option value="Battery">Battery</option>
</select>
{/* Filter 2: Service Type */}
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="all">All Service Types</option>
<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>
</select>
{/* Sorting Dropdown */}
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-bold text-indigo-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="date-desc">📆 Date: Newest First</option>
<option value="date-asc">📆 Date: Oldest First</option>
<option value="cost-desc"> Cost: Highest First</option>
<option value="cost-asc"> Cost: Lowest First</option>
<option value="urgency-desc"> Severity: Critical First</option>
<option value="urgency-asc"> Severity: Cosmetic First</option>
</select>
</div>
{/* Table / List representation */}
{sortedHistory.length === 0 ? (
<div className="text-center py-12 bg-slate-50 rounded-lg border border-dashed border-slate-200">
<Activity className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-sm font-semibold text-slate-500">No matching service records</p>
<p className="text-xs text-slate-400 mt-0.5">Try clearing filters or search variables</p>
</div>
) : (
<div className="overflow-x-auto border border-slate-100 rounded-lg">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 border-b border-slate-100 text-xs font-semibold uppercase tracking-wider text-slate-500">
<th className="py-3 px-4">Record ID</th>
<th className="py-3 px-4">Asset Code</th>
<th className="py-3 px-4">Service Type</th>
<th className="py-3 px-4">Description</th>
<th className="py-3 px-4 text-center">Severity</th>
<th className="py-3 px-4 text-center">Status</th>
<th className="py-3 px-4 text-right">Invoice cost</th>
<th className="py-3 px-4 text-right">Details</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 text-sm text-slate-700">
{sortedHistory.map(record => (
<tr key={record.id} className="hover:bg-slate-50/50 transition-colors">
<td className="py-4 px-4 font-bold text-slate-400">{record.id}</td>
<td className="py-4 px-4">
<div className="flex items-center gap-1.5">
{record.assetType === 'EV Bike' ? (
<Bike className="w-4 h-4 text-purple-600 flex-shrink-0" />
) : (
<Battery className="w-4 h-4 text-green-600 flex-shrink-0" />
)}
<div className="space-y-0.5">
<span className="font-extrabold text-slate-800">{record.assetId}</span>
<span className="text-[10px] text-slate-400 uppercase font-semibold block">{record.assetType}</span>
</div>
</div>
</td>
<td className="py-4 px-4 font-semibold text-slate-700">{record.serviceType}</td>
<td className="py-4 px-4 max-w-sm">
<div className="space-y-1">
<p className="text-xs text-slate-600 line-clamp-2 leading-relaxed">{record.description}</p>
<div className="flex items-center gap-3 text-[10px] text-slate-400 font-semibold">
<span className="flex items-center gap-0.5"><User className="w-3 h-3" /> Tech: {record.technician}</span>
<span className="flex items-center gap-0.5"><Calendar className="w-3 h-3" /> {record.date}</span>
</div>
</div>
</td>
<td className="py-4 px-4 text-center">
<span className={`px-2 py-0.5 text-xs font-semibold rounded-full border inline-block ${severityColors[record.severity]}`}>
{record.severity}
</span>
</td>
<td className="py-4 px-4 text-center">
<span className={`px-2.5 py-0.5 text-xs font-semibold rounded-full border inline-block ${statusHistoryColors[record.status]}`}>
{record.status.replace('_', ' ')}
</span>
</td>
<td className="py-4 px-4 text-right font-extrabold text-slate-800">
{record.actualCost > 0 ? (
<span>{record.actualCost.toLocaleString()}</span>
) : (
<span className="text-slate-400 font-normal italic text-xs">Pending invoice</span>
)}
</td>
<td className="py-4 px-4 text-right">
{/* Deep link details */}
<Link
href={`/admin/maintenance/${record.id}`}
className="p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 hover:text-slate-700 transition-colors inline-block"
title="Open full maintenance record"
>
<Eye className="w-4 h-4" />
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,271 @@
'use client';
import { Plus, X, Save, Battery } from 'lucide-react';
import type { CompanySettings } from '../page';
interface BatteryInvestmentSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activeBatteryTab: number;
setActiveBatteryTab: (n: number) => void;
addBatteryPlan: boolean;
setAddBatteryPlan: (v: boolean) => void;
newBatteryName: string;
setNewBatteryName: (v: string) => void;
newBatteryStatus: string;
setNewBatteryStatus: (v: string) => void;
newBatteryTarget: number;
setNewBatteryTarget: (n: number) => void;
newBatteryStart: string;
setNewBatteryStart: (v: string) => void;
newBatteryEnd: string;
setNewBatteryEnd: (v: string) => void;
newBatteryMin: number;
setNewBatteryMin: (n: number) => void;
newBatteryMax: number;
setNewBatteryMax: (n: number) => void;
newBatteryDuration: number;
setNewBatteryDuration: (n: number) => void;
newBatteryLock: number;
setNewBatteryLock: (n: number) => void;
newBatteryPenalty: number;
setNewBatteryPenalty: (n: number) => void;
newBatteryProfitShare: number;
setNewBatteryProfitShare: (n: number) => void;
newBatteryDesc: string;
setNewBatteryDesc: (v: string) => void;
newBatteryBasePrice: number;
setNewBatteryBasePrice: (n: number) => void;
newBatteryMinQuantity: number;
setNewBatteryMinQuantity: (n: number) => void;
createBatteryPlan: () => void;
handleSave: () => void;
}
export default function BatteryInvestmentSettings({
settings, setSettings,
activeBatteryTab, setActiveBatteryTab,
addBatteryPlan, setAddBatteryPlan,
newBatteryName, setNewBatteryName,
newBatteryStatus, setNewBatteryStatus,
newBatteryTarget, setNewBatteryTarget,
newBatteryStart, setNewBatteryStart,
newBatteryEnd, setNewBatteryEnd,
newBatteryMin, setNewBatteryMin,
newBatteryMax, setNewBatteryMax,
newBatteryDuration, setNewBatteryDuration,
newBatteryLock, setNewBatteryLock,
newBatteryPenalty, setNewBatteryPenalty,
newBatteryProfitShare, setNewBatteryProfitShare,
newBatteryDesc, setNewBatteryDesc,
newBatteryBasePrice, setNewBatteryBasePrice,
newBatteryMinQuantity, setNewBatteryMinQuantity,
createBatteryPlan, handleSave,
}: BatteryInvestmentSettingsProps) {
const calculatedMinInvestment = newBatteryMinQuantity * newBatteryBasePrice;
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800 flex items-center gap-2">
<Battery className="w-5 h-5 text-emerald-600 animate-pulse" />
Battery Investment Plans
</h3>
</div>
<div className="flex items-center justify-between bg-emerald-50 border border-emerald-200 rounded-xl p-4">
<div>
<h4 className="font-semibold text-emerald-800">Battery Investment Plans ({(settings.plans as any).batteryInvestment?.length || 0})</h4>
<p className="text-sm text-emerald-600">Manage high-yield battery pack EV Investment Plans for partners</p>
</div>
<button onClick={() => { setAddBatteryPlan(true); setNewBatteryName(''); }} className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-medium flex items-center gap-2 hover:bg-emerald-700 transition-colors">
<Plus className="w-4 h-4" /> New Plan
</button>
</div>
{addBatteryPlan && (
<div className="bg-white rounded-xl border border-emerald-300 overflow-hidden shadow-sm">
<div className="bg-emerald-50 px-4 py-3 border-b border-emerald-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-emerald-800">New Battery Investment Plan</h4>
<p className="text-sm text-emerald-600 mt-1">Configure high-yield battery fleet assets</p>
</div>
<button onClick={() => setAddBatteryPlan(false)} className="text-emerald-600 hover:text-emerald-800">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Name</label>
<input type="text" value={newBatteryName} onChange={(e) => setNewBatteryName(e.target.value)} placeholder="e.g., Standard Battery Plan" 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">Status</label>
<select value={newBatteryStatus} onChange={(e) => setNewBatteryStatus(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Target Amount ()</label>
<input type="number" value={newBatteryTarget} onChange={(e) => setNewBatteryTarget(parseInt(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">Start Date</label>
<input type="date" value={newBatteryStart} onChange={(e) => setNewBatteryStart(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">End Date</label>
<input type="date" value={newBatteryEnd} onChange={(e) => setNewBatteryEnd(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">Battery Base Price ()</label>
<input type="number" value={newBatteryBasePrice} onChange={(e) => setNewBatteryBasePrice(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" placeholder="Single battery unit cost" />
</div>
<div>
<label className="text-sm text-slate-600">Minimum Quantity (Packs)</label>
<input type="number" value={newBatteryMinQuantity} onChange={(e) => setNewBatteryMinQuantity(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" placeholder="Min battery packs to invest" />
</div>
<div>
<label className="text-sm text-slate-600">Min Investment ()</label>
<div className="flex items-center gap-2 mt-1">
<input type="number" value={calculatedMinInvestment} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100" />
<span className="text-xs text-slate-500 whitespace-nowrap">= Qty × Base Price</span>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Max Investment ()</label>
<input type="number" value={newBatteryMax} onChange={(e) => setNewBatteryMax(parseInt(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">Duration (Months)</label>
<input type="number" value={newBatteryDuration} onChange={(e) => setNewBatteryDuration(parseInt(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">Lock-in Period (Months)</label>
<input type="number" value={newBatteryLock} onChange={(e) => setNewBatteryLock(parseInt(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">Early Exit Penalty (%)</label>
<input type="number" value={newBatteryPenalty} onChange={(e) => setNewBatteryPenalty(parseInt(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">Profit Share Percent (%)</label>
<input type="number" value={newBatteryProfitShare} onChange={(e) => setNewBatteryProfitShare(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={newBatteryDesc} onChange={(e) => setNewBatteryDesc(e.target.value)} placeholder="Enter battery investment plan description" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<button onClick={createBatteryPlan} className="mt-4 px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-medium hover:bg-emerald-700 transition-colors">Create Plan</button>
</div>
</div>
)}
<div className="flex gap-2 border-b border-slate-200">
{((settings.plans as any).batteryInvestment || []).map((plan: any, idx: number) => (
<button key={idx} onClick={() => setActiveBatteryTab(idx)} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeBatteryTab === idx ? 'border-emerald-500 text-emerald-600 font-bold' : 'border-transparent text-slate-500 hover:text-slate-700'}`}> {plan.name}</button>
))}
</div>
{((settings.plans as any).batteryInvestment || []).length > 0 && ((settings.plans as any).batteryInvestment || []).map((plan: any, idx: number) => idx === activeBatteryTab && (
<div key={idx} className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
<div className="bg-emerald-50/50 px-4 py-3 border-b border-emerald-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-emerald-800">{plan.name}</h4>
<p className="text-sm text-emerald-600 mt-1">{plan.description}</p>
</div>
<div className="flex items-center gap-2">
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${plan.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{plan.status}</span>
<button
onClick={() => {
const updated = ((settings.plans as any).batteryInvestment || []).filter((_: any, i: number) => i !== idx);
setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } });
setActiveBatteryTab(0);
}}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Plan"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Name</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Status</label>
<select value={plan.status} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].status = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Target Amount ()</label>
<input type="number" value={plan.targetAmount} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].targetAmount = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Start Date</label>
<input type="date" value={plan.startDate} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].startDate = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">End Date</label>
<input type="date" value={plan.endDate} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].endDate = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Battery Base Price ()</label>
<input type="number" value={plan.batteryBasePrice} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].batteryBasePrice = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Minimum Quantity (Packs)</label>
<input type="number" value={plan.minQuantity} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].minQuantity = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Min Investment ()</label>
<div className="flex items-center gap-2 mt-1">
<input type="number" value={plan.batteryBasePrice * plan.minQuantity} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100" />
<span className="text-xs text-slate-500 whitespace-nowrap">= Qty × Price</span>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Max Investment ()</label>
<input type="number" value={plan.maxInvestment} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].maxInvestment = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Duration (Months)</label>
<input type="number" value={plan.durationMonths} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].durationMonths = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Lock-in Period (Months)</label>
<input type="number" value={plan.lockInMonths} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].lockInMonths = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Early Exit Penalty (%)</label>
<input type="number" value={plan.earlyExitPenalty} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].earlyExitPenalty = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} 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">Profit Share Percent (%)</label>
<input type="number" value={plan.profitSharePercent} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].profitSharePercent = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<div className="mt-4 flex justify-end">
<button onClick={handleSave} className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-medium flex items-center gap-2 hover:bg-emerald-700 transition-colors shadow-sm">
<Save className="w-4 h-4" /> Save Changes
</button>
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -72,13 +72,13 @@ export default function InvestmentSettings({
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800">Investment Plans</h3> <h3 className="text-lg font-semibold text-slate-800">EV Investment Plans</h3>
</div> </div>
<div className="flex items-center justify-between bg-amber-50 border border-amber-200 rounded-xl p-4"> <div className="flex items-center justify-between bg-amber-50 border border-amber-200 rounded-xl p-4">
<div> <div>
<h4 className="font-semibold text-amber-800">Investment Plans ({settings.plans.investment.length})</h4> <h4 className="font-semibold text-amber-800">EV Investment Plans ({settings.plans.investment.length})</h4>
<p className="text-sm text-amber-600">Manage investment plans for investors</p> <p className="text-sm text-amber-600">Manage EV Investment Plans for investors</p>
</div> </div>
<button onClick={() => { setAddInvestPlan(true); setNewInvestName(''); }} className="px-4 py-2 bg-amber-600 text-white rounded-lg text-sm font-medium flex items-center gap-2"> <button onClick={() => { setAddInvestPlan(true); setNewInvestName(''); }} className="px-4 py-2 bg-amber-600 text-white rounded-lg text-sm font-medium flex items-center gap-2">
<Plus className="w-4 h-4" /> New Plan <Plus className="w-4 h-4" /> New Plan

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Plus, Save, Trash2, X } from 'lucide-react'; import { Plus, Save, Trash2, X, Gift } from 'lucide-react';
import { CompanySettings } from '../page'; import { CompanySettings } from '../page';
interface PlanSelectionProps { interface PlanSelectionProps {
@@ -14,6 +14,108 @@ interface PlanSelectionProps {
isDirty?: boolean; isDirty?: boolean;
} }
// Reusable Free Service Conditions editor
function FreeServiceConditions({
conditions,
accentColor,
onChange,
}: {
conditions: { months: number; freeServices: number }[];
accentColor: string;
onChange: (updated: { months: number; freeServices: number }[]) => void;
}) {
const addCondition = () => {
onChange([...conditions, { months: 3, freeServices: 1 }]);
};
const removeCondition = (i: number) => {
onChange(conditions.filter((_, idx) => idx !== i));
};
const updateCondition = (i: number, field: 'months' | 'freeServices', value: number) => {
const updated = conditions.map((c, idx) => idx === i ? { ...c, [field]: value } : c);
onChange(updated);
};
return (
<div className="bg-amber-50 border border-amber-100 rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Gift className="w-4 h-4 text-amber-600" />
<label className="text-xs font-semibold text-amber-700 uppercase tracking-wide">
Free Service Conditions
</label>
<span className="text-[10px] text-amber-500 font-medium bg-amber-100 px-2 py-0.5 rounded-full">
e.g. "3 months → 2 free services"
</span>
</div>
<button
type="button"
onClick={addCondition}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-semibold transition-all ${accentColor} text-white hover:opacity-90`}
>
<Plus className="w-3 h-3" /> Add Condition
</button>
</div>
{conditions.length === 0 && (
<p className="text-xs text-amber-400 italic text-center py-2">
No free service conditions set. Click "Add Condition" to add one.
</p>
)}
<div className="space-y-2">
{conditions.map((cond, i) => (
<div key={i} className="flex items-center gap-3 bg-white border border-amber-100 rounded-lg px-3 py-2 group">
{/* Month input */}
<div className="flex items-center gap-1.5">
<label className="text-xs text-slate-500 font-medium shrink-0">Month:</label>
<input
type="number"
min={1}
max={999}
value={cond.months}
onChange={(e) => updateCondition(i, 'months', parseInt(e.target.value) || 1)}
className="w-16 px-2 py-1 border border-slate-200 rounded-md text-xs text-slate-800 text-center font-semibold focus:outline-none focus:ring-1 focus:ring-amber-400"
/>
</div>
<span className="text-slate-300 text-sm"></span>
{/* Free services input */}
<div className="flex items-center gap-1.5">
<label className="text-xs text-slate-500 font-medium shrink-0">Free Services:</label>
<input
type="number"
min={1}
max={99}
value={cond.freeServices}
onChange={(e) => updateCondition(i, 'freeServices', parseInt(e.target.value) || 1)}
className="w-16 px-2 py-1 border border-slate-200 rounded-md text-xs text-slate-800 text-center font-semibold focus:outline-none focus:ring-1 focus:ring-amber-400"
/>
</div>
{/* Preview badge */}
<span className="flex-1 text-[10px] font-bold text-amber-700 bg-amber-50 border border-amber-100 rounded-full px-2.5 py-1 text-center truncate">
{cond.months} {cond.months === 1 ? 'month' : 'months'} {cond.freeServices} free service{cond.freeServices !== 1 ? 's' : ''} free
</span>
{/* Remove */}
<button
type="button"
onClick={() => removeCondition(i)}
className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded-md transition-all opacity-0 group-hover:opacity-100"
title="Remove condition"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</div>
);
}
export default function PlanSelection({ export default function PlanSelection({
settings, settings,
setSettings, setSettings,
@@ -172,6 +274,18 @@ export default function PlanSelection({
</div> </div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>} {plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</div> </div>
{/* Free Service Conditions */}
<FreeServiceConditions
conditions={plan.freeServiceConditions ?? []}
accentColor="bg-blue-600"
onChange={(updated) => {
const plans = [...settings.plans.singleRent];
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
setSettings({ ...settings, plans: { ...settings.plans, singleRent: plans } });
}}
/>
<div> <div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label> <label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." /> <textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
@@ -312,6 +426,18 @@ export default function PlanSelection({
</div> </div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>} {plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</div> </div>
{/* Free Service Conditions */}
<FreeServiceConditions
conditions={plan.freeServiceConditions ?? []}
accentColor="bg-purple-600"
onChange={(updated) => {
const plans = [...settings.plans.rentToOwn];
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: plans } });
}}
/>
<div> <div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label> <label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." /> <textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
@@ -452,6 +578,18 @@ export default function PlanSelection({
</div> </div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>} {plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</div> </div>
{/* Free Service Conditions */}
<FreeServiceConditions
conditions={plan.freeServiceConditions ?? []}
accentColor="bg-green-600"
onChange={(updated) => {
const plans = [...settings.plans.shareEv];
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
setSettings({ ...settings, plans: { ...settings.plans, shareEv: plans } });
}}
/>
<div> <div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label> <label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." /> <textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
@@ -465,26 +603,16 @@ export default function PlanSelection({
</div> </div>
)} )}
</div> </div>
{deleteModal.type !== null && deleteModal.idx !== null && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> {/* Delete Confirmation Modal */}
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm"> {deleteModal.type !== null && (
<div className="p-4 border-b border-slate-100 flex justify-between items-center"> <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<h3 className="font-semibold text-slate-800 flex items-center gap-2"> <div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 space-y-4">
<Trash2 className="w-5 h-5 text-red-500" /> Delete Plan <h3 className="text-lg font-bold text-slate-800">Delete Plan?</h3>
</h3> <p className="text-sm text-slate-500">This will permanently remove the plan. This action cannot be undone.</p>
<button onClick={() => setDeleteModal({ type: null, idx: null })} className="text-slate-400 hover:text-slate-600 text-2xl">&times;</button> <div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
</div> <button onClick={() => setDeleteModal({ type: null, idx: null })} className="px-4 py-2 border border-slate-200 text-slate-500 rounded-lg text-sm font-medium hover:bg-slate-50">Cancel</button>
<div className="p-4"> <button onClick={handleDeletePlan} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700">Delete Plan</button>
<p className="text-sm text-slate-600">Are you sure you want to delete this plan? This action cannot be undone.</p>
{deleteModal.type && deleteModal.idx !== null && settings.plans[deleteModal.type][deleteModal.idx] && (
<div className="mt-3 p-3 bg-slate-50 rounded-lg">
<p className="text-sm font-medium text-slate-700">{settings.plans[deleteModal.type][deleteModal.idx].name}</p>
</div>
)}
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setDeleteModal({ type: null, idx: null })} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm">Cancel</button>
<button onClick={handleDeletePlan} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700">Delete Plan</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Settings, Package, Palette, Link2, Mail, Monitor, FileCheck, DollarSign, Zap, Users, Plus, X, Save, Pencil, Trash2, FileText } from 'lucide-react'; import { Settings, Package, Palette, Link2, Mail, Monitor, FileCheck, DollarSign, Zap, Users, Plus, X, Save, Pencil, Trash2, FileText, Battery } from 'lucide-react';
import RichTextEditor from '@/components/RichTextEditor'; import RichTextEditor from '@/components/RichTextEditor';
import GeneralSettings from './components/GeneralSettings'; import GeneralSettings from './components/GeneralSettings';
import BrandingSettings from './components/BrandingSettings'; import BrandingSettings from './components/BrandingSettings';
@@ -13,6 +13,7 @@ import PartsSettings from './components/PartsSettings';
import CompanyPolicySettings from './components/CompanyPolicySettings'; import CompanyPolicySettings from './components/CompanyPolicySettings';
import PlanSelection from './components/PlanSelection'; import PlanSelection from './components/PlanSelection';
import InvestmentSettings from './components/InvestmentSettings'; import InvestmentSettings from './components/InvestmentSettings';
import BatteryInvestmentSettings from './components/BatteryInvestmentSettings';
import SwapStationSettings from './components/SwapStationSettings'; import SwapStationSettings from './components/SwapStationSettings';
import RiderRequestSettings from './components/RiderRequestSettings'; import RiderRequestSettings from './components/RiderRequestSettings';
import EmailSMSTemplates from './components/EmailSMSTemplates'; import EmailSMSTemplates from './components/EmailSMSTemplates';
@@ -124,6 +125,7 @@ export interface CompanySettings {
monthlyPenalty2: number; monthlyPenalty2: number;
monthlyPenalty3: number; monthlyPenalty3: number;
ficoSharePercent: number; ficoSharePercent: number;
freeServiceConditions: { months: number; freeServices: number }[];
description: string; description: string;
}[]; }[];
rentToOwn: { rentToOwn: {
@@ -150,6 +152,7 @@ export interface CompanySettings {
profit: number; profit: number;
ficoRentSharePercent: number; ficoRentSharePercent: number;
ficoProfitSharePercent: number; ficoProfitSharePercent: number;
freeServiceConditions: { months: number; freeServices: number }[];
description: string; description: string;
}[]; }[];
shareEv: { shareEv: {
@@ -178,6 +181,7 @@ export interface CompanySettings {
monthlyPenalty3: number; monthlyPenalty3: number;
totalMonthlySubscription: number; totalMonthlySubscription: number;
ficoSharePercent: number; ficoSharePercent: number;
freeServiceConditions: { months: number; freeServices: number }[];
description: string; description: string;
}[]; }[];
investment: { investment: {
@@ -201,6 +205,24 @@ export interface CompanySettings {
ficoRentToOwn: number; ficoRentToOwn: number;
ficoShareEv: number; ficoShareEv: number;
}[]; }[];
batteryInvestment: {
id: string;
tier: string;
name: string;
minQuantity: number;
batteryBasePrice: number;
minInvestment: number;
maxInvestment: number;
durationMonths: number;
profitSharePercent: number;
lockInMonths: number;
earlyExitPenalty: number;
startDate: string;
endDate: string;
targetAmount: number;
status: string;
description: string;
}[];
swapStation: { swapStation: {
id: string; id: string;
name: string; name: string;
@@ -479,6 +501,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty2: 30000, monthlyPenalty2: 30000,
monthlyPenalty3: 50000, monthlyPenalty3: 50000,
ficoSharePercent: 50, ficoSharePercent: 50,
freeServiceConditions: [
{ months: 3, freeServices: 2 },
{ months: 6, freeServices: 4 },
],
description: 'Premium single person rental plan with extra benefits', description: 'Premium single person rental plan with extra benefits',
}, },
{ {
@@ -500,6 +526,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty2: 22000, monthlyPenalty2: 22000,
monthlyPenalty3: 40000, monthlyPenalty3: 40000,
ficoSharePercent: 45, ficoSharePercent: 45,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
{ months: 3, freeServices: 2 },
],
description: 'Standard single person rental plan', description: 'Standard single person rental plan',
}, },
{ {
@@ -521,6 +551,9 @@ const initialSettings: CompanySettings = {
monthlyPenalty2: 18000, monthlyPenalty2: 18000,
monthlyPenalty3: 30000, monthlyPenalty3: 30000,
ficoSharePercent: 40, ficoSharePercent: 40,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
],
description: 'Economy single person rental plan', description: 'Economy single person rental plan',
} }
], ],
@@ -549,6 +582,10 @@ const initialSettings: CompanySettings = {
profit: 20000, profit: 20000,
ficoRentSharePercent: 50, ficoRentSharePercent: 50,
ficoProfitSharePercent: 45, ficoProfitSharePercent: 45,
freeServiceConditions: [
{ months: 3, freeServices: 2 },
{ months: 6, freeServices: 4 },
],
description: 'Premium rent to own plan with high-end EV', description: 'Premium rent to own plan with high-end EV',
}, },
{ {
@@ -575,6 +612,10 @@ const initialSettings: CompanySettings = {
profit: 15000, profit: 15000,
ficoRentSharePercent: 45, ficoRentSharePercent: 45,
ficoProfitSharePercent: 45, ficoProfitSharePercent: 45,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
{ months: 3, freeServices: 2 },
],
description: 'Standard rent to own plan', description: 'Standard rent to own plan',
}, },
{ {
@@ -601,6 +642,9 @@ const initialSettings: CompanySettings = {
profit: 15000, profit: 15000,
ficoRentSharePercent: 40, ficoRentSharePercent: 40,
ficoProfitSharePercent: 40, ficoProfitSharePercent: 40,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
],
description: 'Economy rent to own plan', description: 'Economy rent to own plan',
} }
], ],
@@ -631,6 +675,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty3: 35000, monthlyPenalty3: 35000,
totalMonthlySubscription: 16800, totalMonthlySubscription: 16800,
ficoSharePercent: 50, ficoSharePercent: 50,
freeServiceConditions: [
{ months: 3, freeServices: 2 },
{ months: 6, freeServices: 4 },
],
description: 'Premium shared EV with premium bikes', description: 'Premium shared EV with premium bikes',
}, },
{ {
@@ -659,6 +707,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty3: 25000, monthlyPenalty3: 25000,
totalMonthlySubscription: 11200, totalMonthlySubscription: 11200,
ficoSharePercent: 45, ficoSharePercent: 45,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
{ months: 3, freeServices: 2 },
],
description: 'Standard shared EV plan', description: 'Standard shared EV plan',
}, },
{ {
@@ -687,6 +739,9 @@ const initialSettings: CompanySettings = {
monthlyPenalty3: 20000, monthlyPenalty3: 20000,
totalMonthlySubscription: 8400, totalMonthlySubscription: 8400,
ficoSharePercent: 40, ficoSharePercent: 40,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
],
description: 'Economy shared EV plan', description: 'Economy shared EV plan',
} }
], ],
@@ -734,6 +789,44 @@ const initialSettings: CompanySettings = {
ficoShareEv: 60, ficoShareEv: 60,
}, },
], ],
batteryInvestment: [
{
id: 'bat_inv_demo_1',
name: 'Standard Battery Plan',
tier: 'Economy',
minQuantity: 10,
batteryBasePrice: 15000,
minInvestment: 150000,
maxInvestment: 500000,
durationMonths: 12,
profitSharePercent: 40,
lockInMonths: 3,
earlyExitPenalty: 10,
startDate: '2026-01-01',
endDate: '2026-12-31',
targetAmount: 500000,
status: 'active',
description: 'Investment plan for 10 batteries - entry level battery investment',
},
{
id: 'bat_inv_demo_2',
name: 'Premium Battery Plan',
tier: 'Premium',
minQuantity: 50,
batteryBasePrice: 15000,
minInvestment: 750000,
maxInvestment: 2000000,
durationMonths: 24,
profitSharePercent: 50,
lockInMonths: 6,
earlyExitPenalty: 15,
startDate: '2026-01-01',
endDate: '2026-12-31',
targetAmount: 2000000,
status: 'active',
description: 'Investment plan for 50 batteries - premium scale battery investment',
},
],
swapStation: [ swapStation: [
{ {
id: 'ss_1', id: 'ss_1',
@@ -814,7 +907,26 @@ const initialSettings: CompanySettings = {
export default function CompanySettingsPage() { export default function CompanySettingsPage() {
const [settings, setSettings] = useState<CompanySettings>(initialSettings); const [settings, setSettings] = useState<CompanySettings>(initialSettings);
const [activeTab, setActiveTab] = useState<'general' | 'branding' | 'social' | 'integration' | 'landing' | 'kyc' | 'parts' | 'companyPolicy' | 'plans' | 'investment' | 'swapstation' | 'riderrequest' | 'templates'>('general');
useEffect(() => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('companySettings');
if (stored) {
try {
setSettings(JSON.parse(stored));
} catch (e) {
console.error(e);
}
}
}
}, []);
useEffect(() => {
if (typeof window !== 'undefined' && settings !== initialSettings) {
localStorage.setItem('companySettings', JSON.stringify(settings));
}
}, [settings]);
const [activeTab, setActiveTab] = useState<'general' | 'branding' | 'social' | 'integration' | 'landing' | 'kyc' | 'parts' | 'companyPolicy' | 'plans' | 'investment' | 'batteryinvestment' | 'swapstation' | 'riderrequest' | 'templates'>('general');
const [activeMasterTab, setActiveMasterTab] = useState<'investor' | 'merchant' | 'swapstation' | 'rentalType'>('investor'); const [activeMasterTab, setActiveMasterTab] = useState<'investor' | 'merchant' | 'swapstation' | 'rentalType'>('investor');
const [activeRentalTypeTab, setActiveRentalTypeTab] = useState<'single' | 'shared' | 'renttoown'>('single'); const [activeRentalTypeTab, setActiveRentalTypeTab] = useState<'single' | 'shared' | 'renttoown'>('single');
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
@@ -837,6 +949,8 @@ export default function CompanySettingsPage() {
const [newPolicyDesc, setNewPolicyDesc] = useState(''); const [newPolicyDesc, setNewPolicyDesc] = useState('');
const [newPolicyShowApp, setNewPolicyShowApp] = useState(true); const [newPolicyShowApp, setNewPolicyShowApp] = useState(true);
const [newPolicyShowWeb, setNewPolicyShowWeb] = useState(true); const [newPolicyShowWeb, setNewPolicyShowWeb] = useState(true);
// Bike Investment
const [addInvestPlan, setAddInvestPlan] = useState(false); const [addInvestPlan, setAddInvestPlan] = useState(false);
const [newInvestName, setNewInvestName] = useState(''); const [newInvestName, setNewInvestName] = useState('');
const [newInvestTier, setNewInvestTier] = useState('Standard'); const [newInvestTier, setNewInvestTier] = useState('Standard');
@@ -857,6 +971,24 @@ export default function CompanySettingsPage() {
const [newInvestEvBasePrice, setNewInvestEvBasePrice] = useState(200000); const [newInvestEvBasePrice, setNewInvestEvBasePrice] = useState(200000);
const [newInvestMinQuantity, setNewInvestMinQuantity] = useState(1); const [newInvestMinQuantity, setNewInvestMinQuantity] = useState(1);
// Battery Investment
const [activeBatteryTab, setActiveBatteryTab] = useState(0);
const [addBatteryPlan, setAddBatteryPlan] = useState(false);
const [newBatteryName, setNewBatteryName] = useState('');
const [newBatteryStatus, setNewBatteryStatus] = useState('active');
const [newBatteryTarget, setNewBatteryTarget] = useState(1000000);
const [newBatteryStart, setNewBatteryStart] = useState('2026-01-01');
const [newBatteryEnd, setNewBatteryEnd] = useState('2026-12-31');
const [newBatteryMin, setNewBatteryMin] = useState(150000);
const [newBatteryMax, setNewBatteryMax] = useState(500000);
const [newBatteryDuration, setNewBatteryDuration] = useState(12);
const [newBatteryLock, setNewBatteryLock] = useState(3);
const [newBatteryPenalty, setNewBatteryPenalty] = useState(10);
const [newBatteryProfitShare, setNewBatteryProfitShare] = useState(40);
const [newBatteryDesc, setNewBatteryDesc] = useState('');
const [newBatteryBasePrice, setNewBatteryBasePrice] = useState(15000);
const [newBatteryMinQuantity, setNewBatteryMinQuantity] = useState(10);
const createInvestPlan = () => { const createInvestPlan = () => {
if (newInvestName.trim() && typeof window !== 'undefined') { if (newInvestName.trim() && typeof window !== 'undefined') {
const newPlan = { const newPlan = {
@@ -888,6 +1020,35 @@ export default function CompanySettingsPage() {
} }
}; };
const createBatteryPlan = () => {
if (newBatteryName.trim() && typeof window !== 'undefined') {
const newPlan = {
id: 'bat_inv_' + Date.now(),
name: newBatteryName,
tier: newBatteryMinQuantity >= 50 ? 'Premium' : 'Economy',
batteryBasePrice: newBatteryBasePrice,
minQuantity: newBatteryMinQuantity,
minInvestment: newBatteryBasePrice * newBatteryMinQuantity,
maxInvestment: newBatteryMax,
durationMonths: newBatteryDuration,
profitSharePercent: newBatteryProfitShare,
lockInMonths: newBatteryLock,
earlyExitPenalty: newBatteryPenalty,
startDate: newBatteryStart,
endDate: newBatteryEnd,
targetAmount: newBatteryTarget,
status: newBatteryStatus,
description: newBatteryDesc,
};
const updatedPlans = [...(settings.plans.batteryInvestment || []), newPlan];
updateSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updatedPlans } });
setActiveBatteryTab(updatedPlans.length - 1);
setAddBatteryPlan(false);
setNewBatteryName('');
}
};
const [activeSwapTab, setActiveSwapTab] = useState(0); const [activeSwapTab, setActiveSwapTab] = useState(0);
const [addSwapStationPlan, setAddSwapStationPlan] = useState(false); const [addSwapStationPlan, setAddSwapStationPlan] = useState(false);
const [newSwapName, setNewSwapName] = useState(''); const [newSwapName, setNewSwapName] = useState('');
@@ -1002,6 +1163,7 @@ export default function CompanySettingsPage() {
monthlyPenalty2: 22000, monthlyPenalty2: 22000,
monthlyPenalty3: 40000, monthlyPenalty3: 40000,
ficoSharePercent: 45, ficoSharePercent: 45,
freeServiceConditions: [],
description: '', description: '',
} : type === 'rentToOwn' ? { } : type === 'rentToOwn' ? {
id: newId, id: newId,
@@ -1027,6 +1189,7 @@ export default function CompanySettingsPage() {
profit: 15000, profit: 15000,
ficoRentSharePercent: 45, ficoRentSharePercent: 45,
ficoProfitSharePercent: 45, ficoProfitSharePercent: 45,
freeServiceConditions: [],
description: '', description: '',
} : { } : {
id: newId, id: newId,
@@ -1054,6 +1217,7 @@ export default function CompanySettingsPage() {
monthlyPenalty3: 25000, monthlyPenalty3: 25000,
totalMonthlySubscription: 11200, totalMonthlySubscription: 11200,
ficoSharePercent: 45, ficoSharePercent: 45,
freeServiceConditions: [],
description: '', description: '',
}; };
if (type === 'singleRent') { if (type === 'singleRent') {
@@ -1123,7 +1287,8 @@ export default function CompanySettingsPage() {
{ id: 'kyc', label: 'KYC Documents', icon: Package }, { id: 'kyc', label: 'KYC Documents', icon: Package },
{ id: 'plans', label: 'Plan Selection', icon: Package }, { id: 'plans', label: 'Plan Selection', icon: Package },
{ id: 'investment', label: 'Investment Plan', icon: DollarSign }, { id: 'investment', label: 'EV Investment Plan', icon: DollarSign },
{ id: 'batteryinvestment', label: 'Battery Investment Plan', icon: Battery },
{ id: 'swapstation', label: 'Swap Station Plan (P3)', icon: Zap }, { id: 'swapstation', label: 'Swap Station Plan (P3)', icon: Zap },
{ id: 'riderrequest', label: 'Rider Request Plan (P2)', icon: Users }, { id: 'riderrequest', label: 'Rider Request Plan (P2)', icon: Users },
{ id: 'parts', label: 'EV Parts', icon: Package }, { id: 'parts', label: 'EV Parts', icon: Package },
@@ -1316,6 +1481,29 @@ export default function CompanySettingsPage() {
/> />
)} )}
{activeTab === 'batteryinvestment' && (
<BatteryInvestmentSettings
settings={settings} setSettings={setSettings}
activeBatteryTab={activeBatteryTab} setActiveBatteryTab={setActiveBatteryTab}
addBatteryPlan={addBatteryPlan} setAddBatteryPlan={setAddBatteryPlan}
newBatteryName={newBatteryName} setNewBatteryName={setNewBatteryName}
newBatteryStatus={newBatteryStatus} setNewBatteryStatus={setNewBatteryStatus}
newBatteryTarget={newBatteryTarget} setNewBatteryTarget={setNewBatteryTarget}
newBatteryStart={newBatteryStart} setNewBatteryStart={setNewBatteryStart}
newBatteryEnd={newBatteryEnd} setNewBatteryEnd={setNewBatteryEnd}
newBatteryMin={newBatteryMin} setNewBatteryMin={setNewBatteryMin}
newBatteryMax={newBatteryMax} setNewBatteryMax={setNewBatteryMax}
newBatteryDuration={newBatteryDuration} setNewBatteryDuration={setNewBatteryDuration}
newBatteryLock={newBatteryLock} setNewBatteryLock={setNewBatteryLock}
newBatteryPenalty={newBatteryPenalty} setNewBatteryPenalty={setNewBatteryPenalty}
newBatteryProfitShare={newBatteryProfitShare} setNewBatteryProfitShare={setNewBatteryProfitShare}
newBatteryDesc={newBatteryDesc} setNewBatteryDesc={setNewBatteryDesc}
newBatteryBasePrice={newBatteryBasePrice} setNewBatteryBasePrice={setNewBatteryBasePrice}
newBatteryMinQuantity={newBatteryMinQuantity} setNewBatteryMinQuantity={setNewBatteryMinQuantity}
createBatteryPlan={createBatteryPlan} handleSave={handleSave}
/>
)}
{ {
activeTab === 'swapstation' && ( activeTab === 'swapstation' && (
<SwapStationSettings <SwapStationSettings

View File

@@ -1,183 +1,247 @@
'use client';
import { useState, useEffect } from 'react';
import { investors, bikes, transactions } from '@/data/mockData'; import { investors, bikes, transactions } from '@/data/mockData';
import { Wallet, TrendingUp, Bike, Target, DollarSign, FileText, Phone, BarChart3, Clock, ArrowRight, ShieldCheck, Zap, AlertCircle } from 'lucide-react'; import { Wallet, TrendingUp, Bike, Target, DollarSign, FileText, Phone, BarChart3, Clock, ArrowRight, ShieldCheck, Zap, AlertCircle, Download, X, ExternalLink } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import TransactionList from '@/components/TransactionList'; import TransactionList from '@/components/TransactionList';
import InvestorNotification from '@/components/InvestorNotification';
export default function InvestorDashboardPage() { export default function InvestorDashboardPage() {
const investor = investors[0]; // mock logged-in investor const investor = investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor?.id); const investorBikes = bikes.filter(b => b.investorId === investor?.id);
const recentTransactions = transactions.filter(t => t.investorId === investor.id).slice(0, 5); const recentTransactions = transactions.filter(t => t.investorId === investor.id).slice(0, 5);
const availableBalance = investor.totalEarnings - investor.totalWithdrawn - investor.withdrawalPending; const availableBalance = investor.totalEarnings - investor.totalWithdrawn - investor.withdrawalPending;
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const [showInstallBanner, setShowInstallBanner] = useState(false);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
setShowInstallBanner(true);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setDeferredPrompt(null);
}
setShowInstallBanner(false);
};
const dismissBanner = () => {
setShowInstallBanner(false);
localStorage.setItem('pwa_install_dismissed', 'true');
};
useEffect(() => {
if (localStorage.getItem('pwa_install_dismissed') === 'true') {
setShowInstallBanner(false);
}
}, []);
return ( return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto mb-20 lg:mb-0"> <div className="min-h-screen lg:pt-6 pt-0">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6"> <InvestorNotification isMobile />
<div> <div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Welcome back, {investor.name.split(' ')[0]} 👋</h1> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6">
<p className="text-sm text-slate-500">Here's what's happening with your investments today.</p> <div>
</div> <h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Welcome back, {investor.name.split(' ')[0]} 👋</h1>
<div className="flex items-center gap-2"> <p className="text-sm text-slate-500">Here&apos;s what&apos;s happening with your investments today.</p>
{investor.kycStatus === 'verified' ? (
<span className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-green-50 border border-green-100 text-green-700 text-xs font-bold">
<ShieldCheck className="w-4 h-4" /> KYC Verified
</span>
) : (
<Link href="/investor/profile" className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-amber-50 border border-amber-200 text-amber-700 text-xs font-bold hover:bg-amber-100 transition-colors">
<AlertCircle className="w-4 h-4" /> Complete KYC
</Link>
)}
<Link href="/investor/plans" className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-bold hover:bg-investor-dark transition-colors flex items-center gap-2 shadow-sm shadow-investor/20">
<Zap className="w-4 h-4" /> New Investment
</Link>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-investor/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-purple-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
<Wallet className="w-5 h-5 text-purple-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Total Invested</p>
</div> </div>
<p className="text-2xl font-extrabold text-slate-800 relative z-10">{(investor.totalInvested / 1000).toFixed(0)}k</p> <div className="flex items-center gap-2">
</div> {investor.kycStatus === 'verified' ? (
<span className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-green-50 border border-green-100 text-green-700 text-xs font-bold">
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-green-500/30 transition-all"> <ShieldCheck className="w-4 h-4" /> KYC Verified
<div className="absolute -right-4 -top-4 w-16 h-16 bg-green-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div> </span>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Total Earnings</p>
</div>
<p className="text-2xl font-extrabold text-green-600 relative z-10">{(investor.totalEarnings / 1000).toFixed(1)}k</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-amber-500/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-amber-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center">
<Bike className="w-5 h-5 text-amber-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Active Bikes</p>
</div>
<p className="text-2xl font-extrabold text-amber-600 relative z-10">{investor.activeBikes}</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-blue-500/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-blue-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
<Target className="w-5 h-5 text-blue-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Avg. ROI</p>
</div>
<p className="text-2xl font-extrabold text-blue-600 relative z-10">{investor.roi}%</p>
</div>
</div>
<div className="grid lg:grid-cols-3 gap-6 mb-6">
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800 flex items-center gap-2">
<Bike className="w-5 h-5 text-slate-400" /> My Portfolio Overview
</h2>
<Link href="/investor/plans" className="text-sm font-semibold text-investor hover:text-investor-dark flex items-center gap-1">
View All <ArrowRight className="w-4 h-4" />
</Link>
</div>
<div className="p-5 flex-1">
{investorBikes.length > 0 ? (
<div className="space-y-4">
{investorBikes.slice(0, 3).map(bike => (
<Link
key={bike.id}
href={`/investor/investments/${investor.investments?.[0].id || 'ip1'}`}
className="flex items-center gap-4 p-3 hover:bg-slate-50 rounded-xl transition-colors border border-transparent hover:border-slate-100 group"
>
<div className="w-12 h-12 bg-slate-100 rounded-lg overflow-hidden shrink-0">
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-slate-800 truncate group-hover:text-investor transition-colors">{bike.model}</h4>
<p className="text-xs text-slate-500">{bike.plateNumber}</p>
</div>
<div className="text-right">
<p className="font-bold text-green-600">{bike.currentRent || 0}</p>
<p className="text-[10px] text-slate-400 uppercase">Daily Rent</p>
</div>
<div>
<span className={`inline-flex px-2 py-1 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' :
bike.status === 'available' ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'
}`}>
{bike.status}
</span>
</div>
</Link>
))}
</div>
) : ( ) : (
<div className="h-full flex flex-col items-center justify-center text-center py-8"> <Link href="/investor/profile" className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-amber-50 border border-amber-200 text-amber-700 text-xs font-bold hover:bg-amber-100 transition-colors">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mb-3"> <AlertCircle className="w-4 h-4" /> Complete KYC
<Bike className="w-6 h-6 text-slate-300" /> </Link>
</div>
<p className="text-sm font-semibold text-slate-700">No bikes assigned yet</p>
<p className="text-xs text-slate-500 mt-1 max-w-[200px]">Once you make an investment, assigned bikes will appear here.</p>
</div>
)} )}
{/* <Link href="/investor/plans" className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-bold hover:bg-investor-dark transition-colors flex items-center gap-2 shadow-sm shadow-investor/20">
<Zap className="w-4 h-4" /> New Investment
</Link> */}
</div> </div>
</div> </div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="p-5 border-b border-slate-100"> <div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-investor/30 transition-all">
<h2 className="font-bold text-slate-800">Quick Actions</h2> <div className="absolute -right-4 -top-4 w-16 h-16 bg-purple-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
<Wallet className="w-5 h-5 text-purple-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Total Invested</p>
</div>
<p className="text-2xl font-extrabold text-slate-800 relative z-10">{(investor.totalInvested / 1000).toFixed(0)}k</p>
</div> </div>
<div className="p-5 flex-1 flex flex-col gap-3">
<div className="bg-investor/5 border border-investor/10 rounded-xl p-4 mb-2"> <div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-green-500/30 transition-all">
<p className="text-xs text-investor font-semibold mb-1 uppercase">Available to Withdraw</p> <div className="absolute -right-4 -top-4 w-16 h-16 bg-green-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<p className="text-2xl font-extrabold text-investor mb-3">{availableBalance.toLocaleString()}</p> <div className="flex items-center gap-3 mb-2 relative z-10">
<Link href="/investor/withdraw" className="w-full py-2.5 bg-investor text-white rounded-lg font-bold text-sm hover:bg-investor-dark flex items-center justify-center gap-2 shadow-sm shadow-investor/20 transition-all"> <div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center">
<DollarSign className="w-4 h-4" /> Withdraw Funds <TrendingUp className="w-5 h-5 text-green-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Total Earnings</p>
</div>
<p className="text-2xl font-extrabold text-green-600 relative z-10">{(investor.totalEarnings / 1000).toFixed(1)}k</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-amber-500/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-amber-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center">
<Bike className="w-5 h-5 text-amber-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Active Bikes</p>
</div>
<p className="text-2xl font-extrabold text-amber-600 relative z-10">{investor.activeBikes}</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-blue-500/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-blue-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
<Target className="w-5 h-5 text-blue-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Avg. ROI</p>
</div>
<p className="text-2xl font-extrabold text-blue-600 relative z-10">{investor.roi}%</p>
</div>
</div>
{/* PWA Install Banner */}
{showInstallBanner && (
<div className="bg-gradient-to-r from-investor/10 to-purple-50 border border-investor/20 rounded-2xl p-4 mb-6 animate-in fade-in slide-in-from-bottom-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-investor to-investor-light rounded-xl flex items-center justify-center shrink-0 shadow-lg">
<Download className="w-6 h-6 text-white" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-slate-800">Install JML Investor App</p>
<p className="text-xs text-slate-500 mt-0.5">Add to home screen for quick access & offline support</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={dismissBanner} className="p-2 hover:bg-slate-100 rounded-lg text-slate-400 transition-colors">
<X className="w-5 h-5" />
</button>
<button onClick={handleInstall} className="px-4 py-2 bg-investor text-white rounded-lg font-bold text-sm hover:bg-investor-dark transition-colors flex items-center gap-2 shadow-sm">
<ExternalLink className="w-4 h-4" /> Install
</button>
</div>
</div>
</div>
)}
<div className="grid lg:grid-cols-3 gap-6 mb-6">
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800 flex items-center gap-2">
<Bike className="w-5 h-5 text-slate-400" /> My Portfolio Overview
</h2>
<Link href="/investor/plans" className="text-sm font-semibold text-investor hover:text-investor-dark flex items-center gap-1">
View All <ArrowRight className="w-4 h-4" />
</Link> </Link>
</div> </div>
<div className="p-5 flex-1">
{investorBikes.length > 0 ? (
<div className="space-y-4">
{investorBikes.slice(0, 3).map(bike => (
<Link
key={bike.id}
href={`/investor/investments/${investor.investments?.[0].id || 'ip1'}`}
className="flex items-center gap-4 p-3 hover:bg-slate-50 rounded-xl transition-colors border border-transparent hover:border-slate-100 group"
>
<div className="w-12 h-12 bg-slate-100 rounded-lg overflow-hidden shrink-0">
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-slate-800 truncate group-hover:text-investor transition-colors">{bike.model}</h4>
<p className="text-xs text-slate-500">{bike.plateNumber}</p>
</div>
<div className="text-right">
<p className="font-bold text-green-600">{bike.currentRent || 0}</p>
<p className="text-[10px] text-slate-400 uppercase">Daily Rent</p>
</div>
<div>
<span className={`inline-flex px-2 py-1 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' :
bike.status === 'available' ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'
}`}>
{bike.status}
</span>
</div>
</Link>
))}
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-center py-8">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mb-3">
<Bike className="w-6 h-6 text-slate-300" />
</div>
<p className="text-sm font-semibold text-slate-700">No bikes assigned yet</p>
<p className="text-xs text-slate-500 mt-1 max-w-[200px]">Once you make an investment, assigned bikes will appear here.</p>
</div>
)}
</div>
</div>
<Link href="/investor/profile" className="flex items-center gap-3 p-3 border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group"> <div className="bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col">
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center group-hover:bg-white group-hover:shadow-sm transition-all"> <div className="p-5 border-b border-slate-100">
<FileText className="w-5 h-5 text-slate-500 group-hover:text-investor" /> <h2 className="font-bold text-slate-800">Quick Actions</h2>
</div>
<div className="p-5 flex-1 flex flex-col gap-3">
<div className="bg-investor/5 border border-investor/10 rounded-xl p-4 mb-2">
<p className="text-xs text-investor font-semibold mb-1 uppercase">Available to Withdraw</p>
<p className="text-2xl font-extrabold text-investor mb-3">{availableBalance.toLocaleString()}</p>
<Link href="/investor/withdraw" className="w-full py-2.5 bg-investor text-white rounded-lg font-bold text-sm hover:bg-investor-dark flex items-center justify-center gap-2 shadow-sm shadow-investor/20 transition-all">
<DollarSign className="w-4 h-4" /> Withdraw Funds
</Link>
</div> </div>
<div className="flex-1">
<p className="font-semibold text-sm text-slate-800">Update KYC</p>
<p className="text-xs text-slate-500">Manage documents</p>
</div>
</Link>
<Link href="/investor/plans" className="flex items-center gap-3 p-3 border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group"> <Link href="/investor/profile" className="flex items-center gap-3 p-3 border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group">
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center group-hover:bg-white group-hover:shadow-sm transition-all"> <div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center group-hover:bg-white group-hover:shadow-sm transition-all">
<BarChart3 className="w-5 h-5 text-slate-500 group-hover:text-investor" /> <FileText className="w-5 h-5 text-slate-500 group-hover:text-investor" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-sm text-slate-800">Earnings & P&L</p> <p className="font-semibold text-sm text-slate-800">Update KYC</p>
<p className="text-xs text-slate-500">View detailed reports</p> <p className="text-xs text-slate-500">Manage documents</p>
</div> </div>
</Link> </Link>
<Link href="/investor/plans" className="flex items-center gap-3 p-3 border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group">
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center group-hover:bg-white group-hover:shadow-sm transition-all">
<BarChart3 className="w-5 h-5 text-slate-500 group-hover:text-investor" />
</div>
<div className="flex-1">
<p className="font-semibold text-sm text-slate-800">Earnings & P&L</p>
<p className="text-xs text-slate-500">View detailed reports</p>
</div>
</Link>
</div>
</div> </div>
</div> </div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm"> <div className="bg-white rounded-2xl border border-slate-200 shadow-sm">
<div className="p-5 border-b border-slate-100 flex items-center justify-between"> <div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800 flex items-center gap-2"> <h2 className="font-bold text-slate-800 flex items-center gap-2">
<Clock className="w-5 h-5 text-slate-400" /> Recent Transactions <Clock className="w-5 h-5 text-slate-400" /> Recent Transactions
</h2> </h2>
<button className="text-sm font-semibold text-investor hover:text-investor-dark flex items-center gap-1"> <button className="text-sm font-semibold text-investor hover:text-investor-dark flex items-center gap-1">
View All <ArrowRight className="w-4 h-4" /> View All <ArrowRight className="w-4 h-4" />
</button> </button>
</div> </div>
<div className="p-0"> <div className="p-0">
<TransactionList transactions={recentTransactions} /> <TransactionList transactions={recentTransactions} />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,343 +5,589 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { import {
ArrowLeft, TrendingUp, Bike, DollarSign, Calendar, ArrowLeft, TrendingUp, Bike, DollarSign, Calendar,
CreditCard, FileText, Download, Check, CreditCard, FileText, Download, Check, X,
Printer, BarChart3, Wallet, Clock, Shield, Percent, Activity, AlertTriangle, Printer, BarChart3, Wallet, Clock, Shield,
Receipt, CreditCardIcon, Zap, Smartphone, ChevronRight, Target Receipt, Zap, Smartphone, ChevronRight, Target, Battery, MapPin, Gauge, Building2
} from 'lucide-react'; } from 'lucide-react';
import { investors, bikes, transactions } from '@/data/mockData'; import { investors } from '@/data/mockData';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
interface PaymentRecord {
id: string;
date: string;
amount: number;
installmentNo: number | null;
type: 'full' | 'partial' | 'installment';
method: string;
status: 'completed' | 'pending';
}
export default function InvestorInvestmentDetailPage({ params }: { params: Promise<{ id: string }> }) { export default function InvestorInvestmentDetailPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params); const resolvedParams = use(params);
const { id: investmentId } = resolvedParams; const { id: investmentId } = resolvedParams;
const router = useRouter(); const router = useRouter();
const investor = investors[0]; // mock logged-in investor const investor = investors[0];
const investment = investor.investments?.find((inv: any) => inv.id === investmentId); const investment = investor.investments?.find((inv: any) => inv.id === investmentId);
const [activeTab, setActiveTab] = useState('overview'); const [activeTab, setActiveTab] = useState('overview');
const [showPaymentModal, setShowPaymentModal] = useState(false);
if (!investment) { if (!investment) {
return ( return (
<div className="p-4 lg:p-6"> <div className="min-h-screen pb-20 lg:pb-0">
<div className="text-center py-12"> <InvestorNotification isMobile />
<h2 className="text-xl font-bold text-slate-800">Investment Not Found</h2> <div className="pt-18 lg:pt-0 p-4 lg:p-6">
<p className="text-slate-500 mt-2">The investment you're looking for doesn't exist.</p> <div className="text-center py-12">
<Link href="/investor/portfolio" className="mt-4 inline-flex items-center gap-2 text-investor hover:underline"> <h2 className="text-xl font-bold text-slate-800">Investment Not Found</h2>
<ArrowLeft className="w-4 h-4" /> Back to Portfolio <p className="text-sm text-slate-500 mt-2">The investment you're looking for doesn't exist.</p>
</Link> <Link href="/investor/plans" className="mt-4 inline-flex items-center gap-2 text-investor hover:underline">
<ArrowLeft className="w-4 h-4" /> Back to Investments
</Link>
</div>
</div> </div>
</div> </div>
); );
} }
const planConfig: Record<string, { bg: string; border: string; text: string; badge: string; icon: any }> = { const isBattery = investment.assetType === 'battery' || investment.planName?.toLowerCase().includes('battery');
silver: { bg: 'bg-slate-50', border: 'border-slate-200', text: 'text-slate-700', badge: 'bg-slate-200 text-slate-700', icon: Zap },
gold: { bg: 'bg-amber-50', border: 'border-amber-200', text: 'text-amber-700', badge: 'bg-amber-100 text-amber-700', icon: Shield }, const paymentHistory: PaymentRecord[] = [
platinum: { bg: 'bg-purple-50', border: 'border-purple-200', text: 'text-purple-700', badge: 'bg-purple-100 text-purple-700', icon: TrendingUp }, {
diamond: { bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700', icon: Zap }, id: `pay_${investment.id}`,
date: investment.startDate || '2024-02-01',
amount: investment.totalInvestment,
installmentNo: null,
type: 'full',
method: investment.paymentMethod === 'bank' ? 'Bank Transfer' : 'bKash',
status: 'completed'
}
];
const totalPaid = paymentHistory.reduce((sum, p) => p.status === 'completed' ? sum + p.amount : sum, 0);
const dueAmount = investment.totalInvestment - totalPaid;
const planConfig: Record<string, { badge: string }> = {
silver: { badge: 'bg-slate-200 text-slate-700' },
gold: { badge: 'bg-amber-100 text-amber-700' },
platinum: { badge: 'bg-purple-100 text-purple-700' },
diamond: { badge: 'bg-blue-100 text-blue-700' },
}; };
const style = planConfig[investment.planType] || planConfig.gold; const style = planConfig[investment.planType] || planConfig.gold;
const assignedBikes = bikes.filter((b: any) => b.investorId === investor.id && b.id === 'b1'); // mock filtering for this investment const bikeIds = investment.bikeIds || (investmentId === 'ip1' ? ['b1'] : investmentId === 'ip2' ? ['b2'] : []);
const batteryIds = investment.batteryIds || (isBattery ? ['BAT-001', 'BAT-002', 'BAT-005'] : []);
const investmentTransactions = transactions.filter((t: any) => t.investorId === investor.id && t.type === 'investment_return'); const assignedBikes = [
{ id: 'b1', model: 'Etron ET50', brand: 'Etron', plateNumber: 'Dhaka Metro Cha-1234', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'rented', currentRent: 350, totalEarnings: 114250, batteryLevel: 78, range: 60, location: 'Gulshan 1' },
{ id: 'b2', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'Dhaka Metro Cha-5678', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'available', currentRent: 300, totalEarnings: 12750, batteryLevel: 95, range: 75, location: 'Banani' }
].filter(bike => bikeIds.includes(bike.id));
const assignedBatteries = [
{ id: 'BAT-001', serialNumber: 'SN-2024-00001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'In-Use', cycleCount: 156, soc: '78% / 95%', earnings: 4500, batteryLevel: 78, location: 'Dhaka Central Hub' },
{ id: 'BAT-002', serialNumber: 'SN-2024-00002', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'In-Use', cycleCount: 142, soc: '82% / 96%', earnings: 4500, batteryLevel: 82, location: 'Dhaka Central Hub' },
{ id: 'BAT-005', serialNumber: 'SN-2024-00005', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'Available', cycleCount: 88, soc: '91% / 98%', earnings: 0, batteryLevel: 91, location: 'Dhaka Central Hub' }
].filter(bat => batteryIds.includes(bat.id));
const demoPnl = isBattery ? { grossRevenue: 22500, platformFee: 13500, insurance: 0, maintenance: 0, netProfit: 9000 } : { grossRevenue: 185000, platformFee: 83250, insurance: 15000, maintenance: 8500, netProfit: 78250 };
const demoTransactions = isBattery ? [
{ id: 'tx-bat-1', date: '2024-03-01', description: 'Monthly Yield Share - BAT-001', amount: 4500, status: 'completed' },
{ id: 'tx-bat-2', date: '2024-02-15', description: 'Monthly Yield Share - BAT-002', amount: 4500, status: 'completed' },
{ id: 'tx-bat-funded', date: investment.startDate || '2024-02-01', description: 'Investment Funded - Standard Battery Plan', amount: investment.totalInvestment, status: 'completed' }
] : [
{ id: 'tx1', date: '2024-05-15', description: 'Rental Income - Bike Dhaka Metro Cha-1234', amount: 350, status: 'completed' },
{ id: 'tx2', date: '2024-05-14', description: 'Rental Income - Bike Dhaka Metro Cha-5678', amount: 300, status: 'completed' },
{ id: 'tx3', date: '2024-05-13', description: 'Rental Income - Bike Dhaka Metro Cha-1234', amount: 350, status: 'completed' },
{ id: 'tx4', date: '2024-05-12', description: 'Rental Income - Bike Dhaka Metro Cha-5678', amount: 300, status: 'completed' },
{ id: 'tx-funded', date: investment.startDate || '2024-01-15', description: `Investment Funded - ${investment.planName}`, amount: investment.totalInvestment, status: 'completed' }
];
const handlePaymentSubmit = () => {
const amount = dueAmount;
toast.success(`Payment of ৳${amount.toLocaleString()} submitted successfully!`);
setShowPaymentModal(false);
};
return ( return (
<div className="p-4 lg:p-6 max-w-6xl mx-auto mb-20 lg:mb-0"> <div className="min-h-screen lg:pt-6 pt-0">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6"> <InvestorNotification isMobile />
<div className="flex items-center gap-4"> <div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<button onClick={() => router.back()} className="p-2 hover:bg-slate-100 rounded-lg transition-colors border border-slate-200"> <div className="mb-6">
<ArrowLeft className="w-5 h-5 text-slate-600" /> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
</button>
<div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800 flex items-center gap-2"> <button onClick={() => router.back()} className="p-2 hover:bg-slate-100 rounded-lg transition-colors border border-slate-200">
<Target className="w-5 h-5 lg:w-6 lg:h-6 text-investor" />{investment.planName}</h1> <ArrowLeft className="w-5 h-5 text-slate-600" />
<span className={`px-2.5 py-1 rounded-full text-xs font-bold ${style.badge} capitalize`}> </button>
{investment.planType} Plan <div>
</span> <div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<span className={`px-2.5 py-1 rounded-full text-xs font-bold ${investment.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'} capitalize`}> <h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2">
{investment.status} <Target className="w-5 h-5 lg:w-6 lg:h-6 text-investor" />
</span> {investment.planName}
</h1>
<div className="flex flex-wrap items-center gap-2">
<span
className={`px-2.5 py-1 rounded-full text-xs font-bold ${style.badge} capitalize`}
>
{investment.planType}
</span>
<span
className={`px-2.5 py-1 rounded-full text-xs font-bold ${investment.status === 'active'
? 'bg-green-100 text-green-700'
: 'bg-slate-100 text-slate-600'
} capitalize`}
>
{investment.status}
</span>
</div>
</div>
<p className="text-sm text-slate-500 mt-1">ID: #{investment.id?.toUpperCase()} Started: {investment.startDate}</p>
</div>
</div> </div>
<p className="text-slate-500 text-sm mt-1 flex items-center gap-2"> <button onClick={() => toast.success('Download started')} className="px-4 py-2.5 bg-white border border-slate-200 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 flex justify-center lg:justify-start items-center gap-2 shadow-sm">
ID: <span className="font-mono text-xs bg-slate-100 px-1.5 py-0.5 rounded">#{investment.id?.toUpperCase()}</span> <Download className="w-4 h-4" /> Download
<span className="text-slate-300"></span> Started: {investment.startDate} </button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 lg:gap-4 mb-6">
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
<Wallet className="w-4 h-4 text-purple-600" />
</div>
<p className="text-xs text-slate-500 font-medium">Total Investment</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-slate-800">{investment.totalInvestment.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm bg-green-50/10">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
<Check className="w-4 h-4 text-green-600" />
</div>
<p className="text-xs text-slate-500 font-medium">Already Paid</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-green-600">{totalPaid.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-green-600" />
</div>
<p className="text-xs text-slate-500 font-medium">Total Return</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-green-600">{investment.actualEarnings.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${isBattery ? 'bg-emerald-100' : 'bg-blue-100'}`}>
{isBattery ? (
<Battery className="w-4 h-4 text-emerald-600" />
) : (
<Bike className="w-4 h-4 text-blue-600" />
)}
</div>
<p className="text-xs text-slate-500 font-medium">{isBattery ? 'Batteries' : 'Bikes'}</p>
</div>
<p className={`text-xl lg:text-2xl font-bold ${isBattery ? 'text-emerald-600' : 'text-blue-600'}`}>
{isBattery ? assignedBatteries.length : assignedBikes.length}
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-3">
<button onClick={() => toast.success('Statement download started')} className="px-4 py-2 bg-white border border-slate-200 text-slate-700 rounded-xl text-sm font-bold hover:bg-slate-50 flex items-center gap-2 shadow-sm transition-all">
<Download className="w-4 h-4 text-slate-400" /> Download Statement
</button>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
<Wallet className="w-5 h-5 text-purple-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Invested</p>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investment.totalInvestment.toLocaleString()}</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Total Return</p>
</div>
<p className="text-2xl font-extrabold text-green-600">{investment.actualEarnings.toLocaleString()}</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Pending</p>
</div>
<p className="text-2xl font-extrabold text-amber-600">{(investment.totalInvestment * 0.24 - investment.actualEarnings).toLocaleString()}</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
<Percent className="w-5 h-5 text-blue-600" />
</div>
<p className="text-sm text-slate-500 font-medium">ROI</p>
</div>
<p className="text-2xl font-extrabold text-blue-600">{investment.expectedRoi}%</p>
</div>
</div>
<div className="grid lg:grid-cols-3 gap-6"> <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="lg:col-span-2 space-y-6"> <div className="flex overflow-x-auto border-b border-slate-100 justify-between sm:justify-start px-4 lg:px-0">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> {[
<div className="flex overflow-x-auto bg-slate-50 border-b border-slate-100"> { key: 'overview', label: 'Overview', icon: FileText, count: null },
{['overview', 'bikes', 'pnl', 'transactions'].map((tab) => ( { key: 'bikes', label: isBattery ? 'Batteries' : 'Bikes', icon: isBattery ? Battery : Bike, count: isBattery ? assignedBatteries.length : assignedBikes.length },
<button { key: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length },
key={tab} { key: 'statement', label: 'Statement', icon: Receipt, count: null },
onClick={() => setActiveTab(tab)} ].map((tab) => {
className={`px-6 py-4 text-sm font-bold capitalize transition-colors whitespace-nowrap ${activeTab === tab ? 'text-investor bg-white border-b-2 border-investor' : 'text-slate-500 hover:text-slate-700'}`} const Icon = tab.icon;
> return (
{tab} <button key={tab.key} onClick={() => setActiveTab(tab.key)}
className={`px-4 lg:px-6 py-3 w-full lg:w-auto text-sm font-semibold capitalize whitespace-nowrap transition-colors flex justify-center items-center gap-2 ${activeTab === tab.key ? 'text-investor bg-investor/5 border-b-2 border-investor' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'}`}>
<Icon className="w-4 h-4 " />
<span className="hidden lg:inline">{tab.label}</span>
{tab.count !== null && <span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${activeTab === tab.key ? 'bg-investor text-white' : 'bg-slate-200 text-slate-600'}`}>{tab.count}</span>}
</button> </button>
))} );
</div> })}
</div>
<div className="p-6"> <div className="p-4 lg:p-6">
{activeTab === 'overview' && ( {activeTab === 'overview' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="bg-slate-50 rounded-2xl p-5 border border-slate-200"> <div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
<h4 className="font-bold text-slate-800 mb-4 flex items-center gap-2"> <h4 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<FileText className="w-5 h-5 text-slate-400" /> Investment Details <FileText className="w-4 h-4 text-slate-400" /> Investment Details
</h4> </h4>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<div className="flex justify-between border-b border-slate-200 pb-2"> <div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Plan Name</span> <span className="text-slate-500">Plan Name</span>
<span className="font-bold text-slate-800">{investment.planName}</span> <span className="font-semibold text-slate-800">{investment.planName}</span>
</div>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Plan Type</span>
<span className="font-bold text-slate-800 capitalize">{investment.planType}</span>
</div>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Period</span>
<span className="font-bold text-slate-800">{investment.startDate} - {investment.endDate || 'Ongoing'}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Payment Method</span>
<span className="font-bold text-slate-800 capitalize">{investment.paymentMethod}</span>
</div>
</div> </div>
</div> <div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Plan Type</span>
<div className="bg-slate-50 rounded-2xl p-5 border border-slate-200"> <span className="font-semibold text-slate-800 capitalize">{investment.planType}</span>
<h4 className="font-bold text-slate-800 mb-4 flex items-center gap-2"> </div>
<Shield className="w-5 h-5 text-slate-400" /> Plan Policy <div className="flex justify-between border-b border-slate-200 pb-2">
</h4> <span className="text-slate-500">Period</span>
<div className="space-y-3 text-sm"> <span className="font-semibold text-slate-800">{investment.startDate} - {investment.endDate || 'Ongoing'}</span>
<div className="flex justify-between border-b border-slate-200 pb-2"> </div>
<span className="text-slate-500">Min Duration</span> <div className="flex justify-between">
<span className="font-bold text-slate-800">12 Months</span> <span className="text-slate-500">Payment Method</span>
</div> <span className="font-semibold text-slate-800 capitalize">{investment.paymentMethod}</span>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Lock-in Period</span>
<span className="font-bold text-slate-800">3 Months</span>
</div>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Exit Penalty</span>
<span className="font-bold text-red-500">10%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Maintenance</span>
<span className="font-bold text-green-600">Included</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-5"> <div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
<h4 className="font-bold text-amber-800 mb-4 flex items-center gap-2"> <h4 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<Percent className="w-5 h-5 text-amber-600" /> Profit Sharing Configuration <Shield className="w-4 h-4 text-slate-400" /> Plan Policy
</h4> </h4>
<p className="text-xs text-amber-600 mb-4 uppercase font-bold tracking-wider">Your share based on rental model</p> <div className="space-y-3 text-sm">
<div className="grid grid-cols-3 gap-3"> <div className="flex justify-between border-b border-slate-200 pb-2">
<div className="bg-white rounded-xl p-4 text-center border border-amber-200 shadow-sm"> <span className="text-slate-500">Min Duration</span>
<p className="text-xs text-slate-400 mb-1 font-bold uppercase">Single Rent</p> <span className="font-semibold text-slate-800">12 Months</span>
<p className="text-2xl font-extrabold text-slate-800">55%</p>
</div> </div>
<div className="bg-white rounded-xl p-4 text-center border border-amber-200 shadow-sm"> <div className="flex justify-between border-b border-slate-200 pb-2">
<p className="text-xs text-slate-400 mb-1 font-bold uppercase">Rent to Own</p> <span className="text-slate-500">Lock-in Period</span>
<p className="text-2xl font-extrabold text-slate-800">45%</p> <span className="font-semibold text-slate-800">3 Months</span>
</div> </div>
<div className="bg-white rounded-xl p-4 text-center border border-amber-200 shadow-sm ring-2 ring-amber-500 ring-offset-2"> <div className="flex justify-between border-b border-slate-200 pb-2">
<p className="text-xs text-slate-400 mb-1 font-bold uppercase">Share EV</p> <span className="text-slate-500">Exit Penalty</span>
<p className="text-2xl font-extrabold text-slate-800">40%</p> <span className="font-semibold text-red-500">10%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Maintenance</span>
<span className="font-semibold text-green-600">Included</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)}
{activeTab === 'bikes' && ( {isBattery ? (
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-5">
<h4 className="font-semibold text-emerald-800 mb-4 flex items-center gap-2">
<Percent className="w-4 h-4 text-emerald-600" /> Profit Sharing
</h4>
<p className="text-xs text-emerald-600 mb-4 font-medium">Profit sharing ratio when batteries are utilized</p>
<div className="max-w-xs">
<div className="bg-white rounded-lg p-3 text-center border border-emerald-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Profit Share</p>
<p className="text-xl font-bold text-slate-800">40%</p>
</div>
</div>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
<h4 className="font-semibold text-amber-800 mb-4 flex items-center gap-2">
<Percent className="w-4 h-4 text-amber-600" /> Profit Sharing
</h4>
<p className="text-xs text-amber-600 mb-4 font-medium">Your share based on rental model</p>
<div className="grid grid-cols-3 gap-3">
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Single Rent</p>
<p className="text-xl font-bold text-slate-800">40%</p>
</div>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Rent to Own</p>
<p className="text-xl font-bold text-slate-800">50%</p>
</div>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Share EV</p>
<p className="text-xl font-bold text-slate-800">55%</p>
</div>
</div>
</div>
)}
</div>
)}
{activeTab === 'bikes' && (
isBattery ? (
<div className="space-y-4"> <div className="space-y-4">
{assignedBikes.length > 0 ? assignedBikes.map(bike => ( <p className="text-sm text-slate-500">{assignedBatteries.length} battery pack{assignedBatteries.length !== 1 ? 's' : ''} assigned to this investment</p>
<div key={bike.id} className="p-5 bg-white rounded-2xl border border-slate-200 flex flex-col md:flex-row items-start md:items-center gap-5 hover:border-investor transition-colors group"> {assignedBatteries.map((battery) => (
<div className="w-20 h-20 bg-slate-100 rounded-xl overflow-hidden shrink-0 border border-slate-200"> <Link
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover" /> href={`/investor/investments/${investmentId}/rental-history/${battery.id}`}
key={battery.id}
className="p-4 bg-white rounded-xl border border-slate-200 flex flex-col md:flex-row items-start gap-4 hover:border-emerald-500 hover:shadow-md transition-all block group cursor-pointer"
>
<div className="w-16 h-16 bg-emerald-50 rounded-xl flex items-center justify-center shrink-0 border border-emerald-100 group-hover:scale-105 transition-transform duration-300">
<Battery className="w-8 h-8 text-emerald-600 animate-pulse" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h5 className="font-bold text-lg text-slate-800 truncate">{bike.model}</h5> <div className="flex items-center gap-2 mb-1">
<p className="text-sm text-slate-500">{bike.plateNumber} {bike.brand}</p> <h5 className="font-semibold text-slate-800 group-hover:text-emerald-700 transition-colors">{battery.model}</h5>
<div className="mt-3 flex flex-wrap gap-2"> <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${battery.status === 'In-Use' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}`}>{battery.status}</span>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}`}> </div>
{bike.status} <p className="text-sm text-slate-500">{battery.serialNumber} {battery.brand}</p>
</span> <div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500">
<span className="px-2 py-0.5 rounded bg-slate-100 text-slate-600 text-[10px] font-bold uppercase flex items-center gap-1"> <span className="flex items-center gap-1">Cycle Count: {battery.cycleCount}</span>
Daily: {bike.currentRent} <span className="flex items-center gap-1">SoC / Health: {battery.soc}</span>
</span> <span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {battery.location}</span>
</div> </div>
</div> </div>
<div className="text-right"> <div className="flex items-center gap-3 self-center shrink-0">
<p className="text-xs text-slate-400 mb-1 uppercase font-bold tracking-wider">Total Earnings</p> <div className="text-right">
<p className="text-xl font-extrabold text-green-600">{bike.totalEarnings?.toLocaleString()}</p> <p className="text-xs text-slate-500 mb-1">Est. Monthly Return</p>
<Link href="/investor/portfolio" className="mt-2 text-xs font-bold text-investor hover:underline flex items-center gap-1 justify-end"> <p className="text-lg font-bold text-slate-800">{(investment.monthlyReturn / assignedBatteries.length).toLocaleString()}</p>
Live Track <ChevronRight className="w-3 h-3" /> <p className="text-xs text-slate-400 mt-1">Total: {battery.earnings.toLocaleString()}</p>
</Link> </div>
<ChevronRight className="w-5 h-5 text-slate-400 group-hover:text-emerald-600 group-hover:translate-x-1 transition-all" />
</div> </div>
</div> </Link>
)) : ( ))}
<div className="text-center py-12 border-2 border-dashed border-slate-200 rounded-2xl">
<Bike className="w-12 h-12 mx-auto mb-4 text-slate-300" />
<p className="text-slate-500 font-bold">No bikes assigned yet.</p>
<p className="text-xs text-slate-400 mt-1">Admin will assign bikes to this investment shortly.</p>
</div>
)}
</div> </div>
)} ) : (
<div className="space-y-4">
<p className="text-sm text-slate-500">{assignedBikes.length} bike{assignedBikes.length !== 1 ? 's' : ''} assigned to this investment</p>
{assignedBikes.map((bike) => (
<Link
href={`/investor/investments/${investmentId}/rental-history/${bike.id}`}
key={bike.id}
className="p-4 bg-white rounded-xl border border-slate-200 flex flex-col md:flex-row items-start gap-4 hover:border-investor hover:shadow-md transition-all block group cursor-pointer"
>
<div className="w-24 h-20 bg-slate-100 rounded-lg overflow-hidden shrink-0 group-hover:scale-102 transition-transform duration-300">
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover animate-fade-in" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h5 className="font-semibold text-slate-800 group-hover:text-investor transition-colors">{bike.model}</h5>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : bike.status === 'available' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'}`}>{bike.status}</span>
</div>
<p className="text-sm text-slate-500">{bike.plateNumber} {bike.brand}</p>
<div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1"><Battery className="w-3 h-3" /> {bike.batteryLevel}%</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {bike.location}</span>
</div>
</div>
<div className="flex items-center gap-3 self-center shrink-0">
<div className="text-right">
<p className="text-xs text-slate-500 mb-1">Est. Monthly Return</p>
<p className="text-lg font-bold text-slate-800">{bike.currentRent}</p>
<p className="text-xs text-slate-400 mt-1">Total: {bike.totalEarnings.toLocaleString()}</p>
</div>
<ChevronRight className="w-5 h-5 text-slate-400 group-hover:text-investor group-hover:translate-x-1 transition-all" />
</div>
</Link>
))}
</div>
)
)}
{activeTab === 'pnl' && ( {activeTab === 'statement' && (
<div className="max-w-xl mx-auto"> <div className="max-w-2xl mx-auto">
<div className="bg-slate-50 rounded-2xl p-6 border border-slate-200"> <div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
<h3 className="font-bold text-slate-800 mb-6 text-center text-lg">Detailed P&L Statement</h3> <h3 className="font-semibold text-slate-800 mb-5 text-center text-lg">Profit & Loss Statement</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center pb-4 border-b border-slate-200"> <div className="flex justify-between items-center pb-3 border-b border-slate-200">
<span className="font-bold text-slate-600">Gross Rental Revenue</span> <span className="font-medium text-slate-600">Gross Rental Revenue</span>
<span className="text-xl font-bold text-slate-800">{(investment.actualEarnings / 0.55).toFixed(0)}</span> <span className="text-xl font-bold text-slate-800">{demoPnl.grossRevenue.toLocaleString()}</span>
</div>
<div className="space-y-2 py-2">
<div className="flex justify-between text-sm">
<span className="text-slate-500">Platform Fee {isBattery ? '(60%)' : '(45%)'}</span>
<span className="font-medium text-red-500">-{demoPnl.platformFee.toLocaleString()}</span>
</div> </div>
<div className="space-y-3 py-2"> <div className="flex justify-between text-sm">
<div className="flex justify-between text-sm"> <span className="text-slate-500">Insurance Coverage</span>
<span className="text-slate-500">Platform Management Fee (45%)</span> <span className="font-medium text-slate-600">-{demoPnl.insurance.toLocaleString()}</span>
<span className="text-red-500 font-bold">-{((investment.actualEarnings / 0.55) * 0.45).toFixed(0)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Fleet Insurance & Maintenance</span>
<span className="text-green-600 font-bold tracking-tight uppercase text-[10px]">Included in Platform Fee</span>
</div>
</div> </div>
<div className="flex justify-between items-center pt-4 border-t-2 border-slate-300"> <div className="flex justify-between text-sm">
<span className="text-lg font-extrabold text-slate-800">Net Return to Investor</span> <span className="text-slate-500">Maintenance</span>
<span className="text-2xl font-extrabold text-green-600">{investment.actualEarnings.toLocaleString()}</span> <span className="font-medium text-slate-600">-{demoPnl.maintenance.toLocaleString()}</span>
</div> </div>
</div> </div>
<div className="flex justify-between items-center pt-4 border-t-2 border-slate-300">
<span className="text-lg font-semibold text-slate-800">Your Share {isBattery ? '(40%)' : '(55%)'}</span>
<span className="text-2xl font-bold text-green-600">{demoPnl.netProfit.toLocaleString()}</span>
</div>
</div> </div>
</div> </div>
)} </div>
)}
{activeTab === 'transactions' && ( {activeTab === 'transactions' && (
<div className="overflow-x-auto"> <div className="space-y-4">
<table className="w-full text-left"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm text-slate-500">{demoTransactions.length} transactions found</p>
<div className="flex items-center gap-2">
<select className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"><option>All Types</option><option>Income</option><option>Withdrawal</option></select>
<select className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"><option>Last 30 Days</option><option>Last 7 Days</option><option>This Month</option><option>All Time</option></select>
</div>
</div>
<div className="lg:overflow-x-auto">
<table className="hidden lg:table w-full text-left">
<thead className="bg-slate-50"> <thead className="bg-slate-50">
<tr> <tr><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Date</th><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Description</th><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase text-right">Amount</th><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Status</th></tr>
<th className="px-4 py-3 text-xs font-bold text-slate-500 uppercase tracking-wider">Date</th>
<th className="px-4 py-3 text-xs font-bold text-slate-500 uppercase tracking-wider">Description</th>
<th className="px-4 py-3 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Amount</th>
<th className="px-4 py-3 text-xs font-bold text-slate-500 uppercase tracking-wider">Status</th>
</tr>
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{investmentTransactions.map(t => ( {demoTransactions.map((tx) => (
<tr key={t.id} className="hover:bg-slate-50 transition-colors"> <tr key={tx.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-4 text-sm text-slate-600 whitespace-nowrap">{t.createdAt}</td> <td className="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">{tx.date}</td>
<td className="px-4 py-4"> <td className="px-4 py-3"><p className="text-sm font-medium text-slate-800">{tx.description}</p><p className="text-xs text-slate-400">Ref: {tx.id}</p></td>
<p className="text-sm font-bold text-slate-800">{t.description}</p> <td className={`px-4 py-3 text-sm font-bold text-right ${tx.amount > 0 ? 'text-green-600' : 'text-red-500'}`}>{tx.amount > 0 ? '+' : ''}{tx.amount.toLocaleString()}</td>
<p className="text-xs text-slate-400">Ref: {t.id}</p> <td className="px-4 py-3"><span className="inline-flex px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{tx.status}</span></td>
</td>
<td className="px-4 py-4 text-sm font-extrabold text-green-600 text-right">
+{t.amount.toLocaleString()}
</td>
<td className="px-4 py-4">
<span className="inline-flex px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">
Completed
</span>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
)}
</div>
</div>
</div>
<div className="space-y-6"> <div className="lg:hidden space-y-3">
<div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm"> {demoTransactions.map((tx) => (
<h4 className="font-bold text-slate-800 mb-4 flex items-center gap-2"> <div key={tx.id} className="p-4 bg-white rounded-xl border border-slate-200">
<Zap className="w-5 h-5 text-amber-500" /> Quick Actions <div className="flex items-start justify-between mb-2">
</h4> <div><p className="text-sm font-medium text-slate-800">{tx.description}</p><p className="text-xs text-slate-400">Ref: {tx.id} {tx.date}</p></div>
<div className="space-y-3"> <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${tx.status === 'completed' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>{tx.status}</span>
<button onClick={() => toast.success('Printing report...')} className="w-full px-4 py-3 border border-slate-200 text-slate-700 rounded-xl text-sm font-bold hover:bg-slate-50 flex items-center gap-3 transition-all"> </div>
<Printer className="w-5 h-5 text-slate-400" /> Print Summary <p className={`text-lg font-bold text-right ${tx.amount > 0 ? 'text-green-600' : 'text-red-500'}`}>{tx.amount > 0 ? '+' : ''}{tx.amount.toLocaleString()}</p>
</button> </div>
<button onClick={() => toast.success('Exporting to Excel...')} className="w-full px-4 py-3 border border-slate-200 text-slate-700 rounded-xl text-sm font-bold hover:bg-slate-50 flex items-center gap-3 transition-all"> ))}
<BarChart3 className="w-5 h-5 text-slate-400" /> Export Data </div>
</button>
<button onClick={() => router.push('/investor/withdraw')} className="w-full px-4 py-3 bg-investor text-white rounded-xl text-sm font-bold hover:bg-investor-dark flex items-center gap-3 shadow-md shadow-investor/20 transition-all">
<DollarSign className="w-5 h-5" /> Withdraw Earnings
</button>
</div>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-5"> <div className="flex items-center justify-between pt-4 border-t border-slate-100">
<h4 className="font-bold text-blue-800 mb-3 flex items-center gap-2"> <p className="text-sm text-slate-500">Showing 1-8 of 8</p>
<Smartphone className="w-5 h-5 text-blue-600" /> Support Desk <div className="flex items-center gap-2">
</h4> <button className="px-3 py-1.5 border border-slate-200 rounded-lg text-sm text-slate-600 hover:bg-slate-50 disabled:opacity-50" disabled>Previous</button>
<p className="text-xs text-blue-700 mb-4 font-medium leading-relaxed"> <button className="px-3 py-1.5 border border-slate-200 rounded-lg text-sm text-slate-600 hover:bg-slate-50 disabled:opacity-50" disabled>Next</button>
Need help with this investment or bike assignment? Our team is available 24/7. </div>
</p> </div>
<button onClick={() => toast.success('Connecting to support...')} className="w-full py-2.5 bg-blue-600 text-white rounded-xl font-bold text-sm hover:bg-blue-700 transition-colors"> </div>
Contact Support )}
</button>
{activeTab === 'payments' && (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm text-slate-500">{paymentHistory.length} payments made</p>
{dueAmount > 0 && (
<button onClick={() => setShowPaymentModal(true)} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2">
<DollarSign className="w-4 h-4" /> Make Payment
</button>
)}
</div>
<div className="overflow-x-auto">
<table className="hidden lg:table w-full text-left">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Installment</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase text-right">Amount</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paymentHistory.map((payment) => (
<tr key={payment.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">{payment.date}</td>
<td className="px-4 py-3"><span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span></td>
<td className="px-4 py-3 text-sm text-slate-600">{payment.installmentNo ? `#${payment.installmentNo}` : '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{payment.method}</td>
<td className="px-4 py-3 text-sm font-bold text-green-600 text-right">{payment.amount.toLocaleString()}</td>
<td className="px-4 py-3"><span className="inline-flex px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span></td>
</tr>
))}
</tbody>
</table>
</div>
<div className="lg:hidden space-y-3">
{paymentHistory.map((payment) => (
<div key={payment.id} className="p-4 bg-white rounded-xl border border-slate-200">
<div className="flex items-start justify-between mb-2">
<div>
<p className="text-sm font-medium text-slate-800">{payment.amount.toLocaleString()}</p>
<p className="text-xs text-slate-400">{payment.date} {payment.method}</p>
</div>
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span>
</div>
<div className="flex gap-2 mt-2">
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span>
{payment.installmentNo && <span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-blue-100 text-blue-600">Inst #{payment.installmentNo}</span>}
</div>
</div>
))}
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
{showPaymentModal && dueAmount > 0 && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl">
<div className="flex items-center justify-between p-5 border-b border-slate-100">
<h3 className="text-lg font-bold text-slate-800">Make Payment</h3>
<button onClick={() => setShowPaymentModal(false)} className="p-1 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4">
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Total Investment</span>
<span className="text-sm font-semibold text-slate-800">{investment.totalInvestment.toLocaleString()}</span>
</div>
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Already Paid</span>
<span className="text-sm font-semibold text-green-600">{totalPaid.toLocaleString()}</span>
</div>
<div className="flex justify-between pt-2 border-t border-slate-200">
<span className="text-sm font-semibold text-slate-800">Due Amount</span>
<span className="text-sm font-bold text-amber-600">{dueAmount.toLocaleString()}</span>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-center">
<p className="text-sm text-amber-600 mb-1">Fixed Payment Amount</p>
<p className="text-3xl font-bold text-amber-700">{dueAmount.toLocaleString()}</p>
</div>
<div className="pt-2">
<label className="text-sm font-semibold text-slate-700 mb-2 block">Payment Method</label>
<div className="flex gap-2">
<button className="flex-1 p-3 border border-slate-200 rounded-lg text-center hover:border-investor hover:bg-investor/5 transition-colors">
<Building2 className="w-5 h-5 mx-auto text-slate-600 mb-1" />
<span className="text-xs font-medium text-slate-700">Bank</span>
</button>
<button className="flex-1 p-3 border border-slate-200 rounded-lg text-center hover:border-investor hover:bg-investor/5 transition-colors">
<Smartphone className="w-5 h-5 mx-auto text-slate-600 mb-1" />
<span className="text-xs font-medium text-slate-700">bKash</span>
</button>
</div>
</div>
<button onClick={handlePaymentSubmit} className="w-full py-3 bg-investor text-white rounded-xl font-bold hover:bg-investor-dark transition-colors">
Pay {dueAmount.toLocaleString()}
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }
import { Percent } from 'lucide-react';

View File

@@ -0,0 +1,551 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
ArrowLeft, Battery, Bike, Calendar, Clock, Download, MapPin, Search,
TrendingUp, User, XCircle, CheckCircle2, ChevronLeft, ChevronRight,
Shield, RefreshCw, DollarSign, Activity, AlertCircle, CalendarRange
} from 'lucide-react';
import { investors } from '@/data/mockData';
import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
interface RentalTransaction {
id: string;
date: string;
riderName: string;
duration: string;
amount: number;
status: 'paid' | 'pending' | 'failed';
payoutMethod: string;
swapCount?: number;
}
export default function AssetRentalHistoryPage({ params }: { params: Promise<{ id: string; assetId: string }> }) {
const resolvedParams = use(params);
const { id: investmentId, assetId } = resolvedParams;
const router = useRouter();
const investor = investors[0];
const investment = investor.investments?.find((inv: any) => inv.id === investmentId);
// Asset verification
const isBattery = assetId.startsWith('BAT-') || assetId.startsWith('bat-') || assetId.toLowerCase().includes('battery');
// Specific Asset Details
const bikeDetails = {
id: 'b1',
model: 'Etron ET50',
brand: 'Etron',
plateNumber: 'Dhaka Metro Cha-1234',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&h=600&fit=crop',
status: 'rented',
currentRent: 350,
totalEarnings: 114250,
batteryLevel: 78,
range: '60 km',
location: 'Gulshan 1 Hub',
purchasePrice: 85000,
utilization: '94%',
avgDailyHours: '8.4 hrs',
};
const batteryDetails: Record<string, any> = {
'BAT-001': {
id: 'BAT-001',
serialNumber: 'SN-2024-00001',
brand: 'EVE Energy',
model: 'Li-Ion 60V50Ah',
status: 'In-Use',
cycleCount: 156,
soc: '78% / 95%',
earnings: 4500,
location: 'Dhaka Central Hub',
purchasePrice: 45000,
utilization: '97%',
dailyRent: 150,
},
'BAT-002': {
id: 'BAT-002',
serialNumber: 'SN-2024-00002',
brand: 'EVE Energy',
model: 'Li-Ion 60V50Ah',
status: 'In-Use',
cycleCount: 142,
soc: '82% / 96%',
earnings: 4500,
location: 'Dhaka Central Hub',
purchasePrice: 45000,
utilization: '95%',
dailyRent: 150,
}
};
const currentBattery = batteryDetails[assetId] || batteryDetails['BAT-001'];
const assetDisplayName = isBattery ? currentBattery.model : bikeDetails.model;
const assetSubName = isBattery ? currentBattery.serialNumber : bikeDetails.plateNumber;
// Generate highly realistic rent transaction history
const [transactions] = useState<RentalTransaction[]>(() => {
const list: RentalTransaction[] = [];
const riders = ['Sajib Islam', 'Nayeem Chowdhury', 'Rakib Hasan', 'Kamal Hossain', 'Arifur Rahman'];
const methods = ['bKash', 'Nagad', 'Rocket', 'Bank Transfer'];
const days = isBattery ? 30 : 20;
const baseDate = new Date();
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(baseDate.getDate() - i);
const dateString = date.toISOString().split('T')[0];
const riderIndex = (i + 3) % riders.length;
const methodIndex = (i + 1) % methods.length;
// status distribution
let status: 'paid' | 'pending' | 'failed' = 'paid';
if (i === 1) status = 'pending';
else if (i === 5) status = 'failed';
const amount = isBattery ? currentBattery.dailyRent : bikeDetails.currentRent;
list.push({
id: `TX-${isBattery ? 'BAT' : 'BK'}-${10000 + i}`,
date: dateString,
riderName: riders[riderIndex],
duration: '1 Day',
amount: amount,
status: status,
payoutMethod: methods[methodIndex],
swapCount: isBattery ? 2 + (i % 3) : undefined
});
}
return list;
});
// Filter & Sorting State
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'amount' | 'rider'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 10;
// Handler functions
const handleSort = (field: 'date' | 'amount' | 'rider') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
};
// Filter Logic
const filteredTransactions = transactions.filter(tx => {
if (statusFilter !== 'all' && tx.status !== statusFilter) return false;
if (searchQuery && !tx.riderName.toLowerCase().includes(searchQuery.toLowerCase()) && !tx.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(tx.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(tx.date) > new Date(dateTo)) return false;
return true;
});
// Sort Logic
const sortedTransactions = [...filteredTransactions].sort((a, b) => {
let comparison = 0;
if (sortBy === 'date') {
comparison = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'amount') {
comparison = a.amount - b.amount;
} else if (sortBy === 'rider') {
comparison = a.riderName.localeCompare(b.riderName);
}
return sortOrder === 'desc' ? -comparison : comparison;
});
// Pagination
const totalPages = Math.ceil(sortedTransactions.length / pageSize);
const paginatedTransactions = sortedTransactions.slice((page - 1) * pageSize, page * pageSize);
const statusConfig: Record<string, { label: string; bg: string; color: string; icon: any }> = {
paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle2 },
pending: { label: 'Pending', bg: 'bg-amber-100', color: 'text-amber-700', icon: Clock },
failed: { label: 'Failed', bg: 'bg-red-100', color: 'text-red-700', icon: XCircle },
};
const totalCollected = filteredTransactions
.filter(t => t.status === 'paid')
.reduce((sum, t) => sum + t.amount, 0);
const pendingAmount = filteredTransactions
.filter(t => t.status === 'pending')
.reduce((sum, t) => sum + t.amount, 0);
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 sm:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
{/* Header - EXACT copy of other page's spacing & structure */}
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
<button
onClick={() => router.back()}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors border border-slate-200 bg-white"
>
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div>
<h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2">
{isBattery ? <Battery className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> : <Bike className="w-5 h-5 sm:w-6 sm:h-6 text-investor" />}
{assetDisplayName}
</h1>
<p className="text-sm text-slate-500 mt-1">
ID: #{assetId.toUpperCase()} {isBattery ? `Serial Number: ${currentBattery.serialNumber}` : `Plate: ${bikeDetails.plateNumber}`}
</p>
</div>
</div>
<button
onClick={() => toast.success('Exporting history...')}
className="px-4 py-2.5 bg-white border border-slate-200 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 flex justify-center lg:justify-start items-center gap-2 shadow-sm w-fit"
>
<Download className="w-4 h-4 text-slate-500" /> Export Ledger
</button>
</div>
</div>
{/* Info Grid & Stats Cards - MATCHES OTHER PAGES PRECISELY */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Collected</p>
<p className="text-lg font-bold text-green-600">{totalCollected.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Activity className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Utilization Rate</p>
<p className="text-lg font-bold text-slate-800">{isBattery ? currentBattery.utilization : bikeDetails.utilization}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending</p>
<p className="text-lg font-bold text-amber-600">{pendingAmount.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center shrink-0">
<Shield className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xs text-slate-500">Hub Location</p>
<p className="text-sm font-bold text-slate-800 truncate max-w-[140px] sm:max-w-none">
{isBattery ? currentBattery.location.split(' ')[0] : bikeDetails.location.split(' ')[0]}
</p>
</div>
</div>
</div>
</div>
{/* Main Table Card - MATCHES OTHER PAGES PRECISELY */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Filters - MATCHES OTHER PAGES PRECISELY */}
<div className="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<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 rider..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 focus:outline-none"
/>
</div>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-xs text-slate-500 font-medium">
<CalendarRange className="w-4 h-4" /> Date Range
</div>
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
>
Clear
</button>
)}
<button
onClick={() => {
setSearchQuery('');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setSortBy('date');
setSortOrder('desc');
setPage(1);
toast.success('Filters cleared!');
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
</div>
</div>
</div>
{/* Card View - Mobile/Tablet */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<div key={tx.id} className="p-4 hover:bg-slate-50">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<User className="w-4 h-4 text-slate-400 shrink-0" />
<p className="text-sm font-semibold text-slate-800 truncate">{tx.riderName}</p>
</div>
<p className="text-xs text-slate-400 ml-6">Ref: {tx.id}</p>
{isBattery && (
<p className="text-xs text-slate-600 mt-1 ml-6 font-semibold">Swaps: {tx.swapCount}</p>
)}
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">{tx.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium mt-1 ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div>
</div>
<div className="flex items-center justify-between ml-6 text-xs text-slate-400">
<span>{tx.date}</span>
<span className="capitalize">{tx.payoutMethod}</span>
</div>
</div>
);
})
) : (
<div className="p-8 text-center text-slate-500">
<AlertCircle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Transaction ID
</th>
<th
onClick={() => handleSort('rider')}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Rider (Biker) {sortBy === 'rider' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
{isBattery && (
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Swaps
</th>
)}
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Duration
</th>
<th
onClick={() => handleSort('amount')}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Amount {sortBy === 'amount' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Method
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Status
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<tr key={tx.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-slate-800">{tx.date}</p>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-500 font-mono">
{tx.id}
</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-700 font-medium">{tx.riderName}</span>
</div>
</td>
{isBattery && (
<td className="px-4 py-3 text-sm text-slate-600">
{tx.swapCount} Swaps
</td>
)}
<td className="px-4 py-3 text-sm text-slate-600">
{tx.duration}
</td>
<td className="px-4 py-3">
<p className="text-sm font-bold text-slate-800">{tx.amount.toLocaleString()}</p>
</td>
<td className="px-4 py-3 text-sm text-slate-600 capitalize">
{tx.payoutMethod}
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" />
{status.label}
</span>
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={isBattery ? 8 : 7} className="px-4 py-12 text-center text-slate-500">
<AlertCircle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination - MATCHES OTHER PAGES PRECISELY */}
{sortedTransactions.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedTransactions.length)} of {sortedTransactions.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-medium ${
page === pageNum
? 'bg-investor text-white'
: 'border border-slate-200 hover:bg-slate-50'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,206 @@
'use client';
import { useState } from 'react';
import { Bell, X, ArrowRight, Package, DollarSign, Bike, AlertCircle, CheckCircle, Filter, Check } from 'lucide-react';
import Link from 'next/link';
const mockNotifications = [
{
id: '1',
type: 'rental',
title: 'New Rental Started',
message: 'Your bike AB-1234 has been rented by rider MR-456',
time: '5 min ago',
read: false,
},
{
id: '2',
type: 'earning',
title: 'Earning Credited',
message: '৳450 has been added to your wallet from bike CD-5678',
time: '2 hours ago',
read: false,
},
{
id: '3',
type: 'success',
title: 'Withdrawal Complete',
message: 'Your withdrawal of ৳5,000 has been processed successfully',
time: '1 day ago',
read: true,
},
{
id: '4',
type: 'alert',
title: 'Damage Alert',
message: 'Minor scratch damage has been reported on your bike XY-9012.',
time: '2 days ago',
read: true,
},
{
id: '5',
type: 'rental',
title: 'Rental Ended',
message: 'Bike AB-1234 has been returned by rider MR-456',
time: '3 hours ago',
read: false,
},
{
id: '6',
type: 'earning',
title: 'Daily Earning Summary',
message: 'You earned ৳1,250 from 5 rentals today',
time: 'Yesterday',
read: true,
},
{
id: '7',
type: 'alert',
title: 'Damage Alert',
message: 'Cracked indicator light has been reported on your bike EF-3456.',
time: '5 hours ago',
read: true,
},
{
id: '8',
type: 'success',
title: 'Investment Renewed',
message: 'Your Gold Plan investment has been automatically renewed',
time: '3 days ago',
read: true,
},
];
const iconMap: Record<string, any> = {
rental: Bike,
earning: DollarSign,
success: CheckCircle,
alert: AlertCircle,
default: Package,
};
const typeColors: Record<string, string> = {
rental: 'bg-blue-100 text-blue-600',
earning: 'bg-green-100 text-green-600',
success: 'bg-emerald-100 text-emerald-600',
alert: 'bg-amber-100 text-amber-600',
default: 'bg-slate-100 text-slate-600',
};
export default function InvestorNotificationsPage() {
const [notifications, setNotifications] = useState(mockNotifications);
const [filter, setFilter] = useState<'all' | 'unread'>('all');
const filteredNotifications = filter === 'unread'
? notifications.filter(n => !n.read)
: notifications;
const unreadCount = notifications.filter(n => !n.read).length;
const markAsRead = (id: string) => {
setNotifications(prev => prev.map(n =>
n.id === id ? { ...n, read: true } : n
));
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
};
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2">
<Bell className="w-5 h-5 lg:w-6 lg:h-6 text-investor" /> Notifications
</h1>
<p className="text-sm text-slate-500 mt-1">Stay updated with your investment activities</p>
</div>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className="px-4 py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-2 shadow-sm"
>
<Check className="w-4 h-4" /> Mark all as read
</button>
)}
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<button
onClick={() => setFilter('all')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${filter === 'all'
? 'bg-investor text-white'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
All
</button>
<button
onClick={() => setFilter('unread')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-1.5 ${filter === 'unread'
? 'bg-investor text-white'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
Unread
{unreadCount > 0 && (
<span className="px-1.5 py-0.5 bg-red-500 text-white text-xs font-bold rounded-full">
{unreadCount}
</span>
)}
</button>
</div>
</div>
<div className="divide-y divide-slate-100">
{filteredNotifications.length === 0 ? (
<div className="p-8 text-center">
<Bell className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500">No notifications found</p>
</div>
) : (
filteredNotifications.map((notif) => {
const Icon = iconMap[notif.type] || iconMap.default;
return (
<div
key={notif.id}
onClick={() => markAsRead(notif.id)}
className={`p-4 cursor-pointer transition-all hover:bg-slate-50 ${!notif.read ? 'bg-blue-50/50' : ''
}`}
>
<div className="flex items-start gap-4">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${typeColors[notif.type]}`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<p className={`text-sm font-semibold ${notif.read ? 'text-slate-600' : 'text-slate-800'}`}>
{notif.title}
</p>
{!notif.read && (
<div className="w-2 h-2 bg-investor rounded-full" />
)}
</div>
<p className="text-sm text-slate-500 line-clamp-2">{notif.message}</p>
<p className="text-xs text-slate-400 mt-1">{notif.time}</p>
</div>
</div>
</div>
);
})
)}
</div>
</div>
</div>
</div>
);
}
import InvestorNotification from '@/components/InvestorNotification';

View File

@@ -1,395 +0,0 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
ArrowLeft, Zap, Shield, TrendingUp, Check, Info,
Calendar, DollarSign, CreditCard, FileText, ChevronRight,
AlertCircle, ArrowDown, Wallet, Clock, Activity
} from 'lucide-react';
import toast from 'react-hot-toast';
const PLAN_TEMPLATES = [
{
id: '1bike',
name: '1 Bike Plan',
description: 'Investment plan for 1 bike - perfect for small investors',
evBasePrice: 200000,
minQty: 1,
minInvestment: 200000,
maxInvestment: 1000000,
duration: 12,
lockIn: 3,
exitPenalty: 10,
ficoShare: { single: 45, own: 55, ev: 60 },
icon: Zap,
color: 'bg-blue-600',
lightColor: 'bg-blue-50 text-blue-700'
},
{
id: '5bike',
name: '5 Bike Plan',
description: 'Perfect for established investors looking for better returns',
evBasePrice: 180000,
minQty: 5,
minInvestment: 900000,
maxInvestment: 5000000,
duration: 24,
lockIn: 6,
exitPenalty: 15,
ficoShare: { single: 40, own: 50, ev: 55 },
icon: TrendingUp,
color: 'bg-purple-600',
lightColor: 'bg-purple-50 text-purple-700'
}
];
export default function NewInvestmentPage() {
const router = useRouter();
const [step, setStep] = useState<'select' | 'form'>('select');
const [selectedTemplate, setSelectedTemplate] = useState<typeof PLAN_TEMPLATES[0] | null>(null);
const [formData, setFormData] = useState({
planName: '',
planType: 'Gold',
investmentAmount: 0,
initialPayment: 0,
startDate: new Date().toISOString().split('T')[0],
endDate: '',
paymentMethod: 'Bank Transfer',
transactionRef: '',
description: ''
});
const handleSelectTemplate = (template: typeof PLAN_TEMPLATES[0]) => {
setSelectedTemplate(template);
setFormData({
...formData,
planName: template.name,
investmentAmount: template.minInvestment,
initialPayment: Math.floor(template.minInvestment * 0.5) // Default 50% initial
});
setStep('form');
};
const handleCreateInvestment = (e: React.FormEvent) => {
e.preventDefault();
if (formData.investmentAmount < (selectedTemplate?.minInvestment || 0)) {
toast.error(`Minimum investment for this plan is ৳${selectedTemplate?.minInvestment.toLocaleString()}`);
return;
}
toast.success('Investment request created successfully! Admin will review and assign bikes.');
router.push('/investor/plans');
};
return (
<div className="p-4 lg:p-6 max-w-5xl mx-auto mb-20 lg:mb-0">
<div className="flex items-center gap-4 mb-8">
<button
onClick={() => step === 'form' ? setStep('select') : router.back()}
className="p-2 hover:bg-slate-100 rounded-xl transition-colors border border-slate-200"
>
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div>
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800 flex items-center gap-2">
<Zap className="w-5 h-5 lg:w-6 lg:h-6 text-investor" />
{step === 'select' ? 'Select Investment Plan' : 'Configure New Investment'}
</h1>
<p className="text-sm text-slate-500">
{step === 'select' ? 'Choose a template to start' : `Set up investment under ${selectedTemplate?.name}`}
</p>
</div>
</div>
{step === 'select' && (
<div className="grid md:grid-cols-2 gap-6">
{PLAN_TEMPLATES.map((plan) => {
const Icon = plan.icon;
return (
<div
key={plan.id}
className="bg-white rounded-2xl border-2 border-slate-100 p-6 hover:border-investor transition-all group cursor-pointer shadow-sm hover:shadow-md"
onClick={() => handleSelectTemplate(plan)}
>
<div className={`w-14 h-14 rounded-2xl mb-6 flex items-center justify-center ${plan.lightColor}`}>
<Icon className="w-7 h-7" />
</div>
<h3 className="text-xl font-extrabold text-slate-800 mb-2">{plan.name}</h3>
<p className="text-sm text-slate-500 mb-6 leading-relaxed">{plan.description}</p>
<div className="space-y-3 mb-8">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-400 font-medium">Min Investment</span>
<span className="text-slate-800 font-extrabold">{plan.minInvestment.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-400 font-medium">EV Base Price</span>
<span className="text-slate-800 font-extrabold">{plan.evBasePrice.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-400 font-medium">Duration</span>
<span className="text-slate-800 font-extrabold">{plan.duration} Months</span>
</div>
</div>
<button className="w-full py-3.5 bg-slate-50 text-slate-800 rounded-xl font-bold group-hover:bg-investor group-hover:text-white transition-all flex items-center justify-center gap-2">
Select Template <ChevronRight className="w-4 h-4" />
</button>
</div>
);
})}
</div>
)}
{step === 'form' && selectedTemplate && (
<form onSubmit={handleCreateInvestment} className="space-y-6">
<div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{/* Core Configuration */}
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<h3 className="text-lg font-bold text-slate-800 mb-6 flex items-center gap-2">
<FileText className="w-5 h-5 text-investor" /> Plan Configuration
</h3>
<div className="grid md:grid-cols-2 gap-5 mb-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Plan Name *</label>
<input
type="text"
value={formData.planName}
onChange={(e) => setFormData({...formData, planName: e.target.value})}
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-investor/20 focus:border-investor outline-none transition-all font-medium"
placeholder="e.g. My First Bike"
required
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Plan Type</label>
<select
value={formData.planType}
onChange={(e) => setFormData({...formData, planType: e.target.value})}
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-investor/20 focus:border-investor outline-none transition-all font-medium"
>
<option>Gold</option>
<option>Silver</option>
<option>Platinum</option>
<option>Diamond</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 bg-slate-50 rounded-xl border border-slate-100 mb-6 text-center">
<div>
<p className="text-[10px] text-slate-400 font-bold uppercase mb-1">EV Base Price</p>
<p className="text-sm font-bold text-slate-800">{selectedTemplate.evBasePrice.toLocaleString()}</p>
</div>
<div>
<p className="text-[10px] text-slate-400 font-bold uppercase mb-1">Min Qty</p>
<p className="text-sm font-bold text-slate-800">{selectedTemplate.minQty} Bike(s)</p>
</div>
<div>
<p className="text-[10px] text-slate-400 font-bold uppercase mb-1">Min Invest</p>
<p className="text-sm font-bold text-slate-800">{selectedTemplate.minInvestment.toLocaleString()}</p>
</div>
<div>
<p className="text-[10px] text-slate-400 font-bold uppercase mb-1">Max Invest</p>
<p className="text-sm font-bold text-slate-800">{selectedTemplate.maxInvestment.toLocaleString()}</p>
</div>
</div>
<div className="grid md:grid-cols-2 gap-5">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Investment Amount () *</label>
<input
type="number"
value={formData.investmentAmount}
onChange={(e) => setFormData({...formData, investmentAmount: Number(e.target.value)})}
className="w-full px-4 py-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-investor/20 focus:border-investor outline-none transition-all font-extrabold"
required
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Initial Payment () *</label>
<input
type="number"
value={formData.initialPayment}
onChange={(e) => setFormData({...formData, initialPayment: Number(e.target.value)})}
className="w-full px-4 py-3 border border-investor rounded-xl text-sm focus:ring-2 focus:ring-investor/20 outline-none transition-all font-extrabold text-investor"
placeholder="Amount to pay now"
required
/>
<p className="text-[10px] text-slate-400 mt-1 font-medium">Pay part of your investment now to confirm</p>
</div>
</div>
</div>
{/* Profit Sharing Policy */}
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-investor" /> FICO Share - Jaiben's Profit per Ride
</h3>
<p className="text-xs text-slate-500 mb-6 font-medium">Profit sharing when bikes are rented to end customers</p>
<div className="grid grid-cols-3 gap-4">
<div className="bg-slate-50 rounded-xl p-4 border border-slate-100 text-center">
<p className="text-[10px] text-slate-500 font-bold uppercase mb-1">Single Rent</p>
<p className="text-xl font-extrabold text-slate-800">{selectedTemplate.ficoShare.single}%</p>
</div>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-100 text-center">
<p className="text-[10px] text-slate-500 font-bold uppercase mb-1">Rent to Own</p>
<p className="text-xl font-extrabold text-slate-800">{selectedTemplate.ficoShare.own}%</p>
</div>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-100 text-center">
<p className="text-[10px] text-slate-500 font-bold uppercase mb-1">Share an EV</p>
<p className="text-xl font-extrabold text-slate-800">{selectedTemplate.ficoShare.ev}%</p>
</div>
</div>
</div>
{/* Auto-Journal Entry Visualization */}
<div className="bg-slate-900 rounded-2xl p-6 shadow-xl overflow-hidden relative">
<div className="absolute top-0 right-0 w-32 h-32 bg-investor/10 rounded-full blur-3xl -mr-16 -mt-16" />
<h3 className="text-lg font-bold text-white mb-6 flex items-center gap-2">
<Activity className="w-5 h-5 text-investor" /> Auto-Journal Entry (Draft)
</h3>
<div className="space-y-4 relative z-10">
<div className="flex justify-between items-center text-xs font-bold uppercase text-slate-400 mb-2 px-2">
<span>Account Details</span>
<span>Amount (৳)</span>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-4 flex items-center justify-between">
<div>
<p className="text-[10px] text-green-400 font-bold uppercase mb-1">Debit (Dr)</p>
<p className="text-sm font-bold text-white">Bank - City Bank</p>
<p className="text-[10px] text-slate-500 font-mono">CODE: 1200</p>
</div>
<p className="text-lg font-extrabold text-green-400">৳{formData.investmentAmount.toLocaleString()}</p>
</div>
<div className="flex justify-center -my-2 relative z-20">
<div className="bg-investor w-8 h-8 rounded-full flex items-center justify-center shadow-lg">
<ArrowDown className="w-4 h-4 text-white" />
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-4 flex items-center justify-between">
<div>
<p className="text-[10px] text-blue-400 font-bold uppercase mb-1">Credit (Cr)</p>
<p className="text-sm font-bold text-white">Investor Liabilities</p>
<p className="text-[10px] text-slate-500 font-mono">CODE: 2200</p>
</div>
<p className="text-lg font-extrabold text-blue-400">৳{formData.investmentAmount.toLocaleString()}</p>
</div>
<div className="pt-4 border-t border-white/10 mt-2 flex justify-between items-center px-2">
<p className="text-[10px] text-slate-500 font-medium">Reference: <span className="text-slate-300 font-mono">INV/{new Date().getFullYear()}/AUTO-{Math.random().toString(36).substring(7).toUpperCase()}</span></p>
<p className="text-[10px] text-slate-500 font-medium tracking-tight uppercase">Status: <span className="text-amber-400">Draft - On Creation</span></p>
</div>
</div>
</div>
</div>
<div className="space-y-6">
{/* Time & Period */}
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<h3 className="text-base font-bold text-slate-800 mb-4 flex items-center gap-2">
<Calendar className="w-5 h-5 text-investor" /> Schedule
</h3>
<div className="space-y-4">
<div>
<label className="block text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-2">Start Date *</label>
<input
type="date"
value={formData.startDate}
onChange={(e) => setFormData({...formData, startDate: e.target.value})}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-investor/20 outline-none font-bold"
required
/>
</div>
<div>
<label className="block text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-2">End Date (Optional)</label>
<input
type="date"
value={formData.endDate}
onChange={(e) => setFormData({...formData, endDate: e.target.value})}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-investor/20 outline-none font-bold"
/>
</div>
<div className="p-3 bg-blue-50 rounded-xl border border-blue-100 flex items-start gap-3">
<AlertCircle className="w-4 h-4 text-blue-600 shrink-0 mt-0.5" />
<p className="text-[10px] text-blue-700 font-medium leading-relaxed">
Duration is fixed at <strong>{selectedTemplate.duration} months</strong> with a <strong>{selectedTemplate.lockIn} month</strong> lock-in period as per template.
</p>
</div>
</div>
</div>
{/* Payment Details */}
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<h3 className="text-base font-bold text-slate-800 mb-4 flex items-center gap-2">
<CreditCard className="w-5 h-5 text-investor" /> Payment
</h3>
<div className="space-y-4">
<div>
<label className="block text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-2">Method</label>
<select
value={formData.paymentMethod}
onChange={(e) => setFormData({...formData, paymentMethod: e.target.value})}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-investor/20 outline-none font-bold"
>
<option>Bank Transfer</option>
<option>Mobile Banking</option>
<option>Cheque</option>
<option>Cash</option>
</select>
</div>
<div>
<label className="block text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-2">Reference ID</label>
<input
type="text"
value={formData.transactionRef}
onChange={(e) => setFormData({...formData, transactionRef: e.target.value})}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-investor/20 outline-none font-bold"
placeholder="Auto-generated if empty"
/>
</div>
<div className="pt-4 border-t border-slate-100">
<div className="flex justify-between items-center mb-1">
<span className="text-xs text-slate-500 font-bold uppercase">Pay Now</span>
<span className="text-lg font-extrabold text-investor">৳{formData.initialPayment.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500 font-bold uppercase">Balance Due</span>
<span className="text-sm font-bold text-slate-800">৳{(formData.investmentAmount - formData.initialPayment).toLocaleString()}</span>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="space-y-3">
<button
type="submit"
className="w-full py-4 bg-investor text-white rounded-2xl font-bold text-lg shadow-lg shadow-investor/30 hover:bg-investor-dark transition-all transform hover:-translate-y-1 active:scale-95"
>
Create Investment
</button>
<button
type="button"
onClick={() => setStep('select')}
className="w-full py-3 text-slate-600 font-bold text-sm hover:bg-slate-100 rounded-xl transition-colors"
>
Cancel & Back
</button>
</div>
</div>
</div>
</form>
)}
</div>
);
}

View File

@@ -2,139 +2,500 @@
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Target, ArrowRight, Zap, TrendingUp, CreditCard, Plus, FileText, ChevronRight, Wallet, Clock, Percent } from 'lucide-react'; import { Target, Plus, Zap, ChevronRight, ArrowRight, Edit, Trash2, Eye, TrendingUp, X, CreditCard, Battery } from 'lucide-react';
import { investors } from '@/data/mockData'; import { investors } from '@/data/mockData';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
export default function MyInvestmentsPage() { export default function MyInvestmentsPage() {
const investor = investors[0]; // mock logged-in investor const investor = investors[0];
const [showCreateModal, setShowCreateModal] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
const [newInvestment, setNewInvestment] = useState({
planName: '', planType: 'gold', assetType: 'bike', totalInvestment: 0, initialPayment: 0, paymentType: 'full',
startDate: '', endDate: '', paymentMethod: 'bank', transactionReference: '', notes: ''
});
const PLAN_TEMPLATES = [
{ id: '1bike', name: '1 Bike Plan', tier: 'Standard', evBasePrice: 200000, minQuantity: 1, duration: 12, maxInvestment: 1000000, type: 'bike' },
{ id: '5bike', name: '5 Bike Plan', tier: 'Premium', evBasePrice: 180000, minQuantity: 5, duration: 24, maxInvestment: 5000000, type: 'bike' },
{ id: '10bike', name: '10 Bike Plan', tier: 'Enterprise', evBasePrice: 170000, minQuantity: 10, duration: 36, maxInvestment: 10000000, type: 'bike' },
];
const BATTERY_TEMPLATES = [
{ id: '1battery', name: '1 Battery Pack Plan', tier: 'Standard', evBasePrice: 45000, minQuantity: 1, duration: 12, maxInvestment: 500000, type: 'battery' },
{ id: '5battery', name: '5 Battery Pack Plan', tier: 'Premium', evBasePrice: 42000, minQuantity: 5, duration: 18, maxInvestment: 2000000, type: 'battery' },
{ id: '10battery', name: '10 Battery Pack Fleet', tier: 'Enterprise', evBasePrice: 40000, minQuantity: 10, duration: 24, maxInvestment: 5000000, type: 'battery' },
];
const planConfig: Record<string, { bg: string; border: string; icon: string }> = {
silver: { bg: 'bg-slate-100', border: 'border-slate-300', icon: 'text-slate-500' },
gold: { bg: 'bg-amber-100', border: 'border-amber-300', icon: 'text-amber-500' },
platinum: { bg: 'bg-purple-100', border: 'border-purple-300', icon: 'text-purple-500' },
diamond: { bg: 'bg-blue-100', border: 'border-blue-300', icon: 'text-blue-500' },
};
const handleCreate = () => {
if (!newInvestment.planName || !newInvestment.totalInvestment) {
toast.error('Please fill all required fields');
return;
}
if (newInvestment.paymentType === 'partial' && newInvestment.initialPayment < newInvestment.totalInvestment * 0.5) {
toast.error('Initial payment must be at least 50% of investment amount');
return;
}
toast.success(newInvestment.paymentType === 'partial'
? `Investment created! Initial: ৳${newInvestment.initialPayment.toLocaleString()}, Balance: ৳${(newInvestment.totalInvestment - newInvestment.initialPayment).toLocaleString()}`
: 'Investment created successfully!'
);
setShowCreateModal(false);
setSelectedTemplate(null);
setNewInvestment({ planName: '', planType: 'gold', assetType: 'bike', totalInvestment: 0, initialPayment: 0, paymentType: 'full', startDate: '', endDate: '', paymentMethod: 'bank', transactionReference: '', notes: '' });
};
return ( return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto mb-20 lg:mb-0"> <div className="min-h-screen lg:pt-6 pt-0">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8"> <InvestorNotification isMobile />
<div> <div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2"> {/* Header */}
<Target className="w-5 h-5 lg:w-6 lg:h-6 text-investor" /> My Investments <div className="mb-6">
</h1> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<p className="text-sm text-slate-500 mt-1">Manage your active portfolios and track your earnings.</p> <div>
</div> <h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2">
<Target className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> My Investments
<Link </h1>
href="/investor/plans/new" <p className="text-sm text-slate-500 mt-1">Manage your active portfolios and track your earnings.</p>
className="inline-flex items-center gap-2 px-6 py-3 bg-investor text-white rounded-xl font-bold shadow-lg shadow-investor/20 hover:bg-investor-dark transition-all transform hover:-translate-y-0.5 active:scale-95"
>
<Plus className="w-5 h-5" /> New Investment
</Link>
</div>
{/* Portfolio Summary */}
<div className="mb-10 bg-slate-50 rounded-2xl p-6 border border-slate-200">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
<Target className="w-5 h-5 text-investor" /> Portfolio Overview
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
<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">
<Wallet className="w-5 h-5 text-purple-600" />
</div>
<p className="text-xs text-slate-500 font-bold uppercase tracking-wider">Total Invested</p>
</div> </div>
<p className="text-2xl font-extrabold text-slate-800">{investor.totalInvested.toLocaleString()}</p> {/* <button
</div> onClick={() => setShowCreateModal(true)}
<div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200"> className="px-4 py-2.5 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2 shadow-sm"
<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"> <Plus className="w-4 h-4" /> Create Investment
<TrendingUp className="w-5 h-5 text-green-600" /> </button> */}
</div>
<p className="text-xs text-slate-500 font-bold uppercase tracking-wider">Total Returns</p>
</div>
<p className="text-2xl font-extrabold text-green-600">{investor.totalEarnings.toLocaleString()}</p>
</div>
<div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
<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">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<p className="text-xs text-slate-500 font-bold uppercase tracking-wider">Active Bikes</p>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investor.activeBikes} Units</p>
</div> </div>
</div> </div>
</div>
{/* My Active Investments List */} {/* EV Investment Plans Cards */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> <div className="space-y-6">
<div className="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50"> <div className="flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2"> <div>
<Zap className="w-5 h-5 text-investor" /> Active Investment Plans <h3 className="font-semibold text-slate-800">EV Investment Plans</h3>
</h2> <p className="text-sm text-slate-500">Manage investment portfolios for this investor</p>
<span className="text-xs font-bold px-3 py-1 bg-investor/10 text-investor rounded-full"> </div>
{investor.investments?.length || 0} Total </div>
</span>
</div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="overflow-x-auto"> {investor.investments?.map((inv) => {
<table className="w-full text-left"> const style = planConfig[inv.planType] || planConfig.gold;
<thead> return (
<tr className="bg-slate-50 border-b border-slate-100"> <div key={inv.id} className={`bg-white rounded-xl border ${style.border} overflow-hidden`}>
<th className="px-6 py-4 text-xs font-bold text-slate-400 uppercase tracking-wider">Investment Plan</th> <div className={`${style.bg} p-4 flex items-center justify-between`}>
<th className="px-6 py-4 text-xs font-bold text-slate-400 uppercase tracking-wider">Capital Invested</th> <div>
<th className="px-6 py-4 text-xs font-bold text-slate-400 uppercase tracking-wider text-right">Actual Returns</th> <h4 className="font-bold text-slate-800 flex items-center gap-1.5">
<th className="px-6 py-4 text-xs font-bold text-slate-400 uppercase tracking-wider">Status</th> {inv.planName.toLowerCase().includes('battery') ? (
<th className="px-6 py-4 text-xs font-bold text-slate-400 uppercase tracking-wider text-right">Action</th> <Battery className="w-4 h-4 text-emerald-600 animate-pulse flex-shrink-0" />
</tr> ) : (
</thead> <Zap className="w-4 h-4 text-investor flex-shrink-0" />
<tbody className="divide-y divide-slate-100"> )}
{investor.investments && investor.investments.length > 0 ? investor.investments.map((inv) => ( {inv.planName}
<tr key={inv.id} className="hover:bg-slate-50 transition-colors group"> </h4>
<td className="px-6 py-5"> <p className="text-sm text-slate-500 capitalize">{inv.planType} Plan</p>
<p className="text-base font-bold text-slate-800">{inv.planName}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.5 rounded font-bold uppercase">{inv.planType}</span>
<span className="text-[10px] text-slate-400 font-medium">Started: {inv.startDate}</span>
</div> </div>
</td> <span className={`text-xs font-medium px-2.5 py-1 rounded-full ${inv.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-200 text-slate-600'}`}>
<td className="px-6 py-5">
<p className="text-sm font-extrabold text-slate-700">{inv.totalInvestment.toLocaleString()}</p>
</td>
<td className="px-6 py-5 text-right">
<p className="text-sm font-extrabold text-green-600">{inv.actualEarnings.toLocaleString()}</p>
<p className="text-[10px] text-slate-400 font-bold uppercase mt-1">+{inv.expectedRoi}% ROI</p>
</td>
<td className="px-6 py-5">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold uppercase ${inv.status === 'active' ? 'bg-green-50 text-green-700 border border-green-100' : 'bg-slate-50 text-slate-600 border border-slate-100'
}`}>
<div className={`w-1.5 h-1.5 rounded-full ${inv.status === 'active' ? 'bg-green-500' : 'bg-slate-400'}`} />
{inv.status} {inv.status}
</span> </span>
</td> </div>
<td className="px-6 py-5 text-right"> <div className="p-4 space-y-4">
<Link <div className="grid grid-cols-2 gap-3">
href={`/investor/investments/${inv.id}`} <div className="bg-slate-50 rounded-lg p-3">
className="inline-flex items-center gap-2 text-investor hover:text-investor-dark font-bold text-sm transition-all group-hover:gap-3" <p className="text-xs text-slate-500">Investment</p>
> <p className="font-bold text-slate-800">{inv.totalInvestment.toLocaleString()}</p>
View Details <ChevronRight className="w-4 h-4" /> </div>
</Link> <div className="bg-purple-50 rounded-lg p-3">
</td> <p className="text-xs text-purple-600">Total Return</p>
</tr> <p className="font-bold text-purple-700">{inv.actualEarnings.toLocaleString()}</p>
)) : ( </div>
<tr>
<td colSpan={5} className="px-6 py-16 text-center">
<div className="flex flex-col items-center">
<Target className="w-12 h-12 text-slate-200 mb-3" />
<p className="text-slate-500 font-bold">No active investments found.</p>
<Link href="/investor/plans/new" className="mt-4 text-invest-600 font-bold text-sm hover:underline">
Start your first investment
</Link>
</div> </div>
</td> <div className="bg-slate-50 rounded-lg p-3 space-y-1.5">
</tr> <div className="flex justify-between text-xs">
)} <span className="text-slate-400">Assigned Assets</span>
</tbody> <span className="font-semibold text-slate-800">
</table> {inv.planName.toLowerCase().includes('battery') || inv.assetType === 'battery' ? (
`${inv.batteryIds?.length || 3} Batteries`
) : (
`${inv.bikeIds?.length || 1} Bike${(inv.bikeIds?.length || 1) > 1 ? 's' : ''}`
)}
</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-400">Duration</span>
<span className="font-medium">{inv.durationMonths || 12} months</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-400">Lock-in Period</span>
<span className="font-medium">{inv.lockInMonths || 3} months</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-400">Early Exit Penalty</span>
<span className="font-medium text-red-500">{inv.exitPenaltyPercent || 10}%</span>
</div>
<div className="flex justify-between text-xs border-t border-slate-200/60 pt-1.5">
<span className="text-slate-400">Profit Sharing</span>
<span className="font-semibold text-emerald-600">
{inv.planName.toLowerCase().includes('battery') || inv.assetType === 'battery' ? (
'40%'
) : (
'40% / 50% / 55%'
)}
</span>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-400">{inv.startDate} - {inv.endDate || 'Ongoing'}</span>
<span className="capitalize text-slate-500">{inv.paymentMethod}</span>
</div>
<div className="pt-3 border-t border-slate-100">
<p className="text-xs text-slate-400 mb-2">ID: #{inv.id?.slice(-6) || 'N/A'}</p>
<div className="flex gap-2">
<Link href={`/investor/investments/${inv.id}`} className="flex-1 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg border border-slate-200 text-center">
<Eye className="w-4 h-4 inline mr-1" /> View
</Link>
</div>
</div>
</div>
</div>
);
})}
</div>
{(!investor.investments || investor.investments.length === 0) && (
<div className="text-center py-16 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200">
<TrendingUp className="w-12 h-12 mx-auto mb-3 text-slate-300" />
<h4 className="font-semibold text-slate-600 mb-2">No Investments Yet</h4>
<p className="text-slate-400 mb-4">Create your first investment plan</p>
<button onClick={() => setShowCreateModal(true)} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark">
<Plus className="w-4 h-4 inline mr-1" /> Create Investment
</button>
</div>
)}
</div> </div>
{/* Create Investment Modal */}
{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-3xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
<Plus className="w-5 h-5 text-investor" /> Create New Investment
</h2>
<p className="text-sm text-slate-500">Set up investment for {investor.name}</p>
</div>
<button onClick={() => setShowCreateModal(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 space-y-5">
{!selectedTemplate ? (
<>
{/* Category Selector */}
<div>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Choose Investment Asset Type *</label>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => {
setNewInvestment({ ...newInvestment, assetType: 'bike' });
}}
className={`py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-bold text-sm transition-all ${
newInvestment.assetType === 'bike'
? 'bg-investor/10 border-investor text-investor shadow-sm'
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
}`}
>
<Zap className="w-4 h-4 text-investor animate-pulse" /> Bike Investment Plans
</button>
<button
onClick={() => {
setNewInvestment({ ...newInvestment, assetType: 'battery' });
}}
className={`py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-bold text-sm transition-all ${
newInvestment.assetType === 'battery'
? 'bg-emerald-100 border-emerald-500 text-emerald-800 shadow-sm'
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
}`}
>
<Battery className="w-4 h-4 text-emerald-600 animate-pulse" /> Battery Investment Plans
</button>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-2 block">Select Plan Template</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{(newInvestment.assetType === 'battery' ? BATTERY_TEMPLATES : PLAN_TEMPLATES).map(plan => (
<button
key={plan.id}
onClick={() => {
setSelectedTemplate(plan);
setNewInvestment({
...newInvestment,
planName: plan.name,
planType: plan.tier.toLowerCase() as any,
totalInvestment: plan.evBasePrice * plan.minQuantity,
startDate: new Date().toISOString().split('T')[0],
});
}}
className={`p-4 rounded-lg border-2 text-left transition-all hover:bg-slate-50 ${newInvestment.assetType === 'battery' ? 'border-slate-200 hover:border-emerald-500' : 'border-slate-200 hover:border-investor/50'}`}
>
<p className="font-semibold text-slate-800">{plan.name}</p>
<p className="text-xs text-slate-500 mt-1">{plan.evBasePrice.toLocaleString()} × {plan.minQuantity} {newInvestment.assetType}(s)</p>
<p className="text-sm text-slate-600 mt-1">Duration: {plan.duration} months</p>
</button>
))}
</div>
</div>
</>
) : (
<>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<button onClick={() => setSelectedTemplate(null)} className="text-xs text-investor hover:underline"> Change Template</button>
<span className="text-slate-300">|</span>
<span className="text-sm font-medium text-slate-700">{selectedTemplate.name}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Plan Name *</label>
<input
type="text"
value={newInvestment.planName}
onChange={(e) => setNewInvestment({ ...newInvestment, planName: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="Plan name"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Plan Type</label>
<select
value={newInvestment.planType}
onChange={(e) => setNewInvestment({ ...newInvestment, planType: e.target.value as any })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
<option value="diamond">Diamond</option>
</select>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">{selectedTemplate.type === 'battery' ? 'Battery Pack Cost (৳)' : 'EV Base Price (৳)'}</label>
<input type="number" value={selectedTemplate.evBasePrice} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-50 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Minimum Quantity ({selectedTemplate.type === 'battery' ? 'Packs' : 'Bikes'})</label>
<input type="number" value={selectedTemplate.minQuantity} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-50 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Min Investment ()</label>
<input type="number" value={selectedTemplate.evBasePrice * selectedTemplate.minQuantity} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
<p className="text-xs text-slate-400 mt-1">= Qty × Base Price</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investment Amount () *</label>
<input
type="number"
value={newInvestment.totalInvestment}
onChange={(e) => setNewInvestment({ ...newInvestment, totalInvestment: Number(e.target.value) })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-yellow-50"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Max Investment ()</label>
<input type="number" value={selectedTemplate.maxInvestment} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200">
<h4 className="text-sm font-semibold text-slate-800 mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4 text-investor" /> Payment Options
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<label className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${newInvestment.paymentType === 'full' ? 'border-investor bg-investor/5' : 'border-slate-200 hover:border-slate-300'}`}>
<input
type="radio"
name="paymentType"
value="full"
checked={newInvestment.paymentType === 'full'}
onChange={() => setNewInvestment({ ...newInvestment, paymentType: 'full', initialPayment: newInvestment.totalInvestment })}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-800 text-sm">Full Payment</p>
<p className="text-xs text-slate-500">Pay total amount at once</p>
</div>
<span className="text-lg font-bold text-green-600">{newInvestment.totalInvestment.toLocaleString()}</span>
</label>
<label className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${newInvestment.paymentType === 'partial' ? 'border-investor bg-investor/5' : 'border-slate-200 hover:border-slate-300'}`}>
<input
type="radio"
name="paymentType"
value="partial"
checked={newInvestment.paymentType === 'partial'}
onChange={() => setNewInvestment({ ...newInvestment, paymentType: 'partial', initialPayment: Math.floor(newInvestment.totalInvestment * 0.5) })}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-800 text-sm">Partial Payment</p>
<p className="text-xs text-slate-500">Pay initial amount (50% min)</p>
</div>
<span className="text-sm font-bold text-amber-600">Min {Math.floor(newInvestment.totalInvestment * 0.5).toLocaleString()}</span>
</label>
</div>
{newInvestment.paymentType === 'partial' && (
<div className="border-t border-slate-200 pt-4 mt-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Initial Payment () *</label>
<input
type="number"
value={newInvestment.initialPayment}
onChange={(e) => {
const val = Number(e.target.value);
if (val >= newInvestment.totalInvestment * 0.5 && val <= newInvestment.totalInvestment) {
setNewInvestment({ ...newInvestment, initialPayment: val });
}
}}
className="w-full px-3 py-2 border border-investor rounded-lg text-sm bg-white"
/>
<p className="text-xs text-slate-500 mt-2">Balance: {(newInvestment.totalInvestment - newInvestment.initialPayment).toLocaleString()}</p>
</div>
)}
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Duration (Months)</label>
<input type="number" value={selectedTemplate.duration} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Lock-in Period (Months)</label>
<input type="number" value={3} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Exit Penalty (%)</label>
<input type="number" value={10} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<h4 className="text-sm font-semibold text-amber-800 mb-3 flex items-center gap-2">
<TrendingUp className="w-4 h-4" /> FICO Share - Profit per Ride
</h4>
<p className="text-xs text-amber-600 mb-3">Profit sharing when bikes are rented to end customers</p>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-xs text-slate-500 mb-1 block">Single Rent (%)</label>
<input type="number" value={45} disabled className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm bg-white cursor-not-allowed" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Rent to Own (%)</label>
<input type="number" value={55} disabled className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm bg-white cursor-not-allowed" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Share EV (%)</label>
<input type="number" value={60} disabled className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm bg-white cursor-not-allowed" />
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Start Date *</label>
<input type="date" value={newInvestment.startDate} onChange={(e) => setNewInvestment({ ...newInvestment, startDate: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">End Date</label>
<input type="date" value={newInvestment.endDate} onChange={(e) => setNewInvestment({ ...newInvestment, endDate: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select value={newInvestment.paymentMethod} onChange={(e) => setNewInvestment({ ...newInvestment, paymentMethod: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
<option value="cheque">Cheque</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Transaction Reference</label>
<input type="text" value={newInvestment.transactionReference} onChange={(e) => setNewInvestment({ ...newInvestment, transactionReference: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Auto-generated if empty" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Description</label>
<textarea value={newInvestment.notes} onChange={(e) => setNewInvestment({ ...newInvestment, notes: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Add notes..." />
</div>
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
<h4 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
<TrendingUp className="w-4 h-4" /> Auto-Journal Entry
</h4>
<div className="bg-white rounded-lg p-4 border border-green-100">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-slate-500">Date:</span>
<span className="font-medium">{newInvestment.startDate || 'Not set'}</span>
</div>
<div className="flex items-center justify-between text-sm mb-3">
<span className="text-slate-500">Reference:</span>
<span className="font-medium">{newInvestment.transactionReference || `INV/${new Date().getFullYear()}/auto`}</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200">
<div>
<p className="text-xs text-green-600 font-medium uppercase">Debit (Dr)</p>
<p className="font-medium text-slate-800">{newInvestment.paymentMethod === 'bank' ? 'Bank - City Bank' : newInvestment.paymentMethod === 'cash' ? 'Cash in Hand' : 'bKash Business'} ({newInvestment.paymentMethod === 'bank' ? '1200' : newInvestment.paymentMethod === 'cash' ? '1100' : '1300'})</p>
</div>
<p className="font-bold text-green-700">{newInvestment.paymentType === 'partial' ? newInvestment.initialPayment.toLocaleString() : newInvestment.totalInvestment.toLocaleString()}</p>
</div>
<div className="flex justify-center">
<div className="w-8 h-8 rounded-full bg-green-200 flex items-center justify-center">
<span className="text-green-600"></span>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-200">
<div>
<p className="text-xs text-blue-600 font-medium uppercase">Credit (Cr)</p>
<p className="font-medium text-slate-800">Investor Liabilities (2200)</p>
</div>
<p className="font-bold text-blue-700">{newInvestment.totalInvestment.toLocaleString()}</p>
</div>
</div>
</div>
</div>
</>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowCreateModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
{selectedTemplate && (
<button onClick={handleCreate} disabled={!newInvestment.planName || !newInvestment.totalInvestment} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark disabled:opacity-50 flex items-center gap-2">
<Plus className="w-4 h-4" /> Create Investment
</button>
)}
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
); );

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import {
ChevronLeft, ChevronRight, CheckCircle, XCircle, AlertCircle, Calendar ChevronLeft, ChevronRight, CheckCircle, XCircle, AlertCircle, Calendar
} from 'lucide-react'; } from 'lucide-react';
import { investors, bikes, rentalPayments } from '@/data/mockData'; import { investors, bikes, rentalPayments } from '@/data/mockData';
import InvestorNotification from '@/components/InvestorNotification';
export default function RentalHistoryPage() { export default function RentalHistoryPage() {
const investor = investors.find(i => i.id === 'inv1') || investors[0]; const investor = investors.find(i => i.id === 'inv1') || investors[0];
@@ -64,311 +65,314 @@ export default function RentalHistoryPage() {
}; };
return ( return (
<div className="p-4 sm:p-6 max-w-8xl mx-auto"> <div className="min-h-screen lg:pt-6 pt-0">
{/* Header */} <InvestorNotification isMobile />
<div className="mb-6"> <div className="pt-18 lg:pt-0 p-4 sm:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> {/* Header */}
<div> <div className="mb-6">
<h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<History className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> Rental History <div>
</h1> <h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2">
<p className="text-sm text-slate-500 mt-1">Track daily rental payments from your bikes</p> <History className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> Rental History
</h1>
<p className="text-sm text-slate-500 mt-1">Track daily rental payments from your bikes</p>
</div>
<button className="px-4 py-2.5 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2 shadow-sm w-fit">
<Download className="w-4 h-4" /> Export
</button>
</div> </div>
<button className="px-4 py-2.5 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2 shadow-sm w-fit">
<Download className="w-4 h-4" /> Export
</button>
</div> </div>
</div>
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4"> <div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0"> <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-green-600" /> <DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Collected</p>
<p className="text-lg font-bold text-green-600">{totalCollected.toLocaleString()}</p>
</div>
</div> </div>
<div> </div>
<p className="text-xs text-slate-500">Total Collected</p> <div className="bg-white rounded-xl border border-slate-200 p-4">
<p className="text-lg font-bold text-green-600">{totalCollected.toLocaleString()}</p> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Bike className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Active Rentals</p>
<p className="text-lg font-bold text-slate-800">{activeRentals}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending</p>
<p className="text-lg font-bold text-amber-600">{totalPending.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<AlertCircle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Failed</p>
<p className="text-lg font-bold text-red-600">{totalFailed.toLocaleString()}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Bike className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Active Rentals</p>
<p className="text-lg font-bold text-slate-800">{activeRentals}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending</p>
<p className="text-lg font-bold text-amber-600">{totalPending.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<AlertCircle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Failed</p>
<p className="text-lg font-bold text-red-600">{totalFailed.toLocaleString()}</p>
</div>
</div>
</div>
</div>
{/* Main Table Card */} {/* Main Table Card */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden"> <div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Filters */} {/* Filters */}
<div className="p-4 border-b border-slate-100"> <div className="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3"> <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<div className="relative"> <div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" /> <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input <input
type="text" type="text"
placeholder="Search biker, bike..." placeholder="Search biker, bike..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64" className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64"
/> />
</div> </div>
<select <select
value={bikeFilter} value={bikeFilter}
onChange={(e) => { setBikeFilter(e.target.value); setPage(1); }} onChange={(e) => { setBikeFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="all">All Bikes</option>
{investorBikes.map(bike => (
<option key={bike.id} value={bike.id}>{bike.model}</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-xs text-slate-500 font-medium">
<Calendar className="w-4 h-4" /> Date Range
</div>
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white" className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
> >
Clear <option value="all">All Bikes</option>
</button> {investorBikes.map(bike => (
)} <option key={bike.id} value={bike.id}>{bike.model}</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-xs text-slate-500 font-medium">
<Calendar className="w-4 h-4" /> Date Range
</div>
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
>
Clear
</button>
)}
</div>
</div> </div>
</div> </div>
</div>
{/* Card View - Mobile/Tablet */} {/* Card View - Mobile/Tablet */}
<div className="lg:hidden divide-y divide-slate-100"> <div className="lg:hidden divide-y divide-slate-100">
{paginatedPayments.length > 0 ? paginatedPayments.map((payment) => { {paginatedPayments.length > 0 ? paginatedPayments.map((payment) => {
const status = statusConfig[payment.status] || statusConfig.pending; const status = statusConfig[payment.status] || statusConfig.pending;
const plan = planConfig[payment.planType] || planConfig.single; const plan = planConfig[payment.planType] || planConfig.single;
const StatusIcon = status.icon; const StatusIcon = status.icon;
return ( return (
<div key={payment.id} className="p-4 hover:bg-slate-50"> <div key={payment.id} className="p-4 hover:bg-slate-50">
<div className="flex items-start justify-between gap-3 mb-3"> <div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Bike className="w-4 h-4 text-slate-400 shrink-0" /> <Bike className="w-4 h-4 text-slate-400 shrink-0" />
<p className="text-sm font-semibold text-slate-800 truncate">{payment.bikeModel}</p> <p className="text-sm font-semibold text-slate-800 truncate">{payment.bikeModel}</p>
</div>
<p className="text-xs text-slate-400 ml-6">{payment.plateNumber}</p>
<div className="flex items-center gap-2 mt-1 ml-6">
<User className="w-3 h-3 text-slate-400" />
<p className="text-xs text-slate-600">{payment.bikerName}</p>
</div>
</div> </div>
<p className="text-xs text-slate-400 ml-6">{payment.plateNumber}</p> <div className="text-right shrink-0">
<div className="flex items-center gap-2 mt-1 ml-6"> <p className="text-base font-bold text-slate-800">{payment.amount.toLocaleString()}</p>
<User className="w-3 h-3 text-slate-400" /> <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium mt-1 ${status.bg} ${status.color}`}>
<p className="text-xs text-slate-600">{payment.bikerName}</p> <StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div> </div>
</div> </div>
<div className="text-right shrink-0"> <div className="flex items-center justify-between ml-6 text-xs text-slate-400">
<p className="text-base font-bold text-slate-800">{payment.amount.toLocaleString()}</p> <span>{payment.date}</span>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium mt-1 ${status.bg} ${status.color}`}> <span className={`px-2 py-0.5 rounded ${plan.bg} ${plan.color}`}>{plan.label}</span>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div> </div>
</div> </div>
<div className="flex items-center justify-between ml-6 text-xs text-slate-400"> );
<span>{payment.date}</span> }) : (
<span className={`px-2 py-0.5 rounded ${plan.bg} ${plan.color}`}>{plan.label}</span> <div className="p-8 text-center text-slate-500">
</div> <History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => {
if (sortBy === 'date') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('date'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Biker</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Plan</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Duration</th>
<th
onClick={() => {
if (sortBy === 'amount') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('amount'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Amount {sortBy === 'amount' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedPayments.length > 0 ? paginatedPayments.map((payment) => {
const status = statusConfig[payment.status] || statusConfig.pending;
const plan = planConfig[payment.planType] || planConfig.single;
const StatusIcon = status.icon;
return (
<tr key={payment.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-slate-800">{payment.date}</p>
<p className="text-xs text-slate-400">{payment.transactionId || payment.id}</p>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-800">{payment.bikeModel}</p>
<p className="text-xs text-slate-400">{payment.plateNumber}</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-700">{payment.bikerName}</span>
</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${plan.bg} ${plan.color}`}>
{plan.label}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{payment.duration}</span>
</td>
<td className="px-4 py-3">
<p className="text-sm font-bold text-slate-800">{payment.amount.toLocaleString()}</p>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{payment.paymentMethod}</span>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</td>
</tr>
);
}) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-500">
<History className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{sortedPayments.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedPayments.length)} of {sortedPayments.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = totalPages <= 5 ? i + 1 : page <= 3 ? i + 1 : page >= totalPages - 2 ? totalPages - 4 + i : page - 2 + i;
return (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-medium ${page === pageNum ? 'bg-investor text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{pageNum}
</button>
);
})}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
</div> </div>
);
}) : (
<div className="p-8 text-center text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div> </div>
)} )}
</div> </div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => {
if (sortBy === 'date') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('date'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Biker</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Plan</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Duration</th>
<th
onClick={() => {
if (sortBy === 'amount') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('amount'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Amount {sortBy === 'amount' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedPayments.length > 0 ? paginatedPayments.map((payment) => {
const status = statusConfig[payment.status] || statusConfig.pending;
const plan = planConfig[payment.planType] || planConfig.single;
const StatusIcon = status.icon;
return (
<tr key={payment.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-slate-800">{payment.date}</p>
<p className="text-xs text-slate-400">{payment.transactionId || payment.id}</p>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-800">{payment.bikeModel}</p>
<p className="text-xs text-slate-400">{payment.plateNumber}</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-700">{payment.bikerName}</span>
</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${plan.bg} ${plan.color}`}>
{plan.label}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{payment.duration}</span>
</td>
<td className="px-4 py-3">
<p className="text-sm font-bold text-slate-800">{payment.amount.toLocaleString()}</p>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{payment.paymentMethod}</span>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</td>
</tr>
);
}) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-500">
<History className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{sortedPayments.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedPayments.length)} of {sortedPayments.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = totalPages <= 5 ? i + 1 : page <= 3 ? i + 1 : page >= totalPages - 2 ? totalPages - 4 + i : page - 2 + i;
return (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-medium ${page === pageNum ? 'bg-investor text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{pageNum}
</button>
);
})}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import { CreditCard, Wallet, History, CheckCircle, Clock, Building2, Smartphone, AlertCircle, Settings, X, Bike, ChevronDown, Search, Filter, ChevronLeft, ChevronRight } from 'lucide-react'; import { CreditCard, Wallet, History, CheckCircle, Clock, Building2, Smartphone, AlertCircle, Settings, X, Bike, ChevronDown, Search, Filter, ChevronLeft, ChevronRight } from 'lucide-react';
import { investors, transactions, bikes } from '@/data/mockData'; import { investors, transactions, bikes } from '@/data/mockData';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
export default function InvestorWithdrawPage() { export default function InvestorWithdrawPage() {
const investor = investors.find(i => i.id === 'inv1') || investors[0]; const investor = investors.find(i => i.id === 'inv1') || investors[0];
@@ -107,149 +108,108 @@ export default function InvestorWithdrawPage() {
}; };
return ( return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto"> <div className="min-h-screen lg:pt-6 pt-0 ">
{/* Header */} <InvestorNotification isMobile />
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<div> {/* Header */}
<h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2"> <div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CreditCard className="w-5 h-5 lg:w-6 lg:h-6 text-investor" /> Withdraw Funds
</h1>
<p className="text-sm text-slate-500 mt-1">Request withdrawals to your bank or mobile banking accounts</p>
</div>
<button
onClick={() => setShowAutoWithdrawModal(true)}
className="px-4 py-2.5 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-2 shadow-sm bg-white w-fit"
>
<Settings className="w-4 h-4" /> Configure Auto-Withdraw
</button>
</div>
{/* Balance Cards - Mobile Responsive Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
<div className="sm:col-span-2 bg-investor to-investor-dark rounded-xl p-5 text-white shadow-lg relative overflow-hidden order-first lg:order-first">
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/3 blur-2xl"></div>
<div className="relative z-10">
<div className="flex items-center gap-2 text-investor-light mb-2">
<Wallet className="w-4 h-4" />
<span className="font-medium text-xs">Available Balance</span>
</div>
<p className="text-3xl font-bold mb-1">{availableBalance.toLocaleString()}</p>
<p className="text-xs text-white/80">Ready to withdraw</p>
</div>
</div>
<div className="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
<p className="text-xs text-amber-600 font-medium mb-1">Pending Request</p>
<p className="text-xl font-bold text-amber-600">{investor.pendingEarnings.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
<p className="text-xs text-slate-600 font-medium mb-1">Total Withdrawn</p>
<p className="text-xl font-bold text-slate-700">{investor.totalWithdrawn.toLocaleString()}</p>
</div>
</div>
{/* Alert + Action Button */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-6">
<div className="flex-1 bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3 order-last sm:order-first">
<AlertCircle className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div> <div>
<h4 className="text-sm font-bold text-amber-800 mb-1">Pending Requests</h4> <h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2">
<p className="text-xs text-amber-700">You currently have <b>{investor.pendingEarnings.toLocaleString()}</b> in pending withdrawals. Processing takes 1-3 business days.</p> <CreditCard className="w-5 h-5 lg:w-6 lg:h-6 text-investor" /> Withdraw Funds
</h1>
<p className="text-sm text-slate-500 mt-1">Request withdrawals to your bank or mobile banking accounts</p>
</div> </div>
<button
onClick={() => setShowAutoWithdrawModal(true)}
className="px-4 py-2.5 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-2 shadow-sm bg-white w-fit"
>
<Settings className="w-4 h-4" /> Configure Auto-Withdraw
</button>
</div> </div>
<button
onClick={() => setShowWithdrawModal(true)}
className="px-5 py-3 bg-investor text-white rounded-xl font-semibold text-sm shadow-md hover:bg-investor-dark transition-all flex items-center justify-center gap-2 whitespace-nowrap sm:w-auto"
>
<CreditCard className="w-4 h-4" /> Withdrawal Request
</button>
</div>
{/* Recent Withdrawals Card */} {/* Balance Cards - Mobile Responsive Grid */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
<div className="p-4 border-b border-slate-100 bg-slate-50"> <div className="sm:col-span-2 bg-investor to-investor-dark rounded-xl p-5 text-white shadow-lg relative overflow-hidden order-first lg:order-first">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/3 blur-2xl"></div>
<h3 className="font-semibold text-slate-800 flex items-center gap-2"> <div className="relative z-10">
<History className="w-5 h-5 text-slate-400" /> Recent Withdrawals <div className="flex items-center gap-2 text-investor-light mb-2">
</h3> <Wallet className="w-4 h-4" />
<div className="flex items-center gap-2"> <span className="font-medium text-xs">Available Balance</span>
<select </div>
value={withdrawFilter} <p className="text-3xl font-bold mb-1">{availableBalance.toLocaleString()}</p>
onChange={(e) => { setWithdrawFilter(e.target.value); setWithdrawPage(1); }} <p className="text-xs text-white/80">Ready to withdraw</p>
className="px-3 py-1.5 border border-slate-200 rounded-lg text-xs bg-white"
>
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="pending">Pending</option>
</select>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
<p className="text-xs text-amber-600 font-medium mb-1">Pending Request</p>
<p className="text-xl font-bold text-amber-600">{investor.pendingEarnings.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
<p className="text-xs text-slate-600 font-medium mb-1">Total Withdrawn</p>
<p className="text-xl font-bold text-slate-700">{investor.totalWithdrawn.toLocaleString()}</p>
</div>
</div> </div>
{/* Alert + Action Button */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-6">
<div className="flex-1 bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3 order-last sm:order-first">
<AlertCircle className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<h4 className="text-sm font-bold text-amber-800 mb-1">Pending Requests</h4>
<p className="text-xs text-amber-700">You currently have <b>{investor.pendingEarnings.toLocaleString()}</b> in pending withdrawals. Processing takes 1-3 business days.</p>
</div>
</div>
<button
onClick={() => setShowWithdrawModal(true)}
className="px-5 py-3 bg-investor text-white rounded-xl font-semibold text-sm shadow-md hover:bg-investor-dark transition-all flex items-center justify-center gap-2 whitespace-nowrap sm:w-auto"
>
<CreditCard className="w-4 h-4" /> Withdrawal Request
</button>
</div>
{/* Recent Withdrawals Card */}
{/* Card View - Mobile/Tablet */} <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="lg:hidden divide-y divide-slate-100"> <div className="p-4 border-b border-slate-100 bg-slate-50">
{paginatedWithdrawals.length > 0 ? paginatedWithdrawals.map((t) => ( <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div key={t.id} className="p-4 sm:p-5 hover:bg-slate-50 transition-colors"> <h3 className="font-semibold text-slate-800 flex items-center gap-2">
<div className="flex items-start justify-between gap-3 mb-3"> <History className="w-5 h-5 text-slate-400" /> Recent Withdrawals
<div className="flex-1 min-w-0"> </h3>
<p className="text-sm font-medium text-slate-800 mb-1">{t.description || 'Withdrawal'}</p> <div className="flex items-center gap-2">
<div className="flex items-center gap-2 text-xs text-slate-400"> <select
<span>{t.createdAt}</span> value={withdrawFilter}
{t.referenceNumber && ( onChange={(e) => { setWithdrawFilter(e.target.value); setWithdrawPage(1); }}
<> className="px-3 py-1.5 border border-slate-200 rounded-lg text-xs bg-white"
<span>&bull;</span> >
<span className="hidden sm:inline">{t.referenceNumber}</span> <option value="all">All</option>
</> <option value="completed">Completed</option>
)} <option value="pending">Pending</option>
</div> </select>
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">{t.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-semibold mt-1 ${t.status === 'completed' ? 'bg-green-100 text-green-700' :
t.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{t.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{t.status === 'pending' && <Clock className="w-3 h-3" />}
{t.status}
</span>
</div>
</div> </div>
</div> </div>
)) : ( </div>
<div className="px-4 py-12 text-center text-sm text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
No withdrawals found.
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse"> {/* Card View - Mobile/Tablet */}
<thead> <div className="lg:hidden divide-y divide-slate-100">
<tr className="bg-white border-b border-slate-100"> {paginatedWithdrawals.length > 0 ? paginatedWithdrawals.map((t) => (
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Date</th> <div key={t.id} className="p-4 sm:p-5 hover:bg-slate-50 transition-colors">
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider hidden sm:table-cell">Ref</th> <div className="flex items-start justify-between gap-3 mb-3">
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Description</th> <div className="flex-1 min-w-0">
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Amount</th> <p className="text-sm font-medium text-slate-800 mb-1">{t.description || 'Withdrawal'}</p>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th> <div className="flex items-center gap-2 text-xs text-slate-400">
</tr> <span>{t.createdAt}</span>
</thead> {t.referenceNumber && (
<tbody className="divide-y divide-slate-100"> <>
{paginatedWithdrawals.length > 0 ? paginatedWithdrawals.map((t) => ( <span>&bull;</span>
<tr key={t.id} className="hover:bg-slate-50 transition-colors"> <span className="hidden sm:inline">{t.referenceNumber}</span>
<td className="px-4 py-3.5 text-xs text-slate-600 whitespace-nowrap">{t.createdAt}</td> </>
<td className="px-4 py-3.5 text-xs text-slate-500 hidden sm:table-cell">{t.referenceNumber || '-'}</td> )}
<td className="px-4 py-3.5"> </div>
<p className="text-sm font-medium text-slate-800">{t.description || 'Withdrawal'}</p> </div>
</td> <div className="text-right shrink-0">
<td className="px-4 py-3.5 text-sm font-bold text-slate-800">{t.amount.toLocaleString()}</td> <p className="text-base font-bold text-slate-800">{t.amount.toLocaleString()}</p>
<td className="px-4 py-3.5"> <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-semibold mt-1 ${t.status === 'completed' ? 'bg-green-100 text-green-700' :
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-semibold ${t.status === 'completed' ? 'bg-green-100 text-green-700' :
t.status === 'pending' ? 'bg-amber-100 text-amber-700' : t.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700' 'bg-red-100 text-red-700'
}`}> }`}>
@@ -257,330 +217,374 @@ export default function InvestorWithdrawPage() {
{t.status === 'pending' && <Clock className="w-3 h-3" />} {t.status === 'pending' && <Clock className="w-3 h-3" />}
{t.status} {t.status}
</span> </span>
</td> </div>
</div>
</div>
)) : (
<div className="px-4 py-12 text-center text-sm text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
No withdrawals found.
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-white border-b border-slate-100">
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Date</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider hidden sm:table-cell">Ref</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Description</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Amount</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
</tr> </tr>
)) : ( </thead>
<tr> <tbody className="divide-y divide-slate-100">
<td colSpan={5} className="px-4 py-12 text-center text-sm text-slate-500"> {paginatedWithdrawals.length > 0 ? paginatedWithdrawals.map((t) => (
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" /> <tr key={t.id} className="hover:bg-slate-50 transition-colors">
No withdrawals found. <td className="px-4 py-3.5 text-xs text-slate-600 whitespace-nowrap">{t.createdAt}</td>
</td> <td className="px-4 py-3.5 text-xs text-slate-500 hidden sm:table-cell">{t.referenceNumber || '-'}</td>
</tr> <td className="px-4 py-3.5">
)} <p className="text-sm font-medium text-slate-800">{t.description || 'Withdrawal'}</p>
</tbody> </td>
</table> <td className="px-4 py-3.5 text-sm font-bold text-slate-800">{t.amount.toLocaleString()}</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-semibold ${t.status === 'completed' ? 'bg-green-100 text-green-700' :
t.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{t.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{t.status === 'pending' && <Clock className="w-3 h-3" />}
{t.status}
</span>
</td>
</tr>
)) : (
<tr>
<td colSpan={5} className="px-4 py-12 text-center text-sm text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
No withdrawals found.
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalWithdrawPages > 1 && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs text-slate-500">
Showing {((withdrawPage - 1) * withdrawPageSize) + 1} to {Math.min(withdrawPage * withdrawPageSize, filteredWithdrawals.length)} of {filteredWithdrawals.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setWithdrawPage(p => Math.max(1, p - 1))}
disabled={withdrawPage === 1}
className="p-1.5 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4 text-slate-500" />
</button>
{Array.from({ length: Math.min(5, totalWithdrawPages) }, (_, i) => {
const page = totalWithdrawPages <= 5 ? i + 1 : withdrawPage <= 3 ? i + 1 : withdrawPage >= totalWithdrawPages - 2 ? totalWithdrawPages - 4 + i : withdrawPage - 2 + i;
return (
<button
key={page}
onClick={() => setWithdrawPage(page)}
className={`w-8 h-8 rounded-lg text-xs font-medium ${withdrawPage === page ? 'bg-investor text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{page}
</button>
);
})}
<button
onClick={() => setWithdrawPage(p => Math.min(totalWithdrawPages, p + 1))}
disabled={withdrawPage === totalWithdrawPages}
className="p-1.5 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4 text-slate-500" />
</button>
</div>
</div>
)}
</div> </div>
{/* Pagination */} {/* Withdrawal Request Modal */}
{totalWithdrawPages > 1 && ( {
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3"> showWithdrawModal && (
<p className="text-xs text-slate-500"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
Showing {((withdrawPage - 1) * withdrawPageSize) + 1} to {Math.min(withdrawPage * withdrawPageSize, filteredWithdrawals.length)} of {filteredWithdrawals.length} <div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
</p> <div className="p-4 sm:p-5 border-b border-slate-100 flex items-center justify-between bg-gradient-to-r from-slate-50 to-white">
<div className="flex items-center gap-1"> <h3 className="text-base sm:text-lg font-bold text-slate-800">Create Withdrawal Request</h3>
<button <button onClick={() => setShowWithdrawModal(false)} className="p-1.5 hover:bg-slate-100 rounded-lg transition-colors">
onClick={() => setWithdrawPage(p => Math.max(1, p - 1))} <X className="w-5 h-5 text-slate-400" />
disabled={withdrawPage === 1}
className="p-1.5 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4 text-slate-500" />
</button>
{Array.from({ length: Math.min(5, totalWithdrawPages) }, (_, i) => {
const page = totalWithdrawPages <= 5 ? i + 1 : withdrawPage <= 3 ? i + 1 : withdrawPage >= totalWithdrawPages - 2 ? totalWithdrawPages - 4 + i : withdrawPage - 2 + i;
return (
<button
key={page}
onClick={() => setWithdrawPage(page)}
className={`w-8 h-8 rounded-lg text-xs font-medium ${withdrawPage === page ? 'bg-investor text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{page}
</button> </button>
);
})}
<button
onClick={() => setWithdrawPage(p => Math.min(totalWithdrawPages, p + 1))}
disabled={withdrawPage === totalWithdrawPages}
className="p-1.5 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4 text-slate-500" />
</button>
</div>
</div>
)}
</div>
{/* Withdrawal Request Modal */}
{
showWithdrawModal && (
<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-4 sm:p-5 border-b border-slate-100 flex items-center justify-between bg-gradient-to-r from-slate-50 to-white">
<h3 className="text-base sm:text-lg font-bold text-slate-800">Create Withdrawal Request</h3>
<button onClick={() => setShowWithdrawModal(false)} className="p-1.5 hover:bg-slate-100 rounded-lg transition-colors">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-4 sm:p-5 overflow-y-auto flex-1 space-y-4 sm:space-y-5">
{/* Balance Cards */}
<div className="grid grid-cols-3 gap-2 sm:gap-3">
<div className="bg-green-50 rounded-lg p-3 border border-green-200">
<p className="text-xs text-green-600 font-medium">Available</p>
<p className="text-sm font-bold text-green-700">{availableBalance.toLocaleString()}</p>
</div>
<div className="bg-amber-50 rounded-lg p-3 border border-amber-200">
<p className="text-xs text-amber-600 font-medium">Pending</p>
<p className="text-sm font-bold text-amber-700">{investor.pendingEarnings.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3 border border-slate-200">
<p className="text-xs text-slate-600 font-medium">Withdrawn</p>
<p className="text-sm font-bold text-slate-700">{investor.totalWithdrawn.toLocaleString()}</p>
</div>
</div> </div>
{/* Select Investment Plans & Bikes */} <div className="p-4 sm:p-5 overflow-y-auto flex-1 space-y-4 sm:space-y-5">
<div> {/* Balance Cards */}
<h4 className="font-semibold text-slate-800 mb-2">Select Investment Plans & Bikes</h4> <div className="grid grid-cols-3 gap-2 sm:gap-3">
<div className="space-y-3"> <div className="bg-green-50 rounded-lg p-3 border border-green-200">
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-200"> <p className="text-xs text-green-600 font-medium">Available</p>
<input <p className="text-sm font-bold text-green-700">{availableBalance.toLocaleString()}</p>
type="checkbox"
id="selectAll"
checked={selectAll}
onChange={(e) => toggleSelectAll(e.target.checked)}
className="w-5 h-5 text-investor rounded border-slate-300"
/>
<label htmlFor="selectAll" className="flex-1 cursor-pointer">
<span className="font-semibold text-slate-800">Select All</span>
<p className="text-xs text-slate-500">Include all investments and bikes</p>
</label>
</div> </div>
<div className="bg-amber-50 rounded-lg p-3 border border-amber-200">
<p className="text-xs text-amber-600 font-medium">Pending</p>
<p className="text-sm font-bold text-amber-700">{investor.pendingEarnings.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3 border border-slate-200">
<p className="text-xs text-slate-600 font-medium">Withdrawn</p>
<p className="text-sm font-bold text-slate-700">{investor.totalWithdrawn.toLocaleString()}</p>
</div>
</div>
<div className="border border-slate-200 rounded-lg overflow-hidden"> {/* Select EV Investment Plans & Bikes */}
<div className="bg-slate-100 px-3 py-2 border-b border-slate-200"> <div>
<p className="text-sm font-semibold text-slate-700">Investment Plans</p> <h4 className="font-semibold text-slate-800 mb-2">Select EV Investment Plans & Bikes</h4>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<input
type="checkbox"
id="selectAll"
checked={selectAll}
onChange={(e) => toggleSelectAll(e.target.checked)}
className="w-5 h-5 text-investor rounded border-slate-300"
/>
<label htmlFor="selectAll" className="flex-1 cursor-pointer">
<span className="font-semibold text-slate-800">Select All</span>
<p className="text-xs text-slate-500">Include all investments and bikes</p>
</label>
</div> </div>
<div className="divide-y divide-slate-100">
{investor.investments?.map((inv: any) => { <div className="border border-slate-200 rounded-lg overflow-hidden">
const invBikes = assignedBikes.filter((b: any) => b.investmentId === inv.id); <div className="bg-slate-100 px-3 py-2 border-b border-slate-200">
const invEarnings = invBikes.reduce((sum: number, b: any) => sum + (b.totalEarnings || 0), 0); <p className="text-sm font-semibold text-slate-700">EV Investment Plans</p>
return ( </div>
<div key={inv.id} className="p-3 sm:p-4"> <div className="divide-y divide-slate-100">
<div className="flex items-center gap-3 mb-2"> {investor.investments?.map((inv: any) => {
<input const invBikes = assignedBikes.filter((b: any) => b.investmentId === inv.id);
type="checkbox" const invEarnings = invBikes.reduce((sum: number, b: any) => sum + (b.totalEarnings || 0), 0);
id={`plan-${inv.id}`} return (
checked={selectAll || selectedPlans.includes(inv.id)} <div key={inv.id} className="p-3 sm:p-4">
disabled={selectAll} <div className="flex items-center gap-3 mb-2">
onChange={() => togglePlan(inv.id, invBikes)} <input
className="w-4 h-4 text-investor rounded border-slate-300" type="checkbox"
/> id={`plan-${inv.id}`}
<label htmlFor={`plan-${inv.id}`} className="flex-1 flex items-center justify-between cursor-pointer"> checked={selectAll || selectedPlans.includes(inv.id)}
<div className="flex items-center gap-2"> disabled={selectAll}
<span className="font-medium text-slate-800 text-sm">{inv.planName}</span> onChange={() => togglePlan(inv.id, invBikes)}
<span className={`px-2 py-0.5 rounded text-xs capitalize font-medium ${planColors[inv.planType]}`}>{inv.planType}</span> className="w-4 h-4 text-investor rounded border-slate-300"
</div> />
<span className="text-sm font-bold text-green-600">{invEarnings.toLocaleString()}</span> <label htmlFor={`plan-${inv.id}`} className="flex-1 flex items-center justify-between cursor-pointer">
</label> <div className="flex items-center gap-2">
</div> <span className="font-medium text-slate-800 text-sm">{inv.planName}</span>
{!selectAll && invBikes.length > 0 && ( <span className={`px-2 py-0.5 rounded text-xs capitalize font-medium ${planColors[inv.planType]}`}>{inv.planType}</span>
<div className="ml-6 sm:ml-8 pl-3 sm:pl-4 border-l-2 border-slate-200 space-y-2">
{invBikes.map((bike: any) => (
<div key={bike.id} className="flex items-center gap-2 p-2 bg-white rounded-lg border border-slate-100">
<input
type="checkbox"
id={`bike-${bike.id}`}
checked={selectAll || selectedBikes.includes(bike.id)}
onChange={() => toggleBike(bike.id)}
className="w-4 h-4 text-investor rounded border-slate-300"
/>
<label htmlFor={`bike-${bike.id}`} className="flex-1 flex items-center gap-2 cursor-pointer">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<span className="text-sm font-medium text-slate-700">{bike.model}</span>
<span className="text-xs text-slate-400 ml-2">{bike.plateNumber}</span>
</div>
</label>
<span className="text-xs text-green-600 font-semibold">{bike.totalEarnings?.toLocaleString() || 0}</span>
</div> </div>
))} <span className="text-sm font-bold text-green-600">{invEarnings.toLocaleString()}</span>
</label>
</div> </div>
)} {!selectAll && invBikes.length > 0 && (
</div> <div className="ml-6 sm:ml-8 pl-3 sm:pl-4 border-l-2 border-slate-200 space-y-2">
); {invBikes.map((bike: any) => (
})} <div key={bike.id} className="flex items-center gap-2 p-2 bg-white rounded-lg border border-slate-100">
<input
type="checkbox"
id={`bike-${bike.id}`}
checked={selectAll || selectedBikes.includes(bike.id)}
onChange={() => toggleBike(bike.id)}
className="w-4 h-4 text-investor rounded border-slate-300"
/>
<label htmlFor={`bike-${bike.id}`} className="flex-1 flex items-center gap-2 cursor-pointer">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<span className="text-sm font-medium text-slate-700">{bike.model}</span>
<span className="text-xs text-slate-400 ml-2">{bike.plateNumber}</span>
</div>
</label>
<span className="text-xs text-green-600 font-semibold">{bike.totalEarnings?.toLocaleString() || 0}</span>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Withdrawal Amount */} {/* Withdrawal Amount */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4"> <div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-slate-800 text-sm">Withdrawal Amount</h4> <h4 className="font-semibold text-slate-800 text-sm">Withdrawal Amount</h4>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${selectAll ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}> <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${selectAll ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
{selectAll ? 'All Selected' : `${selectedBikes.length} bikes`} {selectAll ? 'All Selected' : `${selectedBikes.length} bikes`}
</span> </span>
</div>
<p className="text-2xl sm:text-3xl font-bold text-green-700 mb-1">{calculatedAmount.toLocaleString()}</p>
<p className="text-xs text-slate-500">Based on {selectAll ? 'all' : selectedBikes.length} selected bike(s) earnings</p>
</div> </div>
<p className="text-2xl sm:text-3xl font-bold text-green-700 mb-1">{calculatedAmount.toLocaleString()}</p>
<p className="text-xs text-slate-500">Based on {selectAll ? 'all' : selectedBikes.length} selected bike(s) earnings</p>
</div>
{/* Payment Method */} {/* Payment Method */}
<div> <div>
<h4 className="font-semibold text-slate-800 mb-2">Payment Method</h4> <h4 className="font-semibold text-slate-800 mb-2">Payment Method</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{(investor as any).bankAccounts?.map((account: any) => ( {(investor as any).bankAccounts?.map((account: any) => (
<div
key={account.id}
onClick={() => { setPaymentMethod('bank'); setSelectedAccount(account.id); }}
className={`p-3 sm:p-4 rounded-lg border cursor-pointer transition-all ${selectedAccount === account.id ? 'border-investor bg-investor/5 ring-2 ring-investor/20' : 'border-slate-200 hover:border-slate-300'}`}
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center bg-slate-100">
<Building2 className="w-4 h-4 sm:w-5 sm:h-5 text-slate-500" />
</div>
<div>
<p className="text-sm font-semibold text-slate-800">{account.bankName}</p>
<p className="text-xs text-slate-500 font-mono">{account.accountNumber}</p>
</div>
</div>
</div>
))}
</div>
{(investor as any).mobileBanking && (
<div <div
key={account.id} onClick={() => { setPaymentMethod('mobile'); setSelectedAccount('mobile'); }}
onClick={() => { setPaymentMethod('bank'); setSelectedAccount(account.id); }} className={`mt-2 p-3 sm:p-4 rounded-lg border cursor-pointer transition-all ${selectedAccount === 'mobile' ? 'border-investor bg-investor/5 ring-2 ring-investor/20' : 'border-slate-200 hover:border-slate-300'}`}
className={`p-3 sm:p-4 rounded-lg border cursor-pointer transition-all ${selectedAccount === account.id ? 'border-investor bg-investor/5 ring-2 ring-investor/20' : 'border-slate-200 hover:border-slate-300'}`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center bg-slate-100"> <div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-pink-100 flex items-center justify-center">
<Building2 className="w-4 h-4 sm:w-5 sm:h-5 text-slate-500" /> <Smartphone className="w-4 h-4 sm:w-5 sm:h-5 text-pink-600" />
</div> </div>
<div> <div>
<p className="text-sm font-semibold text-slate-800">{account.bankName}</p> <p className="text-sm font-semibold text-slate-800">{(investor as any).mobileBanking}</p>
<p className="text-xs text-slate-500 font-mono">{account.accountNumber}</p> <p className="text-xs text-slate-500 font-mono">{(investor as any).mobileBankingNumber}</p>
</div> </div>
</div> </div>
</div> </div>
))}
</div>
{(investor as any).mobileBanking && (
<div
onClick={() => { setPaymentMethod('mobile'); setSelectedAccount('mobile'); }}
className={`mt-2 p-3 sm:p-4 rounded-lg border cursor-pointer transition-all ${selectedAccount === 'mobile' ? 'border-investor bg-investor/5 ring-2 ring-investor/20' : 'border-slate-200 hover:border-slate-300'}`}
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-pink-100 flex items-center justify-center">
<Smartphone className="w-4 h-4 sm:w-5 sm:h-5 text-pink-600" />
</div>
<div>
<p className="text-sm font-semibold text-slate-800">{(investor as any).mobileBanking}</p>
<p className="text-xs text-slate-500 font-mono">{(investor as any).mobileBankingNumber}</p>
</div>
</div>
</div>
)}
</div>
</div>
<div className="p-4 sm:p-5 border-t border-slate-200 flex justify-end gap-2 sm:gap-3 bg-slate-50">
<button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 sm:py-2.5 border border-slate-300 text-slate-600 rounded-lg text-sm font-medium hover:bg-white hover:shadow-sm transition-all">
Cancel
</button>
<button
onClick={handleSubmitWithdraw}
disabled={!selectedAccount || (selectedBikes.length === 0 && !selectAll)}
className="px-5 py-2 sm:py-2.5 bg-investor text-white rounded-lg text-sm font-semibold hover:bg-investor-dark disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-all flex items-center gap-2"
>
<CreditCard className="w-4 h-4" /> Submit Request
</button>
</div>
</div>
</div>
)
}
{/* Auto-Withdraw Modal */}
{
showAutoWithdrawModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60] p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h3 className="text-lg font-bold text-slate-800">Auto-Withdraw Settings</h3>
<button onClick={() => setShowAutoWithdrawModal(false)} className="p-1.5 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4">
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Enable Auto-Withdraw</label>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<button
onClick={() => setAutoWithdrawEnabled(!autoWithdrawEnabled)}
className={`w-12 h-6 rounded-full transition-colors ${autoWithdrawEnabled ? 'bg-green-500' : 'bg-slate-200'}`}
>
<span className={`block w-5 h-5 bg-white rounded-full shadow transition-transform ${autoWithdrawEnabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button>
<span className="text-sm text-slate-600">Automatically withdraw earnings</span>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Withdrawal Frequency</label>
<select
value={autoWithdrawFreq}
onChange={(e) => setAutoWithdrawFreq(e.target.value)}
className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="as_per_request">As Requested</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Minimum Amount</label>
<div className="flex items-center gap-2">
<span className="px-3 py-2.5 bg-slate-100 rounded-lg text-slate-500 font-bold"></span>
<input
type="number"
value={autoWithdrawMin}
onChange={(e) => setAutoWithdrawMin(e.target.value)}
className="flex-1 px-3 py-2.5 border border-slate-200 rounded-lg text-sm"
placeholder="1000"
/>
</div>
<p className="text-xs text-slate-500 mt-1.5">Minimum balance required for auto-withdrawal</p>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Destination Account</label>
<div className="space-y-2">
{(investor as any).bankAccounts?.map((account: any) => (
<div key={account.id} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
<input
type="radio"
name="destAccount"
checked={autoWithdrawAccount === account.id}
onChange={() => setAutoWithdrawAccount(account.id)}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-700">{account.bankName}</p>
<p className="text-xs text-slate-500 font-mono">{account.accountNumber}</p>
</div>
</div>
))}
{(investor as any).mobileBanking && (
<div className="flex items-center gap-3 p-3 bg-purple-50 rounded-lg border border-purple-100">
<input
type="radio"
name="destAccount"
checked={autoWithdrawAccount === 'mobile'}
onChange={() => setAutoWithdrawAccount('mobile')}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-700">{(investor as any).mobileBanking}</p>
<p className="text-xs text-slate-500 font-mono">{(investor as any).mobileBankingNumber}</p>
</div>
</div>
)} )}
</div> </div>
</div> </div>
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50"> <div className="p-4 sm:p-5 border-t border-slate-200 flex justify-end gap-2 sm:gap-3 bg-slate-50">
<button onClick={() => setShowAutoWithdrawModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-white">Cancel</button> <button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 sm:py-2.5 border border-slate-300 text-slate-600 rounded-lg text-sm font-medium hover:bg-white hover:shadow-sm transition-all">
<button onClick={handleSaveAutoWithdraw} className="px-5 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark shadow-sm">Save Settings</button> Cancel
</button>
<button
onClick={handleSubmitWithdraw}
disabled={!selectedAccount || (selectedBikes.length === 0 && !selectAll)}
className="px-5 py-2 sm:py-2.5 bg-investor text-white rounded-lg text-sm font-semibold hover:bg-investor-dark disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-all flex items-center gap-2"
>
<CreditCard className="w-4 h-4" /> Submit Request
</button>
</div>
</div> </div>
</div> </div>
</div> )
) }
}
{/* Auto-Withdraw Modal */}
{
showAutoWithdrawModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60] p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h3 className="text-lg font-bold text-slate-800">Auto-Withdraw Settings</h3>
<button onClick={() => setShowAutoWithdrawModal(false)} className="p-1.5 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4">
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Enable Auto-Withdraw</label>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<button
onClick={() => setAutoWithdrawEnabled(!autoWithdrawEnabled)}
className={`w-12 h-6 rounded-full transition-colors ${autoWithdrawEnabled ? 'bg-green-500' : 'bg-slate-200'}`}
>
<span className={`block w-5 h-5 bg-white rounded-full shadow transition-transform ${autoWithdrawEnabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button>
<span className="text-sm text-slate-600">Automatically withdraw earnings</span>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Withdrawal Frequency</label>
<select
value={autoWithdrawFreq}
onChange={(e) => setAutoWithdrawFreq(e.target.value)}
className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="as_per_request">As Requested</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Minimum Amount</label>
<div className="flex items-center gap-2">
<span className="px-3 py-2.5 bg-slate-100 rounded-lg text-slate-500 font-bold"></span>
<input
type="number"
value={autoWithdrawMin}
onChange={(e) => setAutoWithdrawMin(e.target.value)}
className="flex-1 px-3 py-2.5 border border-slate-200 rounded-lg text-sm"
placeholder="1000"
/>
</div>
<p className="text-xs text-slate-500 mt-1.5">Minimum balance required for auto-withdrawal</p>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Destination Account</label>
<div className="space-y-2">
{(investor as any).bankAccounts?.map((account: any) => (
<div key={account.id} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
<input
type="radio"
name="destAccount"
checked={autoWithdrawAccount === account.id}
onChange={() => setAutoWithdrawAccount(account.id)}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-700">{account.bankName}</p>
<p className="text-xs text-slate-500 font-mono">{account.accountNumber}</p>
</div>
</div>
))}
{(investor as any).mobileBanking && (
<div className="flex items-center gap-3 p-3 bg-purple-50 rounded-lg border border-purple-100">
<input
type="radio"
name="destAccount"
checked={autoWithdrawAccount === 'mobile'}
onChange={() => setAutoWithdrawAccount('mobile')}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-700">{(investor as any).mobileBanking}</p>
<p className="text-xs text-slate-500 font-mono">{(investor as any).mobileBankingNumber}</p>
</div>
</div>
)}
</div>
</div>
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
<button onClick={() => setShowAutoWithdrawModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-white">Cancel</button>
<button onClick={handleSaveAutoWithdraw} className="px-5 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark shadow-sm">Save Settings</button>
</div>
</div>
</div>
)
}
</div>
</div > </div >
); );
} }

View File

@@ -11,7 +11,7 @@ const inter = Inter({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "JAIBEN Mobility - EV Rental Platform", title: "JAIBEN Mobility - EV Rental Platform",
description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FOCO Investor", description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FICO Investor",
manifest: "/manifest.json", manifest: "/manifest.json",
appleWebApp: { appleWebApp: {
capable: true, capable: true,

View File

@@ -77,7 +77,7 @@ export default function LandingPage() {
</h2> </h2>
<p className="text-slate-400 text-lg lg:text-xl max-w-2xl mx-auto"> <p className="text-slate-400 text-lg lg:text-xl max-w-2xl mx-auto">
Rent, Rent-to-Own, or Invest in EVs. Join Bangladesh&apos;s fastest growing Rent, Rent-to-Own, or Invest in EVs. Join Bangladesh&apos;s fastest growing
electric mobility ecosystem with FOCO model for investors. electric mobility ecosystem with FICO model for investors.
</p> </p>
</div> </div>
@@ -106,7 +106,7 @@ export default function LandingPage() {
<Wallet className="w-6 h-6 text-green-500" /> <Wallet className="w-6 h-6 text-green-500" />
</div> </div>
<h3 className="text-xl font-bold text-white mb-1">Investor</h3> <h3 className="text-xl font-bold text-white mb-1">Investor</h3>
<p className="text-slate-400 text-sm mb-3">FOCO model with guaranteed returns</p> <p className="text-slate-400 text-sm mb-3">FICO model with guaranteed returns</p>
<span className="text-green-500 text-sm font-medium flex items-center gap-1"> <span className="text-green-500 text-sm font-medium flex items-center gap-1">
Login as Investor <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" /> Login as Investor <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span> </span>

View File

@@ -0,0 +1,52 @@
'use client';
import Link from 'next/link';
import { Bell, Package, DollarSign, Bike, AlertCircle, CheckCircle } from 'lucide-react';
const mockNotifications = [
{ id: '1', type: 'rental', title: 'New Rental Started', message: 'Your bike AB-1234 has been rented by rider MR-456', time: '5 min ago', read: false },
{ id: '2', type: 'earning', title: 'Earning Credited', message: '৳450 has been added to your wallet from bike CD-5678', time: '2 hours ago', read: false },
{ id: '3', type: 'success', title: 'Withdrawal Complete', message: 'Your withdrawal of ৳5,000 has been processed successfully', time: '1 day ago', read: true },
{ id: '4', type: 'alert', title: 'Maintenance Alert', message: 'Bike XY-9012 requires maintenance attention', time: '2 days ago', read: true },
];
const iconMap: Record<string, any> = {
rental: Bike, earning: DollarSign, success: CheckCircle, alert: AlertCircle, default: Package,
};
export default function InvestorNotification({ isMobile = false }: { isMobile?: boolean }) {
const unreadCount = mockNotifications.filter(n => !n.read).length;
if (isMobile) {
return (
<div className="lg:hidden fixed top-0 left-0 right-0 h-14 bg-white border-b border-slate-200 flex items-center justify-between px-4 z-40">
<div className="flex items-center gap-2">
<h1 className="text-lg font-extrabold text-accent">JAIBEN</h1>
<span className="text-[10px] text-accent font-medium">Investor</span>
</div>
<Link href="/investor/notifications" className="relative p-2 hover:bg-slate-100 rounded-lg transition-colors">
<Bell className="w-5 h-5 text-slate-600" />
{unreadCount > 0 && (
<span className="absolute top-1 right-1 w-4 h-4 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center">
{unreadCount}
</span>
)}
</Link>
</div>
);
}
return (
<div className="hidden lg:block">
<Link href="/investor/notifications" className="flex items-center justify-between p-3 rounded-lg hover:bg-slate-50 transition-colors">
<div className="flex items-center gap-3">
<Bell className="w-5 h-5 text-slate-600" />
<span className="text-sm font-medium text-slate-700">Notifications</span>
</div>
{unreadCount > 0 && (
<span className="px-2 py-0.5 bg-accent text-white text-xs font-bold rounded-full">{unreadCount}</span>
)}
</Link>
</div>
);
}

View File

@@ -13,7 +13,7 @@ export default function LayoutContent({ children }: LayoutContentProps) {
return ( return (
<> <>
{showSidebar && <Sidebar />} {showSidebar && <Sidebar />}
<main className={showSidebar ? "lg:ml-64 min-h-screen pb-20 lg:pb-0" : "min-h-screen pb-20 lg:pb-0"}> <main className={showSidebar ? "lg:ml-64 min-h-screen pb-4 lg:pb-0" : "min-h-screen pb-4 lg:pb-0"}>
{children} {children}
</main> </main>
</> </>

View File

@@ -23,7 +23,8 @@ import {
LogOut, LogOut,
Calculator, Calculator,
Wrench, Wrench,
Target, User, History Target, User, History, Bell,
Building2
} from 'lucide-react'; } from 'lucide-react';
import { getUserName, getUserRole, logout } from '@/lib/auth'; import { getUserName, getUserRole, logout } from '@/lib/auth';
@@ -38,41 +39,46 @@ const ROLE_LABELS: Record<string, string> = {
merchant: 'Merchant', merchant: 'Merchant',
}; };
const adminNavItems = [ type NavItem = { label: string; href: string; icon: any; isNotification?: boolean };
const adminNavItems: NavItem[] = [
{ label: 'Dashboard', href: '/admin', icon: BarChart3 }, { label: 'Dashboard', href: '/admin', icon: BarChart3 },
{ label: 'Notifications', href: '/admin/notifications', icon: Bell, isNotification: true },
{ label: 'KYC Requests', href: '/admin/kyc', icon: Shield }, { label: 'KYC Requests', href: '/admin/kyc', icon: Shield },
{ label: 'Rentals', href: '/admin/rentals', icon: FileText }, { label: 'Rentals', href: '/admin/rentals', icon: FileText },
{ label: 'Bikers', href: '/admin/bikers', icon: Users }, { label: 'Bikers', href: '/admin/bikers', icon: Users },
{ label: 'Investors', href: '/admin/investors', icon: Wallet }, { label: 'Investors', href: '/admin/investors', icon: Wallet },
{ label: 'Fleet Management', href: '/admin/fleet', icon: Bike }, { label: 'Fleet Management', href: '/admin/fleet', icon: Bike },
{ label: 'Battery Management', href: '/admin/batteries', icon: Battery },
{ label: 'Merchants (P2)', href: '/admin/merchants', icon: Store }, { label: 'Merchants (P2)', href: '/admin/merchants', icon: Store },
{ label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap }, { label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap },
{ label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench }, { label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench },
{ label: 'Service Centers', href: '/admin/service-centers', icon: Building2 },
{ label: 'Accounting', href: '/admin/accounting', icon: Calculator }, { label: 'Accounting', href: '/admin/accounting', icon: Calculator },
{ label: 'Hubs', href: '/admin/hub', icon: MapPin }, { label: 'Hubs', href: '/admin/hub', icon: MapPin },
{ label: 'Reports', href: '/admin/reports', icon: BarChart3 }, { label: 'Reports', href: '/admin/reports', icon: BarChart3 },
{ label: 'Users Management', href: '/admin/users', icon: Users }, { label: 'Users Management', href: '/admin/users', icon: Users },
{ label: 'Roles & Permissions', href: '/admin/roles', icon: Shield }, { label: 'Roles & Permissions', href: '/admin/roles', icon: Shield },
{ label: 'Settings', href: '/admin/settings', icon: Settings }, { label: 'Settings', href: '/admin/settings', icon: Settings },
]; ];
const bikerNavItems = [ const bikerNavItems: NavItem[] = [
{ label: 'Biker Dashboard', href: '/', icon: Bike }, { label: 'Biker Dashboard', href: '/', icon: Bike },
{ label: 'Rent Bike', href: '/rent', icon: Zap }, { label: 'Rent Bike', href: '/rent', icon: Zap },
{ label: 'Browse EVs', href: '/bikes', icon: Battery }, { label: 'Browse EVs', href: '/bikes', icon: Battery },
]; ];
const investorNavItems = [ const investorNavItems: NavItem[] = [
{ label: 'Dashboard', href: '/investor/dashboard', icon: Bike }, { label: 'Dashboard', href: '/investor/dashboard', icon: Bike },
{ label: 'My Investments', href: '/investor/plans', icon: Target }, { label: 'My Investments', href: '/investor/plans', icon: Target },
{ label: 'Rental History', href: '/investor/rental-history', icon: History }, { label: 'Rental History', href: '/investor/rental-history', icon: History },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard }, { label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
{ label: 'Notifications', href: '/investor/notifications', icon: Bell },
{ label: 'My Profile', href: '/investor/profile', icon: User }, { label: 'My Profile', href: '/investor/profile', icon: User },
]; ];
const shopNavItems = [ const shopNavItems: NavItem[] = [
{ label: 'Dashboard', href: '/shop', icon: Store }, { label: 'Dashboard', href: '/shop', icon: Store },
{ label: 'Deliveries', href: '/shop/deliveries', icon: Truck }, { label: 'Deliveries', href: '/shop/deliveries', icon: Truck },
{ label: 'Fleet', href: '/shop/fleet', icon: Bike }, { label: 'Fleet', href: '/shop/fleet', icon: Bike },
@@ -85,9 +91,39 @@ export default function Sidebar() {
const [userName, setUserName] = useState('User'); const [userName, setUserName] = useState('User');
const [userRole, setUserRole] = useState('admin'); const [userRole, setUserRole] = useState('admin');
const [adminUnreadCount, setAdminUnreadCount] = useState(5);
const [investorUnreadCount, setInvestorUnreadCount] = useState(2);
useEffect(() => { useEffect(() => {
setUserName(getUserName() || 'User'); setUserName(getUserName() || 'User');
setUserRole(getUserRole() || 'staff'); setUserRole(getUserRole() || 'staff');
const updateCounts = () => {
const adminNotifs = localStorage.getItem('jaiben_admin_notifications');
if (adminNotifs) {
try {
const parsed = JSON.parse(adminNotifs);
setAdminUnreadCount(parsed.filter((n: any) => !n.read).length);
} catch (e) { }
}
const invNotifs = localStorage.getItem('jaiben_investor_notifications');
if (invNotifs) {
try {
const parsed = JSON.parse(invNotifs);
setInvestorUnreadCount(parsed.filter((n: any) => !n.read).length);
} catch (e) { }
}
};
updateCounts();
window.addEventListener('storage', updateCounts);
window.addEventListener('jaiben_notifications_update', updateCounts);
return () => {
window.removeEventListener('storage', updateCounts);
window.removeEventListener('jaiben_notifications_update', updateCounts);
};
}, []); }, []);
const isAdmin = pathname.startsWith('/admin'); const isAdmin = pathname.startsWith('/admin');
@@ -153,6 +189,8 @@ export default function Sidebar() {
const isChild = item.href !== '/' && pathname.startsWith(item.href + '/'); const isChild = item.href !== '/' && pathname.startsWith(item.href + '/');
const isActive = isExact || isChild; const isActive = isExact || isChild;
const Icon = item.icon; const Icon = item.icon;
const isNotification = item.label === 'Notifications';
return ( return (
<Link <Link
key={item.href} key={item.href}
@@ -168,13 +206,18 @@ export default function Sidebar() {
> >
<Icon className={`w-5 h-5 ${isActive ? 'text-white' : ''}`} /> <Icon className={`w-5 h-5 ${isActive ? 'text-white' : ''}`} />
<span>{item.label}</span> <span>{item.label}</span>
{isNotification && (
<span className="ml-auto px-2 py-0.5 bg-red-500 text-white text-xs font-bold rounded-full">
{isAdmin ? adminUnreadCount : investorUnreadCount}
</span>
)}
</Link> </Link>
); );
})} })}
</nav> </nav>
<div className="absolute bottom-0 left-0 right-0 p-3 border-t border-slate-100 bg-white"> <div className="absolute bottom-0 left-0 right-0 p-3 border-t border-slate-100 bg-white">
<Link href="/admin/users/USR-001" className="flex items-center gap-3 px-3 py-2 hover:bg-slate-50 rounded-lg -mx-1"> <Link href={isInvestor ? '/investor/profile' : '/admin/users/USR-001'} className="flex items-center gap-3 px-3 py-2 hover:bg-slate-50 rounded-lg -mx-1">
<div className="w-8 h-8 rounded-full bg-accent-light flex items-center justify-center"> <div className="w-8 h-8 rounded-full bg-accent-light flex items-center justify-center">
<span className="text-sm font-bold text-accent">{userName.charAt(0).toUpperCase()}</span> <span className="text-sm font-bold text-accent">{userName.charAt(0).toUpperCase()}</span>
</div> </div>
@@ -192,10 +235,10 @@ export default function Sidebar() {
<LogOut className="w-4 h-4 text-slate-400" /> <LogOut className="w-4 h-4 text-slate-400" />
</button> </button>
</Link> </Link>
<div className="mt-2 text-xs text-slate-400 text-center"> {/* <div className="mt-2 text-xs text-slate-400 text-center">
<p>Phase 1 - Core EV Rental</p> <p>Phase 1 - Core EV Rental</p>
<p className="mt-1">v1.0.0</p> <p className="mt-1">v1.0.0</p>
</div> </div> */}
</div> </div>
</aside> </aside>

View File

@@ -96,7 +96,9 @@ export interface InvestmentPlan {
investorId: string; investorId: string;
planName: string; planName: string;
planType: 'silver' | 'gold' | 'platinum' | 'diamond'; planType: 'silver' | 'gold' | 'platinum' | 'diamond';
bikeIds: string[]; bikeIds?: string[];
batteryIds?: string[];
assetType?: 'bike' | 'battery';
totalInvestment: number; totalInvestment: number;
monthlyReturn: number; monthlyReturn: number;
expectedRoi: number; expectedRoi: number;
@@ -107,6 +109,9 @@ export interface InvestmentPlan {
paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque'; paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque';
transactionId?: string; transactionId?: string;
notes?: string; notes?: string;
durationMonths?: number;
lockInMonths?: number;
exitPenaltyPercent?: number;
createdAt: string; createdAt: string;
} }
@@ -131,9 +136,10 @@ export interface Investor {
bankAccountNumber?: string; bankAccountNumber?: string;
bankBranch?: string; bankBranch?: string;
bankRouting?: string; bankRouting?: string;
bankAccounts?: { id: string; bankName: string; accountName: string; accountNumber: string; branch?: string; routing?: string; isPrimary: boolean }[]; bankAccounts?: { id: string; bankName: string; accountName: string; accountNumber: string; branch?: string; routing?: string; isPrimary: boolean; verified?: boolean }[];
mobileBanking?: string; mobileBanking?: string;
mobileBankingNumber?: string; mobileBankingNumber?: string;
mobileBankingVerified?: boolean;
additionalMobileBanking?: { provider: string; number: string; verified: boolean }[]; additionalMobileBanking?: { provider: string; number: string; verified: boolean }[];
emergencyContactName?: string; emergencyContactName?: string;
emergencyContactRelation?: string; emergencyContactRelation?: string;
@@ -345,16 +351,17 @@ export const investors: Investor[] = [
gender: 'male', gender: 'male',
occupation: 'Business', occupation: 'Business',
nidNumber: '1234567890', nidNumber: '1234567890',
tinNumber: '9876543210',
bankName: 'Islami Bank Bangladesh Ltd', bankName: 'Islami Bank Bangladesh Ltd',
bankAccountName: 'Hasan Mahmud', bankAccountName: 'Hasan Mahmud',
bankAccountNumber: '2050 1500 2345', bankAccountNumber: '205015002345',
bankBranch: 'Dhanmondi Branch', bankBranch: 'Dhanmondi Branch',
bankRouting: '140', bankRouting: '140',
bankAccounts: [ bankAccounts: [
{ id: 'ba1', bankName: 'Islami Bank Bangladesh Ltd', accountName: 'Hasan Mahmud', accountNumber: '205015002345', branch: 'Dhanmondi Branch', routing: '140', isPrimary: true }, { id: 'ba1', bankName: 'Islami Bank Bangladesh Ltd', accountName: 'Hasan Mahmud', accountNumber: '205015002345', branch: 'Dhanmondi Branch', routing: '140', isPrimary: true },
{ id: 'ba2', bankName: 'Dutch-Bangla Bank', accountName: 'Hasan Mahmud', accountNumber: '1203456789012', branch: 'Gulshan Branch', routing: '090', isPrimary: false }, { id: 'ba2', bankName: 'Dutch-Bangla Bank', accountName: 'Hasan Mahmud', accountNumber: '1203456789012', branch: 'Gulshan Branch', routing: '090', isPrimary: false },
], ],
mobileBanking: 'Bkash', mobileBanking: 'bKash',
mobileBankingNumber: '01712345678', mobileBankingNumber: '01712345678',
additionalMobileBanking: [ additionalMobileBanking: [
{ provider: 'Nagad', number: '01712345679', verified: true } { provider: 'Nagad', number: '01712345679', verified: true }
@@ -362,7 +369,7 @@ export const investors: Investor[] = [
emergencyContactName: 'Fatema Begum', emergencyContactName: 'Fatema Begum',
emergencyContactRelation: 'Wife', emergencyContactRelation: 'Wife',
emergencyContactPhone: '01712345679', emergencyContactPhone: '01712345679',
totalInvested: 150000, totalInvested: 300000,
totalEarnings: 114250, totalEarnings: 114250,
activeBikes: 2, activeBikes: 2,
withdrawalPending: 3000, withdrawalPending: 3000,
@@ -386,7 +393,8 @@ export const investors: Investor[] = [
referralEarnings: 2500, referralEarnings: 2500,
investments: [ investments: [
{ id: 'ip1', investorId: 'inv1', planName: 'Gold EV Fleet 2024', planType: 'gold', bikeIds: ['b1'], totalInvestment: 85000, monthlyReturn: 2500, expectedRoi: 18, actualEarnings: 10000, startDate: '2024-01-15', endDate: '2025-01-14', status: 'active', paymentMethod: 'bank', transactionId: 'invt1', createdAt: '2024-01-15' }, { id: 'ip1', investorId: 'inv1', planName: 'Gold EV Fleet 2024', planType: 'gold', bikeIds: ['b1'], totalInvestment: 85000, monthlyReturn: 2500, expectedRoi: 18, actualEarnings: 10000, startDate: '2024-01-15', endDate: '2025-01-14', status: 'active', paymentMethod: 'bank', transactionId: 'invt1', createdAt: '2024-01-15' },
{ id: 'ip2', investorId: 'inv1', planName: 'Gold City Commuter', planType: 'gold', bikeIds: ['b2'], totalInvestment: 65000, monthlyReturn: 2125, expectedRoi: 18, actualEarnings: 4250, startDate: '2024-01-20', endDate: '2025-01-19', status: 'active', paymentMethod: 'mobile', transactionId: 'invt2', createdAt: '2024-01-20' } { id: 'ip2', investorId: 'inv1', planName: 'Gold City Commuter', planType: 'gold', bikeIds: ['b2'], totalInvestment: 65000, monthlyReturn: 2125, expectedRoi: 18, actualEarnings: 4250, startDate: '2024-01-20', endDate: '2025-01-19', status: 'active', paymentMethod: 'mobile', transactionId: 'invt2', createdAt: '2024-01-20' },
{ id: 'ip3', investorId: 'inv1', planName: 'Standard Battery Plan', planType: 'silver', assetType: 'battery', batteryIds: ['BAT-001', 'BAT-002'], totalInvestment: 150000, monthlyReturn: 4500, expectedRoi: 16, actualEarnings: 9000, startDate: '2024-02-01', endDate: '2025-02-01', status: 'active', paymentMethod: 'bank', transactionId: 'invt3', createdAt: '2024-02-01' }
] ]
}, },
{ {

View File

@@ -1,12 +1,57 @@
const ALL_PERMISSIONS = [
'kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user',
'settings.kyc_documents_view', 'settings.kyc_documents_config',
'settings.plan_selection_with_condition_view', 'settings.plan_selection_with_condition_config',
'settings.investment_plan_view', 'settings.investment_plan_config',
'settings.battery_investment_plan_view', 'settings.battery_investment_plan_config',
'settings.swap_station_plan_view', 'settings.swap_station_plan_config',
'settings.rider_request_plan_for_merchant_view', 'settings.rider_request_plan_for_merchant_config',
'settings.company_policy_view', 'settings.company_policy_config',
'settings.es_templates_view', 'settings.es_templates_config',
'settings.ev_parts_view', 'settings.ev_parts_config',
'dashboard.view',
'rental.requset', 'rental.accept', 'rental.reject', 'rental.view', 'rental.cancel', 'rental.edit', 'rental.image_approve', 'rental.lock', 'rental.unlock', 'rental.create',
'biker.view', 'biker.create', 'biker.edit', 'biker.delete', 'biker.status_change', 'biker.membership_change', 'biker.kyc_view', 'biker.kyc_update', 'biker.activity_view', 'biker.document_view', 'biker.document_upload', 'biker.document_delete', 'biker.rental_history_view', 'biker.payment_history_view', 'biker.wallet_view', 'biker.note_add', 'biker.note_view', 'biker.export', 'biker.make_valid_user', 'biker.lock', 'biker.unlock',
'investor.view', 'investor.create', 'investor.edit', 'investor.delete', 'investor.plan_assign', 'investor.bank_edit', 'investor.withdraw_request', 'investor.document_upload', 'investor.document_approve', 'investor.notification_view',
'battery.view', 'battery.create', 'battery.edit', 'battery.delete', 'battery.export',
'fleet.view', 'fleet.create', 'fleet.edit', 'fleet.delete', 'fleet.gps_config', 'fleet.export',
'service_center.view', 'service_center.create', 'service_center.edit', 'service_center.delete',
'maintenance.view', 'maintenance.create', 'maintenance.edit', 'maintenance.delete',
'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete', 'accounting.withdraw_process',
'hub.view', 'hub.create', 'hub.edit', 'hub.delete',
'reports.view', 'reports.export',
'users.view', 'users.create', 'users.edit', 'users.delete',
'roles.view', 'roles.config',
'notifications.view', 'messaging.compose', 'messaging.broadcast', 'messaging.schedule'
];
const ROLE_PERMISSIONS: Record<string, string[]> = { const ROLE_PERMISSIONS: Record<string, string[]> = {
super_admin: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view', 'rental.view', 'rental.create', 'rental.accept', 'rental.reject', 'rental.cancel', 'rental.edit', 'rental.image_approve', 'rental.lock', 'rental.unlock'], super_admin: ALL_PERMISSIONS,
admin_manager: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view', 'rental.view', 'rental.create', 'rental.accept', 'rental.reject', 'rental.cancel', 'rental.edit', 'rental.image_approve', 'rental.lock', 'rental.unlock'], admin_manager: ALL_PERMISSIONS.filter(p => !p.includes('delete') || p === 'biker.document_delete' || p === 'fleet.delete' || p === 'battery.delete'),
staff: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'dashboard.view', 'rental.view', 'rental.create'], staff: [
accountant: ['dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete'], 'kyc.request', 'kyc.view', 'kyc.doc_upload',
investor: ['dashboard.view', 'kyc.request', 'kyc.view'], 'settings.kyc_documents_view', 'settings.plan_selection_with_condition_view', 'settings.investment_plan_view', 'settings.battery_investment_plan_view', 'settings.swap_station_plan_view', 'settings.rider_request_plan_for_merchant_view', 'settings.company_policy_view', 'settings.es_templates_view', 'settings.ev_parts_view',
biker: ['dashboard.view', 'kyc.request', 'kyc.view', 'rentals.view', 'rentals.create'], 'dashboard.view',
'swap-station': ['dashboard.view', 'kyc.request', 'kyc.view'], 'rental.view', 'rental.create', 'rental.image_approve',
merchant: ['dashboard.view', 'kyc.request', 'kyc.view', 'merchants.view'], 'biker.view', 'biker.edit', 'biker.kyc_view', 'biker.kyc_update', 'biker.activity_view', 'biker.document_view', 'biker.document_upload', 'biker.rental_history_view', 'biker.payment_history_view', 'biker.wallet_view', 'biker.note_add', 'biker.note_view',
'investor.view', 'investor.document_upload',
'battery.view', 'fleet.view', 'service_center.view', 'maintenance.view', 'maintenance.create', 'accounting.view', 'hub.view', 'reports.view', 'notifications.view'
],
accountant: [
'dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete', 'accounting.withdraw_process', 'reports.view', 'reports.export'
],
investor: [
'dashboard.view', 'kyc.request', 'kyc.view', 'investor.view', 'investor.bank_edit', 'investor.withdraw_request', 'investor.document_upload', 'notifications.view'
],
biker: [
'dashboard.view', 'kyc.request', 'kyc.view', 'rental.requset', 'rental.accept', 'rental.reject', 'rental.view', 'biker.view', 'maintenance.create', 'maintenance.view', 'notifications.view'
],
'swap-station': [
'dashboard.view', 'kyc.request', 'kyc.view', 'notifications.view'
],
merchant: [
'dashboard.view', 'kyc.request', 'kyc.view', 'settings.rider_request_plan_for_merchant_view', 'notifications.view'
],
}; };
export const canRentalAccept = () => hasPermission('rental.accept'); export const canRentalAccept = () => hasPermission('rental.accept');