Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions backend/src/controllers/textJobController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/models/textJobModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
38 changes: 28 additions & 10 deletions frontend/src/app/messages/new/review/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -70,14 +74,17 @@ export default function ReviewAndSendPage() {
const [sending, setSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
const [shortcutUrl, setShortcutUrl] = useState<string | null>(null);
const [sentRecipientCount, setSentRecipientCount] = useState<number | null>(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;
Expand All @@ -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;
}

Expand All @@ -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,
Expand All @@ -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,
})),
});
Expand All @@ -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,
Expand All @@ -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(
/\/+$/,
"",
Expand Down
Loading