diff --git a/src/components/ui/coaching-session-selector.tsx b/src/components/ui/coaching-session-selector.tsx index 2ca2569..8a2140f 100644 --- a/src/components/ui/coaching-session-selector.tsx +++ b/src/components/ui/coaching-session-selector.tsx @@ -12,9 +12,11 @@ import { } from "@/components/ui/select"; import { getDateTimeFromString, Id } from "@/types/general"; import { useCoachingSessions } from "@/lib/api/coaching-sessions"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { DateTime } from "ts-luxon"; import { useCoachingSessionStateStore } from "@/lib/providers/coaching-session-state-store-provider"; +import { fetchOverarchingGoalsByCoachingSessionId } from "@/lib/api/overarching-goals"; +import { OverarchingGoal } from "@/types/overarching-goal"; interface CoachingSessionsSelectorProps extends PopoverProps { /// The CoachingRelationship Id for which to get a list of associated CoachingSessions @@ -30,25 +32,47 @@ function CoachingSessionsSelectItems({ }: { relationshipId: Id; }) { - const { coachingSessions, isLoading, isError } = - useCoachingSessions(relationshipId); + const { + coachingSessions, + isLoading: isLoadingSessions, + isError: isErrorSessions, + } = useCoachingSessions(relationshipId); + const { setCurrentCoachingSessions } = useCoachingSessionStateStore( (state) => state ); + const [goals, setGoals] = useState<(OverarchingGoal[] | undefined)[]>([]); + const [isLoadingGoals, setIsLoadingGoals] = useState(false); - console.debug(`coachingSessions: ${JSON.stringify(coachingSessions)}`); - - // Be sure to cache the list of current coaching sessions in the CoachingSessionStateStore useEffect(() => { if (!coachingSessions.length) return; - console.debug( - `coachingSessions (useEffect): ${JSON.stringify(coachingSessions)}` - ); setCurrentCoachingSessions(coachingSessions); }, [coachingSessions]); - if (isLoading) return
Loading...
; - if (isError) return
Error loading coaching sessions
; + useEffect(() => { + const fetchGoals = async () => { + setIsLoadingGoals(true); + try { + const sessionIds = coachingSessions?.map((session) => session.id) || []; + const goalsPromises = sessionIds.map((id) => + fetchOverarchingGoalsByCoachingSessionId(id) + ); + const fetchedGoals = await Promise.all(goalsPromises); + setGoals(fetchedGoals); + } catch (error) { + console.error("Error fetching goals:", error); + } finally { + setIsLoadingGoals(false); + } + }; + + if (coachingSessions?.length) { + fetchGoals(); + } + }, [coachingSessions]); + + if (isLoadingSessions || isLoadingGoals) return
Loading...
; + if (isErrorSessions) return
Error loading coaching sessions
; if (!coachingSessions?.length) return
No coaching sessions found
; return ( @@ -62,13 +86,25 @@ function CoachingSessionsSelectItems({ .filter( (session) => getDateTimeFromString(session.date) < DateTime.now() ) - .map((session) => ( - - {getDateTimeFromString(session.date).toLocaleString( - DateTime.DATETIME_FULL - )} - - ))} + .map((session) => { + const sessionIndex = coachingSessions.findIndex( + (s) => s.id === session.id + ); + return ( + +
+ + {goals[sessionIndex]?.[0]?.title || "No goal set"} + + + {getDateTimeFromString(session.date).toLocaleString( + DateTime.DATETIME_FULL + )} + +
+
+ ); + })} )} {coachingSessions.some( @@ -80,13 +116,25 @@ function CoachingSessionsSelectItems({ .filter( (session) => getDateTimeFromString(session.date) >= DateTime.now() ) - .map((session) => ( - - {getDateTimeFromString(session.date).toLocaleString( - DateTime.DATETIME_FULL - )} - - ))} + .map((session) => { + const sessionIndex = coachingSessions.findIndex( + (s) => s.id === session.id + ); + return ( + +
+ + {goals[sessionIndex]?.[0]?.title || "No goal set"} + + + {getDateTimeFromString(session.date).toLocaleString( + DateTime.DATETIME_FULL + )} + +
+
+ ); + })} )} @@ -105,23 +153,51 @@ export default function CoachingSessionSelector({ getCurrentCoachingSession, } = useCoachingSessionStateStore((state) => state); + const [currentGoal, setCurrentGoal] = useState(); + const [isLoadingGoal, setIsLoadingGoal] = useState(false); + + const currentSession = currentCoachingSessionId + ? getCurrentCoachingSession(currentCoachingSessionId) + : null; + + useEffect(() => { + const fetchGoal = async () => { + if (!currentCoachingSessionId) return; + + setIsLoadingGoal(true); + try { + const goals = await fetchOverarchingGoalsByCoachingSessionId( + currentCoachingSessionId + ); + setCurrentGoal(goals[0]); + } catch (error) { + console.error("Error fetching goal:", error); + } finally { + setIsLoadingGoal(false); + } + }; + + fetchGoal(); + }, [currentCoachingSessionId]); + const handleSetCoachingSession = (coachingSessionId: Id) => { setCurrentCoachingSessionId(coachingSessionId); if (onSelect) { - onSelect(relationshipId); + onSelect(coachingSessionId); } }; - const currentSession = currentCoachingSessionId - ? getCurrentCoachingSession(currentCoachingSessionId) - : null; - const displayValue = currentSession ? ( - <> - {getDateTimeFromString(currentSession.date).toLocaleString( - DateTime.DATETIME_FULL - )} - +
+ + {currentGoal?.title || "No goal set"} + + + {getDateTimeFromString(currentSession.date).toLocaleString( + DateTime.DATETIME_FULL + )} + +
) : undefined; return ( diff --git a/src/lib/api/overarching-goals.ts b/src/lib/api/overarching-goals.ts index 471fa69..737f69d 100644 --- a/src/lib/api/overarching-goals.ts +++ b/src/lib/api/overarching-goals.ts @@ -8,8 +8,102 @@ import { parseOverarchingGoal, } from "@/types/overarching-goal"; import { ItemStatus, Id } from "@/types/general"; -import { AxiosError, AxiosResponse } from "axios"; +import axios, { AxiosError, AxiosResponse } from "axios"; import { siteConfig } from "@/site.config"; +import useSWR, { useSWRConfig } from "swr"; + +interface ApiResponseOverarchingGoals { + status_code: number; + data: OverarchingGoal[]; +} + +// Fetch all OverarchingGoals associated with a particular User +const fetcherOverarchingGoals = async ( + url: string, + coachingSessionId: Id +): Promise => + axios + .get(url, { + params: { + coaching_session_id: coachingSessionId, + }, + withCredentials: true, + timeout: 5000, + headers: { + "X-Version": siteConfig.env.backendApiVersion, + }, + }) + .then((res) => res.data.data); + +/// A hook to retrieve all OverarchingGoals associated with coachingSessionId +export function useOverarchingGoals(coachingSessionId: Id) { + const { data, error, isLoading } = useSWR( + [ + `${siteConfig.env.backendServiceURL}/overarching_goals`, + coachingSessionId, + ], + ([url, _token]) => fetcherOverarchingGoals(url, coachingSessionId) + ); + const swrConfig = useSWRConfig(); + console.debug(`swrConfig: ${JSON.stringify(swrConfig)}`); + + console.debug(`overarchingGoals data: ${JSON.stringify(data)}`); + + return { + overarchingGoals: Array.isArray(data) ? data : [], + isLoading, + isError: error, + }; +} + +/// A hook to retrieve a single OverarchingGoal by a coachingSessionId +export function useOverarchingGoalByCoachingSessionId(coachingSessionId: Id) { + const { overarchingGoals, isLoading, isError } = + useOverarchingGoals(coachingSessionId); + + return { + overarchingGoal: overarchingGoals.length + ? overarchingGoals[0] + : defaultOverarchingGoal(), + isLoading, + isError: isError, + }; +} + +interface ApiResponseOverarchingGoal { + status_code: number; + data: OverarchingGoal; +} + +// Fetcher for retrieving a single OverarchingGoal by its Id +const fetcherOverarchingGoal = async (url: string): Promise => + axios + .get(url, { + withCredentials: true, + timeout: 5000, + headers: { + "X-Version": siteConfig.env.backendApiVersion, + }, + }) + .then((res) => res.data.data); + +/// A hook to retrieve a single OverarchingGoal by its Id +export function useOverarchingGoal(overarchingGoalId: Id) { + const { data, error, isLoading } = useSWR( + `${siteConfig.env.backendServiceURL}/overarching_goals/${overarchingGoalId}`, + fetcherOverarchingGoal + ); + const swrConfig = useSWRConfig(); + console.debug(`swrConfig: ${JSON.stringify(swrConfig)}`); + + console.debug(`overarchingGoal data: ${JSON.stringify(data)}`); + + return { + overarchingGoal: data || defaultOverarchingGoal(), + isLoading, + isError: error, + }; +} export const fetchOverarchingGoalsByCoachingSessionId = async ( coachingSessionId: Id diff --git a/src/lib/providers/overarching-goal-state-store-provider.tsx b/src/lib/providers/overarching-goal-state-store-provider.tsx new file mode 100644 index 0000000..50dab8b --- /dev/null +++ b/src/lib/providers/overarching-goal-state-store-provider.tsx @@ -0,0 +1,47 @@ +// The purpose of this provider is to provide compatibility with +// Next.js re-rendering and component caching +"use client"; + +import { type ReactNode, createContext, useRef, useContext } from "react"; +import { type StoreApi, useStore } from "zustand"; + +import { + type OverarchingGoalStateStore, + createOverarchingGoalStateStore, +} from "@/lib/stores/overarching-goal-state-store"; + +export const OverarchingGoalStateStoreContext = + createContext | null>(null); + +export interface OverarchingGoalStateStoreProviderProps { + children: ReactNode; +} + +export const OverarchingGoalStateStoreProvider = ({ + children, +}: OverarchingGoalStateStoreProviderProps) => { + const storeRef = useRef>(undefined); + if (!storeRef.current) { + storeRef.current = createOverarchingGoalStateStore(); + } + + return ( + + {children} + + ); +}; + +export const useOverarchingGoalStateStore = ( + selector: (store: OverarchingGoalStateStore) => T +): T => { + const oagStateStoreContext = useContext(OverarchingGoalStateStoreContext); + + if (!oagStateStoreContext) { + throw new Error( + `useOverarchingGoalStateStore must be used within OverarchingGoalStateStoreProvider` + ); + } + + return useStore(oagStateStoreContext, selector); +}; diff --git a/src/lib/providers/root-layout-providers.tsx b/src/lib/providers/root-layout-providers.tsx index 49f2063..96e7f0c 100644 --- a/src/lib/providers/root-layout-providers.tsx +++ b/src/lib/providers/root-layout-providers.tsx @@ -6,6 +6,7 @@ import { SWRConfig } from "swr"; import { OrganizationStateStoreProvider } from "./organization-state-store-provider"; import { CoachingRelationshipStateStoreProvider } from "./coaching-relationship-state-store-provider"; import { CoachingSessionStateStoreProvider } from "./coaching-session-state-store-provider"; +import { OverarchingGoalStateStoreProvider } from "./overarching-goal-state-store-provider"; export function RootLayoutProviders({ children, @@ -24,15 +25,17 @@ export function RootLayoutProviders({ - new Map(), - }} - > - {children} - + + new Map(), + }} + > + {children} + + diff --git a/src/lib/stores/coaching-session-state-store.ts b/src/lib/stores/coaching-session-state-store.ts index 92508f5..2e1a2ab 100644 --- a/src/lib/stores/coaching-session-state-store.ts +++ b/src/lib/stores/coaching-session-state-store.ts @@ -16,6 +16,7 @@ interface CoachingSessionState { interface CoachingSessionsStateActions { getCurrentCoachingSession: (coachingSessionId: Id) => CoachingSession; + getCurrentCoachingSessionId: () => Id; setCurrentCoachingSessionId: (newCoachingSessionId: Id) => void; setCurrentCoachingSession: (newCoachingSession: CoachingSession) => void; setCurrentCoachingSessions: (newCoachingSessions: CoachingSession[]) => void; @@ -51,11 +52,17 @@ export const createCoachingSessionStateStore = ( ) : defaultCoachingSession(); }, + getCurrentCoachingSessionId: () => { + return get().currentCoachingSessionId; + }, setCurrentCoachingSessionId: (newCoachingSessionId) => { set({ currentCoachingSessionId: newCoachingSessionId }); }, setCurrentCoachingSession: (newCoachingSession) => { - set({ currentCoachingSession: newCoachingSession }); + set({ + currentCoachingSession: newCoachingSession, + currentCoachingSessionId: newCoachingSession.id, + }); }, setCurrentCoachingSessions: ( newCoachingSessions: CoachingSession[] diff --git a/src/lib/stores/overarching-goal-state-store.ts b/src/lib/stores/overarching-goal-state-store.ts new file mode 100644 index 0000000..2eea2a7 --- /dev/null +++ b/src/lib/stores/overarching-goal-state-store.ts @@ -0,0 +1,75 @@ +import { Id } from "@/types/general"; +import { + defaultOverarchingGoal, + defaultOverarchingGoals, + getOverarchingGoalById, + OverarchingGoal, +} from "@/types/overarching-goal"; +import { create } from "zustand"; +import { createJSONStorage, devtools, persist } from "zustand/middleware"; + +interface OverarchingGoalState { + currentOverarchingGoalId: Id; + currentOverarchingGoal: OverarchingGoal; + currentOverarchingGoals: OverarchingGoal[]; +} + +interface OverarchingGoalStateActions { + getCurrentOverarchingGoal: (overarchingGoalId: Id) => OverarchingGoal; + setCurrentOverarchingGoalId: (newOverarchingGoalId: Id) => void; + setCurrentOverarchingGoal: (newOverarchingGoal: OverarchingGoal) => void; + setCurrentOverarchingGoals: (newOverarchingGoals: OverarchingGoal[]) => void; + resetOverarchingGoalState(): void; +} + +export type OverarchingGoalStateStore = OverarchingGoalState & + OverarchingGoalStateActions; + +export const defaultInitState: OverarchingGoalState = { + currentOverarchingGoalId: "", + currentOverarchingGoal: defaultOverarchingGoal(), + currentOverarchingGoals: defaultOverarchingGoals(), +}; + +export const createOverarchingGoalStateStore = ( + initState: OverarchingGoalState = defaultInitState +) => { + const oagStateStore = create()( + devtools( + persist( + (set, get) => ({ + ...initState, + + // Expects the array of OverarchingGoals to be fetched and set + getCurrentOverarchingGoal: ( + overarchingGoalId: Id + ): OverarchingGoal => { + return get().currentOverarchingGoals + ? getOverarchingGoalById( + overarchingGoalId, + get().currentOverarchingGoals + ) + : defaultOverarchingGoal(); + }, + setCurrentOverarchingGoalId: (newOverarchingGoalId) => { + set({ currentOverarchingGoalId: newOverarchingGoalId }); + }, + setCurrentOverarchingGoal: (newOverarchingGoal) => { + set({ currentOverarchingGoal: newOverarchingGoal }); + }, + setCurrentOverarchingGoals(newOverarchingGoals: OverarchingGoal[]) { + set({ currentOverarchingGoals: newOverarchingGoals }); + }, + resetOverarchingGoalState(): void { + set(defaultInitState); + }, + }), + { + name: "overarching-goal-state-store", + storage: createJSONStorage(() => sessionStorage), + } + ) + ) + ); + return oagStateStore; +};