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

(feat) Add text-to-speech voice call support for ticket numbers in queue screen #1317

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,15 +1,69 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { DataTableSkeleton } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { useActiveTickets } from './useActiveTickets';
import PatientQueueHeader from '../patient-queue-header/patient-queue-header.component';
import styles from './queue-screen.scss';
import { useSelectedQueueLocationUuid } from '../helpers/helpers';

interface QueueScreenProps {}

const QueueScreen: React.FC<QueueScreenProps> = () => {
const { t } = useTranslation();
const { activeTickets, isLoading, error } = useActiveTickets();
const { activeTickets, isLoading, error, mutate } = useActiveTickets();
const speaker = window.speechSynthesis;
const [isSpeaking, setIsSpeaking] = useState(false);
const selectedLocation = useSelectedQueueLocationUuid();
const locationFilteredTickets = activeTickets;
Copy link
Member

Choose a reason for hiding this comment

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

Looks like we're not using this variable. If so, we should remove it.


Copy link
Member

Choose a reason for hiding this comment

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

Did you mean to filter activeTickets by location here? If not, then we probably don't need the alias and we should use activeTickets directly.

Copy link
Author

@FelixKiprotich350 FelixKiprotich350 Sep 18, 2024

Choose a reason for hiding this comment

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

i did this because ill be making a follow up later to filter by location, am waiting for someone to include the locaion uuid in the response so that i can utilize that,,, just like the selectedlocation variable they are unused for now

Copy link
Member

@denniskigen denniskigen Sep 18, 2024

Choose a reason for hiding this comment

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

Best to not include it until then. It's best to keep things as easily understandable as possible.

Copy link
Author

Choose a reason for hiding this comment

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

well gotten

const rowData = locationFilteredTickets.map((ticket, index) => ({
id: `${index}`,
room: ticket.room,
ticketNumber: ticket.ticketNumber,
status: ticket.status,
}));

Copy link
Member

Choose a reason for hiding this comment

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

Consider memoizing rowData:

  const rowData = useMemo(() => 
    activeTickets.map((ticket, index) => ({
      id: `${index}`,
      room: ticket.room,
      ticketNumber: ticket.ticketNumber,
      status: ticket.status,
    })),
  [activeTickets]);

Also, is there something unique in the response that we could use as the id instead of index? Maybe a UUID?

Copy link
Author

Choose a reason for hiding this comment

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

@denniskigen currently the response does not have uuid thats why i opted to use index

function readTicket(queue, speaker) {
if ('speechSynthesis' in window) {
const message = new SpeechSynthesisUtterance();
const utterance =
'Ticket Number: ' +
queue.ticketNumber.split('-')[0].split('') +
', - ' +
queue.ticketNumber.split('-')[1].split('') +
', Please Proceed To Room, ' +
queue.room;
message.rate = 1;
message.pitch = 1;
message.text = utterance;
return new Promise((resolve) => {
message.onend = resolve;
speaker.speak(message);
});
} else {
return Promise.resolve();
}
}

Copy link
Member

Choose a reason for hiding this comment

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

This could be moved into the component function body and memoized with a useCallback:

  const readTicket = useCallback((queue) => {
    if ('speechSynthesis' in window) {
      const message = new SpeechSynthesisUtterance();
      const [prefix, suffix] = queue.ticketNumber.split('-');
      const utterance = `Ticket Number: ${prefix.split('')}, - ${suffix.split('')}, Please Proceed To Room, ${queue.room}`;
      message.rate = 1;
      message.pitch = 1;
      message.text = utterance;
      return new Promise<void>((resolve) => {
        message.onend = resolve;
        speaker.speak(message);
      });
    }
    return Promise.resolve();
  }, [speaker]);

Also, if the SpeechSynthesisUtterance API is locale-aware (it probably is), we should consider internationalising utterance using something like:

	 const utterance = t(
          'ticketAnnouncement',
          'Ticket number: {{prefix}}, - {{suffix}}, please proceed to room {{room}}',
          {
            prefix: prefix.split('').join(' '),
            suffix: suffix.split('').join(' '),
            room: queue.room,
          },
        );

useEffect(() => {
const readableTickets = locationFilteredTickets.filter((item) => item.status == 'calling');
if (readableTickets.length > 0 && !isSpeaking) {
denniskigen marked this conversation as resolved.
Show resolved Hide resolved
setIsSpeaking(true);
const readTickets = async () => {
for (const ticket of readableTickets) {
await readTicket(ticket, speaker);
}
setIsSpeaking(false);
if (typeof mutate === 'function') {
mutate();
}
};
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (typeof mutate === 'function') {
mutate();
}
mutate?.();


readTickets();
}

return () => {};
}, [locationFilteredTickets, isSpeaking]);

if (isLoading) {
return <DataTableSkeleton row={5} className={styles.queueScreen} role="progressbar" />;
Expand All @@ -19,13 +73,6 @@ const QueueScreen: React.FC<QueueScreenProps> = () => {
return <div>Error</div>;
}
Copy link
Member

Choose a reason for hiding this comment

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

We need a better error state here. Consider using the ErrorState component from the framework:

 return <ErrorState error={error} headerTitle={t('queueScreenError', 'Queue screen error')} />;


const rowData = activeTickets.map((ticket, index) => ({
id: `${index}}`,
room: ticket.room,
ticketNumber: ticket.ticketNumber,
status: ticket.status,
}));

return (
<div>
<PatientQueueHeader title={t('queueScreen', 'Queue screen')} showLocationDropdown />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const useActiveTickets = () => {
const { data, isLoading, error, mutate } = useSWR<{ data: Record<string, { status: string; ticketNumber: string }> }>(
`${restBaseUrl}/queueutil/active-tickets`,
openmrsFetch,
{ refreshInterval: 3000 },
{ refreshInterval: 30000 },
);
const activeTickets =
Array.from(Object.entries(data?.data ?? {}).map(([key, value]) => ({ room: key, ...value }))) ?? [];
Expand Down
Loading