diff --git a/backend/src/controllers/textJobController.ts b/backend/src/controllers/textJobController.ts index ef5a51f8..c7edbefc 100644 --- a/backend/src/controllers/textJobController.ts +++ b/backend/src/controllers/textJobController.ts @@ -9,8 +9,8 @@ import type { NextFunction, Request, RequestHandler, Response } from "express"; const FIRST_NAME_TOKEN = "{{First Name}}"; type Recipient = { - firstName: string; - phoneNumber: string; + firstName?: string; + phoneNumber?: string; }; type CreateTextJobBody = { @@ -27,8 +27,20 @@ export const createTextJob = async (req: Request, res: Response, next: NextFunct return; } + const textableRecipients = recipients + .filter((recipient) => recipient.phoneNumber?.trim()) + .map((recipient) => ({ + firstName: recipient.firstName ?? "", + phoneNumber: recipient.phoneNumber!.trim(), + })); + + if (textableRecipients.length === 0) { + res.status(400).json({ error: "at least one recipient with a phone number is required" }); + return; + } + const jobId = randomUUID(); - await TextJobModel.create({ jobId, message, recipients }); + await TextJobModel.create({ jobId, message, recipients: textableRecipients }); res.status(201).json({ jobId }); } catch (error) { @@ -47,7 +59,7 @@ export const getTextJob: RequestHandler = async (req, res, next) => { res.status(200).json({ messages: job.recipients.map((r) => ({ to: r.phoneNumber, - body: job.message.replaceAll(FIRST_NAME_TOKEN, r.firstName), + body: job.message.replaceAll(FIRST_NAME_TOKEN, r.firstName ?? ""), })), }); } catch (error) { diff --git a/backend/src/models/textJobModel.ts b/backend/src/models/textJobModel.ts index 0a766b5c..cd59adff 100644 --- a/backend/src/models/textJobModel.ts +++ b/backend/src/models/textJobModel.ts @@ -4,7 +4,7 @@ import type { InferSchemaType } from "mongoose"; const recipientSchema = new Schema( { - firstName: { type: String, required: true }, + firstName: { type: String, default: "" }, phoneNumber: { type: String, required: true }, }, { _id: false }, diff --git a/frontend/src/app/messages/new/review/page.tsx b/frontend/src/app/messages/new/review/page.tsx index 30e9f3a2..16d0066a 100644 --- a/frontend/src/app/messages/new/review/page.tsx +++ b/frontend/src/app/messages/new/review/page.tsx @@ -23,6 +23,10 @@ const FIRST_NAME_TOKEN = "{{First Name}}"; const backIcon = backIconAsset as string; const groupsIcon = groupsIconAsset as string; +const hasContactValue = (value: string | undefined) => Boolean(value?.trim()); +const createSuccessMessage = (count: number) => + `Your message has successfully\nbeen sent to ${count} volunteers.`; + export default function ReviewAndSendPage() { const router = useRouter(); @@ -70,14 +74,17 @@ export default function ReviewAndSendPage() { const [sending, setSending] = useState(false); const [sendError, setSendError] = useState(null); const [shortcutUrl, setShortcutUrl] = useState(null); + const [sentRecipientCount, setSentRecipientCount] = useState(null); useEffect(() => { setShortcutUrl(null); + setSentRecipientCount(null); }, [selectedRecipientIds]); const successMessage = useMemo(() => { - return `Your message has successfully\nbeen sent to ${recipientsCount} volunteers.`; - }, [recipientsCount]); + const count = sentRecipientCount ?? recipientsCount; + return createSuccessMessage(count); + }, [recipientsCount, sentRecipientCount]); const handleSend = async () => { if (!canSend || sending) return; @@ -96,9 +103,16 @@ export default function ReviewAndSendPage() { } const allVolunteers = await fetchVolunteers(); - const recipients = allVolunteers.filter((v) => selectedRecipientIds.includes(v._id)); + const recipients = allVolunteers + .filter((v) => selectedRecipientIds.includes(v._id)) + .filter((v) => hasContactValue(v.email)) + .map((v) => ({ + ...v, + firstName: v.firstName ?? "", + lastName: v.lastName ?? "", + })); if (recipients.length === 0) { - setSendError("Could not find recipient data. Please re-select your recipients."); + setSendError("None of the selected recipients have an email address."); return; } @@ -116,7 +130,7 @@ export default function ReviewAndSendPage() { } const historyResult = await createMessageHistory({ - recipients: selectedRecipientIds, + recipients: recipients.map((recipient) => recipient._id), type: "email", subject, body: message, @@ -128,23 +142,26 @@ export default function ReviewAndSendPage() { return; } - sessionStorage.setItem("success-toast", successMessage); + setSentRecipientCount(recipients.length); + sessionStorage.setItem("success-toast", createSuccessMessage(recipients.length)); resetDraft(); router.replace("/communication"); return; } const allVolunteers = await fetchVolunteers(); - const recipients = allVolunteers.filter((v) => selectedRecipientIds.includes(v._id)); + const recipients = allVolunteers + .filter((v) => selectedRecipientIds.includes(v._id)) + .filter((v) => hasContactValue(v.phoneNumber)); if (recipients.length === 0) { - setSendError("Could not find recipient data. Please re-select your recipients."); + setSendError("None of the selected recipients have a phone number."); return; } const jobResult = await createTextJob({ message, recipients: recipients.map((recipient) => ({ - firstName: recipient.firstName, + firstName: recipient.firstName ?? "", phoneNumber: recipient.phoneNumber, })), }); @@ -155,7 +172,7 @@ export default function ReviewAndSendPage() { } const historyResult = await createMessageHistory({ - recipients: selectedRecipientIds, + recipients: recipients.map((recipient) => recipient._id), type: "text", subject: null, body: message, @@ -167,6 +184,7 @@ export default function ReviewAndSendPage() { return; } + setSentRecipientCount(recipients.length); const shortcutBase = (process.env.NEXT_PUBLIC_SHORTCUT_API_URL ?? API_BASE_URL).replace( /\/+$/, "",