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";
5
6interface Customer {
7 f100: number; // Customer ID
8 f101: string; // Name
9 f112?: string; // Phone
10 f113?: string; // Mobile
11 f1126?: string; // SMS Number (sending to)
12 f210?: number; // Marketing preference (6=opt-out, 1=opt-in)
13 f213?: number; // External flags
14 f501?: number; // Set ID
15 fSend?: boolean; // Should send to this customer
16 fPhoneBad?: boolean; // Phone validation failed
17}
18
19type WizardStep = 1 | 2 | 3 | 4;
20
21export default function BulkSmsPage() {
22 const [currentStep, setCurrentStep] = useState<WizardStep>(1);
23 const [smsMessage, setSmsMessage] = useState("");
24 const [testPhone, setTestPhone] = useState("");
25 const [testStatus, setTestStatus] = useState("");
26
27 // Message stats
28 const [messageLength, setMessageLength] = useState(0);
29 const [smsCount, setSmsCount] = useState(0);
30
31 // Customer selection
32 const [selectedCustomers, setSelectedCustomers] = useState<Customer[]>([]);
33 const [allCustomers, setAllCustomers] = useState(false);
34 const [selectedSet, setSelectedSet] = useState(0);
35 const [removeDuplicates, setRemoveDuplicates] = useState(true);
36 const [removeOptOut, setRemoveOptOut] = useState(true);
37
38 // Validation and status
39 const [loading, setLoading] = useState(false);
40 const [phoneCount, setPhoneCount] = useState(0);
41 const [validationStatus, setValidationStatus] = useState("");
42 const [acknowledged, setAcknowledged] = useState(false);
43 const [sendStatus, setSendStatus] = useState("");
44 const [previewText, setPreviewText] = useState("");
45
46 // Calculate SMS count based on message length
47 useEffect(() => {
48 const len = smsMessage.length;
49 setMessageLength(len);
50
51 if (len === 0) {
52 setSmsCount(0);
53 } else if (len <= 160) {
54 setSmsCount(1);
55 } else if (len === 612) {
56 setSmsCount(4);
57 } else {
58 setSmsCount(1 + Math.floor(len / 153));
59 }
60 }, [smsMessage]);
61
62 useEffect(() => {
63 if (allCustomers || selectedSet > 0) {
64 loadCustomers();
65 }
66 }, [allCustomers, selectedSet]);
67
68 useEffect(() => {
69 calculatePhoneCount();
70 }, [selectedCustomers, removeDuplicates, removeOptOut]);
71
72 const loadCustomers = async () => {
73 try {
74 setLoading(true);
75 setValidationStatus("Loading customers...");
76
77 let url = "/api/v1/customers?fields=f1126,f100,f101,f112,f113,f210,f213";
78 if (selectedSet > 0) {
79 url += `&set=${selectedSet}`;
80 }
81
82 const response = await fetch(url);
83
84 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
85 setValidationStatus("⚠️ Customer API not yet implemented - UI demonstration mode");
86 setSelectedCustomers([]);
87 return;
88 }
89
90 const data = await response.json();
91
92 if (data.success && data.data?.DATS) {
93 const customers = Array.isArray(data.data.DATS) ? data.data.DATS : [data.data.DATS];
94
95 const processedCustomers = customers.map((c: Customer) => ({
96 ...c,
97 fSend: true,
98 fPhoneBad: !c.f1126 || c.f1126.length < 10,
99 }));
100
101 setSelectedCustomers(processedCustomers);
102 setValidationStatus("");
103 }
104 } catch (error) {
105 console.error("Error loading customers:", error);
106 setValidationStatus("⚠️ Customer API not available - UI demonstration mode");
107 setSelectedCustomers([]);
108 } finally {
109 setLoading(false);
110 }
111 };
112
113 const calculatePhoneCount = () => {
114 const seen = new Set<string>();
115 let count = 0;
116
117 for (const customer of selectedCustomers) {
118 if (!customer.fSend) continue;
119
120 const phone = customer.f1126?.toLowerCase();
121 if (!phone) continue;
122
123 if (removeDuplicates && seen.has(phone)) continue;
124 if (removeOptOut && customer.f210 === 6) continue;
125
126 seen.add(phone);
127 count++;
128 }
129
130 setPhoneCount(count);
131 };
132
133 const generatePreview = async () => {
134 try {
135 const response = await fetch("/api/v1/sms/preview", {
136 method: "POST",
137 headers: { "Content-Type": "application/json" },
138 body: JSON.stringify({ body: smsMessage }),
139 });
140
141 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
142 setPreviewText(smsMessage);
143 return;
144 }
145
146 const data = await response.json();
147 if (data.success) {
148 setPreviewText(data.preview);
149 }
150 } catch (error) {
151 console.error("Preview error:", error);
152 setPreviewText(smsMessage);
153 }
154 };
155
156 const sendTestSms = async () => {
157 if (!testPhone || testPhone.length < 10) {
158 alert("Please enter a valid phone number");
159 return;
160 }
161
162 try {
163 setLoading(true);
164 setTestStatus("⏳ Sending test SMS...");
165
166 const response = await fetch("/api/v1/sms/send-test", {
167 method: "POST",
168 headers: { "Content-Type": "application/json" },
169 body: JSON.stringify({
170 to: testPhone,
171 message: smsMessage,
172 }),
173 });
174
175 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
176 setTestStatus(`⚠️ SMS API not yet implemented - UI demonstration mode (would send to: ${testPhone})`);
177 return;
178 }
179
180 const data = await response.json();
181 if (data.success) {
182 setTestStatus(`✅ Test SMS queued to ${testPhone}`);
183 } else {
184 setTestStatus("❌ Failed to send test");
185 }
186 } catch (error) {
187 console.error("Test SMS error:", error);
188 setTestStatus("⚠️ SMS API not available");
189 } finally {
190 setLoading(false);
191 }
192 };
193
194 const sendBulkSms = async () => {
195 if (!acknowledged) {
196 alert("Please acknowledge the legal requirements");
197 return;
198 }
199
200 try {
201 setLoading(true);
202 setSendStatus("⏳ Sending SMS messages...");
203
204 const seen = new Set<string>();
205 const recipients: string[] = [];
206
207 for (const customer of selectedCustomers) {
208 if (!customer.fSend) continue;
209
210 const phone = customer.f1126?.toLowerCase();
211 if (!phone) continue;
212
213 if (removeDuplicates && seen.has(phone)) continue;
214 if (removeOptOut && customer.f210 === 6) continue;
215
216 recipients.push(customer.f1126!);
217 seen.add(phone);
218 }
219
220 const response = await fetch("/api/v1/sms/send-bulk", {
221 method: "POST",
222 headers: { "Content-Type": "application/json" },
223 body: JSON.stringify({
224 message: smsMessage,
225 recipients,
226 }),
227 });
228
229 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
230 setSendStatus("⚠️ Bulk SMS API not yet implemented - UI demonstration mode");
231 return;
232 }
233
234 const data = await response.json();
235 if (data.success) {
236 setSendStatus(`✅ Successfully queued ${recipients.length} SMS messages!`);
237 } else {
238 setSendStatus("❌ Failed to queue messages");
239 }
240 } catch (error) {
241 console.error("Bulk send error:", error);
242 setSendStatus("⚠️ SMS API not available");
243 } finally {
244 setLoading(false);
245 }
246 };
247
248 const insertField = (field: string) => {
249 setSmsMessage(prev => prev + field);
250 };
251
252 return (
253 <div className="min-h-screen bg-gray-50">
254 <div className="max-w-7xl mx-auto p-6">
255 {/* Header */}
256 <div className="mb-8">
257 <div className="flex items-center gap-3 mb-2">
258 <Link href="/marketing" className="text-gray-600 hover:text-gray-800">Marketing</Link>
259 <span className="text-gray-400">/</span>
260 <Link href="/marketing/messaging" className="text-gray-600 hover:text-gray-800">Email & Interfaces</Link>
261 <span className="text-gray-400">/</span>
262 <span className="text-gray-900 font-semibold">Send Bulk SMS</span>
263 </div>
264 <h1 className="text-3xl font-bold text-gray-900 mb-2">Send Bulk SMS</h1>
265 <p className="text-gray-600 text-sm">
266 Create and send an SMS/Text message to multiple customers
267 </p>
268 </div>
269
270 {/* Warning Box */}
271 <div className="mb-6 bg-yellow-50 border-l-4 border-yellow-400 p-4">
272 <div className="flex items-start">
273 <span className="text-2xl mr-3">⚠️</span>
274 <div className="text-sm text-yellow-800">
275 <p className="font-semibold mb-1">Extended Functionality</p>
276 <p>Bulk SMS sending is covered by law in many countries. Ensure compliance before sending.</p>
277 </div>
278 </div>
279 </div>
280
281 {/* Step Navigation */}
282 <div className="bg-white rounded-lg shadow-md mb-6 overflow-hidden">
283 <div className="flex">
284 {[1, 2, 3, 4].map((step) => (
285 <button
286 key={step}
287 onClick={() => setCurrentStep(step as WizardStep)}
288 className={`flex-1 px-6 py-5 text-center font-semibold transition-all relative ${
289 currentStep === step
290 ? "bg-[#00946b] text-white shadow-lg z-10"
291 : currentStep > step
292 ? "bg-green-50 text-green-700 hover:bg-green-100"
293 : "bg-gray-100 text-gray-500 hover:bg-gray-200"
294 }`}
295 >
296 <div className="flex flex-col items-center gap-2">
297 <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
298 currentStep === step
299 ? "bg-white text-[#00946b]"
300 : currentStep > step
301 ? "bg-green-600 text-white"
302 : "bg-gray-300 text-gray-600"
303 }`}>
304 {currentStep > step ? "✓" : step}
305 </div>
306 <div className="text-sm">
307 {step === 1 ? "Create Message" :
308 step === 2 ? "Test Message" :
309 step === 3 ? "Select Recipients" :
310 "Finish & Send"}
311 </div>
312 </div>
313 </button>
314 ))}
315 </div>
316
317 <div className="p-6">
318 {/* Step 1: Create Message */}
319 {currentStep === 1 && (
320 <div className="space-y-6">
321 <div>
322 <label className="block text-sm font-medium text-gray-700 mb-2">Message</label>
323 <textarea
324 value={smsMessage}
325 onChange={(e) => {
326 if (e.target.value.length <= 612) {
327 setSmsMessage(e.target.value);
328 }
329 }}
330 rows={5}
331 className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-[#00946b]"
332 placeholder="Enter your SMS message here"
333 />
334 <div className="mt-2 text-sm text-gray-600">
335 Length: {messageLength} / 612 characters, SMS Charge: ×{smsCount}
336 {messageLength > 612 && <span className="text-red-600 ml-2">Message is too long. 612 is max</span>}
337 </div>
338 </div>
339
340 <div className="flex gap-4 flex-wrap">
341 <div>
342 <p className="text-sm font-medium text-gray-700 mb-2">Insert Customer</p>
343 <div className="flex gap-2">
344 <button
345 type="button"
346 onClick={() => insertField('%customer.name%')}
347 className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
348 >
349 Name
350 </button>
351 <button
352 type="button"
353 onClick={() => insertField('%customer.cid%')}
354 className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
355 >
356 Id#
357 </button>
358 </div>
359 </div>
360
361 <div>
362 <p className="text-sm font-medium text-gray-700 mb-2">Insert Account <span className="text-xs text-gray-500">(where applicable)</span></p>
363 <div className="flex gap-2">
364 <button
365 type="button"
366 onClick={() => insertField('%customer.account.name%')}
367 className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
368 >
369 Name
370 </button>
371 <button
372 type="button"
373 onClick={() => insertField('%customer.account.balance%')}
374 className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
375 >
376 Balance
377 </button>
378 </div>
379 </div>
380 </div>
381
382 <div className="border border-gray-300 rounded p-4">
383 <h3 className="text-lg font-semibold mb-2">Preview</h3>
384 <div className="text-sm text-gray-700 whitespace-pre-wrap">
385 {smsMessage || <span className="text-gray-400 italic">Your message preview will appear here</span>}
386 </div>
387 </div>
388
389 <div className="flex justify-end">
390 <button
391 onClick={() => setCurrentStep(2)}
392 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007a59]"
393 >
394 Next: Test Message
395 </button>
396 </div>
397 </div>
398 )}
399
400 {/* Step 2: Test Message */}
401 {currentStep === 2 && (
402 <div className="space-y-6">
403 <div>
404 <label className="block text-sm font-medium text-gray-700 mb-2">
405 What phone number should receive this test message?
406 </label>
407 <input
408 type="tel"
409 value={testPhone}
410 onChange={(e) => setTestPhone(e.target.value)}
411 className="w-full max-w-md px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-[#00946b]"
412 placeholder="Enter phone number"
413 />
414 </div>
415
416 <button
417 onClick={sendTestSms}
418 disabled={loading || !testPhone}
419 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007a59] disabled:opacity-50 disabled:cursor-not-allowed"
420 >
421 Send Test Now
422 </button>
423
424 {testStatus && (
425 <div className="p-4 bg-blue-50 border border-blue-200 rounded">
426 {testStatus}
427 </div>
428 )}
429
430 <div className="flex justify-between">
431 <button
432 onClick={() => setCurrentStep(1)}
433 className="px-6 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
434 >
435 Back
436 </button>
437 <button
438 onClick={() => setCurrentStep(3)}
439 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007a59]"
440 >
441 Next: Select Recipients
442 </button>
443 </div>
444 </div>
445 )}
446
447 {/* Step 3: Select Recipients */}
448 {currentStep === 3 && (
449 <div className="space-y-6">
450 <div>
451 <p className="text-sm font-medium text-gray-700 mb-3">I am sending to:</p>
452 <label className="flex items-center gap-2">
453 <input
454 type="checkbox"
455 checked={allCustomers}
456 onChange={(e) => setAllCustomers(e.target.checked)}
457 className="rounded"
458 />
459 <span>All Customers</span>
460 </label>
461 </div>
462
463 <div className="p-4 bg-gray-50 rounded border border-gray-200">
464 <p className="text-sm font-medium text-gray-700">
465 Probable number of recipients: <span className="text-2xl font-bold text-[#00946b]">{phoneCount}</span>
466 </p>
467 </div>
468
469 <div>
470 <label className="flex items-center gap-2">
471 <input
472 type="checkbox"
473 checked={removeDuplicates}
474 onChange={(e) => setRemoveDuplicates(e.target.checked)}
475 className="rounded"
476 />
477 <span className="text-sm">Remove Duplicate Numbers</span>
478 </label>
479 <p className="text-xs text-gray-500 ml-6">
480 If the same phone number appears on multiple records, send only one message per number
481 </p>
482 </div>
483
484 {validationStatus && (
485 <div className="p-4 bg-yellow-50 border border-yellow-200 rounded text-sm">
486 {validationStatus}
487 </div>
488 )}
489
490 <div className="flex justify-between">
491 <button
492 onClick={() => setCurrentStep(2)}
493 className="px-6 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
494 >
495 Back
496 </button>
497 <button
498 onClick={() => setCurrentStep(4)}
499 className="px-6 py-2 bg-[#00946b] text-white rounded hover:bg-[#007a59]"
500 >
501 Next: Finish & Send
502 </button>
503 </div>
504 </div>
505 )}
506
507 {/* Step 4: Finish and Send */}
508 {currentStep === 4 && (
509 <div className="space-y-6">
510 <div className="p-4 bg-blue-50 border border-blue-200 rounded">
511 <p className="text-sm text-blue-900">
512 The messages will be generated and sent individually to each user. If you are sending a large number
513 of messages, your service provider may apply rate limits or impose other conditions.
514 </p>
515 </div>
516
517 <div>
518 <label className="flex items-start gap-3">
519 <input
520 type="checkbox"
521 checked={acknowledged}
522 onChange={(e) => setAcknowledged(e.target.checked)}
523 className="mt-1 rounded"
524 />
525 <span className="text-sm">
526 I am aware that bulk sending of text messages and/or contents is covered by law in many countries
527 and I have verified this message is permitted.
528 </span>
529 </label>
530 </div>
531
532 <button
533 onClick={sendBulkSms}
534 disabled={!acknowledged || loading}
535 className="w-full py-3 text-lg font-semibold bg-[#00946b] text-white rounded hover:bg-[#007a59] disabled:opacity-50 disabled:cursor-not-allowed"
536 >
537 Start Sending Now
538 </button>
539
540 {sendStatus && (
541 <div className="p-4 bg-green-50 border border-green-200 rounded">
542 {sendStatus}
543 </div>
544 )}
545
546 <div className="border border-gray-300 rounded p-4">
547 <h3 className="text-lg font-semibold mb-2">Message Template:</h3>
548 <div className="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded">
549 {smsMessage}
550 </div>
551 </div>
552
553 <div className="flex justify-between">
554 <button
555 onClick={() => setCurrentStep(3)}
556 className="px-6 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
557 >
558 Back
559 </button>
560 </div>
561 </div>
562 )}
563 </div>
564 </div>
565 </div>
566 </div>
567 );
568}