refactor: simplify investor management UI and remove complex CRUD logic for table view
This commit is contained in:
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { investors as initialInvestors, bikes as initialBikes, transactions } from '@/data/mockData';
|
import { investors as initialInvestors, bikes as initialBikes } from '@/data/mockData';
|
||||||
import type { Investor } from '@/data/mockData';
|
import type { Investor } from '@/data/mockData';
|
||||||
import { Plus, Search, Filter, Download, Upload, Eye, Edit, Trash2, X, LayoutGrid, List, Phone, Mail, MapPin, Calendar, Banknote, TrendingUp, Wallet, AlertTriangle, User, FileText, CreditCard, Smartphone, Shield, Star, ExternalLink } from 'lucide-react';
|
|
||||||
|
type InvestorWithImage = Investor & { profileImage: string };
|
||||||
|
import { Search, Download, Upload, Edit, Trash2, X, LayoutGrid, List, Phone, Mail, Wallet, TrendingUp, Banknote, Bike, Eye } from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
active: 'bg-green-100 text-green-700',
|
active: 'bg-green-100 text-green-700',
|
||||||
@@ -27,164 +30,47 @@ const kycColors: Record<string, string> = {
|
|||||||
not_submitted: 'bg-slate-100 text-slate-500',
|
not_submitted: 'bg-slate-100 text-slate-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const investorsWithImages = initialInvestors.map((inv, idx) => ({
|
||||||
|
...inv,
|
||||||
|
profileImage: `https://picsum.photos/200/200?random=${idx + 1}`,
|
||||||
|
}));
|
||||||
|
|
||||||
export default function InvestorsPage() {
|
export default function InvestorsPage() {
|
||||||
const [investors, setInvestors] = useState<Investor[]>(initialInvestors);
|
const [investors, setInvestors] = useState<InvestorWithImage[]>(investorsWithImages as InvestorWithImage[]);
|
||||||
const [bikes, setBikes] = useState(initialBikes);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
const [planFilter, setPlanFilter] = useState('all');
|
const [kycFilter, setKycFilter] = useState('all');
|
||||||
const [selectedInvestor, setSelectedInvestor] = useState<Investor | null>(null);
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
|
||||||
const [editingInvestor, setEditingInvestor] = useState<Investor | null>(null);
|
|
||||||
const [activeTab, setActiveTab] = useState('personal');
|
|
||||||
const [sortField, setSortField] = useState<string>('name');
|
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
|
||||||
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
||||||
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState<{ show: boolean; investor: InvestorWithImage | null }>({ show: false, investor: null });
|
||||||
const [selectedBikeId, setSelectedBikeId] = useState('');
|
|
||||||
|
|
||||||
const filteredInvestors = investors.filter(inv => {
|
const filteredInvestors = investors.filter(inv => {
|
||||||
const matchesSearch = inv.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
const matchesSearch = inv.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
inv.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
inv.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
inv.phone.includes(searchQuery) ||
|
inv.phone.includes(searchQuery);
|
||||||
inv.id.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
const matchesStatus = statusFilter === 'all' || inv.status === statusFilter;
|
const matchesStatus = statusFilter === 'all' || inv.status === statusFilter;
|
||||||
const matchesPlan = planFilter === 'all' || (inv.investments?.some(inv => inv.planType === planFilter) ?? false);
|
const matchesKyc = kycFilter === 'all' || inv.kycStatus === kycFilter;
|
||||||
return matchesSearch && matchesStatus && matchesPlan;
|
return matchesSearch && matchesStatus && matchesKyc;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedInvestors = [...filteredInvestors].sort((a, b) => {
|
const handleDeleteInvestor = () => {
|
||||||
const aVal = String(a[sortField as keyof Investor] || '');
|
if (deleteModal.investor) {
|
||||||
const bVal = String(b[sortField as keyof Investor] || '');
|
setInvestors(investors.filter(i => i.id !== deleteModal.investor!.id));
|
||||||
if (sortOrder === 'asc') return aVal.localeCompare(bVal);
|
setDeleteModal({ show: false, investor: null });
|
||||||
return bVal.localeCompare(aVal);
|
toast.success('Investor deleted successfully');
|
||||||
});
|
|
||||||
|
|
||||||
const handleSort = (field: string) => {
|
|
||||||
if (sortField === field) {
|
|
||||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
||||||
} else {
|
|
||||||
setSortField(field);
|
|
||||||
setSortOrder('asc');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddInvestor = () => {
|
const getInitials = (name: string) => {
|
||||||
setEditingInvestor({
|
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||||
id: `INV${String(investors.length + 1).padStart(3, '0')}`,
|
|
||||||
userId: `u${100 + investors.length + 1}`,
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
phone: '',
|
|
||||||
address: '',
|
|
||||||
totalInvested: 0,
|
|
||||||
totalEarnings: 0,
|
|
||||||
activeBikes: 0,
|
|
||||||
withdrawalPending: 0,
|
|
||||||
totalWithdrawn: 0,
|
|
||||||
pendingEarnings: 0,
|
|
||||||
roi: 0,
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: new Date().toISOString().split('T')[0],
|
|
||||||
kycStatus: 'not_submitted',
|
|
||||||
riskLevel: 'low',
|
|
||||||
totalReferrals: 0,
|
|
||||||
referralEarnings: 0,
|
|
||||||
investments: [],
|
|
||||||
});
|
|
||||||
setShowModal(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditInvestor = (inv: Investor) => {
|
const isMobile = typeof window !== 'undefined' && window.innerWidth < 1024;
|
||||||
setEditingInvestor(JSON.parse(JSON.stringify(inv)));
|
|
||||||
setShowModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteInvestor = (id: string) => {
|
|
||||||
if (confirm('Are you sure you want to delete this investor?')) {
|
|
||||||
setInvestors(investors.filter(i => i.id !== id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveInvestor = () => {
|
|
||||||
if (editingInvestor) {
|
|
||||||
const existingIndex = investors.findIndex(i => i.id === editingInvestor.id);
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
const newInvestors = [...investors];
|
|
||||||
newInvestors[existingIndex] = editingInvestor;
|
|
||||||
setInvestors(newInvestors);
|
|
||||||
} else {
|
|
||||||
setInvestors([...investors, editingInvestor]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setShowModal(false);
|
|
||||||
setEditingInvestor(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAssignBike = () => {
|
|
||||||
if (selectedInvestor && selectedBikeId) {
|
|
||||||
const bikeIndex = bikes.findIndex(b => b.id === selectedBikeId);
|
|
||||||
if (bikeIndex >= 0) {
|
|
||||||
const updatedBikes = [...bikes];
|
|
||||||
updatedBikes[bikeIndex] = { ...updatedBikes[bikeIndex], investorId: selectedInvestor.id };
|
|
||||||
setBikes(updatedBikes);
|
|
||||||
|
|
||||||
const investorIndex = investors.findIndex(i => i.id === selectedInvestor.id);
|
|
||||||
if (investorIndex >= 0) {
|
|
||||||
const updatedInvestors = [...investors];
|
|
||||||
updatedInvestors[investorIndex] = {
|
|
||||||
...updatedInvestors[investorIndex],
|
|
||||||
activeBikes: updatedInvestors[investorIndex].activeBikes + 1,
|
|
||||||
totalInvested: updatedInvestors[investorIndex].totalInvested + (bikes[bikeIndex].purchasePrice || 0),
|
|
||||||
};
|
|
||||||
setInvestors(updatedInvestors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setShowAssignBikeModal(false);
|
|
||||||
setSelectedBikeId('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnassignBike = (bikeId: string) => {
|
|
||||||
if (selectedInvestor && confirm('Are you sure you want to unassign this bike from the investor?')) {
|
|
||||||
const bikeIndex = bikes.findIndex(b => b.id === bikeId);
|
|
||||||
if (bikeIndex >= 0) {
|
|
||||||
const bike = bikes[bikeIndex];
|
|
||||||
const updatedBikes = [...bikes];
|
|
||||||
updatedBikes[bikeIndex] = { ...updatedBikes[bikeIndex], investorId: undefined };
|
|
||||||
setBikes(updatedBikes);
|
|
||||||
|
|
||||||
const investorIndex = investors.findIndex(i => i.id === selectedInvestor.id);
|
|
||||||
if (investorIndex >= 0) {
|
|
||||||
const updatedInvestors = [...investors];
|
|
||||||
updatedInvestors[investorIndex] = {
|
|
||||||
...updatedInvestors[investorIndex],
|
|
||||||
activeBikes: Math.max(0, updatedInvestors[investorIndex].activeBikes - 1),
|
|
||||||
totalInvested: updatedInvestors[investorIndex].totalInvested - (bike.purchasePrice || 0),
|
|
||||||
};
|
|
||||||
setInvestors(updatedInvestors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const availableBikesForAssignment = bikes.filter(b => !b.investorId && b.status === 'available');
|
|
||||||
|
|
||||||
const assignedBikes = bikes.filter(b => b.investorId === selectedInvestor?.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 lg:p-6">
|
<div className="p-4 lg:p-6">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
<div className="mb-6">
|
||||||
<div>
|
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Investors</h1>
|
||||||
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Investor Management</h1>
|
<p className="text-sm text-slate-500 mt-1">Manage investor accounts and investments</p>
|
||||||
<p className="text-sm text-slate-500 mt-1">Manage investor accounts and investments</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button onClick={handleAddInvestor} className="py-2.5 px-4 bg-investor text-white rounded-lg font-semibold text-sm hover:bg-investor-dark transition-colors flex items-center gap-2">
|
|
||||||
<Plus className="w-4 h-4" /> Add Investor
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
@@ -213,7 +99,7 @@ export default function InvestorsPage() {
|
|||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
||||||
<Banknote className="w-6 h-6 text-blue-600" />
|
<Bike className="w-6 h-6 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-extrabold text-slate-800">{investors.reduce((sum, i) => sum + i.activeBikes, 0)}</p>
|
<p className="text-2xl font-extrabold text-slate-800">{investors.reduce((sum, i) => sum + i.activeBikes, 0)}</p>
|
||||||
@@ -224,7 +110,7 @@ export default function InvestorsPage() {
|
|||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center">
|
||||||
<Calendar className="w-6 h-6 text-amber-600" />
|
<Banknote className="w-6 h-6 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-extrabold text-slate-800">{investors.length}</p>
|
<p className="text-2xl font-extrabold text-slate-800">{investors.length}</p>
|
||||||
@@ -240,17 +126,17 @@ export default function InvestorsPage() {
|
|||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search investors by name, email, phone..."
|
placeholder="Search by name, email, phone..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
|
className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
|
||||||
>
|
>
|
||||||
<option value="all">All Status</option>
|
<option value="all">All Status</option>
|
||||||
<option value="active">Active</option>
|
<option value="active">Active</option>
|
||||||
@@ -259,37 +145,134 @@ export default function InvestorsPage() {
|
|||||||
<option value="suspended">Suspended</option>
|
<option value="suspended">Suspended</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={planFilter}
|
value={kycFilter}
|
||||||
onChange={(e) => setPlanFilter(e.target.value)}
|
onChange={(e) => setKycFilter(e.target.value)}
|
||||||
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
|
className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
|
||||||
>
|
>
|
||||||
<option value="all">All Plans</option>
|
<option value="all">All KYC</option>
|
||||||
<option value="silver">Silver</option>
|
<option value="verified">Verified</option>
|
||||||
<option value="gold">Gold</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="platinum">Platinum</option>
|
|
||||||
<option value="diamond">Diamond</option>
|
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode(viewMode === 'table' ? 'cards' : 'table')}
|
onClick={() => setViewMode(viewMode === 'table' ? 'cards' : 'table')}
|
||||||
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2"
|
className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{viewMode === 'table' ? <LayoutGrid className="w-4 h-4" /> : <List className="w-4 h-4" />}
|
<LayoutGrid className="w-4 h-4" />
|
||||||
{viewMode === 'table' ? 'Cards' : 'Table'}
|
{viewMode === 'table' ? 'Cards' : 'Table'}
|
||||||
</button>
|
</button>
|
||||||
<button className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
|
<button className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
|
||||||
<Download className="w-4 h-4" /> Export
|
<Download className="w-4 h-4" /> Export
|
||||||
</button>
|
</button>
|
||||||
<button className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
|
<button className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
|
||||||
<Upload className="w-4 h-4" /> Import
|
<Upload className="w-4 h-4" /> Import
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'table' ? (
|
{(viewMode === 'cards' || isMobile) ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 p-4">
|
||||||
|
{filteredInvestors.map(inv => (
|
||||||
|
<div key={inv.id} className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{inv.profileImage ? (
|
||||||
|
<img
|
||||||
|
src={inv.profileImage}
|
||||||
|
alt={inv.name}
|
||||||
|
className="w-12 h-12 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center">
|
||||||
|
<span className="text-sm font-bold text-purple-600">{getInitials(inv.name)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-800">{inv.name}</p>
|
||||||
|
<p className="text-xs text-slate-400">{inv.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${kycColors[inv.kycStatus]}`}>
|
||||||
|
{inv.kycStatus}
|
||||||
|
</span>
|
||||||
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${statusColors[inv.status]}`}>
|
||||||
|
{inv.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
<span>{inv.phone}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
|
<span className="truncate">{inv.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
<div className="bg-slate-50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-slate-400">Investment</p>
|
||||||
|
<p className="font-semibold text-purple-600">৳{inv.totalInvested.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-slate-400">Earnings</p>
|
||||||
|
<p className="font-semibold text-green-600">৳{inv.totalEarnings.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-slate-400">ROI</p>
|
||||||
|
<p className="font-semibold text-slate-700">{inv.roi}%</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-slate-400">Bikes</p>
|
||||||
|
<p className="font-semibold text-slate-700">{inv.activeBikes}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-slate-400">Plans</p>
|
||||||
|
<p className="font-semibold text-slate-700">{inv.investments?.length || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-slate-400">Plan Type</p>
|
||||||
|
<span className={`inline-flex text-xs font-medium px-2 py-0.5 rounded-full capitalize ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
|
||||||
|
{inv.investments?.[0]?.planType || 'silver'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 pt-3 border-t border-slate-100">
|
||||||
|
<Link
|
||||||
|
href={`/admin/investors/${inv.id}`}
|
||||||
|
className="flex-1 py-2 px-3 bg-slate-100 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-200 text-center"
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/admin/investors/${inv.id}`}
|
||||||
|
className="p-2 hover:bg-slate-100 rounded-lg"
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 text-slate-400" />
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteModal({ show: true, investor: inv })}
|
||||||
|
className="p-2 hover:bg-red-50 rounded-lg"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-red-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-slate-50">
|
<thead className="bg-slate-50">
|
||||||
<tr>
|
<tr>
|
||||||
|
{/* <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Avatar</th> */}
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investor</th>
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investor</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Contact</th>
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Contact</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investment</th>
|
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investment</th>
|
||||||
@@ -302,13 +285,21 @@ export default function InvestorsPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-50">
|
<tbody className="divide-y divide-slate-50">
|
||||||
{sortedInvestors.map(inv => (
|
{filteredInvestors.map(inv => (
|
||||||
<tr key={inv.id} className="hover:bg-slate-50 transition-colors">
|
<tr key={inv.id} className="hover:bg-slate-50 transition-colors">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<Link href={`/admin/investors/${inv.id}`} className="flex items-center gap-3 hover:bg-slate-50 -m-2 p-2 rounded-lg">
|
<Link href={`/admin/investors/${inv.id}`} className="flex items-center gap-3 hover:bg-slate-50 -m-2 p-2 rounded-lg">
|
||||||
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center">
|
{inv.profileImage ? (
|
||||||
<span className="text-sm font-bold text-purple-600">{inv.name.charAt(0)}</span>
|
<img
|
||||||
</div>
|
src={inv.profileImage}
|
||||||
|
alt={inv.name}
|
||||||
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center">
|
||||||
|
<span className="text-sm font-bold text-purple-600">{getInitials(inv.name)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-slate-700">{inv.name}</p>
|
<p className="text-sm font-medium text-slate-700">{inv.name}</p>
|
||||||
<p className="text-xs text-slate-400">{inv.id}</p>
|
<p className="text-xs text-slate-400">{inv.id}</p>
|
||||||
@@ -331,28 +322,32 @@ export default function InvestorsPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
|
||||||
{inv.investments?.length || 0} Plan{inv.investments?.length !== 1 ? 's' : ''}
|
{inv.investments?.[0]?.planType || 'silver'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[inv.kycStatus]}`}>
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${kycColors[inv.kycStatus]}`}>
|
||||||
{inv.kycStatus}
|
{inv.kycStatus}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[inv.status]}`}>
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${statusColors[inv.status]}`}>
|
||||||
{inv.status}
|
{inv.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => { setSelectedInvestor(inv); setShowDetailsModal(true); }} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
|
<Link href={`/admin/investors/${inv.id}`} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
|
||||||
<Eye className="w-4 h-4 text-blue-500" />
|
<Eye className="w-4 h-4 text-slate-400" />
|
||||||
</button>
|
</Link>
|
||||||
<button onClick={() => handleEditInvestor(inv)} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit">
|
<Link href={`/admin/investors/${inv.id}`} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit">
|
||||||
<Edit className="w-4 h-4 text-slate-400" />
|
<Edit className="w-4 h-4 text-slate-400" />
|
||||||
</button>
|
</Link>
|
||||||
<button onClick={() => handleDeleteInvestor(inv.id)} className="p-2 hover:bg-red-50 rounded-lg" title="Delete">
|
<button
|
||||||
|
onClick={() => setDeleteModal({ show: true, investor: inv })}
|
||||||
|
className="p-2 hover:bg-red-50 rounded-lg"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
<Trash2 className="w-4 h-4 text-red-400" />
|
<Trash2 className="w-4 h-4 text-red-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,510 +357,35 @@ export default function InvestorsPage() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
|
||||||
{sortedInvestors.map(inv => (
|
|
||||||
<Link key={inv.id} href={`/admin/investors/${inv.id}`} className="block bg-slate-50 rounded-xl p-4 hover:shadow-md transition-shadow">
|
|
||||||
<div className="flex items-start justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-12 h-12 rounded-xl bg-white flex items-center justify-center shadow-sm">
|
|
||||||
<span className="text-lg font-bold text-purple-600">{inv.name.charAt(0)}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-slate-700">{inv.name}</p>
|
|
||||||
<p className="text-xs text-slate-400">{inv.id}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button onClick={(e) => { e.preventDefault(); setSelectedInvestor(inv); setShowDetailsModal(true); }} className="p-1.5 hover:bg-white rounded-lg">
|
|
||||||
<Eye className="w-4 h-4 text-blue-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[inv.status]}`}>
|
|
||||||
{inv.status}
|
|
||||||
</span>
|
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
|
|
||||||
{inv.investments?.length || 0} Plans
|
|
||||||
</span>
|
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[inv.kycStatus]}`}>
|
|
||||||
{inv.kycStatus}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
||||||
<div className="bg-white rounded-lg p-2">
|
|
||||||
<p className="text-slate-400">Invested</p>
|
|
||||||
<p className="font-medium text-purple-600">৳{inv.totalInvested.toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg p-2">
|
|
||||||
<p className="text-slate-400">Earnings</p>
|
|
||||||
<p className="font-medium text-green-600">৳{inv.totalEarnings.toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg p-2">
|
|
||||||
<p className="text-slate-400">Bikes</p>
|
|
||||||
<p className="font-medium text-slate-700">{inv.activeBikes}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg p-2">
|
|
||||||
<p className="text-slate-400">ROI</p>
|
|
||||||
<p className="font-medium text-slate-700">{inv.roi}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showModal && editingInvestor && (
|
{deleteModal.show && deleteModal.investor && (
|
||||||
<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-4xl max-h-[90vh] 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-slate-800">
|
|
||||||
{investors.find(i => i.id === editingInvestor.id) ? 'Edit Investor' : 'Add New Investor'}
|
|
||||||
</h2>
|
|
||||||
<button onClick={() => setShowModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
|
||||||
<X className="w-5 h-5 text-slate-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-b border-slate-100 flex overflow-x-auto">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('personal')}
|
|
||||||
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'personal' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
|
|
||||||
>
|
|
||||||
<User className="w-4 h-4 inline mr-1" /> Personal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('financial')}
|
|
||||||
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'financial' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
|
|
||||||
>
|
|
||||||
<Banknote className="w-4 h-4 inline mr-1" /> Financial
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('payment')}
|
|
||||||
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'payment' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
|
|
||||||
>
|
|
||||||
<CreditCard className="w-4 h-4 inline mr-1" /> Payment
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('documents')}
|
|
||||||
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'documents' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
|
|
||||||
>
|
|
||||||
<FileText className="w-4 h-4 inline mr-1" /> Documents
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('investment')}
|
|
||||||
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'investment' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
|
|
||||||
>
|
|
||||||
<TrendingUp className="w-4 h-4 inline mr-1" /> Investment
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-5 overflow-y-auto flex-1">
|
|
||||||
{activeTab === 'personal' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Full Name *</label>
|
|
||||||
<input type="text" value={editingInvestor.name} onChange={(e) => setEditingInvestor({ ...editingInvestor, name: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Enter full name" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone Number *</label>
|
|
||||||
<input type="text" value={editingInvestor.phone} onChange={(e) => setEditingInvestor({ ...editingInvestor, phone: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="01XXXXXXXXX" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Alternate Phone</label>
|
|
||||||
<input type="text" value={editingInvestor.phoneAlt || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, phoneAlt: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Optional" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Email *</label>
|
|
||||||
<input type="email" value={editingInvestor.email} onChange={(e) => setEditingInvestor({ ...editingInvestor, email: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="email@example.com" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Date of Birth</label>
|
|
||||||
<input type="date" value={editingInvestor.dateOfBirth || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, dateOfBirth: 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">NID Number</label>
|
|
||||||
<input type="text" value={editingInvestor.nidNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, nidNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="NID Number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Address</label>
|
|
||||||
<textarea value={editingInvestor.address} onChange={(e) => setEditingInvestor({ ...editingInvestor, address: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter full address" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Emergency Contact</label>
|
|
||||||
<input type="text" value={editingInvestor.emergencyContactName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Contact Name" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Relation</label>
|
|
||||||
<input type="text" value={editingInvestor.emergencyContactRelation || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactRelation: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Relation" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone</label>
|
|
||||||
<input type="text" value={editingInvestor.emergencyContactPhone || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactPhone: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Phone" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
|
|
||||||
<select value={editingInvestor.status} onChange={(e) => setEditingInvestor({ ...editingInvestor, status: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="pending">Pending</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
<option value="suspended">Suspended</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">KYC Status</label>
|
|
||||||
<select value={editingInvestor.kycStatus} onChange={(e) => setEditingInvestor({ ...editingInvestor, kycStatus: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
|
||||||
<option value="verified">Verified</option>
|
|
||||||
<option value="pending">Pending</option>
|
|
||||||
<option value="rejected">Rejected</option>
|
|
||||||
<option value="not_submitted">Not Submitted</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'financial' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Invested (৳)</label>
|
|
||||||
<input type="number" value={editingInvestor.totalInvested} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalInvested: 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">Total Earnings (৳)</label>
|
|
||||||
<input type="number" value={editingInvestor.totalEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalEarnings: 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">Total Withdrawn (৳)</label>
|
|
||||||
<input type="number" value={editingInvestor.totalWithdrawn} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalWithdrawn: 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">Pending Earnings (৳)</label>
|
|
||||||
<input type="number" value={editingInvestor.pendingEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, pendingEarnings: 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">Withdrawal Pending (৳)</label>
|
|
||||||
<input type="number" value={editingInvestor.withdrawalPending} onChange={(e) => setEditingInvestor({ ...editingInvestor, withdrawalPending: 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">ROI (%)</label>
|
|
||||||
<input type="number" value={editingInvestor.roi} onChange={(e) => setEditingInvestor({ ...editingInvestor, roi: 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">Referral Earnings (৳)</label>
|
|
||||||
<input type="number" value={editingInvestor.referralEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, referralEarnings: 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">Total Referrals</label>
|
|
||||||
<input type="number" value={editingInvestor.totalReferrals} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalReferrals: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'payment' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Name</label>
|
|
||||||
<input type="text" value={editingInvestor.bankName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Bank Name" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Branch</label>
|
|
||||||
<input type="text" value={editingInvestor.bankBranch || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankBranch: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Branch" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Name</label>
|
|
||||||
<input type="text" value={editingInvestor.bankAccountName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankAccountName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Account Name" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Number</label>
|
|
||||||
<input type="text" value={editingInvestor.bankAccountNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankAccountNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Account Number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Mobile Banking</label>
|
|
||||||
<select value={editingInvestor.mobileBanking || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, mobileBanking: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
|
||||||
<option value="">Select</option>
|
|
||||||
<option value="Bkash">Bkash</option>
|
|
||||||
<option value="Nagad">Nagad</option>
|
|
||||||
<option value="Rocket">Rocket</option>
|
|
||||||
<option value="Upay">Upay</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Mobile Number</label>
|
|
||||||
<input type="text" value={editingInvestor.mobileBankingNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, mobileBankingNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Mobile Number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">TIN Number</label>
|
|
||||||
<input type="text" value={editingInvestor.tinNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, tinNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="TIN Number" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Passport Number</label>
|
|
||||||
<input type="text" value={editingInvestor.passportNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, passportNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Passport Number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'investment' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Active Bikes</label>
|
|
||||||
<input type="number" value={editingInvestor.activeBikes} onChange={(e) => setEditingInvestor({ ...editingInvestor, activeBikes: 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">Risk Level</label>
|
|
||||||
<select value={editingInvestor.riskLevel} onChange={(e) => setEditingInvestor({ ...editingInvestor, riskLevel: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
|
||||||
<option value="low">Low</option>
|
|
||||||
<option value="medium">Medium</option>
|
|
||||||
<option value="high">High</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-2 block">Investments ({editingInvestor.investments?.length || 0})</label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{editingInvestor.investments?.map((inv, idx) => (
|
|
||||||
<div key={idx} className="p-3 bg-slate-50 rounded-lg">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="font-medium text-sm">{inv.planName}</span>
|
|
||||||
<span className={`text-xs px-2 py-1 rounded-full ${planColors[inv.planType]}`}>{inv.planType}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2 mt-2 text-xs">
|
|
||||||
<div>
|
|
||||||
<span className="text-slate-400">Investment:</span>
|
|
||||||
<span className="font-medium ml-1">৳{inv.totalInvestment.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-slate-400">Monthly:</span>
|
|
||||||
<span className="font-medium ml-1">৳{inv.monthlyReturn.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<p className="text-sm text-slate-500 text-center py-2">Add investments from the single investor page</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Referral Code</label>
|
|
||||||
<input type="text" value={editingInvestor.referralCode || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, referralCode: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Referral Code" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Referred By</label>
|
|
||||||
<input type="text" value={editingInvestor.referredBy || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, referredBy: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Referrer ID" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Notes</label>
|
|
||||||
<textarea value={editingInvestor.notes || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, notes: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={3} placeholder="Additional notes..." />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'documents' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="border-2 border-dashed border-slate-200 rounded-lg p-8 text-center">
|
|
||||||
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-slate-500">Drag and drop files here, or click to browse</p>
|
|
||||||
<button className="mt-2 px-4 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm hover:bg-slate-200">Browse Files</button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{editingInvestor.kycDocuments?.map((doc, idx) => (
|
|
||||||
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<FileText className="w-5 h-5 text-slate-400" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-slate-700 capitalize">{doc.type.replace('_', ' ')}</p>
|
|
||||||
<p className="text-xs text-slate-400">{doc.number || 'No number'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${doc.verified ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
|
|
||||||
{doc.verified ? 'Verified' : 'Pending'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
|
|
||||||
<button onClick={() => setShowModal(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={handleSaveInvestor} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark">Save Investor</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showDetailsModal && selectedInvestor && (
|
|
||||||
<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-2xl max-h-[90vh] overflow-hidden">
|
|
||||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
|
||||||
<h2 className="text-lg font-bold text-slate-800">Investor Details</h2>
|
|
||||||
<button onClick={() => setShowDetailsModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
|
||||||
<X className="w-5 h-5 text-slate-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-5 overflow-y-auto max-h-[60vh]">
|
|
||||||
<div className="flex items-center gap-4 mb-6">
|
|
||||||
<div className="w-16 h-16 rounded-full bg-purple-100 flex items-center justify-center">
|
|
||||||
<span className="text-2xl font-bold text-purple-600">{selectedInvestor.name.charAt(0)}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-bold text-slate-800">{selectedInvestor.name}</h3>
|
|
||||||
<p className="text-sm text-slate-500">{selectedInvestor.id}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
|
||||||
<div className="bg-slate-50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-slate-500 mb-1">Total Invested</p>
|
|
||||||
<p className="text-xl font-bold text-purple-600">৳{selectedInvestor.totalInvested.toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-slate-500 mb-1">Total Earnings</p>
|
|
||||||
<p className="text-xl font-bold text-green-600">৳{selectedInvestor.totalEarnings.toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-slate-500 mb-1">Active Bikes</p>
|
|
||||||
<p className="text-xl font-bold text-slate-800">{selectedInvestor.activeBikes}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-slate-500 mb-1">ROI</p>
|
|
||||||
<p className="text-xl font-bold text-slate-800">{selectedInvestor.roi}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-slate-500">Phone</span>
|
|
||||||
<span className="font-medium">{selectedInvestor.phone}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-slate-500">Email</span>
|
|
||||||
<span className="font-medium">{selectedInvestor.email}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-slate-500">Investment Plans</span>
|
|
||||||
<span className="font-medium capitalize">{selectedInvestor.investments?.length || 0} Plans</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-slate-500">Status</span>
|
|
||||||
<span className={`font-medium capitalize ${selectedInvestor.status === 'active' ? 'text-green-600' : 'text-amber-600'}`}>{selectedInvestor.status}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-slate-500">KYC</span>
|
|
||||||
<span className={`font-medium capitalize ${selectedInvestor.kycStatus === 'verified' ? 'text-green-600' : 'text-amber-600'}`}>{selectedInvestor.kycStatus}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-slate-100">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h4 className="font-semibold text-slate-700">Assigned Bikes ({assignedBikes.length})</h4>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAssignBikeModal(true)}
|
|
||||||
className="text-sm text-investor hover:underline flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" /> Add Bike
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{assignedBikes.map(bike => (
|
|
||||||
<div key={bike.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center">
|
|
||||||
<Banknote className="w-5 h-5 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-slate-700">{bike.model}</p>
|
|
||||||
<p className="text-xs text-slate-400">{bike.plateNumber}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}`}>
|
|
||||||
{bike.status}
|
|
||||||
</span>
|
|
||||||
<button onClick={() => handleUnassignBike(bike.id)} className="p-1 hover:bg-red-50 rounded-lg text-red-400">
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{assignedBikes.length === 0 && (
|
|
||||||
<p className="text-center text-slate-400 py-4">No bikes assigned</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-5 border-t border-slate-100 flex justify-between">
|
|
||||||
<button onClick={() => handleDeleteInvestor(selectedInvestor.id)} className="px-4 py-2 border border-red-200 text-red-600 rounded-lg text-sm hover:bg-red-50">Delete</button>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button onClick={() => { handleEditInvestor(selectedInvestor); setShowDetailsModal(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Edit</button>
|
|
||||||
<Link href={`/admin/investors/${selectedInvestor.id}`} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark flex items-center gap-1">
|
|
||||||
View Full Page <ExternalLink className="w-4 h-4" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showAssignBikeModal && (
|
|
||||||
<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">
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
<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">
|
<div className="p-6 text-center">
|
||||||
<h2 className="text-lg font-bold text-slate-800">Assign Bike to Investor</h2>
|
<div className="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
|
||||||
<button onClick={() => setShowAssignBikeModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
|
<Trash2 className="w-6 h-6 text-red-600" />
|
||||||
<X className="w-5 h-5 text-slate-400" />
|
</div>
|
||||||
</button>
|
<h3 className="text-lg font-bold text-slate-800 mb-2">Delete Investor</h3>
|
||||||
|
<p className="text-slate-500 mb-1">
|
||||||
|
Are you sure you want to delete <span className="font-semibold text-slate-700">{deleteModal.investor.name}</span>?
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-red-500">This action cannot be undone. All investor data will be permanently removed.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-4 border-t border-slate-100 flex gap-3">
|
||||||
<div className="p-5">
|
<button
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
|
onClick={() => setDeleteModal({ show: false, investor: null })}
|
||||||
<select
|
className="flex-1 py-2.5 px-4 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50"
|
||||||
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>
|
Cancel
|
||||||
{availableBikesForAssignment.map(bike => (
|
</button>
|
||||||
<option key={bike.id} value={bike.id}>
|
<button
|
||||||
{bike.model} - {bike.plateNumber} (৳{bike.purchasePrice?.toLocaleString() || 0})
|
onClick={handleDeleteInvestor}
|
||||||
</option>
|
className="flex-1 py-2.5 px-4 bg-red-500 text-white rounded-lg text-sm font-medium hover:bg-red-600"
|
||||||
))}
|
>
|
||||||
</select>
|
Delete
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user