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 toast from 'react-hot-toast';
|
||||||
import { investors as initialInvestors, bikes as initialBikes, transactions as initialTransactions } from '@/data/mockData';
|
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 }> }) {
|
export default function InvestmentDetailPage({ params }: { params: Promise<{ id: string; investmentId: string }> }) {
|
||||||
const resolvedParams = use(params);
|
const resolvedParams = use(params);
|
||||||
const { id: investorId, investmentId } = resolvedParams;
|
const { id: investorId, investmentId } = resolvedParams;
|
||||||
@@ -291,6 +295,19 @@ export default function InvestmentDetailPage({ params }: { params: Promise<{ id:
|
|||||||
const [showAddBikeModal, setShowAddBikeModal] = useState(false);
|
const [showAddBikeModal, setShowAddBikeModal] = useState(false);
|
||||||
const [showRegisterBikeModal, setShowRegisterBikeModal] = useState(false);
|
const [showRegisterBikeModal, setShowRegisterBikeModal] = useState(false);
|
||||||
const [showAssignBatteryModal, setShowAssignBatteryModal] = 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 [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false);
|
||||||
|
|
||||||
const [selectedBikeId, setSelectedBikeId] = useState('');
|
const [selectedBikeId, setSelectedBikeId] = useState('');
|
||||||
@@ -1024,9 +1041,13 @@ export default function InvestmentDetailPage({ params }: { params: Promise<{ id:
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (confirm(`Are you sure you want to unassign battery ${bat.serialNumber}?`)) {
|
setUnassignConfirmModal({
|
||||||
handleUnassignBattery(bat.id);
|
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"
|
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) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (confirm(`Are you sure you want to unassign bike ${bike.model} (${bike.plateNumber})?`)) {
|
setUnassignConfirmModal({
|
||||||
handleUnassignBike(bike.id);
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAddBikeModal && (
|
<AssignBikeModal
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
isOpen={showAddBikeModal}
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
onClose={() => setShowAddBikeModal(false)}
|
||||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
investor={investor}
|
||||||
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
bikes={bikes}
|
||||||
<Bike className="w-5 h-5" /> Assign Bike to Investment
|
preselectedPlanId={investmentId}
|
||||||
</h3>
|
onAssign={(planId, bikeIds) => {
|
||||||
<button onClick={() => setShowAddBikeModal(false)} className="p-1 hover:bg-slate-100 rounded-lg">
|
const assignedBikesList: any[] = [];
|
||||||
<X className="w-5 h-5 text-slate-500" />
|
setBikes(prev => prev.map(b => {
|
||||||
</button>
|
if (bikeIds.includes(b.id)) {
|
||||||
</div>
|
assignedBikesList.push(b);
|
||||||
<div className="p-5 space-y-4">
|
return {
|
||||||
<div>
|
...b,
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Select Bike</label>
|
investorId: investorId,
|
||||||
<select
|
investorName: investor.name,
|
||||||
value={selectedBikeId}
|
investmentId: planId,
|
||||||
onChange={(e) => setSelectedBikeId(e.target.value)}
|
status: 'rented',
|
||||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-investor"
|
totalEarnings: b.totalEarnings || 0
|
||||||
>
|
};
|
||||||
<option value="">Choose a bike...</option>
|
}
|
||||||
{bikes.filter((b: any) => !b.investorId).map((bike: any) => (
|
return b;
|
||||||
<option key={bike.id} value={bike.id}>{bike.model} - {bike.plateNumber}</option>
|
}));
|
||||||
))}
|
|
||||||
</select>
|
setInvestors(prev => prev.map(inv => {
|
||||||
</div>
|
if (inv.id === investor.id) {
|
||||||
</div>
|
return {
|
||||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
|
...inv,
|
||||||
<button onClick={() => setShowAddBikeModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
investments: inv.investments?.map((item: any) => {
|
||||||
Cancel
|
if (item.id === planId) {
|
||||||
</button>
|
const currentBikeIds = item.bikeIds || [];
|
||||||
<button
|
const uniqueNewBikeIds = Array.from(new Set([...currentBikeIds, ...bikeIds]));
|
||||||
onClick={handleAssignBike}
|
return {
|
||||||
disabled={!selectedBikeId}
|
...item,
|
||||||
className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark disabled:opacity-50"
|
bikeIds: uniqueNewBikeIds
|
||||||
>
|
};
|
||||||
Assign Bike
|
}
|
||||||
</button>
|
return item;
|
||||||
</div>
|
})
|
||||||
</div>
|
};
|
||||||
</div>
|
}
|
||||||
)}
|
return inv;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bikeNames = assignedBikesList.map(b => `${b.model} (${b.plateNumber})`).join(', ');
|
||||||
|
toast.success(`Successfully assigned ${bikeIds.length} bike(s): ${bikeNames}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{showRegisterBikeModal && (
|
{showRegisterBikeModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showAssignBatteryModal && (
|
<AssignBatteryModal
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
isOpen={showAssignBatteryModal}
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
onClose={() => setShowAssignBatteryModal(false)}
|
||||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
investor={investor}
|
||||||
<h2 className="text-lg font-bold text-slate-800">Assign Battery to Investment</h2>
|
batteries={batteries}
|
||||||
<button onClick={() => setShowAssignBatteryModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
unassignedBatteries={unassignedBatteries}
|
||||||
<X className="w-5 h-5 text-slate-400" />
|
preselectedPlanId={investmentId}
|
||||||
</button>
|
onAssign={(planId, batteryIds) => {
|
||||||
</div>
|
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">
|
setUnassignedBatteries(prev => prev.filter(b => !batteryIds.includes(b.id)));
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
setInvestors(prev => prev.map(inv => {
|
||||||
<div>
|
if (inv.id === investor.id) {
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Co-ownership Share (%)</label>
|
return {
|
||||||
<input
|
...inv,
|
||||||
type="number"
|
investments: inv.investments?.map((item: any) => {
|
||||||
value={assignBatteryForm.investorShare}
|
if (item.id === planId) {
|
||||||
onChange={(e) => setAssignBatteryForm({ ...assignBatteryForm, investorShare: Number(e.target.value) })}
|
const currentBatteryIds = item.batteryIds || [];
|
||||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
const uniqueNewBatteryIds = Array.from(new Set([...currentBatteryIds, ...batteryIds]));
|
||||||
/>
|
return {
|
||||||
</div>
|
...item,
|
||||||
<div>
|
batteryIds: uniqueNewBatteryIds
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Invested Amount (৳)</label>
|
};
|
||||||
<input
|
}
|
||||||
type="number"
|
return item;
|
||||||
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"
|
}
|
||||||
/>
|
return inv;
|
||||||
</div>
|
}));
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
const batteryNames = assignedBatteriesList.map(b => `${b.brand} ${b.model}`).join(', ');
|
||||||
<div>
|
toast.success(`Successfully assigned ${batteryIds.length} battery/ies: ${batteryNames}`);
|
||||||
<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"
|
|
||||||
/>
|
/>
|
||||||
</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">
|
<UnassignConfirmModal
|
||||||
<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>
|
isOpen={unassignConfirmModal.show}
|
||||||
<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>
|
onClose={() => setUnassignConfirmModal(prev => ({ ...prev, show: false }))}
|
||||||
</div>
|
type={unassignConfirmModal.type}
|
||||||
</div>
|
name={unassignConfirmModal.name}
|
||||||
</div>
|
details={unassignConfirmModal.details}
|
||||||
)}
|
onConfirm={() => {
|
||||||
|
if (unassignConfirmModal.type === 'bike') {
|
||||||
|
handleUnassignBike(unassignConfirmModal.id);
|
||||||
|
} else {
|
||||||
|
handleUnassignBattery(unassignConfirmModal.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{showRegisterBatteryModal && (
|
{showRegisterBatteryModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<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
|
ShieldCheck, Building2, Users, Check, AlertOctagon, Activity, Award, Camera, History, Settings
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import AssignBikeModal from '../components/AssignBikeModal';
|
||||||
|
import AssignBatteryModal from '../components/AssignBatteryModal';
|
||||||
|
import UnassignConfirmModal from '../components/UnassignConfirmModal';
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
active: 'bg-green-100 text-green-700',
|
active: 'bg-green-100 text-green-700',
|
||||||
pending: 'bg-amber-100 text-amber-700',
|
pending: 'bg-amber-100 text-amber-700',
|
||||||
@@ -139,7 +143,42 @@ export default function InvestorDetailPage() {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
|
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
|
||||||
const [selectedBikeId, setSelectedBikeId] = useState('');
|
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 [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({
|
const [registerBikeForm, setRegisterBikeForm] = useState({
|
||||||
plateNumber: '',
|
plateNumber: '',
|
||||||
brand: 'Etron',
|
brand: 'Etron',
|
||||||
@@ -400,6 +439,8 @@ export default function InvestorDetailPage() {
|
|||||||
}, [batteries, investorId]);
|
}, [batteries, investorId]);
|
||||||
|
|
||||||
const [showAssignBatteryModal, setShowAssignBatteryModal] = useState(false);
|
const [showAssignBatteryModal, setShowAssignBatteryModal] = useState(false);
|
||||||
|
const [selectedBatteryIds, setSelectedBatteryIds] = useState<string[]>([]);
|
||||||
|
const [selectedBatteryPlanId, setSelectedBatteryPlanId] = useState('');
|
||||||
const [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false);
|
const [showRegisterBatteryModal, setShowRegisterBatteryModal] = useState(false);
|
||||||
const [showEditBatteryModal, setShowEditBatteryModal] = 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 availableBikesForAssignment = bikes.filter(b => !b.investorId && b.status === 'available');
|
||||||
|
|
||||||
const handleAssignBike = () => {
|
const handleAssignBike = () => {
|
||||||
if (!selectedBikeId) {
|
if (!selectedBikePlanId) {
|
||||||
toast.error('Please select a bike');
|
toast.error('Please select an investment plan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedBikeIds.length === 0) {
|
||||||
|
toast.error('Please select at least one bike');
|
||||||
return;
|
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 => {
|
setBikes(prev => prev.map(b => {
|
||||||
if (b.id === selectedBikeId) {
|
if (selectedBikeIds.includes(b.id)) {
|
||||||
|
assignedBikesList.push(b);
|
||||||
return {
|
return {
|
||||||
...b,
|
...b,
|
||||||
investorId: investorId,
|
investorId: investorId,
|
||||||
investorName: investor.name,
|
investorName: investor.name,
|
||||||
investmentId: evInv?.id || 'ip1',
|
investmentId: selectedBikePlanId,
|
||||||
status: 'rented',
|
status: 'rented',
|
||||||
totalEarnings: b.totalEarnings || 0
|
totalEarnings: b.totalEarnings || 0
|
||||||
};
|
};
|
||||||
@@ -521,9 +563,32 @@ export default function InvestorDetailPage() {
|
|||||||
return b;
|
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);
|
setShowAssignBikeModal(false);
|
||||||
setSelectedBikeId('');
|
setSelectedBikeIds([]);
|
||||||
|
setSelectedBikePlanId('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegisterAndAssignBike = () => {
|
const handleRegisterAndAssignBike = () => {
|
||||||
@@ -676,29 +741,62 @@ export default function InvestorDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAssignBattery = () => {
|
const handleAssignBattery = () => {
|
||||||
if (!assignForm.batteryId) {
|
if (!selectedBatteryPlanId) {
|
||||||
toast.error('Please select a battery');
|
toast.error('Please select an investment plan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedBatteryIds.length === 0) {
|
||||||
|
toast.error('Please select at least one battery');
|
||||||
return;
|
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 = {
|
const assignedObj = {
|
||||||
...batteryToAssign,
|
...batteryToAssign,
|
||||||
investorId: investorId,
|
investorId: investorId,
|
||||||
investorName: investor.name,
|
investorName: investor.name,
|
||||||
investorSharePercentage: Number(assignForm.investorShare),
|
investorSharePercentage: Number(assignForm.investorShare),
|
||||||
investedAmount: Number(assignForm.investedAmount),
|
investedAmount: Number(assignForm.investedAmount),
|
||||||
investmentId: assignForm.investmentId,
|
investmentId: selectedBatteryPlanId,
|
||||||
deposit: Number(assignForm.deposit),
|
deposit: Number(assignForm.deposit),
|
||||||
rentPrice: Number(assignForm.rentPrice),
|
rentPrice: Number(assignForm.rentPrice),
|
||||||
status: 'in-use'
|
status: 'in-use'
|
||||||
};
|
};
|
||||||
|
assignedObjList.push(assignedObj);
|
||||||
|
});
|
||||||
|
|
||||||
setBatteries(prev => [...prev, assignedObj]);
|
setBatteries(prev => [...prev, ...assignedObjList]);
|
||||||
setUnassignedBatteries(prev => prev.filter(b => b.id !== assignForm.batteryId));
|
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);
|
setShowAssignBatteryModal(false);
|
||||||
toast.success('Battery assigned to investment plan!');
|
setSelectedBatteryIds([]);
|
||||||
|
setSelectedBatteryPlanId('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegisterAndAssignBattery = () => {
|
const handleRegisterAndAssignBattery = () => {
|
||||||
@@ -1519,7 +1617,8 @@ export default function InvestorDetailPage() {
|
|||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedBikeId('');
|
setSelectedBikeIds([]);
|
||||||
|
setSelectedBikePlanId('');
|
||||||
setShowAssignBikeModal(true);
|
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"
|
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>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (confirm(`Are you sure you want to unassign bike ${bike.model} (${bike.plateNumber})?`)) {
|
setUnassignConfirmModal({
|
||||||
handleUnassignBike(bike.id);
|
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"
|
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,
|
investorShare: 100,
|
||||||
investedAmount: 45000
|
investedAmount: 45000
|
||||||
});
|
});
|
||||||
|
setSelectedBatteryIds([]);
|
||||||
|
setSelectedBatteryPlanId('');
|
||||||
setShowAssignBatteryModal(true);
|
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"
|
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
|
<Edit className="w-3 h-3" /> Edit
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
title="Unassign Battery"
|
||||||
>
|
>
|
||||||
@@ -2948,122 +3061,58 @@ export default function InvestorDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showAssignBatteryModal && (
|
<AssignBatteryModal
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
isOpen={showAssignBatteryModal}
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden flex flex-col">
|
onClose={() => setShowAssignBatteryModal(false)}
|
||||||
<div className="p-5 border-b border-emerald-100 bg-emerald-50 flex items-center justify-between">
|
investor={investor}
|
||||||
<h2 className="text-lg font-bold text-emerald-800 flex items-center gap-2">
|
batteries={batteries}
|
||||||
<Battery className="w-5 h-5 text-emerald-600 animate-bounce" />
|
unassignedBatteries={unassignedBatteries}
|
||||||
Assign Battery to Partner
|
onAssign={(planId, batteryIds) => {
|
||||||
</h2>
|
const assignedBatteriesList: any[] = [];
|
||||||
<button onClick={() => setShowAssignBatteryModal(false)} className="p-2 hover:bg-emerald-100 rounded-lg text-emerald-600">
|
setBatteries(prev => prev.map(b => {
|
||||||
<X className="w-5 h-5" />
|
if (batteryIds.includes(b.id)) {
|
||||||
</button>
|
assignedBatteriesList.push(b);
|
||||||
</div>
|
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]">
|
setUnassignedBatteries(prev => prev.filter(b => !batteryIds.includes(b.id)));
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Unassigned Battery *</label>
|
setInvestors(prev => prev.map(inv => {
|
||||||
<select
|
if (inv.id === investor.id) {
|
||||||
value={assignForm.batteryId}
|
return {
|
||||||
onChange={(e) => {
|
...inv,
|
||||||
const selected = unassignedBatteries.find(b => b.id === e.target.value);
|
investments: inv.investments?.map((item: any) => {
|
||||||
setAssignForm({
|
if (item.id === planId) {
|
||||||
...assignForm,
|
const currentBatteryIds = item.batteryIds || [];
|
||||||
batteryId: e.target.value,
|
const uniqueNewBatteryIds = Array.from(new Set([...currentBatteryIds, ...batteryIds]));
|
||||||
deposit: selected?.deposit || 5000,
|
return {
|
||||||
rentPrice: selected?.rentPrice || 150,
|
...item,
|
||||||
investedAmount: selected?.purchasePrice || 45000
|
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 && (
|
{showRegisterBatteryModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showAssignBikeModal && (
|
<AssignBikeModal
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
isOpen={showAssignBikeModal}
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
onClose={() => setShowAssignBikeModal(false)}
|
||||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
investor={investor}
|
||||||
<h2 className="text-lg font-bold text-investor flex items-center gap-2">
|
bikes={bikes}
|
||||||
<Bike className="w-5 h-5 text-investor animate-bounce" />
|
onAssign={(planId, bikeIds) => {
|
||||||
Assign Bike to Investor
|
const assignedBikesList: any[] = [];
|
||||||
</h2>
|
setBikes(prev => prev.map(b => {
|
||||||
<button onClick={() => setShowAssignBikeModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
if (bikeIds.includes(b.id)) {
|
||||||
<X className="w-5 h-5 text-slate-400" />
|
assignedBikesList.push(b);
|
||||||
</button>
|
return {
|
||||||
</div>
|
...b,
|
||||||
|
investorId: investorId,
|
||||||
|
investorName: investor.name,
|
||||||
|
investmentId: planId,
|
||||||
|
status: 'rented',
|
||||||
|
totalEarnings: b.totalEarnings || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}));
|
||||||
|
|
||||||
<div className="p-5">
|
setInvestors(prev => prev.map(inv => {
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
|
if (inv.id === investor.id) {
|
||||||
<select
|
return {
|
||||||
value={selectedBikeId}
|
...inv,
|
||||||
onChange={(e) => setSelectedBikeId(e.target.value)}
|
investments: inv.investments?.map((item: any) => {
|
||||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
if (item.id === planId) {
|
||||||
>
|
const currentBikeIds = item.bikeIds || [];
|
||||||
<option value="">Select a bike</option>
|
const uniqueNewBikeIds = Array.from(new Set([...currentBikeIds, ...bikeIds]));
|
||||||
{availableBikesForAssignment.map(bike => (
|
return {
|
||||||
<option key={bike.id} value={bike.id}>
|
...item,
|
||||||
{bike.model} - {bike.plateNumber} (৳{bike.purchasePrice?.toLocaleString() || 0})
|
bikeIds: uniqueNewBikeIds
|
||||||
</option>
|
};
|
||||||
))}
|
}
|
||||||
</select>
|
return item;
|
||||||
</div>
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return inv;
|
||||||
|
}));
|
||||||
|
|
||||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
|
const bikeNames = assignedBikesList.map(b => `${b.model} (${b.plateNumber})`).join(', ');
|
||||||
<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>
|
toast.success(`Successfully assigned ${bikeIds.length} bike(s): ${bikeNames}`);
|
||||||
<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>
|
<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 && (
|
{showRegisterBikeModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<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