refactor: remove battery manual entry fields in biker profile and add battery rental history table to rental details

This commit is contained in:
sazzadulalambd
2026-05-16 14:06:33 +06:00
parent 41530a4691
commit 1882cfbb91
3 changed files with 175 additions and 71 deletions

View File

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

View File

@@ -6,7 +6,7 @@ import Link from 'next/link';
import { import {
ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle, ArrowLeft, Bike, User, Calendar, DollarSign, Wallet, Shield, CheckCircle, XCircle,
Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin, Clock, Edit, Save, Plus, Trash2, Image, Upload, Lock, Unlock, AlertTriangle, MessageSquare, MapPin,
Phone, MessageCircle, Play, Check, X, FileText, Download Phone, MessageCircle, Play, Check, X, FileText, Download, Battery
} from 'lucide-react'; } from 'lucide-react';
import { import {
canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit, canRentalAccept, canRentalReject, canRentalCancel, canRentalEdit,
@@ -41,6 +41,9 @@ interface Rental {
bikeModel: string; bikeModel: string;
bikePlate: string; bikePlate: string;
bikeBattery: number; bikeBattery: number;
batteryId?: string;
batteryName?: string;
batteryRent?: number;
type: RentalType; type: RentalType;
status: RentalStatus; status: RentalStatus;
startDate: string; startDate: string;
@@ -73,6 +76,17 @@ interface Rental {
activatedAt?: string; activatedAt?: string;
lockHistory?: LockEvent[]; lockHistory?: LockEvent[];
paymentHistory?: PaymentHistory[]; paymentHistory?: PaymentHistory[];
batteryHistory?: BatteryRentalHistory[];
}
interface BatteryRentalHistory {
id: string;
batteryId: string;
batteryName: string;
assignedAt: string;
returnedAt?: string;
monthlyRent: number;
status: 'active' | 'returned';
} }
interface LockEvent { 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: 'lh3', action: 'locked', reason: 'Second payment overdue - Day 2 penalty', performedBy: 'System', performedAt: '2024-03-01' },
{ id: 'lh4', action: 'unlocked', reason: 'Payment cleared', performedBy: 'Admin Manager', performedAt: '2024-03-02' }, { id: 'lh4', action: 'unlocked', reason: 'Payment cleared', performedBy: 'Admin Manager', performedAt: '2024-03-02' },
], ],
batteryHistory: [
{ id: 'BAT-RENT-001', batteryId: 'BAT-DH-001', batteryName: 'Galaxy 72V 45Ah', assignedAt: '2024-01-16', monthlyRent: 1500, 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', id: 'RNT-002',
@@ -710,6 +728,53 @@ export default function RentalDetailPage() {
</div> </div>
</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 */} {/* Initial Condition Images */}
{/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */} {/* {(rental.status === 'pending' || rental.status === 'accepted') && rental.initialImages && ( */}
{rental.initialImages && ( {rental.initialImages && (

View File

@@ -33,6 +33,9 @@ interface Rental {
bikeModel: string; bikeModel: string;
bikePlate: string; bikePlate: string;
bikeBattery: number; bikeBattery: number;
batteryId?: string;
batteryName?: string;
batteryRent?: number;
type: RentalType; type: RentalType;
status: RentalStatus; status: RentalStatus;
startDate: string; startDate: string;
@@ -63,6 +66,15 @@ interface Rental {
createdAt: string; createdAt: string;
acceptedAt?: string; acceptedAt?: string;
activatedAt?: string; activatedAt?: string;
batteryHistory?: {
id: string;
batteryId: string;
batteryName: string;
assignedAt: string;
returnedAt?: string;
monthlyRent: number;
status: 'active' | 'returned';
}[];
} }
interface Bike { interface Bike {
@@ -103,6 +115,23 @@ const mockHubs = [
{ id: 'HUB-004', name: 'Mirpur Hub' }, { id: 'HUB-004', name: 'Mirpur Hub' },
]; ];
interface Battery {
id: string;
brand: string;
model: string;
soc: number;
monthlyRent: number;
status: 'available' | 'in-use' | 'maintenance';
}
const mockBatteries: Battery[] = [
{ id: 'BAT-DH-001', brand: 'Galaxy', model: '72V 45Ah', soc: 85, monthlyRent: 1500, status: 'available' },
{ id: 'BAT-DH-002', brand: 'Titan', model: '72V 50Ah', soc: 92, monthlyRent: 1800, status: 'available' },
{ id: 'BAT-DH-003', brand: 'PowerMax', model: '60V 40Ah', soc: 78, monthlyRent: 1200, status: 'available' },
{ id: 'BAT-DH-004', brand: 'UltraCell', model: '72V 55Ah', soc: 88, monthlyRent: 2000, status: 'available' },
{ id: 'BAT-DH-005', brand: 'EcoVolt', model: '48V 30Ah', soc: 65, monthlyRent: 800, status: 'available' },
];
const rentalSettings = { const rentalSettings = {
single: { single: {
Premium: { deposit: 5000, contractMonths: [1, 3, 6, 12], dailyRate: 200, weeklyRate: 1200, monthlyRate: 5000 }, Premium: { deposit: 5000, contractMonths: [1, 3, 6, 12], dailyRate: 200, weeklyRate: 1200, monthlyRate: 5000 },
@@ -304,6 +333,8 @@ export default function RentalsPage() {
subscriptionType: 'daily' | 'weekly' | 'monthly'; subscriptionType: 'daily' | 'weekly' | 'monthly';
contractMonths: number; contractMonths: number;
bikeId: string; bikeId: string;
batteryId: string;
batteryRent: number;
startDate: string; startDate: string;
hubId: string; hubId: string;
depositAmount: number; depositAmount: number;
@@ -316,12 +347,16 @@ export default function RentalsPage() {
subscriptionType: 'daily', subscriptionType: 'daily',
contractMonths: 0, contractMonths: 0,
bikeId: '', bikeId: '',
batteryId: '',
batteryRent: 0,
startDate: new Date().toISOString().split('T')[0], startDate: new Date().toISOString().split('T')[0],
hubId: '', hubId: '',
depositAmount: 0, depositAmount: 0,
depositPaymentMethod: 'cash', depositPaymentMethod: 'cash',
}); });
const availableBatteries = mockBatteries.filter(b => b.status === 'available');
const [showJournalPreview, setShowJournalPreview] = useState(false); const [showJournalPreview, setShowJournalPreview] = useState(false);
useEffect(() => { useEffect(() => {
@@ -439,6 +474,7 @@ export default function RentalsPage() {
const bike = mockBikes.find(b => b.id === newRental.bikeId); const bike = mockBikes.find(b => b.id === newRental.bikeId);
const user = mockUsers.find(u => u.id === newRental.userId); const user = mockUsers.find(u => u.id === newRental.userId);
const hub = mockHubs.find(h => h.id === newRental.hubId); const hub = mockHubs.find(h => h.id === newRental.hubId);
const battery = mockBatteries.find(b => b.id === newRental.batteryId);
const settings = planConditions[newRental.type]?.find(p => p.name === newRental.planConditionName) || planConditions[newRental.type]?.[0]; const settings = planConditions[newRental.type]?.find(p => p.name === newRental.planConditionName) || planConditions[newRental.type]?.[0];
const rental: Rental = { const rental: Rental = {
@@ -450,6 +486,9 @@ export default function RentalsPage() {
bikeModel: bike?.model || '', bikeModel: bike?.model || '',
bikePlate: bike?.plate || '', bikePlate: bike?.plate || '',
bikeBattery: bike?.battery || 0, bikeBattery: bike?.battery || 0,
batteryId: newRental.batteryId || undefined,
batteryName: battery ? `${battery.brand} ${battery.model}` : undefined,
batteryRent: newRental.batteryRent || undefined,
type: newRental.type, type: newRental.type,
status: 'pending', status: 'pending',
startDate: newRental.startDate, startDate: newRental.startDate,
@@ -473,6 +512,14 @@ export default function RentalsPage() {
hubName: hub?.name || '', hubName: hub?.name || '',
imagesApproved: false, imagesApproved: false,
createdAt: new Date().toISOString().split('T')[0], createdAt: new Date().toISOString().split('T')[0],
batteryHistory: newRental.batteryId ? [{
id: `BAT-RENT-${Date.now()}`,
batteryId: newRental.batteryId,
batteryName: battery ? `${battery.brand} ${battery.model}` : '',
assignedAt: new Date().toISOString().split('T')[0],
monthlyRent: newRental.batteryRent,
status: 'active' as const,
}] : undefined,
}; };
setRentals([...rentals, rental]); setRentals([...rentals, rental]);
@@ -486,6 +533,8 @@ export default function RentalsPage() {
subscriptionType: 'daily', subscriptionType: 'daily',
contractMonths: 0, contractMonths: 0,
bikeId: '', bikeId: '',
batteryId: '',
batteryRent: 0,
evModel: '', evModel: '',
startDate: new Date().toISOString().split('T')[0], startDate: new Date().toISOString().split('T')[0],
hubId: '', hubId: '',
@@ -927,8 +976,11 @@ export default function RentalsPage() {
)} )}
{createStep === 3 && ( {createStep === 3 && (
<div className="space-y-4">
<h4 className="font-medium text-slate-700 mb-3">Step 3: Select Bike & Battery</h4>
<div> <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 <select
value={newRental.bikeId} value={newRental.bikeId}
onChange={(e) => setNewRental({ ...newRental, bikeId: e.target.value })} onChange={(e) => setNewRental({ ...newRental, bikeId: e.target.value })}
@@ -940,7 +992,7 @@ export default function RentalsPage() {
))} ))}
</select> </select>
{selectedBike && ( {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 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'}`}> <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}% {selectedBike.battery}%
@@ -948,6 +1000,29 @@ export default function RentalsPage() {
</div> </div>
)} )}
</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">Deposit Amount: ৳{selectedPlan?.deposit.toLocaleString()}</p>
<p className="text-sm text-slate-600">User: {selectedUser?.name}</p> <p className="text-sm text-slate-600">User: {selectedUser?.name}</p>
<p className="text-sm text-slate-600">Bike: {selectedBike?.model} ({selectedBike?.plate})</p> <p className="text-sm text-slate-600">Bike: {selectedBike?.model} ({selectedBike?.plate})</p>
{newRental.batteryId && (
<p className="text-sm text-slate-600">Battery: {availableBatteries.find(b => b.id === newRental.batteryId)?.brand} {availableBatteries.find(b => b.id === newRental.batteryId)?.model} (Rent: ৳{newRental.batteryRent}/month)</p>
)}
<p className="text-sm text-slate-600">Hub: {mockHubs.find(h => h.id === newRental.hubId)?.name}</p> <p className="text-sm text-slate-600">Hub: {mockHubs.find(h => h.id === newRental.hubId)?.name}</p>
{newRental.batteryId && (
<div className="mt-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>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">