feat: add Email and SMS template management configuration page to admin settings

This commit is contained in:
sazzadulalambd
2026-05-13 02:37:29 +06:00
parent aa7cb65cf8
commit 5afe5b13f3
2 changed files with 681 additions and 2 deletions

View File

@@ -0,0 +1,673 @@
'use client';
import { useState } from 'react';
import { Mail, MessageSquare, Pencil, Eye, X, Check, Plus } from 'lucide-react';
import toast from 'react-hot-toast';
interface Template {
id: string;
name: string;
subject?: string;
body: string;
enabled: boolean;
}
interface EmailSMSTemplatesProps {
settings: any;
setSettings: any;
}
const defaultEmailTemplates: Template[] = [
{
id: 'welcome',
name: 'Welcome Email',
subject: 'Welcome to JAIBEN Mobility - Your Journey Starts Here!',
body: `Dear {name},
Welcome to JAIBEN Mobility!
We're thrilled to have you join our community of eco-friendly commuters. Your account has been successfully created.
Your Login Details:
- Phone: {phone}
- Email: {email}
What's Next?
1. Complete your KYC verification to unlock all features
2. Browse our EV rental plans
3. Choose your perfect electric vehicle
If you have any questions, our support team is here to help!
Best regards,
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'password_reset',
name: 'Password Reset',
subject: 'Reset Your JAIBEN Password',
body: `Dear {name},
We received a request to reset your password.
Your OTP code is: {otp}
This code will expire in {expiry_minutes} minutes.
If you didn't request this, please ignore this email or contact support immediately.
Note: Never share this OTP with anyone.
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'rental_confirmation',
name: 'Rental Confirmation',
subject: 'Rental Confirmed - Your EV is Ready!',
body: `Dear {name},
Your rental has been confirmed!
Booking Details:
- Bike: {bike}
- Plan: {plan}
- Start Date: {start_date}
- End Date: {end_date}
- Amount: {amount}
- Deposit: {deposit}
Pickup Location: {pickup_location}
Important:
- Bring your valid driving license
- Carry NID card for verification
- Arrive 15 minutes before scheduled time
Enjoy your ride!
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'payment_reminder',
name: 'Payment Reminder',
subject: 'Payment Due Soon - {amount}',
body: `Dear {name},
This is a friendly reminder that your payment is due.
Amount Due: {amount}
Due Date: {due_date}
Rental: {bike}
Please make your payment to avoid late fees.
Payment Methods:
- bKash: {bkash_number}
- Nagad: {nagad_number}
- Bank Transfer: {bank_account}
Need help? Contact us anytime!
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'due_notice',
name: 'Due Notice',
subject: 'Payment Overdue - Action Required',
body: `Dear {name},
Your payment for the rental of {bike} is overdue.
Overdue Amount: {amount}
Days Overdue: {days_overdue}
Late Fee: {late_fee}
Please make immediate payment to avoid service interruption.
Total Due: {total_due}
If you've already made the payment, please ignore this message or contact us to confirm.
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'kyc_verification',
name: 'KYC Verification',
subject: 'KYC Verification {status}',
body: `Dear {name},
Your KYC verification has been {status}.
{status_message}
{status === 'approved' ? 'You can now access all features and rent EVs!' : 'Please resubmit your documents with correct information.'}
Documents Submitted:
{documents_list}
If you have questions, contact our support team.
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'rental_termination',
name: 'Rental Termination',
subject: 'Rental Agreement Terminated',
body: `Dear {name},
Your rental agreement for {bike} has been terminated.
Termination Details:
- End Date: {end_date}
- Final Amount: {final_amount}
- Refund: {refund_amount}
Please return the vehicle to {return_location} by {return_date}.
Any outstanding charges will be deducted from your deposit.
Thank you for choosing JAIBEN!
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'damage_report',
name: 'Damage Report',
subject: 'Vehicle Damage Report - {bike}',
body: `Dear {name},
A damage report has been filed for your rented vehicle.
Incident Details:
- Date: {incident_date}
- Location: {location}
- Description: {description}
- Estimated Cost: {repair_cost}
Your current deposit: {deposit}
Remaining after repair: {remaining_deposit}
Please contact us within 24 hours if you have any concerns.
JAIBEN Mobility Team`,
enabled: true,
},
];
const defaultSmsTemplates: Template[] = [
{
id: 'otp',
name: 'OTP Code',
body: `JAIBEN: Your OTP is {otp}. Valid for {expiry_minutes} mins. Don't share this code.`,
enabled: true,
},
{
id: 'rental_reminder',
name: 'Rental Reminder',
body: `JAIBEN: Reminder! Your {bike} rental starts on {start_date}. Pick up from {location}. Reply CONFIRM to proceed.`,
enabled: true,
},
{
id: 'payment_due',
name: 'Payment Due',
body: `JAIBEN: Payment of {amount} due on {due_date} for your {bike} rental. Pay via bKash: {bkash}. Avoid late fees!`,
enabled: true,
},
{
id: 'battery_low',
name: 'Battery Low Warning',
body: `JAIBEN: Your {bike} battery is low ({battery_level}%). Visit nearest swap station or charge point. Stay safe!`,
enabled: true,
},
{
id: 'damage_report_sms',
name: 'Damage Report',
body: `JAIBEN: Damage reported on your {bike}. Est. cost: {repair_cost}. Contact {support_phone} within 24hrs.`,
enabled: true,
},
{
id: 'welcome_sms',
name: 'Welcome Message',
body: `Welcome to JAIBEN Mobility {name}! Your account is ready. Download our app or visit {app_link} to rent your first EV!`,
enabled: true,
},
{
id: 'rental_start',
name: 'Rental Started',
body: `JAIBEN: Your {bike} rental has started! Enjoy your ride. Return by {return_time}. Ride safe!`,
enabled: true,
},
{
id: 'rental_end',
name: 'Rental Ending',
body: `JAIBEN: Your {bike} rental ends on {end_date}. Extend at {app_link} or return to {location}. Thanks!`,
enabled: true,
},
{
id: 'swap_reminder',
name: 'Battery Swap Reminder',
body: `JAIBEN: Your battery at {location} is low. Swap station: {station_name}, Distance: {distance}km. Free swap with your plan!`,
enabled: true,
},
{
id: 'kyc_approved',
name: 'KYC Approved',
body: `JAIBEN: Great news! Your KYC is verified. You can now rent EVs. Download app: {app_link} or visit nearest hub!`,
enabled: true,
},
];
const commonVariables = [
{ name: 'name', label: 'Name' },
{ name: 'phone', label: 'Phone' },
{ name: 'email', label: 'Email' },
{ name: 'amount', label: 'Amount' },
{ name: 'date', label: 'Date' },
{ name: 'bike', label: 'Bike Name' },
{ name: 'plan', label: 'Plan' },
{ name: 'otp', label: 'OTP' },
{ name: 'deposit', label: 'Deposit' },
{ name: 'start_date', label: 'Start Date' },
{ name: 'end_date', label: 'End Date' },
{ name: 'location', label: 'Location' },
];
export default function EmailSMSTemplates({ settings, setSettings }: EmailSMSTemplatesProps) {
const [activeTemplateTab, setActiveTemplateTab] = useState<'email' | 'sms'>('email');
const [emailTemplates, setEmailTemplates] = useState<Template[]>(
settings.emailTemplates || defaultEmailTemplates
);
const [smsTemplates, setSmsTemplates] = useState<Template[]>(
settings.smsTemplates || defaultSmsTemplates
);
const [showModal, setShowModal] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<Template | null>(null);
const [editSubject, setEditSubject] = useState('');
const [editBody, setEditBody] = useState('');
const [previewMode, setPreviewMode] = useState(false);
const handleToggleEnabled = (type: 'email' | 'sms', id: string) => {
if (type === 'email') {
const updated = emailTemplates.map(t =>
t.id === id ? { ...t, enabled: !t.enabled } : t
);
setEmailTemplates(updated);
setSettings({ ...settings, emailTemplates: updated });
} else {
const updated = smsTemplates.map(t =>
t.id === id ? { ...t, enabled: !t.enabled } : t
);
setSmsTemplates(updated);
setSettings({ ...settings, smsTemplates: updated });
}
toast.success('Template status updated');
};
const handleEdit = (template: Template, isEmail: boolean) => {
setEditingTemplate(template);
setEditSubject(template.subject || '');
setEditBody(template.body);
setPreviewMode(false);
setShowModal(true);
};
const handleSave = () => {
if (!editingTemplate) return;
const updatedTemplate = {
...editingTemplate,
subject: editSubject,
body: editBody,
};
const newId = updatedTemplate.id || `template_${Date.now()}`;
const templateWithId = { ...updatedTemplate, id: newId };
if (activeTemplateTab === 'email') {
if (emailTemplates.find(t => t.id === editingTemplate.id)) {
const updated = emailTemplates.map(t =>
t.id === editingTemplate.id ? templateWithId : t
);
setEmailTemplates(updated);
setSettings({ ...settings, emailTemplates: updated });
} else {
const updated = [...emailTemplates, templateWithId];
setEmailTemplates(updated);
setSettings({ ...settings, emailTemplates: updated });
}
} else {
if (smsTemplates.find(t => t.id === editingTemplate.id)) {
const updated = smsTemplates.map(t =>
t.id === editingTemplate.id ? templateWithId : t
);
setSmsTemplates(updated);
setSettings({ ...settings, smsTemplates: updated });
} else {
const updated = [...smsTemplates, templateWithId];
setSmsTemplates(updated);
setSettings({ ...settings, smsTemplates: updated });
}
}
setShowModal(false);
toast.success('Template saved successfully');
};
const insertVariable = (varName: string) => {
setEditBody(prev => prev + `{${varName}}`);
};
const previewContent = (body: string) => {
let preview = body
.replace(/{name}/g, 'John Doe')
.replace(/{phone}/g, '+880 1234 567890')
.replace(/{email}/g, 'john@example.com')
.replace(/{amount}/g, '৳12,000')
.replace(/{date}/g, '15 May 2026')
.replace(/{bike}/g, 'EVO Lite')
.replace(/{plan}/g, 'Monthly Premium')
.replace(/{otp}/g, '123456')
.replace(/{deposit}/g, '৳5,000')
.replace(/{start_date}/g, '01 June 2026')
.replace(/{end_date}/g, '30 June 2026')
.replace(/{location}/g, 'Gulshan Hub')
.replace(/{bkash}/g, '01712345678')
.replace(/{nagad}/g, '01712345679')
.replace(/{expiry_minutes}/g, '10')
.replace(/{due_date}/g, '25 May 2026')
.replace(/{days_overdue}/g, '3')
.replace(/{late_fee}/g, '৳600')
.replace(/{total_due}/g, '৳12,600')
.replace(/{status}/g, 'Approved')
.replace(/{status_message}/g, 'Your documents have been verified successfully.')
.replace(/{documents_list}/g, '- NID Card\n- Driving License\n- Photo')
.replace(/{final_amount}/g, '৳10,000')
.replace(/{refund_amount}/g, '৳3,000')
.replace(/{return_date}/g, '20 May 2026')
.replace(/{incident_date}/g, '10 May 2026')
.replace(/{description}/g, 'Front panel scratch')
.replace(/{repair_cost}/g, '৳2,500')
.replace(/{remaining_deposit}/g, '৳2,500')
.replace(/{battery_level}/g, '15%')
.replace(/{return_time}/g, '8:00 PM')
.replace(/{app_link}/g, 'jaiben.com/app')
.replace(/{support_phone}/g, '+880 9611 222 333')
.replace(/{station_name}/g, 'Gulshan Swap Station')
.replace(/{distance}/g, '1.2')
.replace(/{bank_account}/g, 'AC: 1234567890, Bank: City Bank');
return preview;
};
return (
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-slate-800">Email & SMS Templates</h2>
<button
onClick={() => {
setActiveTemplateTab(activeTemplateTab);
setEditingTemplate({ id: '', name: '', subject: '', body: '', enabled: true });
setEditSubject('');
setEditBody('');
setPreviewMode(false);
setShowModal(true);
}}
className="flex items-center gap-2 px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium hover:bg-accent/90 transition-colors"
>
<Plus className="w-4 h-4" />
Add Template
</button>
</div>
<div className="flex gap-2 mb-6 p-1 bg-slate-100 rounded-lg w-fit">
<button
onClick={() => setActiveTemplateTab('email')}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${activeTemplateTab === 'email'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-accent'
}`}
>
<Mail className="w-4 h-4" />
Email Templates
</button>
<button
onClick={() => setActiveTemplateTab('sms')}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${activeTemplateTab === 'sms'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-accent'
}`}
>
<MessageSquare className="w-4 h-4" />
SMS Templates
</button>
</div>
<div className="space-y-3">
{activeTemplateTab === 'email' ? (
emailTemplates.map(template => (
<div
key={template.id}
className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-200"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-slate-800">{template.name}</h3>
<span
className={`px-2 py-0.5 text-xs rounded-full ${template.enabled
? 'bg-green-100 text-green-700'
: 'bg-slate-200 text-slate-500'
}`}
>
{template.enabled ? 'Active' : 'Inactive'}
</span>
</div>
<p className="text-sm text-slate-500 mt-1 truncate">{template.subject || template.body.substring(0, 60)}...</p>
</div>
<div className="flex items-center gap-2 ml-4">
<button
onClick={() => handleToggleEnabled('email', template.id)}
className={`relative w-11 h-6 rounded-full transition-colors ${template.enabled ? 'bg-accent' : 'bg-slate-300'
}`}
>
<span
className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${template.enabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
<button
onClick={() => { setActiveTemplateTab('email'); handleEdit(template, true); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => { setActiveTemplateTab('email'); setEditingTemplate(template); setPreviewMode(true); setShowModal(true); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Eye className="w-4 h-4" />
</button>
</div>
</div>
))
) : (
smsTemplates.map(template => (
<div
key={template.id}
className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-200"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-slate-800">{template.name}</h3>
<span
className={`px-2 py-0.5 text-xs rounded-full ${template.enabled
? 'bg-green-100 text-green-700'
: 'bg-slate-200 text-slate-500'
}`}
>
{template.enabled ? 'Active' : 'Inactive'}
</span>
</div>
<p className="text-sm text-slate-500 mt-1 truncate">{template.body.substring(0, 60)}...</p>
</div>
<div className="flex items-center gap-2 ml-4">
<button
onClick={() => handleToggleEnabled('sms', template.id)}
className={`relative w-11 h-6 rounded-full transition-colors ${template.enabled ? 'bg-accent' : 'bg-slate-300'
}`}
>
<span
className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${template.enabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
<button
onClick={() => { setActiveTemplateTab('sms'); handleEdit(template, false); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => { setActiveTemplateTab('sms'); setEditingTemplate(template); setPreviewMode(true); setShowModal(true); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Eye className="w-4 h-4" />
</button>
</div>
</div>
))
)}
</div>
{showModal && editingTemplate && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
<div className="p-4 border-b border-slate-200 flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-lg font-bold text-slate-800">
{previewMode ? 'Preview' : editingTemplate.id ? 'Edit' : 'Add'} {editingTemplate.id ? '- ' + editingTemplate.name : 'Template'}
</h3>
{previewMode && (
<button
onClick={() => setPreviewMode(false)}
className="ml-2 px-2 py-1 text-xs bg-accent text-white rounded hover:bg-accent/90"
>
Edit
</button>
)}
</div>
<button
onClick={() => setShowModal(false)}
className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 max-h-[calc(90vh-180px)] overflow-y-auto">
{previewMode ? (
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200">
<div className="flex items-center gap-2 mb-3 text-slate-500 text-sm">
<Mail className="w-4 h-4" />
<span>Subject: {editingTemplate.subject || 'N/A'}</span>
</div>
<pre className="whitespace-pre-wrap text-sm text-slate-700 font-sans">
{previewContent(editingTemplate.body)}
</pre>
</div>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Template Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={editingTemplate.name}
onChange={e => setEditingTemplate({ ...editingTemplate, name: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent"
placeholder="e.g. Welcome Email"
/>
</div>
{activeTemplateTab === 'email' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Subject
</label>
<input
type="text"
value={editSubject}
onChange={e => setEditSubject(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent"
placeholder="Enter email subject..."
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Body {activeTemplateTab === 'sms' && '(SMS)'}
</label>
<textarea
value={editBody}
onChange={e => setEditBody(e.target.value)}
rows={12}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent font-mono"
placeholder="Enter template body..."
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Insert Variable
</label>
<div className="flex flex-wrap gap-2">
{commonVariables.map(v => (
<button
key={v.name}
onClick={() => insertVariable(v.name)}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-sm rounded-lg hover:bg-slate-200 transition-colors"
>
{`{${v.name}}`}
</button>
))}
</div>
</div>
<div className="p-3 bg-blue-50 rounded-lg">
<p className="text-xs text-blue-700">
<strong>Tip:</strong> Variables like {'{name}'}, {'{phone}'}, {'{amount}'} will be replaced with actual values when sending.
</p>
</div>
</div>
)}
</div>
<div className="p-4 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button
onClick={() => setShowModal(false)}
className="px-4 py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-100 transition-colors"
>
Cancel
</button>
{!previewMode && (
<button
onClick={handleSave}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium hover:bg-accent-dark transition-colors flex items-center gap-2"
>
<Check className="w-4 h-4" />
Save Template
</button>
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { Settings, Package, Palette, Link2, Mail, Monitor, FileCheck, DollarSign, Zap, Users, Plus, X, Save, Pencil, Trash2 } from 'lucide-react';
import { Settings, Package, Palette, Link2, Mail, Monitor, FileCheck, DollarSign, Zap, Users, Plus, X, Save, Pencil, Trash2, FileText } from 'lucide-react';
import RichTextEditor from '@/components/RichTextEditor';
import GeneralSettings from './components/GeneralSettings';
import BrandingSettings from './components/BrandingSettings';
@@ -15,6 +15,7 @@ import PlanSelection from './components/PlanSelection';
import InvestmentSettings from './components/InvestmentSettings';
import SwapStationSettings from './components/SwapStationSettings';
import RiderRequestSettings from './components/RiderRequestSettings';
import EmailSMSTemplates from './components/EmailSMSTemplates';
export interface CompanySettings {
name: string;
@@ -813,7 +814,7 @@ const initialSettings: CompanySettings = {
export default function CompanySettingsPage() {
const [settings, setSettings] = useState<CompanySettings>(initialSettings);
const [activeTab, setActiveTab] = useState<'general' | 'branding' | 'social' | 'integration' | 'landing' | 'kyc' | 'parts' | 'companyPolicy' | 'plans' | 'investment' | 'swapstation' | 'riderrequest'>('general');
const [activeTab, setActiveTab] = useState<'general' | 'branding' | 'social' | 'integration' | 'landing' | 'kyc' | 'parts' | 'companyPolicy' | 'plans' | 'investment' | 'swapstation' | 'riderrequest' | 'templates'>('general');
const [activeMasterTab, setActiveMasterTab] = useState<'investor' | 'merchant' | 'swapstation' | 'rentalType'>('investor');
const [activeRentalTypeTab, setActiveRentalTypeTab] = useState<'single' | 'shared' | 'renttoown'>('single');
const [saved, setSaved] = useState(false);
@@ -1127,6 +1128,7 @@ export default function CompanySettingsPage() {
{ id: 'riderrequest', label: 'Rider Request Plan (P2)', icon: Users },
{ id: 'parts', label: 'EV Parts', icon: Package },
{ id: 'templates', label: 'Templates', icon: FileText },
];
return (
@@ -1356,6 +1358,10 @@ export default function CompanySettingsPage() {
/>
)}
{activeTab === 'templates' && (
<EmailSMSTemplates settings={settings} setSettings={updateSettings} />
)}
</div >
</div >