Skip to content

Commit

Permalink
Merge pull request #51 from chingu-voyages/feature/fetch-user-appt
Browse files Browse the repository at this point in the history
Fetch User's Appointment and Cancel Function
  • Loading branch information
tdkent authored Nov 25, 2024
2 parents b6dda22 + ed2f13f commit 3922dbf
Show file tree
Hide file tree
Showing 22 changed files with 541 additions and 191 deletions.
32 changes: 30 additions & 2 deletions client/actions/form.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"use server";

import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { serverUrl } from "@/constants";

// CREATE NEW APPT
export async function requestAppt(formValues) {
const response = await fetch(serverUrl + "form", {
const response = await fetch(serverUrl + "appointments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formValues),
Expand All @@ -15,9 +17,10 @@ export async function requestAppt(formValues) {
return { message: data.message };
}

redirect("/form/success");
redirect("/new-appointment/success");
}

// GET ALL APPTS
export async function fetchAppointments() {
const response = await fetch(serverUrl + "appointments", {
cache: "no-store",
Expand Down Expand Up @@ -75,3 +78,28 @@ export async function fetchAppointments() {
address: formatAddress(item.address),
}));
}

// GET SINGLE APPT
//! switch email to google id
export async function fetchSingleAppointment(email) {
const response = await fetch(serverUrl + `appointments/${email}`);
return response.json();
}

// UPDATE SINGLE APPT STATUS
//! switch email to google id
export async function cancelAppointment(email) {
const response = await fetch(serverUrl + "appointments/cancel", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
//! include token in request
body: JSON.stringify({ email }),
});
const data = await response.json();

if (!response.ok) {
return { message: data.message };
}

revalidatePath("/my-appointments");
}
36 changes: 36 additions & 0 deletions client/app/my-appointments/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Suspense } from "react";
import { Box, Stack, Typography } from "@mui/material";
import { getServerSession } from "next-auth";
import { authOptions } from "@/auth";
import MyAppointment from "@/components/appointment/MyAppointment";

export default async function MyAppointmentView() {
const session = await getServerSession(authOptions);

if (!session) {
return <p>You must be signed in to make an appointment</p>;
}

return (
<Box
sx={{
width: { xs: 1, sm: 4 / 5, md: 3 / 4, lg: 3 / 5 },
marginY: { xs: 2, lg: 4 },
marginX: "auto",
}}
>
<Stack direction="column">
<Typography
component="h1"
variant="h1"
sx={{ fontSize: { xs: "2rem", sm: "2.5rem", md: "3rem" } }}
>
My Appointments
</Typography>
<Suspense fallback={<p>Loading...</p>}>
<MyAppointment email={session.user.email} />
</Suspense>
</Stack>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function FormCancelView() {
<ListItem>
<Box
component={Link}
href="/form"
href="/new-appointment"
sx={(theme) => ({ ":hover": { color: theme.palette.branding } })}
>
<Stack direction="row" gap={2} alignItems="center">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import Form from "@/components/form/Form";
import { getServerSession } from "next-auth";
import { authOptions } from "@/auth";
import Form from "@/components/form/Form";

export default async function FormView() {
const session = await getServerSession(authOptions)
const session = await getServerSession(authOptions);
if (!session) {
return <p>You must be signed in to make an appointment</p>;
}
return (
<div>
<h1>Form View</h1>
<Form />
<Form email={session.user.email} />
</div>
);
}
File renamed without changes.
94 changes: 94 additions & 0 deletions client/components/appointment/AppointmentDetails.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
Card,
CardContent,
Divider,
List,
Stack,
Typography,
} from "@mui/material";
import AppointmentListItem from "./AppointmentListItem";
import CancelAppointment from "./CancelAppointment";

function convertHourTo12HourTime(hour) {
const period = hour >= 12 ? "PM" : "AM";
const formattedHour = hour % 12 || 12;
return `${formattedHour}:00 ${period}`;
}

export default function AppointmentDetails({ formData }) {
const {
name,
email,
phone,
address,
status,
date,
timeRange: { earlyTimeHour, lateTimeHour },
} = formData;
const earlyTime = convertHourTo12HourTime(earlyTimeHour);
const lateTime = convertHourTo12HourTime(lateTimeHour);
const preferredTime = `${earlyTime} - ${lateTime}`;
const showCancelBtn = status === "Pending" || status === "Confirmed";

return (
<Card sx={{ mt: 4, padding: 2 }}>
<CardContent>
<Stack gap={2}>
<Typography
component="h2"
variant="h1"
sx={{ fontSize: { xs: "1.2rem", lg: "1.5rem" } }}
>
Appointment Details
</Typography>
<Divider orientation="horizontal" />
</Stack>
<List sx={{ marginY: 2 }}>
<Stack gap={4}>
<Stack gap={1}>
<Typography
variant="subtitle2"
sx={{ fontSize: { xs: "0.9rem", lg: "1rem" } }}
>
Contact Info
</Typography>
<Stack gap={1 / 2}>
<AppointmentListItem label="Name" value={name} />
<AppointmentListItem label="Email" value={email} />
<AppointmentListItem label="Phone" value={phone} />
</Stack>
</Stack>
<Stack gap={1}>
<Typography
variant="subtitle2"
sx={{ fontSize: { xs: "0.9rem", lg: "1rem" } }}
>
Address
</Typography>
<Stack gap={1 / 2}>
<AppointmentListItem value={address} />
</Stack>
</Stack>
<Stack gap={1}>
<Typography
variant="subtitle2"
sx={{ fontSize: { xs: "0.9rem", lg: "1rem" } }}
>
Scheduling
</Typography>
<Stack gap={1 / 2}>
<AppointmentListItem label="Status" value={status} />
<AppointmentListItem label="Date" value={date || "N/A"} />
<AppointmentListItem
label="Preferred Time"
value={preferredTime}
/>
</Stack>
</Stack>
</Stack>
</List>
</CardContent>
{showCancelBtn && <CancelAppointment email={email} />}
</Card>
);
}
11 changes: 11 additions & 0 deletions client/components/appointment/AppointmentListItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ListItem, Typography } from "@mui/material";

export default function ({ label, value }) {
return (
<ListItem disablePadding>
<Typography sx={{ fontSize: { xs: "0.9rem", lg: "1rem" } }}>
{label ? `${label}:` : ""} {value}
</Typography>
</ListItem>
);
}
86 changes: 86 additions & 0 deletions client/components/appointment/CancelAppointment.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client";
import { useState } from "react";
import { Box, Button, Modal, Stack, Typography } from "@mui/material";
import { cancelAppointment } from "@/actions/form";
import ErrorToast from "../errors/ErrorToast";

export default function CancelAppointment({ email }) {
// state
const [modal, setModal] = useState(false);
const [toast, setToast] = useState(false);
const [toastMsg, setToastMsg] = useState("");

const handleConfirm = async () => {
const err = await cancelAppointment(email);
if (err) {
setToast(true);
setToastMsg(err.message);
}
setModal(false);
};

return (
<>
{/* ERROR TOAST */}
<ErrorToast
toast={toast}
setToast={setToast}
toastMsg={toastMsg}
setToastMsg={setToastMsg}
/>

{/* CONFIRMATION MODAL */}
<Modal open={modal} onClose={() => setModal(false)}>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: { xs: 9 / 10, sm: 500 },
bgcolor: "background.paper",
boxShadow: 10,
borderRadius: 2,
p: 4,
}}
>
<Typography>
Are you sure you want to cancel your appointment?
</Typography>
<Stack
gap={2}
direction={{ xs: "column", sm: "row" }}
sx={{ marginTop: 4 }}
>
<Button
variant="outlined"
color="warning"
sx={{ width: { xs: 1, sm: 1 / 2 } }}
onClick={() => setModal(false)}
>
Cancel
</Button>
<Button
variant="contained"
sx={{ width: { xs: 1, sm: 1 / 2 } }}
onClick={handleConfirm}
>
Confirm
</Button>
</Stack>
</Box>
</Modal>

{/* CANCEL BUTTON */}
<Box>
<Button
variant="outlined"
color="warning"
onClick={() => setModal(true)}
>
Cancel Appointment
</Button>
</Box>
</>
);
}
11 changes: 11 additions & 0 deletions client/components/appointment/MyAppointment.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { fetchSingleAppointment } from "@/actions/form";
import NoAppointment from "./NoAppointment";
import AppointmentDetails from "./AppointmentDetails";

export default async function MyAppointment({ email }) {
const fetchResponse = await fetchSingleAppointment(email);
if (!fetchResponse) {
return <NoAppointment />;
}
return <AppointmentDetails formData={fetchResponse} />;
}
14 changes: 14 additions & 0 deletions client/components/appointment/NewAppointment.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";
import { redirect } from "next/navigation";
import { Button } from "@mui/material";

export default function NewAppointment() {
function handleClick() {
redirect("/new-appointment");
}
return (
<Button onClick={handleClick} sx={{ width: "fit-content" }}>
+ New Appointment
</Button>
);
}
13 changes: 13 additions & 0 deletions client/components/appointment/NoAppointment.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Box, Stack, Typography } from "@mui/material";
import NewAppointment from "./NewAppointment";

export default function NoAppointment() {
return (
<Stack gap={4} sx={{ marginY: { xs: 4, lg: 6 } }}>
<Typography component="p" variant="body1" fontStyle="italic">
You haven&apos;t scheduled any appointments.
</Typography>
<NewAppointment />
</Stack>
);
}
26 changes: 26 additions & 0 deletions client/components/errors/ErrorToast.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Alert, Snackbar } from "@mui/material";

export default function ErrorToast({ toast, toastMsg, setToast, setToastMsg }) {
const handleToastClose = () => {
setToast(false);
setToastMsg("");
};

return (
<Snackbar
open={toast}
autoHideDuration={6000}
onClose={handleToastClose}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
>
<Alert
onClose={handleToastClose}
severity="error"
variant="filled"
sx={{ width: "100%" }}
>
{toastMsg}
</Alert>
</Snackbar>
);
}
Loading

0 comments on commit 3922dbf

Please sign in to comment.