diff --git a/client/src/App.jsx b/client/src/App.jsx
index ccda6c8..2fffdea 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -10,6 +10,7 @@ import { Admin } from "./components/admin/Admin";
import { CatchAll } from "./components/CatchAll";
import { Dashboard } from "./components/dashboard/Dashboard";
import { Playground } from "./components/playground/Playground";
+import { Notifications } from "./components/notifications/Notifications";
import { Login } from "./components/login/Login";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { Signup } from "./components/signup/Signup";
@@ -40,6 +41,10 @@ const App = () => {
path="/playground"
element={}
/>
+ } />}
+ />
} />}
diff --git a/client/src/components/notifications/CalendarIcon.jsx b/client/src/components/notifications/CalendarIcon.jsx
new file mode 100644
index 0000000..de23f33
--- /dev/null
+++ b/client/src/components/notifications/CalendarIcon.jsx
@@ -0,0 +1,14 @@
+export const CalendarIcon = () => (
+
+);
diff --git a/client/src/components/notifications/Counter.jsx b/client/src/components/notifications/Counter.jsx
new file mode 100644
index 0000000..1614d61
--- /dev/null
+++ b/client/src/components/notifications/Counter.jsx
@@ -0,0 +1,29 @@
+export const CounterComponent = ({ count }) => {
+ return (
+
+ );
+};
diff --git a/client/src/components/notifications/FilterButton.jsx b/client/src/components/notifications/FilterButton.jsx
new file mode 100644
index 0000000..e846d63
--- /dev/null
+++ b/client/src/components/notifications/FilterButton.jsx
@@ -0,0 +1,141 @@
+import { useEffect, useState } from "react";
+import {
+ Button,
+ HStack,
+ Popover,
+ PopoverBody,
+ PopoverCloseButton,
+ PopoverContent,
+ PopoverTrigger,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
+
+import { CalendarIcon } from "./CalendarIcon";
+import styles from "./FilterButton.module.css";
+import { FilterIcon } from "./FilterIcon";
+
+export const FilterButton = ({ setFilterType, currentFilter }) => {
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+ const toast = useToast();
+ const [type, setType] = useState("all");
+
+ // Watch for changes in both dates
+ useEffect(() => {
+ console.log(startDate, endDate);
+ if (startDate.substring(0,1) === "2" && endDate.substring(0,1) === "2") { // checking if the date is filled out completely
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ console.log(start, end);
+ if (start > end) {
+ toast({
+ title: "Invalid Date Range",
+ description: "Start date must be before end date",
+ status: "error",
+ duration: 3000,
+ isClosable: true,
+ });
+ return;
+ }
+
+ // Update filter with new dates
+ setFilterType(prev => ({
+ ...prev,
+ type: type,
+ startDate: startDate, // The date input already returns YYYY-MM-DD format
+ endDate: endDate
+ }));
+ }
+ }, [startDate, endDate, type, toast]);
+
+ const handleFilterSelect = (type) => {
+ setType(type);
+ setFilterType(prev => ({
+ ...prev,
+ type,
+ }));
+ };
+
+ return (
+
+
+ }
+ display="inline-flex"
+ height="54.795px"
+ padding="8px 16px"
+ justifyContent="center"
+ alignItems="center"
+ gap="4px"
+ flexShrink={0}
+ borderRadius={15}
+ border="1px solid var(--medium-light-grey, #D2D2D2)"
+ background="var(--white, #FFF)"
+ color="var(--medium-grey, #767778)"
+ fontFamily="Inter, sans-serif"
+ fontSize="16px"
+ fontStyle="normal"
+ fontWeight={400}
+ lineHeight="normal"
+ >
+ Filters
+
+
+
+
+
+
+
+
+ Date Range
+
+
+
+ setStartDate(e.target.value)}
+ className={styles.dateInput}
+ />
+ to
+ setEndDate(e.target.value)}
+ className={styles.dateInput}
+ />
+
+
+ Type
+
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/client/src/components/notifications/FilterButton.module.css b/client/src/components/notifications/FilterButton.module.css
new file mode 100644
index 0000000..7e50799
--- /dev/null
+++ b/client/src/components/notifications/FilterButton.module.css
@@ -0,0 +1,149 @@
+/* FilterButton.module.css */
+.modalContent {
+ display: inline-flex;
+ padding: 16px;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ border-radius: 15px;
+ border: 1px solid var(--medium-light-grey, #D2D2D2);
+ background: var(--white, #FFF);
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
+ width: 200px;
+}
+
+.dateRangeHeader {
+ color: var(--medium-grey, #767778);
+ font-family: "Inter", sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ margin-bottom: 16px;
+
+}
+
+.modalBody {
+ padding: 0;
+}
+
+.dateInputContainer {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 16px;
+ width: 280px;
+}
+
+.dateInput {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ border-radius: 5px;
+ border: 1px solid var(--medium-light-grey, #d2d2d2);
+ background-color: #F6F6F6;
+ padding: 8px;
+ font-family: "Inter", sans-serif;
+ font-size: 13px;
+ color: #767778;
+ width: 110px;
+ cursor: pointer;
+ overflow: hidden;
+}
+
+/* Make the calendar icon transparent but keep it clickable */
+.dateInput::-webkit-calendar-picker-indicator {
+ opacity: 0;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ margin: 0;
+ padding: 0;
+ visibility: hidden;
+}
+
+
+/* Remove default styling */
+.dateInput::-webkit-datetime-edit {
+ padding: 0;
+}
+
+.dateInput::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+}
+
+.dateInput::-webkit-datetime-edit-text,
+.dateInput::-webkit-datetime-edit-month-field,
+.dateInput::-webkit-datetime-edit-day-field,
+.dateInput::-webkit-datetime-edit-year-field {
+ color: #767778;
+}
+
+/* When no date is selected (placeholder state) */
+.dateInput:invalid {
+ color: #767778;
+}
+
+
+.dateInput::placeholder{
+ color: var(--medium-grey, #767778);
+ font-family: "Inter", sans-serif;
+ font-size: 13px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px; /* 125% */
+}
+
+.toText {
+ color: var(--medium-grey, #767778);
+ font-family: "Inter", sans-serif;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+}
+
+.typeHeader {
+ color: var(--medium-grey, #767778);
+ font-family: "Inter", sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ margin-bottom: 16px;
+}
+
+.filterButtonGroup {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ align-self: stretch;
+}
+
+.filterButton {
+ border: 1px solid #d2d2d2 !important;
+ border-radius: 15px !important;
+ padding: 0px 15px !important;
+ background: white !important;
+ color: #767778 !important;
+ transition: all 0.2s !important;
+ color: var(--medium-grey, #767778);
+ font-family: "Inter", sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+}
+
+.filterButton:hover {
+ background: #f5f5f5 !important;
+}
+
+.filterButton.active {
+ color: var(--indigo, #4E4AE7) !important;
+ border-radius: 30px !important;
+ border: 1px solid var(--indigo, #4E4AE7) !important;
+ background: var(--light-indigo, #EDEDFD) !important;
+}
diff --git a/client/src/components/notifications/FilterIcon.jsx b/client/src/components/notifications/FilterIcon.jsx
new file mode 100644
index 0000000..02e1a0b
--- /dev/null
+++ b/client/src/components/notifications/FilterIcon.jsx
@@ -0,0 +1,18 @@
+export const FilterIcon = () => (
+
+ ); /** simplified syntax */
\ No newline at end of file
diff --git a/client/src/components/notifications/Notifications.jsx b/client/src/components/notifications/Notifications.jsx
new file mode 100644
index 0000000..9c16c25
--- /dev/null
+++ b/client/src/components/notifications/Notifications.jsx
@@ -0,0 +1,121 @@
+import { useEffect, useState } from "react";
+
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
+import Navbar from "../Navbar";
+import NotificationsComponents from "./NotificationsComponents";
+import { CounterComponent } from "./Counter";
+import { FilterButton } from "./FilterButton";
+import styles from "./Notifications.module.css";
+import { formatDistanceToNow } from 'date-fns';
+
+export const Notifications = () => {
+ const { backend } = useBackendContext();
+
+ const [notifications, setNotifications] = useState([]);
+ const [filterType, setFilterType] = useState({
+ type: "all",
+ startDate: null,
+ endDate: null,
+ }); // all, overdue, neardue
+
+ useEffect(() => {
+ const fetchNotifs = async () => {
+ try {
+ const today = new Date();
+ let endpoints = [];
+ let queryParams = "";
+
+ // If we have dates, add them as query params regardless of filter type
+ if (filterType.startDate && filterType.endDate) {
+ queryParams = `?startDate=${filterType.startDate}&endDate=${filterType.endDate}`;
+ }
+
+ // Only modify the endpoint if we're filtering by type AND it's not "all"
+ if (filterType.type === "all") {
+ endpoints = [`/invoices/overdue${queryParams}`, `/invoices/neardue${queryParams}`];
+ } else {
+ endpoints = [`/invoices/${filterType.type}${queryParams}`];
+ }
+
+ const responses = await Promise.all(
+ endpoints.map(endpoint => backend.get(endpoint))
+ );
+
+ const notifsData = responses.flatMap(res => res.data);
+
+ // Fetch additional data for each invoice (total, paid, event name)
+ const enrichedInvoices = await Promise.all(
+ notifsData.map(async (invoice) => {
+ try {
+ const [totalRes, paidRes, eventRes] = await Promise.all([
+ backend.get(`/invoices/total/${invoice.id}`),
+ backend.get(`/invoices/paid/${invoice.id}`),
+ backend.get(`/events/${invoice.eventId}`),
+ ]);
+
+ const endDate = new Date(invoice.endDate);
+ const dueTime = formatDistanceToNow(endDate, { addSuffix: true });
+ let payStatus = "";
+ if (endDate < today) {
+ payStatus = "overdue";
+ } else {
+ payStatus = "due in one week";
+ }
+
+ return {
+ ...invoice,
+ eventName: eventRes.data[0]?.name || "Unknown Event",
+ total: totalRes.data.total,
+ paid: paidRes.data.paid,
+ payStatus,
+ dueTime
+ };
+ } catch (err) {
+ console.error(`Failed to fetch additional data for invoice ID: ${invoice.id}`, err);
+ return {
+ ...invoice,
+ eventName: "Unknown Event",
+ total: 0,
+ paid: 0,
+ payStatus: "Unknown",
+ dueTime: "Unknown"
+ };
+ }
+ })
+ );
+
+ setNotifications(enrichedInvoices);
+ } catch (err) {
+ console.error("Failed to fetch invoices", err);
+ }
+ };
+ fetchNotifs();
+ }, [filterType, backend]);
+
+
+ return (
+
+
+
+
+
Invoice Notifications
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/components/notifications/Notifications.module.css b/client/src/components/notifications/Notifications.module.css
new file mode 100644
index 0000000..8fc7c1d
--- /dev/null
+++ b/client/src/components/notifications/Notifications.module.css
@@ -0,0 +1,15 @@
+.title {
+ color: #4E4AE7;
+ font-family: "Inter", sans-serif;
+ font-size: 30px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ margin-right: 31px; /** a bit arbitrary */
+}
+
+.titleContainer {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 30px;
+}
\ No newline at end of file
diff --git a/client/src/components/notifications/NotificationsComponents.jsx b/client/src/components/notifications/NotificationsComponents.jsx
new file mode 100644
index 0000000..b47d8ed
--- /dev/null
+++ b/client/src/components/notifications/NotificationsComponents.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import {
+ Box,
+ Flex,
+ Text
+} from '@chakra-ui/react';
+import { format } from "date-fns";
+import { MdEmail } from 'react-icons/md';
+
+
+const NotificationsComponents = ({ notifications }) => {
+ return (
+
+ {notifications.map((item, index) => (
+
+
+
+
+
+
+
+ {item.eventName} is {item.payStatus}
+
+ {item.eventName} | Due: {format(new Date(item.endDate), "MMM d")} | Total: ${item.total} | Paid: ${item.paid}
+
+
+
+ {item.dueTime}
+
+ ))}
+
+ );
+};
+
+export default NotificationsComponents;
\ No newline at end of file
diff --git a/server/routes/invoices.js b/server/routes/invoices.js
index d06b57f..fc84db8 100644
--- a/server/routes/invoices.js
+++ b/server/routes/invoices.js
@@ -8,11 +8,90 @@ invoicesRouter.use(express.json());
// Get all invoices
invoicesRouter.get("/", async (req, res) => {
try {
- const users = await db.query(`SELECT * FROM invoices`);
+ const { startDate, endDate } = req.query;
+ console.log("Query params:", startDate, endDate);
+
+ if (startDate && endDate) {
+ const invoices = await db.any(
+ `SELECT * FROM invoices WHERE start_date >= $1::date AND end_date <= $2::date`,
+ [startDate, endDate]
+ );
+ res.status(200).json(keysToCamel(invoices));
+ } else {
+ const invoices = await db.any(`SELECT * FROM invoices ORDER BY id ASC`);
+
+ if (invoices.length === 0) {
+ return res.status(404).json({ error: "No invoices found." });
+ }
+
+ res.status(200).json(keysToCamel(invoices));
+ }
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
+// Get all overdue invoices with optional date range filtering
+invoicesRouter.get("/overdue", async (req, res) => {
+ try {
+ const { startDate, endDate } = req.query;
+
+ // Base query for overdue invoices
+ let query = `
+ SELECT * FROM invoices
+ WHERE is_sent = false
+ AND payment_status IN ('partial', 'none')
+ AND end_date < CURRENT_DATE`;
+
+ const params = [];
+
+ // Add date range filtering if both dates are provided
+ if (startDate && endDate) {
+ query = `
+ SELECT * FROM invoices
+ WHERE is_sent = false
+ AND payment_status IN ('partial', 'none')
+ AND end_date < CURRENT_DATE
+ AND start_date >= $1
+ AND end_date <= $2`;
+ params.push(startDate, endDate);
+ }
- res.status(200).json(keysToCamel(users));
+ const overdueInvoices = await db.query(query, params);
+ res.status(200).json(keysToCamel(overdueInvoices));
} catch (err) {
- res.status(400).send(err.message);
+ res.status(500).send(err.message);
+ }
+});
+
+// Get all invoices due within the next week with optional date range filtering
+invoicesRouter.get("/neardue", async (req, res) => {
+ try {
+ const { startDate, endDate } = req.query;
+
+ // Base query for neardue invoices
+ let query = `
+ SELECT * FROM invoices
+ WHERE is_sent = false
+ AND end_date BETWEEN CURRENT_DATE AND (CURRENT_DATE + INTERVAL '7 days')`;
+
+ const params = [];
+
+ // Add date range filtering if both dates are provided
+ if (startDate && endDate) {
+ query = `
+ SELECT * FROM invoices
+ WHERE is_sent = false
+ AND end_date BETWEEN CURRENT_DATE AND (CURRENT_DATE + INTERVAL '7 days')
+ AND start_date >= $1
+ AND end_date <= $2`;
+ params.push(startDate, endDate);
+ }
+
+ const nearDueInvoices = await db.query(query, params);
+ res.status(200).json(keysToCamel(nearDueInvoices));
+ } catch (err) {
+ res.status(500).send(err.message);
}
});