feat: add asset management modals for assigning bikes and batteries and confirming unassignments

This commit is contained in:
sazzadulalambd
2026-05-19 19:27:03 +06:00
parent b1dd4b0683
commit 123ba98c9e
5 changed files with 836 additions and 301 deletions

View File

@@ -14,6 +14,10 @@ import {
ShieldCheck, Building2, Users, Check, AlertOctagon, Activity, Award, Camera, History, Settings
} from 'lucide-react';
import AssignBikeModal from '../components/AssignBikeModal';
import AssignBatteryModal from '../components/AssignBatteryModal';
import UnassignConfirmModal from '../components/UnassignConfirmModal';
const statusColors: Record<string, string> = {
active: 'bg-green-100 text-green-700',
pending: 'bg-amber-100 text-amber-700',
@@ -139,7 +143,42 @@ export default function InvestorDetailPage() {
const [showEditModal, setShowEditModal] = useState(false);
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
const [selectedBikeId, setSelectedBikeId] = useState('');
const [selectedBikeIds, setSelectedBikeIds] = useState<string[]>([]);
const [selectedBikePlanId, setSelectedBikePlanId] = useState('');
const getPlanTargetAssetCount = (plan: any) => {
if (plan.assetType === 'battery' || plan.planName?.toLowerCase().includes('battery')) {
if (plan.id === 'ip3') return 2;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
if (nameLower.includes('1')) return 1;
return 1;
} else {
if (plan.id === 'ip1') return 1;
if (plan.id === 'ip2') return 1;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
if (nameLower.includes('1')) return 1;
return 1;
}
};
const [showRegisterBikeModal, setShowRegisterBikeModal] = useState(false);
const [unassignConfirmModal, setUnassignConfirmModal] = useState<{
show: boolean;
type: 'bike' | 'battery';
id: string;
name: string;
details: string;
}>({
show: false,
type: 'bike',
id: '',
name: '',
details: ''
});
const [registerBikeForm, setRegisterBikeForm] = useState({
plateNumber: '',
brand: 'Etron',
@@ -400,6 +439,8 @@ export default function InvestorDetailPage() {
}, [batteries, investorId]);
const [showAssignBatteryModal, setShowAssignBatteryModal] = useState(false);
const [selectedBatteryIds, setSelectedBatteryIds] = useState<string[]>([]);
const [selectedBatteryPlanId, setSelectedBatteryPlanId] = useState('');
const [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false);
const [showEditBatteryModal, setShowEditBatteryModal] = useState(false);
@@ -497,23 +538,24 @@ export default function InvestorDetailPage() {
const availableBikesForAssignment = bikes.filter(b => !b.investorId && b.status === 'available');
const handleAssignBike = () => {
if (!selectedBikeId) {
toast.error('Please select a bike');
if (!selectedBikePlanId) {
toast.error('Please select an investment plan');
return;
}
if (selectedBikeIds.length === 0) {
toast.error('Please select at least one bike');
return;
}
const bikeToAssign = bikes.find(b => b.id === selectedBikeId);
if (!bikeToAssign) return;
// Find first active EV Investment Plan of the investor to attach to
const evInv = investor.investments?.find((inv: any) => inv.assetType === 'bike' || inv.planName.toLowerCase().includes('ev') || inv.planName.toLowerCase().includes('bike')) || investor.investments?.[0];
const assignedBikesList: any[] = [];
setBikes(prev => prev.map(b => {
if (b.id === selectedBikeId) {
if (selectedBikeIds.includes(b.id)) {
assignedBikesList.push(b);
return {
...b,
investorId: investorId,
investorName: investor.name,
investmentId: evInv?.id || 'ip1',
investmentId: selectedBikePlanId,
status: 'rented',
totalEarnings: b.totalEarnings || 0
};
@@ -521,9 +563,32 @@ export default function InvestorDetailPage() {
return b;
}));
toast.success(`Bike ${bikeToAssign.model} (${bikeToAssign.plateNumber}) successfully assigned!`);
// Update investor's investments to hold the bike IDs
setInvestors(prev => prev.map(inv => {
if (inv.id === investor.id) {
return {
...inv,
investments: inv.investments?.map((item: any) => {
if (item.id === selectedBikePlanId) {
const currentBikeIds = item.bikeIds || [];
const uniqueNewBikeIds = Array.from(new Set([...currentBikeIds, ...selectedBikeIds]));
return {
...item,
bikeIds: uniqueNewBikeIds
};
}
return item;
})
};
}
return inv;
}));
const bikeNames = assignedBikesList.map(b => `${b.model} (${b.plateNumber})`).join(', ');
toast.success(`Successfully assigned ${selectedBikeIds.length} bike(s): ${bikeNames}`);
setShowAssignBikeModal(false);
setSelectedBikeId('');
setSelectedBikeIds([]);
setSelectedBikePlanId('');
};
const handleRegisterAndAssignBike = () => {
@@ -676,29 +741,62 @@ export default function InvestorDetailPage() {
};
const handleAssignBattery = () => {
if (!assignForm.batteryId) {
toast.error('Please select a battery');
if (!selectedBatteryPlanId) {
toast.error('Please select an investment plan');
return;
}
if (selectedBatteryIds.length === 0) {
toast.error('Please select at least one battery');
return;
}
const batteryToAssign = unassignedBatteries.find(b => b.id === assignForm.batteryId);
if (!batteryToAssign) return;
const assignedObj = {
...batteryToAssign,
investorId: investorId,
investorName: investor.name,
investorSharePercentage: Number(assignForm.investorShare),
investedAmount: Number(assignForm.investedAmount),
investmentId: assignForm.investmentId,
deposit: Number(assignForm.deposit),
rentPrice: Number(assignForm.rentPrice),
status: 'in-use'
};
const assignedObjList: any[] = [];
const chosenBatteries = unassignedBatteries.filter(b => selectedBatteryIds.includes(b.id));
setBatteries(prev => [...prev, assignedObj]);
setUnassignedBatteries(prev => prev.filter(b => b.id !== assignForm.batteryId));
chosenBatteries.forEach(batteryToAssign => {
const assignedObj = {
...batteryToAssign,
investorId: investorId,
investorName: investor.name,
investorSharePercentage: Number(assignForm.investorShare),
investedAmount: Number(assignForm.investedAmount),
investmentId: selectedBatteryPlanId,
deposit: Number(assignForm.deposit),
rentPrice: Number(assignForm.rentPrice),
status: 'in-use'
};
assignedObjList.push(assignedObj);
});
setBatteries(prev => [...prev, ...assignedObjList]);
setUnassignedBatteries(prev => prev.filter(b => !selectedBatteryIds.includes(b.id)));
// Update investor's investments to hold the battery IDs
setInvestors(prev => prev.map(inv => {
if (inv.id === investor.id) {
return {
...inv,
investments: inv.investments?.map((item: any) => {
if (item.id === selectedBatteryPlanId) {
const currentBatteryIds = item.batteryIds || [];
const uniqueNewBatteryIds = Array.from(new Set([...currentBatteryIds, ...selectedBatteryIds]));
return {
...item,
batteryIds: uniqueNewBatteryIds
};
}
return item;
})
};
}
return inv;
}));
const serialsList = chosenBatteries.map(b => b.serialNumber).join(', ');
toast.success(`Successfully assigned ${selectedBatteryIds.length} battery pack(s): ${serialsList}`);
setShowAssignBatteryModal(false);
toast.success('Battery assigned to investment plan!');
setSelectedBatteryIds([]);
setSelectedBatteryPlanId('');
};
const handleRegisterAndAssignBattery = () => {
@@ -1519,7 +1617,8 @@ export default function InvestorDetailPage() {
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => {
setSelectedBikeId('');
setSelectedBikeIds([]);
setSelectedBikePlanId('');
setShowAssignBikeModal(true);
}}
className="px-4 py-2 bg-blue-50 text-blue-700 border border-blue-200 rounded-lg text-sm font-semibold hover:bg-blue-100 flex items-center gap-2 transition-colors"
@@ -1626,9 +1725,13 @@ export default function InvestorDetailPage() {
</Link>
<button
onClick={() => {
if (confirm(`Are you sure you want to unassign bike ${bike.model} (${bike.plateNumber})?`)) {
handleUnassignBike(bike.id);
}
setUnassignConfirmModal({
show: true,
type: 'bike',
id: bike.id,
name: bike.model,
details: bike.plateNumber
});
}}
className="text-xs text-red-600 hover:text-red-800 font-semibold transition-colors px-2 py-1 bg-red-50 rounded"
>
@@ -1674,6 +1777,8 @@ export default function InvestorDetailPage() {
investorShare: 100,
investedAmount: 45000
});
setSelectedBatteryIds([]);
setSelectedBatteryPlanId('');
setShowAssignBatteryModal(true);
}}
className="px-4 py-2 bg-emerald-50 text-emerald-700 border border-emerald-200 rounded-lg text-sm font-semibold hover:bg-emerald-100 flex items-center gap-2 transition-colors"
@@ -1793,7 +1898,15 @@ export default function InvestorDetailPage() {
<Edit className="w-3 h-3" /> Edit
</button>
<button
onClick={() => handleUnassignBattery(battery.id)}
onClick={() => {
setUnassignConfirmModal({
show: true,
type: 'battery',
id: battery.id,
name: `${battery.brand} ${battery.model}`,
details: battery.serialNumber
});
}}
className="px-2 py-1 text-xs font-bold text-red-600 hover:bg-red-50 rounded transition-all flex items-center gap-0.5"
title="Unassign Battery"
>
@@ -2948,122 +3061,58 @@ export default function InvestorDetailPage() {
</div>
)}
{showAssignBatteryModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden flex flex-col">
<div className="p-5 border-b border-emerald-100 bg-emerald-50 flex items-center justify-between">
<h2 className="text-lg font-bold text-emerald-800 flex items-center gap-2">
<Battery className="w-5 h-5 text-emerald-600 animate-bounce" />
Assign Battery to Partner
</h2>
<button onClick={() => setShowAssignBatteryModal(false)} className="p-2 hover:bg-emerald-100 rounded-lg text-emerald-600">
<X className="w-5 h-5" />
</button>
</div>
<AssignBatteryModal
isOpen={showAssignBatteryModal}
onClose={() => setShowAssignBatteryModal(false)}
investor={investor}
batteries={batteries}
unassignedBatteries={unassignedBatteries}
onAssign={(planId, batteryIds) => {
const assignedBatteriesList: any[] = [];
setBatteries(prev => prev.map(b => {
if (batteryIds.includes(b.id)) {
assignedBatteriesList.push(b);
return {
...b,
investorId: investorId,
investorName: investor.name,
investmentId: planId,
status: 'in-use',
investedAmount: b.purchasePrice || 45000,
investorSharePercentage: 100,
deposit: b.deposit || 5000,
rentPrice: b.rentPrice || 150
};
}
return b;
}));
<div className="p-5 space-y-4 overflow-y-auto max-h-[75vh]">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Unassigned Battery *</label>
<select
value={assignForm.batteryId}
onChange={(e) => {
const selected = unassignedBatteries.find(b => b.id === e.target.value);
setAssignForm({
...assignForm,
batteryId: e.target.value,
deposit: selected?.deposit || 5000,
rentPrice: selected?.rentPrice || 150,
investedAmount: selected?.purchasePrice || 45000
});
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Choose a battery pack</option>
{unassignedBatteries.map(bat => (
<option key={bat.id} value={bat.id}>
{bat.brand} {bat.model} ({bat.serialNumber})
</option>
))}
</select>
</div>
setUnassignedBatteries(prev => prev.filter(b => !batteryIds.includes(b.id)));
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Link to Investment Plan *</label>
<select
value={assignForm.investmentId}
onChange={(e) => setAssignForm({ ...assignForm, investmentId: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select plan</option>
{investor.investments?.map((inv: any) => (
<option key={inv.id} value={inv.id}>
{inv.planName} (#{inv.id?.slice(-6)})
</option>
))}
</select>
</div>
setInvestors(prev => prev.map(inv => {
if (inv.id === investor.id) {
return {
...inv,
investments: inv.investments?.map((item: any) => {
if (item.id === planId) {
const currentBatteryIds = item.batteryIds || [];
const uniqueNewBatteryIds = Array.from(new Set([...currentBatteryIds, ...batteryIds]));
return {
...item,
batteryIds: uniqueNewBatteryIds
};
}
return item;
})
};
}
return inv;
}));
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Refundable Deposit (৳) *</label>
<input
type="number"
value={assignForm.deposit}
onChange={(e) => setAssignForm({ ...assignForm, deposit: Number(e.target.value) })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Daily Rent Price (৳) *</label>
<input
type="number"
value={assignForm.rentPrice}
onChange={(e) => setAssignForm({ ...assignForm, rentPrice: Number(e.target.value) })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investor Share % *</label>
<input
type="number"
value={assignForm.investorShare}
onChange={(e) => setAssignForm({ ...assignForm, investorShare: Number(e.target.value) })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Contribution (৳) *</label>
<input
type="number"
value={assignForm.investedAmount}
onChange={(e) => setAssignForm({ ...assignForm, investedAmount: Number(e.target.value) })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
</div>
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button
onClick={() => setShowAssignBatteryModal(false)}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-50"
>
Cancel
</button>
<button
onClick={handleAssignBattery}
disabled={!assignForm.batteryId || !assignForm.investmentId}
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-semibold hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
>
Assign Battery
</button>
</div>
</div>
</div>
)}
const batteryNames = assignedBatteriesList.map(b => `${b.brand} ${b.model}`).join(', ');
toast.success(`Successfully assigned ${batteryIds.length} battery/ies: ${batteryNames}`);
}}
/>
{showRegisterBatteryModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
@@ -3340,42 +3389,67 @@ export default function InvestorDetailPage() {
</div>
)}
{showAssignBikeModal && (
<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-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-investor flex items-center gap-2">
<Bike className="w-5 h-5 text-investor animate-bounce" />
Assign Bike to Investor
</h2>
<button onClick={() => setShowAssignBikeModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<AssignBikeModal
isOpen={showAssignBikeModal}
onClose={() => setShowAssignBikeModal(false)}
investor={investor}
bikes={bikes}
onAssign={(planId, bikeIds) => {
const assignedBikesList: any[] = [];
setBikes(prev => prev.map(b => {
if (bikeIds.includes(b.id)) {
assignedBikesList.push(b);
return {
...b,
investorId: investorId,
investorName: investor.name,
investmentId: planId,
status: 'rented',
totalEarnings: b.totalEarnings || 0
};
}
return b;
}));
<div className="p-5">
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
<select
value={selectedBikeId}
onChange={(e) => setSelectedBikeId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select a bike</option>
{availableBikesForAssignment.map(bike => (
<option key={bike.id} value={bike.id}>
{bike.model} - {bike.plateNumber} (৳{bike.purchasePrice?.toLocaleString() || 0})
</option>
))}
</select>
</div>
setInvestors(prev => prev.map(inv => {
if (inv.id === investor.id) {
return {
...inv,
investments: inv.investments?.map((item: any) => {
if (item.id === planId) {
const currentBikeIds = item.bikeIds || [];
const uniqueNewBikeIds = Array.from(new Set([...currentBikeIds, ...bikeIds]));
return {
...item,
bikeIds: uniqueNewBikeIds
};
}
return item;
})
};
}
return inv;
}));
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowAssignBikeModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={handleAssignBike} disabled={!selectedBikeId} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark disabled:opacity-50">Assign Bike</button>
</div>
</div>
</div>
)}
const bikeNames = assignedBikesList.map(b => `${b.model} (${b.plateNumber})`).join(', ');
toast.success(`Successfully assigned ${bikeIds.length} bike(s): ${bikeNames}`);
}}
/>
<UnassignConfirmModal
isOpen={unassignConfirmModal.show}
onClose={() => setUnassignConfirmModal(prev => ({ ...prev, show: false }))}
type={unassignConfirmModal.type}
name={unassignConfirmModal.name}
details={unassignConfirmModal.details}
onConfirm={() => {
if (unassignConfirmModal.type === 'bike') {
handleUnassignBike(unassignConfirmModal.id);
} else {
handleUnassignBattery(unassignConfirmModal.id);
}
}}
/>
{showRegisterBikeModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">