This commit is contained in:
Iliyan Angelov
2025-11-23 18:59:18 +02:00
parent be07802066
commit 627959f52b
1840 changed files with 236564 additions and 3475 deletions

View 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;