feat: add asset management modals for assigning bikes and batteries and confirming unassignments
This commit is contained in:
@@ -12,6 +12,10 @@ import {
|
||||
import toast from 'react-hot-toast';
|
||||
import { investors as initialInvestors, bikes as initialBikes, transactions as initialTransactions } from '@/data/mockData';
|
||||
|
||||
import AssignBikeModal from '../../../components/AssignBikeModal';
|
||||
import AssignBatteryModal from '../../../components/AssignBatteryModal';
|
||||
import UnassignConfirmModal from '../../../components/UnassignConfirmModal';
|
||||
|
||||
export default function InvestmentDetailPage({ params }: { params: Promise<{ id: string; investmentId: string }> }) {
|
||||
const resolvedParams = use(params);
|
||||
const { id: investorId, investmentId } = resolvedParams;
|
||||
@@ -291,6 +295,19 @@ export default function InvestmentDetailPage({ params }: { params: Promise<{ id:
|
||||
const [showAddBikeModal, setShowAddBikeModal] = useState(false);
|
||||
const [showRegisterBikeModal, setShowRegisterBikeModal] = useState(false);
|
||||
const [showAssignBatteryModal, setShowAssignBatteryModal] = 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 [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false);
|
||||
|
||||
const [selectedBikeId, setSelectedBikeId] = useState('');
|
||||
@@ -1024,9 +1041,13 @@ export default function InvestmentDetailPage({ params }: { params: Promise<{ id:
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm(`Are you sure you want to unassign battery ${bat.serialNumber}?`)) {
|
||||
handleUnassignBattery(bat.id);
|
||||
}
|
||||
setUnassignConfirmModal({
|
||||
show: true,
|
||||
type: 'battery',
|
||||
id: bat.id,
|
||||
name: `${bat.brand} ${bat.model}`,
|
||||
details: bat.serialNumber
|
||||
});
|
||||
}}
|
||||
className="p-2 hover:bg-red-50 rounded-lg text-slate-400 hover:text-red-600 transition-colors"
|
||||
>
|
||||
@@ -1169,9 +1190,13 @@ export default function InvestmentDetailPage({ params }: { params: Promise<{ id:
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
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="p-2 hover:bg-red-50 rounded-lg text-slate-400 hover:text-red-600 transition-colors"
|
||||
>
|
||||
@@ -1295,47 +1320,53 @@ export default function InvestmentDetailPage({ params }: { params: Promise<{ id:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAddBikeModal && (
|
||||
<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">
|
||||
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
||||
<Bike className="w-5 h-5" /> Assign Bike to Investment
|
||||
</h3>
|
||||
<button onClick={() => setShowAddBikeModal(false)} className="p-1 hover:bg-slate-100 rounded-lg">
|
||||
<X className="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">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 focus:outline-none focus:ring-2 focus:ring-investor"
|
||||
>
|
||||
<option value="">Choose a bike...</option>
|
||||
{bikes.filter((b: any) => !b.investorId).map((bike: any) => (
|
||||
<option key={bike.id} value={bike.id}>{bike.model} - {bike.plateNumber}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
|
||||
<button onClick={() => setShowAddBikeModal(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 font-medium hover:bg-investor-dark disabled:opacity-50"
|
||||
>
|
||||
Assign Bike
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<AssignBikeModal
|
||||
isOpen={showAddBikeModal}
|
||||
onClose={() => setShowAddBikeModal(false)}
|
||||
investor={investor}
|
||||
bikes={bikes}
|
||||
preselectedPlanId={investmentId}
|
||||
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;
|
||||
}));
|
||||
|
||||
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;
|
||||
}));
|
||||
|
||||
const bikeNames = assignedBikesList.map(b => `${b.model} (${b.plateNumber})`).join(', ');
|
||||
toast.success(`Successfully assigned ${bikeIds.length} bike(s): ${bikeNames}`);
|
||||
}}
|
||||
/>
|
||||
|
||||
{showRegisterBikeModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
@@ -1433,83 +1464,74 @@ export default function InvestmentDetailPage({ params }: { params: Promise<{ id:
|
||||
</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">
|
||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold text-slate-800">Assign Battery to Investment</h2>
|
||||
<button onClick={() => setShowAssignBatteryModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
<AssignBatteryModal
|
||||
isOpen={showAssignBatteryModal}
|
||||
onClose={() => setShowAssignBatteryModal(false)}
|
||||
investor={investor}
|
||||
batteries={batteries}
|
||||
unassignedBatteries={unassignedBatteries}
|
||||
preselectedPlanId={investmentId}
|
||||
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">
|
||||
<div>
|
||||
<label className="text-sm font-medium 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 a battery</option>
|
||||
{unassignedBatteries.map(bat => (
|
||||
<option key={bat.id} value={bat.id}>
|
||||
{bat.brand} {bat.model} - SN: {bat.serialNumber} (৳{bat.purchasePrice?.toLocaleString() || 0})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
setUnassignedBatteries(prev => prev.filter(b => !batteryIds.includes(b.id)));
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Co-ownership Share (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={assignBatteryForm.investorShare}
|
||||
onChange={(e) => setAssignBatteryForm({ ...assignBatteryForm, 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">Invested Amount (৳)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={assignBatteryForm.investedAmount}
|
||||
onChange={(e) => setAssignBatteryForm({ ...assignBatteryForm, investedAmount: Number(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
</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-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Security Deposit (৳)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={assignBatteryForm.deposit}
|
||||
onChange={(e) => setAssignBatteryForm({ ...assignBatteryForm, deposit: Number(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
const batteryNames = assignedBatteriesList.map(b => `${b.brand} ${b.model}`).join(', ');
|
||||
toast.success(`Successfully assigned ${batteryIds.length} battery/ies: ${batteryNames}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Daily Rent (৳)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={assignBatteryForm.rentPrice}
|
||||
onChange={(e) => setAssignBatteryForm({ ...assignBatteryForm, rentPrice: 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 hover:bg-slate-50">Cancel</button>
|
||||
<button onClick={handleAssignBattery} disabled={!selectedBatteryId} className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm hover:bg-emerald-700 disabled:opacity-50">Assign Battery</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{showRegisterBatteryModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
|
||||
@@ -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 assignedObjList: any[] = [];
|
||||
const chosenBatteries = unassignedBatteries.filter(b => selectedBatteryIds.includes(b.id));
|
||||
|
||||
chosenBatteries.forEach(batteryToAssign => {
|
||||
const assignedObj = {
|
||||
...batteryToAssign,
|
||||
investorId: investorId,
|
||||
investorName: investor.name,
|
||||
investorSharePercentage: Number(assignForm.investorShare),
|
||||
investedAmount: Number(assignForm.investedAmount),
|
||||
investmentId: assignForm.investmentId,
|
||||
investmentId: selectedBatteryPlanId,
|
||||
deposit: Number(assignForm.deposit),
|
||||
rentPrice: Number(assignForm.rentPrice),
|
||||
status: 'in-use'
|
||||
};
|
||||
assignedObjList.push(assignedObj);
|
||||
});
|
||||
|
||||
setBatteries(prev => [...prev, assignedObj]);
|
||||
setUnassignedBatteries(prev => prev.filter(b => b.id !== assignForm.batteryId));
|
||||
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
|
||||
});
|
||||
setUnassignedBatteries(prev => prev.filter(b => !batteryIds.includes(b.id)));
|
||||
|
||||
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;
|
||||
}));
|
||||
|
||||
const batteryNames = assignedBatteriesList.map(b => `${b.brand} ${b.model}`).join(', ');
|
||||
toast.success(`Successfully assigned ${batteryIds.length} battery/ies: ${batteryNames}`);
|
||||
}}
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
{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">
|
||||
|
||||
191
src/app/admin/investors/components/AssignBatteryModal.tsx
Normal file
191
src/app/admin/investors/components/AssignBatteryModal.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Battery, X } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface AssignBatteryModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
investor: any;
|
||||
batteries: any[];
|
||||
unassignedBatteries: any[];
|
||||
preselectedPlanId?: string;
|
||||
onAssign: (planId: string, batteryIds: string[]) => void;
|
||||
}
|
||||
|
||||
export default function AssignBatteryModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
investor,
|
||||
batteries,
|
||||
unassignedBatteries,
|
||||
preselectedPlanId = '',
|
||||
onAssign
|
||||
}: AssignBatteryModalProps) {
|
||||
const [selectedPlanId, setSelectedPlanId] = useState(preselectedPlanId);
|
||||
const [selectedBatteryIds, setSelectedBatteryIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedPlanId(preselectedPlanId);
|
||||
setSelectedBatteryIds([]);
|
||||
}
|
||||
}, [isOpen, preselectedPlanId]);
|
||||
|
||||
if (!isOpen || !investor) return null;
|
||||
|
||||
const getPlanTargetAssetCount = (plan: any) => {
|
||||
if (!plan) return 1;
|
||||
if (plan.assetType === 'battery' || plan.planName?.toLowerCase().includes('battery')) {
|
||||
if (plan.id === 'ip3') return 2;
|
||||
const nameLower = plan.planName?.toLowerCase() || '';
|
||||
if (nameLower.includes('10')) return 10;
|
||||
if (nameLower.includes('5')) return 5;
|
||||
return 1;
|
||||
} else {
|
||||
if (plan.id === 'ip1' || plan.id === 'ip2') return 1;
|
||||
const nameLower = plan.planName?.toLowerCase() || '';
|
||||
if (nameLower.includes('10')) return 10;
|
||||
if (nameLower.includes('5')) return 5;
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
const selectedPlan = investor.investments?.find((inv: any) => inv.id === selectedPlanId);
|
||||
const assignedCount = selectedPlan ? batteries.filter(b => b.investmentId === selectedPlan.id).length : 0;
|
||||
const targetCount = selectedPlan ? getPlanTargetAssetCount(selectedPlan) : 0;
|
||||
const remainingCapacity = selectedPlan ? Math.max(0, targetCount - assignedCount) : 0;
|
||||
|
||||
const handleAssignSubmit = () => {
|
||||
if (!selectedPlanId) {
|
||||
toast.error('Please select an investment plan');
|
||||
return;
|
||||
}
|
||||
if (selectedBatteryIds.length === 0) {
|
||||
toast.error('Please select at least one battery');
|
||||
return;
|
||||
}
|
||||
onAssign(selectedPlanId, selectedBatteryIds);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden flex flex-col">
|
||||
<div className="p-5 border-b border-emerald-100 bg-emerald-50 flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold text-emerald-800 flex items-center gap-2">
|
||||
<Battery className="w-5 h-5 text-emerald-600 animate-bounce" />
|
||||
Assign Battery to Partner
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-2 hover:bg-emerald-100 rounded-lg text-emerald-600">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-4 overflow-y-auto max-h-[70vh]">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-1.5 block">Link to Investment Plan *</label>
|
||||
<select
|
||||
value={selectedPlanId}
|
||||
disabled={!!preselectedPlanId}
|
||||
onChange={(e) => {
|
||||
setSelectedPlanId(e.target.value);
|
||||
setSelectedBatteryIds([]);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 disabled:bg-slate-50 disabled:text-slate-500"
|
||||
>
|
||||
<option value="">Select plan</option>
|
||||
{investor.investments?.filter((inv: any) => inv.assetType === 'battery' || inv.planName.toLowerCase().includes('battery')).map((inv: any) => {
|
||||
const curAssigned = batteries.filter(b => b.investmentId === inv.id).length;
|
||||
const target = getPlanTargetAssetCount(inv);
|
||||
const rem = Math.max(0, target - curAssigned);
|
||||
return (
|
||||
<option key={inv.id} value={inv.id}>
|
||||
{inv.planName} (Remaining: {rem} / {target} Pack{target !== 1 ? 's' : ''})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedPlanId && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-semibold text-slate-700 block">Select Battery Pack(s) *</label>
|
||||
<span className="text-xs font-bold text-emerald-700 bg-emerald-100 px-2 py-0.5 rounded-full">
|
||||
Selected: {selectedBatteryIds.length} / {remainingCapacity}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{remainingCapacity === 0 ? (
|
||||
<div className="p-3 text-center text-xs text-amber-700 bg-amber-50 rounded-lg border border-amber-200">
|
||||
This plan has reached its full capacity of {targetCount} battery pack(s). Unassign some batteries first to assign new ones.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-slate-200 rounded-lg max-h-56 overflow-y-auto divide-y divide-slate-100">
|
||||
{unassignedBatteries.map(bat => {
|
||||
const isChecked = selectedBatteryIds.includes(bat.id);
|
||||
const isDisabled = !isChecked && selectedBatteryIds.length >= remainingCapacity;
|
||||
return (
|
||||
<label
|
||||
key={bat.id}
|
||||
className={`flex items-center justify-between p-3 cursor-pointer text-sm transition-colors ${
|
||||
isChecked ? 'bg-emerald-50/50' : isDisabled ? 'opacity-50 cursor-not-allowed bg-slate-50' : 'hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
if (selectedBatteryIds.length < remainingCapacity) {
|
||||
setSelectedBatteryIds([...selectedBatteryIds, bat.id]);
|
||||
} else {
|
||||
toast.error(`Cannot select more than ${remainingCapacity} batteries`);
|
||||
}
|
||||
} else {
|
||||
setSelectedBatteryIds(selectedBatteryIds.filter(id => id !== bat.id));
|
||||
}
|
||||
}}
|
||||
className="rounded text-emerald-600 focus:ring-emerald-500 border-slate-300 w-4 h-4"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">{bat.brand} {bat.model}</p>
|
||||
<p className="text-xs text-slate-500">SN: {bat.serialNumber}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-slate-600 font-medium text-xs">৳{bat.purchasePrice?.toLocaleString() || 0}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{unassignedBatteries.length === 0 && (
|
||||
<div className="p-4 text-center text-slate-400 text-sm">No unassigned batteries available</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 bg-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAssignSubmit}
|
||||
disabled={!selectedPlanId || selectedBatteryIds.length === 0}
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-semibold hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-colors"
|
||||
>
|
||||
Assign {selectedBatteryIds.length > 0 ? `${selectedBatteryIds.length} Battery/ies` : 'Battery'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
191
src/app/admin/investors/components/AssignBikeModal.tsx
Normal file
191
src/app/admin/investors/components/AssignBikeModal.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Bike, X } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface AssignBikeModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
investor: any;
|
||||
bikes: any[];
|
||||
preselectedPlanId?: string;
|
||||
onAssign: (planId: string, bikeIds: string[]) => void;
|
||||
}
|
||||
|
||||
export default function AssignBikeModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
investor,
|
||||
bikes,
|
||||
preselectedPlanId = '',
|
||||
onAssign
|
||||
}: AssignBikeModalProps) {
|
||||
const [selectedPlanId, setSelectedPlanId] = useState(preselectedPlanId);
|
||||
const [selectedBikeIds, setSelectedBikeIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedPlanId(preselectedPlanId);
|
||||
setSelectedBikeIds([]);
|
||||
}
|
||||
}, [isOpen, preselectedPlanId]);
|
||||
|
||||
if (!isOpen || !investor) return null;
|
||||
|
||||
const getPlanTargetAssetCount = (plan: any) => {
|
||||
if (!plan) return 1;
|
||||
if (plan.assetType === 'battery' || plan.planName?.toLowerCase().includes('battery')) {
|
||||
if (plan.id === 'ip3') return 2;
|
||||
const nameLower = plan.planName?.toLowerCase() || '';
|
||||
if (nameLower.includes('10')) return 10;
|
||||
if (nameLower.includes('5')) return 5;
|
||||
return 1;
|
||||
} else {
|
||||
if (plan.id === 'ip1' || plan.id === 'ip2') return 1;
|
||||
const nameLower = plan.planName?.toLowerCase() || '';
|
||||
if (nameLower.includes('10')) return 10;
|
||||
if (nameLower.includes('5')) return 5;
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
const selectedPlan = investor.investments?.find((inv: any) => inv.id === selectedPlanId);
|
||||
const assignedCount = selectedPlan ? bikes.filter(b => b.investmentId === selectedPlan.id).length : 0;
|
||||
const targetCount = selectedPlan ? getPlanTargetAssetCount(selectedPlan) : 0;
|
||||
const remainingCapacity = selectedPlan ? Math.max(0, targetCount - assignedCount) : 0;
|
||||
|
||||
const availableBikes = bikes.filter(b => !b.investorId && b.status === 'available');
|
||||
|
||||
const handleAssignSubmit = () => {
|
||||
if (!selectedPlanId) {
|
||||
toast.error('Please select an investment plan');
|
||||
return;
|
||||
}
|
||||
if (selectedBikeIds.length === 0) {
|
||||
toast.error('Please select at least one bike');
|
||||
return;
|
||||
}
|
||||
onAssign(selectedPlanId, selectedBikeIds);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden flex flex-col">
|
||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold text-investor flex items-center gap-2">
|
||||
<Bike className="w-5 h-5 text-investor animate-bounce" />
|
||||
Assign Bike to Investor
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-4 overflow-y-auto max-h-[70vh]">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-600 mb-1.5 block">Select Investment Plan *</label>
|
||||
<select
|
||||
value={selectedPlanId}
|
||||
disabled={!!preselectedPlanId}
|
||||
onChange={(e) => {
|
||||
setSelectedPlanId(e.target.value);
|
||||
setSelectedBikeIds([]);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-investor disabled:bg-slate-50 disabled:text-slate-500"
|
||||
>
|
||||
<option value="">Choose an active plan</option>
|
||||
{investor.investments?.filter((inv: any) => inv.assetType === 'bike' || !inv.assetType || inv.planName.toLowerCase().includes('ev') || inv.planName.toLowerCase().includes('bike')).map((inv: any) => {
|
||||
const curAssigned = bikes.filter(b => b.investmentId === inv.id).length;
|
||||
const target = getPlanTargetAssetCount(inv);
|
||||
const rem = Math.max(0, target - curAssigned);
|
||||
return (
|
||||
<option key={inv.id} value={inv.id}>
|
||||
{inv.planName} (Remaining: {rem} / {target} Bike{target !== 1 ? 's' : ''})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedPlanId && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-semibold text-slate-700 block">Select Bike(s) *</label>
|
||||
<span className="text-xs font-bold text-investor bg-investor/10 px-2 py-0.5 rounded-full">
|
||||
Selected: {selectedBikeIds.length} / {remainingCapacity}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{remainingCapacity === 0 ? (
|
||||
<div className="p-3 text-center text-xs text-amber-700 bg-amber-50 rounded-lg border border-amber-200">
|
||||
This plan has reached its full capacity of {targetCount} bike(s). Unassign some bikes first to assign new ones.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-slate-200 rounded-lg max-h-48 overflow-y-auto divide-y divide-slate-100">
|
||||
{availableBikes.map(bike => {
|
||||
const isChecked = selectedBikeIds.includes(bike.id);
|
||||
const isDisabled = !isChecked && selectedBikeIds.length >= remainingCapacity;
|
||||
return (
|
||||
<label
|
||||
key={bike.id}
|
||||
className={`flex items-center justify-between p-3 cursor-pointer text-sm transition-colors ${
|
||||
isChecked ? 'bg-investor/5' : isDisabled ? 'opacity-50 cursor-not-allowed bg-slate-50' : 'hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
if (selectedBikeIds.length < remainingCapacity) {
|
||||
setSelectedBikeIds([...selectedBikeIds, bike.id]);
|
||||
} else {
|
||||
toast.error(`Cannot select more than ${remainingCapacity} bikes`);
|
||||
}
|
||||
} else {
|
||||
setSelectedBikeIds(selectedBikeIds.filter(id => id !== bike.id));
|
||||
}
|
||||
}}
|
||||
className="rounded text-investor focus:ring-investor border-slate-300 w-4 h-4"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">{bike.model} • {bike.brand}</p>
|
||||
<p className="text-xs text-slate-500">{bike.plateNumber}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-slate-600 font-medium text-xs">৳{bike.purchasePrice?.toLocaleString() || 0}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{availableBikes.length === 0 && (
|
||||
<div className="p-4 text-center text-slate-400 text-sm">No unassigned available bikes found</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 bg-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAssignSubmit}
|
||||
disabled={!selectedPlanId || selectedBikeIds.length === 0}
|
||||
className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-semibold hover:bg-investor-dark disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-colors"
|
||||
>
|
||||
Assign {selectedBikeIds.length > 0 ? `${selectedBikeIds.length} Bike(s)` : 'Bike'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/app/admin/investors/components/UnassignConfirmModal.tsx
Normal file
57
src/app/admin/investors/components/UnassignConfirmModal.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface UnassignConfirmModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
type: 'bike' | 'battery';
|
||||
name: string;
|
||||
details: string;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export default function UnassignConfirmModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
type,
|
||||
name,
|
||||
details,
|
||||
onConfirm
|
||||
}: UnassignConfirmModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-slate-100">
|
||||
<div className="p-6 text-center">
|
||||
<div className="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-red-100">
|
||||
<AlertTriangle className="w-8 h-8 text-red-600 animate-pulse" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-2">Unassign Confirmation</h3>
|
||||
<p className="text-sm text-slate-500 mb-6 px-1">
|
||||
Are you sure you want to unassign {type} <span className="font-semibold text-slate-800">{name}</span> ({details})?
|
||||
</p>
|
||||
|
||||
<div className="flex gap-3 justify-center">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 border border-slate-200 text-slate-600 rounded-xl text-sm font-semibold hover:bg-slate-50 transition-colors flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onClose();
|
||||
}}
|
||||
className="px-5 py-2.5 bg-red-600 text-white rounded-xl text-sm font-semibold hover:bg-red-700 hover:shadow-lg transition-all flex-1"
|
||||
>
|
||||
Unassign
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user