diff --git a/cdk/api_gateway/lambda_code/api_defaults.py b/cdk/api_gateway/lambda_code/api_defaults.py index 370014f1..640c380b 100644 --- a/cdk/api_gateway/lambda_code/api_defaults.py +++ b/cdk/api_gateway/lambda_code/api_defaults.py @@ -396,3 +396,24 @@ def checkAndCleanRequestFields(data: dict, field_check): pass return data + +def validTimestamp(timestamp: str) -> bool: + """ + Checks if a timestamp matches the TIMESTAMP_FORMAT. + + :params timestamp: The timestamp to compare. + :returns: True if the timestamp matches the expected format. + False otherwise. + """ + + # Parse the timestamp into a datetime object + try: + parsed = datetime.strptime(timestamp, TIMESTAMP_FORMAT) + + # Timestamp not valid if an error occurs during parsing + except: + return False + + # "Recreate" the timestamp from the parsed object according + # to the TIMESTAMP_FORMAT and compare it to the original + return timestamp == parsed.strftime(TIMESTAMP_FORMAT) diff --git a/cdk/api_gateway/lambda_code/equipment_handler/equipment_handler.py b/cdk/api_gateway/lambda_code/equipment_handler/equipment_handler.py index b75667d5..a40c8d78 100644 --- a/cdk/api_gateway/lambda_code/equipment_handler/equipment_handler.py +++ b/cdk/api_gateway/lambda_code/equipment_handler/equipment_handler.py @@ -454,9 +454,7 @@ def validateEquipmentRequestBody(self, data: dict): # Ensure timestamp is in the correct format - try: - datetime.strptime(data['timestamp'], TIMESTAMP_FORMAT) - except ValueError: + if not validTimestamp(data['timestamp']): errorMsg: str = f"Timestamp not in the approved format. Approved format is 'YYYY-MM-DDThh:mm:ss'." raise InvalidRequestBody(errorMsg) diff --git a/cdk/api_gateway/lambda_code/qualifications_handler/qualifications_handler.py b/cdk/api_gateway/lambda_code/qualifications_handler/qualifications_handler.py index 3730c696..cacb065e 100644 --- a/cdk/api_gateway/lambda_code/qualifications_handler/qualifications_handler.py +++ b/cdk/api_gateway/lambda_code/qualifications_handler/qualifications_handler.py @@ -350,9 +350,7 @@ def validateQualificationRequestBody(self, data: dict): raise InvalidRequestBody(errorMsg) # Ensure last_updated is in the correct format - try: - datetime.strptime(data['last_updated'], TIMESTAMP_FORMAT) - except ValueError: + if not validTimestamp(data['last_updated']): errorMsg: str = f"Timestamp 'last_updated 'not in the approved format. Approved format is 'YYYY-MM-DDThh:mm:ss'." raise InvalidRequestBody(errorMsg) diff --git a/cdk/api_gateway/lambda_code/tiger_training_handler/tiger_training_handler.py b/cdk/api_gateway/lambda_code/tiger_training_handler/tiger_training_handler.py index 0d9584a7..c67a646e 100644 --- a/cdk/api_gateway/lambda_code/tiger_training_handler/tiger_training_handler.py +++ b/cdk/api_gateway/lambda_code/tiger_training_handler/tiger_training_handler.py @@ -18,9 +18,6 @@ http = urllib3.PoolManager() -# Taken from api_gateway/lambda_code/api_defaults.py -BACKEND_TIMESTAMP_FORMAT: str = "%Y-%m-%dT%H:%M:%S" - # The bridge timestamp expects the seconds to be in milliseconds (3 decimal places) BRIDGE_TIMESTAMP_FORMAT: str = "%Y-%m-%dT%H:%M:%S.%f" # bridge also (as far as observed) only uses the timezone offset of -04:00 @@ -193,9 +190,9 @@ def get_completed_enrollments(self, bridge_url: str, auth_token: str, # Get the user_id to use for the learner user_id = learner_lookup[enrollment["links"]["learner"]["id"]] - # Convert updated_at timestamp to BACKEND_TIMESTAMP_FORMAT + # Convert updated_at timestamp to TIMESTAMP_FORMAT updated_datetime = datetime.fromisoformat(enrollment['updated_at']) - last_updated = updated_datetime.strftime(BACKEND_TIMESTAMP_FORMAT) + last_updated = updated_datetime.strftime(TIMESTAMP_FORMAT) # Update the learner's enrolled courses if they completed the course if enrollment["state"].lower() == "complete": diff --git a/cdk/api_gateway/lambda_code/visits_handler/visits_handler.py b/cdk/api_gateway/lambda_code/visits_handler/visits_handler.py index 54639c4c..64408d73 100644 --- a/cdk/api_gateway/lambda_code/visits_handler/visits_handler.py +++ b/cdk/api_gateway/lambda_code/visits_handler/visits_handler.py @@ -345,9 +345,7 @@ def validateVisitRequestBody(self, data: dict): raise InvalidRequestBody(errorMsg) # Ensure timestamp is in the correct format - try: - datetime.strptime(data['timestamp'], TIMESTAMP_FORMAT) - except ValueError: + if not validTimestamp(data['timestamp']): errorMsg: str = f"Timestamp not in the approved format. Approved format is 'YYYY-MM-DDThh:mm:ss'." raise InvalidRequestBody(errorMsg) diff --git a/site/visitor-console/src/components/EquipmentFormStages/InitialInfo.tsx b/site/visitor-console/src/components/EquipmentFormStages/InitialInfo.tsx index 0e08a305..92004bfb 100644 --- a/site/visitor-console/src/components/EquipmentFormStages/InitialInfo.tsx +++ b/site/visitor-console/src/components/EquipmentFormStages/InitialInfo.tsx @@ -123,7 +123,7 @@ const InitialInfo: React.FC = ({ {errors.printer_name && ( @@ -141,7 +141,7 @@ const InitialInfo: React.FC = ({ className="form-control" type="text" placeholder="Enter a name for the print" - {...register("printer_3d_info.print_name")} + {...register("print_name")} /> {errors.print_name && (

{errors.print_name.message}

@@ -158,7 +158,7 @@ const InitialInfo: React.FC = ({ className="form-control" type="text" placeholder="Enter print duration" - {...register("printer_3d_info.print_duration")} + {...register("print_duration")} /> {errors.print_duration && (

{errors.print_duration.message}

@@ -178,10 +178,12 @@ const InitialInfo: React.FC = ({ className="form-control" type="text" placeholder="Enter print mass" - {...register("printer_3d_info.print_mass_estimate")} + {...register("print_mass_estimate")} /> - {errors.print_mass && ( -

{errors.print_mass.message}

+ {errors.print_mass_estimate && ( +

+ {errors.print_mass_estimate.message} +

)} @@ -200,7 +202,7 @@ const InitialInfo: React.FC = ({ className="form-control" type="text" placeholder="Enter resin volume" - {...register("printer_3d_info.resin_volume")} + {...register("resin_volume")} /> {errors.resin_volume && (

{errors.resin_volume.message}

@@ -214,7 +216,7 @@ const InitialInfo: React.FC = ({ {errors.resin_type && ( diff --git a/site/visitor-console/src/pages/App.tsx b/site/visitor-console/src/pages/App.tsx index 5d103a25..e3b17a9d 100644 --- a/site/visitor-console/src/pages/App.tsx +++ b/site/visitor-console/src/pages/App.tsx @@ -12,19 +12,17 @@ import EquipmentForm from "./EquipmentForm"; import Visits from "./Visits"; import EquipmentUsage from "./EquipmentUsage"; -const App = () => { - useEffect(() => { - const config = getAmplifyConfig(); - Amplify.configure({ - Auth: { - Cognito: { - userPoolId: config.userPoolId, - userPoolClientId: config.userPoolClientId, - }, - }, - }); - }, []); +const config = getAmplifyConfig(); +Amplify.configure({ + Auth: { + Cognito: { + userPoolId: config.userPoolId, + userPoolClientId: config.userPoolClientId, + }, + }, +}); +const App = () => { return ( diff --git a/site/visitor-console/src/pages/EquipmentForm.tsx b/site/visitor-console/src/pages/EquipmentForm.tsx index 6bf05ffd..b699c980 100644 --- a/site/visitor-console/src/pages/EquipmentForm.tsx +++ b/site/visitor-console/src/pages/EquipmentForm.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Link, Navigate, useNavigate } from "react-router-dom"; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; @@ -20,61 +20,91 @@ import { is3DPrinter, } from "../library/constants"; +// Used for transforming objects before POST to api +const Printer3DInfoKeys = new Set([ + "printer_name", + "print_name", + "print_duration", + "print_status", + "print_notes", + "print_mass", + "print_mass_estimate", + "resin_volume", + "resin_type", +]); + const stageSchemas = [ // First stage - Initial Info yup.object({ - user_id: yup.string().required(), - location: yup.string().required(), - equipment_type: yup.string().required(), - equipment_history: yup.string().required(), + user_id: yup.string().required().label("User ID"), + location: yup.string().required().label("Makerspace Location"), + equipment_type: yup.string().required().label("Equipment Type"), + equipment_history: yup.string().required().label("Equipment History"), // Printer fields with conditional validation - printer_3d_info: yup - .object() - .shape({ - printer_name: yup.string().required(), - print_name: yup.string().required(), - print_duration: yup.string().required(), - print_status: yup.string().default("In Progress"), - print_notes: yup.string().default(""), - - // Specifically require estimated print mass when using plastic 3d printers - print_mass_estimate: yup.string().when("equipment_type", { - is: FDM_PRINTER_STRING, - then: yup.string().required(), - otherwise: yup.string().notRequired(), - }), - // Always default print mass to the unknown value as the print isn't finished - print_mass: yup.string().when("equipment_type", { - is: FDM_PRINTER_STRING, - then: yup.string().default(""), - otherwise: yup.string().notRequired(), - }), - - // Specifically require resin volume and type when using resin 3d printers - resin_volume: yup.string().when("equipment_type", { - is: SLA_PRINTER_STRING, - then: yup.string().required(), - otherwise: yup.string().notRequired(), - }), - resin_type: yup.string().when("equipment_type", { - is: SLA_PRINTER_STRING, - then: yup.string().required(), - otherwise: yup.string().notRequired(), - }), - }) - .when("equipment_type", { - // .when() here is for printer_3d_info - is: is3DPrinter, - then: yup.object().required(), - otherwise: yup.object().notRequired(), - }), + // These will be transformed into the appropriate printer_3d_info object + // before sending the full data object to the api. + printer_name: yup.string().when("equipment_type", { + is: is3DPrinter, + then: yup.string().required().label("Printer Name"), + otherwise: yup.string().notRequired(), + }), + print_name: yup.string().when("equipment_type", { + is: is3DPrinter, + then: yup.string().required().label("Print Name"), + otherwise: yup.string().notRequired(), + }), + print_duration: yup.string().when("equipment_type", { + is: is3DPrinter, + then: yup.string().required().label("Print Duration"), + otherwise: yup.string().notRequired(), + }), + print_status: yup.string().when("equipment_type", { + is: is3DPrinter, + then: yup.string().default("In Progress"), + otherwise: yup.string().notRequired(), + }), + print_notes: yup.string().when("equipment_type", { + is: is3DPrinter, + then: yup.string().default(""), + otherwise: yup.string().notRequired(), + }), + + // Specifically require estimated print mass when using plastic 3d printers + print_mass_estimate: yup.string().when("equipment_type", { + is: FDM_PRINTER_STRING, + then: yup.string().required().label("Print Mass Estimate"), + otherwise: yup.string().notRequired(), + }), + + // Default print mass to the unknown value as the print hasn't finished + print_mass: yup.string().when("equipment_type", { + is: FDM_PRINTER_STRING, + then: (schema) => schema.default(""), + otherwise: (schema) => schema.notRequired(), + }), + + // Specifically require resin volume and type when using resin 3d printers + resin_volume: yup.string().when("equipment_type", { + is: SLA_PRINTER_STRING, + then: (schema) => schema.required().label("Resin Volume"), + otherwise: (schema) => schema.notRequired(), + }), + resin_type: yup.string().when("equipment_type", { + is: SLA_PRINTER_STRING, + then: (schema) => schema.required().label("Resin Type"), + otherwise: (schema) => schema.notRequired(), + }), }), // Second stage - Project Details yup.object({ - project_name: yup.string().required(), - project_type: yup.string().oneOf(projectTypes).required(), + project_name: yup.string().required().label("Project Name"), + project_type: yup + .string() + .oneOf(projectTypes) + .required() + .label("Project Type"), project_details: yup.string(), department: yup.string(), class_number: yup.string().when("project_type", { @@ -82,38 +112,38 @@ const stageSchemas = [ then: yup .string() .matches(/^[A-Z]{4}-[0-9]{4}$/, "Invalid class number format") - .required(), + .required() + .label("Class Number"), }), faculty_name: yup.string().when("project_type", { is: "Class", - then: yup.string().required(), + then: yup.string().required().label("Faculty Name"), }), project_sponsor: yup.string().when("project_type", { is: "Class", - then: yup.string().required(), + then: yup.string().required().label("Project Sponsor"), }), organization_affiliation: yup.string().when("project_type", { is: "Club", - then: yup.string().required(), + then: yup.string().required().label("Organization Affiliation"), }), }), // Final stage - Survey yup.object({ - intern: yup.string().required(), - satisfaction: yup.string().required(), - difficulties: yup.string().required(), + intern: yup.string(), + satisfaction: yup.string(), + difficulties: yup.string(), issue_description: yup.string(), }), ]; const EquipmentForm = () => { const navigate = useNavigate(); - const [saved, setSaved] = useState(false); const [stage, setStage] = useState(0); const [formData, setFormData] = useState>({}); - const currentSchema = stageSchemas[stage]; + const currentSchema = stageSchemas[stage] ?? yup.object(); type FormData = yup.InferType; const { @@ -122,65 +152,14 @@ const EquipmentForm = () => { control, reset, watch, + setValue, formState: { errors }, } = useForm({ resolver: yupResolver(currentSchema), mode: "onChange", - defaultValues: formData as FormData, + shouldUnregister: true, }); - const onSubmit = handleSubmit((data) => { - setFormData((prevData) => ({ ...prevData, ...data })); - - if (stage < stageComponents.length - 1) { - setStage((prevStage) => prevStage + 1); - } else { - post_equipment_form({ ...formData, ...data } as EquipmentSchema); - } - }); - - const post_equipment_form = async ( - form_data: EquipmentSchema - ): Promise => { - // Add print status and notes defaults to form data - const dataWithDefaults = { - ...form_data, - timestamp: new Date().toISOString().split(".")[0], - }; - console.log("Form Submission:", JSON.stringify(dataWithDefaults, null, 2)); - - //try { - // const response = await fetch(`${api_endpoint}/equipment`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(dataWithDefaults), - // }); - - // if (response.ok) { - // console.log( - // "Data successfully sent to the API:", - // await response.json() - // ); - // setSaved(true); - // } else { - // console.error( - // "Failed to send data to the API:", - // response.status, - // await response.text() - // ); - // console.log("Failed to submit the form."); - // } - //} catch (error) { - // console.error("An error occurred while submitting the form:", error); - //} - }; - - if (saved) { - navigate("/"); - } - const stageComponents = [ { , ]; + const [stageData, setStageData] = useState( + new Array(stageComponents.length).fill(null) + ); + + const onSubmit = handleSubmit((data) => { + console.log(`Submitted data: ${JSON.stringify(data, null, 2)}`); + + setFormData((prevData) => ({ ...prevData, ...data })); + + setStageData((prevData) => { + const updatedData = [...prevData]; + updatedData[stage] = { ...data }; + return updatedData; + }); + + if (stage < stageComponents.length - 1) { + setStage((prevStage) => prevStage + 1); + reset({ ...formData, ...data }); + } else { + // A bit scuffed having duplicate code, but this is + // necessary to provide the post request with the + // latest data. + setStageData((prevData) => { + const updatedData = [...prevData]; + updatedData[stage] = { ...data }; + post_equipment_form(updatedData); + return updatedData; + }); + } + }); + + const post_equipment_form = async (latestStageData: any[]): Promise => { + // Compress the array of stage data into one object + // Note: reduce() works as expected as long as all keys are unique. + // It overwrites the same keys with the value of the last one evaluated. + const allStageData = latestStageData.reduce( + (acc, obj) => ({ ...acc, ...obj }), + {} + ); + + // Transform data relating to 3d printers into a printer_3d_info object + const transformedData = Object.keys(allStageData).reduce( + (acc, key) => { + //console.log(`Checking key: ${key}`); + if (Printer3DInfoKeys.has(key)) { + acc.printer_3d_info[key] = allStageData[key]; // Move to "printer_3d_info" + console.log(`Adding value: ${allStageData[key]}`); + console.log(`Acc is now: ${JSON.stringify(acc, null, 2)}`); + } else { + acc[key] = allStageData[key]; // Keep other fields + } + return acc; + }, + { printer_3d_info: {} } as Record + ); + + // Remove the printer_3d_info key if the equipment_type is not a printer + // Currently guaranteed to have this field via the attempted transformation + // from above. + if (!is3DPrinter(transformedData.equipment_type)) { + delete transformedData.printer_3d_info; + } + + // Add the timestamp to the data + const dataWithDefaults = { + ...transformedData, + timestamp: new Date().toISOString().split(".")[0], + }; + console.log("Form Submission:", JSON.stringify(dataWithDefaults, null, 2)); + + try { + const response = await fetch(`${api_endpoint}/equipment`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(dataWithDefaults), + }); + + if (response.ok) { + console.log( + "Data successfully sent to the API:", + await response.json() + ); + navigate("/"); + } else { + console.error( + "Failed to send data to the API:", + response.status, + await response.text() + ); + console.log("Failed to submit the form."); + } + } catch (error) { + console.error("An error occurred while submitting the form:", error); + } + }; + return ( { className="btn btn-secondary" onClick={() => { setStage((prevStage) => prevStage - 1); + reset(formData as FormData); }} > diff --git a/site/visitor-console/src/pages/EquipmentUsage.tsx b/site/visitor-console/src/pages/EquipmentUsage.tsx index 83e82b5f..679d8baf 100644 --- a/site/visitor-console/src/pages/EquipmentUsage.tsx +++ b/site/visitor-console/src/pages/EquipmentUsage.tsx @@ -36,9 +36,10 @@ const EquipmentUsage = () => { const data = await response.json(); console.log(`Data received:\n${JSON.stringify(data, null, 2)}`); - const logs = Array.isArray(data.equipment_logs) + const logs: EquipmentLog[] = Array.isArray(data.equipment_logs) ? data.equipment_logs : []; + setEquipmentLogs(logs); } catch (error) { console.error("Error fetching equipment logs:", error); @@ -108,6 +109,95 @@ const EquipmentUsage = () => { } }; + const appendLogs = (logs: EquipmentLog[]) => { + setEquipmentLogs((prevLogs) => { + // Create a Map to ensure unique logs based on user_id and timestamp + const logMap = new Map(); + + // Add previous logs to the Map + prevLogs.forEach((log) => { + const key = `${log.user_id}-${log.timestamp}`; // Composite key + logMap.set(key, log); // Add the log to the Map + }); + + // Add new logs from the fetched data to the Map + logs.forEach((log) => { + const key = `${log.user_id}-${log.timestamp}`; // Composite key + logMap.set(key, log); // Add the log to the Map + }); + + // Convert the Map back to an array and return it + return Array.from(logMap.values()); + }); + }; + + const handleSearch = async (user_id: string) => { + setLoading(true); + setError(null); + try { + const response = await fetch( + `${api_endpoint}/equipment/${user_id}?limit=50`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Api-Key": import.meta.env.VITE_BACKEND_KEY, + }, + } + ); + + if (!response.ok) { + throw new Error( + `Error fetching equipment logs for ${user_id}: ${response.statusText}` + ); + } + + const data = await response.json(); + console.log(`Data received:\n${JSON.stringify(data, null, 2)}`); + const logs: EquipmentLog[] = Array.isArray(data.equipment_logs) + ? data.equipment_logs + : []; + + appendLogs(logs); + } catch (error) { + console.error("Error fetching equipment logs:", error); + } finally { + setLoading(false); + } + }; + + const handleRefresh = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch(`${api_endpoint}/equipment?limit=50`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Api-Key": import.meta.env.VITE_BACKEND_KEY, + }, + }); + + if (!response.ok) { + throw new Error( + `Error fetching equipment logs: ${response.statusText}` + ); + } + + const data = await response.json(); + console.log(`Data received:\n${JSON.stringify(data, null, 2)}`); + const logs: EquipmentLog[] = Array.isArray(data.equipment_logs) + ? data.equipment_logs + : []; + + appendLogs(logs); + } catch (error) { + console.error("Error fetching equipment logs:", error); + } finally { + setLoading(false); + } + }; + const filteredLogs = equipmentLogs.filter((log) => searchUsername ? log.user_id.includes(searchUsername) : true ); @@ -122,7 +212,17 @@ const EquipmentUsage = () => {
-
+
+
+ +
+
diff --git a/site/visitor-console/src/pages/Home.tsx b/site/visitor-console/src/pages/Home.tsx index bef6b855..f2180a74 100644 --- a/site/visitor-console/src/pages/Home.tsx +++ b/site/visitor-console/src/pages/Home.tsx @@ -13,7 +13,7 @@ const Home = () => { >
- + diff --git a/site/visitor-console/src/pages/Visits.tsx b/site/visitor-console/src/pages/Visits.tsx index 93c5e2d6..a59ce658 100644 --- a/site/visitor-console/src/pages/Visits.tsx +++ b/site/visitor-console/src/pages/Visits.tsx @@ -5,15 +5,15 @@ import { withAuthenticator } from "@aws-amplify/ui-react"; import { Link } from "react-router-dom"; // Define the type for a visit -type Visit = { +interface Visit { location: string; user_id: string; timestamp: string; -}; +} -type VisitResponse = { - visits: Visit[]; -}; +interface VisitLog extends Visit { + _ignore?: string; +} const Visits = () => { const [searchUsername, setSearchUsername] = useState(""); @@ -39,8 +39,11 @@ const Visits = () => { throw new Error(`Error fetching visits: ${response.statusText}`); } - const data: VisitResponse = await response.json(); - setVisits(data.visits); + const data = await response.json(); + console.log(`Data received:\n${JSON.stringify(data, null, 2)}`); + const logs: VisitLog[] = Array.isArray(data.visits) ? data.visits : []; + + setVisits(logs); } catch (error: any) { console.error("Fetch error:", error); setError(error.message || "An unknown error occurred."); @@ -53,6 +56,89 @@ const Visits = () => { fetchVisits(); }, []); + const appendLogs = (logs: VisitLog[]) => { + setVisits((prevLogs) => { + // Create a Map to ensure unique logs based on user_id and timestamp + const logMap = new Map(); + + // Add previous logs to the Map + prevLogs.forEach((log) => { + const key = `${log.user_id}-${log.timestamp}`; // Composite key + logMap.set(key, log); // Add the log to the Map + }); + + // Add new logs from the fetched data to the Map + logs.forEach((log) => { + const key = `${log.user_id}-${log.timestamp}`; // Composite key + logMap.set(key, log); // Add the log to the Map + }); + + // Convert the Map back to an array and return it + return Array.from(logMap.values()); + }); + }; + + const handleSearch = async (user_id: string) => { + setLoading(true); + setError(null); + try { + const response = await fetch( + `${api_endpoint}/visits/${user_id}?limit=50`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Api-Key": import.meta.env.VITE_BACKEND_KEY, + }, + } + ); + + if (!response.ok) { + throw new Error( + `Error fetching visits for ${user_id}: ${response.statusText}` + ); + } + + const data = await response.json(); + console.log(`Data received:\n${JSON.stringify(data, null, 2)}`); + const logs: VisitLog[] = Array.isArray(data.visits) ? data.visits : []; + + appendLogs(logs); + } catch (error) { + console.error("Error fetching visits:", error); + } finally { + setLoading(false); + } + }; + + const handleRefresh = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch(`${api_endpoint}/visits?limit=50`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Api-Key": import.meta.env.VITE_BACKEND_KEY, + }, + }); + + if (!response.ok) { + throw new Error(`Error fetching visits: ${response.statusText}`); + } + + const data = await response.json(); + console.log(`Data received:\n${JSON.stringify(data, null, 2)}`); + const logs: VisitLog[] = Array.isArray(data.visits) ? data.visits : []; + + appendLogs(logs); + } catch (error) { + console.error("Error fetching visits:", error); + } finally { + setLoading(false); + } + }; + const filteredVisits = visits.filter( (visit) => (locationFilter === "All" || visit.location === locationFilter) && @@ -95,13 +181,20 @@ const Visits = () => { Cooper +