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
5 changes: 0 additions & 5 deletions backend/src/controllers/templateController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,6 @@ export const createTemplate: RequestHandler = async (req, res, next) => {
return res.status(400).json({ error: "Email template requires subject" });
}

const template = await TemplateModel.findOne({ title });
if (template) {
return res.status(409).json({ error: "Template with this title aleady exists" });
}

const newTemplate = await TemplateModel.create({
title,
message,
Expand Down
30 changes: 23 additions & 7 deletions frontend/src/app/communication/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";

import { useTextingFlowStore } from "../messages/new/_store/textingFlowStore";
Expand All @@ -19,21 +20,36 @@ export async function handleEmailClick() {
}

export default function CommunicationPage() {
const searchParams = useSearchParams();
const requestedMode = searchParams.get("mode");
const [emailReady, setEmailReady] = useState(false);
const mode = useTextingFlowStore((s) => s.mode);
const setMode = useTextingFlowStore((s) => s.setMode);

useEffect(() => {
let mounted = true;
const wantsEmail =
requestedMode === "email" || sessionStorage.getItem("pendingMode") === "email";

const finalizeMicrosoftLogin = async () => {
const account = await initMsal();

if (!mounted) return;

if (wantsEmail && account) {
setMode("email");
sessionStorage.removeItem("pendingMode");
setEmailReady(true);
return;
}

if (wantsEmail && !account) {
sessionStorage.setItem("pendingMode", "email");
await signInWithOutlook();
return;
}

if (mounted) {
const pendingMode = sessionStorage.getItem("pendingMode");
if (account && pendingMode === "email") {
setMode("email");
sessionStorage.removeItem("pendingMode");
}
setEmailReady(true);
}
};
Expand All @@ -43,11 +59,11 @@ export default function CommunicationPage() {
return () => {
mounted = false;
};
}, [mode]);
}, [requestedMode, setMode]);

const pendingMode = sessionStorage.getItem("pendingMode");

if (mode === "text" && !pendingMode) {
if (mode === "text" && !pendingMode && requestedMode !== "email") {
return <NewMessagePage />;
}

Expand Down
14 changes: 5 additions & 9 deletions frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
import { signOut } from "firebase/auth";
import Image from "next/image";
import Link from "next/link";
import { type MouseEvent, useState } from "react";
import { useState } from "react";

import styles from "./page.module.css";

import { handleEmailClick } from "@/app/communication/page";
import { useTextingFlowStore } from "@/app/messages/new/_store/textingFlowStore";
import icCaretRightAsset from "@/assets/chevron_backward.svg";
import icMessageAsset from "@/assets/ic_message.svg";
Expand All @@ -28,11 +27,6 @@ export default function Dashboard() {
const [showLogoutModal, setShowLogoutModal] = useState(false);
const setMode = useTextingFlowStore((s) => s.setMode);

const handleSendEmailCardClick = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
void handleEmailClick();
};

const CARDS = [
{
href: "/communication",
Expand All @@ -42,8 +36,10 @@ export default function Dashboard() {
icon: icMessage,
},
{
href: "/communication",
onClick: handleSendEmailCardClick,
href: "/communication?mode=email",
onClick: () => {
sessionStorage.setItem("pendingMode", "email");
},
majorText: "Send Email",
minorText: "Send detailed announcements",
icon: mail,
Expand Down
15 changes: 8 additions & 7 deletions frontend/src/app/messages/new/review/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
}

.page {
height: 100dvh;
height: 100%;
min-height: 0;
overflow: hidden;
background: #ffffff;
display: flex;
flex-direction: column;
Expand All @@ -18,6 +20,7 @@
position: sticky;
top: 0;
z-index: 1000;
flex: 0 0 auto;
height: 64px;
display: grid;
grid-template-columns: 48px 1fr 48px;
Expand Down Expand Up @@ -65,7 +68,7 @@
overflow-y: auto;
-webkit-overflow-scrolling: touch;
max-width: 720px;
padding-top: 25px;
padding: 25px 0 12px;
margin: 0 auto;
}

Expand Down Expand Up @@ -187,13 +190,11 @@
}

.bottomCta {
min-width: min(420px, calc(100% - 30px));
width: 100%;
bottom: calc(102px + 12px + env(safe-area-inset-bottom));
width: min(720px, calc(100% - 30px));
display: flex;
flex: 0 0 auto;
align-self: center;
justify-self: flex-start;
margin-top: 16px;
margin: 0 auto calc(16px + env(safe-area-inset-bottom));
gap: 16px;
flex-direction: column;
}
Expand Down
40 changes: 38 additions & 2 deletions frontend/src/app/messages/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@
width: 100%;
min-width: 0;
max-width: 100%;
overflow-x: auto;
overflow-x: visible;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
}

.messageFilterPills.messageFilterPills {
display: flex;
}

.emptyState {
Expand Down Expand Up @@ -126,6 +129,25 @@
margin-top: 16px;
}

.datePeriodClearButton {
margin-right: auto;
border: none;
background: transparent;
color: #676767;
font-family: var(--Font-family-Body, "Open Sans");
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.5px;
text-decoration: underline;
cursor: pointer;
}

.datePeriodClearButton:hover {
color: #141414;
}

.datePeriodPrimaryButton,
.datePeriodSecondaryButton {
display: flex;
Expand Down Expand Up @@ -162,6 +184,13 @@
}

@media (max-width: 747px) {
.messageFilterPills.messageFilterPills {
display: flex;
height: auto;
min-height: 32px;
flex-wrap: wrap;
}

.datePaneSwitch {
display: flex;
width: 100%;
Expand Down Expand Up @@ -215,6 +244,7 @@
}

.datePeriodButtons {
flex-wrap: wrap;
justify-content: stretch;
}

Expand All @@ -223,6 +253,12 @@
flex: 1;
}

.datePeriodClearButton {
width: 100%;
margin-right: 0;
text-align: center;
}

.typeDateGroup {
align-items: flex-end;
flex-direction: column;
Expand Down
54 changes: 39 additions & 15 deletions frontend/src/app/messages/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const unionIcon = unionIconAsset as string;
const ITEMS_PER_PAGE = 25;
const FETCH_LIMIT = 100;

type MessageTypeFilter = "all" | "text" | "email";
type MessageTypeFilter = "text" | "email";

const formatDate = (value: string) => {
return new Date(value).toLocaleDateString("en-US", {
Expand Down Expand Up @@ -71,7 +71,9 @@ export default function MessagesPage() {
const [messages, setMessages] = useState<Message[]>([]);
const [selectedMessage, setSelectedMessage] = useState<Message | undefined>(undefined);
const [search, setSearch] = useState("");
const [typeFilter, setTypeFilter] = useState<MessageTypeFilter>("all");
const [typeFilters, setTypeFilters] = useState<Set<MessageTypeFilter>>(
() => new Set(["text", "email"]),
);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
Expand Down Expand Up @@ -136,7 +138,7 @@ export default function MessagesPage() {

useEffect(() => {
setCurrentPage(1);
}, [search, typeFilter, startDate, endDate]);
}, [search, typeFilters, startDate, endDate]);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Expand All @@ -152,20 +154,20 @@ export default function MessagesPage() {

const filteredMessages = useMemo(() => {
return messages.filter((message) => {
if (typeFilter !== "all" && message.type !== typeFilter) return false;
if (!typeFilters.has(message.type)) return false;
const messageTime = new Date(message.timestamp).getTime();
if (startDate && messageTime < startDate.getTime()) return false;
if (endDate && messageTime > endDate.getTime()) return false;
return matchesQuery(message, search);
});
}, [endDate, messages, search, startDate, typeFilter]);
}, [endDate, messages, search, startDate, typeFilters]);

const displayedMessages = filteredMessages.slice(
(currentPage - 1) * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE,
);

const typeOptions: { label: string; value: Exclude<MessageTypeFilter, "all"> }[] = [
const typeOptions: { label: string; value: MessageTypeFilter }[] = [
{ label: "Text", value: "text" },
{ label: "Email", value: "email" },
];
Expand Down Expand Up @@ -210,7 +212,10 @@ export default function MessagesPage() {
</form>
</div>

<div className={searchBarStyles.tagsContainer} ref={filterRef}>
<div
className={`${searchBarStyles.tagsContainer} ${styles.messageFilterPills}`}
ref={filterRef}
>
<div className={searchBarStyles.pillWrapper}>
<button
type="button"
Expand Down Expand Up @@ -245,7 +250,7 @@ export default function MessagesPage() {
>
<div className={searchBarStyles.dropdownItemContainer}>
{typeOptions.map((option) => {
const selected = typeFilter === option.value;
const selected = typeFilters.has(option.value);
return (
<button
key={option.value}
Expand All @@ -255,11 +260,17 @@ export default function MessagesPage() {
}`}
role="menuitemcheckbox"
aria-checked={selected}
onClick={() =>
setTypeFilter((current) =>
current === option.value ? "all" : option.value,
)
}
onClick={() => {
setTypeFilters((current) => {
const next = new Set(current);
if (next.has(option.value)) {
next.delete(option.value);
} else {
next.add(option.value);
}
return next;
});
}}
>
<span
className={`${searchBarStyles.checkBox} ${
Expand All @@ -286,8 +297,8 @@ export default function MessagesPage() {
<button
type="button"
className={searchBarStyles.dropdownClearButton}
onClick={() => setTypeFilter("all")}
disabled={typeFilter === "all"}
onClick={() => setTypeFilters(new Set())}
disabled={typeFilters.size === 0}
>
Clear
</button>
Expand Down Expand Up @@ -460,6 +471,19 @@ export default function MessagesPage() {
</div>
</div>
<div className={styles.datePeriodButtons}>
<button
type="button"
className={styles.datePeriodClearButton}
onClick={() => {
setDraftStartDate(null);
setDraftEndDate(null);
setStartDate(null);
setEndDate(null);
setIsDatePeriodModalOpen(false);
}}
>
Clear Date Range
</button>
<button
type="button"
className={styles.datePeriodSecondaryButton}
Expand Down
Loading
Loading