From 0918f2dc388be765fe561b9de61d9802618cfcc1 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Thu, 26 Sep 2024 16:17:49 -0500 Subject: [PATCH 1/2] Actions are fully working. Can list/add/update/delete. --- src/app/coaching-sessions/[id]/page.tsx | 64 ++- .../ui/coaching-sessions/actions-list.tsx | 463 ++++++++++++++++++ .../ui/coaching-sessions/agreements-list.tsx | 2 +- src/lib/api/actions.ts | 210 ++++++++ src/lib/api/agreements.ts | 20 +- src/types/action.ts | 97 ++++ src/types/agreement.ts | 4 +- src/types/general.ts | 36 +- 8 files changed, 873 insertions(+), 23 deletions(-) create mode 100644 src/components/ui/coaching-sessions/actions-list.tsx create mode 100644 src/lib/api/actions.ts create mode 100644 src/types/action.ts diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index dca1075..ed980da 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -45,17 +45,21 @@ import { fetchNotesByCoachingSessionId, updateNote, } from "@/lib/api/notes"; -import { Note, noteToString } from "@/types/note"; +import { noteToString } from "@/types/note"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; -import { Id } from "@/types/general"; +import { ActionStatus, Id } from "@/types/general"; import { AgreementsList } from "@/components/ui/coaching-sessions/agreements-list"; -import { Agreement, agreementToString } from "@/types/agreement"; +import { Agreement } from "@/types/agreement"; import { createAgreement, deleteAgreement, updateAgreement, } from "@/lib/api/agreements"; import { siteConfig } from "@/site.config"; +import { ActionsList } from "@/components/ui/coaching-sessions/actions-list"; +import { Action } from "@/types/action"; +import { createAction, deleteAction, updateAction } from "@/lib/api/actions"; +import { DateTime } from "ts-luxon"; // export const metadata: Metadata = { // title: "Coaching Session", @@ -133,6 +137,49 @@ export default function CoachingSessionsPage() { }); }; + const handleActionAdded = ( + body: string, + status: ActionStatus, + dueBy: DateTime + ): Promise => { + // Calls the backend endpoint that creates and stores a full Action entity + return createAction(coachingSessionId, body, status, dueBy) + .then((action) => { + return action; + }) + .catch((err) => { + console.error("Failed to create new Action: " + err); + throw err; + }); + }; + + const handleActionEdited = ( + id: Id, + body: string, + status: ActionStatus, + dueBy: DateTime + ): Promise => { + return updateAction(id, coachingSessionId, body, status, dueBy) + .then((action) => { + return action; + }) + .catch((err) => { + console.error("Failed to update Action (id: " + id + "): " + err); + throw err; + }); + }; + + const handleActionDeleted = (id: Id): Promise => { + return deleteAction(id) + .then((action) => { + return action; + }) + .catch((err) => { + console.error("Failed to update Action (id: " + id + "): " + err); + throw err; + }); + }; + const handleInputChange = (value: string) => { setNote(value); @@ -252,7 +299,16 @@ export default function CoachingSessionsPage() { - {/*
Actions
*/} +
+ +
{/*
Program
*/} diff --git a/src/components/ui/coaching-sessions/actions-list.tsx b/src/components/ui/coaching-sessions/actions-list.tsx new file mode 100644 index 0000000..25d5302 --- /dev/null +++ b/src/components/ui/coaching-sessions/actions-list.tsx @@ -0,0 +1,463 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, ArrowUpDown, Save, CalendarClock } from "lucide-react"; +import { + ActionStatus, + actionStatusToString, + Id, + stringToActionStatus, +} from "@/types/general"; +import { fetchActionsByCoachingSessionId } from "@/lib/api/actions"; +import { DateTime } from "ts-luxon"; +import { siteConfig } from "@/site.config"; +import { Action, actionToString } from "@/types/action"; +import { cn } from "@/lib/utils"; +import { format } from "date-fns"; + +const ActionsList: React.FC<{ + coachingSessionId: Id; + userId: Id; + locale: string | "us"; + onActionAdded: ( + body: string, + status: ActionStatus, + dueBy: DateTime + ) => Promise; + onActionEdited: ( + id: Id, + body: string, + status: ActionStatus, + dueBy: DateTime + ) => Promise; + onActionDeleted: (id: Id) => Promise; +}> = ({ + coachingSessionId, + userId, + locale, + onActionAdded: onActionAdded, + onActionEdited: onActionEdited, + onActionDeleted: onActionDeleted, +}) => { + enum ActionSortField { + Body = "body", + DueBy = "due_by", + Status = "status", + CreatedAt = "created_at", + UpdatedAt = "updated_at", + } + + const [actions, setActions] = useState([]); + const [newBody, setNewBody] = useState(""); + const [newStatus, setNewStatus] = useState( + ActionStatus.NotStarted + ); + const [newDueBy, setNewDueBy] = useState(DateTime.now()); + const [editingId, setEditingId] = useState(null); + const [editBody, setEditBody] = useState(""); + const [editStatus, setEditStatus] = useState( + ActionStatus.NotStarted + ); + const [editDueBy, setEditDueBy] = useState(DateTime.now()); + const [sortColumn, setSortColumn] = useState( + ActionSortField.CreatedAt + ); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); + + const addAction = () => { + if (newBody.trim() === "") return; + + // Call the external onActionAdded handler function which should + // store this action in the backend database + onActionAdded(newBody, newStatus, newDueBy) + .then((action) => { + console.trace( + "Newly created Action (onActionAdded): " + actionToString(action) + ); + setActions((prevActions) => [...prevActions, action]); + }) + .catch((err) => { + console.error("Failed to create new Action: " + err); + throw err; + }); + + setNewBody(""); + setNewStatus(ActionStatus.NotStarted); + setNewDueBy(DateTime.now()); + }; + + const updateAction = async ( + id: Id, + newBody: string, + newStatus: ActionStatus, + newDueBy: DateTime + ) => { + const body = newBody.trim(); + if (body === "") return; + + try { + const updatedActions = await Promise.all( + actions.map(async (action) => { + if (action.id === id) { + // Call the external onActionEdited handler function which should + // update the stored version of this action in the backend database + action = await onActionEdited(id, body, newStatus, newDueBy) + .then((updatedAction) => { + console.trace( + "Updated Action (onActionUpdated): " + + actionToString(updatedAction) + ); + + return updatedAction; + }) + .catch((err) => { + console.error( + "Failed to update Action (id: " + id + "): " + err + ); + throw err; + }); + } + return action; + }) + ); + + setActions(updatedActions); + setEditingId(null); + setEditBody(""); + setEditStatus(ActionStatus.NotStarted); + setEditDueBy(DateTime.now()); + } catch (err) { + console.error("Failed to update Action (id: " + id + "): ", err); + throw err; + } + }; + + const deleteAction = (id: Id) => { + if (id === "") return; + + // Call the external onActionDeleted handler function which should + // delete this action from the backend database + onActionDeleted(id) + .then((action) => { + console.trace( + "Deleted Action (onActionDeleted): " + actionToString(action) + ); + setActions(actions.filter((action) => action.id !== id)); + }) + .catch((err) => { + console.error("Failed to Action (id: " + id + "): " + err); + throw err; + }); + }; + + const sortActions = (column: keyof Action) => { + if (column === sortColumn) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + setSortColumn(column); + setSortDirection("asc"); + } + }; + + const sortedActions = [...actions].sort((a, b) => { + const aValue = a[sortColumn as keyof Action]!; + const bValue = b[sortColumn as keyof Action]!; + + if (aValue < bValue) return sortDirection === "asc" ? -1 : 1; + if (aValue > bValue) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + + useEffect(() => { + async function loadActions() { + if (!coachingSessionId) return; + + await fetchActionsByCoachingSessionId(coachingSessionId) + .then((actions) => { + console.debug("setActions: " + JSON.stringify(actions)); + setActions(actions); + }) + .catch(([err]) => { + console.error("Failed to fetch Actions: " + err); + }); + } + loadActions(); + }, [coachingSessionId]); + + return ( +
+
+
+ + + + sortActions(ActionSortField.Body)} + className={`cursor-pointer ${ + sortColumn === ActionSortField.Body + ? "underline" + : "no-underline" + }`} + > + Action + + sortActions(ActionSortField.Status)} + className={`cursor-pointer hidden sm:table-cell ${ + sortColumn === ActionSortField.Status + ? "underline" + : "no-underline" + }`} + > + Status + + sortActions(ActionSortField.DueBy)} + className={`cursor-pointer hidden md:table-cell ${ + sortColumn === ActionSortField.DueBy + ? "underline" + : "no-underline" + }`} + > + Due By + + + sortActions(ActionSortField.CreatedAt)} + className={`cursor-pointer hidden md:table-cell ${ + sortColumn === ActionSortField.CreatedAt + ? "underline" + : "no-underline" + }`} + > + Created + + + + + + + + {sortedActions.map((action) => ( + + + {/* Edit an existing action */} + {editingId === action.id ? ( +
+ setEditBody(e.target.value)} + onKeyPress={(e) => + e.key === "Enter" && + updateAction( + action.id, + editBody, + editStatus, + editDueBy + ) + } + className="flex-grow" + /> + + + + + + + + setEditDueBy( + date + ? DateTime.fromJSDate(date) + : DateTime.now() + ) + } + initialFocus + /> + + + + {/* TODO: add a circular X button to cancel updating */} +
+ ) : ( + action.body + )} +
+ + {actionStatusToString(action.status)} + + + {action.due_by + .setLocale(siteConfig.locale) + .toLocaleString(DateTime.DATE_MED)} + + + {action.created_at + .setLocale(siteConfig.locale) + .toLocaleString(DateTime.DATETIME_MED)} + + + {action.updated_at + .setLocale(siteConfig.locale) + .toLocaleString(DateTime.DATETIME_MED)} + + + + + + + + { + setEditingId(action.id); + setEditBody(action.body ?? ""); + setEditStatus(action.status); + setEditDueBy(action.due_by); + }} + > + Edit + + deleteAction(action.id)} + > + Delete + + + + +
+ ))} +
+
+
+ {/* Create a new action */} +
+ setNewBody(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && addAction()} + placeholder="Enter new action" + className="flex-grow" + /> + + + + + + + setNewDueBy(date ? DateTime.fromJSDate(date) : DateTime.now()) + } + initialFocus + /> + + + +
+
+
+ ); +}; + +export { ActionsList }; diff --git a/src/components/ui/coaching-sessions/agreements-list.tsx b/src/components/ui/coaching-sessions/agreements-list.tsx index 94897b5..97051b1 100644 --- a/src/components/ui/coaching-sessions/agreements-list.tsx +++ b/src/components/ui/coaching-sessions/agreements-list.tsx @@ -190,7 +190,7 @@ const AgreementsList: React.FC<{ Agreement sortAgreements("created_at")} + onClick={() => sortAgreements(AgreementSortField.CreatedAt)} className={`cursor-pointer hidden sm:table-cell ${ sortColumn === AgreementSortField.CreatedAt ? "underline" diff --git a/src/lib/api/actions.ts b/src/lib/api/actions.ts new file mode 100644 index 0000000..d2d64f6 --- /dev/null +++ b/src/lib/api/actions.ts @@ -0,0 +1,210 @@ +// Interacts with the action endpoints + +import { Action, actionToString, defaultAction, isAction, isActionArray, parseAction } from "@/types/action"; +import { ActionStatus, Id } from "@/types/general"; +import { AxiosError, AxiosResponse } from "axios"; +import { DateTime } from "ts-luxon"; + +export const fetchActionsByCoachingSessionId = async ( + coachingSessionId: Id + ): Promise => { + const axios = require("axios"); + + var actions: Action[] = []; + var err: string = ""; + + const data = await axios + .get(`http://localhost:4000/actions`, { + params: { + coaching_session_id: coachingSessionId, + }, + withCredentials: true, + setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend + headers: { + "X-Version": "0.0.1", + }, + }) + .then(function (response: AxiosResponse) { + // handle success + var actions_data = response.data.data; + if (isActionArray(actions_data)) { + actions_data.forEach((actions_data: any) => { + actions.push(parseAction(actions_data)) + }); + } + }) + .catch(function (error: AxiosError) { + // handle error + if (error.response?.status == 401) { + err = "Retrieval of Actions failed: unauthorized."; + } else if (error.response?.status == 404) { + err = "Retrieval of Actions failed: Actions by coaching session Id (" + coachingSessionId + ") not found."; + } else { + err = + `Retrieval of Actions by coaching session Id (` + coachingSessionId + `) failed.`; + } + }); + + if (err) { + console.error(err); + throw err; + } + + return actions; + }; + +export const createAction = async ( + coaching_session_id: Id, + body: string, + status: ActionStatus, + due_by: DateTime + ): Promise => { + const axios = require("axios"); + + const newActionJson = { + coaching_session_id: coaching_session_id, + body: body, + due_by: due_by, + status: status, + }; + console.debug("newActionJson: " + JSON.stringify(newActionJson)); + // A full real action to be returned from the backend with the same body + var createdAction: Action = defaultAction(); + var err: string = ""; + + const data = await axios + .post(`http://localhost:4000/actions`, newActionJson, { + withCredentials: true, + setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend + headers: { + "X-Version": "0.0.1", + "Content-Type": "application/json", + }, + }) + .then(function (response: AxiosResponse) { + // handle success + const action_data = response.data.data; + if (isAction(action_data)) { + createdAction = parseAction(action_data); + } + }) + .catch(function (error: AxiosError) { + // handle error + console.error(error.response?.status); + if (error.response?.status == 401) { + err = "Creation of Action failed: unauthorized."; + } else if (error.response?.status == 500) { + err = "Creation of Action failed: internal server error."; + } else { + err = `Creation of new Action failed.`; + } + } + ); + + if (err) { + console.error(err); + throw err; + } + + return createdAction; + }; + + export const updateAction = async ( + id: Id, + coaching_session_id: Id, + body: string, + status: ActionStatus, + due_by: DateTime + ): Promise => { + const axios = require("axios"); + + var updatedAction: Action = defaultAction(); + var err: string = ""; + + const newActionJson = { + coaching_session_id: coaching_session_id, + body: body, + status: status, + due_by: due_by, + }; + console.debug("newActionJson: " + JSON.stringify(newActionJson)); + + const data = await axios + .put(`http://localhost:4000/actions/${id}`, newActionJson, { + withCredentials: true, + setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend + headers: { + "X-Version": "0.0.1", + "Content-Type": "application/json", + }, + }) + .then(function (response: AxiosResponse) { + // handle success + const action_data = response.data.data; + if (isAction(action_data)) { + updatedAction = parseAction(action_data); + } + }) + .catch(function (error: AxiosError) { + // handle error + console.error(error.response?.status); + if (error.response?.status == 401) { + err = "Update of Action failed: unauthorized."; + } else if (error.response?.status == 500) { + err = "Update of Action failed: internal server error."; + } else { + err = `Update of new Action failed.`; + } + }); + + if (err) { + console.error(err); + throw err; + } + + return updatedAction; + }; + + export const deleteAction = async ( + id: Id + ): Promise => { + const axios = require("axios"); + + var deletedAction: Action = defaultAction(); + var err: string = ""; + + const data = await axios + .delete(`http://localhost:4000/actions/${id}`, { + withCredentials: true, + setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend + headers: { + "X-Version": "0.0.1", + "Content-Type": "application/json", + }, + }) + .then(function (response: AxiosResponse) { + // handle success + const action_data = response.data.data; + if (isAction(action_data)) { + deletedAction = parseAction(action_data); + } + }) + .catch(function (error: AxiosError) { + // handle error + console.error(error.response?.status); + if (error.response?.status == 401) { + err = "Deletion of Action failed: unauthorized."; + } else if (error.response?.status == 500) { + err = "Deletion of Action failed: internal server error."; + } else { + err = `Deletion of Action failed.`; + } + }); + + if (err) { + console.error(err); + throw err; + } + + return deletedAction; + }; diff --git a/src/lib/api/agreements.ts b/src/lib/api/agreements.ts index 96d93de..3389066 100644 --- a/src/lib/api/agreements.ts +++ b/src/lib/api/agreements.ts @@ -1,9 +1,8 @@ // Interacts with the agreement endpoints import { Agreement, defaultAgreement, isAgreement, isAgreementArray, parseAgreement } from "@/types/agreement"; -import { CompletionStatus, Id } from "@/types/general"; +import { Id } from "@/types/general"; import { AxiosError, AxiosResponse } from "axios"; -import { DateTime } from "ts-luxon"; export const fetchAgreementsByCoachingSessionId = async ( coachingSessionId: Id @@ -64,8 +63,6 @@ export const createAgreement = async ( coaching_session_id: coaching_session_id, user_id: user_id, body: body, - status: CompletionStatus.NotStarted, - status_changed_at: DateTime.now() }; console.debug("newAgreementJson: " + JSON.stringify(newAgreementJson)); // A full real note to be returned from the backend with the same body @@ -83,9 +80,9 @@ export const createAgreement = async ( }) .then(function (response: AxiosResponse) { // handle success - const agreementStr = response.data.data; - if (isAgreement(agreementStr)) { - createdAgreement = parseAgreement(agreementStr); + const agreement_data = response.data.data; + if (isAgreement(agreement_data)) { + createdAgreement = parseAgreement(agreement_data); } }) .catch(function (error: AxiosError) { @@ -96,7 +93,7 @@ export const createAgreement = async ( } else if (error.response?.status == 500) { err = "Creation of Agreement failed: internal server error."; } else { - err = `Creation of new Agreement failed.`; + err = `Creation of Agreement failed.`; } } ); @@ -181,9 +178,10 @@ export const createAgreement = async ( }) .then(function (response: AxiosResponse) { // handle success - if (isAgreement(response.data.data)) { - deletedAgreement = response.data.data; - } + const agreement_data = response.data.data; + if (isAgreement(agreement_data)) { + deletedAgreement = parseAgreement(agreement_data); + } }) .catch(function (error: AxiosError) { // handle error diff --git a/src/types/action.ts b/src/types/action.ts new file mode 100644 index 0000000..59db47d --- /dev/null +++ b/src/types/action.ts @@ -0,0 +1,97 @@ +import { DateTime } from "ts-luxon"; +import { ActionStatus, Id, SortOrder } from "@/types/general"; + +// This must always reflect the Rust struct on the backend +// entity::actions::Model +export interface Action { + id: Id; + coaching_session_id: Id, + body?: string, + user_id: Id, + status: ActionStatus, + status_changed_at: DateTime, + due_by: DateTime; + created_at: DateTime; + updated_at: DateTime; +} + +// The main purpose of having this parsing function is to be able to parse the +// returned DateTimeWithTimeZone (Rust type) string into something that ts-luxon +// will agree to work with internally. +export function parseAction(data: any): Action { + if (!isAction(data)) { + throw new Error('Invalid Action object data'); + } + return { + id: data.id, + coaching_session_id: data.coaching_session_id, + body: data.body, + user_id: data.user_id, + status: data.status, + status_changed_at: DateTime.fromISO(data.status_changed_at.toString()), + due_by: DateTime.fromISO(data.due_by.toString()), + created_at: DateTime.fromISO(data.created_at.toString()), + updated_at: DateTime.fromISO(data.updated_at.toString()), + }; +} + +export function isAction(value: unknown): value is Action { + if (!value || typeof value !== "object") { + return false; + } + const object = value as Record; + + return ( + (typeof object.id === "string" && + typeof object.coaching_session_id === "string" && + typeof object.user_id === "string" && + typeof object.status === "string" && + typeof object.status_changed_at === "string" && + typeof object.due_by === "string" && + typeof object.created_at === "string" && + typeof object.updated_at === "string") || + typeof object.body === "string" // body is optional + ); + } + +export function isActionArray(value: unknown): value is Action[] { + return Array.isArray(value) && value.every(isAction); +} + +export function sortActionArray(actions: Action[], order: SortOrder): Action[] { + if (order == SortOrder.Ascending) { + actions.sort((a, b) => + new Date(a.updated_at.toString()).getTime() - new Date(b.updated_at.toString()).getTime()); + } else if (order == SortOrder.Descending) { + actions.sort((a, b) => + new Date(b.updated_at.toString()).getTime() - new Date(a.updated_at.toString()).getTime()); + } + return actions; +} + +export function defaultAction(): Action { + const now = DateTime.now(); + return { + id: "", + coaching_session_id: "", + body: "", + user_id: "", + status: ActionStatus.NotStarted, + status_changed_at: now, + due_by: now, + created_at: now, + updated_at: now, + }; + } + + export function defaultActions(): Action[] { + return [defaultAction()]; + } + + export function actionToString(action: Action): string { + return JSON.stringify(action); + } + + export function actionsToString(actions: Action[]): string { + return JSON.stringify(actions); + } \ No newline at end of file diff --git a/src/types/agreement.ts b/src/types/agreement.ts index 2ec3980..b57be30 100644 --- a/src/types/agreement.ts +++ b/src/types/agreement.ts @@ -1,5 +1,5 @@ import { DateTime } from "ts-luxon"; -import { CompletionStatus, Id, SortOrder } from "@/types/general"; +import { Id, SortOrder } from "@/types/general"; // This must always reflect the Rust struct on the backend // entity::agreements::Model @@ -61,7 +61,7 @@ export function sortAgreementArray(agreements: Agreement[], order: SortOrder): A } export function defaultAgreement(): Agreement { - var now = DateTime.now(); + const now = DateTime.now(); return { id: "", coaching_session_id: "", diff --git a/src/types/general.ts b/src/types/general.ts index de69ecb..6cbb74b 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -8,9 +8,35 @@ export enum SortOrder { Descending = "descending" } -export enum CompletionStatus { - NotStarted = "not_started", - InProgress = "in_progress", - Completed = "completed", - WontDo = "wont_do" +export enum ActionStatus { + NotStarted = "NotStarted", + InProgress = "InProgress", + Completed = "Completed", + WontDo = "WontDo" +} + +export function stringToActionStatus(statusString: string): (ActionStatus) { + const status = statusString.trim(); + + if (status == "InProgress") { + return ActionStatus.InProgress; + } else if (status == "Completed") { + return ActionStatus.Completed; + } else if (status == "WontDo") { + return ActionStatus.WontDo; + } else { + return ActionStatus.NotStarted; + } +} + +export function actionStatusToString(actionStatus: ActionStatus): (string) { + if (actionStatus == "InProgress") { + return "In Progress"; + } else if (actionStatus == "Completed") { + return "Completed"; + } else if (actionStatus == "WontDo") { + return "Won't Do"; + } else { + return "Not Started"; + } } \ No newline at end of file From 1ce5b7d8036d082e0340dfde5abca6971df5dfe6 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Thu, 26 Sep 2024 16:41:11 -0500 Subject: [PATCH 2/2] Add missing "key" to Select so the source builds cleanly. --- src/components/ui/coaching-sessions/actions-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/coaching-sessions/actions-list.tsx b/src/components/ui/coaching-sessions/actions-list.tsx index 25d5302..0d03659 100644 --- a/src/components/ui/coaching-sessions/actions-list.tsx +++ b/src/components/ui/coaching-sessions/actions-list.tsx @@ -309,7 +309,7 @@ const ActionsList: React.FC<{ {Object.values(ActionStatus).map((s) => ( - + {actionStatusToString(s)} ))}