Skip to content
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
147 changes: 138 additions & 9 deletions src/app/coaching-sessions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,86 @@ import { cn } from "@/lib/utils";
import { models, types } from "@/data/models";
import { current, future, past } from "@/data/presets";
import { useAppStateStore } from "@/lib/providers/app-state-store-provider";
//import { useAuthStore } from "@/lib/providers/auth-store-provider";
import { useEffect, useState } from "react";
import {
createNote,
fetchNotesByCoachingSessionId,
updateNote,
} from "@/lib/api/notes";
import { Note, noteToString } from "@/types/note";
import { useAuthStore } from "@/lib/providers/auth-store-provider";
import { Id } from "@/types/general";

// export const metadata: Metadata = {
// title: "Coaching Session",
// description: "Coaching session main page, where the good stuff happens.",
// };

export default function CoachingSessionsPage() {
const [isOpen, setIsOpen] = React.useState(false);
//const { isLoggedIn, userId } = useAuthStore((state) => state);
const { organizationId, relationshipId, coachingSessionId } =
useAppStateStore((state) => state);
const [isOpen, setIsOpen] = useState(false);
const [noteId, setNoteId] = useState<Id>("");
const [note, setNote] = useState<string>("");
const [syncStatus, setSyncStatus] = useState<string>("");
const { userId } = useAuthStore((state) => state);
const { coachingSessionId } = useAppStateStore((state) => state);

useEffect(() => {
async function fetchNote() {
if (!coachingSessionId) return;

await fetchNotesByCoachingSessionId(coachingSessionId)
.then((notes) => {
// Apparently it's normal for this to be triggered twice in modern
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting

// React versions in strict + development modes
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
const note = notes[0];
console.trace("note: " + noteToString(note));
setNoteId(note.id);
setNote(note.body);
})
.catch((err) => {
console.error(
"Failed to fetch Note for current coaching session: " + err
);

createNote(coachingSessionId, userId, "")
.then((note) => {
// Apparently it's normal for this to be triggered twice in modern
// React versions in strict + development modes
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
console.trace("New empty note: " + noteToString(note));
setNoteId(note.id);
})
.catch((err) => {
console.error("Failed to create new empty Note: " + err);
});
});
}
fetchNote();
}, [coachingSessionId, !note]);

const handleInputChange = (value: string) => {
setNote(value);

if (noteId && coachingSessionId && userId) {
updateNote(noteId, coachingSessionId, userId, value)
.then((note) => {
// Apparently it's normal for this to be triggered twice in modern
// React versions in strict + development modes
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
console.trace("Updated Note: " + noteToString(note));
setSyncStatus("All changes saved");
})
.catch((err) => {
setSyncStatus("Failed to save changes");
console.error("Failed to update Note: " + err);
});
}
};

const handleKeyDown = () => {
setSyncStatus("");
};

return (
<>
Expand Down Expand Up @@ -156,10 +224,14 @@ export default function CoachingSessionsPage() {
</TabsList>
<TabsContent value="notes">
<div className="flex h-full flex-col space-y-4">
<Textarea
placeholder="Session notes"
className="p-4 min-h-[400px] md:min-h-[630px] lg:min-h-[630px]"
/>
<CoachingNotes
value={note}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
></CoachingNotes>
<p className="text-sm text-muted-foreground">
{syncStatus}
</p>
</div>
</TabsContent>
<TabsContent value="program">
Expand Down Expand Up @@ -194,3 +266,60 @@ export default function CoachingSessionsPage() {
</>
);
}

// A debounced input CoachingNotes textarea component
// TODO: move this into the components dir
const CoachingNotes: React.FC<{
value: string;
onChange: (value: string) => void;
onKeyDown: () => void;
}> = ({ value, onChange, onKeyDown }) => {
const WAIT_INTERVAL = 1000;
const [timer, setTimer] = useState<number | undefined>(undefined);
const [note, setNote] = useState<string>(value);

// Make sure the internal value prop updates when the component interface's
// value prop changes.
useEffect(() => {
setNote(value);
}, [value]);

const handleSessionNoteChange = (
e: React.ChangeEvent<HTMLTextAreaElement>
) => {
const newValue = e.target.value;
setNote(newValue);

if (timer) {
clearTimeout(timer);
}

const newTimer = window.setTimeout(() => {
onChange(newValue);
}, WAIT_INTERVAL);

setTimer(newTimer);
};

const handleOnKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
onKeyDown();
};

useEffect(() => {
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [timer]);

return (
<Textarea
placeholder="Session notes"
value={note}
className="p-4 min-h-[400px] md:min-h-[630px] lg:min-h-[630px]"
onChange={handleSessionNoteChange}
onKeyDown={handleOnKeyDown}
/>
);
};
173 changes: 173 additions & 0 deletions src/lib/api/notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Interacts with the note endpoints

import { Id } from "@/types/general";
import { defaultNote, isNote, isNoteArray, Note, noteToString, parseNote } from "@/types/note";
import { AxiosError, AxiosResponse } from "axios";

export const fetchNotesByCoachingSessionId = async (
coachingSessionId: Id
): Promise<Note[]> => {
const axios = require("axios");

var notes: Note[] = [];
var err: string = "";

const data = await axios
.get(`http://localhost:4000/notes`, {
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
if (response?.status == 204) {
console.error("Retrieval of Note failed: no content.");
err = "Retrieval of Note failed: no content.";
} else {
var notes_data = response.data.data;
if (isNoteArray(notes_data)) {
notes_data.forEach((note_data: any) => {
notes.push(parseNote(note_data))
});
}
}
})
.catch(function (error: AxiosError) {
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
console.error("Retrieval of Note failed: unauthorized.");
err = "Retrieval of Note failed: unauthorized.";
} else {
console.log(error);
console.error(
`Retrieval of Note by coaching session Id (` + coachingSessionId + `) failed.`
);
err =
`Retrieval of Note by coaching session Id (` + coachingSessionId + `) failed.`;
}
});

if (err)
throw err;

return notes;
};

export const createNote = async (
coaching_session_id: Id,
user_id: Id,
body: string
): Promise<Note> => {
const axios = require("axios");

const newNoteJson = {
coaching_session_id: coaching_session_id,
user_id: user_id,
body: body
};
console.debug("newNoteJson: " + JSON.stringify(newNoteJson));
// A full real note to be returned from the backend with the same body
var createdNote: Note = defaultNote();
var err: string = "";

//var strNote: string = noteToString(note);
const data = await axios
.post(`http://localhost:4000/notes`, newNoteJson, {
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 noteStr = response.data.data;
if (isNote(noteStr)) {
createdNote = parseNote(noteStr);
}
})
.catch(function (error: AxiosError) {
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
console.error("Creation of Note failed: unauthorized.");
err = "Creation of Note failed: unauthorized.";
} else if (error.response?.status == 500) {
console.error(
"Creation of Note failed: internal server error."
);
err = "Creation of Note failed: internal server error.";
} else {
console.log(error);
console.error(`Creation of new Note failed.`);
err = `Creation of new Note failed.`;
}
}
);

if (err)
throw err;

return createdNote;
};

export const updateNote = async (
id: Id,
user_id: Id,
coaching_session_id: Id,
body: string,
): Promise<Note> => {
const axios = require("axios");

var updatedNote: Note = defaultNote();
var err: string = "";

const newNoteJson = {
coaching_session_id: coaching_session_id,
user_id: user_id,
body: body
};

const data = await axios
.put(`http://localhost:4000/notes/${id}`, newNoteJson, {
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
if (isNote(response.data.data)) {
updatedNote = response.data.data;
}
})
.catch(function (error: AxiosError) {
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
console.error("Update of Note failed: unauthorized.");
err = "Update of Organization failed: unauthorized.";
} else if (error.response?.status == 500) {
console.error("Update of Organization failed: internal server error.");
err = "Update of Organization failed: internal server error.";
} else {
console.log(error);
console.error(`Update of new Organization failed.`);
err = `Update of new Organization failed.`;
}
});

if (err)
throw err;

return updatedNote;
};
Loading