695 lines
34 KiB
TypeScript
695 lines
34 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
import { useParams, useRouter } from 'next/navigation';
|
|||
|
|
import {
|
|||
|
|
Shield, Check, Clock, Bike, User, Phone,
|
|||
|
|
MapPin, FileText, Image, DollarSign, Wrench, Battery,
|
|||
|
|
CheckCircle, XCircle, ArrowLeft, Save, Printer, Send,
|
|||
|
|
MessageSquare, Edit, UserCheck, Wallet, Store, Globe, Calendar, Briefcase, Plus, Upload
|
|||
|
|
} from 'lucide-react';
|
|||
|
|
|
|||
|
|
type ApplicationSource = 'app' | 'web' | 'walkin' | 'referral';
|
|||
|
|
type KYCType = 'biker' | 'investor' | 'shop' | 'merchant' | 'general';
|
|||
|
|
type RiderPlan = 'daily_rent' | 'weekly_rent' | 'monthly_rent' | 'rent_to_own' | 'share_ev';
|
|||
|
|
type VerificationStage = 'application' | 'document_collection' | 'risk_check' | 'plan_selection' | 'payment' | 'agreement' | 'allocated' | 'active';
|
|||
|
|
|
|||
|
|
interface Document {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
status: 'pending' | 'uploaded' | 'approved' | 'rejected';
|
|||
|
|
imageUrl?: string;
|
|||
|
|
rejectedReason?: string;
|
|||
|
|
uploadedAt?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface Request {
|
|||
|
|
id: string;
|
|||
|
|
applicationSource: ApplicationSource;
|
|||
|
|
sourceDetails?: string;
|
|||
|
|
name: string;
|
|||
|
|
phone: string;
|
|||
|
|
email: string;
|
|||
|
|
type: KYCType;
|
|||
|
|
status: 'pending' | 'documents_needed' | 'under_review' | 'risk_check' | 'approved' | 'rejected';
|
|||
|
|
verificationStage: VerificationStage;
|
|||
|
|
submittedAt: string;
|
|||
|
|
location: string;
|
|||
|
|
address: string;
|
|||
|
|
requiredDocuments: Document[];
|
|||
|
|
riderPlan?: RiderPlan;
|
|||
|
|
nomineeDetails?: { name: string; phone: string; relationship: string; nid: string };
|
|||
|
|
employmentInfo?: { company: string; dailyEarning: number; whyEV: string; experience: string };
|
|||
|
|
riskCheck?: { nidVerified: boolean; nomineeNidVerified: boolean; deliveryPlatformStatus: string; paymentReliability: string; notes: string };
|
|||
|
|
agreement?: { dailyRentObligation: number; latePenalty: number; signedAt?: string };
|
|||
|
|
evAllocation?: { evId: string; bikeModel: string; batteryId: string; hubLocation: string; gpsActivated: boolean };
|
|||
|
|
securityDeposit?: number;
|
|||
|
|
advancePayment?: number;
|
|||
|
|
paymentMethod?: 'bank' | 'wallet' | 'cash';
|
|||
|
|
bikeRequested?: string;
|
|||
|
|
scheduleDate?: string;
|
|||
|
|
notes: string[];
|
|||
|
|
messageHistory: { date: string; message: string; from: 'admin' | 'user' }[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mockRequests: Request[] = [
|
|||
|
|
{
|
|||
|
|
id: 'REQ001',
|
|||
|
|
applicationSource: 'app',
|
|||
|
|
name: 'Rahim Ahmed',
|
|||
|
|
phone: '01712345678',
|
|||
|
|
email: 'rahim@email.com',
|
|||
|
|
type: 'biker',
|
|||
|
|
status: 'pending',
|
|||
|
|
verificationStage: 'application',
|
|||
|
|
submittedAt: '2024-03-20',
|
|||
|
|
location: 'Gulshan, Dhaka',
|
|||
|
|
address: 'House 12, Road 5, Gulshan 1',
|
|||
|
|
requiredDocuments: [
|
|||
|
|
{ id: 'd1', name: 'NID Front', status: 'uploaded', uploadedAt: '2024-03-20' },
|
|||
|
|
{ id: 'd2', name: 'NID Back', status: 'uploaded', uploadedAt: '2024-03-20' },
|
|||
|
|
{ id: 'd3', name: 'Driving License', status: 'pending' },
|
|||
|
|
{ id: 'd4', name: 'Profile Photo', status: 'uploaded', uploadedAt: '2024-03-20' },
|
|||
|
|
],
|
|||
|
|
riderPlan: 'daily_rent',
|
|||
|
|
employmentInfo: { company: 'Foodpanda', dailyEarning: 2500, whyEV: 'Low maintenance, good for delivery', experience: '3 years bike riding' },
|
|||
|
|
nomineeDetails: { name: 'Fatema', phone: '01712345699', relationship: 'Wife', nid: '1234567890123' },
|
|||
|
|
securityDeposit: 5000,
|
|||
|
|
advancePayment: 500,
|
|||
|
|
bikeRequested: 'AIMA Lightning',
|
|||
|
|
notes: ['Downloaded app and applied through mobile'],
|
|||
|
|
messageHistory: [],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'REQ002',
|
|||
|
|
applicationSource: 'walkin',
|
|||
|
|
sourceDetails: 'Gulshan Hub',
|
|||
|
|
name: 'Karim Hasan',
|
|||
|
|
phone: '01712345679',
|
|||
|
|
email: 'karim@email.com',
|
|||
|
|
type: 'investor',
|
|||
|
|
status: 'documents_needed',
|
|||
|
|
verificationStage: 'document_collection',
|
|||
|
|
submittedAt: '2024-03-19',
|
|||
|
|
location: 'Banani, Dhaka',
|
|||
|
|
address: 'Flat 3B, House 22, Banani',
|
|||
|
|
requiredDocuments: [
|
|||
|
|
{ id: 'd5', name: 'NID', status: 'uploaded', uploadedAt: '2024-03-19' },
|
|||
|
|
{ id: 'd6', name: 'TIN Certificate', status: 'pending' },
|
|||
|
|
{ id: 'd7', name: 'Bank Statement', status: 'pending' },
|
|||
|
|
],
|
|||
|
|
notes: ['Walked in at Gulshan office - referred by current biker'],
|
|||
|
|
messageHistory: [],
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const statusColors: Record<string, string> = {
|
|||
|
|
pending: 'bg-amber-100 text-amber-700',
|
|||
|
|
documents_needed: 'bg-orange-100 text-orange-700',
|
|||
|
|
under_review: 'bg-blue-100 text-blue-700',
|
|||
|
|
risk_check: 'bg-purple-100 text-purple-700',
|
|||
|
|
approved: 'bg-green-100 text-green-700',
|
|||
|
|
rejected: 'bg-red-100 text-red-700',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const stageLabels: Record<string, string> = {
|
|||
|
|
application: 'Application',
|
|||
|
|
document_collection: 'Documents',
|
|||
|
|
risk_check: 'Risk Check',
|
|||
|
|
plan_selection: 'Plan Selection',
|
|||
|
|
payment: 'Payment',
|
|||
|
|
agreement: 'Agreement',
|
|||
|
|
allocated: 'EV Allocated',
|
|||
|
|
active: 'Active',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const sourceLabels: Record<string, string> = {
|
|||
|
|
app: 'Mobile App',
|
|||
|
|
web: 'Website',
|
|||
|
|
walkin: 'Walk-in',
|
|||
|
|
referral: 'Referral',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const planLabels: Record<string, string> = {
|
|||
|
|
daily_rent: 'Daily Rent',
|
|||
|
|
weekly_rent: 'Weekly Rent',
|
|||
|
|
monthly_rent: 'Monthly Rent',
|
|||
|
|
rent_to_own: 'Rent-to-Own',
|
|||
|
|
share_ev: 'Share EV',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const typeIcons: Record<string, any> = {
|
|||
|
|
biker: Bike,
|
|||
|
|
investor: DollarSign,
|
|||
|
|
shop: Store,
|
|||
|
|
merchant: User,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default function KYCDetailPage() {
|
|||
|
|
const params = useParams();
|
|||
|
|
const router = useRouter();
|
|||
|
|
const id = params.id as string;
|
|||
|
|
|
|||
|
|
const [request, setRequest] = useState<Request | null>(null);
|
|||
|
|
const [editMode, setEditMode] = useState(false);
|
|||
|
|
const [editForm, setEditForm] = useState<Partial<Request>>({});
|
|||
|
|
const [showMessageModal, setShowMessageModal] = useState(false);
|
|||
|
|
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
|||
|
|
const [newNoteText, setNewNoteText] = useState('');
|
|||
|
|
const [newMessageText, setNewMessageText] = useState('');
|
|||
|
|
const [showAddDocModal, setShowAddDocModal] = useState(false);
|
|||
|
|
const [newDocName, setNewDocName] = useState('');
|
|||
|
|
const [showUploadDocModal, setShowUploadDocModal] = useState(false);
|
|||
|
|
const [uploadDocId, setUploadDocId] = useState<string | null>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const found = mockRequests.find(r => r.id === id);
|
|||
|
|
if (found) {
|
|||
|
|
setRequest(found);
|
|||
|
|
setEditForm(found);
|
|||
|
|
}
|
|||
|
|
}, [id]);
|
|||
|
|
|
|||
|
|
if (!request) {
|
|||
|
|
return (
|
|||
|
|
<div className="p-6 flex items-center justify-center min-h-[50vh]">
|
|||
|
|
<div className="text-center">
|
|||
|
|
<Shield className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
|||
|
|
<p className="text-slate-500">Request not found</p>
|
|||
|
|
<button
|
|||
|
|
onClick={() => router.push('/admin/kyc')}
|
|||
|
|
className="mt-4 px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
Back to KYC
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSaveEdit = () => {
|
|||
|
|
setRequest(prev => prev ? { ...prev, ...editForm } : null);
|
|||
|
|
setEditMode(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleAddNote = () => {
|
|||
|
|
if (!request || !newNoteText.trim()) return;
|
|||
|
|
setRequest(prev => prev ? { ...prev, notes: [...prev.notes, newNoteText] } : null);
|
|||
|
|
setNewNoteText('');
|
|||
|
|
setShowAddNoteModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSendMessage = () => {
|
|||
|
|
if (!request || !newMessageText.trim()) return;
|
|||
|
|
setRequest(prev => prev ? {
|
|||
|
|
...prev,
|
|||
|
|
messageHistory: [...prev.messageHistory, { date: new Date().toISOString().split('T')[0], message: newMessageText, from: 'admin' as const }]
|
|||
|
|
} : null);
|
|||
|
|
setNewMessageText('');
|
|||
|
|
setShowMessageModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleAddDocument = () => {
|
|||
|
|
if (!request || !newDocName.trim()) return;
|
|||
|
|
setRequest(prev => prev ? {
|
|||
|
|
...prev,
|
|||
|
|
requiredDocuments: [...prev.requiredDocuments, { id: `doc-${Date.now()}`, name: newDocName, status: 'pending' as const }]
|
|||
|
|
} : null);
|
|||
|
|
setNewDocName('');
|
|||
|
|
setShowAddDocModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleApproveDocument = (docId: string) => {
|
|||
|
|
if (!request) return;
|
|||
|
|
setRequest(prev => prev ? {
|
|||
|
|
...prev,
|
|||
|
|
requiredDocuments: prev.requiredDocuments.map(doc =>
|
|||
|
|
doc.id === docId ? { ...doc, status: 'approved' as const } : doc
|
|||
|
|
)
|
|||
|
|
} : null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleRejectDocument = (docId: string) => {
|
|||
|
|
const reason = prompt('Enter rejection reason:');
|
|||
|
|
if (reason) {
|
|||
|
|
setRequest(prev => prev ? {
|
|||
|
|
...prev,
|
|||
|
|
requiredDocuments: prev.requiredDocuments.map(doc =>
|
|||
|
|
doc.id === docId ? { ...doc, status: 'rejected' as const, rejectedReason: reason } : doc
|
|||
|
|
)
|
|||
|
|
} : null);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleUploadDocument = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|||
|
|
const file = e.target.files?.[0];
|
|||
|
|
if (!file || !uploadDocId || !request) return;
|
|||
|
|
const imageUrl = URL.createObjectURL(file);
|
|||
|
|
setRequest(prev => prev ? {
|
|||
|
|
...prev,
|
|||
|
|
requiredDocuments: prev.requiredDocuments.map(doc =>
|
|||
|
|
doc.id === uploadDocId ? { ...doc, status: 'uploaded' as const, imageUrl, uploadedAt: new Date().toISOString() } : doc
|
|||
|
|
)
|
|||
|
|
} : null);
|
|||
|
|
setShowUploadDocModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const openUploadModal = (docId: string) => {
|
|||
|
|
setUploadDocId(docId);
|
|||
|
|
setShowUploadDocModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const TypeIcon = typeIcons[request.type];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
|||
|
|
<button
|
|||
|
|
onClick={() => router.push('/admin/kyc')}
|
|||
|
|
className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4"
|
|||
|
|
>
|
|||
|
|
<ArrowLeft className="w-4 h-4" /> Back to KYC
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
|||
|
|
<div className="p-6 border-b border-slate-100">
|
|||
|
|
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
|||
|
|
<div>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<h1 className="text-2xl font-extrabold text-slate-800">{request.id}</h1>
|
|||
|
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[request.status]}`}>
|
|||
|
|
{request.status.replace('_', ' ')}
|
|||
|
|
</span>
|
|||
|
|
{request.type === 'biker' && request.verificationStage && (
|
|||
|
|
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-cyan-100 text-cyan-700">
|
|||
|
|
{stageLabels[request.verificationStage]}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<p className="text-slate-500 mt-1">{request.name} • {request.submittedAt}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
{editMode ? (
|
|||
|
|
<>
|
|||
|
|
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
|||
|
|
<Save className="w-4 h-4" /> Save
|
|||
|
|
</button>
|
|||
|
|
<button onClick={() => { setEditForm(request); setEditMode(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
|||
|
|
Cancel
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
{request.type === 'biker' && request.status !== 'approved' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
if (confirm('Approve this request and create biker profile?')) {
|
|||
|
|
setRequest(prev => prev ? { ...prev, status: 'approved', verificationStage: 'active' } : null);
|
|||
|
|
alert('Biker created successfully!');
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<Bike className="w-4 h-4" /> Make Biker
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{request.type === 'investor' && request.status !== 'approved' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
if (confirm('Approve this request and create investor profile?')) {
|
|||
|
|
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
|
|||
|
|
alert('Investor created successfully!');
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<DollarSign className="w-4 h-4" /> Make Investor
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{request.type === 'shop' && request.status !== 'approved' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
if (confirm('Approve this request and create shop profile?')) {
|
|||
|
|
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
|
|||
|
|
alert('Shop created successfully!');
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<Store className="w-4 h-4" /> Make Shop
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{request.type === 'merchant' && request.status !== 'approved' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
if (confirm('Approve this request and create merchant profile?')) {
|
|||
|
|
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
|
|||
|
|
alert('Merchant created successfully!');
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="px-4 py-2 bg-orange-600 text-white rounded-lg text-sm hover:bg-orange-700 flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<User className="w-4 h-4" /> Make Merchant
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
|||
|
|
<Edit className="w-4 h-4" /> Edit
|
|||
|
|
</button>
|
|||
|
|
<button onClick={() => setShowAddNoteModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
|||
|
|
<MessageSquare className="w-4 h-4" /> Note
|
|||
|
|
</button>
|
|||
|
|
<button onClick={() => setShowMessageModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
|||
|
|
<Send className="w-4 h-4" /> Message
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
|
|||
|
|
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
|
|||
|
|
<User className="w-5 h-5" /> Personal Info
|
|||
|
|
</h3>
|
|||
|
|
{editMode ? (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.name || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
|||
|
|
placeholder="Full Name"
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.phone || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
|
|||
|
|
placeholder="Phone"
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
<input
|
|||
|
|
type="email"
|
|||
|
|
value={editForm.email || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
|
|||
|
|
placeholder="Email"
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.location || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, location: e.target.value })}
|
|||
|
|
placeholder="Location"
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-blue-600">Name</span><span className="text-sm font-medium text-blue-800">{request.name}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-blue-600">Phone</span><span className="text-sm font-medium text-blue-800">{request.phone}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-blue-600">Email</span><span className="text-sm font-medium text-blue-800">{request.email || '-'}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-blue-600">Location</span><span className="text-sm font-medium text-blue-800">{request.location}</span></div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
|
|||
|
|
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
|
|||
|
|
<Globe className="w-5 h-5" /> Source
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-green-600">Source</span><span className="text-sm font-medium text-green-800">{sourceLabels[request.applicationSource]}</span></div>
|
|||
|
|
{request.sourceDetails && <div className="flex justify-between"><span className="text-sm text-green-600">Details</span><span className="text-sm font-medium text-green-800">{request.sourceDetails}</span></div>}
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-green-600">Type</span><span className="text-sm font-medium text-green-800 capitalize flex items-center gap-1"><TypeIcon className="w-4 h-4" /> {request.type}</span></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{request.type === 'biker' && (
|
|||
|
|
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
|||
|
|
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
|||
|
|
<Calendar className="w-5 h-5" /> Plan Selection
|
|||
|
|
</h3>
|
|||
|
|
{editMode ? (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<select
|
|||
|
|
value={editForm.riderPlan || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, riderPlan: e.target.value as RiderPlan })}
|
|||
|
|
className="w-full px-3 py-2 border border-purple-200 rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="">Select Plan</option>
|
|||
|
|
<option value="daily_rent">Daily Rent</option>
|
|||
|
|
<option value="weekly_rent">Weekly Rent</option>
|
|||
|
|
<option value="monthly_rent">Monthly Rent</option>
|
|||
|
|
<option value="rent_to_own">Rent-to-Own</option>
|
|||
|
|
<option value="share_ev">Share EV</option>
|
|||
|
|
</select>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.bikeRequested || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, bikeRequested: e.target.value })}
|
|||
|
|
placeholder="Bike Model"
|
|||
|
|
className="w-full px-3 py-2 border border-purple-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-purple-600">Plan</span><span className="text-sm font-medium text-purple-800">{request.riderPlan ? planLabels[request.riderPlan] : '-'}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-purple-600">Bike</span><span className="text-sm font-medium text-purple-800">{request.bikeRequested || '-'}</span></div>
|
|||
|
|
{request.scheduleDate && <div className="flex justify-between"><span className="text-sm text-purple-600">Schedule</span><span className="text-sm font-medium text-purple-800">{request.scheduleDate}</span></div>}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{request.type === 'biker' && request.nomineeDetails && (
|
|||
|
|
<div className="bg-pink-50 p-4 rounded-xl border border-pink-100">
|
|||
|
|
<h3 className="font-semibold text-pink-800 mb-3 flex items-center gap-2">
|
|||
|
|
<User className="w-5 h-5" /> Nominee Details
|
|||
|
|
</h3>
|
|||
|
|
{editMode ? (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.nomineeDetails?.name || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, nomineeDetails: { ...editForm.nomineeDetails!, name: e.target.value } })}
|
|||
|
|
placeholder="Nominee Name"
|
|||
|
|
className="w-full px-3 py-2 border border-pink-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.nomineeDetails?.phone || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, nomineeDetails: { ...editForm.nomineeDetails!, phone: e.target.value } })}
|
|||
|
|
placeholder="Phone"
|
|||
|
|
className="w-full px-3 py-2 border border-pink-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.nomineeDetails?.relationship || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, nomineeDetails: { ...editForm.nomineeDetails!, relationship: e.target.value } })}
|
|||
|
|
placeholder="Relationship"
|
|||
|
|
className="w-full px-3 py-2 border border-pink-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-pink-600">Name</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.name}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-pink-600">Phone</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.phone}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-pink-600">Relationship</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.relationship}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-pink-600">NID</span><span className="text-sm font-medium text-pink-800">{request.nomineeDetails.nid}</span></div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
|
|||
|
|
<div className="flex items-center justify-between mb-3">
|
|||
|
|
<h3 className="font-semibold text-amber-800 flex items-center gap-2">
|
|||
|
|
<FileText className="w-5 h-5" /> Documents ({request.requiredDocuments.length})
|
|||
|
|
</h3>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowAddDocModal(true)}
|
|||
|
|
className="text-xs px-2 py-1 bg-white rounded border border-amber-200 text-amber-700 hover:bg-amber-100 flex items-center gap-1"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-3 h-3" /> Add
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{request.requiredDocuments.map((doc) => (
|
|||
|
|
<div key={doc.id} className="flex items-center justify-between p-2 bg-white rounded-lg">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{doc.status === 'pending' && <Clock className="w-4 h-4 text-amber-400" />}
|
|||
|
|
{doc.status === 'uploaded' && <FileText className="w-4 h-4 text-blue-400" />}
|
|||
|
|
{doc.status === 'approved' && <CheckCircle className="w-4 h-4 text-green-500" />}
|
|||
|
|
{doc.status === 'rejected' && <XCircle className="w-4 h-4 text-red-500" />}
|
|||
|
|
<span className="text-sm text-slate-700">{doc.name}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
{doc.status === 'pending' && (
|
|||
|
|
<button onClick={() => openUploadModal(doc.id)} className="p-1 bg-amber-100 text-amber-600 rounded hover:bg-amber-200" title="Upload"><Upload className="w-4 h-4" /></button>
|
|||
|
|
)}
|
|||
|
|
{(doc.status === 'uploaded' || doc.status === 'approved') && doc.imageUrl && (
|
|||
|
|
<button onClick={() => window.open(doc.imageUrl, '_blank')} className="p-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200" title="View"><Image className="w-4 h-4" /></button>
|
|||
|
|
)}
|
|||
|
|
{doc.status === 'uploaded' && (
|
|||
|
|
<>
|
|||
|
|
<button onClick={() => handleApproveDocument(doc.id)} className="p-1 bg-green-100 text-green-600 rounded hover:bg-green-200" title="Approve"><CheckCircle className="w-4 h-4" /></button>
|
|||
|
|
<button onClick={() => handleRejectDocument(doc.id)} className="p-1 bg-red-100 text-red-600 rounded hover:bg-red-200" title="Reject"><XCircle className="w-4 h-4" /></button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
{doc.status === 'approved' && <span className="text-xs px-2 py-1 bg-green-100 text-green-700 rounded-full">Approved</span>}
|
|||
|
|
{doc.status === 'rejected' && <span className="text-xs px-2 py-1 bg-red-100 text-red-700 rounded-full">Rejected</span>}
|
|||
|
|
{doc.status === 'pending' && <span className="text-xs px-2 py-1 bg-amber-100 text-amber-700 rounded-full">Pending</span>}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{request.type === 'biker' && request.employmentInfo && (
|
|||
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
|||
|
|
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
|||
|
|
<Briefcase className="w-5 h-5" /> Employment
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-slate-600">Company</span><span className="text-sm font-medium text-slate-800">{request.employmentInfo.company}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-slate-600">Daily Earning</span><span className="text-sm font-medium text-slate-800">৳{request.employmentInfo.dailyEarning}</span></div>
|
|||
|
|
<div className="flex justify-between"><span className="text-sm text-slate-600">Experience</span><span className="text-sm font-medium text-slate-800">{request.employmentInfo.experience}</span></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
|||
|
|
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
|||
|
|
<MessageSquare className="w-5 h-5" /> Notes ({request.notes.length})
|
|||
|
|
</h3>
|
|||
|
|
{request.notes.length > 0 ? (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{request.notes.map((note, idx) => (
|
|||
|
|
<div key={idx} className="text-sm text-slate-600 p-2 bg-white rounded-lg">• {note}</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm text-slate-400">No notes yet</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-indigo-50 p-4 rounded-xl border border-indigo-100">
|
|||
|
|
<h3 className="font-semibold text-indigo-800 mb-3 flex items-center gap-2">
|
|||
|
|
<Send className="w-5 h-5" /> Messages ({request.messageHistory.length})
|
|||
|
|
</h3>
|
|||
|
|
{request.messageHistory.length > 0 ? (
|
|||
|
|
<div className="space-y-2 max-h-40 overflow-y-auto">
|
|||
|
|
{request.messageHistory.map((msg, idx) => (
|
|||
|
|
<div key={idx} className={`text-sm p-2 rounded-lg ${msg.from === 'admin' ? 'bg-blue-100 text-blue-800' : 'bg-white text-slate-600'}`}>
|
|||
|
|
<span className="font-medium">{msg.from === 'admin' ? 'Admin' : 'User'}:</span> {msg.message}
|
|||
|
|
<span className="text-xs text-slate-400 ml-2">{msg.date}</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm text-indigo-400">No messages yet</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{showMessageModal && (
|
|||
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|||
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
|||
|
|
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Send Message</h3>
|
|||
|
|
<button onClick={() => setShowMessageModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4">
|
|||
|
|
<textarea
|
|||
|
|
value={newMessageText}
|
|||
|
|
onChange={(e) => setNewMessageText(e.target.value)}
|
|||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="Type message to send to user..."
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
|||
|
|
<button onClick={() => setShowMessageModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|||
|
|
<button onClick={handleSendMessage} disabled={!newMessageText.trim()} className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50">Send</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{showAddNoteModal && (
|
|||
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|||
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
|||
|
|
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Add Note</h3>
|
|||
|
|
<button onClick={() => setShowAddNoteModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4">
|
|||
|
|
<textarea
|
|||
|
|
value={newNoteText}
|
|||
|
|
onChange={(e) => setNewNoteText(e.target.value)}
|
|||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="Enter note..."
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
|||
|
|
<button onClick={() => setShowAddNoteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|||
|
|
<button onClick={handleAddNote} disabled={!newNoteText.trim()} className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50">Save</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{showAddDocModal && (
|
|||
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|||
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
|||
|
|
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Request New Document</h3>
|
|||
|
|
<button onClick={() => setShowAddDocModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={newDocName}
|
|||
|
|
onChange={(e) => setNewDocName(e.target.value)}
|
|||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
|||
|
|
placeholder="Document name..."
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
|||
|
|
<button onClick={() => setShowAddDocModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|||
|
|
<button onClick={handleAddDocument} disabled={!newDocName.trim()} className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50">Add Document</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{showUploadDocModal && (
|
|||
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|||
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
|||
|
|
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Upload Document</h3>
|
|||
|
|
<button onClick={() => setShowUploadDocModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4">
|
|||
|
|
<input
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*,.pdf"
|
|||
|
|
onChange={handleUploadDocument}
|
|||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm file:mr-4 file:px-4 file:py-2 file:bg-accent file:text-white file:rounded-lg file:border-0 cursor-pointer"
|
|||
|
|
/>
|
|||
|
|
<p className="text-xs text-slate-500 mt-2">Supported: JPG, PNG, PDF</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
|||
|
|
<button onClick={() => setShowUploadDocModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|