From fda14f2259fc6ee5d0aa161ff309408cbc2041c4 Mon Sep 17 00:00:00 2001 From: mseok Date: Sun, 10 Nov 2024 02:55:48 -0600 Subject: [PATCH 1/2] Finished edit functionality for accompanying passengers --- .eslintrc.cjs | 1 + src/api/queries.ts | 57 +++- .../PassengerDetailsModal.module.css | 24 ++ .../PassengerDetailsModal.tsx | 281 +++++++++++++++--- 4 files changed, 311 insertions(+), 52 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d42ef58..6dfe245 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,6 +21,7 @@ module.exports = { }, plugins: ["@typescript-eslint", "react", "autofix"], rules: { + "import/no-named-as-default-member": "off", "react/react-in-jsx-scope": "off", "react/jsx-uses-react": "off", "arrow-body-style": "off", diff --git a/src/api/queries.ts b/src/api/queries.ts index 0662110..81ac9c7 100644 --- a/src/api/queries.ts +++ b/src/api/queries.ts @@ -109,7 +109,7 @@ export const getUserByAirtableRecordId = ( }) .then((res) => res.data); -export const updatePassenger = ( +export const updatePassenger = async ( passenger: { Street: string; Country: string; @@ -130,6 +130,7 @@ export const updatePassenger = ( "Date of Birth": passenger.DateOfBirth, // Correct Airtable field name "Military Service": passenger.MilitaryService, }; + const data = { records: [ { @@ -139,13 +140,55 @@ export const updatePassenger = ( ], }; - return axios - .put(`${process.env.VITE_HOST}/passenger/${airtableRecordId}`, data, { - headers: { - Authorization: `Bearer ${token}`, + try { + const response = await axios.put( + `${process.env.VITE_HOST}/passenger/${airtableRecordId}`, + data, + { + headers: { + Authorization: `Bearer ${token}`, + }, + timeout: 5000, }, - }) - .then((res) => res.data); + ); + + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response) { + // Server responded with error status (4xx, 5xx) + switch (error.response.status) { + case 400: + throw new Error("Invalid request data. Please check your input."); + case 401: + throw new Error("Authentication failed. Please log in again."); + case 403: + throw new Error( + "You do not have permission to perform this action.", + ); + case 404: + throw new Error(`Passenger record ${airtableRecordId} not found.`); + case 429: + throw new Error("Too many requests. Please try again later."); + case 500: + throw new Error("Server error. Please try again later."); + default: + throw new Error( + `Server error: ${error.response.data.message || "Unknown error occurred"}`, + ); + } + } else if (error.request) { + // Request was made but no response received + if (error.code === "ECONNABORTED") { + throw new Error("Request timed out. Please try again."); + } + throw new Error("Network error. Please check your connection."); + } + } + throw new Error( + `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } }; export const getAllFlightsForUser = ( diff --git a/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.module.css b/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.module.css index 7afbf1c..f928ed4 100644 --- a/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.module.css +++ b/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.module.css @@ -28,3 +28,27 @@ align-items: center; gap: 2rem; /* Adjust the value as needed */ } + +.footer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.editButton { + margin-left: auto; + color: var(--miracle-color-medium); +} + +.editButton:hover { + cursor: pointer; + color: var(--miracle-color-yellow); +} + +.buttonOptions { + display: flex; + flex-direction: row; + gap: 1rem; + justify-content: center; +} diff --git a/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.tsx b/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.tsx index d152724..7202aeb 100644 --- a/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.tsx +++ b/src/pages/PassengersPage/components/PassengerDetailsModal/PassengerDetailsModal.tsx @@ -1,66 +1,257 @@ import styles from "./PassengerDetailsModal.module.css"; import Modal from "../../../../components/Modal/Modal"; +import { ButtonColor } from "../../../../components/Button/Button.definitions"; +import Button from "../../../../components/Button/Button"; +import Icon from "../../../../components/CustomIcon/Icon"; +import Select from "../../../../components/Select/Select"; +import Input from "../../../../components/Input/Input"; +import { COUNTRIES } from "../../../../util/constants.util"; +import { updatePassenger } from "../../../../api/queries"; +import * as yup from "yup"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useAuth } from "@clerk/clerk-react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; import type { PassengerDetailsModalProps } from "./PassengerDetailsModal.definitions"; const PassengerDetailsModal = ({ passenger, onClose, }: PassengerDetailsModalProps) => { + const [editMode, setEditMode] = useState(false); + const { getToken } = useAuth(); + const queryClient = useQueryClient(); + + const schema = yup.object().shape({ + Street: yup.string().required("Street is required"), + Country: yup.string().required("Country is required"), + Email: yup + .string() + .email("Invalid email format") + .required("Email is required"), + Gender: yup.string().required("Gender is required"), + DateOfBirth: yup.string().required("Date of Birth is required"), + MilitaryService: yup.string().required("Military status is required"), + // Notes: yup.string(), + // Add other field validations as needed + }); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + Street: passenger["Street"], + Country: passenger["Country"], + Email: passenger["Email"], + Gender: passenger["Gender"], + DateOfBirth: passenger["Date of Birth"], + MilitaryService: passenger["Military Service"], + // Notes: passenger["Notes"], + // Add other fields as needed + }, + }); + + interface PassengerFormData { + Street: string; + Country: string; + Email: string; + DateOfBirth: string; + MilitaryService: string; + Gender: string; + Notes?: string; + } + + const { mutate } = useMutation({ + mutationFn: async (data: PassengerFormData) => { + const token = await getToken(); + return updatePassenger( + { + ...data, + }, + passenger.id, + token, + ); + }, + onSuccess: () => { + setEditMode(false); + onClose(); + queryClient.invalidateQueries({ + queryKey: ["accompanyingPassengers"], + }); + }, + onError: (error) => { + console.error("passenger update failed: ", error); + }, + }); + + const onSubmit = async (formData: PassengerFormData) => { + const apiData = { + Street: formData.Street, + Country: formData.Country, + Email: formData.Email, + DateOfBirth: formData.DateOfBirth, + MilitaryService: formData.MilitaryService, + Gender: formData.Gender, + Notes: formData.Notes, + }; + + mutate(apiData); + }; + return ( <> -
-
- Gender{" "} - - {passenger["Gender"]} - -
-
- DOB{" "} - - {passenger["Date of Birth"].split("T")[0]}{" "} - +
{ + onSubmit(data); + })} + > +
+
+ Gender{" "} + {!editMode ? ( + + {passenger["Gender"]} + + ) : ( + + )} +
-
- {/* make another passenger group for address where each line of the address is separated */} -
- Address{" "} - - {passenger["Street"]} - - - {passenger["Country"]} - -
-
- Military{" "} - - {passenger["Military Service"]} - -
-
+ {/* make another passenger group for address where each line of the address is separated */}
- # of Flight Legs{" "} - - {passenger["# of Flight Legs"]} - + Address{" "} + {!editMode ? ( + <> + + {passenger["Street"]} + + + {passenger["Country"]} + + + ) : ( + <> + + + )} +
+
+
+ + # of Flight Legs + {" "} + + {passenger["# of Flight Legs"]} + +
+
+ + # of Booked Flight Requests + {" "} + + {passenger["# of Booked Flight Requests"]} + +
+
+ {/*
+ Notes + {!editMode ? ( + + {passenger["Notes"] || "Notes go here"} + + ) : ( + + )} +
*/} +
+ {!editMode && ( +
{ + setEditMode(!editMode); + }} + > + +
+ )} + {editMode && ( +
+
+ )}
-
-
- Notes - Notes go here -
+ } header={passenger["Full Name"]} From 3d0600417a899d24c67db5aad88464699dc84f7f Mon Sep 17 00:00:00 2001 From: mseok Date: Sun, 10 Nov 2024 03:13:23 -0600 Subject: [PATCH 2/2] Fix test fail --- .../tests/PassengerDetailsModal.test.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/pages/PassengersPage/components/PassengerDetailsModal/tests/PassengerDetailsModal.test.tsx b/src/pages/PassengersPage/components/PassengerDetailsModal/tests/PassengerDetailsModal.test.tsx index 25f8a8b..3540325 100644 --- a/src/pages/PassengersPage/components/PassengerDetailsModal/tests/PassengerDetailsModal.test.tsx +++ b/src/pages/PassengersPage/components/PassengerDetailsModal/tests/PassengerDetailsModal.test.tsx @@ -1,14 +1,29 @@ import { createTestPassengerData } from "../../../../../util/test-data.util"; import PassengerDetailsModal from "../PassengerDetailsModal"; import { render, fireEvent } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +// Mock useAuth to return a mock getToken function +jest.mock("@clerk/clerk-react", () => ({ + useAuth: () => ({ + getToken: jest.fn().mockResolvedValue("mock-token"), // Mock token + }), +})); + +// Create a new QueryClient for each test +const createTestQueryClient = () => new QueryClient(); describe("PassengerDetailsModal", () => { const mockOnClose = jest.fn(); const mockPatient = createTestPassengerData(); it("renders patient details correctly", () => { + const queryClient = createTestQueryClient(); + const { getByText } = render( - , + + + , ); expect(getByText("Gender")).toBeTruthy(); @@ -19,8 +34,12 @@ describe("PassengerDetailsModal", () => { }); it("calls onClose when the modal action is triggered", () => { + const queryClient = createTestQueryClient(); + const component = render( - , + + , + , ); fireEvent.click(component.getByTestId("modal-close")); // Assuming the action triggers on a button click