Skip to content

Commit

Permalink
Merge pull request #60 from refactor-group/display_overarching_goal_i…
Browse files Browse the repository at this point in the history
…n_coaching_session_selector
  • Loading branch information
jhodapp authored Dec 30, 2024
2 parents 5737e20 + 8067381 commit e192903
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 46 deletions.
146 changes: 111 additions & 35 deletions src/components/ui/coaching-session-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <div>Loading...</div>;
if (isError) return <div>Error loading coaching sessions</div>;
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 <div>Loading...</div>;
if (isErrorSessions) return <div>Error loading coaching sessions</div>;
if (!coachingSessions?.length) return <div>No coaching sessions found</div>;

return (
Expand All @@ -62,13 +86,25 @@ function CoachingSessionsSelectItems({
.filter(
(session) => getDateTimeFromString(session.date) < DateTime.now()
)
.map((session) => (
<SelectItem value={session.id} key={session.id}>
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</SelectItem>
))}
.map((session) => {
const sessionIndex = coachingSessions.findIndex(
(s) => s.id === session.id
);
return (
<SelectItem value={session.id} key={session.id}>
<div className="flex flex-col w-full">
<span className="text-left truncate overflow-hidden">
{goals[sessionIndex]?.[0]?.title || "No goal set"}
</span>
<span className="text-sm text-gray-400">
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</span>
</div>
</SelectItem>
);
})}
</SelectGroup>
)}
{coachingSessions.some(
Expand All @@ -80,13 +116,25 @@ function CoachingSessionsSelectItems({
.filter(
(session) => getDateTimeFromString(session.date) >= DateTime.now()
)
.map((session) => (
<SelectItem value={session.id} key={session.id}>
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</SelectItem>
))}
.map((session) => {
const sessionIndex = coachingSessions.findIndex(
(s) => s.id === session.id
);
return (
<SelectItem value={session.id} key={session.id}>
<div className="flex flex-col w-full">
<span className="text-left truncate overflow-hidden">
{goals[sessionIndex]?.[0]?.title || "No goal set"}
</span>
<span className="text-sm text-gray-400">
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</span>
</div>
</SelectItem>
);
})}
</SelectGroup>
)}
</>
Expand All @@ -105,23 +153,51 @@ export default function CoachingSessionSelector({
getCurrentCoachingSession,
} = useCoachingSessionStateStore((state) => state);

const [currentGoal, setCurrentGoal] = useState<OverarchingGoal | undefined>();
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
)}
</>
<div className="flex flex-col w-[28rem]">
<span className="truncate overflow-hidden text-left">
{currentGoal?.title || "No goal set"}
</span>
<span className="text-sm text-gray-500 text-left">
{getDateTimeFromString(currentSession.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</span>
</div>
) : undefined;

return (
Expand Down
96 changes: 95 additions & 1 deletion src/lib/api/overarching-goals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OverarchingGoal[]> =>
axios
.get<ApiResponseOverarchingGoals>(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<OverarchingGoal[]>(
[
`${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<OverarchingGoal> =>
axios
.get<ApiResponseOverarchingGoal>(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<OverarchingGoal>(
`${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
Expand Down
47 changes: 47 additions & 0 deletions src/lib/providers/overarching-goal-state-store-provider.tsx
Original file line number Diff line number Diff line change
@@ -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<StoreApi<OverarchingGoalStateStore> | null>(null);

export interface OverarchingGoalStateStoreProviderProps {
children: ReactNode;
}

export const OverarchingGoalStateStoreProvider = ({
children,
}: OverarchingGoalStateStoreProviderProps) => {
const storeRef = useRef<StoreApi<OverarchingGoalStateStore>>(undefined);
if (!storeRef.current) {
storeRef.current = createOverarchingGoalStateStore();
}

return (
<OverarchingGoalStateStoreContext.Provider value={storeRef.current}>
{children}
</OverarchingGoalStateStoreContext.Provider>
);
};

export const useOverarchingGoalStateStore = <T,>(
selector: (store: OverarchingGoalStateStore) => T
): T => {
const oagStateStoreContext = useContext(OverarchingGoalStateStoreContext);

if (!oagStateStoreContext) {
throw new Error(
`useOverarchingGoalStateStore must be used within OverarchingGoalStateStoreProvider`
);
}

return useStore(oagStateStoreContext, selector);
};
21 changes: 12 additions & 9 deletions src/lib/providers/root-layout-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,15 +25,17 @@ export function RootLayoutProviders({
<OrganizationStateStoreProvider>
<CoachingRelationshipStateStoreProvider>
<CoachingSessionStateStoreProvider>
<SWRConfig
value={{
revalidateIfStale: true,
focusThrottleInterval: 10000,
provider: () => new Map(),
}}
>
{children}
</SWRConfig>
<OverarchingGoalStateStoreProvider>
<SWRConfig
value={{
revalidateIfStale: true,
focusThrottleInterval: 10000,
provider: () => new Map(),
}}
>
{children}
</SWRConfig>
</OverarchingGoalStateStoreProvider>
</CoachingSessionStateStoreProvider>
</CoachingRelationshipStateStoreProvider>
</OrganizationStateStoreProvider>
Expand Down
Loading

0 comments on commit e192903

Please sign in to comment.