feat: expand KYC types with swapstation support and implement SMS history tracking with templating system
This commit is contained in:
@@ -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 = () => {
|
||||||
@@ -240,7 +333,7 @@ export default function KYCDetailPage() {
|
|||||||
} : null);
|
} : null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRejectDocument = (docId: string) => {
|
const handleRejectDocument = (docId: string) => {
|
||||||
const reason = prompt('Enter rejection reason:');
|
const reason = prompt('Enter rejection reason:');
|
||||||
if (reason) {
|
if (reason) {
|
||||||
setRequest(prev => prev ? {
|
setRequest(prev => prev ? {
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user