Compare commits

...

20 Commits

Author SHA1 Message Date
sazzadulalambd
fb1eff4931 feat: implement maintenance invoice creation flow with dynamic pricing, quantity editing, and PDF breakdown reporting 2026-05-17 00:45:04 +06:00
sazzadulalambd
0274e9a90b feat: enhance maintenance records with structured part management, service cost tracking, and dynamic invoice calculation 2026-05-17 00:23:08 +06:00
sazzadulalambd
48fd93fea8 feat: implement real-time rental map dashboard and integrate location tracking updates into admin modules 2026-05-16 22:34:44 +06:00
sazzadulalambd
1feab1fa23 feat: add live rental tracking map view with GPS coordinates and real-time refresh 2026-05-16 21:47:01 +06:00
sazzadulalambd
4c25990e70 feat: add support for battery swapping status and bike image gallery management in fleet details 2026-05-16 20:54:17 +06:00
sazzadulalambd
de9499b567 feat: enhance bike overview with detailed battery tracking and rental subscription data 2026-05-16 20:44:19 +06:00
sazzadulalambd
ec487f6d27 feat: add hub tracking to damage and maintenance records with selection UI 2026-05-16 20:19:23 +06:00
sazzadulalambd
36b12772b7 feat: include hub information in battery damage and maintenance records 2026-05-16 20:00:28 +06:00
sazzadulalambd
4b1ff96db2 feat: implement PWA install prompt banner for investor dashboard 2026-05-16 19:46:43 +06:00
sazzadulalambd
bd18c265ca feat: add damage and maintenance history tracking with CRUD functionality to battery detail page 2026-05-16 19:33:28 +06:00
sazzadulalambd
ce4bbfaf81 fix: include batteryRentPending in rental interface and sanitize pending rent calculation 2026-05-16 15:28:33 +06:00
sazzadulalambd
adbcded611 feat: add battery rent support and integrate adjusted battery costs into rental billing calculations 2026-05-16 15:09:19 +06:00
sazzadulalambd
21c408f828 feat: integrate battery selection and rental billing calculations into rental details view 2026-05-16 15:09:12 +06:00
sazzadulalambd
1882cfbb91 refactor: remove battery manual entry fields in biker profile and add battery rental history table to rental details 2026-05-16 14:06:33 +06:00
sazzadulalambd
41530a4691 feat: add manual BMS data refresh functionality and expand battery purchase form with accounting fields 2026-05-16 12:36:00 +06:00
sazzadulalambd
e932e6b817 refactor: remove RefreshCw icon and unused BMS refresh functionality from battery detail page 2026-05-16 12:20:18 +06:00
sazzadulalambd
b39f3981fc feat: implement battery editing functionality with modal and state management 2026-05-16 12:19:54 +06:00
sazzadulalambd
f5cd411a05 feat: implement battery management module with list and detail views 2026-05-16 12:10:49 +06:00
sazzadulalambd
62b8d567bd refactor: improve responsive layout and add payment history section to investment details page 2026-05-16 10:45:06 +06:00
sazzadulalambd
d8e82cef19 feat: add payment tracking and manual payment submission to investment details and configure standalone deployment mode 2026-05-16 10:20:12 +06:00
15 changed files with 4492 additions and 359 deletions

3
.gitignore vendored
View File

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

20
deploy.sh Executable file
View File

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

View File

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

19
server.js Normal file
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -9,7 +9,7 @@ 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
AlertCircle, Image as ImageIcon, Camera
} from 'lucide-react';
interface GPSDevice {
@@ -74,6 +74,8 @@ interface DamageRecord {
estimatedCost?: number;
actualCost?: number;
status: 'reported' | 'under_repair' | 'repaired' | 'claim_rejected';
hubId?: string;
hubName?: string;
images?: string[];
billImage?: string;
resolvedAt?: string;
@@ -89,9 +91,26 @@ interface MaintenanceRecord {
cost: number;
nextDueDate?: string;
status: 'scheduled' | 'in_progress' | 'completed';
hubId?: string;
hubName?: string;
notes?: string;
}
interface BatteryHistory {
id: string;
batteryId: string;
brand: string;
model: string;
serialNumber: string;
assignedDate: string;
returnedDate?: string;
swappedToBatteryId?: string;
status: 'active' | 'returned' | 'swapped';
socStart: number;
socEnd?: number;
monthlyRent?: number;
}
interface Bike {
id: string;
model: string;
@@ -100,10 +119,19 @@ interface Bike {
plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number;
location?: string; // deprecated - use hubId/hubName
currentBatteryId?: string;
currentBatteryBrand?: string;
currentBatteryModel?: string;
location?: string;
hubId?: string;
hubName?: string;
assignedTo?: string;
renterPhone?: string;
renterNid?: string;
rentalStartDate?: string;
subscriptionType?: 'daily' | 'weekly' | 'monthly';
weeklyRent?: number;
monthlyRent?: number;
investorId?: string;
investorName?: string;
purchaseDate?: string;
@@ -124,11 +152,18 @@ interface Bike {
assignmentHistory?: BikeAssignment[];
damageHistory?: DamageRecord[];
maintenanceHistory?: MaintenanceRecord[];
batteryHistory?: BatteryHistory[];
bikeImages?: {
front?: string;
back?: string;
left?: string;
right?: string;
};
}
const mockBikes: Bike[] = [
{
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15',
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, currentBatteryId: 'BAT-001', currentBatteryBrand: 'EVE Energy', currentBatteryModel: 'Li-Ion 60V50Ah', location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', renterPhone: '01712345678', renterNid: '1234567890', rentalStartDate: '2024-03-01', subscriptionType: 'weekly', weeklyRent: 2400, hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15',
gpsDevice: { id: 'GPS001', phone: '01712345601', imei: '861234567890123', lastActive: '2024-03-21 14:30', signal: 85, battery: 72 },
documents: [
{ type: 'registration', number: 'REG-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true },
@@ -149,14 +184,36 @@ const mockBikes: Bike[] = [
{ id: 'A005', action: 'Insurance Renewed', details: 'Insurance renewed for 1 year', date: '2024-01-15', by: 'Admin' },
],
damageHistory: [
{ id: 'DMG001', date: '2024-02-10', type: 'accident', description: 'Minor collision at Mirpur intersection', reportedBy: 'Jamal Khan', reportedAt: '2024-02-10 14:30', estimatedCost: 5000, actualCost: 4500, status: 'repaired', resolvedAt: '2024-02-15' },
{ id: 'DMG002', date: '2024-03-15', type: 'wear_tear', description: 'Front tire wear - replaced', reportedBy: 'Rahim Ahmed', reportedAt: '2024-03-15 09:00', estimatedCost: 2500, actualCost: 2200, status: 'repaired', resolvedAt: '2024-03-16' },
{ id: 'DMG001', date: '2024-02-10', type: 'accident', description: 'Minor collision at Mirpur intersection', reportedBy: 'Jamal Khan', reportedAt: '2024-02-10 14:30', estimatedCost: 5000, actualCost: 4500, status: 'repaired', resolvedAt: '2024-02-15', hubId: 'HUB-001', hubName: 'Gulshan Hub' },
{ id: 'DMG002', date: '2024-03-15', type: 'wear_tear', description: 'Front tire wear - replaced', reportedBy: 'Rahim Ahmed', reportedAt: '2024-03-15 09:00', estimatedCost: 2500, actualCost: 2200, status: 'repaired', resolvedAt: '2024-03-16', hubId: 'HUB-002', hubName: 'Banani Hub' },
],
maintenanceHistory: [
{ id: 'MNT001', date: '2024-03-01', type: 'routine', description: 'Full service - oil change, brake check, tire rotation', performedBy: 'Service Center', cost: 1500, nextDueDate: '2024-04-01', status: 'completed' },
{ id: 'MNT002', date: '2024-02-15', type: 'battery', description: 'Battery health check and terminal cleaning', performedBy: 'Service Center', cost: 500, nextDueDate: '2024-05-15', status: 'completed' },
{ id: 'MNT003', date: '2024-01-20', type: 'tire', description: 'Tire pressure check and inflation', performedBy: 'Service Center', cost: 300, nextDueDate: '2024-04-20', status: 'completed' },
]
{ id: 'MNT001', date: '2024-03-01', type: 'routine', description: 'Full service - oil change, brake check, tire rotation', performedBy: 'Service Center', cost: 1500, nextDueDate: '2024-04-01', status: 'completed', hubId: 'HUB-001', hubName: 'Gulshan Hub' },
{ id: 'MNT002', date: '2024-02-15', type: 'battery', description: 'Battery health check and terminal cleaning', performedBy: 'Service Center', cost: 500, nextDueDate: '2024-05-15', status: 'completed', hubId: 'HUB-003', hubName: 'Uttara Hub' },
{ id: 'MNT003', date: '2024-01-20', type: 'tire', description: 'Tire pressure check and inflation', performedBy: 'Service Center', cost: 300, nextDueDate: '2024-04-20', status: 'completed', hubId: 'HUB-004', hubName: 'Mirpur Hub' },
],
batteryHistory: [
{ id: 'BH001', batteryId: 'BAT-001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2024-00001', assignedDate: '2024-03-15', status: 'active', socStart: 85, monthlyRent: 1500 },
{ id: 'BH002', batteryId: 'BAT-005', brand: 'Samsung SDI', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2023-00045', assignedDate: '2024-01-10', returnedDate: '2024-03-14', status: 'returned', socStart: 80, socEnd: 45, monthlyRent: 1200 },
{ id: 'BH003', batteryId: 'BAT-008', brand: 'LG Chem', model: 'Li-Ion 48V40Ah', serialNumber: 'SN-2023-00012', assignedDate: '2023-11-05', returnedDate: '2024-01-09', status: 'swapped', socStart: 90, socEnd: 60, monthlyRent: 1000 },
{ id: 'BH004', batteryId: 'BAT-003', brand: 'Panasonic', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2023-00078', assignedDate: '2023-08-20', returnedDate: '2023-11-04', status: 'swapped', socStart: 88, socEnd: 55, monthlyRent: 1500 },
{ id: 'BH005', batteryId: 'BAT-002', brand: 'Sony', model: 'Li-Ion 48V35Ah', serialNumber: 'SN-2023-00034', assignedDate: '2023-05-15', returnedDate: '2023-08-19', status: 'returned', socStart: 75, socEnd: 40, monthlyRent: 900 },
{ id: 'BH006', batteryId: 'BAT-012', brand: 'BYD', model: 'LiFePO4 60V40Ah', serialNumber: 'SN-2023-00056', assignedDate: '2024-04-20', returnedDate: '2024-06-15', status: 'returned', socStart: 92, socEnd: 35, monthlyRent: 1300 },
{ id: 'BH007', batteryId: 'BAT-015', brand: 'CATL', model: 'Li-Ion 72V50Ah', serialNumber: 'SN-2024-00089', assignedDate: '2024-06-20', status: 'active', socStart: 88, monthlyRent: 1800 },
{ id: 'BH008', batteryId: 'BAT-009', brand: 'Tongsheng', model: 'Li-Ion 48V45Ah', serialNumber: 'SN-2023-00023', assignedDate: '2023-02-10', returnedDate: '2023-05-14', status: 'returned', socStart: 82, socEnd: 50, monthlyRent: 1100 },
{ id: 'BH009', batteryId: 'BAT-011', brand: 'Binek', model: 'Li-Ion 60V48Ah', serialNumber: 'SN-2023-00067', assignedDate: '2024-02-01', returnedDate: '2024-04-18', status: 'returned', socStart: 78, socEnd: 42, monthlyRent: 1400 },
{ id: 'BH010', batteryId: 'BAT-007', brand: 'Kexin', model: 'Li-Ion 48V36Ah', serialNumber: 'SN-2022-00045', assignedDate: '2022-12-05', returnedDate: '2023-02-08', status: 'returned', socStart: 85, socEnd: 55, monthlyRent: 850 },
{ id: 'BH011', batteryId: 'BAT-004', brand: 'Faraday', model: 'LiFePO4 48V42Ah', serialNumber: 'SN-2022-00089', assignedDate: '2022-09-15', returnedDate: '2022-12-04', status: 'returned', socStart: 90, socEnd: 48, monthlyRent: 1200 },
{ id: 'BH012', batteryId: 'BAT-006', brand: 'Reliance', model: 'Lead Acid 48V32Ah', serialNumber: 'SN-2022-00034', assignedDate: '2022-06-20', returnedDate: '2022-09-14', status: 'returned', socStart: 95, socEnd: 30, monthlyRent: 600 },
{ id: 'BH013', batteryId: 'BAT-020', brand: 'Maxell', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2024-00123', assignedDate: '2024-07-10', swappedToBatteryId: 'BAT-025', status: 'swapped', socStart: 75, socEnd: 65, monthlyRent: 1400 },
{ id: 'BH014', batteryId: 'BAT-018', brand: 'Nikola', model: 'LiFePO4 48V40Ah', serialNumber: 'SN-2023-00078', assignedDate: '2024-05-05', swappedToBatteryId: 'BAT-022', status: 'swapped', socStart: 82, socEnd: 55, monthlyRent: 1150 },
],
bikeImages: {
front: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
back: 'https://images.unsplash.com/photo-1591637333184-19aa84de3fbd?w=400&h=300&fit=crop',
left: 'https://images.unsplash.com/photo-1622185135505-2d795043906a?w=400&h=300&fit=crop',
right: 'https://images.unsplash.com/photo-1609630875171-b1321377ee53?w=400&h=300&fit=crop',
}
},
{
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01',
@@ -258,6 +315,13 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
{ value: 'other', label: 'Other' },
];
const mockHubs = [
{ id: 'HUB-001', name: 'Gulshan Hub' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const handleAddDamage = (damage: DamageRecord) => {
setBikes(bikes.map(b => {
if (b.id === bike.id) {
@@ -415,6 +479,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Hub</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Reported By</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Est. Cost</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actual Cost</th>
@@ -430,6 +495,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<span className="text-sm text-slate-700 capitalize">{damage.type.replace('_', ' ')}</span>
</td>
<td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{damage.description}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.hubName || '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.reportedBy}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.estimatedCost || 0}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{damage.actualCost || '-'}</td>
@@ -498,6 +564,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Hub</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Performed By</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Cost</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Next Due</th>
@@ -513,6 +580,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<span className="text-sm text-slate-700 capitalize">{maintenance.type}</span>
</td>
<td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{maintenance.description}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.hubName || '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.performedBy}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{maintenance.cost}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td>
@@ -591,6 +659,15 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
}
function OverviewTab({ bike }: { bike: Bike }) {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;
const batteryHistory = bike.batteryHistory || [];
const totalPages = Math.ceil(batteryHistory.length / itemsPerPage);
const paginatedHistory = batteryHistory.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
@@ -614,20 +691,38 @@ function OverviewTab({ bike }: { bike: Bike }) {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Plate Number</p>
<p className="font-semibold text-slate-700">{bike.plateNumber}</p>
<p className="text-xs text-slate-500">Bike</p>
<p className="font-semibold text-slate-700">{bike.brand} {bike.model}</p>
<p className="text-xs text-slate-500 mt-1">{bike.plateNumber}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Battery</p>
<p className={`font-semibold ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
<p className={`font-semibold text-lg ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
{bike.currentBatteryId && (
<p className="text-xs font-medium text-slate-700 mt-1">{bike.currentBatteryId}</p>
)}
{bike.currentBatteryBrand && (
<p className="text-xs text-slate-500">{bike.currentBatteryBrand} {bike.currentBatteryModel}</p>
)}
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Current Renter</p>
<p className="font-semibold text-slate-700">{bike.assignedTo || 'Available'}</p>
{bike.renterPhone && <p className="text-xs text-slate-500 mt-1">{bike.renterPhone}</p>}
{bike.rentalStartDate && <p className="text-xs text-slate-400 mt-1">Since: {bike.rentalStartDate}</p>}
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Daily Rate</p>
<p className="font-semibold text-green-600">{bike.currentRent || 0}</p>
<p className="text-xs text-slate-500">Subscription</p>
<p className="font-semibold text-slate-700 capitalize">{bike.subscriptionType || 'Daily'}</p>
{bike.subscriptionType === 'weekly' && bike.weeklyRent && (
<p className="text-xs text-green-600 mt-1">{bike.weeklyRent}/week</p>
)}
{bike.subscriptionType === 'monthly' && bike.monthlyRent && (
<p className="text-xs text-green-600 mt-1">{bike.monthlyRent}/month</p>
)}
{bike.subscriptionType === 'daily' && (
<p className="text-xs text-green-600 mt-1">{bike.currentRent || 0}/day</p>
)}
</div>
</div>
</div>
@@ -681,6 +776,91 @@ function OverviewTab({ bike }: { bike: Bike }) {
</div>
)}
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Battery History</h3>
{batteryHistory.length > 0 ? (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery ID</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Brand/Model</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned Date</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned/Swapped</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Start SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">End SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedHistory.map(bh => (
<tr key={bh.id} className="hover:bg-slate-50">
<td className="px-3 py-2 text-sm text-slate-700 font-medium">{bh.batteryId}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.brand} {bh.model}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.assignedDate}</td>
<td className="px-3 py-2 text-sm text-slate-600">
{bh.status === 'swapped' ? (
<>
<span className="text-blue-600 block">Swapped to {bh.swappedToBatteryId}</span>
<span className="text-blue-600 text-[10px] block opacity-75">{bh.returnedDate}</span>
</>
) : bh.returnedDate || '-'}
</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socStart}%</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socEnd ? `${bh.socEnd}%` : '-'}</td>
<td className="px-3 py-2 text-sm text-green-600">{bh.monthlyRent}</td>
<td className="px-3 py-2">
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bh.status === 'active' ? 'bg-green-100 text-green-700' : bh.status === 'swapped' ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'}`}>
{bh.status === 'active' ? 'Active' : bh.status === 'swapped' ? 'Swapped' : 'Returned'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-100">
<p className="text-sm text-slate-500">
Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, batteryHistory.length)} of {batteryHistory.length} batteries
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-1 text-sm rounded-lg ${currentPage === page ? 'bg-accent text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</>
) : (
<div className="text-center py-8 text-slate-500">
No battery history found
</div>
)}
</div>
</div>
);
}
@@ -768,9 +948,50 @@ function GPSTab({ bike }: { bike: Bike }) {
function DocumentsTab({ bike }: { bike: Bike }) {
const docs = bike.documents || [];
const [images, setImages] = useState(bike.bikeImages || { front: '', back: '', left: '', right: '' });
const [uploading, setUploading] = useState<string | null>(null);
const handleImageUpload = (view: 'front' | 'back' | 'left' | 'right', e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploading(view);
const reader = new FileReader();
reader.onloadend = () => {
setImages(prev => ({ ...prev, [view]: reader.result as string }));
setUploading(null);
};
reader.readAsDataURL(file);
}
};
return (
<div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Images</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{(['front', 'back', 'left', 'right'] as const).map(view => (
<div key={view} className="space-y-2">
<p className="text-xs font-medium text-slate-500 capitalize">{view} View</p>
<div className="relative aspect-video bg-slate-100 rounded-lg overflow-hidden border border-slate-200">
{images[view] ? (
<img src={images[view]} alt={`${view} view`} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center">
<Camera className="w-8 h-8 text-slate-300" />
</div>
)}
<label className="absolute inset-0 cursor-pointer hover:bg-black/20 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<span className="bg-white px-3 py-1 rounded-full text-xs font-medium text-slate-700 shadow">
{uploading === view ? 'Uploading...' : 'Upload'}
</span>
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(view, e)} />
</label>
</div>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3>
{docs.length === 0 ? (
@@ -1305,8 +1526,17 @@ function DamageModal({ bike, damage, onClose, onSave }: { bike: Bike; damage: Da
estimatedCost: damage?.estimatedCost || 0,
actualCost: damage?.actualCost || 0,
status: damage?.status || 'reported',
hubId: damage?.hubId || '',
hubName: damage?.hubName || '',
});
const mockHubs = [
{ id: 'HUB-001', name: 'Gulshan Hub' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const damageTypes = [
{ value: 'accident', label: 'Accident' },
{ value: 'theft', label: 'Theft' },
@@ -1379,6 +1609,19 @@ function DamageModal({ bike, damage, onClose, onSave }: { bike: Bike; damage: Da
required
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Hub</label>
<select
value={formData.hubId}
onChange={(e) => setFormData({ ...formData, hubId: e.target.value, hubName: mockHubs.find(h => h.id === e.target.value)?.name || '' })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select Hub</option>
{mockHubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Estimated Cost (৳)</label>
@@ -1435,8 +1678,17 @@ function MaintenanceModal({ bike, maintenance, onClose, onSave }: { bike: Bike;
cost: maintenance?.cost || 0,
nextDueDate: maintenance?.nextDueDate || '',
status: maintenance?.status || 'completed',
hubId: maintenance?.hubId || '',
hubName: maintenance?.hubName || '',
});
const mockHubs = [
{ id: 'HUB-001', name: 'Gulshan Hub' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const maintenanceTypes = [
{ value: 'routine', label: 'Routine Service' },
{ value: 'battery', label: 'Battery' },
@@ -1510,6 +1762,19 @@ function MaintenanceModal({ bike, maintenance, onClose, onSave }: { bike: Bike;
required
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Hub</label>
<select
value={formData.hubId}
onChange={(e) => setFormData({ ...formData, hubId: e.target.value, hubName: mockHubs.find(h => h.id === e.target.value)?.name || '' })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select Hub</option>
{mockHubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Cost (৳)</label>

View File

@@ -6,8 +6,10 @@ import {
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
CheckCircle, XCircle, ChevronLeft, Save, Printer, Send, QrCode,
Wallet, Building, Edit, MessageSquare, Calendar, ArrowLeft
Wallet, Building, Edit, MessageSquare, Calendar, ArrowLeft, Trash2,
Package, Settings
} from 'lucide-react';
import Link from 'next/link';
type TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense';
@@ -16,6 +18,16 @@ type DamageSeverity = 'critical' | 'major' | 'minor' | 'cosmetic';
type MaintenanceStatus = 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
type PaymentStatus = 'pending' | 'approved' | 'paid' | 'rejected';
interface PartUsed {
id: string;
partId: string;
partName: string;
quantity: number;
unitPrice: number;
totalPrice: number;
addedAt: string;
}
interface MaintenanceRecord {
id: string;
date: string;
@@ -35,7 +47,8 @@ interface MaintenanceRecord {
location: string;
estimatedCost: number;
actualCost?: number;
partsUsed?: string[];
serviceCost?: number;
partsUsed?: PartUsed[];
images: { id: string; name: string; url: string; uploadedAt: string }[];
assignedTo?: string;
notes: string[];
@@ -44,6 +57,30 @@ interface MaintenanceRecord {
createdBy: string;
}
interface EVPart {
id: string;
name: string;
buyingPrice: number;
sellingPrice: number;
category: string;
inStock: boolean;
}
const mockParts: EVPart[] = [
{ id: 'PRT-001', name: 'Front fender', buyingPrice: 800, sellingPrice: 1500, category: 'Body Parts', inStock: true },
{ id: 'PRT-002', name: 'Rear fender', buyingPrice: 900, sellingPrice: 1600, category: 'Body Parts', inStock: true },
{ id: 'PRT-003', name: 'Mounting brackets', buyingPrice: 400, sellingPrice: 800, category: 'Hardware', inStock: true },
{ id: 'PRT-004', name: 'Brake pads', buyingPrice: 350, sellingPrice: 600, category: 'Brakes', inStock: true },
{ id: 'PRT-005', name: 'Front wheel', buyingPrice: 2500, sellingPrice: 4000, category: 'Wheels', inStock: true },
{ id: 'PRT-006', name: 'Rear wheel', buyingPrice: 2200, sellingPrice: 3500, category: 'Wheels', inStock: true },
{ id: 'PRT-007', name: 'Motor controller', buyingPrice: 3500, sellingPrice: 5500, category: 'Electrical', inStock: true },
{ id: 'PRT-008', name: 'Display unit', buyingPrice: 1200, sellingPrice: 2000, category: 'Electrical', inStock: true },
{ id: 'PRT-009', name: 'Throttle', buyingPrice: 450, sellingPrice: 750, category: 'Controls', inStock: true },
{ id: 'PRT-010', name: 'Handlebar', buyingPrice: 800, sellingPrice: 1400, category: 'Controls', inStock: true },
{ id: 'PRT-011', name: 'Seat', buyingPrice: 600, sellingPrice: 1000, category: 'Comfort', inStock: true },
{ id: 'PRT-012', name: 'Side stand', buyingPrice: 250, sellingPrice: 450, category: 'Hardware', inStock: true },
];
const mockMaintenance: MaintenanceRecord[] = [
{
id: 'MNT-001',
@@ -64,7 +101,16 @@ const mockMaintenance: MaintenanceRecord[] = [
location: 'Gulshan, Dhaka',
estimatedCost: 3500,
actualCost: 3200,
partsUsed: ['Front fender', 'Mounting brackets'],
partsUsed: [
{ id: 'PU-001', partId: 'PRT-001', partName: 'Front fender', quantity: 1, unitPrice: 1500, totalPrice: 1500, addedAt: '2024-03-21' },
{ id: 'PU-002', partId: 'PRT-003', partName: 'Mounting brackets', quantity: 2, unitPrice: 800, totalPrice: 1600, addedAt: '2024-03-21' },
{ id: 'PU-003', partId: 'PRT-004', partName: 'Brake pads', quantity: 2, unitPrice: 600, totalPrice: 1200, addedAt: '2024-03-21' },
{ id: 'PU-004', partId: 'PRT-005', partName: 'Front wheel', quantity: 1, unitPrice: 4000, totalPrice: 4000, addedAt: '2024-03-21' },
{ id: 'PU-005', partId: 'PRT-009', partName: 'Throttle', quantity: 1, unitPrice: 750, totalPrice: 750, addedAt: '2024-03-21' },
{ id: 'PU-006', partId: 'PRT-011', partName: 'Seat', quantity: 1, unitPrice: 1000, totalPrice: 1000, addedAt: '2024-03-21' },
{ id: 'PU-007', partId: 'PRT-010', partName: 'Handlebar', quantity: 1, unitPrice: 1250, totalPrice: 1250, addedAt: '2024-03-21' },
],
serviceCost: 3200,
images: [
{ id: 'img1', name: 'Damage Front', url: '', uploadedAt: '2024-03-21' },
{ id: 'img2', name: 'Damage Side', url: '', uploadedAt: '2024-03-21' },
@@ -237,9 +283,19 @@ export default function MaintenanceDetailPage() {
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
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);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<'online' | 'offline' | 'later' | null>(null);
const [showPaymentSuccess, setShowPaymentSuccess] = useState(false);
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
const [newNoteText, setNewNoteText] = useState('');
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);
@@ -286,18 +342,22 @@ export default function MaintenanceDetailPage() {
const handlePayment = (source: 'bank' | 'cash' | 'biker') => {
if (!record) return;
const cost = record.actualCost || record.estimatedCost;
const cost = ((record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0) + (record.serviceCost || 0)) + invoiceData.tips - invoiceData.discount;
setRecord(prev => prev ? { ...prev, paymentStatus: 'paid' } : null);
setRecord(prev => prev ? { ...prev, paymentStatus: 'paid', actualCost: cost, status: 'completed', resolvedAt: new Date().toISOString().split('T')[0] } : null);
setShowPaymentModal(false);
setShowInvoiceModal(true);
setShowPaymentSuccess(true);
};
const handleGenerateInvoice = () => {
if (!record) return;
import('jspdf').then(jsPDF => {
const doc = new jsPDF.default();
const cost = record.actualCost || record.estimatedCost;
const partsTotal = record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0;
const serviceCost = record.serviceCost || 0;
const subtotal = partsTotal + serviceCost;
const total = subtotal + invoiceData.tips - invoiceData.discount;
const cost = total || record.estimatedCost;
const qrData = `INV-${record.id}|${record.bikePlate}|${record.type}|${cost}|${new Date().toISOString().split('T')[0]}`;
doc.setFontSize(20);
@@ -348,11 +408,22 @@ export default function MaintenanceDetailPage() {
doc.setFontSize(11);
doc.text('Cost Breakdown', 20, 175);
doc.setFontSize(10);
doc.text(`Estimated Cost: ৳${record.estimatedCost}`, 20, 181);
if (record.actualCost) doc.text(`Actual Cost: ৳${record.actualCost}`, 20, 187);
if (record.partsUsed && record.partsUsed.length > 0) {
doc.text(`Parts: ${record.partsUsed.join(', ')}`, 20, 193);
doc.text(`Parts Total: ৳${partsTotal.toLocaleString()}`, 20, 181);
doc.text(`Service Cost (Labor): ৳${serviceCost.toLocaleString()}`, 20, 187);
doc.text(`Subtotal: ৳${subtotal.toLocaleString()}`, 20, 193);
if (invoiceData.tips > 0) {
doc.setTextColor(22, 163, 74);
doc.text(`Tips: +৳${invoiceData.tips.toLocaleString()}`, 20, 199);
doc.setTextColor(100);
}
if (invoiceData.discount > 0) {
doc.setTextColor(220, 38, 38);
doc.text(`Discount: -৳${invoiceData.discount.toLocaleString()}`, 20, 205);
doc.setTextColor(100);
}
doc.setFontSize(12);
doc.setTextColor(0);
doc.text(`Total: ৳${total.toLocaleString()}`, 20, 215);
doc.setFontSize(14);
doc.setTextColor(6, 95, 70);
@@ -375,6 +446,8 @@ export default function MaintenanceDetailPage() {
setShowAddNoteModal(false);
};
const invoiceTotal = record ? ((record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0) + (record.serviceCost || 0)) + invoiceData.tips - invoiceData.discount : 0;
return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
<button
@@ -414,20 +487,47 @@ export default function MaintenanceDetailPage() {
</>
) : (
<>
{!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' && (
)}
{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"
>
<Check className="w-4 h-4" /> Complete
<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)}
@@ -438,10 +538,10 @@ export default function MaintenanceDetailPage() {
)}
{record.paymentStatus === 'paid' && (
<button
onClick={() => setShowInvoiceModal(true)}
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" /> Invoice
<Printer className="w-4 h-4" /> Print Invoice
</button>
)}
</>
@@ -538,142 +638,6 @@ export default function MaintenanceDetailPage() {
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
<FileText className="w-5 h-5" /> Description
</h3>
{editMode ? (
<textarea
value={editForm.description || ''}
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm"
rows={3}
/>
) : (
<>
<p className="text-sm text-amber-700 mb-3">{record.description}</p>
<div className="flex items-center gap-2 text-xs text-amber-600">
<MapPin className="w-3 h-3" /> {record.location}
</div>
</>
)}
</div>
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Cost Details
</h3>
<div className="grid grid-cols-2 gap-3">
<div className="bg-white p-3 rounded-lg">
<p className="text-xs text-purple-600">Estimated</p>
{editMode ? (
<input
type="number"
value={editForm.estimatedCost || 0}
onChange={(e) => setEditForm({ ...editForm, estimatedCost: parseInt(e.target.value) })}
className="w-full px-2 py-1 border border-purple-200 rounded text-sm"
/>
) : (
<p className="text-lg font-bold text-purple-800">{record.estimatedCost}</p>
)}
</div>
<div className="bg-white p-3 rounded-lg">
<p className="text-xs text-purple-600">Actual</p>
<p className="text-lg font-bold text-purple-800">{record.actualCost || record.estimatedCost}</p>
</div>
</div>
</div>
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
<User className="w-5 h-5" /> Assigned To
</h3>
{editMode ? (
<select
value={editForm.assignedTo || ''}
onChange={(e) => setEditForm({ ...editForm, assignedTo: e.target.value })}
className="w-full px-3 py-2 border border-cyan-200 rounded-lg text-sm"
>
<option value="">Select Service Center</option>
<option value="Service Center A">Service Center A</option>
<option value="Service Center B">Service Center B</option>
<option value="Authorized Service Center">Authorized Service Center</option>
<option value="Gulshan Hub">Gulshan Hub</option>
<option value="Banani Hub">Banani Hub</option>
<option value="Dhanmondi Hub">Dhanmondi Hub</option>
</select>
) : (
<p className="text-sm text-cyan-700">{record.assignedTo || 'Not assigned'}</p>
)}
</div>
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
<Wrench className="w-5 h-5" /> Parts Used
</h3>
{editMode && (
<select
onChange={(e) => {
if (e.target.value) {
const currentParts = editForm.partsUsed || [];
if (!currentParts.includes(e.target.value)) {
setEditForm({ ...editForm, partsUsed: [...currentParts, e.target.value] });
}
e.target.value = '';
}
}}
className="px-2 py-1 text-xs border border-orange-200 rounded"
>
<option value="">+ Add Part</option>
<option value="Front fender">Front fender</option>
<option value="Rear fender">Rear fender</option>
<option value="Mirror">Mirror</option>
<option value="Headlight">Headlight</option>
<option value="Tail light">Tail light</option>
<option value="Brake pad">Brake pad</option>
<option value="Brake shoe">Brake shoe</option>
<option value="Chain">Chain</option>
<option value="Battery">Battery</option>
<option value="Motor">Motor</option>
<option value="Controller">Controller</option>
<option value="Throttle">Throttle</option>
<option value="Lever">Lever</option>
<option value="Stand">Stand</option>
<option value="Seat">Seat</option>
<option value="Tyre">Tyre</option>
<option value="Tube">Tube</option>
<option value="Mounting brackets">Mounting brackets</option>
<option value="Bolt set">Bolt set</option>
</select>
)}
</div>
<div className="flex flex-wrap gap-2">
{(editMode ? editForm.partsUsed : record.partsUsed)?.map((part, idx) => (
<span key={idx} className="px-3 py-1 bg-white rounded-full text-sm text-orange-700 border border-orange-200 flex items-center gap-1">
{part}
{editMode && (
<button
onClick={() => {
const updated = [...(editForm.partsUsed || [])];
updated.splice(idx, 1);
setEditForm({ ...editForm, partsUsed: updated });
}}
className="ml-1 text-orange-400 hover:text-red-500"
>
×
</button>
)}
</span>
))}
{(editMode ? editForm.partsUsed : record.partsUsed)?.length === 0 && (
<p className="text-sm text-orange-400">No parts added</p>
)}
</div>
</div>
<div className="bg-indigo-50 p-4 rounded-xl border border-indigo-100">
<h3 className="font-semibold text-indigo-800 mb-3 flex items-center gap-2">
@@ -709,6 +673,193 @@ export default function MaintenanceDetailPage() {
)}
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
<FileText className="w-5 h-5" /> Description
</h3>
{editMode ? (
<textarea
value={editForm.description || ''}
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm"
rows={3}
/>
) : (
<>
<p className="text-sm text-amber-700 mb-3">{record.description}</p>
<div className="flex items-center gap-2 text-xs text-amber-600">
<MapPin className="w-3 h-3" /> {record.location}
</div>
</>
)}
</div>
</div>
<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">
@@ -762,34 +913,90 @@ export default function MaintenanceDetailPage() {
{showCompleteModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100">
<h3 className="font-semibold text-slate-800">Complete Maintenance</h3>
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
<div className="p-4 border-b border-slate-100 bg-gradient-to-r from-green-50 to-emerald-50">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<FileText className="w-5 h-5 text-green-600" /> {invoiceCreated ? 'Edit Invoice' : 'Create Invoice'}
</h3>
<p className="text-xs text-slate-500">{record.id} {record.bikeModel}</p>
</div>
<div className="p-4 space-y-4">
<div className="bg-green-50 p-4 rounded-lg">
<p className="text-sm text-green-700">Enter actual cost to complete this record</p>
<div 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>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm font-medium text-slate-600 mb-2 block">Actual Cost ()</label>
<label className="text-xs font-medium text-slate-500 mb-1 block">Add Tips ()</label>
<input
type="number"
value={actualCost}
onChange={(e) => setActualCost(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-lg font-bold"
min="0"
value={invoiceData.tips}
onChange={(e) => setInvoiceData({ ...invoiceData, tips: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="0"
/>
</div>
<div className="text-sm text-slate-500">
Estimated: {record.estimatedCost}
<div>
<label className="text-xs font-medium text-slate-500 mb-1 block">Discount ()</label>
<input
type="number"
min="0"
value={invoiceData.discount}
onChange={(e) => setInvoiceData({ ...invoiceData, discount: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="0"
/>
</div>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-100">
<div className="flex justify-between items-center mb-2">
<span className="text-slate-600">Subtotal:</span>
<span className="font-medium text-slate-700">{((record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0) + (record.serviceCost || 0)).toLocaleString()}</span>
</div>
{invoiceData.tips > 0 && (
<div className="flex justify-between items-center mb-2">
<span className="text-slate-500">Tips:</span>
<span className="text-green-600">+{invoiceData.tips.toLocaleString()}</span>
</div>
)}
{invoiceData.discount > 0 && (
<div className="flex justify-between items-center mb-2">
<span className="text-slate-500">Discount:</span>
<span className="text-red-500">-{invoiceData.discount.toLocaleString()}</span>
</div>
)}
<div className="border-t border-green-200 pt-2 mt-2">
<div className="flex justify-between items-center">
<span className="font-bold text-green-800">Total Amount:</span>
<span className="text-2xl font-bold text-green-700">{(((record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0) + (record.serviceCost || 0)) + invoiceData.tips - invoiceData.discount).toLocaleString()}</span>
</div>
</div>
</div>
<p className="text-xs text-slate-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" /> After payment, this maintenance will be marked as completed
</p>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setShowCompleteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
<button onClick={() => { setShowCompleteModal(false); setInvoiceData({ tips: 0, discount: 0 }); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
<button
onClick={handleComplete}
onClick={() => { setInvoiceCreated(true); setShowCompleteModal(false); }}
className="px-4 py-2 bg-green-600 text-white rounded-lg flex items-center gap-2"
>
<Check className="w-4 h-4" /> Mark Complete
<FileText className="w-4 h-4" /> {invoiceCreated ? 'Update Invoice' : 'Create Invoice'}
</button>
</div>
</div>
@@ -799,9 +1006,14 @@ export default function MaintenanceDetailPage() {
{showPaymentModal && record && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
<div className="p-4 border-b border-slate-100">
<h3 className="font-semibold text-slate-800">Process Payment - {record.id}</h3>
<p className="text-sm text-slate-500">Amount: {record.actualCost || record.estimatedCost}</p>
<div className="p-4 border-b border-slate-100 bg-blue-50">
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
<Wallet className="w-5 h-5 text-blue-600" /> Process Payment
</h3>
<div className="mt-2 bg-white p-3 rounded-lg border border-blue-100">
<p className="text-xs text-slate-500">Invoice Total</p>
<p className="text-2xl font-bold text-blue-700">{invoiceTotal.toLocaleString()}</p>
</div>
</div>
<div className="p-4 space-y-4">
<p className="text-sm text-slate-600 mb-2">Select payment method:</p>
@@ -828,22 +1040,26 @@ export default function MaintenanceDetailPage() {
<Wallet className="w-5 h-5 text-green-600" />
</div>
<div className="text-left">
<p className="font-medium text-slate-800">Cash</p>
<p className="text-xs text-slate-500">Debit Cash (1100) Credit Maintenance (5400)</p>
<p className="font-medium text-slate-800">Offline (Cash)</p>
<p className="text-xs text-slate-500">Pay with cash</p>
</div>
</div>
</button>
<button
onClick={() => handlePayment('biker')}
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-purple-500 hover:bg-purple-50 transition-colors"
onClick={() => {
if (!record) return;
setRecord(prev => prev ? { ...prev, paymentStatus: 'pending' } : null);
setShowPaymentModal(false);
}}
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-orange-500 hover:bg-orange-50 transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<User className="w-5 h-5 text-purple-600" />
<div 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 className="text-left">
<p className="font-medium text-slate-800">Biker Wallet</p>
<p className="text-xs text-slate-500">Deduct from rider wallet</p>
<p className="font-medium text-slate-800">Pay Later</p>
<p className="text-xs text-slate-500">Mark as pending payment</p>
</div>
</div>
</button>
@@ -936,6 +1152,202 @@ export default function MaintenanceDetailPage() {
</div>
</div>
)}
{showAddPartModal && (
<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 flex items-center gap-2">
<Package className="w-5 h-5 text-orange-600" /> Add Part
</h3>
<button onClick={() => setShowAddPartModal(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">Select Part from EV Parts</label>
<p className="text-xs text-slate-400 mb-2">Prices are based on selling price from Settings</p>
<div className="relative mb-3">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search parts..."
value={partSearch}
onChange={(e) => setPartSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div className="max-h-48 overflow-y-auto border border-slate-200 rounded-lg">
{mockParts.filter(p => p.name.toLowerCase().includes(partSearch.toLowerCase())).map(part => (
<div
key={part.id}
onClick={() => { setSelectedPart(part); setPartQuantity(1); }}
className={`p-3 border-b border-slate-100 cursor-pointer hover:bg-orange-50 ${selectedPart?.id === part.id ? 'bg-orange-50 border-orange-300' : ''}`}
>
<div className="flex justify-between items-center">
<div>
<p className="font-medium text-slate-700">{part.name}</p>
<p className="text-xs text-slate-500">{part.category}</p>
</div>
<div className="text-right">
<p className="font-bold text-orange-600">{part.sellingPrice.toLocaleString()}</p>
<p className="text-xs text-slate-400">per unit</p>
</div>
</div>
</div>
))}
</div>
</div>
{selectedPart && (
<div className="bg-orange-50 p-4 rounded-lg border border-orange-200">
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="text-xs font-medium text-slate-500 mb-1 block">Quantity</label>
<input
type="number"
min="1"
value={partQuantity}
onChange={(e) => setPartQuantity(Math.max(1, parseInt(e.target.value) || 1))}
className="w-full px-3 py-2 border border-orange-200 rounded-lg text-sm"
/>
</div>
<div className="flex-1">
<label className="text-xs font-medium text-slate-500 mb-1 block">Unit Price</label>
<p className="text-lg font-bold text-orange-700">{selectedPart.sellingPrice}</p>
</div>
<div className="flex-1">
<label className="text-xs font-medium text-slate-500 mb-1 block">Total</label>
<p className="text-lg font-bold text-orange-700">{(selectedPart.sellingPrice * partQuantity).toLocaleString()}</p>
</div>
</div>
</div>
)}
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setShowAddPartModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
<button
onClick={() => {
if (selectedPart) {
const newPart: PartUsed = {
id: 'PU-' + Date.now(),
partId: selectedPart.id,
partName: selectedPart.name,
quantity: partQuantity,
unitPrice: selectedPart.sellingPrice,
totalPrice: selectedPart.sellingPrice * partQuantity,
addedAt: new Date().toISOString().split('T')[0]
};
setRecord(prev => prev ? { ...prev, partsUsed: [...(prev.partsUsed || []), newPart] } : null);
setShowAddPartModal(false);
setSelectedPart(null);
}
}}
disabled={!selectedPart}
className="px-4 py-2 bg-orange-600 text-white rounded-lg disabled:opacity-50 flex items-center gap-2"
>
<Plus className="w-4 h-4" /> Add Part
</button>
</div>
</div>
</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">
<div className="p-8 text-center">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-12 h-12 text-green-600" />
</div>
<h3 className="text-2xl font-bold text-green-600 mb-2">Payment Successful!</h3>
<p className="text-slate-600 mb-4">Maintenance has been completed</p>
<div className="bg-slate-50 p-4 rounded-lg text-left mb-6">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Maintenance ID</span>
<span className="text-sm font-bold text-slate-700">{record?.id}</span>
</div>
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Bike</span>
<span className="text-sm font-medium text-slate-700">{record?.bikeModel}</span>
</div>
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Status</span>
<span className="text-sm font-bold text-green-600">Completed</span>
</div>
<div className="flex justify-between pt-2 border-t border-slate-200">
<span className="text-sm font-semibold text-slate-700">Total Paid</span>
<span className="text-lg font-bold text-green-600">{((record?.partsUsed?.reduce((s, p) => s + p.totalPrice, 0) || 0) + (record?.serviceCost || 0) + invoiceData.tips - invoiceData.discount).toLocaleString()}</span>
</div>
</div>
<button
onClick={() => { setShowPaymentSuccess(false); setInvoiceData({ tips: 0, discount: 0 }); }}
className="px-6 py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700"
>
Done
</button>
</div>
</div>
</div>
)}
</div>
);
}

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
Phone, MessageCircle, Play, Check, X, FileText, Download, Battery
} from 'lucide-react';
import {
canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit,
@@ -41,6 +41,9 @@ interface Rental {
bikeModel: string;
bikePlate: string;
bikeBattery: number;
batteryId?: string;
batteryName?: string;
batteryRent?: number;
type: RentalType;
status: RentalStatus;
startDate: string;
@@ -73,6 +76,17 @@ interface Rental {
activatedAt?: string;
lockHistory?: LockEvent[];
paymentHistory?: PaymentHistory[];
batteryHistory?: BatteryRentalHistory[];
}
interface BatteryRentalHistory {
id: string;
batteryId: string;
batteryName: string;
assignedAt: string;
returnedAt?: string;
monthlyRent: number;
status: 'active' | 'returned';
}
interface LockEvent {
@@ -114,6 +128,7 @@ const mockRentals: Rental[] = [
dailyRate: 150,
weeklyRate: 900,
monthlyRate: 3500,
batteryRent: 1500,
totalPaid: 38500,
dueRental: 0,
pendingRent: 0,
@@ -146,6 +161,10 @@ const mockRentals: Rental[] = [
{ id: 'lh3', action: 'locked', reason: 'Second payment overdue - Day 2 penalty', performedBy: 'System', performedAt: '2024-03-01' },
{ id: 'lh4', action: 'unlocked', reason: 'Payment cleared', performedBy: 'Admin Manager', performedAt: '2024-03-02' },
],
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: 'RNT-002',
@@ -269,6 +288,23 @@ const mockHubs = [
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
interface BatteryOption {
id: string;
brand: string;
model: string;
soc: number;
monthlyRent: number;
status: 'available' | 'in-use';
}
const mockBatteries: BatteryOption[] = [
{ id: 'BAT-DH-001', brand: 'Galaxy', model: '72V 45Ah', soc: 85, monthlyRent: 1500, status: 'available' },
{ id: 'BAT-DH-002', brand: 'Titan', model: '72V 50Ah', soc: 92, monthlyRent: 1800, status: 'available' },
{ id: 'BAT-DH-003', brand: 'PowerMax', model: '60V 40Ah', soc: 78, monthlyRent: 1200, status: 'available' },
{ id: 'BAT-DH-004', brand: 'UltraCell', model: '72V 55Ah', soc: 88, monthlyRent: 2000, status: 'available' },
{ id: 'BAT-DH-005', brand: 'EcoVolt', model: '48V 30Ah', soc: 65, monthlyRent: 800, status: 'available' },
];
const mockDamageHistory = [
{ id: 'DMG-001', date: '2024-02-10', description: 'Minor scratch on left mirror', severity: 'minor', status: 'resolved' },
{ id: 'DMG-002', date: '2024-03-05', description: 'Front fender dented', severity: 'moderate', status: 'reported' },
@@ -316,6 +352,8 @@ export default function RentalDetailPage() {
const [documents, setDocuments] = useState(mockDocuments);
const [showUploadModal, setShowUploadModal] = useState(false);
const [uploadDocName, setUploadDocName] = useState('');
const [showAddBatteryModal, setShowAddBatteryModal] = useState(false);
const [selectedBatteryId, setSelectedBatteryId] = useState('');
const [acceptPermission, setAcceptPermission] = useState(false);
const [rejectPermission, setRejectPermission] = useState(false);
@@ -468,6 +506,31 @@ export default function RentalDetailPage() {
setRental(prev => prev ? { ...prev, status: 'active', activatedAt: new Date().toISOString().split('T')[0] } : null);
};
const handleAddBattery = () => {
if (!selectedBatteryId || !rental) return;
const battery = mockBatteries.find(b => b.id === selectedBatteryId);
if (!battery) return;
const newBatteryHistory: BatteryRentalHistory = {
id: `BAT-RENT-${Date.now()}`,
batteryId: battery.id,
batteryName: `${battery.brand} ${battery.model}`,
assignedAt: new Date().toISOString().split('T')[0],
monthlyRent: battery.monthlyRent,
status: 'active',
};
setRental(prev => prev ? {
...prev,
batteryId: battery.id,
batteryName: `${battery.brand} ${battery.model}`,
batteryRent: (prev.batteryRent || 0) + battery.monthlyRent,
batteryHistory: [...(prev.batteryHistory || []), newBatteryHistory],
} : null);
setShowAddBatteryModal(false);
setSelectedBatteryId('');
};
const handleAddNote = () => {
if (!newNote.trim()) return;
setNotes(prev => [...prev, { id: `n${Date.now()}`, text: newNote, createdAt: new Date().toISOString().split('T')[0] }]);
@@ -684,12 +747,36 @@ export default function RentalDetailPage() {
<div className="space-y-2">
<div className="flex justify-between"><span className="text-sm text-slate-600">Type</span><span className="text-sm font-medium text-slate-800 capitalize">{rental.subscriptionType}</span></div>
<div className="flex justify-between">
<span className="text-sm text-slate-600">Rate</span>
<span className="text-sm text-slate-600">Bike Rate</span>
<span className="text-sm font-medium text-slate-800">
{rental.subscriptionType === 'daily' ? rental.dailyRate : rental.subscriptionType === 'weekly' ? rental.weeklyRate : rental.monthlyRate}/
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
</span>
</div>
{rental.batteryRent && rental.batteryRent > 0 && (
<>
<div className="flex justify-between">
<span className="text-sm text-amber-600">Battery Rate</span>
<span className="text-sm font-medium text-amber-700">
{Math.round(rental.batteryRent / (rental.subscriptionType === 'daily' ? 30 : rental.subscriptionType === 'weekly' ? 4 : 1))}/
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
</span>
</div>
<div className="pt-2 mt-2 border-t border-slate-100">
<div className="flex justify-between">
<span className="text-sm font-medium text-emerald-700">Total (Bike + Battery)</span>
<span className="text-sm font-bold text-emerald-700">
{rental.subscriptionType === 'daily'
? rental.dailyRate + Math.round(rental.batteryRent / 30)
: rental.subscriptionType === 'weekly'
? rental.weeklyRate + Math.round(rental.batteryRent / 4)
: rental.monthlyRate + rental.batteryRent}/
{rental.subscriptionType === 'daily' ? 'day' : rental.subscriptionType === 'weekly' ? 'week' : 'month'}
</span>
</div>
</div>
</>
)}
</div>
</div>
@@ -710,6 +797,124 @@ export default function RentalDetailPage() {
</div>
</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">
<Battery className="w-5 h-5 text-amber-500" /> Battery Rental History
</h3>
<button onClick={() => setShowAddBatteryModal(true)} className="px-3 py-1.5 bg-amber-600 text-white rounded-lg text-xs font-medium hover:bg-amber-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Battery
</button>
</div>
{rental.batteryHistory && rental.batteryHistory.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery 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">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">Monthly Rent</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{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 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">
<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>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-sm text-slate-500">No battery assigned yet.</p>
)}
{rental.batteryHistory?.some(b => b.status === 'active') && (
<div className="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<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
</p>
</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">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div>
<label className="text-sm text-slate-600 mb-1 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>
{mockBatteries.map(bat => (
<option key={bat.id} value={bat.id}>{bat.brand} {bat.model} - SOC: {bat.soc}% - Rent: {bat.monthlyRent}/month</option>
))}
</select>
</div>
{selectedBatteryId && (() => {
const batteryMonthlyRent = mockBatteries.find(b => b.id === selectedBatteryId)?.monthlyRent || 0;
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>
<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>
</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">
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>
</div>
</div>
</div>
</div>
)}
{/* Initial Condition Images */}
{/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */}
{rental.initialImages && (

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
'use client';
import { useState, useEffect } from 'react';
import { investors, bikes, transactions } from '@/data/mockData';
import { Wallet, TrendingUp, Bike, Target, DollarSign, FileText, Phone, BarChart3, Clock, ArrowRight, ShieldCheck, Zap, AlertCircle } from 'lucide-react';
import { Wallet, TrendingUp, Bike, Target, DollarSign, FileText, Phone, BarChart3, Clock, ArrowRight, ShieldCheck, Zap, AlertCircle, Download, X, ExternalLink } from 'lucide-react';
import Link from 'next/link';
import TransactionList from '@/components/TransactionList';
import InvestorNotification from '@/components/InvestorNotification';
@@ -11,6 +14,40 @@ export default function InvestorDashboardPage() {
const availableBalance = investor.totalEarnings - investor.totalWithdrawn - investor.withdrawalPending;
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const [showInstallBanner, setShowInstallBanner] = useState(false);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
setShowInstallBanner(true);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setDeferredPrompt(null);
}
setShowInstallBanner(false);
};
const dismissBanner = () => {
setShowInstallBanner(false);
localStorage.setItem('pwa_install_dismissed', 'true');
};
useEffect(() => {
if (localStorage.getItem('pwa_install_dismissed') === 'true') {
setShowInstallBanner(false);
}
}, []);
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
@@ -82,6 +119,29 @@ export default function InvestorDashboardPage() {
</div>
</div>
{/* PWA Install Banner */}
{showInstallBanner && (
<div className="bg-gradient-to-r from-investor/10 to-purple-50 border border-investor/20 rounded-2xl p-4 mb-6 animate-in fade-in slide-in-from-bottom-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-investor to-investor-light rounded-xl flex items-center justify-center shrink-0 shadow-lg">
<Download className="w-6 h-6 text-white" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-slate-800">Install JML Investor App</p>
<p className="text-xs text-slate-500 mt-0.5">Add to home screen for quick access & offline support</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={dismissBanner} className="p-2 hover:bg-slate-100 rounded-lg text-slate-400 transition-colors">
<X className="w-5 h-5" />
</button>
<button onClick={handleInstall} className="px-4 py-2 bg-investor text-white rounded-lg font-bold text-sm hover:bg-investor-dark transition-colors flex items-center gap-2 shadow-sm">
<ExternalLink className="w-4 h-4" /> Install
</button>
</div>
</div>
</div>
)}
<div className="grid lg:grid-cols-3 gap-6 mb-6">
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">

View File

@@ -13,6 +13,16 @@ import { investors } from '@/data/mockData';
import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
interface PaymentRecord {
id: string;
date: string;
amount: number;
installmentNo: number | null;
type: 'full' | 'partial' | 'installment';
method: string;
status: 'completed' | 'pending';
}
export default function InvestorInvestmentDetailPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params);
const { id: investmentId } = resolvedParams;
@@ -23,7 +33,12 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
const [activeTab, setActiveTab] = useState('overview');
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [selectedInstallment, setSelectedInstallment] = useState<'full' | '2' | '3'>('3');
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 (
@@ -42,6 +57,9 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
);
}
const totalPaid = paymentHistory.reduce((sum, p) => p.status === 'completed' ? sum + p.amount : sum, 0);
const dueAmount = investment.totalInvestment - totalPaid;
const planConfig: Record<string, { badge: string }> = {
silver: { badge: 'bg-slate-200 text-slate-700' },
gold: { badge: 'bg-amber-100 text-amber-700' },
@@ -70,18 +88,25 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
{ id: 'tx8', date: '2024-05-08', description: 'Rental Income - Bike CD-5678', amount: 500, status: 'completed' },
];
const dueAmount = investment.totalInvestment * 0.33;
const paidAmount = investment.totalInvestment * 0.67;
const handlePaymentSubmit = () => {
toast.success(`Payment of ৳${(dueAmount / (selectedInstallment === '2' ? 2 : 3)).toLocaleString()} initiated successfully!`);
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;
}
toast.success(`Payment of ৳${amount.toLocaleString()} submitted successfully!`);
setShowPaymentModal(false);
setPaymentAmount('');
};
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-20 lg:mb-0">
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
@@ -89,17 +114,33 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div>
<div className="flex items-center gap-2">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2">
<Target className="w-5 h-5 lg:w-6 lg:h-6 text-investor" />{investment.planName}
<Target className="w-5 h-5 lg:w-6 lg:h-6 text-investor" />
{investment.planName}
</h1>
<span className={`px-2.5 py-1 rounded-full text-xs font-bold ${style.badge} capitalize`}>{investment.planType}</span>
<span className={`px-2.5 py-1 rounded-full text-xs font-bold ${investment.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'} capitalize`}>{investment.status}</span>
<div className="flex flex-wrap items-center gap-2">
<span
className={`px-2.5 py-1 rounded-full text-xs font-bold ${style.badge} capitalize`}
>
{investment.planType}
</span>
<span
className={`px-2.5 py-1 rounded-full text-xs font-bold ${investment.status === 'active'
? 'bg-green-100 text-green-700'
: 'bg-slate-100 text-slate-600'
} capitalize`}
>
{investment.status}
</span>
</div>
</div>
<p className="text-sm text-slate-500 mt-1">ID: #{investment.id?.toUpperCase()} Started: {investment.startDate}</p>
</div>
</div>
<button onClick={() => toast.success('Download started')} className="px-4 py-2.5 bg-white border border-slate-200 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-2 shadow-sm">
<button onClick={() => toast.success('Download started')} className="px-4 py-2.5 bg-white border border-slate-200 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 flex justify-center lg:justify-start items-center gap-2 shadow-sm">
<Download className="w-4 h-4" /> Download
</button>
</div>
@@ -122,7 +163,7 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</div>
<p className="text-xs text-slate-500 font-medium">Total Return</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-green-600">{(investment.actualEarnings / 1000).toFixed(0)}k</p>
<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">
@@ -148,18 +189,17 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</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">
<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: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length },
{ key: 'statement', label: 'Statement', icon: Receipt, count: null },
].map((tab) => {
const Icon = tab.icon;
return (
<button key={tab.key} onClick={() => setActiveTab(tab.key)}
className={`px-3 lg:px-6 py-3 text-sm font-semibold capitalize whitespace-nowrap transition-colors flex items-center gap-2 ${activeTab === tab.key ? 'text-investor bg-investor/5 border-b-2 border-investor' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'}`}>
className={`px-4 lg:px-6 py-3 w-full lg:w-auto text-sm font-semibold capitalize whitespace-nowrap transition-colors flex justify-center items-center gap-2 ${activeTab === tab.key ? 'text-investor bg-investor/5 border-b-2 border-investor' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'}`}>
<Icon className="w-4 h-4 " />
<span className="hidden lg:inline">{tab.label}</span>
{tab.count !== null && <span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${activeTab === tab.key ? 'bg-investor text-white' : 'bg-slate-200 text-slate-600'}`}>{tab.count}</span>}
@@ -241,6 +281,41 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</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
</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>
</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>
</div>
</div>
)}
@@ -346,6 +421,62 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
</div>
</div>
)}
{activeTab === 'payments' && (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm text-slate-500">{paymentHistory.length} payments made</p>
<button onClick={() => setShowPaymentModal(true)} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2">
<DollarSign className="w-4 h-4" /> Make Payment
</button>
</div>
<div className="overflow-x-auto">
<table className="hidden lg:table w-full text-left">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Installment</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase text-right">Amount</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paymentHistory.map((payment) => (
<tr key={payment.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">{payment.date}</td>
<td className="px-4 py-3"><span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span></td>
<td className="px-4 py-3 text-sm text-slate-600">{payment.installmentNo ? `#${payment.installmentNo}` : '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{payment.method}</td>
<td className="px-4 py-3 text-sm font-bold text-green-600 text-right">{payment.amount.toLocaleString()}</td>
<td className="px-4 py-3"><span className="inline-flex px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span></td>
</tr>
))}
</tbody>
</table>
</div>
<div className="lg:hidden space-y-3">
{paymentHistory.map((payment) => (
<div key={payment.id} className="p-4 bg-white rounded-xl border border-slate-200">
<div className="flex items-start justify-between mb-2">
<div>
<p className="text-sm font-medium text-slate-800">{payment.amount.toLocaleString()}</p>
<p className="text-xs text-slate-400">{payment.date} {payment.method}</p>
</div>
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span>
</div>
<div className="flex gap-2 mt-2">
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span>
{payment.installmentNo && <span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-blue-100 text-blue-600">Inst #{payment.installmentNo}</span>}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
@@ -354,55 +485,44 @@ export default function InvestorInvestmentDetailPage({ params }: { params: Promi
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl">
<div className="flex items-center justify-between p-5 border-b border-slate-100">
<h3 className="text-lg font-bold text-slate-800">Pay Due Amount</h3>
<h3 className="text-lg font-bold text-slate-800">Make Payment</h3>
<button onClick={() => setShowPaymentModal(false)} className="p-1 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4">
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-center">
<p className="text-sm text-amber-700 mb-1">Total Due</p>
<p className="text-3xl font-bold text-amber-600">{dueAmount.toLocaleString()}</p>
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Total Investment</span>
<span className="text-sm font-semibold text-slate-800">{investment.totalInvestment.toLocaleString()}</span>
</div>
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Already Paid</span>
<span className="text-sm font-semibold text-green-600">{totalPaid.toLocaleString()}</span>
</div>
<div className="flex justify-between pt-2 border-t border-slate-200">
<span className="text-sm font-semibold text-slate-800">Due Amount</span>
<span className="text-sm font-bold text-amber-600">{dueAmount.toLocaleString()}</span>
</div>
</div>
<div>
<p className="text-sm font-semibold text-slate-700 mb-3">Installment List</p>
<div className="space-y-2">
<div className="p-3 bg-green-50 border border-green-200 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-600" />
<div>
<p className="text-sm font-semibold text-slate-800">Installment 1</p>
<p className="text-xs text-slate-500">Paid on Jan 15, 2024</p>
</div>
</div>
<span className="text-sm font-bold text-green-600">{(investment.totalInvestment * 0.4).toLocaleString()}</span>
</div>
<div className="p-3 bg-amber-50 border border-amber-300 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-amber-600" />
<div>
<p className="text-sm font-semibold text-slate-800">Installment 2</p>
<p className="text-xs text-amber-600">Due: Jun 15, 2024</p>
</div>
</div>
<span className="text-sm font-bold text-amber-600">{(investment.totalInvestment * 0.3).toLocaleString()}</span>
</div>
<div className="p-3 bg-amber-50 border border-amber-300 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-amber-600" />
<div>
<p className="text-sm font-semibold text-slate-800">Installment 3</p>
<p className="text-xs text-amber-600">Due: Jul 15, 2024</p>
</div>
</div>
<span className="text-sm font-bold text-amber-600">{(investment.totalInvestment * 0.3).toLocaleString()}</span>
<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>
<div className="pt-2">
<p className="text-sm font-semibold text-slate-700 mb-2">Select Payment</p>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Payment Method</label>
<div className="flex gap-2">
<button className="flex-1 p-3 border border-slate-200 rounded-lg text-center hover:border-investor hover:bg-investor/5 transition-colors">
<Building2 className="w-5 h-5 mx-auto text-slate-600 mb-1" />
@@ -416,7 +536,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 Now {(investment.totalInvestment * 0.3).toLocaleString()}
Pay {paymentAmount ? parseFloat(paymentAmount).toLocaleString() : '0'}
</button>
</div>
</div>

View File

@@ -47,6 +47,7 @@ const adminNavItems: NavItem[] = [
{ label: 'Bikers', href: '/admin/bikers', icon: Users },
{ label: 'Investors', href: '/admin/investors', icon: Wallet },
{ label: 'Fleet Management', href: '/admin/fleet', icon: Bike },
{ label: 'Battery Management', href: '/admin/batteries', icon: Battery },
{ label: 'Merchants (P2)', href: '/admin/merchants', icon: Store },
{ label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap },