From 79242de5e2e975b7bfcb9c38f10f51cdfe4b3f27 Mon Sep 17 00:00:00 2001 From: Amy Chen Date: Fri, 5 Jan 2024 12:21:30 -0800 Subject: [PATCH] unresponded message display --- src/components/MessagingView.tsx | 190 ++++++++++++++++++++++--------- src/util/util.js | 164 ++++++++++++++++---------- 2 files changed, 238 insertions(+), 116 deletions(-) diff --git a/src/components/MessagingView.tsx b/src/components/MessagingView.tsx index d161f51..4d4df5c 100644 --- a/src/components/MessagingView.tsx +++ b/src/components/MessagingView.tsx @@ -1,68 +1,74 @@ import * as React from "react"; -import {FhirClientContext, FhirClientContextType} from "../FhirClientContext"; +import { FhirClientContext, FhirClientContextType } from "../FhirClientContext"; import Communication from "../model/Communication"; import Patient from "../model/Patient"; -import {CommunicationRequest} from "../model/CommunicationRequest"; -import {IBundle_Entry, ICodeableConcept, ICoding, IReference, IResource} from "@ahryman40k/ts-fhir-types/lib/R4"; +import { CommunicationRequest } from "../model/CommunicationRequest"; import { - Alert, - AlertTitle, - Box, - Button, - Chip, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormControlLabel, - Grid, - IconButton, - List, - Radio, - RadioGroup, - Snackbar, - Stack, - Tab, - Tabs, - TextField, - TextFieldProps, - Typography, + IBundle_Entry, + ICodeableConcept, + ICoding, + IReference, + IResource, +} from "@ahryman40k/ts-fhir-types/lib/R4"; +import { + Alert, + AlertTitle, + Box, + Button, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Grid, + IconButton, + List, + Radio, + RadioGroup, + Snackbar, + Stack, + Tab, + Tabs, + TextField, + TextFieldProps, + Typography, } from "@mui/material"; -import {ArrowBackIos} from "@mui/icons-material"; +import { ArrowBackIos } from "@mui/icons-material"; import LoadingButton from "@mui/lab/LoadingButton"; import EditIcon from "@mui/icons-material/Edit"; import InfoIcon from "@mui/icons-material/Info"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import * as moment from "moment"; -import {DateTimePicker} from "@mui/x-date-pickers/DateTimePicker"; -import { DateTimeValidationError} from '@mui/x-date-pickers/models'; -import {grey, lightBlue, teal} from "@mui/material/colors"; -import {IsaccMessageCategory} from "../model/CodeSystem"; -import {Error, Refresh, Warning} from "@mui/icons-material"; +import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; +import { DateTimeValidationError } from "@mui/x-date-pickers/models"; +import { grey, lightBlue, teal, deepPurple } from "@mui/material/colors"; +import { IsaccMessageCategory } from "../model/CodeSystem"; +import { Error, Refresh, Warning } from "@mui/icons-material"; import Client from "fhirclient/lib/Client"; -import {Bundle} from "../model/Bundle"; -import {getEnv} from "../util/util"; -import {getClientAppURL, getFhirData, getUserName } from "../util/isacc_util"; +import { Bundle } from "../model/Bundle"; +import { getEnv, getTimeAgoDisplay, isFutureDate } from "../util/util"; +import { getClientAppURL, getFhirData, getUserName } from "../util/isacc_util"; type MessageType = "sms" | "outside communication" | "comment"; type MessageStatus = "sent" | "received"; interface Message { - date: string, - content: string, - type: MessageType, - status: MessageStatus + date: string; + content: string; + type: MessageType; + status: MessageStatus; } const defaultMessage: Message = { - date: new Date().toISOString(), - content: "", - type: "sms", - status: "sent" + date: new Date().toISOString(), + content: "", + type: "sms", + status: "sent", }; const abortController = new AbortController(); @@ -433,6 +439,7 @@ export default class MessagingView extends React.Component< + {this._buildUnrespondedMessageDisplay()} {this._buildNextScheduledMessageDisplay()} {this.state.error && ( @@ -482,8 +489,13 @@ export default class MessagingView extends React.Component< theme.spacing(1.5) }}> Next scheduled outgoing message - - + + {MessagingView.displayDateTime( patient.nextScheduledMessageDateTime @@ -506,6 +518,64 @@ export default class MessagingView extends React.Component< ); } + private _buildUnrespondedMessageDisplay(): React.ReactNode { + // @ts-ignore + const context: FhirClientContextType = this.context; + // @ts-ignore + const patient: Patient = context.patient; + if ( + !patient.lastUnfollowedMessageDateTime || + isFutureDate(new Date(patient.lastUnfollowedMessageDateTime)) + ) { + return null; + } + return ( + theme.spacing(1.5) }}> + + Un-responded message + + + + Time since reply:{" "} + {getTimeAgoDisplay( + new Date(patient.lastUnfollowedMessageDateTime) + )} + + + To clear this alert: respond with a{" "} + manual message, record an{" "} + outside communication, or use the{" "} + button below for messages that don't need a + response. + + + + + + + ); + } + private _buildMessageTypeSelector(): React.ReactNode { const tabRootStyleProps = { margin: { @@ -931,7 +1001,7 @@ export default class MessagingView extends React.Component< client .update(targetEntry) .then(() => { - this.handleLastUnfollowedDateTime(targetEntry); + this.handleLastUnfollowedDateTimeByCommunication(targetEntry); const existingEntryIndex = this.state.communications?.findIndex( (item) => item.id === targetEntry.id @@ -1083,7 +1153,9 @@ export default class MessagingView extends React.Component< ); } - private handleLastUnfollowedDateTime(communication: Communication) { + private handleLastUnfollowedDateTimeByCommunication( + communication: Communication + ) { // @ts-ignore const context: FhirClientContextType = this.context; const patient: Patient = context.patient; @@ -1104,17 +1176,25 @@ export default class MessagingView extends React.Component< new Date(communication.sent) > new Date(existingLastUnfollowedMessageDateTime) ) { - // set to a future date, this is so that `Time Since Reply` can be sorted - patient.lastUnfollowedMessageDateTime = Patient.UNSET_LAST_UNFOLLOWED_DATETIME; - const client = context.client; - if (client) { - // @ts-ignore - client.update(patient); - } + this.unsetLastUnfollowedDateTime(); } } } + private unsetLastUnfollowedDateTime() { + // @ts-ignore + const context: FhirClientContextType = this.context; + const patient: Patient = context.patient; + // set to a future date, this is so that `Time Since Reply` can be sorted + patient.lastUnfollowedMessageDateTime = + Patient.UNSET_LAST_UNFOLLOWED_DATETIME; + const client = context.client; + if (client) { + // @ts-ignore + client.update(patient).then(() => this.setState({})); + } + } + private saveNonSMSMessage() { // @ts-ignore let context: FhirClientContextType = this.context; @@ -1155,7 +1235,7 @@ export default class MessagingView extends React.Component< ); this._save(newCommunication, (savedResult: IResource) => { console.log("Saved new communication:", savedResult); - this.handleLastUnfollowedDateTime(newCommunication); + this.handleLastUnfollowedDateTimeByCommunication(newCommunication); this.setState({ activeMessage: { ...defaultMessage, diff --git a/src/util/util.js b/src/util/util.js index 9c33379..9360794 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -1,66 +1,66 @@ export function fetchEnvData() { - if (window["appConfig"] && Object.keys(window["appConfig"]).length) { - console.log("Window config variables added. "); - return; + if (window["appConfig"] && Object.keys(window["appConfig"]).length) { + console.log("Window config variables added. "); + return; + } + const setConfig = function () { + if (!xhr.readyState === xhr.DONE) { + return; } - const setConfig = function () { - if (!xhr.readyState === xhr.DONE) { - return; - } - if (xhr.status !== 200) { - console.log("Request failed! "); - return; - } - var envObj = JSON.parse(xhr.responseText); - window["appConfig"] = {}; - //assign window process env variables for access by app - //won't be overridden when Node initializing env variables - for (var key in envObj) { - if (!window["appConfig"][key]) { - window["appConfig"][key] = envObj[key]; - } - } - }; - var xhr = new XMLHttpRequest(); - xhr.open("GET", "/env.json", false); - xhr.onreadystatechange = function () { - //in the event of a communication error (such as the server going down), - //or error happens when parsing data - //an exception will be thrown in the onreadystatechange method when accessing the response properties, e.g. status. - try { - setConfig(); - } catch (e) { - console.log("Caught exception " + e); - } - }; + if (xhr.status !== 200) { + console.log("Request failed! "); + return; + } + var envObj = JSON.parse(xhr.responseText); + window["appConfig"] = {}; + //assign window process env variables for access by app + //won't be overridden when Node initializing env variables + for (var key in envObj) { + if (!window["appConfig"][key]) { + window["appConfig"][key] = envObj[key]; + } + } + }; + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/env.json", false); + xhr.onreadystatechange = function () { + //in the event of a communication error (such as the server going down), + //or error happens when parsing data + //an exception will be thrown in the onreadystatechange method when accessing the response properties, e.g. status. try { - xhr.send(); + setConfig(); } catch (e) { - console.log("Request failed to send. Error: ", e); + console.log("Caught exception " + e); } - xhr.ontimeout = function (e) { - // XMLHttpRequest timed out. - console.log("request to fetch env.json file timed out ", e); - }; + }; + try { + xhr.send(); + } catch (e) { + console.log("Request failed to send. Error: ", e); + } + xhr.ontimeout = function (e) { + // XMLHttpRequest timed out. + console.log("request to fetch env.json file timed out ", e); + }; } export function getEnv(key) { - //window application global variables - if (window["appConfig"] && window["appConfig"][key]) - return window["appConfig"][key]; - const envDefined = typeof process !== "undefined" && process.env; - //enviroment variables as defined in Node - if (envDefined && process.env[key]) return process.env[key]; - return ""; + //window application global variables + if (window["appConfig"] && window["appConfig"][key]) + return window["appConfig"][key]; + const envDefined = typeof process !== "undefined" && process.env; + //enviroment variables as defined in Node + if (envDefined && process.env[key]) return process.env[key]; + return ""; } export function getEnvs() { - const appConfig = window["appConfig"] ? window["appConfig"] : {}; - const processEnvs = process.env ? process.env : {}; - return { - ...appConfig, - ...processEnvs, - }; + const appConfig = window["appConfig"] ? window["appConfig"] : {}; + const processEnvs = process.env ? process.env : {}; + return { + ...appConfig, + ...processEnvs, + }; } /* @@ -68,13 +68,55 @@ export function getEnvs() { * @param dateString in string * @return boolean */ -export function dateInPast (dateString) { - const dateToCompare = new Date(dateString); - dateToCompare.setSeconds(0); - dateToCompare.setMilliseconds(0); - const today = new Date(); - today.setSeconds(0); - today.setMilliseconds(0); - return dateToCompare < today; +export function dateInPast(dateString) { + const dateToCompare = new Date(dateString); + dateToCompare.setSeconds(0); + dateToCompare.setMilliseconds(0); + const today = new Date(); + today.setSeconds(0); + today.setMilliseconds(0); + return dateToCompare < today; +} + +export function isFutureDate(date) { + const today = new Date(); + if (!(date instanceof Date)) { + return (new Date(date)) > today; + } + return date > today; +} + +/* + * @param objDate of type Date object + * @returns text display of time ago as string e.g. < 50 seconds, < 1 hour, 1 day 2 hours, 3 hours, 3 days + */ +export function getTimeAgoDisplay(objDate) { + if (!objDate || isNaN(objDate)) return null; + const today = new Date(); + const total = today - objDate; + if (isFutureDate(objDate)) return ""; + const seconds = Math.floor((today - objDate) / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + if (seconds < 5) { + return "now"; + } else if (seconds < 60) { + return `< ${seconds} second${seconds > 1 ? "s" : ""}`; + } else if (minutes < 60) { + return `< 1 hour`; + } else if (hours < 24) { + return `${hours} hour${hours > 1 ? "s" : ""}`.trim(); + } else { + if (days >= 1) { + const hoursRemain = Math.floor((total / (1000 * 60 * 60)) % 24); + return `${days} day${days > 1 ? "s" : ""} ${ + hoursRemain > 0 + ? hoursRemain + " hour" + (hoursRemain > 1 ? "s" : "") + : "" + }`.trim(); + } + return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; + } } -export const queryPatientIdKey = 'launch_queryPatientId'; +export const queryPatientIdKey = "launch_queryPatientId";