Skip to content

Commit abe1d5e

Browse files
authored
feat: add severity levels and sorting to announcements (#2558)
1 parent 5134548 commit abe1d5e

8 files changed

Lines changed: 272 additions & 28 deletions

File tree

client/src/components/Admin/Announcements.jsx

Lines changed: 114 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import {
44
CircularProgress,
55
Container,
66
Dialog,
7+
FormControl,
78
FormControlLabel,
89
IconButton,
10+
InputLabel,
11+
MenuItem,
12+
Select,
913
Paper,
1014
Stack,
1115
Switch,
@@ -19,9 +23,7 @@ import {
1923
TextField,
2024
Typography,
2125
} from "@mui/material";
22-
import {
23-
Delete as DeleteIcon,
24-
} from "@mui/icons-material";
26+
import { Delete as DeleteIcon } from "@mui/icons-material";
2527
import { useFormik } from "formik";
2628
import React, { useState } from "react";
2729
import * as Yup from "yup";
@@ -35,6 +37,8 @@ const Announcements = () => {
3537
const [editAnnouncement, setEditAnnouncement] = useState(null);
3638
const [page, setPage] = React.useState(0);
3739
const [rowsPerPage, setRowsPerPage] = React.useState(10);
40+
const [sortBy, setSortBy] = useState("created_at");
41+
const [sortDirection, setSortDirection] = useState("desc");
3842

3943
const {
4044
data: announcementsData,
@@ -58,6 +62,7 @@ const Announcements = () => {
5862
title: announcement.title,
5963
description: announcement.description,
6064
is_enabled: isEnabled,
65+
severity: announcement.severity,
6166
});
6267
await announcementRefetch();
6368
} catch (error) {
@@ -70,6 +75,7 @@ const Announcements = () => {
7075
title: "",
7176
description: "",
7277
is_enabled: false,
78+
severity: "info",
7379
},
7480
validationSchema: Yup.object({
7581
title: Yup.string().trim().required("Title is required"),
@@ -90,6 +96,7 @@ const Announcements = () => {
9096
title: editAnnouncement?.title || "",
9197
description: editAnnouncement?.description || "",
9298
is_enabled: editAnnouncement?.is_enabled || false,
99+
severity: editAnnouncement?.severity || "info",
93100
},
94101
validationSchema: Yup.object({
95102
title: Yup.string().trim().required("Title is required"),
@@ -110,6 +117,32 @@ const Announcements = () => {
110117
setPage(0);
111118
};
112119

120+
const sortedAnnouncements = React.useMemo(() => {
121+
if (!announcementsData) return [];
122+
return [...announcementsData].sort((a, b) => {
123+
let aVal = a[sortBy];
124+
let bVal = b[sortBy];
125+
126+
if (sortBy === "created_at") {
127+
aVal = new Date(aVal);
128+
bVal = new Date(bVal);
129+
} else if (sortBy === "severity") {
130+
const severityMap = { info: 0, warning: 1, error: 2, success: 3 };
131+
aVal = severityMap[aVal];
132+
bVal = severityMap[bVal];
133+
} else if (typeof aVal === "string") {
134+
aVal = aVal.toLowerCase();
135+
bVal = bVal.toLowerCase();
136+
}
137+
138+
if (sortDirection === "asc") {
139+
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
140+
} else {
141+
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
142+
}
143+
});
144+
}, [announcementsData, sortBy, sortDirection]);
145+
113146
if (announcementsLoading || !announcementsData) {
114147
return (
115148
<Stack
@@ -139,29 +172,58 @@ const Announcements = () => {
139172
<Typography variant="h2" style={{ margin: 0, fontWeight: "bold" }}>
140173
Announcements
141174
</Typography>
142-
<Button
143-
variant="contained"
144-
type="button"
145-
onClick={() => setAnnouncementModalOpen(true)}
146-
>
147-
Add New Announcement
148-
</Button>
175+
<Box sx={{ display: "flex", gap: 2, marginTop: 1 }}>
176+
<FormControl size="small" sx={{ minWidth: 150 }}>
177+
<InputLabel>Sort By</InputLabel>
178+
<Select
179+
value={sortBy}
180+
onChange={(e) => setSortBy(e.target.value)}
181+
label="Sort By"
182+
data-testid="sort-by-select"
183+
>
184+
<MenuItem value="created_at">Created Date</MenuItem>
185+
<MenuItem value="severity">Severity</MenuItem>
186+
<MenuItem value="title">Title</MenuItem>
187+
<MenuItem value="announcementId">ID</MenuItem>
188+
</Select>
189+
</FormControl>
190+
<FormControl size="small" sx={{ minWidth: 150 }}>
191+
<InputLabel>Order</InputLabel>
192+
<Select
193+
value={sortDirection}
194+
onChange={(e) => setSortDirection(e.target.value)}
195+
label="Order"
196+
>
197+
<MenuItem value="asc">Ascending</MenuItem>
198+
<MenuItem value="desc">Descending</MenuItem>
199+
</Select>
200+
</FormControl>
201+
<Button
202+
variant="contained"
203+
type="button"
204+
onClick={() => setAnnouncementModalOpen(true)}
205+
>
206+
Add New Announcement
207+
</Button>
208+
</Box>
149209
</Box>
150210

151211
<TableContainer component={Paper} elevation={3}>
152212
<Table aria-label="collapsible table">
153213
<TableHead>
154214
<TableRow>
155215
<TableCell />
156-
<TableCell align="left"> Announcement ID </TableCell>
216+
<TableCell align="left">Announcement ID</TableCell>
157217
<TableCell align="left">Announcement Title</TableCell>
158218
<TableCell align="left">Announcement Description</TableCell>
219+
<TableCell align="left">Created Date</TableCell>
220+
<TableCell align="left">Severity</TableCell>
159221
<TableCell align="left">Enabled</TableCell>
160222
<TableCell />
161223
</TableRow>
162224
</TableHead>
163225
<TableBody>
164-
{(announcementsData || [])
226+
{(sortedAnnouncements || [])
165227
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
166228
.map((row) => (
167229
<TableRow
@@ -196,14 +258,23 @@ const Announcements = () => {
196258
<TableCell align="left" component="th" scope="row">
197259
{row.description}
198260
</TableCell>
261+
<TableCell align="left" component="th" scope="row">
262+
{new Date(row.created_at).toLocaleDateString()}
263+
</TableCell>
264+
<TableCell align="left" component="th" scope="row">
265+
{row.severity}
266+
</TableCell>
199267
<TableCell>
200268
<FormControlLabel
201269
control={
202270
<Switch
203271
color="success"
204272
checked={row.is_enabled}
205273
onChange={(e) =>
206-
handleIsEnabled(row.announcementId, e.target.checked)
274+
handleIsEnabled(
275+
row.announcementId,
276+
e.target.checked
277+
)
207278
}
208279
/>
209280
}
@@ -282,6 +353,21 @@ const Announcements = () => {
282353
{announcementFormik.errors.description}
283354
</Typography>
284355
)}
356+
<FormControl fullWidth margin="normal">
357+
<InputLabel>Severity</InputLabel>
358+
<Select
359+
name="severity"
360+
value={announcementFormik.values.severity}
361+
onChange={announcementFormik.handleChange}
362+
label="Severity"
363+
>
364+
<MenuItem value="info">Info</MenuItem>
365+
<MenuItem value="warning">Warning</MenuItem>
366+
<MenuItem value="error">Error</MenuItem>
367+
<MenuItem value="success">Success</MenuItem>
368+
</Select>
369+
</FormControl>
370+
285371
<FormControlLabel
286372
control={
287373
<Switch
@@ -352,6 +438,20 @@ const Announcements = () => {
352438
{editFormik.errors.description}
353439
</Typography>
354440
)}
441+
<FormControl fullWidth margin="normal">
442+
<InputLabel>Severity</InputLabel>
443+
<Select
444+
name="severity"
445+
value={editFormik.values.severity}
446+
onChange={editFormik.handleChange}
447+
label="Severity"
448+
>
449+
<MenuItem value="info">Info</MenuItem>
450+
<MenuItem value="warning">Warning</MenuItem>
451+
<MenuItem value="error">Error</MenuItem>
452+
<MenuItem value="success">Success</MenuItem>
453+
</Select>
454+
</FormControl>
355455
<FormControlLabel
356456
control={
357457
<Switch
@@ -386,7 +486,7 @@ const Announcements = () => {
386486
<TablePagination
387487
rowsPerPageOptions={[10, 25, 100]}
388488
component="div"
389-
count={(announcementsData || []).length}
489+
count={sortedAnnouncements.length}
390490
rowsPerPage={rowsPerPage}
391491
page={page}
392492
onPageChange={(event, newPage) => setPage(newPage)}

client/src/hooks/useAnnouncements.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const useAnnouncements = () => {
1010
const fetchApi = async () => {
1111
setLoading(true);
1212
try {
13-
const announcements = await announcementService.getAllAnnouncements();
13+
const announcements = await announcementService.getAllAnnouncements();
1414
const processed = announcements
1515
.slice()
1616
.sort((a, b) => a.id - b.id)
@@ -19,6 +19,8 @@ export const useAnnouncements = () => {
1919
title: announcement.title,
2020
description: announcement.description,
2121
is_enabled: announcement.is_enabled,
22+
severity: announcement.severity,
23+
created_at: announcement.created_at,
2224
}));
2325
setData(processed);
2426
setLoading(false);
@@ -35,4 +37,4 @@ export const useAnnouncements = () => {
3537
}, [fetch]);
3638

3739
return { data, error, loading, refetch: fetch };
38-
};
40+
};

client/tests/announcements.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import mockRequests from "./helpers/mocks";
2+
import test from "./helpers/test";
3+
import { expect } from "@playwright/test";
4+
5+
test.describe("Announcements", () => {
6+
test("Should display announcements page with table", async ({
7+
page,
8+
helpers,
9+
}) => {
10+
await mockRequests(page, {
11+
announcements: [
12+
{
13+
id: 1,
14+
title: "Welcome to Food Oasis!",
15+
description: "This is a test announcement.",
16+
created_at: "2025-07-19T10:30:00.000Z",
17+
severity: "info",
18+
is_enabled: true,
19+
},
20+
{
21+
id: 2,
22+
title: "System Update",
23+
description: "We have updated our system.",
24+
created_at: "2025-07-18T15:45:00.000Z",
25+
severity: "warning",
26+
is_enabled: true,
27+
},
28+
],
29+
});
30+
31+
await helpers.mockLogin();
32+
await page.getByTestId("menu-button").click();
33+
await page.getByRole("link", { name: "Announcements" }).click();
34+
35+
await expect(
36+
page.getByRole("heading", { name: "Announcements" })
37+
).toBeVisible();
38+
await expect(page.getByRole("table")).toBeVisible();
39+
await expect(page.getByText("Welcome to Food Oasis!")).toBeVisible();
40+
await expect(page.getByText("System Update")).toBeVisible();
41+
});
42+
43+
test("Should sort by severity", async ({ page, helpers }) => {
44+
await mockRequests(page, {
45+
announcements: [
46+
{
47+
id: 1,
48+
title: "Info announcement",
49+
description: "This is an info announcement.",
50+
created_at: "2025-07-19T10:30:00.000Z",
51+
severity: "info",
52+
is_enabled: true,
53+
},
54+
{
55+
id: 2,
56+
title: "Warning announcement",
57+
description: "This is a warning announcement.",
58+
created_at: "2025-07-18T15:45:00.000Z",
59+
severity: "warning",
60+
is_enabled: true,
61+
},
62+
],
63+
});
64+
65+
await helpers.mockLogin();
66+
await page.getByTestId("menu-button").click();
67+
await page.getByRole("link", { name: "Announcements" }).click();
68+
69+
await page.getByTestId("sort-by-select").click();
70+
await page.getByRole("option", { name: "Severity" }).click();
71+
72+
await expect(page.getByTestId("sort-by-select")).toContainText("Severity");
73+
});
74+
75+
test("Should display severity values", async ({ page, helpers }) => {
76+
await mockRequests(page, {
77+
announcements: [
78+
{
79+
id: 1,
80+
title: "Info announcement",
81+
description: "This is an info announcement.",
82+
created_at: "2025-07-19T10:30:00.000Z",
83+
severity: "info",
84+
is_enabled: true,
85+
},
86+
{
87+
id: 2,
88+
title: "Warning announcement",
89+
description: "This is a warning announcement.",
90+
created_at: "2025-07-18T15:45:00.000Z",
91+
severity: "warning",
92+
is_enabled: true,
93+
},
94+
],
95+
});
96+
97+
await helpers.mockLogin();
98+
await page.getByTestId("menu-button").click();
99+
await page.getByRole("link", { name: "Announcements" }).click();
100+
101+
await expect(page.getByText("info")).toBeVisible();
102+
await expect(page.getByText("warning")).toBeVisible();
103+
});
104+
});

client/tests/helpers/mocks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,16 @@ const MOCKS = {
106106
title: "Welcome to Food Oasis!",
107107
message: "This is a test announcement.",
108108
date: "2025-07-19",
109+
created_at: "2025-07-19T10:30:00.000Z",
110+
severity: "info",
109111
},
110112
{
111113
id: 2,
112114
title: "System Update",
113115
message: "We have updated our system.",
114116
date: "2025-07-18",
117+
created_at: "2025-07-18T15:45:00.000Z",
118+
severity: "warning",
115119
},
116120
],
117121
"tags/1": [

0 commit comments

Comments
 (0)