refactor: remove battery manual entry fields in biker profile and add battery rental history table to rental details
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
@@ -146,6 +160,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',
|
||||
@@ -710,6 +728,53 @@ export default function RentalDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Battery Rental History */}
|
||||
{rental.batteryHistory && rental.batteryHistory.length > 0 && (
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200">
|
||||
<h3 className="font-semibold text-slate-700 mb-3 flex items-center gap-2">
|
||||
<Battery className="w-5 h-5 text-amber-500" /> Battery Rental History
|
||||
</h3>
|
||||
<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>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Initial Condition Images */}
|
||||
{/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */}
|
||||
{rental.initialImages && (
|
||||
|
||||
@@ -33,6 +33,9 @@ interface Rental {
|
||||
bikeModel: string;
|
||||
bikePlate: string;
|
||||
bikeBattery: number;
|
||||
batteryId?: string;
|
||||
batteryName?: string;
|
||||
batteryRent?: number;
|
||||
type: RentalType;
|
||||
status: RentalStatus;
|
||||
startDate: string;
|
||||
@@ -63,6 +66,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 +115,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 },
|
||||
@@ -304,6 +333,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 +347,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 +474,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 +486,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 +512,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 +533,8 @@ export default function RentalsPage() {
|
||||
subscriptionType: 'daily',
|
||||
contractMonths: 0,
|
||||
bikeId: '',
|
||||
batteryId: '',
|
||||
batteryRent: 0,
|
||||
evModel: '',
|
||||
startDate: new Date().toISOString().split('T')[0],
|
||||
hubId: '',
|
||||
@@ -927,26 +976,52 @@ export default function RentalsPage() {
|
||||
)}
|
||||
|
||||
{createStep === 3 && (
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-700 mb-3">Step 3: Select Bike</h4>
|
||||
<select
|
||||
value={newRental.bikeId}
|
||||
onChange={(e) => setNewRental({ ...newRental, bikeId: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Select Bike...</option>
|
||||
{availableBikes.map(bike => (
|
||||
<option key={bike.id} value={bike.id}>{bike.model} - {bike.plate}</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedBike && (
|
||||
<div className="mt-3 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}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-slate-700 mb-3">Step 3: Select Bike & Battery</h4>
|
||||
|
||||
<div>
|
||||
<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 })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Select Bike...</option>
|
||||
{availableBikes.map(bike => (
|
||||
<option key={bike.id} value={bike.id}>{bike.model} - {bike.plate}</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedBike && (
|
||||
<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}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-slate-600 mb-1 block">Select Battery (Optional)</label>
|
||||
<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-2 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<p className="text-sm text-amber-700">Battery Monthly Rent: <span className="font-bold">৳{newRental.batteryRent}/month</span></p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -958,7 +1033,18 @@ 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} (Rent: ৳{newRental.batteryRent}/month)</p>
|
||||
)}
|
||||
<p className="text-sm text-slate-600">Hub: {mockHubs.find(h => h.id === newRental.hubId)?.name}</p>
|
||||
{newRental.batteryId && (
|
||||
<div className="mt-2 pt-2 border-t border-slate-200">
|
||||
<p className="text-sm font-medium text-emerald-700">
|
||||
Combined Monthly Rent: ৳{(newRental.subscriptionType === 'daily' ? selectedPlan?.dailyRate : newRental.subscriptionType === 'weekly' ? selectedPlan?.weeklyRate : selectedPlan?.monthlyRate) + newRental.batteryRent}/month
|
||||
<span className="text-xs text-slate-500 ml-1">(Bike: ৳{newRental.subscriptionType === 'daily' ? selectedPlan?.dailyRate : newRental.subscriptionType === 'weekly' ? selectedPlan?.weeklyRate : selectedPlan?.monthlyRate} + Battery: ৳{newRental.batteryRent})</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user