feat: expand KYC types with swapstation support and implement SMS history tracking with templating system

This commit is contained in:
sazzadulalambd
2026-05-05 02:32:51 +06:00
parent 7ff02cf732
commit 37f86c2363
2 changed files with 131 additions and 50 deletions

View File

@@ -11,7 +11,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
type ApplicationSource = 'app' | 'web' | 'walkin' | 'referral'; type ApplicationSource = 'app' | 'web' | 'walkin' | 'referral';
type KYCType = 'biker' | 'investor' | 'shop' | 'merchant' | 'general'; type KYCType = 'biker' | 'investor' | 'swapstation' | 'merchant' | 'general';
type RiderPlan = 'daily_rent' | 'weekly_rent' | 'monthly_rent' | 'rent_to_own' | 'share_ev'; 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'; type VerificationStage = 'application' | 'document_collection' | 'risk_check' | 'plan_selection' | 'payment' | 'agreement' | 'allocated' | 'active';
@@ -24,6 +24,38 @@ interface Document {
uploadedAt?: string; uploadedAt?: string;
} }
interface InvestmentPlan {
planName: string;
planType: string;
bikes: number;
amount: number;
monthlyReturn: number;
expectedROI: number;
}
interface SwapStationPlan {
planType: string;
cabinets: number;
amount: number;
batteries: number;
monthlyRent: number;
}
interface MerchantPlan {
companyCategory: string;
bikersRequested: number;
amount: number;
monthlyBudget: number;
requiredArea: string;
}
interface SmsHistory {
id: string;
message: string;
sentAt: string;
sentBy: string;
}
interface Request { interface Request {
id: string; id: string;
applicationSource: ApplicationSource; applicationSource: ApplicationSource;
@@ -42,6 +74,9 @@ interface Request {
riderPlan?: RiderPlan; riderPlan?: RiderPlan;
nomineeDetails?: { name: string; phone: string; relationship: string; nid: string }; nomineeDetails?: { name: string; phone: string; relationship: string; nid: string };
employmentInfo?: { company: string; dailyEarning: number; whyEV: string; experience: string }; employmentInfo?: { company: string; dailyEarning: number; whyEV: string; experience: string };
investmentPlan?: InvestmentPlan;
swapStationPlan?: SwapStationPlan;
merchantPlan?: MerchantPlan;
riskCheck?: { nidVerified: boolean; nomineeNidVerified: boolean; deliveryPlatformStatus: string; paymentReliability: string; notes: string }; riskCheck?: { nidVerified: boolean; nomineeNidVerified: boolean; deliveryPlatformStatus: string; paymentReliability: string; notes: string };
agreement?: { dailyRentObligation: number; latePenalty: number; signedAt?: string }; agreement?: { dailyRentObligation: number; latePenalty: number; signedAt?: string };
evAllocation?: { evId: string; bikeModel: string; batteryId: string; hubLocation: string; gpsActivated: boolean }; evAllocation?: { evId: string; bikeModel: string; batteryId: string; hubLocation: string; gpsActivated: boolean };
@@ -51,7 +86,7 @@ interface Request {
bikeRequested?: string; bikeRequested?: string;
scheduleDate?: string; scheduleDate?: string;
notes: string[]; notes: string[];
messageHistory: { date: string; message: string; from: 'admin' | 'user' }[]; smsHistory?: SmsHistory[];
} }
const mockRequests: Request[] = [ const mockRequests: Request[] = [
@@ -80,13 +115,14 @@ const mockRequests: Request[] = [
advancePayment: 500, advancePayment: 500,
bikeRequested: 'AIMA Lightning', bikeRequested: 'AIMA Lightning',
notes: ['Downloaded app and applied through mobile'], notes: ['Downloaded app and applied through mobile'],
messageHistory: [], smsHistory: [
{ id: 'sms1', message: 'Your KYC application is under review', sentAt: '2024-03-21', sentBy: 'admin' },
],
}, },
{ {
id: 'REQ002', id: 'REQ002',
applicationSource: 'walkin', applicationSource: 'walkin',
sourceDetails: 'Gulshan Hub', hubId: 'hub2',
hubId: 'hub1',
name: 'Karim Hasan', name: 'Karim Hasan',
phone: '01712345679', phone: '01712345679',
email: 'karim@email.com', email: 'karim@email.com',
@@ -102,7 +138,58 @@ const mockRequests: Request[] = [
{ id: 'd7', name: 'Bank Statement', status: 'pending' }, { id: 'd7', name: 'Bank Statement', status: 'pending' },
], ],
notes: ['Walked in at Gulshan office - referred by current biker'], notes: ['Walked in at Gulshan office - referred by current biker'],
messageHistory: [], smsHistory: [],
investmentPlan: { planName: 'Gold Plan', planType: 'Gold', bikes: 5, amount: 2500000, monthlyReturn: 8, expectedROI: 96 },
nomineeDetails: { name: 'John Doe', phone: '01712345690', relationship: 'Brother', nid: '1234567890124' },
},
{
id: 'REQ003',
applicationSource: 'web',
hubId: 'hub1',
name: 'Dhaka Swap Station',
phone: '01712345680',
email: 'dhakaswap@email.com',
type: 'swapstation',
status: 'under_review',
verificationStage: 'document_collection',
submittedAt: '2024-03-18',
location: 'Gulshan, Dhaka',
address: 'Plot 45, Road 11, Gulshan 1',
requiredDocuments: [
{ id: 'd8', name: 'Trade License', status: 'uploaded', uploadedAt: '2024-03-18' },
{ id: 'd9', name: 'Owner NID', status: 'uploaded', uploadedAt: '2024-03-18' },
{ id: 'd10', name: 'Station Photos', status: 'pending' },
{ id: 'd11', name: 'Electricity Bill', status: 'pending' },
],
employmentInfo: { company: 'Dhaka Swap Station', dailyEarning: 0, whyEV: 'https://maps.google.com/...', experience: 'Battery swap station' },
swapStationPlan: { planType: 'Premium', cabinets: 12, amount: 1800000, batteries: 24, monthlyRent: 45000 },
nomineeDetails: { name: 'Mahbub', phone: '01712345691', relationship: 'Brother', nid: '1234567890125' },
notes: ['Located in commercial area'],
smsHistory: [],
},
{
id: 'REQ004',
applicationSource: 'app',
hubId: 'hub3',
name: 'Foodpanda Bangladesh',
phone: '01712345681',
email: 'bd@foodpanda.com',
type: 'merchant',
status: 'pending',
verificationStage: 'application',
submittedAt: '2024-03-21',
location: 'Dhaka',
address: 'House 12, Gulshan Avenue',
requiredDocuments: [
{ id: 'd12', name: 'Trade License', status: 'pending' },
{ id: 'd13', name: 'Company NID', status: 'pending' },
{ id: 'd14', name: 'Company Documents', status: 'pending' },
],
employmentInfo: { company: 'Foodpanda Bangladesh', dailyEarning: 5000000, whyEV: 'Food Delivery', experience: 'Leading food delivery platform' },
merchantPlan: { companyCategory: 'Food Delivery', bikersRequested: 100, amount: 5000000, monthlyBudget: 10000000, requiredArea: 'Dhaka' },
nomineeDetails: { name: 'Admin User', phone: '01712345692', relationship: 'Office', nid: '1234567890126' },
notes: ['Request for 100 riders'],
smsHistory: [],
}, },
]; ];
@@ -152,7 +239,7 @@ const planLabels: Record<string, string> = {
const typeIcons: Record<string, any> = { const typeIcons: Record<string, any> = {
biker: Bike, biker: Bike,
investor: DollarSign, investor: DollarSign,
shop: Store, swapstation: Store,
merchant: User, merchant: User,
}; };
@@ -214,10 +301,16 @@ export default function KYCDetailPage() {
if (!request || !newMessageText.trim()) return; if (!request || !newMessageText.trim()) return;
setRequest(prev => prev ? { setRequest(prev => prev ? {
...prev, ...prev,
messageHistory: [...prev.messageHistory, { date: new Date().toISOString().split('T')[0], message: newMessageText, from: 'admin' as const }] status: 'documents_needed' as const,
smsHistory: [
...(prev.smsHistory || []),
{ id: `sms-${Date.now()}`, message: newMessageText, sentAt: new Date().toISOString(), sentBy: 'admin' }
],
notes: [...prev.notes, newMessageText]
} : null); } : null);
setNewMessageText(''); setNewMessageText('');
setShowMessageModal(false); setShowMessageModal(false);
alert(`Message sent to ${request.phone}`);
}; };
const handleAddDocument = () => { const handleAddDocument = () => {
@@ -272,6 +365,12 @@ const handleRejectDocument = (docId: string) => {
const TypeIcon = typeIcons[request.type]; const TypeIcon = typeIcons[request.type];
const smsTemplates = [
{ id: 'docs', label: 'Documents Required', message: 'Please upload your required documents to proceed with your KYC application.' },
{ id: 'review', label: 'Under Review', message: 'Your KYC application is now under review. We will notify you once the process is completed.' },
{ id: 'approval', label: 'Pending Approval', message: 'Your application is pending final approval. You will be notified once approved.' },
];
return ( return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto"> <div className="p-4 lg:p-6 max-w-8xl mx-auto">
<button <button
@@ -336,7 +435,7 @@ const handleRejectDocument = (docId: string) => {
<DollarSign className="w-4 h-4" /> Make Investor <DollarSign className="w-4 h-4" /> Make Investor
</button> </button>
)} )}
{request.type === 'shop' && request.status !== 'approved' && ( {request.type === 'swapstation' && request.status !== 'approved' && (
<button <button
onClick={() => { onClick={() => {
if (confirm('Approve this request and create shop profile?')) { if (confirm('Approve this request and create shop profile?')) {
@@ -594,24 +693,6 @@ const handleRejectDocument = (docId: string) => {
<p className="text-sm text-slate-400">No notes yet</p> <p className="text-sm text-slate-400">No notes yet</p>
)} )}
</div> </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> </div>
</div> </div>
@@ -624,6 +705,18 @@ const handleRejectDocument = (docId: string) => {
<button onClick={() => setShowMessageModal(false)} className="text-slate-400 hover:text-slate-600">×</button> <button onClick={() => setShowMessageModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
</div> </div>
<div className="p-4"> <div className="p-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Quick Messages</label>
<div className="flex flex-wrap gap-2 mb-4">
{smsTemplates.map(tmpl => (
<button
key={tmpl.id}
onClick={() => setNewMessageText(tmpl.message)}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200"
>
{tmpl.label}
</button>
))}
</div>
<textarea <textarea
value={newMessageText} value={newMessageText}
onChange={(e) => setNewMessageText(e.target.value)} onChange={(e) => setNewMessageText(e.target.value)}

View File

@@ -716,34 +716,22 @@ export default function RequestsPage() {
<label className="text-sm font-medium text-slate-600 mb-2 block">Quick Messages</label> <label className="text-sm font-medium text-slate-600 mb-2 block">Quick Messages</label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button <button
onClick={() => setMessageText('Please upload your NID document.')} onClick={() => setMessageText('Please upload your required documents to proceed with your KYC application.')}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200" className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200"
> >
Request NID Documents Required
</button> </button>
<button <button
onClick={() => setMessageText('Please upload your Driving License.')} onClick={() => setMessageText('Your KYC application is now under review. We will notify you once the process is completed.')}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200" className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200"
> >
Request License Under Review
</button> </button>
<button <button
onClick={() => setMessageText('Please upload your TIN Certificate.')} onClick={() => setMessageText('Your application is pending final approval. You will be notified once approved.')}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200" className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200"
> >
Request TIN Pending Approval
</button>
<button
onClick={() => setMessageText('Please upload your Bank Statement (last 3 months).')}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200"
>
Request Bank Statement
</button>
<button
onClick={() => setMessageText('Your document is not clear. Please re-upload with better quality.')}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg hover:bg-slate-200"
>
Document Unclear
</button> </button>
</div> </div>
</div> </div>