diff --git a/client/actions/form.js b/client/actions/form.js index 5541cf7..00c82b5 100644 --- a/client/actions/form.js +++ b/client/actions/form.js @@ -86,12 +86,13 @@ export async function cancelAppointment(email) { revalidatePath("/my-appointments"); } -export async function updateVisitedOnServer(id) { - const response = await fetch(serverUrl + `appointments/${id}`, { +export async function updateStatusOnServer(id, newStatus) { + const response = await fetch(serverUrl + `appointments/${id}/status-change`, { method: "PATCH", headers: { "Content-Type": "application/json", }, + body: JSON.stringify({ newStatus }), }); const data = await response.json(); @@ -99,20 +100,6 @@ export async function updateVisitedOnServer(id) { if (!response.ok) { return { message: data.message }; } -} -// export async function updateStatusOnServer(address, status) { -// const response = await fetch(serverUrl + "appointments/status-change", { -// method: "PATCH", -// headers: { -// "Content-Type": "application/json", -// }, -// body: JSON.stringify({ address, status }), -// }); - -// const data = await response.json(); - -// if (!response.ok) { -// return { message: data.message }; -// } -// } + revalidatePath("/admin-dashboard"); +} diff --git a/client/app/admin-dashboard/page.jsx b/client/app/admin-dashboard/page.jsx index ac7b019..4a9d44a 100644 --- a/client/app/admin-dashboard/page.jsx +++ b/client/app/admin-dashboard/page.jsx @@ -6,14 +6,17 @@ import Spinner from "@/components/Spinner"; import { fetchAppointments } from "@/actions/form"; async function Dashboard() { - const appointments = await fetchAppointments(); + const initAppointments = await fetchAppointments(); + + console.log("dashboard", initAppointments); + return ( - + - + ); diff --git a/client/components/admin_dashboard/Grid.jsx b/client/components/admin_dashboard/Grid.jsx index 44d09be..0e5b054 100644 --- a/client/components/admin_dashboard/Grid.jsx +++ b/client/components/admin_dashboard/Grid.jsx @@ -7,42 +7,9 @@ import { GridToolbarContainer, } from "@mui/x-data-grid"; import { Paper, Box, Tooltip } from "@mui/material"; -import Button from "@mui/material/Button"; import SearchBar from "./SearchBar"; -import StatusChange from "./StatusChange"; -import { fetchAppointments, updateVisitedOnServer } from "@/actions/form"; - -const formatName = (name) => { - const [firstName, ...rest] = name.split(" "); - const lastName = rest.join(" "); - if (!lastName) { - return firstName; - } - return `${lastName}, ${firstName}`; -}; - -const formatTime = (hour) => { - return hour <= 12 ? `${hour}a` : `${hour - 12}p`; -}; - -const formatTimeRange = (timeRange) => { - return `${formatTime(timeRange?.preferredEarlyTime)} - ${formatTime( - timeRange?.preferredLateTime - )}`; -}; - -const formatDateCreated = (date) => { - return `${date.toLocaleDateString()} ${date.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })}`; -}; - -const formatPhone = (phone) => { - const cleaned = ("" + phone).replace(/\D/g, ""); - const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/); - return match ? `(${match[1]}) ${match[2]}-${match[3]}` : phone; -}; +import { getColumns } from "./grid-components/columns"; +import { updateStatusOnServer } from "@/actions/form"; const paginationModel = { page: 0, pageSize: 15 }; @@ -52,81 +19,21 @@ const Toolbar = () => ( ); -export default function Grid({ rows }) { +export default function Grid({ rows, refreshData }) { const [searchText, setSearchText] = useState(""); - const [customRows, setCustomRows] = useState(rows); - const columns = [ - { - field: "visitOrder", - headerName: "Visit Order", - width: 190, - valueGetter: (_, row) => row.schedule.order, - }, - { - field: "markVisited", - headerName: "Visited?", - width: 190, - renderCell: (params) => { - const { id, status } = params.row; - - return ( - - ); - }, - }, - { - field: "status", - headerName: "Status", - width: 190, - renderCell: (status) => , - }, - { - valueFormatter: formatName, - field: "name", - headerName: "Name", - width: 190, - }, - { - valueFormatter: formatDateCreated, - field: "dateCreated", - headerName: "Requested on", - width: 190, - }, - { - valueFormatter: formatTimeRange, - field: "timeRange", - headerName: "Timeslot", - width: 190, - }, - { - valueFormatter: formatPhone, - field: "phone", - headerName: "Phone", - width: 190, - }, - { field: "email", headerName: "Email", width: 190 }, - { - field: "address", - headerName: "Address", - width: 190, - }, - ]; - - const toggleVisited = async (id) => { - await updateVisitedOnServer(id); - const updatedRows = await fetchAppointments(); - setCustomRows(updatedRows); + const toggleVisited = async (id, status) => { + await updateStatusOnServer(id, status); + await refreshData(); }; + const columns = useMemo( + () => getColumns(refreshData, toggleVisited), + [refreshData] + ); + const filteredRows = useMemo(() => { - if (!searchText) return customRows; + if (!searchText) return rows; const needToFormat = { dateCreated: (value) => formatDateCreated(new Date(value)), @@ -135,7 +42,7 @@ export default function Grid({ rows }) { row.markVisited ? "Visited" : "Need to Visit", }; - return customRows.filter((row) => + return rows.filter((row) => columns.some((col) => { const value = row[col.field]; const formattedValue = needToFormat[col.field] @@ -147,7 +54,7 @@ export default function Grid({ rows }) { .includes(searchText.toLowerCase()); }) ); - }, [searchText, customRows]); + }, [searchText, rows]); const onSearchChange = (value) => { setSearchText(value); diff --git a/client/components/admin_dashboard/Map.jsx b/client/components/admin_dashboard/Map.jsx index c120e03..9672866 100644 --- a/client/components/admin_dashboard/Map.jsx +++ b/client/components/admin_dashboard/Map.jsx @@ -3,8 +3,8 @@ import { GoogleMap, LoadScript } from "@react-google-maps/api"; import { googleApiKey, appointmentsMapId } from "@/constants"; import React, { useRef } from "react"; -export default function Map({ appointments }) { - if (!appointments.length) { +export default function Map({ initAppointments }) { + if (!initAppointments.length) { return

No map data available

; } @@ -15,7 +15,7 @@ export default function Map({ appointments }) { height: "400px", }; - const startCoords = appointments.map((item) => ({ + const startCoords = initAppointments.map((item) => ({ location: item.location, visitOrder: item.schedule.order, customerName: item.name, @@ -40,7 +40,6 @@ export default function Map({ appointments }) { }); }); }; - console.log(appointments[0]); return ( { + const updatedAppts = await fetchAppointments(); + + setAppointments(updatedAppts); + }; + + console.log("reso table - grid", appointments); -export default function ReservationTable({ appointments }) { return ( - + ); } diff --git a/client/components/admin_dashboard/StatusChange.jsx b/client/components/admin_dashboard/grid-components/StatusChange.jsx similarity index 59% rename from client/components/admin_dashboard/StatusChange.jsx rename to client/components/admin_dashboard/grid-components/StatusChange.jsx index 3a05e66..2830e93 100644 --- a/client/components/admin_dashboard/StatusChange.jsx +++ b/client/components/admin_dashboard/grid-components/StatusChange.jsx @@ -1,11 +1,13 @@ +"use client"; import React from "react"; import { Box, InputLabel, MenuItem, FormControl, Select } from "@mui/material"; +import { updateStatusOnServer } from "@/actions/form"; -export default function StatusChange() { - const [status, setStatus] = React.useState("Requested"); - - const handleChange = (e) => { - setStatus(e.target.value); +export default function StatusChange({ id, refreshData, currentStatus }) { + const handleChange = async (e, id) => { + const newStatus = e.target.value; + await updateStatusOnServer(id, newStatus); + await refreshData(); }; return ( @@ -14,15 +16,15 @@ export default function StatusChange() { diff --git a/client/components/admin_dashboard/grid-components/VisitedChip.jsx b/client/components/admin_dashboard/grid-components/VisitedChip.jsx new file mode 100644 index 0000000..cc71689 --- /dev/null +++ b/client/components/admin_dashboard/grid-components/VisitedChip.jsx @@ -0,0 +1,17 @@ +import { Button } from "@mui/material"; + +const VisitedChip = ({ id, status, toggleVisited }) => { + const visited = status === "Visited" || status === "Completed"; + + return ( + + ); +}; + +export default VisitedChip; diff --git a/client/components/admin_dashboard/grid-components/columns.jsx b/client/components/admin_dashboard/grid-components/columns.jsx new file mode 100644 index 0000000..c98079d --- /dev/null +++ b/client/components/admin_dashboard/grid-components/columns.jsx @@ -0,0 +1,74 @@ +import VisitedChip from "./VisitedChip"; +import StatusChange from "./StatusChange"; +import { + formatName, + formatDateCreated, + formatTimeRange, + formatPhone, +} from "./formatters"; + +export const getColumns = (refreshData, toggleVisited) => [ + { + field: "visitOrder", + headerName: "Visit Order", + width: 190, + valueGetter: (_, row) => row.schedule.order, + }, + { + field: "markVisited", + headerName: "Visited?", + width: 190, + renderCell: (params) => ( + + ), + }, + { + field: "status", + headerName: "Status", + width: 190, + renderCell: (params) => { + const { id, status } = params.row; + return ( + + ); + }, + }, + { + valueFormatter: formatName, + field: "name", + headerName: "Name", + width: 190, + }, + { + valueFormatter: formatDateCreated, + field: "dateCreated", + headerName: "Requested on", + width: 190, + }, + { + valueFormatter: formatTimeRange, + field: "timeRange", + headerName: "Timeslot", + width: 190, + }, + { + valueFormatter: formatPhone, + field: "phone", + headerName: "Phone", + width: 190, + }, + { field: "email", headerName: "Email", width: 190 }, + { + field: "address", + headerName: "Address", + width: 190, + }, +]; diff --git a/client/components/admin_dashboard/grid-components/formatters.jsx b/client/components/admin_dashboard/grid-components/formatters.jsx new file mode 100644 index 0000000..67293e6 --- /dev/null +++ b/client/components/admin_dashboard/grid-components/formatters.jsx @@ -0,0 +1,31 @@ +export const formatName = (name) => { + const [firstName, ...rest] = name.split(" "); + const lastName = rest.join(" "); + if (!lastName) { + return firstName; + } + return `${lastName}, ${firstName}`; +}; + +export const formatTime = (hour) => { + return hour <= 12 ? `${hour}a` : `${hour - 12}p`; +}; + +export const formatTimeRange = (timeRange) => { + return `${formatTime(timeRange?.preferredEarlyTime)} - ${formatTime( + timeRange?.preferredLateTime + )}`; +}; + +export const formatDateCreated = (date) => { + return `${date.toLocaleDateString()} ${date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}`; +}; + +export const formatPhone = (phone) => { + const cleaned = ("" + phone).replace(/\D/g, ""); + const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/); + return match ? `(${match[1]}) ${match[2]}-${match[3]}` : phone; +}; diff --git a/server/src/controllers/appointments.controller.js b/server/src/controllers/appointments.controller.js index 7abe511..b50f81d 100644 --- a/server/src/controllers/appointments.controller.js +++ b/server/src/controllers/appointments.controller.js @@ -120,7 +120,10 @@ export async function getAllAppointments(req, res, next) { try { const appointments = await Appointment.find(); const withScheduling = appendSchedule( - appointments.map((a) => a.toObject()) + appointments.map((a) => ({ + ...a.toObject(), + id: a.id, + })) ); res.status(200).json(withScheduling); } catch (error) { @@ -188,22 +191,3 @@ export async function cancelAppointment(req, res, next) { return next({ message: "An internal server error occurred" }); } } - -export async function updateVisited(req, res) { - const { id } = req.params; - - try { - const data = await Appointment.findById(id); - console.log("data", data); - if (!data) { - return res.status(404).json({ message: "Could not grab data" }); - } - const newStatus = data.status === "Confirmed" ? "Visited" : "Confirmed"; - data.status = newStatus; - const visited = await data.save(); - console.log("server: visited", visited); - return res.status(200).json(visited); - } catch (error) { - return res.status(500).json({ message: "Server error: updating status" }); - } -} diff --git a/server/src/models/appointments.model.js b/server/src/models/appointments.model.js index 0691c2a..d3916b4 100644 --- a/server/src/models/appointments.model.js +++ b/server/src/models/appointments.model.js @@ -16,7 +16,7 @@ const appointmentSchema = new mongoose.Schema({ }, status: { type: String, - enum: ["Pending", "Confirmed", "Cancelled", "Visited"], + enum: ["Requested", "Confirmed", "Scheduled", "Completed", "Cancelled"], default: "Pending", }, notifications: { diff --git a/server/src/routes/appointments.routes.js b/server/src/routes/appointments.routes.js index 9743c89..57c5d99 100644 --- a/server/src/routes/appointments.routes.js +++ b/server/src/routes/appointments.routes.js @@ -4,9 +4,9 @@ import { getSingleAppointment, getUsersAppointments, newAppointment, - updateVisited, cancelAppointment, } from "../controllers/appointments.controller.js"; +import { updateStatus } from "../scheduling/scheduler.js"; const router = Router(); @@ -25,7 +25,7 @@ router.get("/:email/all", getUsersAppointments); // PATCH "/appointments/cancel" router.patch("/cancel", cancelAppointment); -// PATCH "/appointments/:id" -router.patch("/:id", updateVisited); +// PATCH "/appointments/:id/status-change" +router.patch("/:id/status-change", updateStatus); export default router; diff --git a/server/src/scheduling/scheduler.js b/server/src/scheduling/scheduler.js index 8ba9506..ba53071 100644 --- a/server/src/scheduling/scheduler.js +++ b/server/src/scheduling/scheduler.js @@ -1,3 +1,5 @@ +import Appointment from "../models/appointments.model.js"; + /** * Comparator function for 2 appointments. See the definition of array.sort() */ @@ -8,21 +10,47 @@ const appointmentComparator = (a, b) => a.dateCreated - b.dateCreated; * Doesn't change the order of the array. */ const appendSchedule = (appointments) => { - const comparator = (a, b) => appointmentComparator(a.appointment, b.appointment) - const sortedArray = appointments.map((appointment, index) => ({ appointment, index })).sort(comparator); - - const orderMap = new Map(); - sortedArray.forEach((item, index) => { - const visitOrder = index + 1; - orderMap.set(item.index, visitOrder); - }); - - return appointments.map((appointment, index) => ({ - ...appointment, - schedule: { - order: orderMap.get(index), - }, - })) + const comparator = (a, b) => + appointmentComparator(a.appointment, b.appointment); + const sortedArray = appointments + .map((appointment, index) => ({ appointment, index })) + .sort(comparator); + + const orderMap = new Map(); + sortedArray.forEach((item, index) => { + const visitOrder = index + 1; + orderMap.set(item.index, visitOrder); + }); + + return appointments.map((appointment, index) => ({ + ...appointment, + schedule: { + order: orderMap.get(index), + }, + })); +}; + +export async function updateStatus(req, res) { + const { id } = req.params; + const { newStatus } = req.body; + + try { + const data = await Appointment.findByIdAndUpdate( + { _id: id }, + { status: newStatus }, + { new: true } + ); + + if (!data) { + return res.status(404).json({ message: "Could not get data" }); + } + + console.log("data.status", data.status); + return res.status(200).json(data.status); + } catch (error) { + console.log(error); + return res.status(500).json({ message: "Server error: saving new status" }); + } } -export { appendSchedule } \ No newline at end of file +export { appendSchedule };