updates
This commit is contained in:
947
Frontend/src/pages/admin/EmailCampaignManagementPage.tsx
Normal file
947
Frontend/src/pages/admin/EmailCampaignManagementPage.tsx
Normal file
@@ -0,0 +1,947 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Mail,
|
||||
Plus,
|
||||
Send,
|
||||
Eye,
|
||||
Edit,
|
||||
Trash2,
|
||||
Users,
|
||||
BarChart3,
|
||||
Calendar,
|
||||
Filter,
|
||||
Search,
|
||||
FileText,
|
||||
TrendingUp,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
Play,
|
||||
Pause,
|
||||
RefreshCw,
|
||||
X,
|
||||
Save,
|
||||
Layers,
|
||||
Target
|
||||
} from 'lucide-react';
|
||||
import { emailCampaignService, Campaign, CampaignSegment, EmailTemplate, DripSequence, CampaignAnalytics } from '../../services/api/emailCampaignService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { formatDate } from '../../utils/format';
|
||||
|
||||
type CampaignTab = 'campaigns' | 'segments' | 'templates' | 'drip-sequences' | 'analytics';
|
||||
|
||||
const EmailCampaignManagementPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<CampaignTab>('campaigns');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
|
||||
const [segments, setSegments] = useState<CampaignSegment[]>([]);
|
||||
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
|
||||
const [dripSequences, setDripSequences] = useState<DripSequence[]>([]);
|
||||
const [selectedCampaign, setSelectedCampaign] = useState<Campaign | null>(null);
|
||||
const [analytics, setAnalytics] = useState<CampaignAnalytics | null>(null);
|
||||
const [showCampaignModal, setShowCampaignModal] = useState(false);
|
||||
const [showSegmentModal, setShowSegmentModal] = useState(false);
|
||||
const [showTemplateModal, setShowTemplateModal] = useState(false);
|
||||
const [showDripModal, setShowDripModal] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [dripForm, setDripForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
trigger_event: ''
|
||||
});
|
||||
const [filters, setFilters] = useState({
|
||||
status: '',
|
||||
campaign_type: ''
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
const [campaignForm, setCampaignForm] = useState({
|
||||
name: '',
|
||||
subject: '',
|
||||
html_content: '',
|
||||
text_content: '',
|
||||
campaign_type: 'newsletter',
|
||||
segment_id: undefined as number | undefined,
|
||||
scheduled_at: '',
|
||||
template_id: undefined as number | undefined,
|
||||
from_name: '',
|
||||
from_email: '',
|
||||
track_opens: true,
|
||||
track_clicks: true
|
||||
});
|
||||
|
||||
const [segmentForm, setSegmentForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
criteria: {
|
||||
role: '',
|
||||
has_bookings: undefined as boolean | undefined,
|
||||
is_vip: undefined as boolean | undefined,
|
||||
last_booking_days: undefined as number | undefined
|
||||
}
|
||||
});
|
||||
|
||||
const [templateForm, setTemplateForm] = useState({
|
||||
name: '',
|
||||
subject: '',
|
||||
html_content: '',
|
||||
text_content: '',
|
||||
category: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'campaigns') {
|
||||
fetchCampaigns();
|
||||
} else if (activeTab === 'segments') {
|
||||
fetchSegments();
|
||||
} else if (activeTab === 'templates') {
|
||||
fetchTemplates();
|
||||
} else if (activeTab === 'drip-sequences') {
|
||||
fetchDripSequences();
|
||||
}
|
||||
}, [activeTab, filters, currentPage]);
|
||||
|
||||
const fetchCampaigns = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await emailCampaignService.getCampaigns({
|
||||
status: filters.status || undefined,
|
||||
campaign_type: filters.campaign_type || undefined,
|
||||
limit: 20,
|
||||
offset: (currentPage - 1) * 20
|
||||
});
|
||||
setCampaigns(data);
|
||||
setTotalPages(Math.ceil(data.length / 20));
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to fetch campaigns');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSegments = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await emailCampaignService.getSegments();
|
||||
setSegments(data);
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to fetch segments');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await emailCampaignService.getTemplates();
|
||||
setTemplates(data);
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to fetch templates');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDripSequences = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await emailCampaignService.getDripSequences();
|
||||
setDripSequences(data);
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to fetch drip sequences');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCampaign = async () => {
|
||||
try {
|
||||
if (editingItem) {
|
||||
await emailCampaignService.updateCampaign(editingItem.id, campaignForm);
|
||||
toast.success('Campaign updated');
|
||||
} else {
|
||||
await emailCampaignService.createCampaign(campaignForm);
|
||||
toast.success('Campaign created');
|
||||
}
|
||||
setShowCampaignModal(false);
|
||||
resetCampaignForm();
|
||||
fetchCampaigns();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to save campaign');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendCampaign = async (campaignId: number) => {
|
||||
if (!window.confirm('Are you sure you want to send this campaign?')) return;
|
||||
try {
|
||||
const result = await emailCampaignService.sendCampaign(campaignId);
|
||||
toast.success(`Campaign sent! ${result.sent} emails sent, ${result.failed} failed`);
|
||||
fetchCampaigns();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to send campaign');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewAnalytics = async (campaignId: number) => {
|
||||
try {
|
||||
const data = await emailCampaignService.getCampaignAnalytics(campaignId);
|
||||
setAnalytics(data);
|
||||
setActiveTab('analytics');
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to fetch analytics');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSegment = async () => {
|
||||
try {
|
||||
// Build criteria object
|
||||
const criteria: any = {};
|
||||
if (segmentForm.criteria.role) criteria.role = segmentForm.criteria.role;
|
||||
if (segmentForm.criteria.has_bookings !== undefined) criteria.has_bookings = segmentForm.criteria.has_bookings;
|
||||
if (segmentForm.criteria.is_vip !== undefined) criteria.is_vip = segmentForm.criteria.is_vip;
|
||||
if (segmentForm.criteria.last_booking_days) criteria.last_booking_days = segmentForm.criteria.last_booking_days;
|
||||
|
||||
await emailCampaignService.createSegment({
|
||||
name: segmentForm.name,
|
||||
description: segmentForm.description,
|
||||
criteria
|
||||
});
|
||||
toast.success('Segment created');
|
||||
setShowSegmentModal(false);
|
||||
resetSegmentForm();
|
||||
fetchSegments();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to create segment');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateTemplate = async () => {
|
||||
try {
|
||||
await emailCampaignService.createTemplate(templateForm);
|
||||
toast.success('Template created');
|
||||
setShowTemplateModal(false);
|
||||
resetTemplateForm();
|
||||
fetchTemplates();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to create template');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateDripSequence = async () => {
|
||||
try {
|
||||
await emailCampaignService.createDripSequence(dripForm);
|
||||
toast.success('Drip sequence created');
|
||||
setShowDripModal(false);
|
||||
resetDripForm();
|
||||
fetchDripSequences();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Failed to create drip sequence');
|
||||
}
|
||||
};
|
||||
|
||||
const resetCampaignForm = () => {
|
||||
setCampaignForm({
|
||||
name: '',
|
||||
subject: '',
|
||||
html_content: '',
|
||||
text_content: '',
|
||||
campaign_type: 'newsletter',
|
||||
segment_id: undefined,
|
||||
scheduled_at: '',
|
||||
template_id: undefined,
|
||||
from_name: '',
|
||||
from_email: '',
|
||||
track_opens: true,
|
||||
track_clicks: true
|
||||
});
|
||||
setEditingItem(null);
|
||||
};
|
||||
|
||||
const resetSegmentForm = () => {
|
||||
setSegmentForm({
|
||||
name: '',
|
||||
description: '',
|
||||
criteria: {
|
||||
role: '',
|
||||
has_bookings: undefined,
|
||||
is_vip: undefined,
|
||||
last_booking_days: undefined
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetTemplateForm = () => {
|
||||
setTemplateForm({
|
||||
name: '',
|
||||
subject: '',
|
||||
html_content: '',
|
||||
text_content: '',
|
||||
category: ''
|
||||
});
|
||||
};
|
||||
|
||||
const resetDripForm = () => {
|
||||
setDripForm({
|
||||
name: '',
|
||||
description: '',
|
||||
trigger_event: ''
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'sent': return 'bg-green-100 text-green-800';
|
||||
case 'sending': return 'bg-blue-100 text-blue-800';
|
||||
case 'scheduled': return 'bg-yellow-100 text-yellow-800';
|
||||
case 'draft': return 'bg-gray-100 text-gray-800';
|
||||
case 'paused': return 'bg-orange-100 text-orange-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && !campaigns.length && !segments.length && !templates.length) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-400/5 via-transparent to-purple-600/5 rounded-3xl blur-3xl"></div>
|
||||
<div className="relative bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-blue-200/30 p-8">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-400 to-purple-600 rounded-2xl blur-lg opacity-50"></div>
|
||||
<div className="relative p-4 rounded-2xl bg-gradient-to-br from-blue-500 via-blue-500 to-purple-600 shadow-xl border border-blue-400/50">
|
||||
<Mail className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold bg-gradient-to-r from-slate-900 via-blue-700 to-slate-900 bg-clip-text text-transparent">
|
||||
Email Marketing & Campaigns
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">Create, manage, and track email campaigns</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ id: 'campaigns', label: 'Campaigns', icon: Mail },
|
||||
{ id: 'segments', label: 'Segments', icon: Target },
|
||||
{ id: 'templates', label: 'Templates', icon: FileText },
|
||||
{ id: 'drip-sequences', label: 'Drip Campaigns', icon: Layers },
|
||||
{ id: 'analytics', label: 'Analytics', icon: BarChart3 }
|
||||
].map(tab => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as CampaignTab)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl font-medium transition-all ${
|
||||
activeTab === tab.id
|
||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6">
|
||||
{activeTab === 'campaigns' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-xl font-semibold">Email Campaigns</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
resetCampaignForm();
|
||||
setShowCampaignModal(true);
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create Campaign
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<select
|
||||
value={filters.status}
|
||||
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg"
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="scheduled">Scheduled</option>
|
||||
<option value="sending">Sending</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="paused">Paused</option>
|
||||
</select>
|
||||
<select
|
||||
value={filters.campaign_type}
|
||||
onChange={(e) => setFilters({ ...filters, campaign_type: e.target.value })}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg"
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
<option value="newsletter">Newsletter</option>
|
||||
<option value="promotional">Promotional</option>
|
||||
<option value="transactional">Transactional</option>
|
||||
<option value="abandoned_booking">Abandoned Booking</option>
|
||||
<option value="welcome">Welcome</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Campaigns Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-3 px-4 font-semibold">Name</th>
|
||||
<th className="text-left py-3 px-4 font-semibold">Type</th>
|
||||
<th className="text-left py-3 px-4 font-semibold">Status</th>
|
||||
<th className="text-left py-3 px-4 font-semibold">Recipients</th>
|
||||
<th className="text-left py-3 px-4 font-semibold">Open Rate</th>
|
||||
<th className="text-left py-3 px-4 font-semibold">Click Rate</th>
|
||||
<th className="text-left py-3 px-4 font-semibold">Date</th>
|
||||
<th className="text-left py-3 px-4 font-semibold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{campaigns.map((campaign) => (
|
||||
<tr key={campaign.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="py-3 px-4 font-medium">{campaign.name}</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-600">{campaign.campaign_type}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(campaign.status)}`}>
|
||||
{campaign.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">{campaign.total_recipients}</td>
|
||||
<td className="py-3 px-4">
|
||||
{campaign.open_rate !== null && campaign.open_rate !== undefined
|
||||
? `${campaign.open_rate.toFixed(2)}%`
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{campaign.click_rate !== null && campaign.click_rate !== undefined
|
||||
? `${campaign.click_rate.toFixed(2)}%`
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-600">
|
||||
{campaign.sent_at ? formatDate(campaign.sent_at) : formatDate(campaign.created_at)}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleViewAnalytics(campaign.id)}
|
||||
className="px-3 py-1 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-sm"
|
||||
>
|
||||
Analytics
|
||||
</button>
|
||||
{campaign.status === 'draft' && (
|
||||
<button
|
||||
onClick={() => handleSendCampaign(campaign.id)}
|
||||
className="px-3 py-1 bg-green-500 text-white rounded-lg hover:bg-green-600 text-sm"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'segments' && (
|
||||
<SegmentsTab
|
||||
segments={segments}
|
||||
onRefresh={fetchSegments}
|
||||
onCreate={() => setShowSegmentModal(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'templates' && (
|
||||
<TemplatesTab
|
||||
templates={templates}
|
||||
onRefresh={fetchTemplates}
|
||||
onCreate={() => setShowTemplateModal(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'drip-sequences' && (
|
||||
<DripSequencesTab
|
||||
sequences={dripSequences}
|
||||
onRefresh={fetchDripSequences}
|
||||
onCreate={() => {
|
||||
resetDripForm();
|
||||
setShowDripModal(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'analytics' && analytics && (
|
||||
<AnalyticsTab analytics={analytics} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Campaign Modal */}
|
||||
{showCampaignModal && (
|
||||
<CampaignModal
|
||||
form={campaignForm}
|
||||
setForm={setCampaignForm}
|
||||
segments={segments}
|
||||
templates={templates}
|
||||
onSave={handleCreateCampaign}
|
||||
onClose={() => {
|
||||
setShowCampaignModal(false);
|
||||
resetCampaignForm();
|
||||
}}
|
||||
editing={!!editingItem}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Segment Modal */}
|
||||
{showSegmentModal && (
|
||||
<SegmentModal
|
||||
form={segmentForm}
|
||||
setForm={setSegmentForm}
|
||||
onSave={handleCreateSegment}
|
||||
onClose={() => {
|
||||
setShowSegmentModal(false);
|
||||
resetSegmentForm();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Template Modal */}
|
||||
{showTemplateModal && (
|
||||
<TemplateModal
|
||||
form={templateForm}
|
||||
setForm={setTemplateForm}
|
||||
onSave={handleCreateTemplate}
|
||||
onClose={() => {
|
||||
setShowTemplateModal(false);
|
||||
resetTemplateForm();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Drip Sequence Modal */}
|
||||
{showDripModal && (
|
||||
<DripSequenceModal
|
||||
form={dripForm}
|
||||
setForm={setDripForm}
|
||||
onSave={handleCreateDripSequence}
|
||||
onClose={() => {
|
||||
setShowDripModal(false);
|
||||
resetDripForm();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Sub-components
|
||||
const SegmentsTab: React.FC<{
|
||||
segments: CampaignSegment[];
|
||||
onRefresh: () => void;
|
||||
onCreate: () => void;
|
||||
}> = ({ segments, onCreate }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-xl font-semibold">Segments</h3>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
||||
>
|
||||
Create Segment
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{segments.map((segment) => (
|
||||
<div key={segment.id} className="border rounded-xl p-4">
|
||||
<h4 className="font-semibold">{segment.name}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">{segment.description}</p>
|
||||
<p className="text-sm text-blue-600 mt-2">
|
||||
Estimated: {segment.estimated_count || 0} users
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TemplatesTab: React.FC<{
|
||||
templates: EmailTemplate[];
|
||||
onRefresh: () => void;
|
||||
onCreate: () => void;
|
||||
}> = ({ templates, onCreate }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-xl font-semibold">Email Templates</h3>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
||||
>
|
||||
Create Template
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{templates.map((template) => (
|
||||
<div key={template.id} className="border rounded-xl p-4">
|
||||
<h4 className="font-semibold">{template.name}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">{template.subject}</p>
|
||||
{template.category && (
|
||||
<span className="inline-block mt-2 px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs">
|
||||
{template.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DripSequencesTab: React.FC<{
|
||||
sequences: DripSequence[];
|
||||
onRefresh: () => void;
|
||||
onCreate: () => void;
|
||||
}> = ({ sequences, onCreate }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-xl font-semibold">Drip Sequences</h3>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create Sequence
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{sequences.map((sequence) => (
|
||||
<div key={sequence.id} className="border rounded-xl p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 className="font-semibold">{sequence.name}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">{sequence.description}</p>
|
||||
<p className="text-sm text-blue-600 mt-2">
|
||||
{sequence.step_count} steps
|
||||
{sequence.trigger_event && ` • Trigger: ${sequence.trigger_event}`}
|
||||
</p>
|
||||
</div>
|
||||
<button className="px-3 py-1 bg-blue-500 text-white rounded-lg text-sm">
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AnalyticsTab: React.FC<{ analytics: CampaignAnalytics }> = ({ analytics }) => (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xl font-semibold">Campaign Analytics</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="bg-blue-50 rounded-xl p-6 border border-blue-100">
|
||||
<p className="text-sm text-blue-600">Open Rate</p>
|
||||
<p className="text-3xl font-bold text-blue-800 mt-2">{analytics.open_rate.toFixed(2)}%</p>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-xl p-6 border border-green-100">
|
||||
<p className="text-sm text-green-600">Click Rate</p>
|
||||
<p className="text-3xl font-bold text-green-800 mt-2">{analytics.click_rate.toFixed(2)}%</p>
|
||||
</div>
|
||||
<div className="bg-purple-50 rounded-xl p-6 border border-purple-100">
|
||||
<p className="text-sm text-purple-600">Total Opened</p>
|
||||
<p className="text-3xl font-bold text-purple-800 mt-2">{analytics.total_opened}</p>
|
||||
</div>
|
||||
<div className="bg-orange-50 rounded-xl p-6 border border-orange-100">
|
||||
<p className="text-sm text-orange-600">Total Clicked</p>
|
||||
<p className="text-3xl font-bold text-orange-800 mt-2">{analytics.total_clicked}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CampaignModal: React.FC<{
|
||||
form: any;
|
||||
setForm: (form: any) => void;
|
||||
segments: CampaignSegment[];
|
||||
templates: EmailTemplate[];
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
editing: boolean;
|
||||
}> = ({ form, setForm, segments, templates, onSave, onClose, editing }) => (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h4 className="text-lg font-semibold">{editing ? 'Edit Campaign' : 'Create Campaign'}</h4>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Campaign Name"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Subject"
|
||||
value={form.subject}
|
||||
onChange={(e) => setForm({ ...form, subject: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
<select
|
||||
value={form.campaign_type}
|
||||
onChange={(e) => setForm({ ...form, campaign_type: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
>
|
||||
<option value="newsletter">Newsletter</option>
|
||||
<option value="promotional">Promotional</option>
|
||||
<option value="transactional">Transactional</option>
|
||||
<option value="abandoned_booking">Abandoned Booking</option>
|
||||
<option value="welcome">Welcome</option>
|
||||
</select>
|
||||
<select
|
||||
value={form.segment_id || ''}
|
||||
onChange={(e) => setForm({ ...form, segment_id: e.target.value ? parseInt(e.target.value) : undefined })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
>
|
||||
<option value="">No Segment (All Users)</option>
|
||||
{segments.map(s => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<textarea
|
||||
placeholder="HTML Content"
|
||||
value={form.html_content}
|
||||
onChange={(e) => setForm({ ...form, html_content: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
rows={10}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
||||
>
|
||||
{editing ? 'Update' : 'Create'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SegmentModal: React.FC<{
|
||||
form: any;
|
||||
setForm: (form: any) => void;
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ form, setForm, onSave, onClose }) => (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl p-6 max-w-2xl w-full">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h4 className="text-lg font-semibold">Create Segment</h4>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Segment Name"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Description"
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
rows={3}
|
||||
/>
|
||||
<select
|
||||
value={form.criteria.role}
|
||||
onChange={(e) => setForm({ ...form, criteria: { ...form.criteria, role: e.target.value } })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
>
|
||||
<option value="">All Roles</option>
|
||||
<option value="customer">Customer</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="staff">Staff</option>
|
||||
</select>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TemplateModal: React.FC<{
|
||||
form: any;
|
||||
setForm: (form: any) => void;
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ form, setForm, onSave, onClose }) => (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h4 className="text-lg font-semibold">Create Template</h4>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Template Name"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Subject"
|
||||
value={form.subject}
|
||||
onChange={(e) => setForm({ ...form, subject: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="HTML Content"
|
||||
value={form.html_content}
|
||||
onChange={(e) => setForm({ ...form, html_content: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
rows={15}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DripSequenceModal: React.FC<{
|
||||
form: any;
|
||||
setForm: (form: any) => void;
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ form, setForm, onSave, onClose }) => (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl p-6 max-w-2xl w-full">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h4 className="text-lg font-semibold">Create Drip Sequence</h4>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Sequence Name"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Description"
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
rows={3}
|
||||
/>
|
||||
<select
|
||||
value={form.trigger_event}
|
||||
onChange={(e) => setForm({ ...form, trigger_event: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
|
||||
>
|
||||
<option value="">No Trigger (Manual)</option>
|
||||
<option value="user_signup">User Signup</option>
|
||||
<option value="booking_created">Booking Created</option>
|
||||
<option value="booking_cancelled">Booking Cancelled</option>
|
||||
<option value="check_in">Check In</option>
|
||||
<option value="check_out">Check Out</option>
|
||||
</select>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default EmailCampaignManagementPage;
|
||||
|
||||
Reference in New Issue
Block a user