Skip to content

Commit e192903

Browse files
authored
Merge pull request #60 from refactor-group/display_overarching_goal_in_coaching_session_selector
2 parents 5737e20 + 8067381 commit e192903

File tree

6 files changed

+348
-46
lines changed

6 files changed

+348
-46
lines changed

src/components/ui/coaching-session-selector.tsx

Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import {
1212
} from "@/components/ui/select";
1313
import { getDateTimeFromString, Id } from "@/types/general";
1414
import { useCoachingSessions } from "@/lib/api/coaching-sessions";
15-
import { useEffect } from "react";
15+
import { useEffect, useState } from "react";
1616
import { DateTime } from "ts-luxon";
1717
import { useCoachingSessionStateStore } from "@/lib/providers/coaching-session-state-store-provider";
18+
import { fetchOverarchingGoalsByCoachingSessionId } from "@/lib/api/overarching-goals";
19+
import { OverarchingGoal } from "@/types/overarching-goal";
1820

1921
interface CoachingSessionsSelectorProps extends PopoverProps {
2022
/// The CoachingRelationship Id for which to get a list of associated CoachingSessions
@@ -30,25 +32,47 @@ function CoachingSessionsSelectItems({
3032
}: {
3133
relationshipId: Id;
3234
}) {
33-
const { coachingSessions, isLoading, isError } =
34-
useCoachingSessions(relationshipId);
35+
const {
36+
coachingSessions,
37+
isLoading: isLoadingSessions,
38+
isError: isErrorSessions,
39+
} = useCoachingSessions(relationshipId);
40+
3541
const { setCurrentCoachingSessions } = useCoachingSessionStateStore(
3642
(state) => state
3743
);
44+
const [goals, setGoals] = useState<(OverarchingGoal[] | undefined)[]>([]);
45+
const [isLoadingGoals, setIsLoadingGoals] = useState(false);
3846

39-
console.debug(`coachingSessions: ${JSON.stringify(coachingSessions)}`);
40-
41-
// Be sure to cache the list of current coaching sessions in the CoachingSessionStateStore
4247
useEffect(() => {
4348
if (!coachingSessions.length) return;
44-
console.debug(
45-
`coachingSessions (useEffect): ${JSON.stringify(coachingSessions)}`
46-
);
4749
setCurrentCoachingSessions(coachingSessions);
4850
}, [coachingSessions]);
4951

50-
if (isLoading) return <div>Loading...</div>;
51-
if (isError) return <div>Error loading coaching sessions</div>;
52+
useEffect(() => {
53+
const fetchGoals = async () => {
54+
setIsLoadingGoals(true);
55+
try {
56+
const sessionIds = coachingSessions?.map((session) => session.id) || [];
57+
const goalsPromises = sessionIds.map((id) =>
58+
fetchOverarchingGoalsByCoachingSessionId(id)
59+
);
60+
const fetchedGoals = await Promise.all(goalsPromises);
61+
setGoals(fetchedGoals);
62+
} catch (error) {
63+
console.error("Error fetching goals:", error);
64+
} finally {
65+
setIsLoadingGoals(false);
66+
}
67+
};
68+
69+
if (coachingSessions?.length) {
70+
fetchGoals();
71+
}
72+
}, [coachingSessions]);
73+
74+
if (isLoadingSessions || isLoadingGoals) return <div>Loading...</div>;
75+
if (isErrorSessions) return <div>Error loading coaching sessions</div>;
5276
if (!coachingSessions?.length) return <div>No coaching sessions found</div>;
5377

5478
return (
@@ -62,13 +86,25 @@ function CoachingSessionsSelectItems({
6286
.filter(
6387
(session) => getDateTimeFromString(session.date) < DateTime.now()
6488
)
65-
.map((session) => (
66-
<SelectItem value={session.id} key={session.id}>
67-
{getDateTimeFromString(session.date).toLocaleString(
68-
DateTime.DATETIME_FULL
69-
)}
70-
</SelectItem>
71-
))}
89+
.map((session) => {
90+
const sessionIndex = coachingSessions.findIndex(
91+
(s) => s.id === session.id
92+
);
93+
return (
94+
<SelectItem value={session.id} key={session.id}>
95+
<div className="flex flex-col w-full">
96+
<span className="text-left truncate overflow-hidden">
97+
{goals[sessionIndex]?.[0]?.title || "No goal set"}
98+
</span>
99+
<span className="text-sm text-gray-400">
100+
{getDateTimeFromString(session.date).toLocaleString(
101+
DateTime.DATETIME_FULL
102+
)}
103+
</span>
104+
</div>
105+
</SelectItem>
106+
);
107+
})}
72108
</SelectGroup>
73109
)}
74110
{coachingSessions.some(
@@ -80,13 +116,25 @@ function CoachingSessionsSelectItems({
80116
.filter(
81117
(session) => getDateTimeFromString(session.date) >= DateTime.now()
82118
)
83-
.map((session) => (
84-
<SelectItem value={session.id} key={session.id}>
85-
{getDateTimeFromString(session.date).toLocaleString(
86-
DateTime.DATETIME_FULL
87-
)}
88-
</SelectItem>
89-
))}
119+
.map((session) => {
120+
const sessionIndex = coachingSessions.findIndex(
121+
(s) => s.id === session.id
122+
);
123+
return (
124+
<SelectItem value={session.id} key={session.id}>
125+
<div className="flex flex-col w-full">
126+
<span className="text-left truncate overflow-hidden">
127+
{goals[sessionIndex]?.[0]?.title || "No goal set"}
128+
</span>
129+
<span className="text-sm text-gray-400">
130+
{getDateTimeFromString(session.date).toLocaleString(
131+
DateTime.DATETIME_FULL
132+
)}
133+
</span>
134+
</div>
135+
</SelectItem>
136+
);
137+
})}
90138
</SelectGroup>
91139
)}
92140
</>
@@ -105,23 +153,51 @@ export default function CoachingSessionSelector({
105153
getCurrentCoachingSession,
106154
} = useCoachingSessionStateStore((state) => state);
107155

156+
const [currentGoal, setCurrentGoal] = useState<OverarchingGoal | undefined>();
157+
const [isLoadingGoal, setIsLoadingGoal] = useState(false);
158+
159+
const currentSession = currentCoachingSessionId
160+
? getCurrentCoachingSession(currentCoachingSessionId)
161+
: null;
162+
163+
useEffect(() => {
164+
const fetchGoal = async () => {
165+
if (!currentCoachingSessionId) return;
166+
167+
setIsLoadingGoal(true);
168+
try {
169+
const goals = await fetchOverarchingGoalsByCoachingSessionId(
170+
currentCoachingSessionId
171+
);
172+
setCurrentGoal(goals[0]);
173+
} catch (error) {
174+
console.error("Error fetching goal:", error);
175+
} finally {
176+
setIsLoadingGoal(false);
177+
}
178+
};
179+
180+
fetchGoal();
181+
}, [currentCoachingSessionId]);
182+
108183
const handleSetCoachingSession = (coachingSessionId: Id) => {
109184
setCurrentCoachingSessionId(coachingSessionId);
110185
if (onSelect) {
111-
onSelect(relationshipId);
186+
onSelect(coachingSessionId);
112187
}
113188
};
114189

115-
const currentSession = currentCoachingSessionId
116-
? getCurrentCoachingSession(currentCoachingSessionId)
117-
: null;
118-
119190
const displayValue = currentSession ? (
120-
<>
121-
{getDateTimeFromString(currentSession.date).toLocaleString(
122-
DateTime.DATETIME_FULL
123-
)}
124-
</>
191+
<div className="flex flex-col w-[28rem]">
192+
<span className="truncate overflow-hidden text-left">
193+
{currentGoal?.title || "No goal set"}
194+
</span>
195+
<span className="text-sm text-gray-500 text-left">
196+
{getDateTimeFromString(currentSession.date).toLocaleString(
197+
DateTime.DATETIME_FULL
198+
)}
199+
</span>
200+
</div>
125201
) : undefined;
126202

127203
return (

src/lib/api/overarching-goals.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,102 @@ import {
88
parseOverarchingGoal,
99
} from "@/types/overarching-goal";
1010
import { ItemStatus, Id } from "@/types/general";
11-
import { AxiosError, AxiosResponse } from "axios";
11+
import axios, { AxiosError, AxiosResponse } from "axios";
1212
import { siteConfig } from "@/site.config";
13+
import useSWR, { useSWRConfig } from "swr";
14+
15+
interface ApiResponseOverarchingGoals {
16+
status_code: number;
17+
data: OverarchingGoal[];
18+
}
19+
20+
// Fetch all OverarchingGoals associated with a particular User
21+
const fetcherOverarchingGoals = async (
22+
url: string,
23+
coachingSessionId: Id
24+
): Promise<OverarchingGoal[]> =>
25+
axios
26+
.get<ApiResponseOverarchingGoals>(url, {
27+
params: {
28+
coaching_session_id: coachingSessionId,
29+
},
30+
withCredentials: true,
31+
timeout: 5000,
32+
headers: {
33+
"X-Version": siteConfig.env.backendApiVersion,
34+
},
35+
})
36+
.then((res) => res.data.data);
37+
38+
/// A hook to retrieve all OverarchingGoals associated with coachingSessionId
39+
export function useOverarchingGoals(coachingSessionId: Id) {
40+
const { data, error, isLoading } = useSWR<OverarchingGoal[]>(
41+
[
42+
`${siteConfig.env.backendServiceURL}/overarching_goals`,
43+
coachingSessionId,
44+
],
45+
([url, _token]) => fetcherOverarchingGoals(url, coachingSessionId)
46+
);
47+
const swrConfig = useSWRConfig();
48+
console.debug(`swrConfig: ${JSON.stringify(swrConfig)}`);
49+
50+
console.debug(`overarchingGoals data: ${JSON.stringify(data)}`);
51+
52+
return {
53+
overarchingGoals: Array.isArray(data) ? data : [],
54+
isLoading,
55+
isError: error,
56+
};
57+
}
58+
59+
/// A hook to retrieve a single OverarchingGoal by a coachingSessionId
60+
export function useOverarchingGoalByCoachingSessionId(coachingSessionId: Id) {
61+
const { overarchingGoals, isLoading, isError } =
62+
useOverarchingGoals(coachingSessionId);
63+
64+
return {
65+
overarchingGoal: overarchingGoals.length
66+
? overarchingGoals[0]
67+
: defaultOverarchingGoal(),
68+
isLoading,
69+
isError: isError,
70+
};
71+
}
72+
73+
interface ApiResponseOverarchingGoal {
74+
status_code: number;
75+
data: OverarchingGoal;
76+
}
77+
78+
// Fetcher for retrieving a single OverarchingGoal by its Id
79+
const fetcherOverarchingGoal = async (url: string): Promise<OverarchingGoal> =>
80+
axios
81+
.get<ApiResponseOverarchingGoal>(url, {
82+
withCredentials: true,
83+
timeout: 5000,
84+
headers: {
85+
"X-Version": siteConfig.env.backendApiVersion,
86+
},
87+
})
88+
.then((res) => res.data.data);
89+
90+
/// A hook to retrieve a single OverarchingGoal by its Id
91+
export function useOverarchingGoal(overarchingGoalId: Id) {
92+
const { data, error, isLoading } = useSWR<OverarchingGoal>(
93+
`${siteConfig.env.backendServiceURL}/overarching_goals/${overarchingGoalId}`,
94+
fetcherOverarchingGoal
95+
);
96+
const swrConfig = useSWRConfig();
97+
console.debug(`swrConfig: ${JSON.stringify(swrConfig)}`);
98+
99+
console.debug(`overarchingGoal data: ${JSON.stringify(data)}`);
100+
101+
return {
102+
overarchingGoal: data || defaultOverarchingGoal(),
103+
isLoading,
104+
isError: error,
105+
};
106+
}
13107

14108
export const fetchOverarchingGoalsByCoachingSessionId = async (
15109
coachingSessionId: Id
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// The purpose of this provider is to provide compatibility with
2+
// Next.js re-rendering and component caching
3+
"use client";
4+
5+
import { type ReactNode, createContext, useRef, useContext } from "react";
6+
import { type StoreApi, useStore } from "zustand";
7+
8+
import {
9+
type OverarchingGoalStateStore,
10+
createOverarchingGoalStateStore,
11+
} from "@/lib/stores/overarching-goal-state-store";
12+
13+
export const OverarchingGoalStateStoreContext =
14+
createContext<StoreApi<OverarchingGoalStateStore> | null>(null);
15+
16+
export interface OverarchingGoalStateStoreProviderProps {
17+
children: ReactNode;
18+
}
19+
20+
export const OverarchingGoalStateStoreProvider = ({
21+
children,
22+
}: OverarchingGoalStateStoreProviderProps) => {
23+
const storeRef = useRef<StoreApi<OverarchingGoalStateStore>>(undefined);
24+
if (!storeRef.current) {
25+
storeRef.current = createOverarchingGoalStateStore();
26+
}
27+
28+
return (
29+
<OverarchingGoalStateStoreContext.Provider value={storeRef.current}>
30+
{children}
31+
</OverarchingGoalStateStoreContext.Provider>
32+
);
33+
};
34+
35+
export const useOverarchingGoalStateStore = <T,>(
36+
selector: (store: OverarchingGoalStateStore) => T
37+
): T => {
38+
const oagStateStoreContext = useContext(OverarchingGoalStateStoreContext);
39+
40+
if (!oagStateStoreContext) {
41+
throw new Error(
42+
`useOverarchingGoalStateStore must be used within OverarchingGoalStateStoreProvider`
43+
);
44+
}
45+
46+
return useStore(oagStateStoreContext, selector);
47+
};

src/lib/providers/root-layout-providers.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SWRConfig } from "swr";
66
import { OrganizationStateStoreProvider } from "./organization-state-store-provider";
77
import { CoachingRelationshipStateStoreProvider } from "./coaching-relationship-state-store-provider";
88
import { CoachingSessionStateStoreProvider } from "./coaching-session-state-store-provider";
9+
import { OverarchingGoalStateStoreProvider } from "./overarching-goal-state-store-provider";
910

1011
export function RootLayoutProviders({
1112
children,
@@ -24,15 +25,17 @@ export function RootLayoutProviders({
2425
<OrganizationStateStoreProvider>
2526
<CoachingRelationshipStateStoreProvider>
2627
<CoachingSessionStateStoreProvider>
27-
<SWRConfig
28-
value={{
29-
revalidateIfStale: true,
30-
focusThrottleInterval: 10000,
31-
provider: () => new Map(),
32-
}}
33-
>
34-
{children}
35-
</SWRConfig>
28+
<OverarchingGoalStateStoreProvider>
29+
<SWRConfig
30+
value={{
31+
revalidateIfStale: true,
32+
focusThrottleInterval: 10000,
33+
provider: () => new Map(),
34+
}}
35+
>
36+
{children}
37+
</SWRConfig>
38+
</OverarchingGoalStateStoreProvider>
3639
</CoachingSessionStateStoreProvider>
3740
</CoachingRelationshipStateStoreProvider>
3841
</OrganizationStateStoreProvider>

0 commit comments

Comments
 (0)