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 ( + <> + + Are you sure you want to send message to {recipientDescriptions[recipients]}? + + + + + + + + ); +}; 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)} + /> + + + ) : 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