Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the OverarchingGoal.title to the CoachingSessionSelector component #60

Merged
merged 1 commit into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading