Compare commits

...

41 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
40 changed files with 14670 additions and 3181 deletions

1
JML Submodule

Submodule JML added at 7332f85512

View File

@@ -6,7 +6,8 @@ import {
DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building,
ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List,
Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet,
Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike
Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike,
Clock, Check, CheckCircle
} from 'lucide-react';
export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense';
@@ -71,6 +72,20 @@ export interface AccountingTransaction {
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[] = [
{ 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 },
@@ -223,7 +238,7 @@ function generateAutoJournalEntries(type: TransactionType, amount: number, descr
}
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 [accounts] = useState(defaultAccounts);
const [journalEntries, setJournalEntries] = useState(mockJournalEntries);
@@ -234,6 +249,14 @@ export default function AccountingPage() {
const [showModal, setShowModal] = useState(false);
const [editingTransaction, setEditingTransaction] = 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 itemsPerPage = 10;
@@ -417,7 +440,8 @@ export default function AccountingPage() {
{ id: 'transactions', label: 'Transactions', icon: Receipt },
{ id: 'journal', label: 'Journal', icon: BookOpen },
{ id: 'ledger', label: 'Ledger', icon: List },
{ id: 'accounts', label: 'Chart of Accounts', icon: Calculator },
{ id: 'accounts', label: 'Accounts', icon: Calculator },
{ id: 'withdraw', label: 'Withdraw', icon: ArrowDownLeft },
];
return (
@@ -453,6 +477,11 @@ export default function AccountingPage() {
>
<Icon className="w-4 h-4" />
{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>
);
})}
@@ -547,6 +576,289 @@ export default function AccountingPage() {
{activeTab === 'ledger' && <LedgerView accounts={accounts} journalEntries={journalEntries} dateFrom={dateFrom} dateTo={dateTo} />}
{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
isOpen={showModal}
onClose={() => { setShowModal(false); setEditingTransaction(null); }}

View File

@@ -5,7 +5,8 @@ import Link from 'next/link';
import {
Battery, ArrowLeft, X, BatteryCharging, Activity, Gauge, MapPin, Bike, User, History,
Calendar, DollarSign, CheckCircle, Clock, ArrowRightLeft, Handshake, TrendingUp, Edit,
RefreshCw, AlertTriangle, Wrench, Plus, Trash2
RefreshCw, AlertTriangle, Wrench, Plus, Trash2, Search, ArrowUpDown, ChevronLeft,
ChevronRight, CheckCircle2, XCircle, AlertCircle
} from 'lucide-react';
interface BMSData {
@@ -97,7 +98,12 @@ interface Battery {
lastMaintenance?: string;
nextMaintenance?: string;
bmsData?: BMSData;
monthlyRent?: number;
deposit?: number;
rentPrice?: number;
investorId?: string;
investorName?: string;
investorSharePercentage?: number;
investedAmount?: number;
ownershipLogs: OwnershipLog[];
damageHistory?: DamageRecord[];
maintenanceHistory?: MaintenanceRecord[];
@@ -126,7 +132,12 @@ const mockBattery: Battery = {
assignedBikerPhone: '01712345678',
currentHubId: 'HUB-001',
currentHubName: 'JAIBEN Head Office',
monthlyRent: 1500,
deposit: 5000,
rentPrice: 150,
investorId: 'INV-001',
investorName: 'Tahmid Rahman',
investorSharePercentage: 60,
investedAmount: 45000,
lastMaintenance: '2024-03-01',
nextMaintenance: '2024-06-01',
bmsData: { voltage: 67.2, current: -2.5, soc: 78, temperature: 32, cycles: 156, health: 95, timestamp: '2024-03-28 12:00:00' },
@@ -549,12 +560,14 @@ export default function BatteryDetailPage({ params }: { params: Promise<{ id: st
<p className="text-xs text-slate-500">Purchase Price</p>
<p className="font-medium text-slate-700">{battery.purchasePrice.toLocaleString()}</p>
</div>
{battery.monthlyRent && (
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Monthly Rent</p>
<p className="font-medium text-green-600">{battery.monthlyRent}/month</p>
</div>
)}
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Deposit</p>
<p className="font-medium text-emerald-600">{(battery.deposit || 5000).toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Daily Rent Price</p>
<p className="font-medium text-blue-600">{(battery.rentPrice || 150).toLocaleString()}/day</p>
</div>
</div>
)}
@@ -646,21 +659,29 @@ export default function BatteryDetailPage({ params }: { params: Promise<{ id: st
<DollarSign className="w-5 h-5 text-green-600" />
<h4 className="font-medium text-slate-700">Rent Information</h4>
</div>
{battery.monthlyRent ? (
<div className="bg-green-50 rounded-lg p-5 border border-green-100">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p className="text-sm text-green-600 mb-1">Monthly Rent Amount</p>
<p className="text-3xl font-bold text-green-700">{battery.monthlyRent}</p>
<p className="text-xs text-green-500 mt-1">per month</p>
</div>
<div>
<p className="text-sm text-green-600 mb-1">Status</p>
<p className="text-lg font-semibold text-green-700">{battery.assignedBikerName ? 'Rented' : 'Not Rented'}</p>
<p className="text-xs text-green-500 mt-1">{battery.assignedBikerName ? `to ${battery.assignedBikerName}` : 'Available for rental'}</p>
{battery.rentPrice ? (
<>
<div className="bg-green-50 rounded-lg p-5 border border-green-100">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p className="text-sm text-blue-600 mb-1">Daily Rent Price</p>
<p className="text-2xl font-bold text-blue-700">{battery.rentPrice.toLocaleString()}</p>
<p className="text-xs text-blue-500 mt-1">per day</p>
</div>
<div>
<p className="text-sm text-emerald-600 mb-1">Battery Deposit</p>
<p className="text-2xl font-bold text-emerald-700">{(battery.deposit || 0).toLocaleString()}</p>
<p className="text-xs text-emerald-500 mt-1">refundable</p>
</div>
<div>
<p className="text-sm text-green-600 mb-1">Status</p>
<p className="text-lg font-semibold text-green-700">{battery.assignedBikerName ? 'Rented' : 'Not Rented'}</p>
<p className="text-xs text-green-500 mt-1">{battery.assignedBikerName ? `to ${battery.assignedBikerName}` : 'Available for rental'}</p>
</div>
</div>
</div>
</div>
<BatteryRentalTab battery={battery} />
</>
) : (
<div className="bg-slate-50 rounded-lg p-5 text-center text-slate-500">
This battery is not set up for rental.
@@ -970,8 +991,49 @@ export default function BatteryDetailPage({ params }: { params: Promise<{ id: st
<p className="font-medium text-blue-900">{battery.currentHubName || battery.currentStationName || 'Not Assigned'}</p>
</div>
<div className="bg-white rounded-lg p-3">
<p className="text-xs text-blue-500 mb-1">Monthly Rent</p>
<p className="font-medium text-green-600">{battery.monthlyRent}/month</p>
<p className="text-xs text-blue-500 mb-1">Daily Rent</p>
<p className="font-medium text-green-600">{(battery.rentPrice || 150)}/day</p>
</div>
</div>
</div>
)}
{battery.investorId && (
<div className="bg-emerald-50 rounded-xl border border-emerald-200 p-5 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
<Handshake className="w-5 h-5 text-emerald-600" />
</div>
<div>
<h3 className="font-semibold text-emerald-800">Investor Co-Ownership Details</h3>
<span className="text-xs text-emerald-600 bg-emerald-100 px-2 py-0.5 rounded-full font-medium">Active Investment</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg p-3 border border-emerald-100/50">
<p className="text-xs text-emerald-600 mb-1">Investor Name</p>
<Link href={`/admin/investors/${battery.investorId}`} className="font-bold text-slate-800 hover:text-accent hover:underline flex items-center gap-1">
{battery.investorName} <TrendingUp className="w-3.5 h-3.5 text-emerald-500" />
</Link>
<p className="text-xs text-slate-400">ID: {battery.investorId}</p>
</div>
<div className="bg-white rounded-lg p-3 border border-emerald-100/50">
<p className="text-xs text-emerald-600 mb-1">Ownership Share (%)</p>
<p className="font-bold text-emerald-700 text-lg">{battery.investorSharePercentage}%</p>
<p className="text-xs text-slate-400">Share of Daily Rent Revenue</p>
</div>
<div className="bg-white rounded-lg p-3 border border-emerald-100/50">
<p className="text-xs text-emerald-600 mb-1">Invested Amount</p>
<p className="font-bold text-slate-800">{(battery.investedAmount || 0).toLocaleString()}</p>
<p className="text-xs text-slate-400">Total Capital Contributed</p>
</div>
<div className="bg-white rounded-lg p-3 border border-emerald-100/50">
<p className="text-xs text-emerald-600 mb-1">Investor Daily Payout</p>
<p className="font-bold text-emerald-600">{(((battery.rentPrice || 150) * (battery.investorSharePercentage || 0)) / 100).toLocaleString()}/day</p>
<p className="text-xs text-slate-400">Calculated from Daily Rent</p>
</div>
</div>
</div>
@@ -1099,11 +1161,20 @@ function EditBatteryModal({
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Monthly Rent ()</label>
<label className="block text-sm font-medium text-slate-700 mb-1">Deposit () *</label>
<input
type="number"
value={formData.monthlyRent || 0}
onChange={(e) => handleChange('monthlyRent', parseInt(e.target.value))}
value={formData.deposit || 0}
onChange={(e) => handleChange('deposit', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Daily Rent Price () *</label>
<input
type="number"
value={formData.rentPrice || 0}
onChange={(e) => handleChange('rentPrice', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
@@ -1131,6 +1202,52 @@ function EditBatteryModal({
/>
</div>
</div>
<div className="border-t border-slate-100 pt-4 mt-4">
<h3 className="text-sm font-bold text-slate-800 mb-3">Investor Assignment (Co-Ownership)</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Investor ID</label>
<input
type="text"
value={formData.investorId || ''}
onChange={(e) => handleChange('investorId', e.target.value)}
placeholder="INV-001"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Investor Name</label>
<input
type="text"
value={formData.investorName || ''}
onChange={(e) => handleChange('investorName', e.target.value)}
placeholder="Tahmid Rahman"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Ownership Share (%)</label>
<input
type="number"
value={formData.investorSharePercentage || 0}
onChange={(e) => handleChange('investorSharePercentage', parseInt(e.target.value))}
placeholder="60"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Invested Amount ()</label>
<input
type="number"
value={formData.investedAmount || 0}
onChange={(e) => handleChange('investedAmount', parseInt(e.target.value))}
placeholder="45000"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
</div>
</div>
</div>
<div className="p-5 border-t border-slate-100 flex gap-3 sticky bottom-0 bg-white">
<button onClick={onClose} className="flex-1 py-2.5 px-4 border border-slate-200 rounded-lg font-semibold text-sm hover:bg-slate-50">
@@ -1147,3 +1264,346 @@ function EditBatteryModal({
</div>
);
}
function BatteryRentalTab({ battery }: { battery: any }) {
// Generate highly realistic rent transaction history
const [transactions] = useState<any[]>(() => {
const list: any[] = [];
const riders = [battery.assignedBikerName || 'Sofiq Rahman', 'Karim Ahmed', 'Sajib Islam', 'Nayeem Chowdhury', 'Rakib Hasan'];
const methods = ['bKash', 'Nagad', 'Rocket', 'Bank Transfer'];
const days = 30;
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) % 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 = battery.rentPrice || 150;
list.push({
id: `TX-BAT-${10400 + 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 (
<div className="space-y-4 mt-6">
<div className="flex items-center gap-2">
<DollarSign className="w-5 h-5 text-green-600" />
<h4 className="font-semibold text-slate-700 text-sm">Rental Transaction History</h4>
</div>
<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>
)}
</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>
);
}

View File

@@ -5,7 +5,7 @@ import Link from 'next/link';
import {
Battery, Plus, Search, Filter, X, Download, Upload, MoreHorizontal,
BatteryCharging, BatteryWarning, BatteryFull, Zap, Thermometer,
Activity, Clock, MapPin, Bike, User, History, TrendingUp, DollarSign,
Activity, Clock, MapPin, Bike, User, History, TrendingUp, DollarSign, Handshake,
Eye, Edit, Trash2, RefreshCw, CheckCircle, AlertTriangle, ChevronRight,
ArrowUpDown, Calendar, Gauge, Timer
} from 'lucide-react';
@@ -58,9 +58,17 @@ interface Battery {
assignedBikerName?: string;
currentStationId?: string;
currentStationName?: string;
hubId?: string;
hubName?: string;
lastMaintenance?: string;
nextMaintenance?: string;
bmsData?: BMSData;
deposit?: number;
rentPrice?: number;
investorId?: string;
investorName?: string;
investorSharePercentage?: number;
investedAmount?: number;
history: BatteryHistory[];
}
@@ -97,6 +105,12 @@ const mockBatteries: Battery[] = [
voltage: 60,
purchaseDate: '2024-01-15',
purchasePrice: 45000,
deposit: 5000,
rentPrice: 150,
investorId: 'INV-001',
investorName: 'Tahmid Rahman',
investorSharePercentage: 60,
investedAmount: 45000,
warrantyExpiry: '2027-01-15',
status: 'in-use',
currentSoc: 78,
@@ -127,6 +141,8 @@ const mockBatteries: Battery[] = [
voltage: 48,
purchaseDate: '2024-02-01',
purchasePrice: 38000,
deposit: 4000,
rentPrice: 120,
warrantyExpiry: '2027-02-01',
status: 'available',
currentSoc: 100,
@@ -148,6 +164,8 @@ const mockBatteries: Battery[] = [
voltage: 60,
purchaseDate: '2024-01-20',
purchasePrice: 42000,
deposit: 4500,
rentPrice: 130,
warrantyExpiry: '2027-01-20',
status: 'maintenance',
currentSoc: 15,
@@ -170,6 +188,8 @@ const mockBatteries: Battery[] = [
voltage: 60,
purchaseDate: '2024-02-15',
purchasePrice: 46000,
deposit: 5000,
rentPrice: 150,
warrantyExpiry: '2027-02-15',
status: 'in-use',
currentSoc: 92,
@@ -198,6 +218,8 @@ const mockBatteries: Battery[] = [
voltage: 60,
purchaseDate: '2024-03-01',
purchasePrice: 50000,
deposit: 5500,
rentPrice: 160,
warrantyExpiry: '2027-03-01',
status: 'charging',
currentSoc: 85,
@@ -219,6 +241,8 @@ const mockBatteries: Battery[] = [
voltage: 48,
purchaseDate: '2023-06-15',
purchasePrice: 28000,
deposit: 3000,
rentPrice: 90,
warrantyExpiry: '2026-06-15',
status: 'retired',
currentSoc: 0,
@@ -254,6 +278,13 @@ const mockStations: SwapStation[] = [
{ id: 'SS-005', name: 'Mirpur Swap Station', location: 'Mirpur 1, Section 2' },
];
const hubs = [
{ id: 'HUB-001', name: 'JAIBEN Head Office' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const statusColors: Record<string, string> = {
available: 'bg-green-100 text-green-700',
'in-use': 'bg-blue-100 text-blue-700',
@@ -591,9 +622,9 @@ export default function BatteriesPage() {
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<button onClick={() => handleViewDetails(battery)} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
<Link href={`/admin/batteries/${battery.id}`} className="p-2 hover:bg-slate-100 rounded-lg inline-flex items-center" title="View Details">
<Eye className="w-4 h-4 text-blue-500" />
</button>
</Link>
<button onClick={() => handleViewHistory(battery)} className="p-2 hover:bg-slate-100 rounded-lg" title="History">
<History className="w-4 h-4 text-purple-500" />
</button>
@@ -666,9 +697,9 @@ export default function BatteriesPage() {
)}
<div className="flex items-center gap-1">
<button onClick={() => handleViewDetails(battery)} className="flex-1 py-1.5 text-xs font-medium text-blue-600 hover:bg-blue-50 rounded-lg">
<div className="flex-1 py-1.5 text-xs font-medium text-blue-600 hover:bg-blue-50 rounded-lg text-center">
Details
</button>
</div>
<button onClick={() => handleViewHistory(battery)} className="flex-1 py-1.5 text-xs font-medium text-purple-600 hover:bg-purple-50 rounded-lg">
History
</button>
@@ -714,27 +745,7 @@ export default function BatteriesPage() {
</div>
)}
{/* Details Modal */}
{showDetailsModal && selectedBattery && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Battery Details</h2>
<button onClick={() => setShowDetailsModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<BatteryDetails
battery={selectedBattery}
bikes={mockBikes}
bikers={mockBikers}
stations={mockStations}
onAssign={handleAssignToBike}
onClose={() => setShowDetailsModal(false)}
/>
</div>
</div>
)}
{/* History Modal */}
{showHistoryModal && selectedBattery && (
@@ -789,12 +800,17 @@ function BatteryForm({ battery, onSave, onCancel }: { battery: Battery | null; o
voltage: 60,
purchaseDate: new Date().toISOString().split('T')[0],
purchasePrice: 0,
transactionMethod: 'cash',
autoJournal: true,
autoJournalSource: 'supplier',
warrantyExpiry: '',
status: 'available',
currentSoc: 100,
health: 100,
cycleCount: 0,
history: [],
hubId: '',
hubName: '',
});
const handleChange = (field: keyof Battery, value: any) => {
@@ -882,6 +898,26 @@ function BatteryForm({ battery, onSave, onCancel }: { battery: Battery | null; o
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>
<label className="block text-sm font-medium text-slate-700 mb-1">Deposit () *</label>
<input
type="number"
value={formData.deposit || 0}
onChange={(e) => handleChange('deposit', parseInt(e.target.value))}
placeholder="5000"
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>
<label className="block text-sm font-medium text-slate-700 mb-1">Daily Rent Price () *</label>
<input
type="number"
value={formData.rentPrice || 0}
onChange={(e) => handleChange('rentPrice', parseInt(e.target.value))}
placeholder="150"
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>
<label className="block text-sm font-medium text-slate-700 mb-1">Transaction Method</label>
<select
@@ -897,33 +933,23 @@ function BatteryForm({ battery, onSave, onCancel }: { battery: Battery | null; o
<option value="other">Other</option>
</select>
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.autoJournal || false}
onChange={(e) => handleChange('autoJournal', e.target.checked)}
className="w-4 h-4 text-accent rounded border-slate-300 focus:ring-accent"
/>
<span className="text-sm text-slate-700">Auto-Journal Entry</span>
</label>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Hub</label>
<select
value={formData.hubId || ''}
onChange={(e) => {
const hub = hubs.find(h => h.id === e.target.value);
handleChange('hubId', e.target.value);
handleChange('hubName', hub?.name || '');
}}
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 Hub...</option>
{hubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
{formData.autoJournal && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Auto-Journal Source</label>
<select
value={formData.autoJournalSource || 'supplier'}
onChange={(e) => handleChange('autoJournalSource', e.target.value as any)}
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="supplier">Supplier Purchase</option>
<option value="import">Import</option>
<option value="internal">Internal Transfer</option>
<option value="transfer">Transfer</option>
<option value="other">Other</option>
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Warranty Expiry</label>
<input
@@ -1040,6 +1066,14 @@ function BatteryDetails({
<p className="text-xs text-slate-500 mb-1">Purchase Price</p>
<p className="font-semibold text-slate-700">{battery.purchasePrice.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Battery Deposit</p>
<p className="font-semibold text-emerald-700">{(battery.deposit || 0).toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Daily Rent Price</p>
<p className="font-semibold text-blue-700">{(battery.rentPrice || 0).toLocaleString()}/day</p>
</div>
</div>
{battery.bmsData && (

View File

@@ -862,7 +862,7 @@ export default function BikerDetailPage() {
<ArrowLeft className="w-4 h-4" /> Back to Bikers
</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="relative group">
{biker.profileImage ? (

View File

@@ -9,7 +9,8 @@ import {
GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet,
MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3,
History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus,
AlertCircle, Image as ImageIcon, Camera
AlertCircle, Image as ImageIcon, Camera, Search, ArrowUpDown, ChevronLeft,
ChevronRight, RefreshCw, CheckCircle2, XCircle
} from 'lucide-react';
interface GPSDevice {
@@ -1048,93 +1049,395 @@ function DocumentsTab({ 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) => {
switch (type) {
case 'single': return `৳${rate}/day`;
case 'shared': return `৳${rate / 2}+${rate / 2} (2 person)`;
case 'rent-to-own': return `৳${rate}/day`;
default: return `৳${rate}`;
const days = 25;
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) % 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 (
<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-3">Rental History</h3>
{history.length === 0 ? (
<div className="text-center py-8">
<History className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<p className="text-sm text-slate-500">No rental history yet.</p>
</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>
{/* Dynamic Rental Metrics - Sleek and Responsive */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<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>
<span className="font-semibold text-green-600">৳350/day</span>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<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>
<p className="text-xs text-slate-500">Total Collected</p>
<p className="text-lg font-bold text-green-600">৳{totalCollected.toLocaleString()}</p>
</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 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>
);

View File

@@ -17,6 +17,7 @@ interface Bike {
plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number;
batteryId?: string;
location?: string; // deprecated - use hubId/hubName
hubId?: string;
hubName?: string;
@@ -56,6 +57,15 @@ const hubs = [
{ 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> = {
available: 'bg-green-100 text-green-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="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' :
selectedMapBike.status === 'rented' ? 'bg-blue-100 text-blue-700' :
selectedMapBike.status === 'maintenance' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-500'
selectedMapBike.status === 'rented' ? 'bg-blue-100 text-blue-700' :
selectedMapBike.status === 'maintenance' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-500'
}`}>
{selectedMapBike.status}
</span>
@@ -182,7 +192,7 @@ export default function FleetPage() {
<div className="flex justify-between">
<span className="text-slate-500">Battery</span>
<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>
</div>
<div className="flex justify-between">
@@ -488,6 +498,7 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
plateNumber: '',
status: 'available',
batteryLevel: 100,
batteryId: '',
location: '', // deprecated
hubId: '',
hubName: '',
@@ -563,14 +574,27 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
</select>
</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))}
<label className="block text-sm font-medium text-slate-700 mb-1">Battery</label>
<select
value={formData.batteryId || ''}
onChange={(e) => {
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"
/>
>
<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>
<label className="block text-sm font-medium text-slate-700 mb-1">Hub *</label>
<select
@@ -588,6 +612,16 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
))}
</select>
</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>
<label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label>
<input
@@ -812,9 +846,9 @@ function FleetMap({ bikes, onSelectBike, selectedBike, large }: { bikes: Bike[];
<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"}
className={`${data.bikes[0].status === 'available' ? 'fill-green-500' :
data.bikes[0].status === 'rented' ? 'fill-blue-500' :
data.bikes[0].status === 'maintenance' ? 'fill-amber-500' :
'fill-slate-400'
data.bikes[0].status === 'rented' ? 'fill-blue-500' :
data.bikes[0].status === 'maintenance' ? 'fill-amber-500' :
'fill-slate-400'
}`}
filter="url(#shadow)"
/>

View File

@@ -4,7 +4,8 @@ import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
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';
interface Hub {
@@ -29,6 +30,17 @@ interface BikeInfo {
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 = {
id: 'HUB-001',
name: 'JAIBEN Head Office',
@@ -52,6 +64,15 @@ const mockHubBikes: BikeInfo[] = [
{ 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 {
id: 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 },
];
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() {
const params = useParams();
const router = useRouter();
@@ -77,10 +162,36 @@ export default function HubDetailPage() {
const [hub, setHub] = useState<Hub>(mockHub);
const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes);
const [batteries, setBatteries] = useState<BatteryInfo[]>(mockHubBatteries);
const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals);
const [editMode, setEditMode] = useState(false);
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 = () => {
setHub(editForm);
@@ -155,26 +266,44 @@ export default function HubDetailPage() {
<button
onClick={() => setActiveTab('overview')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Overview
</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
onClick={() => setActiveTab('bikes')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Bikes ({bikes.length})
</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
onClick={() => setActiveTab('rentals')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Rentals ({rentals.length})
@@ -337,11 +466,199 @@ export default function HubDetailPage() {
</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' && (
<div>
<div className="flex items-center justify-between mb-4">
<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
</button>
</div>
@@ -351,8 +668,8 @@ export default function HubDetailPage() {
<div className="flex items-center justify-between mb-2">
<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' :
bike.status === 'rented' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
bike.status === 'rented' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{bike.status}
</span>
@@ -360,6 +677,73 @@ export default function HubDetailPage() {
<p className="font-medium text-slate-800">{bike.model}</p>
<p className="text-sm text-slate-500">{bike.plate}</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>
@@ -370,9 +754,9 @@ export default function HubDetailPage() {
<div>
<div className="flex items-center justify-between mb-4">
<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
</button>
</button> */}
</div>
<div className="overflow-x-auto">
<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>
</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-blue-100 text-blue-700'
}`}>
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-blue-100 text-blue-700'
}`}>
{rental.status}
</span>
</td>
@@ -433,6 +816,382 @@ export default function HubDetailPage() {
)}
</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>
);
}

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

View File

@@ -7,7 +7,7 @@ import {
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
CheckCircle, XCircle, ChevronLeft, Save, Printer, Send, QrCode,
Wallet, Building, Edit, MessageSquare, Calendar, ArrowLeft, Trash2,
Package, Settings
Package, Settings, History, ArrowRight
} from 'lucide-react';
import Link from 'next/link';
@@ -282,9 +282,14 @@ export default function MaintenanceDetailPage() {
const [showCompleteModal, setShowCompleteModal] = useState(false);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
// AI OCR Simulation States
const [isOcrProcessing, setIsOcrProcessing] = useState(false);
const [ocrComplete, setOcrComplete] = useState(false);
const [ocrFileName, setOcrFileName] = useState('');
const [ocrStep, setOcrStep] = useState('');
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
const [showAddPartModal, setShowAddPartModal] = useState(false);
const [showAddServiceCostModal, setShowAddServiceCostModal] = useState(false);
const [partSearch, setPartSearch] = useState('');
const [invoiceData, setInvoiceData] = useState({ tips: 0, discount: 0 });
const [invoiceCreated, setInvoiceCreated] = useState(false);
@@ -295,14 +300,13 @@ export default function MaintenanceDetailPage() {
const [actualCost, setActualCost] = useState('');
const [selectedPart, setSelectedPart] = useState<EVPart | null>(null);
const [partQuantity, setPartQuantity] = useState(1);
const [serviceCostInput, setServiceCostInput] = useState('');
useEffect(() => {
const found = mockMaintenance.find(r => r.id === id);
if (found) {
setRecord(found);
setEditForm(found);
setActualCost(found.actualCost?.toString() || found.estimatedCost.toString());
setActualCost(found.actualCost?.toString() || '');
}
}, [id]);
@@ -457,12 +461,12 @@ export default function MaintenanceDetailPage() {
<ArrowLeft className="w-4 h-4" /> Back to Maintenance
</button>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-12 lg:mb-0">
<div className="p-6 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-extrabold text-slate-800">{record.id}</h1>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">{record.id}</h1>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${severityColors[record.severity]}`}>
{record.severity}
</span>
@@ -475,77 +479,86 @@ export default function MaintenanceDetailPage() {
</div>
<p className="text-slate-500 mt-1">{typeLabels[record.type]} {record.date}</p>
</div>
<div className="flex gap-2">
{editMode ? (
<>
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
<Save className="w-4 h-4" /> Save
</button>
<button onClick={() => setEditMode(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
Cancel
</button>
</>
) : (
<>
{!invoiceCreated && (
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
<Edit className="w-4 h-4" /> Edit
</button>
)}
{!invoiceCreated && (
<button onClick={() => setShowAddNoteModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
<MessageSquare className="w-4 h-4" /> Note
</button>
)}
{record.status !== 'completed' && !invoiceCreated && (
<button
onClick={() => setShowCompleteModal(true)}
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
>
<FileText className="w-4 h-4" /> Create Invoice
</button>
)}
{record.status !== 'completed' && invoiceCreated && (
<>
<button
onClick={handleGenerateInvoice}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
>
<Printer className="w-4 h-4" /> Print Invoice
</button>
<button
onClick={() => setShowPaymentModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
>
<DollarSign className="w-4 h-4" /> Proceed to Payment
</button>
<button
onClick={() => setShowCompleteModal(true)}
className="px-3 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50"
title="Edit Invoice"
>
<Edit className="w-4 h-4" />
</button>
</>
)}
{record.status === 'completed' && record.paymentStatus !== 'paid' && (
<button
onClick={() => setShowPaymentModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
>
<DollarSign className="w-4 h-4" /> Payment
</button>
)}
{record.paymentStatus === 'paid' && (
<button
onClick={handleGenerateInvoice}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
>
<Printer className="w-4 h-4" /> Print Invoice
</button>
)}
</>
<div className="flex flex-wrap items-center gap-2 relative">
{ocrComplete && (
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 text-xs font-bold rounded-lg mr-2 animate-fadeIn">
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" /> Invoice Locked (OCR Synced)
</div>
)}
<div className={`flex flex-wrap gap-2`}>
{editMode ? (
<>
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
<Save className="w-4 h-4" /> Save
</button>
<button onClick={() => setEditMode(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
Cancel
</button>
</>
) : (
<>
{!invoiceCreated && (
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
<Edit className="w-4 h-4" /> Edit
</button>
)}
{!invoiceCreated && (
<button onClick={() => setShowAddNoteModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
<MessageSquare className="w-4 h-4" /> Note
</button>
)}
<div className={`flex flex-wrap gap-2 filter blur-[1.5px] opacity-50 pointer-events-none`}>
{record.status !== 'completed' && !invoiceCreated && (
<button
onClick={() => setShowCompleteModal(true)}
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
>
<FileText className="w-4 h-4" /> Create Invoice
</button>
)}
{record.status !== 'completed' && invoiceCreated && (
<>
<button
onClick={handleGenerateInvoice}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
>
<Printer className="w-4 h-4" /> Print Invoice
</button>
<button
onClick={() => setShowPaymentModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
>
<DollarSign className="w-4 h-4" /> Proceed to Payment
</button>
<button
onClick={() => setShowCompleteModal(true)}
className="px-3 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50"
title="Edit Invoice"
>
<Edit className="w-4 h-4" />
</button>
</>
)}
{record.status === 'completed' && record.paymentStatus !== 'paid' && (
<button
onClick={() => setShowPaymentModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
>
<DollarSign className="w-4 h-4" /> Payment
</button>
)}
{record.paymentStatus === 'paid' && (
<button
onClick={handleGenerateInvoice}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
>
<Printer className="w-4 h-4" /> Print Invoice
</button>
)}
</div>
</>
)}
</div>
</div>
</div>
</div>
@@ -619,6 +632,47 @@ export default function MaintenanceDetailPage() {
)}
</div>
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100 relative overflow-hidden transition-all duration-300">
<div className="absolute inset-0 bg-slate-900/5 backdrop-blur-[1px] flex items-center justify-center z-10 p-2">
<span className="px-3 py-1 bg-slate-900 text-white border border-slate-700 text-[10px] font-bold rounded-lg uppercase tracking-wider flex items-center gap-1.5 shadow-md">
<CheckCircle className="w-3.5 h-3.5 text-emerald-400" /> Populated via AI OCR
</span>
</div>
<div className={ocrComplete ? 'filter blur-[1.5px] opacity-60 pointer-events-none' : ''}>
<h3 className="font-semibold text-orange-800 mb-3 flex items-center gap-2">
<History className="w-5 h-5" /> Issue History
</h3>
<div className="space-y-2">
{record.batteryId && (
<Link
href={`/admin/maintenance/history/battery/${record.batteryId}?from=${record.id}`}
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
>
<div className="flex items-center gap-2">
<Battery className="w-4 h-4 text-green-600" />
<span className="text-sm text-slate-700">Battery History</span>
</div>
<ArrowRight className="w-4 h-4 text-orange-400" />
</Link>
)}
{record.bikeId && (
<Link
href={`/admin/maintenance/history/bike/${record.bikeId}?from=${record.id}`}
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
>
<div className="flex items-center gap-2">
<Bike className="w-4 h-4 text-purple-600" />
<span className="text-sm text-slate-700">Fleet History</span>
</div>
<ArrowRight className="w-4 h-4 text-orange-400" />
</Link>
)}
</div>
</div>
</div>
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
<User className="w-5 h-5" /> Reporter
@@ -693,174 +747,6 @@ export default function MaintenanceDetailPage() {
</>
)}
</div>
</div>
<div className="space-y-4">
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Cost Details
</h3>
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 p-4 rounded-xl border border-purple-100">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-purple-800 flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Cost Breakdown
</h3>
{editMode && (
<input
type="number"
value={editForm.estimatedCost || 0}
onChange={(e) => setEditForm({ ...editForm, estimatedCost: parseInt(e.target.value) })}
className="px-2 py-1 border border-purple-200 rounded text-sm w-24"
/>
)}
</div>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Estimated Cost:</span>
<span className="font-medium text-purple-700">{record.estimatedCost.toLocaleString()}</span>
</div>
<div className="border-t border-purple-100 pt-2">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600 flex items-center gap-1">
<Package className="w-4 h-4" /> Parts Total:
</span>
<span className="font-medium text-orange-600">{record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString() || 0}</span>
</div>
<div className="flex justify-between items-center text-sm mt-1">
<span className="text-slate-600 flex items-center gap-1">
<Wrench className="w-4 h-4" /> Service Cost (Labor):
</span>
<span className="font-medium text-blue-600">{(record.serviceCost || 0).toLocaleString()}</span>
</div>
</div>
<div className="border-t-2 border-purple-200 pt-2 mt-2">
<div className="flex justify-between items-center">
<span className="font-semibold text-purple-800">Actual Total Cost:</span>
<span className="text-xl font-bold text-purple-800">
{((record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0) || 0) + (record.serviceCost || 0)).toLocaleString()}
</span>
</div>
</div>
</div>
</div>
</div>
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
<User className="w-5 h-5" /> Assigned To
</h3>
{editMode ? (
<select
value={editForm.assignedTo || ''}
onChange={(e) => setEditForm({ ...editForm, assignedTo: e.target.value })}
className="w-full px-3 py-2 border border-cyan-200 rounded-lg text-sm"
>
<option value="">Select Service Center</option>
<option value="Service Center A">Service Center A</option>
<option value="Service Center B">Service Center B</option>
<option value="Authorized Service Center">Authorized Service Center</option>
<option value="Gulshan Hub">Gulshan Hub</option>
<option value="Banani Hub">Banani Hub</option>
<option value="Dhanmondi Hub">Dhanmondi Hub</option>
</select>
) : (
<p className="text-sm text-cyan-700">{record.assignedTo || 'Not assigned'}</p>
)}
</div>
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
<Wrench className="w-5 h-5" /> Parts Used
</h3>
{!editMode && (
<button
onClick={() => setShowAddPartModal(true)}
className="px-3 py-1 bg-orange-600 text-white text-xs rounded-lg hover:bg-orange-700 flex items-center gap-1"
>
<Plus className="w-3 h-3" /> Add Part
</button>
)}
</div>
<div className="space-y-2">
{(record.partsUsed || []).length > 0 ? (
<table className="w-full text-sm">
<thead className="bg-orange-100">
<tr>
<th className="px-3 py-2 text-left text-orange-800 font-medium">Part</th>
<th className="px-3 py-2 text-center text-orange-800 font-medium">Qty</th>
<th className="px-3 py-2 text-right text-orange-800 font-medium">Unit Price</th>
<th className="px-3 py-2 text-right text-orange-800 font-medium">Total</th>
<th className="px-3 py-2 text-center text-orange-800 font-medium">Action</th>
</tr>
</thead>
<tbody>
{record.partsUsed?.map((part) => (
<tr key={part.id} className="bg-white border-b border-orange-100">
<td className="px-3 py-2 text-slate-700">{part.partName}</td>
<td className="px-3 py-2 text-center">
<input
type="number"
min="1"
value={part.quantity}
onChange={(e) => {
const newQty = Math.max(1, parseInt(e.target.value) || 1);
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.map(p =>
p.id === part.id
? { ...p, quantity: newQty, totalPrice: newQty * p.unitPrice }
: p
)
} : null);
}}
className="w-16 px-2 py-1 border border-orange-200 rounded text-center text-sm"
/>
</td>
<td className="px-3 py-2 text-right text-slate-600">{part.unitPrice.toLocaleString()}</td>
<td className="px-3 py-2 text-right font-medium text-orange-700">{part.totalPrice.toLocaleString()}</td>
<td className="px-3 py-2 text-center">
<button
onClick={() => {
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.filter(p => p.id !== part.id)
} : null);
}}
className="text-red-400 hover:text-red-600 p-1"
>
<Trash2 className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
<tfoot className="bg-orange-50">
<tr>
<td colSpan={3} className="px-3 py-2 text-right font-semibold text-orange-800">Parts Total:</td>
<td className="px-3 py-2 text-right font-bold text-orange-700">
{record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString()}
</td>
<td></td>
</tr>
</tfoot>
</table>
) : (
<p className="text-sm text-orange-400">No parts added</p>
)}
</div>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<MessageSquare className="w-5 h-5" /> Notes ({(editMode ? editForm.notes : record.notes)?.length})
@@ -907,6 +793,348 @@ export default function MaintenanceDetailPage() {
)}
</div>
</div>
</div>
<div className="space-y-4">
{/* Manual Invoice OCR Card */}
<div className="bg-indigo-50/70 p-4 rounded-xl border border-indigo-100 space-y-3 relative overflow-hidden transition-all duration-300">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-indigo-800 text-sm flex items-center gap-2">
<FileText className="w-5 h-5 text-indigo-600" /> {ocrComplete ? 'Manual Invoice OCR Upload' : 'Upload Manual Invoice'}
</h3>
{ocrComplete && (
<span className="px-2.5 py-0.5 bg-emerald-100 border border-emerald-200 text-emerald-800 text-[10px] font-bold rounded-full uppercase tracking-wider animate-pulse">
OCR Synced
</span>
)}
</div>
{!isOcrProcessing && !ocrComplete && (
<div className="border border-dashed border-indigo-300 hover:border-indigo-500 rounded-xl p-5 text-center cursor-pointer transition-all bg-white relative group hover:bg-indigo-50/50">
<input
type="file"
accept="image/*,.pdf"
className="absolute inset-0 opacity-0 cursor-pointer z-10"
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) return;
setOcrFileName(file.name);
setIsOcrProcessing(true);
setOcrStep('1/3: Parsing document layout & digital signature...');
setTimeout(() => {
setOcrStep('2/3: Running AI layout analysis & line item extraction...');
setTimeout(() => {
setOcrStep('3/3: Auto-populating ledger items and actual costs...');
setTimeout(() => {
setIsOcrProcessing(false);
setOcrComplete(true);
// Populate parts dynamically with mock OCR data!
setRecord(prev => {
if (!prev) return null;
return {
...prev,
partsUsed: [
{ id: 'PU-OCR-1', partId: 'PRT-001', partName: 'Front fender (OCR Extracted)', quantity: 1, unitPrice: 1500, totalPrice: 1500, addedAt: new Date().toISOString().split('T')[0] },
{ id: 'PU-OCR-2', partId: 'PRT-003', partName: 'Mounting brackets (OCR Extracted)', quantity: 2, unitPrice: 800, totalPrice: 1600, addedAt: new Date().toISOString().split('T')[0] },
{ id: 'PU-OCR-3', partId: 'PRT-004', partName: 'Brake pads (OCR Extracted)', quantity: 2, unitPrice: 600, totalPrice: 1200, addedAt: new Date().toISOString().split('T')[0] },
],
serviceCost: 1800,
actualCost: 6100, // 1500 + 1600 + 1200 + 1800
notes: [...prev.notes, `OCR Scan Success: Extracted items from manual invoice "${file.name}".`]
};
});
}, 1000);
}, 1200);
}, 1000);
}}
/>
<div className="flex flex-col items-center justify-center space-y-2">
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-600 group-hover:scale-110 transition-transform">
<Plus className="w-5 h-5" />
</div>
<div>
<p className="text-sm font-semibold text-slate-700">Upload manual invoice receipt</p>
<p className="text-xs text-slate-500 mt-0.5">No file chosen</p>
<p className="text-[10px] text-slate-400 mt-2 font-medium">Supports PDF, PNG, JPG (e.g. MNT-001-invoice.pdf)</p>
</div>
</div>
</div>
)}
{isOcrProcessing && (
<div className="p-4 bg-white border border-indigo-100 rounded-xl space-y-3">
<div className="flex justify-between text-xs">
<span className="text-indigo-800 font-semibold animate-pulse">Running JAIBEN AI OCR Engine...</span>
<span className="text-indigo-600 font-bold">{ocrStep.split(':')[0]}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-1.5 overflow-hidden border border-slate-200">
<div className="bg-gradient-to-r from-indigo-500 to-indigo-700 h-full w-2/3 animate-pulse rounded-full"></div>
</div>
<p className="text-[10px] text-slate-500 italic">{ocrStep}</p>
</div>
)}
{ocrComplete && (
<div className="p-4 bg-emerald-50 border border-emerald-100 rounded-xl space-y-2">
<div className="flex items-center gap-2 text-emerald-800 text-xs font-bold">
<CheckCircle className="w-4 h-4 text-emerald-600" />
<span>OCR PARSING COMPLETE</span>
</div>
<div className="text-xs text-slate-600 space-y-1.5 bg-white p-3 rounded-lg border border-emerald-100">
<p>📄 Document: <strong className="text-slate-800 font-semibold">{ocrFileName}</strong></p>
<p>🔧 Extracted Parts: <strong className="text-slate-800 font-semibold">3 items</strong></p>
<p> Labor Service Costs: <strong className="text-slate-800 font-semibold">1,800</strong></p>
<p>💰 Auto Total Synced: <strong className="text-slate-800 font-semibold">6,100</strong></p>
</div>
<button
onClick={() => {
setOcrComplete(false);
setOcrFileName('');
const found = mockMaintenance.find(r => r.id === id);
if (found) {
setRecord(found);
}
}}
className="w-full py-1.5 mt-2 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg transition-colors border border-slate-200 cursor-pointer"
>
Reset OCR Invoice Attachment
</button>
</div>
)}
</div>
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100 relative overflow-hidden transition-all duration-300">
{/* {ocrComplete && ( */}
<div className="absolute inset-0 bg-slate-900/5 backdrop-blur-[1px] flex items-center justify-center z-10 p-2">
<span className="px-3 py-1 bg-slate-900 text-white border border-slate-700 text-[10px] font-bold rounded-lg uppercase tracking-wider flex items-center gap-1.5 shadow-md">
<CheckCircle className="w-3.5 h-3.5 text-emerald-400" /> Populated via AI OCR
</span>
</div>
{/* )} */}
<div className={ocrComplete ? 'filter blur-[1.5px] opacity-60 pointer-events-none' : ''}>
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Cost Details
</h3>
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 p-4 rounded-xl border border-purple-100">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-purple-800 flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Cost Breakdown
</h3>
{editMode && (
<input
type="number"
value={editForm.estimatedCost || 0}
onChange={(e) => setEditForm({ ...editForm, estimatedCost: parseInt(e.target.value) })}
className="px-2 py-1 border border-purple-200 rounded text-sm w-24"
/>
)}
</div>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Estimated Cost:</span>
<span className="font-medium text-purple-700">{record.estimatedCost.toLocaleString()}</span>
</div>
<div className="border-t border-purple-100 pt-2">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600 flex items-center gap-1">
<Package className="w-4 h-4" /> Parts Total:
</span>
<span className="font-medium text-orange-600">{record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString() || 0}</span>
</div>
<div className="flex justify-between items-center text-sm mt-1">
<span className="text-slate-600 flex items-center gap-1">
<Wrench className="w-4 h-4" /> Service Cost (Labor):
</span>
<span className="font-medium text-blue-600">{(record.serviceCost || 0).toLocaleString()}</span>
</div>
</div>
<div className="border-t-2 border-purple-200 pt-2 mt-2">
<div className="flex justify-between items-center">
<span className="font-semibold text-purple-800">Actual Total Cost:</span>
<span className="text-xl font-bold text-purple-800">
{((record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0) || 0) + (record.serviceCost || 0)).toLocaleString()}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
<User className="w-5 h-5" /> Assigned To
</h3>
{editMode ? (
<select
value={editForm.assignedTo || ''}
onChange={(e) => setEditForm({ ...editForm, assignedTo: e.target.value })}
className="w-full px-3 py-2 border border-cyan-200 rounded-lg text-sm"
>
<option value="">Select Service Center</option>
<option value="Service Center A">Service Center A</option>
<option value="Service Center B">Service Center B</option>
<option value="Authorized Service Center">Authorized Service Center</option>
<option value="Gulshan Hub">Gulshan Hub</option>
<option value="Banani Hub">Banani Hub</option>
<option value="Dhanmondi Hub">Dhanmondi Hub</option>
</select>
) : (
<p className="text-sm text-cyan-700">{record.assignedTo || 'Not assigned'}</p>
)}
</div>
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100 relative overflow-hidden transition-all duration-300">
{/* {ocrComplete && ( */}
<div className="absolute inset-0 bg-slate-900/5 backdrop-blur-[1px] flex items-center justify-center z-10 p-2">
<span className="px-3 py-1 bg-slate-900 text-white border border-slate-700 text-[10px] font-bold rounded-lg uppercase tracking-wider flex items-center gap-1.5 shadow-md">
<CheckCircle className="w-3.5 h-3.5 text-emerald-400" /> Populated via AI OCR
</span>
</div>
{/* )} */}
<div className={ocrComplete ? 'filter blur-[1.5px] opacity-60 pointer-events-none' : ''}>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
<Wrench className="w-5 h-5" /> Parts Used
</h3>
{!editMode && (
<button
onClick={() => setShowAddPartModal(true)}
className="px-3 py-1 bg-orange-600 text-white text-xs rounded-lg hover:bg-orange-700 flex items-center gap-1"
>
<Plus className="w-3 h-3" /> Add Part
</button>
)}
</div>
<div className="space-y-3">
{record.partsUsed && record.partsUsed.length > 0 ? (
<>
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-orange-100">
<tr>
<th className="px-3 py-2 text-left text-orange-800 font-medium">Part</th>
<th className="px-3 py-2 text-center text-orange-800 font-medium">Qty</th>
<th className="px-3 py-2 text-right text-orange-800 font-medium">Unit Price</th>
<th className="px-3 py-2 text-right text-orange-800 font-medium">Total</th>
<th className="px-3 py-2 text-center text-orange-800 font-medium">Action</th>
</tr>
</thead>
<tbody>
{record.partsUsed?.map((part) => (
<tr key={part.id} className="bg-white border-b border-orange-100">
<td className="px-3 py-2 text-slate-700">{part.partName}</td>
<td className="px-3 py-2 text-center">
<input
type="number"
min="1"
value={part.quantity}
onChange={(e) => {
const newQty = Math.max(1, parseInt(e.target.value) || 1);
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.map(p =>
p.id === part.id
? { ...p, quantity: newQty, totalPrice: newQty * p.unitPrice }
: p
)
} : null);
}}
className="w-16 px-2 py-1 border border-orange-200 rounded text-center text-sm"
/>
</td>
<td className="px-3 py-2 text-right text-slate-600">{part.unitPrice.toLocaleString()}</td>
<td className="px-3 py-2 text-right font-medium text-orange-700">{part.totalPrice.toLocaleString()}</td>
<td className="px-3 py-2 text-center">
<button
onClick={() => {
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.filter(p => p.id !== part.id)
} : null);
}}
className="text-red-400 hover:text-red-600 p-1"
>
<Trash2 className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-3">
{record.partsUsed.map((part) => (
<div key={part.id} className="bg-white rounded-lg border border-orange-200 p-3">
<div className="flex items-start justify-between mb-2">
<span className="font-medium text-slate-800">{part.partName}</span>
<button
onClick={() => {
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.filter(p => p.id !== part.id)
} : null);
}}
className="text-red-400 hover:text-red-600 p-1"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="flex items-center justify-between text-sm mb-2">
<div className="flex items-center gap-2">
<span className="text-slate-500">Qty:</span>
<input
type="number"
min="1"
value={part.quantity}
onChange={(e) => {
const newQty = Math.max(1, parseInt(e.target.value) || 1);
setRecord(prev => prev ? {
...prev,
partsUsed: prev.partsUsed?.map(p =>
p.id === part.id
? { ...p, quantity: newQty, totalPrice: newQty * p.unitPrice }
: p
)
} : null);
}}
className="w-16 px-2 py-1 border border-orange-200 rounded text-center text-sm"
/>
</div>
<span className="text-slate-600">{part.unitPrice.toLocaleString()}/unit</span>
</div>
<div className="pt-2 border-t border-orange-100 flex justify-between">
<span className="text-xs text-slate-500">Total</span>
<span className="font-bold text-orange-700">{part.totalPrice.toLocaleString()}</span>
</div>
</div>
))}
</div>
</>
) : (
<p className="text-sm text-orange-400">No parts added</p>
)}
{record.partsUsed && record.partsUsed.length > 0 && (
<div className="bg-orange-50 rounded-lg p-3 flex justify-between items-center">
<span className="font-semibold text-orange-800">Parts Total:</span>
<span className="text-lg font-bold text-orange-700">
{record.partsUsed.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString()}
</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -922,17 +1150,22 @@ export default function MaintenanceDetailPage() {
</div>
<div className="p-4 space-y-4">
<div className="bg-slate-50 p-3 rounded-lg">
<div className="flex justify-between text-sm mb-2">
<span className="text-slate-500">Estimated Cost:</span>
<span className="font-medium text-slate-600">{record.estimatedCost.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm mb-2">
<span className="text-slate-500">Parts Total:</span>
<span className="font-medium text-orange-600">{record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0).toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm mb-2">
<span className="text-slate-500">Service Cost (Labor):</span>
<span className="font-medium text-blue-600">{(record.serviceCost || 0).toLocaleString()}</span>
<div>
<label className="text-xs font-medium text-blue-600 mb-1 block">Service Cost (Labor)</label>
<div className="flex gap-2">
<input
type="number"
min="0"
value={record.serviceCost || ''}
onChange={(e) => setRecord(prev => prev ? { ...prev, serviceCost: parseInt(e.target.value) || 0 } : null)}
className="flex-1 px-3 py-2 border border-blue-200 rounded-lg text-sm"
placeholder="Enter labor cost"
/>
</div>
</div>
</div>
@@ -1256,59 +1489,6 @@ export default function MaintenanceDetailPage() {
</div>
)}
<div className="fixed bottom-4 right-4 z-40">
<button
onClick={() => setShowAddServiceCostModal(true)}
className="px-4 py-3 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 flex items-center gap-2"
>
<DollarSign className="w-5 h-5" /> Add Service Cost
</button>
</div>
{showAddServiceCostModal && (
<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-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<Wrench className="w-5 h-5 text-blue-600" /> Add Service Cost
</h3>
<button onClick={() => setShowAddServiceCostModal(false)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Service Cost (Labor charge)</label>
<input
type="number"
value={serviceCostInput}
onChange={(e) => setServiceCostInput(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-lg"
placeholder="Enter service cost"
/>
</div>
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-600">Current Service Cost: <span className="font-bold text-blue-600">{record?.serviceCost || 0}</span></p>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setShowAddServiceCostModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
<button
onClick={() => {
const cost = parseFloat(serviceCostInput) || 0;
setRecord(prev => prev ? { ...prev, serviceCost: cost } : null);
setShowAddServiceCostModal(false);
setServiceCostInput('');
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg flex items-center gap-2"
>
<Plus className="w-4 h-4" /> Add Cost
</button>
</div>
</div>
</div>
)}
{showPaymentSuccess && (
<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">

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';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import {
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,
MessageSquare, Filter, Calendar, Save, Printer, Send
MessageSquare, Filter, Calendar, Save, Printer, Send, Activity
} 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';
@@ -245,7 +246,9 @@ const typeIcons: Record<string, any> = {
};
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 [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
@@ -259,20 +262,45 @@ export default function MaintenancePage() {
const [expandedNotes, setExpandedNotes] = useState<string[]>([]);
const [newNoteText, setNewNoteText] = useState('');
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
const [reportType, setReportType] = useState<'damage' | 'maintenance'>('damage');
const [showSuccessModal, setShowSuccessModal] = useState(false);
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 ||
r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.id.toLowerCase().includes(searchQuery.toLowerCase());
r.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
(r.batteryId && r.batteryId.toLowerCase().includes(searchQuery.toLowerCase()));
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 = {
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,
inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length,
completed: records.filter(r => r.status === 'completed').length,
@@ -397,7 +425,7 @@ export default function MaintenancePage() {
};
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>
<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 className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-red-100">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{stats.critical}</p>
<p className="text-sm text-slate-500">Critical</p>
<div className="flex-1">
<div className="flex items-baseline justify-between gap-2">
<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 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="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
<Wrench className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{stats.inProgress}</p>
<p className="text-sm text-slate-500">In Progress</p>
<div className="flex-1">
<div className="flex items-baseline justify-between gap-2">
<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 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="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-green-600" />
</div>
<div>
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p>
<div className="flex-1">
<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>
</div>
</div>
@@ -458,7 +495,7 @@ export default function MaintenancePage() {
</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="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center">
<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>
</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="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => setActiveTab('all')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${activeTab === 'all' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
>
All
</button>
<button
onClick={() => setActiveTab('damage')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'damage' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
>
<AlertTriangle className="w-4 h-4" /> Damage
</button>
<button
onClick={() => setActiveTab('repair')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'repair' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
>
<Wrench className="w-4 h-4" /> Repair
</button>
<button
onClick={() => setActiveTab('service')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'service' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
>
<Wrench className="w-4 h-4" /> Service
</button>
<button
onClick={() => setActiveTab('battery_swap')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'battery_swap' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
>
<Battery className="w-4 h-4" /> Battery
</button>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg">
<button
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'}`}
>
<AlertTriangle className="w-4 h-4" /> Damage
</button>
<button
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'}`}
>
<Wrench className="w-4 h-4" /> Maintenance
</button>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setTargetType('all')}
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'}`}
>
All
</button>
<button
onClick={() => setTargetType('battery')}
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'}`}
>
<Battery className="w-4 h-4" /> Battery
</button>
<button
onClick={() => setTargetType('fleet')}
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 className="flex-1">
<div className="relative">
@@ -537,47 +676,59 @@ export default function MaintenancePage() {
{filteredRecords.map(record => {
const TypeIcon = typeIcons[record.type];
return (
<Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-5 hover:bg-slate-50 transition-colors">
<div className="flex flex-col lg:flex-row lg:items-start gap-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center">
<TypeIcon className="w-6 h-6 text-slate-600" />
<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-3 lg:gap-4">
<div className="flex items-start gap-3">
<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-5 h-5 lg:w-6 lg:h-6 text-slate-600" />
</div>
<div>
<div className="flex items-center gap-2">
<p className="font-semibold text-slate-800">{record.id}</p>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${severityColors[record.severity]}`}>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-1.5 lg:gap-2">
<p className="font-semibold text-slate-800 text-sm lg:text-base">{record.id}</p>
{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}
</span>
</div>
<p className="text-sm text-slate-500 flex items-center gap-2">
<Bike className="w-3 h-3" /> {record.bikeModel} ({record.bikePlate})
<span className="text-slate-300">|</span>
<User className="w-3 h-3" /> {record.reporterName}
<p className="text-xs lg:text-sm text-slate-500 flex flex-wrap items-center gap-x-1 lg:gap-x-2">
<span className="flex items-center gap-1"><Bike className="w-3 h-3" /> {record.bikeModel}</span>
<span className="hidden sm:inline text-slate-300">|</span>
<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>
</div>
</div>
<div className="flex-1">
<p className="text-sm text-slate-700">{record.description}</p>
<div className="flex flex-wrap gap-4 text-sm text-slate-500 mt-1">
<div className="flex-1 min-w-0">
<p className="text-sm text-slate-700 line-clamp-2">{record.description}</p>
<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">
<Clock className="w-3 h-3" /> {record.date}
</p>
<p className="flex items-center gap-1">
<MapPin className="w-3 h-3" /> {record.location}
<p className="flex items-center gap-1 truncate max-w-[100px] lg:max-w-none">
<MapPin className="w-3 h-3 flex-shrink-0" /> <span className="truncate">{record.location}</span>
</p>
{record.images.length > 0 && (
<p className="flex items-center gap-1 text-blue-600">
<Image className="w-3 h-3" /> {record.images.length} photos
<ImageIcon className="w-3 h-3" /> {record.images.length}
</p>
)}
{record.notes.length > 0 && (
<button
onClick={() => toggleNotes(record.id)}
onClick={(e) => { e.preventDefault(); toggleNotes(record.id); }}
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" />}
</button>
)}
@@ -592,24 +743,24 @@ export default function MaintenancePage() {
)}
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<div className="flex items-center justify-between lg:justify-end gap-2 lg:gap-3 mt-2 lg:mt-0">
<div className="text-left lg:text-right">
<p className="text-sm font-medium text-slate-700">{record.actualCost || record.estimatedCost}</p>
<p className="text-xs text-slate-500">{record.paymentStatus === 'paid' ? 'Paid' : record.paymentStatus === 'approved' ? 'Approved' : 'Payment ' + record.paymentStatus}</p>
</div>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[record.status]}`}>
{record.status === 'reported' && <Clock className="w-3 h-3" />}
{record.status === 'in_progress' && <Wrench className="w-3 h-3" />}
{record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />}
{record.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{record.status === 'cancelled' && <XCircle className="w-3 h-3" />}
{record.status.replace('_', ' ')}
</span>
<div className="flex items-center gap-1.5 lg:gap-2">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${statusColors[record.status]}`}>
{record.status === 'reported' && <Clock className="w-3 h-3" />}
{record.status === 'in_progress' && <Wrench className="w-3 h-3" />}
{record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />}
{record.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{record.status === 'cancelled' && <XCircle className="w-3 h-3" />}
<span className="hidden sm:inline">{record.status.replace('_', ' ')}</span>
</span>
<div className="flex gap-1">
<button
onClick={(e) => { e.preventDefault(); setSelectedRecord(record); setShowDetailsModal(true); }}
className="p-2 hover:bg-slate-100 rounded-lg text-slate-500"
onClick={(e) => { e.preventDefault(); router.push(`/admin/maintenance/${record.id}`); }}
className="p-1.5 lg:p-2 hover:bg-slate-100 rounded-lg text-slate-500"
title="View Details"
>
<Eye className="w-4 h-4" />
@@ -714,7 +865,7 @@ export default function MaintenancePage() {
<div className="grid grid-cols-4 gap-2">
{selectedRecord.images.map((img) => (
<div key={img.id} className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
<Image className="w-8 h-8 text-slate-400" />
<ImageIcon className="w-8 h-8 text-slate-400" />
<span className="text-xs text-slate-500 mt-1">{img.name}</span>
</div>
))}
@@ -794,53 +945,146 @@ export default function MaintenancePage() {
</button>
</div>
<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>
<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">
<option value="damage">Damage</option>
<option value="repair">Repair</option>
<option value="service">Service</option>
<option value="battery_swap">Battery Swap</option>
<option value="inspection">Inspection</option>
<option value="other">Other</option>
<option value="">Select Category</option>
{reportType === 'damage' ? (
<>
<option value="damage">Damage</option>
<option value="repair">Repair</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>
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="critical">Critical</option>
<option value="major">Major</option>
<option value="minor">Minor</option>
<option value="cosmetic">Cosmetic</option>
</select>
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Bike ID *</label>
<input type="text" placeholder="EV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
{reportType === 'damage' ? (
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="critical">Critical</option>
<option value="major">Major</option>
<option value="minor">Minor</option>
<option value="cosmetic">Cosmetic</option>
</select>
</div>
) : (
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">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>
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID</label>
<input type="text" placeholder="BAT-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<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>
<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>
<label className="text-xs font-medium text-slate-600 mb-1 block">Location *</label>
<input type="text" placeholder="Where did the issue occur?" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Estimated Cost</label>
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Location / Hub *</label>
<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">
{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>
<label className="text-xs font-medium text-slate-600 mb-1 block">Upload Images</label>
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center">
<Image className="w-8 h-8 text-slate-300 mx-auto mb-2" />
<p className="text-sm text-slate-500">Drag and drop or click to upload</p>
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-accent cursor-pointer">
<ImageIcon className="w-8 h-8 text-slate-300 mx-auto mb-2" />
<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>
@@ -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">
Cancel
</button>
<button onClick={() => { setShowNewModal(false); alert('Issue reported successfully!'); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark">
<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
</button>
</div>
@@ -953,6 +1197,24 @@ export default function MaintenancePage() {
</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>
);
}

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 {
ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle,
Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin,
Phone, MessageCircle, Play, Check, X, FileText, Download, Battery
Phone, MessageCircle, Play, Check, X, FileText, Download, Battery, Printer
} from 'lucide-react';
import {
canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit,
@@ -86,9 +86,35 @@ interface BatteryRentalHistory {
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 {
id: string;
action: 'locked' | 'unlocked';
@@ -162,8 +188,8 @@ const mockRentals: Rental[] = [
{ 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, status: 'active' },
{ id: 'BAT-RENT-002', batteryId: 'BAT-DH-002', batteryName: 'Titan 72V 50Ah', assignedAt: '2024-02-20', returnedAt: '2024-03-15', monthlyRent: 1800, status: 'returned' },
{ 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 },
],
},
{
@@ -354,6 +380,21 @@ export default function RentalDetailPage() {
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 [rejectPermission, setRejectPermission] = useState(false);
@@ -511,15 +552,60 @@ export default function RentalDetailPage() {
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: `BAT-RENT-${Date.now()}`,
id: newBatHistId,
batteryId: battery.id,
batteryName: `${battery.brand} ${battery.model}`,
assignedAt: new Date().toISOString().split('T')[0],
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,
@@ -527,8 +613,12 @@ export default function RentalDetailPage() {
batteryRent: (prev.batteryRent || 0) + battery.monthlyRent,
batteryHistory: [...(prev.batteryHistory || []), newBatteryHistory],
} : null);
setShowBatteryInvoicePreview(newInvoice);
setShowAddBatteryModal(false);
setSelectedBatteryId('');
setBatteryDeposit(0);
setBatteryDepositMethod('cash');
};
const handleAddNote = () => {
@@ -559,6 +649,25 @@ export default function RentalDetailPage() {
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 typeBadge = getTypeBadge(rental.type);
const paymentBadge = getPaymentStatusBadge(rental.paymentStatus);
@@ -579,6 +688,47 @@ export default function RentalDetailPage() {
return (
<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">
<ArrowLeft className="w-4 h-4" /> Back to Rentals
</Link>
@@ -798,9 +948,9 @@ export default function RentalDetailPage() {
</div>
{/* Battery Rental History */}
<div className="bg-white p-4 rounded-xl border border-slate-200">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-slate-700 flex items-center gap-2">
<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">
@@ -812,102 +962,189 @@ export default function RentalDetailPage() {
<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">Battery Name</th>
<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 text-sm text-slate-700 font-mono">{bat.batteryId}</td>
<td className="px-3 py-2 text-sm text-slate-700">{bat.batteryName}</td>
<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 || '-'}</td>
<td className="px-3 py-2 text-sm font-medium text-green-600">{bat.monthlyRent}</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>
) : (
<p className="text-sm text-slate-500">No battery assigned yet.</p>
<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="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<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-medium">Active Battery Rent: </span>
{rental.batteryHistory.filter(b => b.status === 'active').reduce((sum, b) => sum + b.monthlyRent, 0)}/month
<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 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Add Battery to Rental</h3>
<button onClick={() => setShowAddBatteryModal(false)} className="text-slate-400 hover:text-slate-600">
<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-4 space-y-4">
<div className="p-5 space-y-4">
{/* Battery Select */}
<div>
<label className="text-sm text-slate-600 mb-1 block">Select Battery</label>
<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="">Select Battery...</option>
<option value="">Choose a battery...</option>
{mockBatteries.map(bat => (
<option key={bat.id} value={bat.id}>{bat.brand} {bat.model} - SOC: {bat.soc}% - Rent: {bat.monthlyRent}/month</option>
<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 batteryMonthlyRent = mockBatteries.find(b => b.id === selectedBatteryId)?.monthlyRent || 0;
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-3 bg-amber-50 border border-amber-200 rounded-lg space-y-2">
<p className="text-sm text-amber-700">
Battery Monthly Rent: <span className="font-bold">{batteryMonthlyRent}/month</span>
</p>
<div className="text-xs text-amber-600 pt-2 border-t border-amber-100">
<p className="font-medium mb-1">Calculated Rate by Subscription:</p>
<p> Daily: {Math.round(batteryMonthlyRent / 30)}/day ({batteryMonthlyRent}/30)</p>
<p> Weekly: {Math.round(batteryMonthlyRent / 4)}/week ({batteryMonthlyRent}/4)</p>
<p> Monthly (30 days): {batteryMonthlyRent}/month</p>
<p> Monthly (31 days): {batteryMonthlyRent}/month</p>
<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="pt-2 mt-2 border-t border-amber-100">
<p className="text-xs font-medium text-amber-700">Your Current Subscription: {rental.subscriptionType}</p>
<p className="text-sm font-bold text-amber-800">
You will pay: {rental.subscriptionType === 'daily'
? Math.round(batteryMonthlyRent / 30)
: rental.subscriptionType === 'weekly'
? Math.round(batteryMonthlyRent / 4)
: batteryMonthlyRent}/{rental.subscriptionType}
</p>
<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)} className="flex-1 py-2 px-4 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
<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 px-4 bg-amber-600 text-white rounded-lg text-sm hover:bg-amber-700 disabled:opacity-50">
Add Battery
<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>
@@ -915,6 +1152,243 @@ export default function RentalDetailPage() {
</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 */}
{/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */}
{rental.initialImages && (
@@ -1123,38 +1597,127 @@ export default function RentalDetailPage() {
</div>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-700 flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-500" /> Rental Documents
<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-blue-50">
<h3 className="font-semibold text-blue-900 flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-500" /> Rental Documents & Invoices
</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">
<Upload className="w-4 h-4" /> Upload Document
<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-3 h-3" /> Upload
</button>
</div>
<div className="space-y-2">
{mockDocuments.map(doc => (
<div key={doc.id} className="p-3 bg-slate-50 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-700">{doc.name}</p>
<p className="text-xs text-slate-500">{doc.uploadedAt}</p>
{/* EV Rental Invoices */}
<div className="px-4 pt-4 pb-2">
<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>
<div className="space-y-1.5">
{invoices.filter(inv => inv.type === 'ev_rental').length === 0 && (
<p className="text-xs text-slate-400 italic py-1">No EV rental invoices.</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>
<button onClick={() => {
const link = document.createElement('a');
link.href = '#';
link.download = doc.name;
alert(`Downloading: ${doc.name}`);
}} className="px-3 py-1.5 text-blue-600 hover:bg-blue-50 rounded-lg text-sm flex items-center gap-1">
<Download className="w-4 h-4" /> Download
</button>
<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">
<FileText className="w-4 h-4" /> View
</button>
</div>
))}
))}
</div>
</div>
{/* Battery Rental Invoices */}
<div className="px-4 pt-3 pb-4">
<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>
<div className="space-y-1.5">
{invoices.filter(inv => inv.type === 'battery_rental').length === 0 && (
<p className="text-xs text-slate-400 italic py-1">No battery rental invoices.</p>
)}
{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>

View File

@@ -1,7 +1,7 @@
'use client';
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 {
key: string;
@@ -35,11 +35,10 @@ interface Role {
}
const buildDefaultGroups = (): PermissionGroup[] => [
{
id: 'kyc',
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,
permissions: [
{ 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 },
},
{
label: 'Plan Selection with Condition',
label: 'Plan Selection with EV Condition',
view: { key: 'settings.plan_selection_with_condition_view', label: 'View', 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 },
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',
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 },
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: [],
},
{
id: 'dashboard',
title: 'Dashboard',
description: 'Access to main dashboard',
description: 'Access to main dashboard insights and summary metrics.',
icon: BarChart3,
permissions: [
{ key: 'dashboard.view', label: 'View Dashboard', enabled: false },
@@ -101,133 +115,188 @@ const buildDefaultGroups = (): PermissionGroup[] => [
{
id: '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,
permissions: [
{ key: 'rentals.view', label: 'View', enabled: false },
{ key: 'rentals.create', label: 'Create', enabled: false },
{ key: 'rentals.edit', label: 'Edit', enabled: false },
{ key: 'rentals.delete', label: 'Delete', enabled: false },
{ key: 'rental.view', label: 'View Rentals', enabled: false },
{ key: 'rental.create', label: 'Create Rental', enabled: false },
{ key: 'rental.requset', label: 'Rental Request', 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',
title: 'Bikers',
description: 'Manage bikers',
title: 'Bikers Management',
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,
permissions: [
{ key: 'bikers.view', label: 'View', enabled: false },
{ key: 'bikers.create', label: 'Create', enabled: false },
{ key: 'bikers.edit', label: 'Edit', enabled: false },
{ key: 'bikers.delete', label: 'Delete', enabled: false },
{ key: 'biker.view', label: 'View Biker Profile', enabled: false },
{ key: 'biker.create', label: 'Create Biker Profile', enabled: false },
{ key: 'biker.edit', label: 'Edit Biker Profile', 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',
title: 'Investors',
description: 'Manage investors',
title: 'Investors Management',
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,
permissions: [
{ key: 'investors.view', label: 'View', enabled: false },
{ key: 'investors.create', label: 'Create', enabled: false },
{ key: 'investors.edit', label: 'Edit', enabled: false },
{ key: 'investors.delete', label: 'Delete', enabled: false },
{ key: 'investor.view', label: 'View Investors', enabled: false },
{ key: 'investor.create', label: 'Create Investor Profile', enabled: false },
{ key: 'investor.edit', label: 'Edit Investor Details', 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',
title: 'Fleet',
description: 'Manage fleet vehicles',
title: 'Fleet Management',
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,
permissions: [
{ key: 'fleet.view', label: 'View', enabled: false },
{ key: 'fleet.create', label: 'Create', enabled: false },
{ key: 'fleet.edit', label: 'Edit', enabled: false },
{ key: 'fleet.delete', label: 'Delete', enabled: false },
{ key: 'fleet.view', label: 'View Fleet Vehicles', enabled: false },
{ key: 'fleet.create', label: 'Register New Bike', enabled: false },
{ key: 'fleet.edit', label: 'Edit Fleet Vehicle', 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',
title: 'Merchants',
description: 'Manage merchants',
icon: Store,
id: 'service_centers',
title: 'Service Centers',
description: 'Add and manage service center listings, locations, and capacity. Track fleet service logs, maintenance logs, and uploaded dealer/biker invoices.',
icon: Wrench,
permissions: [
{ key: 'merchants.view', label: 'View', enabled: false },
{ key: 'merchants.create', label: 'Create', enabled: false },
{ key: 'merchants.edit', label: 'Edit', enabled: false },
{ key: 'merchants.delete', label: 'Delete', 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 },
{ key: 'service_center.view', label: 'View Service Centers', enabled: false },
{ key: 'service_center.create', label: 'Create Service Center', enabled: false },
{ key: 'service_center.edit', label: 'Edit Service Center Details', enabled: false },
{ key: 'service_center.delete', label: 'Remove Service Center', enabled: false },
]
},
{
id: 'maintenance',
title: 'Maintenance',
description: 'Manage maintenance requests',
icon: Wrench,
title: 'Damage & Maintenance',
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: Settings,
permissions: [
{ key: 'maintenance.view', label: 'View', enabled: false },
{ key: 'maintenance.create', label: 'Create', enabled: false },
{ key: 'maintenance.edit', label: 'Edit', enabled: false },
{ key: 'maintenance.delete', label: 'Delete', enabled: false },
{ key: 'maintenance.view', label: 'View Damage Logs', enabled: false },
{ key: 'maintenance.create', label: 'Report Damage/Claim Free Service', enabled: false },
{ key: 'maintenance.edit', label: 'Update Repair Details', enabled: false },
{ key: 'maintenance.delete', label: 'Delete Repair Log', enabled: false },
]
},
{
id: '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,
permissions: [
{ key: 'accounting.view', label: 'View', enabled: false },
{ key: 'accounting.create', label: 'Create', enabled: false },
{ key: 'accounting.edit', label: 'Edit', enabled: false },
{ key: 'accounting.delete', label: 'Delete', enabled: false },
{ key: 'accounting.view', label: 'View Financial Ledger', enabled: false },
{ key: 'accounting.create', label: 'Create Financial Entry', enabled: false },
{ key: 'accounting.edit', label: 'Modify Financial Entry', 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',
title: 'Reports',
description: 'View and generate reports',
title: 'Reports & Analytics',
description: 'Generate, schedule, view, and export comprehensive administrative reports for KYC, fleet, investment payouts, rentals, and general ledger operations.',
icon: TrendingUp,
permissions: [
{ key: 'reports.view', label: 'View', enabled: false },
{ key: 'reports.export', label: 'Export', enabled: false },
{ key: 'reports.view', label: 'View Reports', enabled: false },
{ key: 'reports.export', label: 'Export Reports (CSV/PDF)', enabled: false },
]
},
{
id: 'users',
title: 'Users',
description: 'Manage system users',
title: 'Users & Staff Management',
description: 'Admin-only module for managing internal administrative users, hub managers, front desk staff, accountants, and Super Admins.',
icon: UserCog,
permissions: [
{ key: 'users.view', label: 'View', enabled: false },
{ key: 'users.create', label: 'Create', enabled: false },
{ key: 'users.edit', label: 'Edit', enabled: false },
{ key: 'users.delete', label: 'Delete', enabled: false },
{ key: 'users.view', label: 'View System Users', enabled: false },
{ key: 'users.create', label: 'Create Staff User', enabled: false },
{ key: 'users.edit', label: 'Edit Staff details', 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[] = [
@@ -238,7 +307,12 @@ const mockRoles: Role[] = [
isDefault: false,
permissionGroups: buildDefaultGroups().map(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 => ({
...p,
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',
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,
permissionGroups: buildDefaultGroups().map(g => {
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') {
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;
})
},
@@ -319,7 +444,16 @@ const mockRoles: Role[] = [
...g,
permissions: g.permissions.map(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 (
<div className="p-6 space-y-6">
<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 className="flex items-center justify-between bg-amber-50 border border-amber-200 rounded-xl p-4">
<div>
<h4 className="font-semibold text-amber-800">Investment Plans ({settings.plans.investment.length})</h4>
<p className="text-sm text-amber-600">Manage investment plans for investors</p>
<h4 className="font-semibold text-amber-800">EV Investment Plans ({settings.plans.investment.length})</h4>
<p className="text-sm text-amber-600">Manage EV Investment Plans for investors</p>
</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">
<Plus className="w-4 h-4" /> New Plan

View File

@@ -1,7 +1,7 @@
'use client';
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';
interface PlanSelectionProps {
@@ -14,6 +14,108 @@ interface PlanSelectionProps {
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({
settings,
setSettings,
@@ -172,6 +274,18 @@ export default function PlanSelection({
</div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</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>
<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..." />
@@ -312,6 +426,18 @@ export default function PlanSelection({
</div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</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>
<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..." />
@@ -452,6 +578,18 @@ export default function PlanSelection({
</div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</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>
<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..." />
@@ -465,26 +603,16 @@ export default function PlanSelection({
</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">
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<Trash2 className="w-5 h-5 text-red-500" /> Delete Plan
</h3>
<button onClick={() => setDeleteModal({ type: null, idx: null })} className="text-slate-400 hover:text-slate-600 text-2xl">&times;</button>
</div>
<div className="p-4">
<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>
{/* Delete Confirmation Modal */}
{deleteModal.type !== null && (
<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 p-6 space-y-4">
<h3 className="text-lg font-bold text-slate-800">Delete Plan?</h3>
<p className="text-sm text-slate-500">This will permanently remove the plan. This action cannot be undone.</p>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<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>
<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>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { Settings, Package, Palette, Link2, Mail, Monitor, FileCheck, DollarSign, Zap, Users, Plus, X, Save, Pencil, Trash2, FileText } from 'lucide-react';
import { useState, useEffect } from '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 GeneralSettings from './components/GeneralSettings';
import BrandingSettings from './components/BrandingSettings';
@@ -13,6 +13,7 @@ import PartsSettings from './components/PartsSettings';
import CompanyPolicySettings from './components/CompanyPolicySettings';
import PlanSelection from './components/PlanSelection';
import InvestmentSettings from './components/InvestmentSettings';
import BatteryInvestmentSettings from './components/BatteryInvestmentSettings';
import SwapStationSettings from './components/SwapStationSettings';
import RiderRequestSettings from './components/RiderRequestSettings';
import EmailSMSTemplates from './components/EmailSMSTemplates';
@@ -124,6 +125,7 @@ export interface CompanySettings {
monthlyPenalty2: number;
monthlyPenalty3: number;
ficoSharePercent: number;
freeServiceConditions: { months: number; freeServices: number }[];
description: string;
}[];
rentToOwn: {
@@ -150,6 +152,7 @@ export interface CompanySettings {
profit: number;
ficoRentSharePercent: number;
ficoProfitSharePercent: number;
freeServiceConditions: { months: number; freeServices: number }[];
description: string;
}[];
shareEv: {
@@ -178,6 +181,7 @@ export interface CompanySettings {
monthlyPenalty3: number;
totalMonthlySubscription: number;
ficoSharePercent: number;
freeServiceConditions: { months: number; freeServices: number }[];
description: string;
}[];
investment: {
@@ -201,6 +205,24 @@ export interface CompanySettings {
ficoRentToOwn: 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: {
id: string;
name: string;
@@ -479,6 +501,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty2: 30000,
monthlyPenalty3: 50000,
ficoSharePercent: 50,
freeServiceConditions: [
{ months: 3, freeServices: 2 },
{ months: 6, freeServices: 4 },
],
description: 'Premium single person rental plan with extra benefits',
},
{
@@ -500,6 +526,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty2: 22000,
monthlyPenalty3: 40000,
ficoSharePercent: 45,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
{ months: 3, freeServices: 2 },
],
description: 'Standard single person rental plan',
},
{
@@ -521,6 +551,9 @@ const initialSettings: CompanySettings = {
monthlyPenalty2: 18000,
monthlyPenalty3: 30000,
ficoSharePercent: 40,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
],
description: 'Economy single person rental plan',
}
],
@@ -549,6 +582,10 @@ const initialSettings: CompanySettings = {
profit: 20000,
ficoRentSharePercent: 50,
ficoProfitSharePercent: 45,
freeServiceConditions: [
{ months: 3, freeServices: 2 },
{ months: 6, freeServices: 4 },
],
description: 'Premium rent to own plan with high-end EV',
},
{
@@ -575,6 +612,10 @@ const initialSettings: CompanySettings = {
profit: 15000,
ficoRentSharePercent: 45,
ficoProfitSharePercent: 45,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
{ months: 3, freeServices: 2 },
],
description: 'Standard rent to own plan',
},
{
@@ -601,6 +642,9 @@ const initialSettings: CompanySettings = {
profit: 15000,
ficoRentSharePercent: 40,
ficoProfitSharePercent: 40,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
],
description: 'Economy rent to own plan',
}
],
@@ -631,6 +675,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty3: 35000,
totalMonthlySubscription: 16800,
ficoSharePercent: 50,
freeServiceConditions: [
{ months: 3, freeServices: 2 },
{ months: 6, freeServices: 4 },
],
description: 'Premium shared EV with premium bikes',
},
{
@@ -659,6 +707,10 @@ const initialSettings: CompanySettings = {
monthlyPenalty3: 25000,
totalMonthlySubscription: 11200,
ficoSharePercent: 45,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
{ months: 3, freeServices: 2 },
],
description: 'Standard shared EV plan',
},
{
@@ -687,6 +739,9 @@ const initialSettings: CompanySettings = {
monthlyPenalty3: 20000,
totalMonthlySubscription: 8400,
ficoSharePercent: 40,
freeServiceConditions: [
{ months: 2, freeServices: 1 },
],
description: 'Economy shared EV plan',
}
],
@@ -734,6 +789,44 @@ const initialSettings: CompanySettings = {
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: [
{
id: 'ss_1',
@@ -814,7 +907,26 @@ const initialSettings: CompanySettings = {
export default function CompanySettingsPage() {
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 [activeRentalTypeTab, setActiveRentalTypeTab] = useState<'single' | 'shared' | 'renttoown'>('single');
const [saved, setSaved] = useState(false);
@@ -837,6 +949,8 @@ export default function CompanySettingsPage() {
const [newPolicyDesc, setNewPolicyDesc] = useState('');
const [newPolicyShowApp, setNewPolicyShowApp] = useState(true);
const [newPolicyShowWeb, setNewPolicyShowWeb] = useState(true);
// Bike Investment
const [addInvestPlan, setAddInvestPlan] = useState(false);
const [newInvestName, setNewInvestName] = useState('');
const [newInvestTier, setNewInvestTier] = useState('Standard');
@@ -857,6 +971,24 @@ export default function CompanySettingsPage() {
const [newInvestEvBasePrice, setNewInvestEvBasePrice] = useState(200000);
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 = () => {
if (newInvestName.trim() && typeof window !== 'undefined') {
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 [addSwapStationPlan, setAddSwapStationPlan] = useState(false);
const [newSwapName, setNewSwapName] = useState('');
@@ -1002,6 +1163,7 @@ export default function CompanySettingsPage() {
monthlyPenalty2: 22000,
monthlyPenalty3: 40000,
ficoSharePercent: 45,
freeServiceConditions: [],
description: '',
} : type === 'rentToOwn' ? {
id: newId,
@@ -1027,6 +1189,7 @@ export default function CompanySettingsPage() {
profit: 15000,
ficoRentSharePercent: 45,
ficoProfitSharePercent: 45,
freeServiceConditions: [],
description: '',
} : {
id: newId,
@@ -1054,6 +1217,7 @@ export default function CompanySettingsPage() {
monthlyPenalty3: 25000,
totalMonthlySubscription: 11200,
ficoSharePercent: 45,
freeServiceConditions: [],
description: '',
};
if (type === 'singleRent') {
@@ -1123,7 +1287,8 @@ export default function CompanySettingsPage() {
{ id: 'kyc', label: 'KYC Documents', 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: 'riderrequest', label: 'Rider Request Plan (P2)', icon: Users },
{ 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' && (
<SwapStationSettings

View File

@@ -67,9 +67,9 @@ export default function InvestorDashboardPage() {
<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">
{/* <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>
</Link> */}
</div>
</div>

View File

@@ -33,12 +33,6 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
const [activeTab, setActiveTab] = useState('overview');
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [paymentAmount, setPaymentAmount] = useState('');
const paymentHistory: PaymentRecord[] = [
{ id: 'pay1', date: '2024-01-15', amount: 400000, installmentNo: 1, type: 'installment', method: 'Bank Transfer', status: 'completed' },
{ id: 'pay2', date: '2024-02-15', amount: 150000, installmentNo: null, type: 'partial', method: 'bKash', status: 'completed' },
];
if (!investment) {
return (
@@ -57,6 +51,20 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
);
}
const isBattery = investment.assetType === 'battery' || investment.planName?.toLowerCase().includes('battery');
const paymentHistory: PaymentRecord[] = [
{
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;
@@ -68,39 +76,38 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
};
const style = planConfig[investment.planType] || planConfig.gold;
const demoBikes = [
{ id: 'b1', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'AB-1234', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'rented', currentRent: 450, totalEarnings: 45000, batteryLevel: 85, range: 60, location: 'Dhaka Central Hub' },
{ id: 'b2', model: 'Apex E-Bike Pro', brand: 'Apex', plateNumber: 'CD-5678', image: 'https://images.unsplash.com/photo-1622185135505-2d795043906a?w=400&h=300&fit=crop', status: 'rented', currentRent: 500, totalEarnings: 52000, batteryLevel: 72, range: 55, location: 'Gulshan Area' },
{ id: 'b3', model: 'Niu NQi Sport', brand: 'Niu', plateNumber: 'EF-9012', image: 'https://images.unsplash.com/photo-1591353230407-2b14a8e4c8d3?w=400&h=300&fit=crop', status: 'available', currentRent: 0, totalEarnings: 28000, batteryLevel: 95, range: 70, location: 'Mirpur Depot' },
{ id: 'b4', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'GH-3456', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'maintenance', currentRent: 0, totalEarnings: 15000, batteryLevel: 0, range: 0, location: 'Service Center' },
];
const bikeIds = investment.bikeIds || (investmentId === 'ip1' ? ['b1'] : investmentId === 'ip2' ? ['b2'] : []);
const batteryIds = investment.batteryIds || (isBattery ? ['BAT-001', 'BAT-002', 'BAT-005'] : []);
const demoPnl = { grossRevenue: 185000, platformFee: 83250, insurance: 15000, maintenance: 8500, netProfit: 78250 };
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 demoTransactions = [
{ id: 'tx1', date: '2024-05-15', description: 'Rental Income - Bike AB-1234', amount: 450, status: 'completed' },
{ id: 'tx2', date: '2024-05-14', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' },
{ id: 'tx3', date: '2024-05-13', description: 'Rental Income - Bike AB-1234', amount: 450, status: 'completed' },
{ id: 'tx4', date: '2024-05-12', description: 'Rental Income - Bike EF-9012', amount: 350, status: 'completed' },
{ id: 'tx5', date: '2024-05-11', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' },
{ id: 'tx6', date: '2024-05-10', description: 'Withdrawal to Bank', amount: -10000, status: 'completed' },
{ id: 'tx7', date: '2024-05-09', description: 'Rental Income - Bike AB-1234', amount: 450, status: 'completed' },
{ id: 'tx8', date: '2024-05-08', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' },
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 = parseFloat(paymentAmount);
if (!amount || amount <= 0) {
toast.error('Please enter a valid amount');
return;
}
if (amount > dueAmount) {
toast.error('Amount exceeds due amount');
return;
}
const amount = dueAmount;
toast.success(`Payment of ৳${amount.toLocaleString()} submitted successfully!`);
setShowPaymentModal(false);
setPaymentAmount('');
};
return (
@@ -146,7 +153,7 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4 mb-6">
<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">
@@ -156,6 +163,20 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</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">
@@ -165,34 +186,30 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</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-amber-200 shadow-sm bg-amber-50/50">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-amber-200 rounded-lg flex items-center justify-center">
<Clock className="w-4 h-4 text-amber-600" />
</div>
<p className="text-xs text-amber-700 font-medium">Due Amount</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-amber-600">{dueAmount.toLocaleString()}</p>
<button onClick={() => setShowPaymentModal(true)} className="mt-2 w-full py-1.5 bg-amber-500 text-white text-xs font-bold rounded-lg hover:bg-amber-600 transition-colors">
Pay Now
</button>
</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-blue-100 rounded-lg flex items-center justify-center">
<Bike className="w-4 h-4 text-blue-600" />
<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">Bikes</p>
<p className="text-xs text-slate-500 font-medium">{isBattery ? 'Batteries' : 'Bikes'}</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-blue-600">{demoBikes.length}</p>
<p className={`text-xl lg:text-2xl font-bold ${isBattery ? 'text-emerald-600' : 'text-blue-600'}`}>
{isBattery ? assignedBatteries.length : assignedBikes.length}
</p>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="flex overflow-x-auto border-b border-slate-100 justify-between sm:justify-start px-4 lg:px-0">
{[
{ key: 'overview', label: 'Overview', icon: FileText, count: null },
{ key: 'bikes', label: 'Bikes', icon: Bike, count: demoBikes.length },
{ key: 'bikes', label: isBattery ? 'Batteries' : 'Bikes', icon: isBattery ? Battery : Bike, count: isBattery ? assignedBatteries.length : assignedBikes.length },
{ key: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length },
{ key: 'statement', label: 'Statement', icon: Receipt, count: null },
].map((tab) => {
@@ -261,92 +278,117 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</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">55%</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">45%</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">40%</p>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-4">
<h4 className="font-semibold text-green-800 flex items-center gap-2">
<DollarSign className="w-4 h-4 text-green-600" /> Payment History
{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>
<button onClick={() => setShowPaymentModal(true)} className="px-3 py-1.5 bg-green-600 text-white text-xs font-bold rounded-lg hover:bg-green-700 transition-colors">
Make Payment
</button>
<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="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-white/50">
<tr>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Date</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Type</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Method</th>
<th className="px-3 py-2 text-right text-xs font-semibold text-slate-500">Amount</th>
<th className="px-3 py-2 text-center text-xs font-semibold text-slate-500">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-green-100">
{paymentHistory.map((payment) => (
<tr key={payment.id} className="bg-white/50">
<td className="px-3 py-2 text-slate-600">{payment.date}</td>
<td className="px-3 py-2"><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-3 py-2 text-slate-600">{payment.method}</td>
<td className="px-3 py-2 text-right font-bold text-green-700">{payment.amount.toLocaleString()}</td>
<td className="px-3 py-2 text-center"><span className="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 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>
)}
</div>
)}
{activeTab === 'bikes' && (
<div className="space-y-4">
<p className="text-sm text-slate-500">{demoBikes.length} bikes assigned to this investment</p>
{demoBikes.map((bike) => (
<div 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/30 transition-colors">
<div className="w-24 h-20 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">
<div className="flex items-center gap-2 mb-1">
<h5 className="font-semibold text-slate-800">{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>
isBattery ? (
<div className="space-y-4">
<p className="text-sm text-slate-500">{assignedBatteries.length} battery pack{assignedBatteries.length !== 1 ? 's' : ''} assigned to this investment</p>
{assignedBatteries.map((battery) => (
<Link
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>
<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"><Gauge className="w-3 h-3" /> {bike.range} km</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {bike.location}</span>
<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-emerald-700 transition-colors">{battery.model}</h5>
<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>
</div>
<p className="text-sm text-slate-500">{battery.serialNumber} {battery.brand}</p>
<div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1">Cycle Count: {battery.cycleCount}</span>
<span className="flex items-center gap-1">SoC / Health: {battery.soc}</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {battery.location}</span>
</div>
</div>
</div>
<div className="text-right shrink-0">
<p className="text-xs text-slate-500 mb-1">Daily Rent</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>
</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">{(investment.monthlyReturn / assignedBatteries.length).toLocaleString()}</p>
<p className="text-xs text-slate-400 mt-1">Total: {battery.earnings.toLocaleString()}</p>
</div>
<ChevronRight className="w-5 h-5 text-slate-400 group-hover:text-emerald-600 group-hover:translate-x-1 transition-all" />
</div>
</Link>
))}
</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 === 'statement' && (
@@ -359,12 +401,21 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
<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 (45%)</span><span className="font-medium text-red-500">-{demoPnl.platformFee.toLocaleString()}</span></div>
<div className="flex justify-between text-sm"><span className="text-slate-500">Insurance Coverage</span><span className="font-medium text-slate-600">-{demoPnl.insurance.toLocaleString()}</span></div>
<div className="flex justify-between text-sm"><span className="text-slate-500">Maintenance</span><span className="font-medium text-slate-600">-{demoPnl.maintenance.toLocaleString()}</span></div>
<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 className="flex justify-between text-sm">
<span className="text-slate-500">Insurance Coverage</span>
<span className="font-medium text-slate-600">-{demoPnl.insurance.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Maintenance</span>
<span className="font-medium text-slate-600">-{demoPnl.maintenance.toLocaleString()}</span>
</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 (55%)</span>
<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>
@@ -426,9 +477,12 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
<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>
<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>
{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">
@@ -481,7 +535,7 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</div>
</div>
{showPaymentModal && (
{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">
@@ -506,19 +560,9 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</div>
</div>
<div>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Enter Amount</label>
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></span>
<input type="number" value={paymentAmount} onChange={(e) => setPaymentAmount(e.target.value)}
placeholder="Enter amount"
className="w-full pl-8 pr-4 py-3 border border-slate-200 rounded-xl text-lg font-semibold focus:outline-none focus:border-investor" />
</div>
<div className="flex gap-2 mt-2">
<button onClick={() => setPaymentAmount(dueAmount.toString())} className="px-3 py-1 text-xs bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200">Full Due</button>
<button onClick={() => setPaymentAmount((dueAmount / 2).toString())} className="px-3 py-1 text-xs bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200">Half</button>
<button onClick={() => setPaymentAmount((dueAmount / 4).toString())} className="px-3 py-1 text-xs bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200">25%</button>
</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">
@@ -536,7 +580,7 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</div>
<button onClick={handlePaymentSubmit} className="w-full py-3 bg-investor text-white rounded-xl font-bold hover:bg-investor-dark transition-colors">
Pay {paymentAmount ? parseFloat(paymentAmount).toLocaleString() : '0'}
Pay {dueAmount.toLocaleString()}
</button>
</div>
</div>

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

@@ -32,8 +32,8 @@ const mockNotifications = [
{
id: '4',
type: 'alert',
title: 'Maintenance Alert',
message: 'Bike XY-9012 requires maintenance attention',
title: 'Damage Alert',
message: 'Minor scratch damage has been reported on your bike XY-9012.',
time: '2 days ago',
read: true,
},
@@ -56,8 +56,8 @@ const mockNotifications = [
{
id: '7',
type: 'alert',
title: 'Low Battery Warning',
message: 'Bike EF-3456 battery is below 20%',
title: 'Damage Alert',
message: 'Cracked indicator light has been reported on your bike EF-3456.',
time: '5 hours ago',
read: true,
},

View File

@@ -2,7 +2,7 @@
import { useState } from 'react';
import Link from 'next/link';
import { Target, Plus, Zap, ChevronRight, ArrowRight, Edit, Trash2, Eye, TrendingUp, X, CreditCard } 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 toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
@@ -12,14 +12,20 @@ export default function MyInvestmentsPage() {
const [showCreateModal, setShowCreateModal] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
const [newInvestment, setNewInvestment] = useState({
planName: '', planType: 'gold', totalInvestment: 0, initialPayment: 0, paymentType: 'full',
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 },
{ id: '5bike', name: '5 Bike Plan', tier: 'Premium', evBasePrice: 180000, minQuantity: 5, duration: 24, maxInvestment: 5000000 },
{ id: '10bike', name: '10 Bike Plan', tier: 'Enterprise', evBasePrice: 170000, minQuantity: 10, duration: 36, maxInvestment: 10000000 },
{ 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 }> = {
@@ -44,7 +50,7 @@ export default function MyInvestmentsPage() {
);
setShowCreateModal(false);
setSelectedTemplate(null);
setNewInvestment({ planName: '', planType: 'gold', totalInvestment: 0, initialPayment: 0, paymentType: 'full', startDate: '', endDate: '', paymentMethod: 'bank', transactionReference: '', notes: '' });
setNewInvestment({ planName: '', planType: 'gold', assetType: 'bike', totalInvestment: 0, initialPayment: 0, paymentType: 'full', startDate: '', endDate: '', paymentMethod: 'bank', transactionReference: '', notes: '' });
};
return (
@@ -60,20 +66,20 @@ export default function MyInvestmentsPage() {
</h1>
<p className="text-sm text-slate-500 mt-1">Manage your active portfolios and track your earnings.</p>
</div>
<button
{/* <button
onClick={() => setShowCreateModal(true)}
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"
>
<Plus className="w-4 h-4" /> Create Investment
</button>
</button> */}
</div>
</div>
{/* Investment Plans Cards */}
{/* EV Investment Plans Cards */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-slate-800">Investment Plans</h3>
<h3 className="font-semibold text-slate-800">EV Investment Plans</h3>
<p className="text-sm text-slate-500">Manage investment portfolios for this investor</p>
</div>
</div>
@@ -85,7 +91,14 @@ export default function MyInvestmentsPage() {
<div key={inv.id} className={`bg-white rounded-xl border ${style.border} overflow-hidden`}>
<div className={`${style.bg} p-4 flex items-center justify-between`}>
<div>
<h4 className="font-semibold text-slate-800">{inv.planName}</h4>
<h4 className="font-bold text-slate-800 flex items-center gap-1.5">
{inv.planName.toLowerCase().includes('battery') ? (
<Battery className="w-4 h-4 text-emerald-600 animate-pulse flex-shrink-0" />
) : (
<Zap className="w-4 h-4 text-investor flex-shrink-0" />
)}
{inv.planName}
</h4>
<p className="text-sm text-slate-500 capitalize">{inv.planType} Plan</p>
</div>
<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'}`}>
@@ -104,17 +117,37 @@ export default function MyInvestmentsPage() {
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3 space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-slate-400">Assigned Assets</span>
<span className="font-semibold text-slate-800">
{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">12 months</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">3 months</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">10%</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">
@@ -166,10 +199,41 @@ export default function MyInvestmentsPage() {
<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">
{PLAN_TEMPLATES.map(plan => (
{(newInvestment.assetType === 'battery' ? BATTERY_TEMPLATES : PLAN_TEMPLATES).map(plan => (
<button
key={plan.id}
onClick={() => {
@@ -182,10 +246,10 @@ export default function MyInvestmentsPage() {
startDate: new Date().toISOString().split('T')[0],
});
}}
className="p-4 rounded-lg border-2 border-slate-200 hover:border-investor/50 text-left transition-all hover:bg-slate-50"
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} bikes</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>
))}
@@ -228,11 +292,11 @@ export default function MyInvestmentsPage() {
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">EV Base Price ()</label>
<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</label>
<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>

View File

@@ -12,6 +12,8 @@ import {
import { investors } from '@/data/mockData';
import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
import { logout } from '@/lib/auth';
function SectionCard({ title, icon: Icon, children, headerBg = 'bg-slate-50', headerBorder = 'border-slate-100', editKey, editingSection, setEditingSection, onEdit, editForm, setEditForm }: {
title: string; icon: any; children: React.ReactNode; headerBg?: string; headerBorder?: string; editKey?: string; editingSection?: string | null; setEditingSection?: (s: string | null) => void; onEdit?: () => void; editForm?: any; setEditForm?: any
@@ -54,6 +56,14 @@ export default function InvestorProfilePage() {
const [showUploadDocModal, setShowUploadDocModal] = useState(false);
const [uploadDocForm, setUploadDocForm] = useState({ docType: '', docNumber: '', docFile: null as File | null });
const [showApprovalModal, setShowApprovalModal] = useState(false);
const [approvalModalConfig, setApprovalModalConfig] = useState({ actionType: '', fieldName: '' });
const triggerApprovalRequest = (actionType: string, fieldName: string) => {
setApprovalModalConfig({ actionType, fieldName });
setShowApprovalModal(true);
};
const [showAddBankModal, setShowAddBankModal] = useState(false);
const [showEditBankModal, setShowEditBankModal] = useState(false);
const [showDeleteBankModal, setShowDeleteBankModal] = useState(false);
@@ -93,29 +103,35 @@ export default function InvestorProfilePage() {
};
const handleSaveBank = () => {
toast.success('Bank account saved successfully!');
setShowAddBankModal(false);
setShowEditBankModal(false);
setNewBankForm({ bankName: '', accountName: '', accountNumber: '', branch: '', routing: '', isPrimary: false });
triggerApprovalRequest('save/edit', 'Bank Account');
};
const handleDeleteBank = () => {
toast.success('Bank account deleted!');
setShowDeleteBankModal(false);
triggerApprovalRequest('delete', 'Bank Account');
};
const handleSaveMobile = () => {
toast.success('Mobile banking saved successfully!');
setShowAddMobileModal(false);
setShowEditMobileModal(false);
setNewMobileForm({ provider: '', number: '' });
triggerApprovalRequest('save/edit', 'Mobile Banking Account');
};
const handleDeleteMobile = () => {
toast.success('Mobile banking deleted!');
setShowDeleteMobileModal(false);
triggerApprovalRequest('delete', 'Mobile Banking Account');
};
const handleLogout = () => {
logout();
window.location.href = '/login';
};
const tabs = [
{ id: 'personal', label: 'Personal Info', icon: User },
{ id: 'nominee', label: 'Nominee & Emergency', icon: Heart },
@@ -131,40 +147,43 @@ export default function InvestorProfilePage() {
<div className="max-w-8xl mx-auto mb-12 lg:mb-0">
{/* Profile Header */}
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-4 sm:mb-6">
<div className="p-4 sm:p-5 flex flex-col sm:flex-row items-start gap-4 sm:gap-5">
<div className="relative group shrink-0">
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center shadow-lg">
<span className="text-xl sm:text-2xl font-bold text-white">{investor.name.charAt(0)}</span>
<div className="p-4 sm:p-5 flex flex-col md:flex-row items-start justify-between gap-4 md:gap-5">
<div className="flex flex-col sm:flex-row items-start gap-4 sm:gap-5 flex-1 w-full">
<div className="relative group shrink-0">
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center shadow-lg">
<span className="text-xl sm:text-2xl font-bold text-white">{investor.name.charAt(0)}</span>
</div>
<label className="absolute bottom-0 right-0 w-7 h-7 sm:w-8 sm:h-8 bg-blue-600 rounded-full flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity shadow-lg border-2 border-white">
<Camera className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-white" />
<input type="file" accept="image/*" className="hidden" />
</label>
</div>
<label className="absolute bottom-0 right-0 w-7 h-7 sm:w-8 sm:h-8 bg-blue-600 rounded-full flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity shadow-lg border-2 border-white">
<Camera className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-white" />
<input type="file" accept="image/*" className="hidden" />
</label>
</div>
<div className="flex-1 w-full">
<div className="flex flex-wrap items-center gap-2 mb-2">
<h1 className="text-xl sm:text-2xl font-bold text-slate-800">{investor.name}</h1>
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.kycStatus === 'verified' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
<ShieldCheck className="w-3 h-3 inline mr-1" /> KYC {investor.kycStatus}
</span>
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
{investor.status}
</span>
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.riskLevel === 'low' ? 'bg-green-100 text-green-700' : investor.riskLevel === 'medium' ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}>
Risk: {investor.riskLevel}
</span>
</div>
<p className="text-xs sm:text-sm text-slate-500 mb-2">{investor.id} &bull; {investor.email}</p>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="text-xs font-medium px-2 py-0.5 rounded bg-purple-50 text-purple-700">Ref: {investor.referralCode}</span>
<span className="text-xs text-slate-400">{investor.totalReferrals} Referrals</span>
<span className="text-xs text-slate-400">&bull; {investor.investments?.length || 0} Investments</span>
<span className="text-xs text-slate-400">&bull; Referral Earnings: {investor.referralEarnings?.toLocaleString() || 0}</span>
<div className="flex-1 w-full">
<div className="flex flex-wrap items-center gap-2 mb-2">
<h1 className="text-xl sm:text-2xl font-bold text-slate-800">{investor.name}</h1>
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.kycStatus === 'verified' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
<ShieldCheck className="w-3 h-3 inline mr-1" /> KYC {investor.kycStatus}
</span>
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
{investor.status}
</span>
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.riskLevel === 'low' ? 'bg-green-100 text-green-700' : investor.riskLevel === 'medium' ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}>
Risk: {investor.riskLevel}
</span>
</div>
<p className="text-xs sm:text-sm text-slate-500 mb-2">{investor.id} &bull; {investor.email}</p>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="text-xs font-medium px-2 py-0.5 rounded bg-purple-50 text-purple-700">Ref: {investor.referralCode}</span>
<span className="text-xs text-slate-400">{investor.totalReferrals} Referrals</span>
<span className="text-xs text-slate-400">&bull; {investor.investments?.length || 0} Investments</span>
<span className="text-xs text-slate-400">&bull; Referral Earnings: {investor.referralEarnings?.toLocaleString() || 0}</span>
</div>
</div>
</div>
</div>
<div className="border-t border-slate-100 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 divide-x divide-slate-100 bg-slate-50/50">
<div className="p-3 sm:p-4 text-center">
<p className="text-xs text-purple-600 font-medium">Invested</p>
@@ -922,10 +941,14 @@ export default function InvestorProfilePage() {
</div>
<p className="text-xs text-slate-400">{session.location} {session.time}</p>
</div>
{!session.current && <button className="px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 rounded-lg">Revoke</button>}
{!session.current ? (
<button className="px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 rounded-lg">Revoke</button>
) : (
<button onClick={handleLogout} className="px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 rounded-lg">Log Out</button>
)}
</div>
))}
<button className="w-full py-3 bg-red-50 text-red-600 rounded-lg text-sm font-medium hover:bg-red-100 flex items-center justify-center gap-2">
<button onClick={handleLogout} className="w-full py-3 bg-red-50 text-red-600 rounded-lg text-sm font-medium hover:bg-red-100 flex items-center justify-center gap-2">
<LogOut className="w-4 h-4" /> Sign Out All Devices
</button>
</div>
@@ -1102,7 +1125,7 @@ export default function InvestorProfilePage() {
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowTaxModal(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={() => { toast.success('Tax information updated'); setShowTaxModal(false); }} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark">Save</button>
<button onClick={() => { setShowTaxModal(false); triggerApprovalRequest('save/edit', 'Tax Information'); }} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark">Save</button>
</div>
</div>
</div>
@@ -1193,6 +1216,35 @@ export default function InvestorProfilePage() {
</div>
</div>
)}
{showApprovalModal && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-slate-100 animate-in fade-in zoom-in duration-200">
<div className="p-6 text-center space-y-4">
<div className="w-16 h-16 bg-amber-50 rounded-full flex items-center justify-center mx-auto text-amber-500 border border-amber-200">
<ShieldCheck className="w-8 h-8" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-bold text-slate-800">Verification Request Submitted</h3>
<p className="text-sm text-slate-500 leading-relaxed">
Your request to <strong className="text-slate-700">{approvalModalConfig.actionType}</strong> your <strong className="text-slate-700">{approvalModalConfig.fieldName}</strong> has been successfully sent to JAIBEN administrators.
</p>
<p className="text-xs text-slate-400 bg-slate-50 p-2.5 rounded-lg border border-slate-100 italic">
For security reasons, all updates to bank credentials, mobile wallets, or tax IDs require manual review. You will be notified once our team verifies the changes.
</p>
</div>
</div>
<div className="p-4 bg-slate-50 border-t border-slate-100 flex justify-center">
<button
onClick={() => setShowApprovalModal(false)}
className="w-full py-2.5 bg-investor hover:bg-investor-dark text-white rounded-xl text-sm font-semibold transition-all shadow-md shadow-investor/10"
>
Understood & Continue
</button>
</div>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -341,9 +341,9 @@ export default function InvestorWithdrawPage() {
</div>
</div>
{/* Select Investment Plans & Bikes */}
{/* Select EV Investment Plans & Bikes */}
<div>
<h4 className="font-semibold text-slate-800 mb-2">Select Investment Plans & Bikes</h4>
<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
@@ -361,7 +361,7 @@ export default function InvestorWithdrawPage() {
<div className="border border-slate-200 rounded-lg overflow-hidden">
<div className="bg-slate-100 px-3 py-2 border-b border-slate-200">
<p className="text-sm font-semibold text-slate-700">Investment Plans</p>
<p className="text-sm font-semibold text-slate-700">EV Investment Plans</p>
</div>
<div className="divide-y divide-slate-100">
{investor.investments?.map((inv: any) => {

View File

@@ -11,7 +11,7 @@ const inter = Inter({
export const metadata: Metadata = {
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",
appleWebApp: {
capable: true,

View File

@@ -77,7 +77,7 @@ export default function LandingPage() {
</h2>
<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
electric mobility ecosystem with FOCO model for investors.
electric mobility ecosystem with FICO model for investors.
</p>
</div>
@@ -106,7 +106,7 @@ export default function LandingPage() {
<Wallet className="w-6 h-6 text-green-500" />
</div>
<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">
Login as Investor <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>

View File

@@ -23,7 +23,8 @@ import {
LogOut,
Calculator,
Wrench,
Target, User, History, Bell
Target, User, History, Bell,
Building2
} from 'lucide-react';
import { getUserName, getUserRole, logout } from '@/lib/auth';
@@ -42,6 +43,7 @@ type NavItem = { label: string; href: string; icon: any; isNotification?: boolea
const adminNavItems: NavItem[] = [
{ label: 'Dashboard', href: '/admin', icon: BarChart3 },
{ label: 'Notifications', href: '/admin/notifications', icon: Bell, isNotification: true },
{ label: 'KYC Requests', href: '/admin/kyc', icon: Shield },
{ label: 'Rentals', href: '/admin/rentals', icon: FileText },
{ label: 'Bikers', href: '/admin/bikers', icon: Users },
@@ -52,13 +54,13 @@ const adminNavItems: NavItem[] = [
{ label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap },
{ 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: 'Hubs', href: '/admin/hub', icon: MapPin },
{ label: 'Reports', href: '/admin/reports', icon: BarChart3 },
{ label: 'Users Management', href: '/admin/users', icon: Users },
{ label: 'Roles & Permissions', href: '/admin/roles', icon: Shield },
{ label: 'Settings', href: '/admin/settings', icon: Settings },
];
const bikerNavItems: NavItem[] = [
@@ -89,9 +91,39 @@ export default function Sidebar() {
const [userName, setUserName] = useState('User');
const [userRole, setUserRole] = useState('admin');
const [adminUnreadCount, setAdminUnreadCount] = useState(5);
const [investorUnreadCount, setInvestorUnreadCount] = useState(2);
useEffect(() => {
setUserName(getUserName() || 'User');
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');
@@ -175,7 +207,9 @@ export default function Sidebar() {
<Icon className={`w-5 h-5 ${isActive ? 'text-white' : ''}`} />
<span>{item.label}</span>
{isNotification && (
<span className="ml-auto px-2 py-0.5 bg-accent text-white text-xs font-bold rounded-full">2</span>
<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>
);
@@ -183,7 +217,7 @@ export default function Sidebar() {
</nav>
<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">
<span className="text-sm font-bold text-accent">{userName.charAt(0).toUpperCase()}</span>
</div>
@@ -201,10 +235,10 @@ export default function Sidebar() {
<LogOut className="w-4 h-4 text-slate-400" />
</button>
</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 className="mt-1">v1.0.0</p>
</div>
</div> */}
</div>
</aside>

View File

@@ -96,7 +96,9 @@ export interface InvestmentPlan {
investorId: string;
planName: string;
planType: 'silver' | 'gold' | 'platinum' | 'diamond';
bikeIds: string[];
bikeIds?: string[];
batteryIds?: string[];
assetType?: 'bike' | 'battery';
totalInvestment: number;
monthlyReturn: number;
expectedRoi: number;
@@ -107,6 +109,9 @@ export interface InvestmentPlan {
paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque';
transactionId?: string;
notes?: string;
durationMonths?: number;
lockInMonths?: number;
exitPenaltyPercent?: number;
createdAt: string;
}
@@ -131,9 +136,10 @@ export interface Investor {
bankAccountNumber?: string;
bankBranch?: 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;
mobileBankingNumber?: string;
mobileBankingVerified?: boolean;
additionalMobileBanking?: { provider: string; number: string; verified: boolean }[];
emergencyContactName?: string;
emergencyContactRelation?: string;
@@ -363,7 +369,7 @@ export const investors: Investor[] = [
emergencyContactName: 'Fatema Begum',
emergencyContactRelation: 'Wife',
emergencyContactPhone: '01712345679',
totalInvested: 150000,
totalInvested: 300000,
totalEarnings: 114250,
activeBikes: 2,
withdrawalPending: 3000,
@@ -387,7 +393,8 @@ export const investors: Investor[] = [
referralEarnings: 2500,
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: '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[]> = {
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'],
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'],
staff: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'dashboard.view', 'rental.view', 'rental.create'],
accountant: ['dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete'],
investor: ['dashboard.view', 'kyc.request', 'kyc.view'],
biker: ['dashboard.view', 'kyc.request', 'kyc.view', 'rentals.view', 'rentals.create'],
'swap-station': ['dashboard.view', 'kyc.request', 'kyc.view'],
merchant: ['dashboard.view', 'kyc.request', 'kyc.view', 'merchants.view'],
super_admin: ALL_PERMISSIONS,
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',
'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',
'dashboard.view',
'rental.view', 'rental.create', 'rental.image_approve',
'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');