diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index d0fea8f..53a3355 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -31,11 +31,4 @@ jobs:
run: npm run lint
- name: Build
- run: npm run build
-
- - name: Deploy to GH Pages
- uses: peaceiris/actions-gh-pages@v3
- if: ${{ github.ref == 'refs/heads/main' }}
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: build
+ run: npm run build
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 1550cf1..872d977 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import PickupPage from "./pages/PickupPage";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import DonorsPage from "./pages/DonorsPage";
+import MessagePage from "./pages/MessagingPage";
const router = createBrowserRouter([
{
@@ -31,6 +32,10 @@ const router = createBrowserRouter([
path: "donors",
element: ,
},
+ {
+ path: "messaging",
+ element: ,
+ },
{
path: "*",
element: ,
diff --git a/src/components/common/ConfirmMessageButton.tsx b/src/components/common/ConfirmMessageButton.tsx
new file mode 100644
index 0000000..3e8e718
--- /dev/null
+++ b/src/components/common/ConfirmMessageButton.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+import IconButton from "@mui/material/IconButton";
+import SendIcon from "@mui/icons-material/Send";
+import Dialog from "@mui/material/Dialog";
+import DialogTitle from "@mui/material/DialogTitle";
+import DialogActions from "@mui/material/DialogActions";
+import Button from "@mui/material/Button";
+import { useTheme } from "@mui/material";
+import { Recipient, recipientDescriptions } from "../messaging/Messaging";
+
+interface ConfirmMessageButtonProps {
+ recipients: Recipient;
+ message: string;
+ setMessage(string): void;
+ setOpenSnackbar(boolean): void;
+ setSnackbarMessage(string): void;
+}
+
+export const ConfirmMessageButton = ({ recipients, message, setMessage, setOpenSnackbar, setSnackbarMessage }: ConfirmMessageButtonProps) => {
+ const [open, setOpen] = React.useState(false);
+ const theme = useTheme();
+
+ const handleCloseDialog = () => {
+ setOpen(false);
+ };
+
+ const sendMessage = async (message: string, recipients: Recipient) => {
+ try {
+ console.log(message);
+ console.log(recipients);
+ setSnackbarMessage("Message successfully sent");
+ setMessage("");
+ } catch {
+ setSnackbarMessage("Failed to send message");
+ }
+ setOpenSnackbar(true);
+ handleCloseDialog();
+ };
+
+ return (
+ <>
+
+ }
+ sx={{
+ backgroundColor: theme.palette.success.main,
+ my: 1
+ }}
+ onClick={() => setOpen(true)}
+ >Send
+ >
+ );
+};
diff --git a/src/components/dashboard/listItems.tsx b/src/components/dashboard/listItems.tsx
index 7e79154..42235cb 100644
--- a/src/components/dashboard/listItems.tsx
+++ b/src/components/dashboard/listItems.tsx
@@ -4,6 +4,7 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import DashboardIcon from "@mui/icons-material/Dashboard";
import PeopleIcon from "@mui/icons-material/People";
+import MessageIcon from "@mui/icons-material/Message";
import AirlineStopsIcon from "@mui/icons-material/AirlineStops";
import ShoppingBagIcon from "@mui/icons-material/ShoppingBag";
import { NavigateFunction } from "react-router-dom";
@@ -35,6 +36,12 @@ export const listItems = ({ navigate }: { navigate: NavigateFunction }) => {
+ { navigate("/messaging"); }}>
+
+
+
+
+
);
};
\ No newline at end of file
diff --git a/src/components/messaging/Messaging.tsx b/src/components/messaging/Messaging.tsx
new file mode 100644
index 0000000..5330fe1
--- /dev/null
+++ b/src/components/messaging/Messaging.tsx
@@ -0,0 +1,90 @@
+import React, { useState } from "react";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import TextField from "@mui/material/TextField";
+import DeleteIcon from "@mui/icons-material/Delete";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import MenuItem from "@mui/material/MenuItem";
+import Select from "@mui/material/Select";
+import { useSnackbar } from "../../hooks/useSnackbar";
+import { ConfirmMessageButton } from "../common/ConfirmMessageButton";
+import { getAuth } from "firebase/auth";
+import useGetAdmins from "../../hooks/useGetAdmins";
+
+export enum Recipient {
+ All = "All",
+ PickedUp = "PickedUp",
+ NotPickedUp = "NotPickedUp"
+}
+
+export const recipientDescriptions = {
+ "All": "All Donors",
+ "PickedUp": "Donors who have picked up",
+ "NotPickedUp": "Donors who have not picked up"
+};
+
+export default function Messaging() {
+ const theme = useTheme();
+ const auth = getAuth();
+ const { data: admins, isLoading: loadingAdmins } = useGetAdmins();
+
+ const { setOpenSnackbar, setSnackbarMessage, snackbar } = useSnackbar();
+
+ const [message, setMessage] = useState("");
+ const [recipients, setRecipients] = useState(Recipient.All);
+
+ return (
+ <>
+
+
+ Messaging
+ {
+ !loadingAdmins && admins?.map((admin) => admin.email).indexOf(auth.currentUser?.email ?? "") !== -1 ? (
+ <>
+
+ Select Recipients:
+
+
+ setMessage(e.target.value)}
+ />
+ }
+ sx={{
+ backgroundColor: theme.palette.error.main,
+ my: 1,
+ mx: 1
+ }}
+ onClick={() => setMessage("")}
+ >Clear
+ >
+ ) : Only site admins can use this feature
+ }
+ {snackbar}
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/hooks/useGetOrders.tsx b/src/hooks/useGetOrders.tsx
index e3e742c..d3fa42c 100644
--- a/src/hooks/useGetOrders.tsx
+++ b/src/hooks/useGetOrders.tsx
@@ -1,15 +1,17 @@
import { db } from "../config";
-import { collection, getDocs, query } from "firebase/firestore";
+import { collection, getDocs, limit, query } from "firebase/firestore";
import { useQuery } from "@tanstack/react-query";
import { ORDERS_COLLECTION } from "../constants";
import { Order } from "../types/Order";
+const ORDER_LIMIT = 3000;
+
const useGetOrders = () => {
return useQuery({
queryKey: [ORDERS_COLLECTION],
queryFn: async () => {
const clientsRef = collection(db, ORDERS_COLLECTION);
- const snapshot = await getDocs(query(clientsRef));
+ const snapshot = await getDocs(query(clientsRef, limit(ORDER_LIMIT)));
const orderObjects = snapshot.docs.map((doc) => {
const data = doc.data();
return {
diff --git a/src/pages/MessagingPage.tsx b/src/pages/MessagingPage.tsx
new file mode 100644
index 0000000..32b9011
--- /dev/null
+++ b/src/pages/MessagingPage.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { Dashboard } from "../components/dashboard/Dashboard";
+import Messaging from "../components/messaging/Messaging";
+
+export default function MessagingPage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/utils/csvUtils.tsx b/src/utils/csvUtils.tsx
index 67c8b2b..2da3f51 100644
--- a/src/utils/csvUtils.tsx
+++ b/src/utils/csvUtils.tsx
@@ -18,25 +18,28 @@ export const getCSVDataAndUpload = async (csv: string, orders: Order[], mutation
return "Please submit non-empty file.";
}
- const records = parse(csv,
- {
- skip_empty_lines: true,
- columns: true,
- bom: true,
- skip_records_with_empty_values: true
- }
- );
+ try {
+ const headers = csv.split("\n")[0];
+ assertCsvHeaders(headers);
- const newOrders = records.map(csvToOrder);
+ const records = parse(csv,
+ {
+ skip_empty_lines: true,
+ columns: true,
+ bom: true,
+ skip_records_with_empty_values: true
+ }
+ );
- const orderIds = new Set(orders.map((order) => order.id));
- const filteredOrders = newOrders.filter((order) => !orderIds.has(order.id));
+ const newOrders = records.map(csvToOrder);
+
+ const orderIds = new Set(orders.map((order) => order.id));
+ const filteredOrders = newOrders.filter((order) => !orderIds.has(order.id));
- try {
await mutationHook.mutateAsync(filteredOrders);
return "Successfully uploaded CSV rows";
- } catch {
- return "Could not upload CSV rows";
+ } catch (error) {
+ return error.message;
}
};
@@ -65,11 +68,7 @@ const csvToOrder = (data: any): Order => {
};
};
-const stripReturn = (word: string) => {
- return word.replace("\r", "");
-};
-
-const assertCsvHeaders = (line: string): boolean => {
+const assertCsvHeaders = (line: string): void => {
const expectedHeaders = [
"First Name", "Last Name", "Cell Phone",
"Home Phone", "E-mail", "Customer Comments",
@@ -77,13 +76,12 @@ const assertCsvHeaders = (line: string): boolean => {
];
const headers = line.split(",");
+ console.log(headers);
- if (!headers) { return false; }
- if (headers.length != expectedHeaders.length) { return false; }
+ if (!headers) { throw new Error("Could not find CSV headers"); }
+ if (headers.length != expectedHeaders.length) { throw new Error(`Incorrect number of headers. Found ${headers.length}. Expected ${expectedHeaders.length}`); }
for (let i = 0; i < expectedHeaders.length; i++) {
- if (!headers[i].startsWith(expectedHeaders[i])) { return false; }
+ if (!headers[i].trim().startsWith(expectedHeaders[i])) { throw new Error(`Invalid header: "${headers[i]}"`); }
}
-
- return true;
};
\ No newline at end of file