EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2
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";
14
15interface Customer {
16 f100: number; // Customer ID
17 f101: string; // Name
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
24}
25
26type WizardStep = 1 | 2 | 3 | 4;
27
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("");
34
35 // Dropdown states for toolbar
36 const [showHeadingMenu, setShowHeadingMenu] = useState(false);
37 const [showAlignMenu, setShowAlignMenu] = useState(false);
38
39 // Initialize Tiptap editor
40 const editor = useEditor({
41 extensions: [
42 StarterKit,
43 TextStyle,
44 Color,
45 Underline,
46 Highlight.configure({ multicolor: true }),
47 TextAlign.configure({
48 types: ['heading', 'paragraph'],
49 }),
50 LinkExtension.configure({
51 openOnClick: false,
52 }),
53 Image.configure({
54 inline: true,
55 allowBase64: true,
56 }),
57 ],
58 content: emailBody,
59 onUpdate: ({ editor }) => {
60 setEmailBody(editor.getHTML());
61 },
62 immediatelyRender: false, // Prevent SSR hydration mismatch
63 });
64
65 // Customer selection
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);
73
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("");
81
82 useEffect(() => {
83 if (allCustomers) {
84 loadCustomers();
85 }
86 }, [allCustomers]);
87
88 useEffect(() => {
89 calculateEmailCount();
90 }, [selectedCustomers, removeDuplicates, dropBadEmails, removeOptOut, removeExternalDeny]);
91
92 useEffect(() => {
93 // Generate preview with debouncing
94 const timer = setTimeout(() => {
95 if (emailBody && emailBody !== "Insert your email message here") {
96 generatePreview();
97 }
98 }, 1000);
99 return () => clearTimeout(timer);
100 }, [emailBody]);
101
102 const loadCustomers = async () => {
103 try {
104 setLoading(true);
105 setValidationStatus("Loading customers...");
106
107 const response = await fetch("/api/v1/customers?fields=f115,f100,f101,f210,f213");
108
109 if (!response.ok) {
110 throw new Error(`API error: ${response.status}`);
111 }
112
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([]);
118 return;
119 }
120
121 const data = await response.json();
122
123 if (data.success && data.data?.DATS) {
124 const customers = Array.isArray(data.data.DATS) ? data.data.DATS : [data.data.DATS];
125
126 // Initialize customer data
127 const processedCustomers = customers.map((c: Customer) => ({
128 ...c,
129 fSend: true,
130 fEmailBad: false,
131 }));
132
133 setSelectedCustomers(processedCustomers);
134
135 // Validate email addresses
136 validateEmails(processedCustomers);
137 }
138 } catch (error) {
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([]);
143 } finally {
144 setLoading(false);
145 }
146 };
147
148 const validateEmails = async (customers: Customer[]) => {
149 try {
150 setValidationStatus("⏳ Validating email addresses...");
151
152 const emails = customers.map(c => c.f115).filter(e => e);
153
154 const response = await fetch("/api/v1/email/validate", {
155 method: "POST",
156 headers: { "Content-Type": "application/json" },
157 body: JSON.stringify({ emails }),
158 });
159
160 if (!response.ok) {
161 console.log("Email validation API not available");
162 setValidationStatus("⚠️ Email validation API not yet implemented - skipping validation");
163 return;
164 }
165
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");
169 return;
170 }
171
172 const data = await response.json();
173
174 if (data.success && data.results) {
175 let goodCount = 0;
176 let badCount = 0;
177
178 const updated = customers.map(customer => {
179 const validation = data.results.find((r: any) => r.email === customer.f115);
180 if (validation && validation.errorMask !== 0) {
181 badCount++;
182 return { ...customer, fEmailBad: true, fSend: !dropBadEmails };
183 }
184 goodCount++;
185 return customer;
186 });
187
188 setSelectedCustomers(updated);
189 setValidationStatus(`✅ ${goodCount} valid, ❌ ${badCount} invalid`);
190 }
191 } catch (error) {
192 console.error("Email validation error:", error);
193 setValidationStatus("Validation skipped");
194 }
195 };
196
197 const calculateEmailCount = () => {
198 const seen = new Set<string>();
199 let count = 0;
200
201 for (const customer of selectedCustomers) {
202 if (!customer.fSend) continue;
203
204 const email = customer.f115?.toLowerCase();
205 if (!email) continue;
206
207 // Check filters
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;
212
213 seen.add(email);
214 count++;
215 }
216
217 setEmailCount(count);
218 };
219
220 const generatePreview = async () => {
221 try {
222 const response = await fetch("/api/v1/email/preview", {
223 method: "POST",
224 headers: { "Content-Type": "application/json" },
225 body: JSON.stringify({ body: emailBody }),
226 });
227
228 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
229 setPreviewHtml(emailBody);
230 return;
231 }
232
233 const data = await response.json();
234 if (data.success) {
235 setPreviewHtml(data.preview);
236 }
237 } catch (error) {
238 console.error("Preview error:", error);
239 setPreviewHtml(emailBody);
240 }
241 };
242
243 const sendTestEmail = async () => {
244 if (!testEmail) {
245 alert("Please enter an email address");
246 return;
247 }
248
249 try {
250 setLoading(true);
251 const response = await fetch("/api/v1/email/send-test", {
252 method: "POST",
253 headers: { "Content-Type": "application/json" },
254 body: JSON.stringify({
255 to: testEmail,
256 subject: emailSubject,
257 body: emailBody,
258 }),
259 });
260
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})`);
263 return;
264 }
265
266 const data = await response.json();
267 if (data.success) {
268 setTestStatus(`✅ Test email queued to ${testEmail}`);
269 } else {
270 setTestStatus("❌ Failed to send test");
271 }
272 } catch (error) {
273 console.error("Test email error:", error);
274 setTestStatus("⚠️ Email API not available");
275 } finally {
276 setLoading(false);
277 }
278 };
279
280 const sendBulkEmail = async () => {
281 if (!acknowledged) {
282 alert("Please acknowledge the legal requirements");
283 return;
284 }
285
286 try {
287 setLoading(true);
288 setSendStatus("Preparing emails...");
289
290 // Get final recipient list
291 const recipients: string[] = [];
292 const seen = new Set<string>();
293
294 for (const customer of selectedCustomers) {
295 if (!customer.fSend) continue;
296
297 const email = customer.f115?.toLowerCase();
298 if (!email) continue;
299
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;
304
305 recipients.push(customer.f115);
306 seen.add(email);
307 }
308
309 const response = await fetch("/api/v1/email/send-bulk", {
310 method: "POST",
311 headers: { "Content-Type": "application/json" },
312 body: JSON.stringify({
313 subject: emailSubject,
314 body: emailBody,
315 recipients,
316 }),
317 });
318
319 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
320 setSendStatus("⚠️ Bulk email API not yet implemented - UI demonstration mode");
321 return;
322 }
323
324 const data = await response.json();
325 if (data.success) {
326 setSendStatus(`✅ Successfully queued ${recipients.length} emails!`);
327 } else {
328 setSendStatus("❌ Failed to queue emails");
329 }
330 } catch (error) {
331 console.error("Bulk send error:", error);
332 setSendStatus("⚠️ Email API not available");
333 } finally {
334 setLoading(false);
335 }
336 };
337
338 const insertField = (field: string) => {
339 setEmailBody(prev => prev + field);
340 };
341
342 return (
343 <div className="min-h-screen bg-gray-50">
344 <div className="max-w-7xl mx-auto p-6">
345 {/* Header */}
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>
353 </div>
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
357 </p>
358 </div>
359
360 {/* Warning Box */}
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>
367 </div>
368 </div>
369 </div>
370
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) => (
375 <button
376 key={step}
377 onClick={() => setCurrentStep(step as WizardStep)}
378 className={`flex-1 px-6 py-5 text-center font-semibold transition-all relative ${
379 currentStep === step
380 ? "bg-[#00946b] text-white shadow-lg z-10"
381 : currentStep > step
382 ? "bg-green-50 text-green-700 hover:bg-green-100"
383 : "bg-gray-100 text-gray-500 hover:bg-gray-200"
384 }`}
385 >
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 ${
388 currentStep === step
389 ? "bg-white text-[#00946b]"
390 : currentStep > step
391 ? "bg-green-600 text-white"
392 : "bg-gray-300 text-gray-600"
393 }`}>
394 {currentStep > step ? "✓" : step}
395 </div>
396 <div className="text-sm">
397 {step === 1 ? "Create Email" :
398 step === 2 ? "Test Email" :
399 step === 3 ? "Select Recipients" :
400 "Finish & Send"}
401 </div>
402 </div>
403 {/* Progress connector line */}
404 {step < 4 && (
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' }} />
408 )}
409 </button>
410 ))}
411 </div>
412
413 <div className="p-6">
414 {/* Step 1: Create Email */}
415 {currentStep === 1 && (
416 <div className="space-y-6">
417 <div>
418 <label className="block text-sm font-medium text-gray-700 mb-2">Subject</label>
419 <input
420 type="text"
421 value={emailSubject}
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"
425 />
426 </div>
427
428 <div>
429 <label className="block text-sm font-medium text-gray-700 mb-2">Email Content</label>
430
431 {/* Tiptap Toolbar */}
432 {editor && (
433 <div className="border border-gray-300 rounded-t bg-white p-2 flex flex-wrap items-center gap-1.5 shadow-sm">
434
435 {/* Heading Dropdown */}
436 <div className="relative">
437 <button
438 type="button"
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]"
441 title="Text Style"
442 >
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' :
447 'Paragraph'}
448 </span>
449 <span className="text-xs">▼</span>
450 </button>
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]">
453 <button
454 type="button"
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' : ''}`}
457 >
458 Paragraph
459 </button>
460 <button
461 type="button"
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' : ''}`}
464 >
465 Heading 1
466 </button>
467 <button
468 type="button"
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' : ''}`}
471 >
472 Heading 2
473 </button>
474 <button
475 type="button"
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' : ''}`}
478 >
479 Heading 3
480 </button>
481 </div>
482 )}
483 </div>
484
485 <div className="w-px h-6 bg-gray-300"></div>
486
487 {/* Text Formatting Group */}
488 <div className="flex gap-0.5">
489 <button
490 type="button"
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)"
494 >
495 <strong>B</strong>
496 </button>
497 <button
498 type="button"
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)"
502 >
503 <em>I</em>
504 </button>
505 <button
506 type="button"
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)"
510 >
511 <u>U</u>
512 </button>
513 <button
514 type="button"
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"
518 >
519 <s>S</s>
520 </button>
521 </div>
522
523 <div className="w-px h-6 bg-gray-300"></div>
524
525 {/* Color & Highlight */}
526 <input
527 type="color"
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"
531 title="Text Color"
532 />
533 <button
534 type="button"
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' : ''}`}
537 title="Highlight"
538 >
539 🖍️
540 </button>
541
542 <div className="w-px h-6 bg-gray-300"></div>
543
544 {/* Alignment Dropdown */}
545 <div className="relative">
546 <button
547 type="button"
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"
551 >
552 <span>
553 {editor.isActive({ textAlign: 'center' }) ? '⬌' :
554 editor.isActive({ textAlign: 'right' }) ? '➡️' :
555 editor.isActive({ textAlign: 'justify' }) ? '⬍' :
556 '⬅️'}
557 </span>
558 <span className="text-xs">▼</span>
559 </button>
560 {showAlignMenu && (
561 <div className="absolute top-full left-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10 min-w-[140px]">
562 <button
563 type="button"
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' : ''}`}
566 >
567 <span>⬅️</span> Align Left
568 </button>
569 <button
570 type="button"
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' : ''}`}
573 >
574 <span>⬌</span> Align Center
575 </button>
576 <button
577 type="button"
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' : ''}`}
580 >
581 <span>➡️</span> Align Right
582 </button>
583 <button
584 type="button"
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' : ''}`}
587 >
588 <span>⬍</span> Justify
589 </button>
590 </div>
591 )}
592 </div>
593
594 <div className="w-px h-6 bg-gray-300"></div>
595
596 {/* Lists & Indent */}
597 <div className="flex gap-0.5">
598 <button
599 type="button"
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' : ''}`}
602 title="Bullet List"
603 >
604
605 </button>
606 <button
607 type="button"
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"
611 >
612 1.
613 </button>
614 <button
615 type="button"
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"
619 title="Indent"
620 >
621
622 </button>
623 <button
624 type="button"
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"
628 title="Outdent"
629 >
630
631 </button>
632 </div>
633
634 <div className="w-px h-6 bg-gray-300"></div>
635
636 {/* Quote & Code */}
637 <div className="flex gap-0.5">
638 <button
639 type="button"
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' : ''}`}
642 title="Blockquote"
643 >
644 "
645 </button>
646 <button
647 type="button"
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' : ''}`}
650 title="Code Block"
651 >
652 {'</>'}
653 </button>
654 </div>
655
656 <div className="w-px h-6 bg-gray-300"></div>
657
658 {/* Insert Group */}
659 <div className="flex gap-0.5">
660 <button
661 type="button"
662 onClick={() => {
663 const url = window.prompt('Enter URL:');
664 if (url) {
665 editor.chain().focus().setLink({ href: url }).run();
666 }
667 }}
668 className={`px-2.5 py-1.5 text-sm rounded hover:bg-gray-100 ${editor.isActive('link') ? 'bg-gray-200' : ''}`}
669 title="Insert Link"
670 >
671 🔗
672 </button>
673 <button
674 type="button"
675 onClick={() => {
676 const url = window.prompt('Enter image URL:');
677 if (url) {
678 editor.chain().focus().setImage({ src: url }).run();
679 }
680 }}
681 className="px-2.5 py-1.5 text-sm rounded hover:bg-gray-100"
682 title="Insert Image"
683 >
684 🖼️
685 </button>
686 <button
687 type="button"
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"
691 >
692
693 </button>
694 </div>
695
696 <div className="w-px h-6 bg-gray-300"></div>
697
698 {/* Clear Formatting */}
699 <button
700 type="button"
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"
704 >
705 Clear
706 </button>
707 </div>
708 )}
709
710 {/* Tiptap Editor Content */}
711 <div className="border border-gray-300 rounded-b bg-white">
712 <EditorContent
713 editor={editor}
714 className="prose max-w-none p-4 min-h-[300px] focus:outline-none"
715 />
716 </div>
717 </div>
718
719 <div className="flex gap-4 flex-wrap">
720 <div>
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>
724 </div>
725 <div>
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>
729 </div>
730 </div>
731
732 <div className="mt-6 border border-gray-300 rounded p-4">
733 <h3 className="font-semibold text-gray-900 mb-2">Preview</h3>
734 <div
735 className="prose max-w-none"
736 dangerouslySetInnerHTML={{ __html: previewHtml || emailBody }}
737 />
738 </div>
739
740 <button
741 onClick={() => setCurrentStep(2)}
742 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007555]"
743 >
744 Next: Test Email
745 </button>
746 </div>
747 )}
748
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.
754 </p>
755
756 <div>
757 <label className="block text-sm font-medium text-gray-700 mb-2">
758 Test Email Address
759 </label>
760 <input
761 type="email"
762 value={testEmail}
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"
766 />
767 </div>
768
769 <button
770 onClick={sendTestEmail}
771 disabled={loading}
772 className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
773 >
774 {loading ? "Sending..." : "Send Test Now"}
775 </button>
776
777 {testStatus && (
778 <div className="p-3 bg-gray-100 rounded text-sm">{testStatus}</div>
779 )}
780
781 <div className="flex gap-3">
782 <button
783 onClick={() => setCurrentStep(1)}
784 className="px-6 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
785 >
786 Back
787 </button>
788 <button
789 onClick={() => setCurrentStep(3)}
790 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007555]"
791 >
792 Next: Select Recipients
793 </button>
794 </div>
795 </div>
796 )}
797
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>
804 </div>
805
806 <div>
807 <label className="flex items-center gap-2">
808 <input
809 type="checkbox"
810 checked={allCustomers}
811 onChange={(e) => setAllCustomers(e.target.checked)}
812 className="rounded"
813 />
814 <span className="text-sm text-gray-700">Send to All Customers</span>
815 </label>
816 </div>
817
818 <div className="space-y-3">
819 <label className="flex items-center gap-2">
820 <input
821 type="checkbox"
822 checked={removeDuplicates}
823 onChange={(e) => setRemoveDuplicates(e.target.checked)}
824 className="rounded"
825 />
826 <span className="text-sm text-gray-700">Remove Duplicate Addresses</span>
827 </label>
828
829 <label className="flex items-center gap-2">
830 <input
831 type="checkbox"
832 checked={dropBadEmails}
833 onChange={(e) => setDropBadEmails(e.target.checked)}
834 className="rounded"
835 />
836 <span className="text-sm text-gray-700">Ignore Invalid Email Addresses</span>
837 </label>
838
839 <label className="flex items-center gap-2">
840 <input
841 type="checkbox"
842 checked={removeOptOut}
843 onChange={(e) => setRemoveOptOut(e.target.checked)}
844 className="rounded"
845 />
846 <span className="text-sm text-gray-700">Remove Opt-Out Customers</span>
847 </label>
848
849 <label className="flex items-center gap-2">
850 <input
851 type="checkbox"
852 checked={removeExternalDeny}
853 onChange={(e) => setRemoveExternalDeny(e.target.checked)}
854 className="rounded"
855 />
856 <span className="text-sm text-gray-700">Remove "Do Not Use External Agencies"</span>
857 </label>
858 </div>
859
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>
863 </p>
864 {validationStatus && (
865 <p className="text-sm text-gray-600 mt-2">{validationStatus}</p>
866 )}
867 </div>
868
869 <div className="flex gap-3">
870 <button
871 onClick={() => setCurrentStep(2)}
872 className="px-6 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
873 >
874 Back
875 </button>
876 <button
877 onClick={() => setCurrentStep(4)}
878 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007555]"
879 >
880 Next: Finish and Send
881 </button>
882 </div>
883 </div>
884 )}
885
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>
892 </div>
893
894 <label className="flex items-start gap-3">
895 <input
896 type="checkbox"
897 checked={acknowledged}
898 onChange={(e) => setAcknowledged(e.target.checked)}
899 className="mt-1 rounded"
900 />
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.
903 </span>
904 </label>
905
906 <button
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"
910 >
911 {loading ? "Sending..." : "Start Sending Now"}
912 </button>
913
914 {sendStatus && (
915 <div className="p-4 bg-gray-100 rounded text-lg font-medium">{sendStatus}</div>
916 )}
917
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 }} />
923 </div>
924 </div>
925
926 <button
927 onClick={() => setCurrentStep(3)}
928 className="px-6 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
929 >
930 Back
931 </button>
932 </div>
933 )}
934 </div>
935 </div>
936 </div>
937 </div>
938 );
939}