3import React, { useState, useEffect } from "react";
4import Link from "next/link";
5import { useEditor, EditorContent } from "@tiptap/react";
6import StarterKit from "@tiptap/starter-kit";
7import { TextStyle } from "@tiptap/extension-text-style";
8import { Color } from "@tiptap/extension-color";
9import { Link as LinkExtension } from "@tiptap/extension-link";
10import { Image } from "@tiptap/extension-image";
11import { TextAlign } from "@tiptap/extension-text-align";
12import { Underline } from "@tiptap/extension-underline";
13import { Highlight } from "@tiptap/extension-highlight";
16 f100: number; // Customer ID
18 f115: string; // Email
19 f210?: number; // Marketing preference (6=opt-out, 1=opt-in)
20 f213?: number; // External flags
21 f501?: number; // Set ID
22 fSend?: boolean; // Should send to this customer
23 fEmailBad?: boolean; // Email validation failed
26type WizardStep = 1 | 2 | 3 | 4;
28export default function BulkEmailPage() {
29 const [currentStep, setCurrentStep] = useState<WizardStep>(1);
30 const [emailSubject, setEmailSubject] = useState("");
31 const [emailBody, setEmailBody] = useState("<p>Insert your email message here</p>");
32 const [testEmail, setTestEmail] = useState("");
33 const [testStatus, setTestStatus] = useState("");
35 // Dropdown states for toolbar
36 const [showHeadingMenu, setShowHeadingMenu] = useState(false);
37 const [showAlignMenu, setShowAlignMenu] = useState(false);
39 // Initialize Tiptap editor
40 const editor = useEditor({
46 Highlight.configure({ multicolor: true }),
48 types: ['heading', 'paragraph'],
50 LinkExtension.configure({
59 onUpdate: ({ editor }) => {
60 setEmailBody(editor.getHTML());
62 immediatelyRender: false, // Prevent SSR hydration mismatch
66 const [selectedCustomers, setSelectedCustomers] = useState<Customer[]>([]);
67 const [allCustomers, setAllCustomers] = useState(false);
68 const [selectedSet, setSelectedSet] = useState(0);
69 const [removeDuplicates, setRemoveDuplicates] = useState(true);
70 const [dropBadEmails, setDropBadEmails] = useState(true);
71 const [removeOptOut, setRemoveOptOut] = useState(true);
72 const [removeExternalDeny, setRemoveExternalDeny] = useState(false);
74 // Validation and status
75 const [loading, setLoading] = useState(false);
76 const [emailCount, setEmailCount] = useState(0);
77 const [validationStatus, setValidationStatus] = useState("");
78 const [acknowledged, setAcknowledged] = useState(false);
79 const [sendStatus, setSendStatus] = useState("");
80 const [previewHtml, setPreviewHtml] = useState("");
89 calculateEmailCount();
90 }, [selectedCustomers, removeDuplicates, dropBadEmails, removeOptOut, removeExternalDeny]);
93 // Generate preview with debouncing
94 const timer = setTimeout(() => {
95 if (emailBody && emailBody !== "Insert your email message here") {
99 return () => clearTimeout(timer);
102 const loadCustomers = async () => {
105 setValidationStatus("Loading customers...");
107 const response = await fetch("/api/v1/customers?fields=f115,f100,f101,f210,f213");
110 throw new Error(`API error: ${response.status}`);
113 const contentType = response.headers.get("content-type");
114 if (!contentType || !contentType.includes("application/json")) {
115 console.error("API endpoint not implemented yet");
116 setValidationStatus("⚠️ Customer API endpoint not yet implemented");
117 setSelectedCustomers([]);
121 const data = await response.json();
123 if (data.success && data.data?.DATS) {
124 const customers = Array.isArray(data.data.DATS) ? data.data.DATS : [data.data.DATS];
126 // Initialize customer data
127 const processedCustomers = customers.map((c: Customer) => ({
133 setSelectedCustomers(processedCustomers);
135 // Validate email addresses
136 validateEmails(processedCustomers);
139 console.error("Error loading customers:", error);
140 setValidationStatus("⚠️ Customer API not available - UI demonstration mode");
141 // Set demo data for UI testing
142 setSelectedCustomers([]);
148 const validateEmails = async (customers: Customer[]) => {
150 setValidationStatus("⏳ Validating email addresses...");
152 const emails = customers.map(c => c.f115).filter(e => e);
154 const response = await fetch("/api/v1/email/validate", {
156 headers: { "Content-Type": "application/json" },
157 body: JSON.stringify({ emails }),
161 console.log("Email validation API not available");
162 setValidationStatus("⚠️ Email validation API not yet implemented - skipping validation");
166 const contentType = response.headers.get("content-type");
167 if (!contentType || !contentType.includes("application/json")) {
168 setValidationStatus("⚠️ Email validation API not yet implemented - skipping validation");
172 const data = await response.json();
174 if (data.success && data.results) {
178 const updated = customers.map(customer => {
179 const validation = data.results.find((r: any) => r.email === customer.f115);
180 if (validation && validation.errorMask !== 0) {
182 return { ...customer, fEmailBad: true, fSend: !dropBadEmails };
188 setSelectedCustomers(updated);
189 setValidationStatus(`✅ ${goodCount} valid, ❌ ${badCount} invalid`);
192 console.error("Email validation error:", error);
193 setValidationStatus("Validation skipped");
197 const calculateEmailCount = () => {
198 const seen = new Set<string>();
201 for (const customer of selectedCustomers) {
202 if (!customer.fSend) continue;
204 const email = customer.f115?.toLowerCase();
205 if (!email) continue;
208 if (removeDuplicates && seen.has(email)) continue;
209 if (dropBadEmails && customer.fEmailBad) continue;
210 if (removeOptOut && customer.f210 === 6) continue;
211 if (removeExternalDeny && customer.f213 && (customer.f213 & 2)) continue;
217 setEmailCount(count);
220 const generatePreview = async () => {
222 const response = await fetch("/api/v1/email/preview", {
224 headers: { "Content-Type": "application/json" },
225 body: JSON.stringify({ body: emailBody }),
228 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
229 setPreviewHtml(emailBody);
233 const data = await response.json();
235 setPreviewHtml(data.preview);
238 console.error("Preview error:", error);
239 setPreviewHtml(emailBody);
243 const sendTestEmail = async () => {
245 alert("Please enter an email address");
251 const response = await fetch("/api/v1/email/send-test", {
253 headers: { "Content-Type": "application/json" },
254 body: JSON.stringify({
256 subject: emailSubject,
261 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
262 setTestStatus(`⚠️ Email API not yet implemented - UI demonstration mode (would send to: ${testEmail})`);
266 const data = await response.json();
268 setTestStatus(`✅ Test email queued to ${testEmail}`);
270 setTestStatus("❌ Failed to send test");
273 console.error("Test email error:", error);
274 setTestStatus("⚠️ Email API not available");
280 const sendBulkEmail = async () => {
282 alert("Please acknowledge the legal requirements");
288 setSendStatus("Preparing emails...");
290 // Get final recipient list
291 const recipients: string[] = [];
292 const seen = new Set<string>();
294 for (const customer of selectedCustomers) {
295 if (!customer.fSend) continue;
297 const email = customer.f115?.toLowerCase();
298 if (!email) continue;
300 if (removeDuplicates && seen.has(email)) continue;
301 if (dropBadEmails && customer.fEmailBad) continue;
302 if (removeOptOut && customer.f210 === 6) continue;
303 if (removeExternalDeny && customer.f213 && (customer.f213 & 2)) continue;
305 recipients.push(customer.f115);
309 const response = await fetch("/api/v1/email/send-bulk", {
311 headers: { "Content-Type": "application/json" },
312 body: JSON.stringify({
313 subject: emailSubject,
319 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
320 setSendStatus("⚠️ Bulk email API not yet implemented - UI demonstration mode");
324 const data = await response.json();
326 setSendStatus(`✅ Successfully queued ${recipients.length} emails!`);
328 setSendStatus("❌ Failed to queue emails");
331 console.error("Bulk send error:", error);
332 setSendStatus("⚠️ Email API not available");
338 const insertField = (field: string) => {
339 setEmailBody(prev => prev + field);
343 <div className="min-h-screen bg-gray-50">
344 <div className="max-w-7xl mx-auto p-6">
346 <div className="mb-8">
347 <div className="flex items-center gap-3 mb-2">
348 <Link href="/marketing" className="text-gray-600 hover:text-gray-800">Marketing</Link>
349 <span className="text-gray-400">/</span>
350 <Link href="/marketing/messaging" className="text-gray-600 hover:text-gray-800">Email & Interfaces</Link>
351 <span className="text-gray-400">/</span>
352 <span className="text-gray-900 font-semibold">Send Bulk Email</span>
354 <h1 className="text-3xl font-bold text-gray-900 mb-2">Send Bulk Email</h1>
355 <p className="text-gray-600 text-sm">
356 Create and send an email to multiple customers
361 <div className="mb-6 bg-yellow-50 border-l-4 border-yellow-400 p-4">
362 <div className="flex items-start">
363 <span className="text-2xl mr-3">⚠️</span>
364 <div className="text-sm text-yellow-800">
365 <p className="font-semibold mb-1">Extended Functionality</p>
366 <p>Bulk email sending is covered by law in many countries. Ensure compliance before sending.</p>
371 {/* Step Navigation */}
372 <div className="bg-white rounded-lg shadow-md mb-6 overflow-hidden">
373 <div className="flex">
374 {[1, 2, 3, 4].map((step) => (
377 onClick={() => setCurrentStep(step as WizardStep)}
378 className={`flex-1 px-6 py-5 text-center font-semibold transition-all relative ${
380 ? "bg-[#00946b] text-white shadow-lg z-10"
382 ? "bg-green-50 text-green-700 hover:bg-green-100"
383 : "bg-gray-100 text-gray-500 hover:bg-gray-200"
386 <div className="flex flex-col items-center gap-2">
387 <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
389 ? "bg-white text-[#00946b]"
391 ? "bg-green-600 text-white"
392 : "bg-gray-300 text-gray-600"
394 {currentStep > step ? "✓" : step}
396 <div className="text-sm">
397 {step === 1 ? "Create Email" :
398 step === 2 ? "Test Email" :
399 step === 3 ? "Select Recipients" :
403 {/* Progress connector line */}
405 <div className={`absolute top-1/2 right-0 w-full h-0.5 -translate-y-1/2 ${
406 currentStep > step ? "bg-green-600" : "bg-gray-300"
407 } -z-10`} style={{ width: 'calc(100% - 48px)', left: '24px' }} />
413 <div className="p-6">
414 {/* Step 1: Create Email */}
415 {currentStep === 1 && (
416 <div className="space-y-6">
418 <label className="block text-sm font-medium text-gray-700 mb-2">Subject</label>
422 onChange={(e) => setEmailSubject(e.target.value)}
423 className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-[#00946b]"
424 placeholder="Enter email subject"
429 <label className="block text-sm font-medium text-gray-700 mb-2">Email Content</label>
431 {/* Tiptap Toolbar */}
433 <div className="border border-gray-300 rounded-t bg-white p-2 flex flex-wrap items-center gap-1.5 shadow-sm">
435 {/* Heading Dropdown */}
436 <div className="relative">
439 onClick={() => setShowHeadingMenu(!showHeadingMenu)}
440 className="px-3 py-1.5 text-sm border border-gray-300 rounded hover:bg-gray-50 flex items-center gap-1 min-w-[100px]"
443 <span className="flex-1 text-left">
444 {editor.isActive('heading', { level: 1 }) ? 'Heading 1' :
445 editor.isActive('heading', { level: 2 }) ? 'Heading 2' :
446 editor.isActive('heading', { level: 3 }) ? 'Heading 3' :
449 <span className="text-xs">▼</span>
451 {showHeadingMenu && (
452 <div className="absolute top-full left-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10 min-w-[160px]">
455 onClick={() => { editor.chain().focus().setParagraph().run(); setShowHeadingMenu(false); }}
456 className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 ${!editor.isActive('heading') ? 'bg-gray-50' : ''}`}
462 onClick={() => { editor.chain().focus().toggleHeading({ level: 1 }).run(); setShowHeadingMenu(false); }}
463 className={`w-full text-left px-4 py-2 text-lg font-bold hover:bg-gray-100 ${editor.isActive('heading', { level: 1 }) ? 'bg-gray-50' : ''}`}
469 onClick={() => { editor.chain().focus().toggleHeading({ level: 2 }).run(); setShowHeadingMenu(false); }}
470 className={`w-full text-left px-4 py-2 text-base font-semibold hover:bg-gray-100 ${editor.isActive('heading', { level: 2 }) ? 'bg-gray-50' : ''}`}
476 onClick={() => { editor.chain().focus().toggleHeading({ level: 3 }).run(); setShowHeadingMenu(false); }}
477 className={`w-full text-left px-4 py-2 text-sm font-semibold hover:bg-gray-100 ${editor.isActive('heading', { level: 3 }) ? 'bg-gray-50' : ''}`}
485 <div className="w-px h-6 bg-gray-300"></div>
487 {/* Text Formatting Group */}
488 <div className="flex gap-0.5">
491 onClick={() => editor.chain().focus().toggleBold().run()}
492 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('bold') ? 'bg-gray-200' : ''}`}
493 title="Bold (Ctrl+B)"
499 onClick={() => editor.chain().focus().toggleItalic().run()}
500 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('italic') ? 'bg-gray-200' : ''}`}
501 title="Italic (Ctrl+I)"
507 onClick={() => editor.chain().focus().toggleUnderline().run()}
508 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('underline') ? 'bg-gray-200' : ''}`}
509 title="Underline (Ctrl+U)"
515 onClick={() => editor.chain().focus().toggleStrike().run()}
516 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('strike') ? 'bg-gray-200' : ''}`}
517 title="Strikethrough"
523 <div className="w-px h-6 bg-gray-300"></div>
525 {/* Color & Highlight */}
528 onChange={(e) => editor.chain().focus().setColor(e.target.value).run()}
529 value={editor.getAttributes('textStyle').color || '#000000'}
530 className="w-7 h-7 border border-gray-300 rounded cursor-pointer"
535 onClick={() => editor.chain().focus().toggleHighlight({ color: '#fff59d' }).run()}
536 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('highlight') ? 'bg-yellow-200' : ''}`}
542 <div className="w-px h-6 bg-gray-300"></div>
544 {/* Alignment Dropdown */}
545 <div className="relative">
548 onClick={() => setShowAlignMenu(!showAlignMenu)}
549 className="px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 flex items-center gap-1"
550 title="Text Alignment"
553 {editor.isActive({ textAlign: 'center' }) ? '⬌' :
554 editor.isActive({ textAlign: 'right' }) ? '➡️' :
555 editor.isActive({ textAlign: 'justify' }) ? '⬍' :
558 <span className="text-xs">▼</span>
561 <div className="absolute top-full left-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10 min-w-[140px]">
564 onClick={() => { editor.chain().focus().setTextAlign('left').run(); setShowAlignMenu(false); }}
565 className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center gap-2 ${editor.isActive({ textAlign: 'left' }) ? 'bg-gray-50' : ''}`}
567 <span>⬅️</span> Align Left
571 onClick={() => { editor.chain().focus().setTextAlign('center').run(); setShowAlignMenu(false); }}
572 className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center gap-2 ${editor.isActive({ textAlign: 'center' }) ? 'bg-gray-50' : ''}`}
574 <span>⬌</span> Align Center
578 onClick={() => { editor.chain().focus().setTextAlign('right').run(); setShowAlignMenu(false); }}
579 className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center gap-2 ${editor.isActive({ textAlign: 'right' }) ? 'bg-gray-50' : ''}`}
581 <span>➡️</span> Align Right
585 onClick={() => { editor.chain().focus().setTextAlign('justify').run(); setShowAlignMenu(false); }}
586 className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center gap-2 ${editor.isActive({ textAlign: 'justify' }) ? 'bg-gray-50' : ''}`}
588 <span>⬍</span> Justify
594 <div className="w-px h-6 bg-gray-300"></div>
596 {/* Lists & Indent */}
597 <div className="flex gap-0.5">
600 onClick={() => editor.chain().focus().toggleBulletList().run()}
601 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('bulletList') ? 'bg-gray-200' : ''}`}
608 onClick={() => editor.chain().focus().toggleOrderedList().run()}
609 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('orderedList') ? 'bg-gray-200' : ''}`}
610 title="Numbered List"
616 onClick={() => editor.chain().focus().sinkListItem('listItem').run()}
617 disabled={!editor.can().sinkListItem('listItem')}
618 className="px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
625 onClick={() => editor.chain().focus().liftListItem('listItem').run()}
626 disabled={!editor.can().liftListItem('listItem')}
627 className="px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
634 <div className="w-px h-6 bg-gray-300"></div>
637 <div className="flex gap-0.5">
640 onClick={() => editor.chain().focus().toggleBlockquote().run()}
641 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('blockquote') ? 'bg-gray-200' : ''}`}
648 onClick={() => editor.chain().focus().toggleCodeBlock().run()}
649 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('codeBlock') ? 'bg-gray-200' : ''}`}
656 <div className="w-px h-6 bg-gray-300"></div>
659 <div className="flex gap-0.5">
663 const url = window.prompt('Enter URL:');
665 editor.chain().focus().setLink({ href: url }).run();
668 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('link') ? 'bg-gray-200' : ''}`}
676 const url = window.prompt('Enter image URL:');
678 editor.chain().focus().setImage({ src: url }).run();
681 className="px-2.5 py-1.5 text-sm rounded hover:bg-gray-100"
688 onClick={() => editor.chain().focus().setHorizontalRule().run()}
689 className="px-2.5 py-1.5 text-sm rounded hover:bg-gray-100"
690 title="Horizontal Rule"
696 <div className="w-px h-6 bg-gray-300"></div>
698 {/* Clear Formatting */}
701 onClick={() => editor.chain().focus().unsetAllMarks().clearNodes().run()}
702 className="px-2.5 py-1.5 text-xs rounded hover:bg-gray-100 text-gray-600"
703 title="Clear Formatting"
710 {/* Tiptap Editor Content */}
711 <div className="border border-gray-300 rounded-b bg-white">
714 className="prose max-w-none p-4 min-h-[300px] focus:outline-none"
719 <div className="flex gap-4 flex-wrap">
721 <p className="text-sm font-medium mb-2" style={{ color: 'var(--text)' }}>Insert Customer</p>
722 <button onClick={() => insertField("%customer.name%")} className="px-3 py-1 bg-brand/10 text-brand rounded text-sm mr-2">Name</button>
723 <button onClick={() => insertField("%customer.cid%")} className="px-3 py-1 bg-brand/10 text-brand rounded text-sm">ID#</button>
726 <p className="text-sm font-medium mb-2" style={{ color: 'var(--text)' }}>Insert Account</p>
727 <button onClick={() => insertField("%customer.account.name%")} className="px-3 py-1 bg-success/10 text-success rounded text-sm mr-2">Name</button>
728 <button onClick={() => insertField("%customer.account.balance%")} className="px-3 py-1 bg-success/10 text-success rounded text-sm">Balance</button>
732 <div className="mt-6 border border-gray-300 rounded p-4">
733 <h3 className="font-semibold text-gray-900 mb-2">Preview</h3>
735 className="prose max-w-none"
736 dangerouslySetInnerHTML={{ __html: previewHtml || emailBody }}
741 onClick={() => setCurrentStep(2)}
742 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007555]"
749 {/* Step 2: Test Email */}
750 {currentStep === 2 && (
751 <div className="space-y-6">
752 <p className="text-gray-700">
753 Send a test email to verify the content and formatting before sending to all recipients.
757 <label className="block text-sm font-medium text-gray-700 mb-2">
763 onChange={(e) => setTestEmail(e.target.value)}
764 className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-[#00946b]"
765 placeholder="your.email@example.com"
770 onClick={sendTestEmail}
772 className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
774 {loading ? "Sending..." : "Send Test Now"}
778 <div className="p-3 bg-gray-100 rounded text-sm">{testStatus}</div>
781 <div className="flex gap-3">
783 onClick={() => setCurrentStep(1)}
784 className="px-6 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
789 onClick={() => setCurrentStep(3)}
790 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007555]"
792 Next: Select Recipients
798 {/* Step 3: Select Recipients */}
799 {currentStep === 3 && (
800 <div className="space-y-6">
801 <div className="bg-blue-50 border-l-4 border-blue-400 p-4 text-sm text-blue-800">
802 <p className="font-semibold">Tip:</p>
803 <p>This system is designed to create bulk individual customized emails. If you want to manually type addresses, use your email program with BCC.</p>
807 <label className="flex items-center gap-2">
810 checked={allCustomers}
811 onChange={(e) => setAllCustomers(e.target.checked)}
814 <span className="text-sm text-gray-700">Send to All Customers</span>
818 <div className="space-y-3">
819 <label className="flex items-center gap-2">
822 checked={removeDuplicates}
823 onChange={(e) => setRemoveDuplicates(e.target.checked)}
826 <span className="text-sm text-gray-700">Remove Duplicate Addresses</span>
829 <label className="flex items-center gap-2">
832 checked={dropBadEmails}
833 onChange={(e) => setDropBadEmails(e.target.checked)}
836 <span className="text-sm text-gray-700">Ignore Invalid Email Addresses</span>
839 <label className="flex items-center gap-2">
842 checked={removeOptOut}
843 onChange={(e) => setRemoveOptOut(e.target.checked)}
846 <span className="text-sm text-gray-700">Remove Opt-Out Customers</span>
849 <label className="flex items-center gap-2">
852 checked={removeExternalDeny}
853 onChange={(e) => setRemoveExternalDeny(e.target.checked)}
856 <span className="text-sm text-gray-700">Remove "Do Not Use External Agencies"</span>
860 <div className="p-4 bg-gray-100 rounded">
861 <p className="text-lg font-semibold text-gray-900">
862 Probable Recipients: <span className="text-2xl text-[#00946b]">{emailCount}</span>
864 {validationStatus && (
865 <p className="text-sm text-gray-600 mt-2">{validationStatus}</p>
869 <div className="flex gap-3">
871 onClick={() => setCurrentStep(2)}
872 className="px-6 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
877 onClick={() => setCurrentStep(4)}
878 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007555]"
880 Next: Finish and Send
886 {/* Step 4: Finish and Send */}
887 {currentStep === 4 && (
888 <div className="space-y-6">
889 <div className="bg-red-50 border-l-4 border-red-400 p-4 text-sm text-red-800">
890 <p>The emails will be generated and sent individually to each user. If you are sending a large number of emails, your internet provider or email systems may flag them as spam.</p>
891 <p className="mt-2">Email is not a guaranteed delivery mechanism. A percentage of recipients may never see this email.</p>
894 <label className="flex items-start gap-3">
897 checked={acknowledged}
898 onChange={(e) => setAcknowledged(e.target.checked)}
899 className="mt-1 rounded"
901 <span className="text-sm text-gray-700">
902 I am aware that bulk emailing and/or contents is covered by law in many countries and I have verified this email is permitted.
907 onClick={sendBulkEmail}
908 disabled={!acknowledged || loading}
909 className="px-8 py-3 bg-[#00946b] text-white rounded-lg text-lg font-semibold hover:bg-[#007555] disabled:opacity-50 disabled:cursor-not-allowed"
911 {loading ? "Sending..." : "Start Sending Now"}
915 <div className="p-4 bg-gray-100 rounded text-lg font-medium">{sendStatus}</div>
918 <div className="mt-6 border border-gray-300 rounded p-4">
919 <h3 className="font-semibold text-gray-900 mb-2">Email Template:</h3>
920 <div className="text-sm text-gray-700">
921 <p><strong>Subject:</strong> {emailSubject}</p>
922 <div className="mt-2 border-t pt-2" dangerouslySetInnerHTML={{ __html: emailBody }} />
927 onClick={() => setCurrentStep(3)}
928 className="px-6 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"