Skip to content

Commit

Permalink
Enhancements to Encounter Discussion/Notes | Fix Translation (#10156)
Browse files Browse the repository at this point in the history
  • Loading branch information
rajku-dev authored Feb 12, 2025
1 parent 29060ed commit a6cc4cf
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 43 deletions.
5 changes: 5 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,7 @@
"encounter_notes__failed_send_message": "Failed to send message",
"encounter_notes__new": "New",
"encounter_notes__no_discussions": "No discussions yet",
"encounter_notes__no_unused_threads": "Please enter a custom title for thread",
"encounter_notes__select_create_thread": "Select or create a thread to start messaging",
"encounter_notes__start_conversation": "Start the Conversation",
"encounter_notes__start_new_discussion": "Start New Discussion",
Expand Down Expand Up @@ -1006,6 +1007,7 @@
"facility_updated_successfully": "Facility updated successfully",
"failed_to_create_appointment": "Failed to create an appointment",
"failed_to_link_abha_number": "Failed to link ABHA Number. Please try again later.",
"failed_to_send_message": "Failed to send message",
"false": "False",
"fast_track_testing_reason": "Fast track testing reason",
"features": "Features",
Expand Down Expand Up @@ -1314,6 +1316,7 @@
"medicines_administered": "Medicine(s) administered",
"medicines_administered_error": "Error administering medicine(s)",
"member_id_required": "Member Id is required",
"messages": "Messages",
"method": "Method",
"middleware_hostname": "Middleware Hostname",
"middleware_hostname_example": "e.g. example.ohc.network",
Expand Down Expand Up @@ -1523,6 +1526,7 @@
"page_not_found": "Page Not Found",
"pain": "Pain",
"pain_chart_description": "Mark region and intensity of pain",
"participants": "Participants",
"passport_number": "Passport Number",
"password": "Password",
"password_length_validation": "Use at least <strong>8 characters</strong>",
Expand Down Expand Up @@ -2103,6 +2107,7 @@
"third_party_software_licenses": "Third Party Software Licenses",
"this_action_is_irreversible": "This action is irreversible. Once a file is archived it cannot be unarchived.",
"this_file_has_been_archived": "This file has been archived and cannot be unarchived.",
"thread_already_exists": "Thread with this title already exists",
"time": "Time",
"time_slot": "Time Slot",
"title": "Title",
Expand Down
148 changes: 105 additions & 43 deletions src/pages/Encounters/tabs/EncounterNotesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
Info,
Loader2,
MessageCircle,
MessageSquare,
MessageSquarePlus,
Plus,
Send,
Users,
} from "lucide-react";
import { Link, usePathParams } from "raviger";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useInView } from "react-intersection-observer";
Expand All @@ -25,6 +27,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
Expand Down Expand Up @@ -61,6 +64,7 @@ import { Thread } from "@/types/notes/threads";
const MESSAGES_LIMIT = 20;

// Thread templates for quick selection

const threadTemplates = [
"Treatment Plan",
"Medication Notes",
Expand Down Expand Up @@ -118,6 +122,7 @@ const ThreadItem = ({
// Message item component
const MessageItem = ({ message }: { message: Message }) => {
const authUser = useAuthUser();
const { facilityId } = usePathParams("/facility/:facilityId/*")!;
const isCurrentUser = authUser?.external_id === message.created_by.id;

return (
Expand All @@ -135,15 +140,19 @@ const MessageItem = ({ message }: { message: Message }) => {
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex">
<Avatar
name={message.created_by.username}
imageUrl={message.created_by.profile_picture_url}
className="w-8 h-8 rounded-full object-cover"
/>
</div>
</TooltipTrigger>
<Link
href={`/facility/${facilityId}/users/${message.created_by.username}`}
>
<TooltipTrigger asChild>
<div className="flex pr-2">
<Avatar
name={message.created_by.username}
imageUrl={message.created_by.profile_picture_url}
className="w-8 h-8 rounded-full object-cover ring-1 ring-transparent hover:ring-red-200 transition"
/>
</div>
</TooltipTrigger>
</Link>
<TooltipContent>
<p>{message.created_by.username}</p>
</TooltipContent>
Expand Down Expand Up @@ -192,11 +201,13 @@ const NewThreadDialog = ({
onClose,
onCreate,
isCreating,
threadsUnused,
}: {
isOpen: boolean;
onClose: () => void;
onCreate: (title: string) => void;
isCreating: boolean;
threadsUnused: string[];
}) => {
const { t } = useTranslation();
const [title, setTitle] = useState("");
Expand All @@ -218,13 +229,15 @@ const NewThreadDialog = ({
<InfoTooltip content={t("encounter_notes__create_discussion")} />
</DialogTitle>
<DialogDescription className="text-sm text-left">
{t("encounter_notes__choose_template")}
{threadsUnused.length === 0
? t("encounter_notes__no_unused_threads")
: t("encounter_notes__choose_template")}
</DialogDescription>
</DialogHeader>

<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{threadTemplates.map((template) => (
{threadsUnused.map((template) => (
<Badge
key={template}
variant="primary"
Expand All @@ -246,9 +259,10 @@ const NewThreadDialog = ({
</div>

<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isCreating}>
{t("Cancel")}
</Button>
<DialogClose asChild disabled={isCreating}>
<Button variant="outline">{t("cancel")}</Button>
</DialogClose>

<Button
onClick={() => onCreate(title)}
disabled={!title.trim() || isCreating}
Expand All @@ -258,7 +272,7 @@ const NewThreadDialog = ({
) : (
<MessageSquarePlus className="h-4 w-4 mr-2" />
)}
{t("Create")}
{t("create")}
</Button>
</DialogFooter>
</DialogContent>
Expand Down Expand Up @@ -308,6 +322,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
const [newMessage, setNewMessage] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
const { ref, inView } = useInView();
const [commentAdded, setCommentAdded] = useState(false);

// Fetch threads
const { data: threadsData, isLoading: threadsLoading } = useQuery({
Expand All @@ -318,17 +333,11 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
}),
});

// Auto-select first thread
useEffect(() => {
if (threadsData?.results.length && !selectedThread) {
setSelectedThread(threadsData.results[0].id);
}
}, [threadsData, selectedThread]);

// Fetch messages with infinite scroll
const {
data: messagesData,
isLoading: messagesLoading,
isFetching: isFetchingMessages,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
Expand Down Expand Up @@ -382,28 +391,63 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["messages", selectedThread] });
setNewMessage("");
setTimeout(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, 100);
setCommentAdded(true);
},
});

// Handle infinite scroll
// handle scrolling to last message when new message is added

useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
if (commentAdded && !isFetchingMessages) {
messagesEndRef.current?.scrollIntoView();
setCommentAdded(false);
}
}, [inView, hasNextPage, fetchNextPage]);
}, [commentAdded, isFetchingMessages]);

const [threads, setThreads] = useState<string[]>([...threadTemplates]);

// Auto-select first thread

// Scroll to bottom on initial load and thread change
useEffect(() => {
if (messagesData && !messagesLoading && !isFetchingNextPage) {
if (threadsData?.results.length) {
if (!selectedThread) setSelectedThread(threadsData.results[0].id);
const threadTitles = threadsData.results.map((thread) => thread.title);
setThreads(
threads.filter((template) => !threadTitles.includes(template)),
);
}
}, [threadsData, selectedThread]);

// hack to scroll to bottom on initial load

useEffect(() => {
messagesEndRef.current?.scrollIntoView();
}, [messagesLoading]);

// Handle infinite scroll

useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
messagesEndRef.current?.scrollIntoView();
}
}, [selectedThread, messagesData, messagesLoading, isFetchingNextPage]);
}, [
inView,
hasNextPage,
fetchNextPage,
messagesData,
isFetchingNextPage,
messagesLoading,
]);

const handleCreateThread = (title: string) => {
if (title.trim()) {
if (
threadsData?.results.some((thread) => thread.title === title.trim())
) {
toast.error(t("thread_already_exists"));
return;
}
createThreadMutation.mutate({
title: title.trim(),
encounter: encounter.id,
Expand All @@ -423,6 +467,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
}

const messages = messagesData?.pages.flatMap((page) => page.results) ?? [];
const totalMessages = messagesData?.pages[0]?.count ?? 0;

return (
<div className="flex h-[calc(100vh-12rem)]">
Expand Down Expand Up @@ -529,8 +574,8 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
{/* Main Content */}
<div className="flex-1 min-w-0">
<div className="flex flex-col h-full pb-[60px] lg:pb-0">
{/* Mobile Header */}
<div className="lg:hidden p-4 border-b bg-background sticky top-0 z-10">
{/* Header */}
<div className="p-4 border-b bg-background sticky top-0 z-10">
{selectedThread ? (
<div className="flex items-center gap-3">
<h2 className="text-base font-medium truncate flex-1">
Expand All @@ -539,24 +584,40 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
?.title
}
</h2>
<div className="flex items-center gap-2 text-xs text-gray-500">
<Users className="h-4 w-4" />
<span>{messages.length}</span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2 text-xs text-gray-500 cursor-pointer">
<Users className="h-4 w-4" />
<span>
{new Set(messages.map((m) => m.created_by.id)).size}
</span>
<MessageSquare className="h-4 w-4 ml-3" />
<span>{totalMessages}</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
{t("participants")}:{" "}
{new Set(messages.map((m) => m.created_by.id)).size}
</p>
<p>
{t("messages")}: {totalMessages}
</p>
</TooltipContent>
</Tooltip>
</div>
) : (
<div className="text-center text-sm font-medium text-gray-500">
{t("encounter_notes__select_create_thread")}
</div>
)}
</div>

{selectedThread ? (
<>
{messagesLoading ? (
<div className="flex-1 p-4">
<div className="space-y-4">
<CardListSkeleton count={3} />
<CardListSkeleton count={4} />
</div>
</div>
) : (
Expand All @@ -580,17 +641,17 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
<MessageItem key={message.id} message={message} />
))
)}
{isFetchingNextPage && (
{isFetchingNextPage ? (
<div className="py-2">
<div className="space-y-4">
<CardListSkeleton count={3} />
</div>
</div>
) : (
<div ref={ref} />
)}
<div ref={ref} />
</div>
</ScrollArea>

{/* Message Input */}
<div className="border-t bg-background p-4 sticky bottom-0">
<form onSubmit={handleSendMessage}>
Expand Down Expand Up @@ -662,6 +723,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => {
onClose={() => setShowNewThreadDialog(false)}
onCreate={handleCreateThread}
isCreating={createThreadMutation.isPending}
threadsUnused={threads}
/>
</div>
);
Expand Down

0 comments on commit a6cc4cf

Please sign in to comment.