mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 08:13:23 +00:00
Remove email outbox collection and process job; refactor email templates with rich text support and slug generation
This commit is contained in:
@@ -1,9 +1,51 @@
|
||||
import { BeforeDashboardClient as BeforeDashboardClient_fc6e7dd366b9e2c8ce77d31252122343 } from 'temp-project/client'
|
||||
import { BeforeDashboardServer as BeforeDashboardServer_c4406fcca100b2553312c5a3d7520a3f } from 'temp-project/rsc'
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
|
||||
export const importMap = {
|
||||
'temp-project/client#BeforeDashboardClient':
|
||||
BeforeDashboardClient_fc6e7dd366b9e2c8ce77d31252122343,
|
||||
'temp-project/rsc#BeforeDashboardServer':
|
||||
BeforeDashboardServer_c4406fcca100b2553312c5a3d7520a3f,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
|
||||
}
|
||||
|
||||
75
dev/app/api/create-test-user/route.ts
Normal file
75
dev/app/api/create-test-user/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const body = await request.json()
|
||||
|
||||
// Generate random user data if not provided
|
||||
const userData = {
|
||||
email: body.email || `user-${Date.now()}@example.com`,
|
||||
password: body.password || 'TestPassword123!',
|
||||
firstName: body.firstName || 'Test',
|
||||
lastName: body.lastName || 'User',
|
||||
}
|
||||
|
||||
// Create the user
|
||||
const user = await payload.create({
|
||||
collection: 'users',
|
||||
data: userData,
|
||||
})
|
||||
|
||||
// Check if email was queued
|
||||
await new Promise(resolve => setTimeout(resolve, 500)) // Brief delay for email processing
|
||||
|
||||
const { docs: emails } = await payload.find({
|
||||
collection: 'emails' as const,
|
||||
where: {
|
||||
to: {
|
||||
equals: userData.email,
|
||||
},
|
||||
},
|
||||
limit: 1,
|
||||
sort: '-createdAt',
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
},
|
||||
emailQueued: emails.length > 0,
|
||||
email: emails.length > 0 ? {
|
||||
id: emails[0].id,
|
||||
subject: emails[0].subject,
|
||||
status: emails[0].status,
|
||||
} : null,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error creating test user:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to create user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({
|
||||
message: 'Use POST to create a test user',
|
||||
example: {
|
||||
email: 'optional@example.com',
|
||||
password: 'optional',
|
||||
firstName: 'optional',
|
||||
lastName: 'optional',
|
||||
},
|
||||
note: 'All fields are optional. Random values will be generated if not provided.',
|
||||
})
|
||||
}
|
||||
75
dev/app/api/process-emails/route.ts
Normal file
75
dev/app/api/process-emails/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Queue the combined email queue processing job
|
||||
const job = await payload.jobs.queue({
|
||||
task: 'process-email-queue',
|
||||
input: {},
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: 'Email queue processing job queued successfully (will process both pending and failed emails)',
|
||||
jobId: job.id,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Process emails error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to process emails',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Get email queue statistics
|
||||
const pending = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'pending' } },
|
||||
})
|
||||
|
||||
const processing = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'processing' } },
|
||||
})
|
||||
|
||||
const sent = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'sent' } },
|
||||
})
|
||||
|
||||
const failed = await payload.count({
|
||||
collection: 'emails' as const,
|
||||
where: { status: { equals: 'failed' } },
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
statistics: {
|
||||
pending: pending.totalDocs,
|
||||
processing: processing.totalDocs,
|
||||
sent: sent.totalDocs,
|
||||
failed: failed.totalDocs,
|
||||
total: pending.totalDocs + processing.totalDocs + sent.totalDocs + failed.totalDocs,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get email stats error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to get email statistics',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
86
dev/app/api/test-email/route.ts
Normal file
86
dev/app/api/test-email/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const body = await request.json()
|
||||
const { type = 'send', templateSlug, to, variables, scheduledAt } = body
|
||||
|
||||
let result
|
||||
if (type === 'send') {
|
||||
// Send immediately
|
||||
result = await sendEmail(payload, {
|
||||
templateSlug,
|
||||
to,
|
||||
variables,
|
||||
})
|
||||
} else if (type === 'schedule') {
|
||||
// Schedule for later
|
||||
result = await scheduleEmail(payload, {
|
||||
templateSlug,
|
||||
to,
|
||||
variables,
|
||||
scheduledAt: scheduledAt ? new Date(scheduledAt) : new Date(Date.now() + 60000), // Default to 1 minute
|
||||
})
|
||||
} else {
|
||||
return Response.json({ error: 'Invalid type. Use "send" or "schedule"' }, { status: 400 })
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
emailId: result,
|
||||
message: type === 'send' ? 'Email sent successfully' : 'Email scheduled successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Test email error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to send email',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Get email templates
|
||||
const { docs: templates } = await payload.find({
|
||||
collection: 'email-templates' as const,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
// Get email queue status
|
||||
const { docs: queuedEmails, totalDocs } = await payload.find({
|
||||
collection: 'emails' as const,
|
||||
limit: 10,
|
||||
sort: '-createdAt',
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
templates,
|
||||
outbox: {
|
||||
emails: queuedEmails,
|
||||
total: totalDocs,
|
||||
},
|
||||
mailing: {
|
||||
pluginActive: !!(payload as any).mailing,
|
||||
service: !!(payload as any).mailing?.service,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get mailing status error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to get mailing status',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
344
dev/app/mailing-test/page.tsx
Normal file
344
dev/app/mailing-test/page.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface Template {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
subject: string
|
||||
variables?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
description?: string
|
||||
}>
|
||||
previewData?: Record<string, any>
|
||||
}
|
||||
|
||||
interface QueuedEmail {
|
||||
id: string
|
||||
subject: string
|
||||
to: string[]
|
||||
status: string
|
||||
createdAt: string
|
||||
sentAt?: string
|
||||
attempts: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default function MailingTestPage() {
|
||||
const [templates, setTemplates] = useState<Template[]>([])
|
||||
const [queuedEmails, setQueuedEmails] = useState<QueuedEmail[]>([])
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
||||
const [toEmail, setToEmail] = useState<string>('test@example.com')
|
||||
const [variables, setVariables] = useState<Record<string, any>>({})
|
||||
const [emailType, setEmailType] = useState<'send' | 'schedule'>('send')
|
||||
const [scheduleDate, setScheduleDate] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [message, setMessage] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/test-email')
|
||||
const data = await response.json()
|
||||
setTemplates(data.templates || [])
|
||||
setQueuedEmails(data.outbox?.emails || [])
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTemplateChange = (templateSlug: string) => {
|
||||
setSelectedTemplate(templateSlug)
|
||||
const template = templates.find(t => t.slug === templateSlug)
|
||||
if (template?.previewData) {
|
||||
setVariables(template.previewData)
|
||||
}
|
||||
}
|
||||
|
||||
const sendTestEmail = async () => {
|
||||
if (!selectedTemplate || !toEmail) {
|
||||
setMessage('Please select a template and enter an email address')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/test-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: emailType,
|
||||
templateSlug: selectedTemplate,
|
||||
to: toEmail,
|
||||
variables,
|
||||
scheduledAt: emailType === 'schedule' ? scheduleDate : undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
setMessage(`✅ ${result.message} (ID: ${result.emailId})`)
|
||||
fetchData() // Refresh email queue
|
||||
} else {
|
||||
setMessage(`❌ Error: ${result.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const processEmailQueue = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/process-emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
setMessage(result.success ? `✅ ${result.message}` : `❌ ${result.error}`)
|
||||
|
||||
setTimeout(() => {
|
||||
fetchData() // Refresh after a delay to see status changes
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
setMessage(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTemplateData = templates.find(t => t.slug === selectedTemplate)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1>📧 PayloadCMS Mailing Plugin Test</h1>
|
||||
|
||||
{message && (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
margin: '10px 0',
|
||||
backgroundColor: message.startsWith('✅') ? '#d4edda' : '#f8d7da',
|
||||
border: `1px solid ${message.startsWith('✅') ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
borderRadius: '4px',
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
||||
{/* Send Email Form */}
|
||||
<div style={{ border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
|
||||
<h2>Send Test Email</h2>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>Email Template:</label>
|
||||
<select
|
||||
value={selectedTemplate}
|
||||
onChange={(e) => handleTemplateChange(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
>
|
||||
<option value="">Select a template...</option>
|
||||
{templates.map(template => (
|
||||
<option key={template.id} value={template.slug}>{template.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>To Email:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={toEmail}
|
||||
onChange={(e) => setToEmail(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>Type:</label>
|
||||
<label style={{ marginRight: '15px' }}>
|
||||
<input
|
||||
type="radio"
|
||||
value="send"
|
||||
checked={emailType === 'send'}
|
||||
onChange={(e) => setEmailType(e.target.value as 'send' | 'schedule')}
|
||||
/>
|
||||
Send Now
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="schedule"
|
||||
checked={emailType === 'schedule'}
|
||||
onChange={(e) => setEmailType(e.target.value as 'send' | 'schedule')}
|
||||
/>
|
||||
Schedule
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{emailType === 'schedule' && (
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>Schedule Date:</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduleDate}
|
||||
onChange={(e) => setScheduleDate(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTemplateData?.variables && (
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<h3>Template Variables:</h3>
|
||||
{selectedTemplateData.variables.map(variable => (
|
||||
<div key={variable.name} style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{variable.name} {variable.required && <span style={{ color: 'red' }}>*</span>}
|
||||
{variable.description && <small style={{ color: '#666' }}> - {variable.description}</small>}
|
||||
</label>
|
||||
<input
|
||||
type={variable.type === 'number' ? 'number' : variable.type === 'date' ? 'datetime-local' : 'text'}
|
||||
value={variables[variable.name] || ''}
|
||||
onChange={(e) => setVariables({
|
||||
...variables,
|
||||
[variable.name]: variable.type === 'number' ? Number(e.target.value) :
|
||||
variable.type === 'boolean' ? e.target.checked :
|
||||
e.target.value
|
||||
})}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={sendTestEmail}
|
||||
disabled={loading || !selectedTemplate || !toEmail}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: loading ? '#ccc' : '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Sending...' : emailType === 'send' ? 'Send Email' : 'Schedule Email'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={processEmailQueue}
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: loading ? '#ccc' : '#17a2b8',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
marginTop: '10px',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Queuing...' : 'Process Email Queue (Pending + Failed)'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Email Queue */}
|
||||
<div style={{ border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
||||
<h2>Email Queue</h2>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#6c757d',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{queuedEmails.length === 0 ? (
|
||||
<p style={{ color: '#666' }}>No emails in queue</p>
|
||||
) : (
|
||||
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||
{queuedEmails.map(email => (
|
||||
<div
|
||||
key={email.id}
|
||||
style={{
|
||||
border: '1px solid #eee',
|
||||
padding: '12px',
|
||||
marginBottom: '10px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: email.status === 'sent' ? '#f8f9fa' :
|
||||
email.status === 'failed' ? '#fff3cd' :
|
||||
email.status === 'processing' ? '#e3f2fd' : 'white',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>{email.subject}</div>
|
||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||
To: {Array.isArray(email.to) ? email.to.join(', ') : email.to} | Status: <strong>{email.status}</strong> | Attempts: {email.attempts}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
Created: {new Date(email.createdAt).toLocaleString()}
|
||||
{email.sentAt && ` | Sent: ${new Date(email.sentAt).toLocaleString()}`}
|
||||
</div>
|
||||
{email.error && (
|
||||
<div style={{ fontSize: '12px', color: '#dc3545', marginTop: '5px' }}>
|
||||
Error: {email.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Templates Overview */}
|
||||
<div style={{ marginTop: '30px', border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
|
||||
<h2>Available Templates</h2>
|
||||
{templates.length === 0 ? (
|
||||
<p style={{ color: '#666' }}>No templates available</p>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '15px' }}>
|
||||
{templates.map(template => (
|
||||
<div key={template.id} style={{ border: '1px solid #eee', padding: '15px', borderRadius: '4px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0' }}>{template.name}</h3>
|
||||
<p style={{ margin: '0 0 10px 0', color: '#666', fontSize: '14px' }}>{template.subject}</p>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
Variables: {template.variables?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user