diff --git a/functions/models/src/codes/quantityUnit.ts b/functions/models/src/codes/quantityUnit.ts index f9db0c45..2214ed97 100644 --- a/functions/models/src/codes/quantityUnit.ts +++ b/functions/models/src/codes/quantityUnit.ts @@ -7,6 +7,7 @@ // import { type FHIRQuantity } from '../fhir/baseTypes/fhirQuantity.js' +import { type Observation } from '../types/observation.js' export class QuantityUnit { // Static Properties @@ -72,14 +73,17 @@ export class QuantityUnit { ) } - convert(value: number, target: QuantityUnit): number | undefined { - return QuantityUnitConverter.allValues + convert(observation: Observation): Observation | undefined { + const value = QuantityUnitConverter.allValues .find( (converter) => - converter.sourceUnit.equals(this) && - converter.targetUnit.equals(target), + converter.sourceUnit.equals(observation.unit) && + converter.targetUnit.equals(this), ) - ?.convert(value) + ?.convert(observation.value) + return value !== undefined ? + { ...observation, value, unit: this } + : undefined } fhirQuantity(value: number): FHIRQuantity { diff --git a/functions/models/src/fhir/fhirQuestionnaireResponse.ts b/functions/models/src/fhir/fhirQuestionnaireResponse.ts index 0aee1813..94aa9bee 100644 --- a/functions/models/src/fhir/fhirQuestionnaireResponse.ts +++ b/functions/models/src/fhir/fhirQuestionnaireResponse.ts @@ -20,37 +20,76 @@ import { optionalish } from '../helpers/optionalish.js' import { SchemaConverter } from '../helpers/schemaConverter.js' import { type SymptomQuestionnaireResponse } from '../types/symptomQuestionnaireResponse.js' -export const fhirQuestionnaireResponseItemConverter = new Lazy( - () => - new SchemaConverter({ - schema: z.object({ - answer: optionalish( - z - .object({ - valueCoding: optionalish( - z.lazy(() => fhirCodingConverter.value.schema), - ), - }) - .array(), - ), - linkId: optionalish(z.string()), - }), - encode: (object) => ({ - answer: - object.answer?.flatMap((value) => ({ - valueCoding: - value.valueCoding ? - fhirCodingConverter.value.encode(value.valueCoding) - : null, - })) ?? null, - linkId: object.linkId ?? null, - }), - }), -) +const fhirQuestionnaireResponseItemBaseConverter = new SchemaConverter({ + schema: z.object({ + answer: optionalish( + z + .object({ + valueCoding: optionalish( + z.lazy(() => fhirCodingConverter.value.schema), + ), + }) + .array(), + ), + linkId: optionalish(z.string()), + }), + encode: (object) => ({ + answer: + object.answer?.flatMap((value) => ({ + valueCoding: + value.valueCoding ? + fhirCodingConverter.value.encode(value.valueCoding) + : null, + })) ?? null, + linkId: object.linkId ?? null, + }), +}) + +export interface FHIRQuestionnaireResponseItemValue + extends z.input< + typeof fhirQuestionnaireResponseItemBaseConverter.value.schema + > { + item?: + | Array> + | null + | undefined +} -export type FHIRQuestionnaireResponseItem = z.output< - typeof fhirQuestionnaireResponseItemConverter.value.schema -> +export const fhirQuestionnaireResponseItemConverter = (() => { + const fhirQuestionnaireResponseItemSchema: z.ZodType< + FHIRQuestionnaireResponseItem, + z.ZodTypeDef, + FHIRQuestionnaireResponseItemValue + > = fhirQuestionnaireResponseItemBaseConverter.value.schema.extend({ + item: optionalish( + z.array(z.lazy(() => fhirQuestionnaireResponseItemSchema)), + ), + }) + + function fhirQuestionnaireResponseItemEncode( + object: z.output, + ): z.input { + return { + ...fhirQuestionnaireResponseItemBaseConverter.value.encode(object), + item: + object.item ? + object.item.map(fhirQuestionnaireResponseItemConverter.value.encode) + : null, + } + } + + return new SchemaConverter({ + schema: fhirQuestionnaireResponseItemSchema, + encode: fhirQuestionnaireResponseItemEncode, + }) +})() + +export interface FHIRQuestionnaireResponseItem + extends z.output< + typeof fhirQuestionnaireResponseItemBaseConverter.value.schema + > { + item?: FHIRQuestionnaireResponseItem[] +} export const fhirQuestionnaireResponseConverter = new Lazy( () => @@ -274,12 +313,41 @@ export class FHIRQuestionnaireResponse extends FHIRResource { // Methods numericSingleAnswerForLink(linkId: string): number { - const answers = - this.item?.find((item) => item.linkId === linkId)?.answer ?? [] + for (const item of this.item ?? []) { + const answer = this.numericSingleAnswerForNestedItem(linkId, item) + if (answer !== undefined) return answer + } + throw new Error(`No answer found in response for linkId ${linkId}.`) + } + + private numericSingleAnswerForNestedItem( + linkId: string, + item: FHIRQuestionnaireResponseItem, + ): number | undefined { + if (item.linkId === linkId) { + return this.numericSingleAnswerForItem(linkId, item) + } + for (const child of item.item ?? []) { + const childAnswer = this.numericSingleAnswerForNestedItem(linkId, child) + if (childAnswer !== undefined) return childAnswer + } + return undefined + } + + private numericSingleAnswerForItem( + linkId: string, + item: FHIRQuestionnaireResponseItem, + ): number { + const answers = item.answer ?? [] if (answers.length !== 1) - throw new Error(`Zero or multiple answers found for linkId ${linkId}.`) + throw new Error( + `Zero or multiple answers found in response item for linkId ${linkId}.`, + ) const code = answers[0].valueCoding?.code - if (!code) throw new Error(`No answer code found for linkId ${linkId}.`) + if (!code) + throw new Error( + `No answer code found in response item for linkId ${linkId}.`, + ) return parseInt(code) } } diff --git a/functions/resources/fonts/OpenSans-BoldItalic.ttf b/functions/resources/fonts/OpenSans-BoldItalic.ttf new file mode 100644 index 00000000..85589283 Binary files /dev/null and b/functions/resources/fonts/OpenSans-BoldItalic.ttf differ diff --git a/functions/resources/fonts/OpenSans-BoldItalic.ttf.license b/functions/resources/fonts/OpenSans-BoldItalic.ttf.license new file mode 100644 index 00000000..c9c42c60 --- /dev/null +++ b/functions/resources/fonts/OpenSans-BoldItalic.ttf.license @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) +# SPDX-License-Identifier: OFL-1.1-no-RFN + +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/functions/resources/fonts/OpenSans-Italic.ttf b/functions/resources/fonts/OpenSans-Italic.ttf new file mode 100644 index 00000000..29ff6938 Binary files /dev/null and b/functions/resources/fonts/OpenSans-Italic.ttf differ diff --git a/functions/resources/fonts/OpenSans-Italic.ttf.license b/functions/resources/fonts/OpenSans-Italic.ttf.license new file mode 100644 index 00000000..c9c42c60 --- /dev/null +++ b/functions/resources/fonts/OpenSans-Italic.ttf.license @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) +# SPDX-License-Identifier: OFL-1.1-no-RFN + +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/functions/src/functions/exportHealthSummary.ts b/functions/src/functions/exportHealthSummary.ts index f3a1f22f..5b2b118a 100644 --- a/functions/src/functions/exportHealthSummary.ts +++ b/functions/src/functions/exportHealthSummary.ts @@ -45,9 +45,11 @@ export const exportHealthSummary = validatedOnCall( if (request.data.weightUnit !== undefined && weightUnit === undefined) throw new https.HttpsError('invalid-argument', 'Invalid weight unit') + const now = new Date() const healthSummaryService = factory.healthSummary() const data = await healthSummaryService.getHealthSummaryData( request.data.userId, + now, weightUnit ?? QuantityUnit.lbs, ) const pdf = generateHealthSummary(data, { diff --git a/functions/src/healthSummary/generate+localizations.ts b/functions/src/healthSummary/generate+localizations.ts index a620e3f7..40097285 100644 --- a/functions/src/healthSummary/generate+localizations.ts +++ b/functions/src/healthSummary/generate+localizations.ts @@ -9,297 +9,294 @@ import { LocalizedText, type UserMedicationRecommendationDoseSchedule, - type QuantityUnit, type FHIRAppointment, + type SymptomScore, } from '@stanfordbdhg/engagehf-models' +import { + type HealthSummaryDizzinessCategory, + healthSummaryKeyPointTexts, + type HealthSummaryMedicationRecommendationsCategory, + type HealthSummarySymptomScoreCategory, + type HealthSummaryWeightCategory, +} from './keyPointsMessage.js' export function healthSummaryLocalizations(languages: string[]) { + function localize(strings: Record): string { + return new LocalizedText(strings).localize(...languages) + } return { header: { - title: new LocalizedText({ - en: 'ENGAGE-HF Mobile App Health Summary', - }).localize(...languages), + title: localize({ + en: 'ENGAGE-HF Health Summary', + }), dateOfBirthLine(date: Date | null) { - return new LocalizedText({ + return localize({ en: `DOB: ${date !== null ? formatDate(date) : '---'}`, - }).localize(...languages) + }) }, providerLine(name: string | null) { - return new LocalizedText({ + return localize({ en: `Provider: ${name ?? '---'}`, - }).localize(...languages) + }) }, nextAppointmentLine(appointment: FHIRAppointment | null) { const date = appointment?.start const providerNames = appointment?.providerNames ?? [] const providerText = providerNames.length === 0 ? '' : providerNames.join(', ') + ' ' - return new LocalizedText({ - en: `Next Appointment: ${providerText}${date !== undefined ? formatDate(date) : '---'}`, - }).localize(...languages) + return localize({ + en: `Next Appointment: ${providerText}${date !== undefined ? `${formatDate(date)} at ${formatTime(date)}` : '---'}`, + }) }, pageNumberTitle(number: number) { - return new LocalizedText({ + return localize({ en: `Page ${number}`, - }).localize(...languages) - }, - }, - medicationsSection: { - title: new LocalizedText({ - en: 'MEDICATIONS', - }).localize(...languages), - currentTitle: new LocalizedText({ - en: 'Current Medications', - }).localize(...languages), - currentText: new LocalizedText({ - en: 'Before your next clinic appointment, check off which medications you have been taking below:', - }).localize(...languages), - recommendationsTitle: new LocalizedText({ - en: 'Potential Positive Changes', - }).localize(...languages), - recommendationsText: new LocalizedText({ - en: 'Please discuss optimizing these medications with your care them at your next clinic appointment.', - }).localize(...languages), - recommendationsHint: new LocalizedText({ - en: 'Aim to make one positive change!', - }).localize(...languages), - }, - medicationsTable: { - nameHeader: new LocalizedText({ - en: 'My medications', - }).localize(...languages), - doseHeader: new LocalizedText({ - en: 'Dose', - }).localize(...languages), - targetDoseHeader: new LocalizedText({ - en: 'Target dose', - }).localize(...languages), - recommendationHeader: new LocalizedText({ - en: 'Potential Positive Change', - }).localize(...languages), - commentsHeader: new LocalizedText({ - en: 'Questions/Comments', - }).localize(...languages), - doseSchedule( - schedule: UserMedicationRecommendationDoseSchedule, - unit: string, - ): string { - const prefix = - schedule.quantity.map((quantity) => quantity.toString()).join('/') + - ' ' + - unit + - ' ' - switch (schedule.frequency) { - case 1: - return new LocalizedText({ - en: prefix + 'daily', - }).localize(...languages) - case 2: - return new LocalizedText({ - en: prefix + 'twice daily', - }).localize(...languages) - default: - return new LocalizedText({ - en: prefix + `${schedule.frequency}x daily`, - }).localize(...languages) - } - }, - }, - vitalsSection: { - title: new LocalizedText({ - en: 'VITALS OVER LAST 2 WEEKS', - }).localize(...languages), - averageSystolicText(number: number | null) { - const observationText = formatValue(number, null, { - fractionalDigitCount: 0, - }) - return new LocalizedText({ - en: `Average Systolic Blood Pressure: ${observationText}`, - }).localize(...languages) - }, - averageDiastolicText(number: number | null) { - const observationText = formatValue(number, null, { - fractionalDigitCount: 0, }) - return new LocalizedText({ - en: `Average Diastolic Blood Pressure: ${observationText}`, - }).localize(...languages) }, - averageHeartRateText(number: number | null) { - const observationText = formatValue(number, null, { - fractionalDigitCount: 0, - }) - return new LocalizedText({ - en: `Average Heart Rate: ${observationText}`, - }).localize(...languages) - }, - currentBodyWeightText( - observation: { value: number; unit: QuantityUnit } | null, - ) { - const observationText = formatValue( - observation?.value ?? null, - observation?.unit ?? null, - { fractionalDigitCount: 0 }, - ) - return new LocalizedText({ - en: `Current Weight: ${observationText}`, - }).localize(...languages) - }, - averageBodyWeightText( - observation: { value: number; unit: QuantityUnit } | null, - ) { - const observationText = formatValue( - observation?.value ?? null, - observation?.unit ?? null, - { fractionalDigitCount: 0 }, + }, + keyPointsSection: { + title: localize({ + en: 'KEY POINTS', + }), + defaultText: localize({ + en: 'No key points available. Please discuss with your care team for more information.', + }), + text(input: { + recommendations: HealthSummaryMedicationRecommendationsCategory | null + symptomScore: HealthSummarySymptomScoreCategory | null + dizziness: HealthSummaryDizzinessCategory | null + weight: HealthSummaryWeightCategory | null + }): string[] | null { + if ( + input.recommendations === null || + input.symptomScore === null || + input.dizziness === null || + input.weight === null ) - return new LocalizedText({ - en: `Last Week Average Weight: ${observationText}`, - }).localize(...languages) + return null + + const messages = + healthSummaryKeyPointTexts({ + recommendations: input.recommendations, + symptomScore: input.symptomScore, + dizziness: input.dizziness, + weight: input.weight, + }) ?? [] + + if (messages.length === 0) return null + + return messages.map((text) => text.localize(...languages)) }, - dryWeightText(observation: { value: number; unit: QuantityUnit } | null) { - const observationText = formatValue( - observation?.value ?? null, - observation?.unit ?? null, - { fractionalDigitCount: 0 }, - ) - return new LocalizedText({ - en: `Prior Dry Weight: ${observationText}`, - }).localize(...languages) + }, + currentMedicationsSection: { + title: localize({ + en: 'CURRENT HEART MEDICATIONS', + }), + description: localize({ + en: 'Here are meds you are taking for your heart function, your current dose, and the target dose that we aim to get to. The target dose is the dose we know best helps strengthen your heart.', + }), + table: { + nameHeader: localize({ + en: 'My Medications', + }), + currentDoseHeader: localize({ + en: 'Current Dose', + }), + targetDoseHeader: localize({ + en: 'Target Dose', + }), + doseSchedule( + schedule: UserMedicationRecommendationDoseSchedule, + unit: string, + ): string { + const prefix = + schedule.quantity.map((quantity) => quantity.toString()).join('/') + + ' ' + + unit + + ' ' + switch (schedule.frequency) { + case 1: + return localize({ + en: prefix + 'daily', + }) + case 2: + return localize({ + en: prefix + 'twice daily', + }) + default: + return localize({ + en: prefix + `${schedule.frequency}x daily`, + }) + } + }, }, }, - symptomScoresSection: { - title: new LocalizedText({ - en: 'SYMPTOM SURVEY [KCCQ-12] REPORT', - }).localize(...languages), - description: new LocalizedText({ - en: 'These symptom scores range from 0-100.\nA score of 0 indicates severe symptoms.\nA score of 100 indicates you are doing extremely well.', - }).localize(...languages), - personalSummary: { - title: new LocalizedText({ - en: 'Personal Summary:', - }).localize(...languages), - above90Improving: new LocalizedText({ - en: 'Your heart symptoms score has increased. This means you are feeling better. Your score is overall very good. Continuing to take your meds will be important for keeping you feeling well.', - }).localize(...languages), - above90NotImproving: new LocalizedText({ - en: 'Your heart symptom score remains very good. Continuing to take your meds will be important for keeping you feeling well.', - }).localize(...languages), - below90Improving: new LocalizedText({ - en: 'Your heart symptoms score has increased. This means you have been feeling better. There is still room to continue improving how you feel. Getting on the best doses of heart failure medicines can help you feeling better. Consider discussing further with your care team.', - }).localize(...languages), - below90Stable: new LocalizedText({ - en: 'Your heart symptom score is stable. There is still room to continue improving how you feel. Getting on the best doses of heart failure medicines can help you feeling better. Consider discussing further with your care team.', - }).localize(...languages), - below90Worsening: new LocalizedText({ - en: 'Your heart symptoms score has decreased. This means you are feeling worse. Consider talking to your care team about adjusting your heart failure medications as these have been shown to improve symptoms long term.', - }).localize(...languages), + medicationRecommendationsSection: { + title: localize({ + en: 'POTENTIAL MED CHANGES TO HELP HEART', + }), + description: localize({ + en: 'These potential changes are based on your meds, vital signs, and lab values. These changes are expected to help your heart work better. Discuss these potential changes with your care team at your next clinic appointment.', + }), + hint: localize({ + en: 'Aim to make one positive change!', + }), + }, + symptomScoresSummarySection: { + title: localize({ + en: 'SYMPTOM [KCCQ-12] REPORT', + }), + description: localize({ + en: 'These symptom scores range from 0-100. 0 indicates severe symptoms. 100 indicates you are doing extremely well. Blue line is current and grey line is previous. This is the overall score. The Overall Score is an average of physical limits, social limits, quality of life, and symptoms. Detailed results for each are on the Table on page 2.', + }), + personalSummary(input: { + previousScore: SymptomScore | null + currentScore: SymptomScore | null + }): string | undefined { + const currentScore = input.currentScore + const previousScore = input.previousScore + if (currentScore !== null && previousScore !== null) { + const currentScoreText = currentScore.overallScore.toString() + '%' + const previousScoreText = previousScore.overallScore.toString() + '%' + + if (currentScore.overallScore >= 90) { + if (currentScore.overallScore - previousScore.overallScore >= 10) { + return localize({ + en: `Your heart symptoms score increased from ${previousScoreText} to ${currentScoreText}. This means you are feeling better. Your score is overall very good. Continuing to take your meds will be important for keeping you feeling well.`, + }) + } else { + return localize({ + en: 'Your heart symptom score remains very good. Continuing to take your meds will be important for keeping you feeling well.', + }) + } + } else { + const improvement = + currentScore.overallScore - previousScore.overallScore + if (improvement >= 10) { + return localize({ + en: `Your heart symptoms score increased from ${previousScoreText} to ${currentScoreText}. This means you have been feeling better. There is still room to continue improving how you feel. Getting on the best doses of heart failure medicines can help you feeling better. Consider discussing further with your care team.`, + }) + } else if (improvement > -10) { + return localize({ + en: 'Your heart symptom score is stable. There is still room to continue improving how you feel. Getting on the best doses of heart failure medicines can help you feeling better. Consider discussing further with your care team.', + }) + } else { + return localize({ + en: `Your heart symptoms score decreased from ${previousScoreText} to ${currentScoreText}. This means you are feeling worse. Consider talking to your care team about adjusting your heart failure medications as these have been shown to decrease symptoms long term.`, + }) + } + } + } + return undefined }, }, - symptomScoresTable: { - dateHeader: new LocalizedText({ + symptomScoresTableSection: { + title: localize({ + en: 'Symptom Scores [KCCQ-12] Over Time', + }), + description: localize({ + en: 'This is a detailed report of your symptom scores over time. The graph above shows the overall score. 100 is better and 0 is worse. Each KCCQ-12 question is from one of these categories. Your Overall Score is the average of the other categories.', + }), + dateHeader: localize({ en: '', - }).localize(...languages), - overallScoreHeader: new LocalizedText({ + }), + overallScoreHeader: localize({ en: 'Overall Score', - }).localize(...languages), - physicalLimitsScoreHeader: new LocalizedText({ + }), + physicalLimitsScoreHeader: localize({ en: 'Physical Limits', - }).localize(...languages), - socialLimitsScoreHeader: new LocalizedText({ + }), + socialLimitsScoreHeader: localize({ en: 'Social Limits', - }).localize(...languages), - qualityOfLifeScoreHeader: new LocalizedText({ + }), + qualityOfLifeScoreHeader: localize({ en: 'Quality of Life', - }).localize(...languages), - symptomFrequencyScoreHeader: new LocalizedText({ + }), + symptomFrequencyScoreHeader: localize({ en: 'Heart Failure Symptoms', - }).localize(...languages), - dizzinessScoreHeader: new LocalizedText({ + }), + dizzinessScoreHeader: localize({ en: 'Dizziness', - }).localize(...languages), + }), formatDate(date: Date) { return formatDate(date) }, }, - detailedVitalsSection: { - title: new LocalizedText({ - en: 'DETAILS OF VITALS', - }).localize(...languages), - bodyWeightTitle: new LocalizedText({ + vitalsSection: { + title: localize({ + en: 'VITALS OVER LAST 2 WEEKS', + }), + bodyWeightTitle: localize({ en: 'Weight', - }).localize(...languages), + }), bodyWeightTable: { - titleHeader: new LocalizedText({ + titleHeader: localize({ en: '', - }).localize(...languages), - currentHeader: new LocalizedText({ + }), + currentHeader: localize({ en: 'Current', - }).localize(...languages), - sevenDayAverageHeader: new LocalizedText({ + }), + sevenDayAverageHeader: localize({ en: '7-Day Average', - }).localize(...languages), - lastVisitHeader: new LocalizedText({ - en: 'Last Visit', - }).localize(...languages), - rangeHeader: new LocalizedText({ + }), + rangeHeader: localize({ en: 'Range', - }).localize(...languages), - rowTitle: new LocalizedText({ + }), + rowTitle: localize({ en: 'Weight', - }).localize(...languages), + }), }, - heartRateTitle: new LocalizedText({ + heartRateTitle: localize({ en: 'Heart Rate', - }).localize(...languages), + }), heartRateTable: { - titleHeader: new LocalizedText({ + titleHeader: localize({ en: '', - }).localize(...languages), - medianHeader: new LocalizedText({ + }), + medianHeader: localize({ en: 'Median', - }).localize(...languages), - iqrHeader: new LocalizedText({ + }), + iqrHeader: localize({ en: 'IQR', - }).localize(...languages), - percentageUnder50Header: new LocalizedText({ + }), + percentageUnder50Header: localize({ en: '% Under 50', - }).localize(...languages), - percentageOver120Header: new LocalizedText({ + }), + percentageOver120Header: localize({ en: '% Over 120', - }).localize(...languages), - rowTitle: new LocalizedText({ + }), + rowTitle: localize({ en: 'Heart Rate', - }).localize(...languages), + }), }, - systolicBloodPressureTitle: new LocalizedText({ + systolicBloodPressureTitle: localize({ en: 'Systolic Blood Pressure', - }).localize(...languages), - diastolicBloodPressureTitle: new LocalizedText({ + }), + diastolicBloodPressureTitle: localize({ en: 'Diastolic Blood Pressure', - }).localize(...languages), + }), bloodPressureTable: { - titleHeader: new LocalizedText({ + titleHeader: localize({ en: '', - }).localize(...languages), - medianHeader: new LocalizedText({ + }), + medianHeader: localize({ en: 'Median', - }).localize(...languages), - iqrHeader: new LocalizedText({ + }), + iqrHeader: localize({ en: 'IQR', - }).localize(...languages), - percentageUnder90Header: new LocalizedText({ + }), + percentageUnder90Header: localize({ en: '% Under 90 mmHg', - }).localize(...languages), - percentageOver180Header: new LocalizedText({ + }), + percentageOver180Header: localize({ en: '% Over 180 mmHg', - }).localize(...languages), - systolicRowTitle: new LocalizedText({ + }), + systolicRowTitle: localize({ en: 'Systolic', - }).localize(...languages), - diastolicRowTitle: new LocalizedText({ + }), + diastolicRowTitle: localize({ en: 'Diastolic', - }).localize(...languages), + }), }, }, } @@ -313,15 +310,9 @@ function formatDate(date: Date): string { }) } -function formatValue( - value: number | null, - unit: QuantityUnit | null, - options: { fractionalDigitCount: number } = { - fractionalDigitCount: 0, - }, -): string { - if (value === null) return '---' - const valueString = value.toFixed(options.fractionalDigitCount) - if (unit === null) return valueString - return `${valueString} ${unit.unit}` +function formatTime(date: Date): string { + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + }) } diff --git a/functions/src/healthSummary/generate.ts b/functions/src/healthSummary/generate.ts index 88fdaf30..502cc0cc 100644 --- a/functions/src/healthSummary/generate.ts +++ b/functions/src/healthSummary/generate.ts @@ -6,10 +6,7 @@ // SPDX-License-Identifier: MIT // -import fs from 'fs' -import { Resvg, type ResvgRenderOptions } from '@resvg/resvg-js' import { - average, percentage, presortedMedian, presortedPercentile, @@ -17,16 +14,12 @@ import { UserMedicationRecommendationType, } from '@stanfordbdhg/engagehf-models' import { logger } from 'firebase-functions' -import { jsPDF } from 'jspdf' import 'jspdf-autotable' /* eslint-disable-line */ -import { - type Styles, - type CellDef, - type UserOptions, -} from 'jspdf-autotable' /* eslint-disable-line */ +import { type CellDef } from 'jspdf-autotable' /* eslint-disable-line */ import { healthSummaryLocalizations } from './generate+localizations.js' import { generateChartSvg } from './generateChart.js' import { generateSpeedometerSvg } from './generateSpeedometer.js' +import { PdfGenerator } from './pdfGenerator.js' import { type HealthSummaryData } from '../models/healthSummaryData.js' export interface HealthSummaryOptions { @@ -58,192 +51,113 @@ export function generateHealthSummary( logger.debug( `generateHealthSummary: ${data.symptomScores.length} symptom scores.`, ) - const generator = new HealthSummaryPDFGenerator(data, options) - generator.addFirstPage() - generator.addSecondPage() - return generator.finish() -} + const generator = new HealthSummaryPdfGenerator(data, options) -enum FontStyle { - normal = 'normal', - bold = 'bold', -} + generator.addPageHeader() + generator.addKeyPointsSection() + generator.addCurrentMedicationSection() + generator.addMedicationRecommendationsSection() + generator.addSymptomScoresSummarySection() -interface TextStyle { - fontName: string - fontStyle: FontStyle - fontWeight?: string - fontSize: number - color?: [number, number, number] + generator.newPage() + generator.addSymptomScoresTableSection() + generator.addVitalsSection() + + return generator.finish() } -class HealthSummaryPDFGenerator { +class HealthSummaryPdfGenerator extends PdfGenerator { // Properties data: HealthSummaryData options: HealthSummaryOptions - doc: jsPDF - pageWidth = 612 - pageHeight = 792 - margins = { top: 50, bottom: 50, left: 40, right: 40 } - cursor = { x: this.margins.left, y: this.margins.top } - - colors = { - black: [0, 0, 0] as [number, number, number], - primary: [0, 117, 116] as [number, number, number], - lightGray: [211, 211, 211] as [number, number, number], - } - texts: ReturnType - fontName = 'Open Sans' - textStyles = { - h1: { - fontName: this.fontName, - fontStyle: FontStyle.bold, - fontSize: 18, - } as TextStyle, - h2: { - fontName: this.fontName, - fontStyle: FontStyle.normal, - fontSize: 16, - color: this.colors.primary, - } as TextStyle, - h3: { - fontName: this.fontName, - fontStyle: FontStyle.normal, - fontSize: 15, - } as TextStyle, - body: { - fontName: this.fontName, - fontStyle: FontStyle.normal, - fontSize: 10, - } as TextStyle, - bodyColored: { - fontName: this.fontName, - fontStyle: FontStyle.normal, - fontSize: 10, - color: this.colors.primary, - } as TextStyle, - bodyColoredBold: { - fontName: this.fontName, - fontStyle: FontStyle.bold, - fontSize: 10, - color: this.colors.primary, - } as TextStyle, - bodyBold: { - fontName: this.fontName, - fontStyle: FontStyle.bold, - fontSize: 10, - } as TextStyle, - } - // Constructor constructor(data: HealthSummaryData, options: HealthSummaryOptions) { + super() this.data = data this.options = options this.texts = healthSummaryLocalizations(options.languages) - this.doc = new jsPDF('p', 'pt', [this.pageWidth, this.pageHeight], true) - this.addFont( - 'resources/fonts/OpenSans-Regular.ttf', - this.fontName, - FontStyle.normal, - ) - this.addFont( - 'resources/fonts/OpenSans-Bold.ttf', - this.fontName, - FontStyle.bold, - ) } // Methods - addFirstPage() { - this.addPageHeader() - this.addMedicationSection() - this.addVitalsSection() - this.addSymptomScoresSection() - } + addPageHeader() { + this.addText(this.texts.header.title, this.textStyles.h2) + this.addText(this.data.name ?? '---', this.textStyles.h1) + this.addText( + this.texts.header.dateOfBirthLine(this.data.dateOfBirth ?? null), + ) + this.addText(this.texts.header.providerLine(this.data.providerName ?? null)) + this.addText( + this.texts.header.nextAppointmentLine(this.data.nextAppointment ?? null), + ) - addSecondPage() { - this.addPage() - this.addVitalsChartsSection() - } + const innerWidth = this.pageWidth - this.margins.left - this.margins.right + const pageNumberText = this.texts.header.pageNumberTitle( + this.doc.getNumberOfPages(), + ) + const pageNumberWidth = this.doc.getTextWidth(pageNumberText) + this.cursor.x = this.margins.left + innerWidth - pageNumberWidth + this.cursor.y -= this.textStyles.body.fontSize + this.addText(pageNumberText, this.textStyles.body, pageNumberWidth) + this.cursor.x = this.margins.left - finish(): Buffer { - logger.debug( - `HealthSummaryPDFGenerator.finish: ${this.doc.getNumberOfPages()} pages total.`, + this.addLine( + { x: this.cursor.x, y: this.cursor.y }, + { x: this.cursor.x + innerWidth, y: this.cursor.y }, + this.colors.black, + 1, ) - return Buffer.from(this.doc.output('arraybuffer')) + this.moveDown(this.textStyles.body.fontSize / 2) } - // Helpers - - private addMedicationSection() { - this.addSectionTitle(this.texts.medicationsSection.title) - this.moveDown(4) + addKeyPointsSection() { + this.addSectionTitle(this.texts.keyPointsSection.title) - this.splitTwoColumns( - (columnWidth) => { - this.addText( - this.texts.medicationsSection.currentTitle, - this.textStyles.bodyBold, - columnWidth, - ) - this.moveDown(this.textStyles.body.fontSize + 4) - this.addText( - this.texts.medicationsSection.currentText, - this.textStyles.body, - columnWidth, - ) - this.moveDown(this.textStyles.body.fontSize) - }, - (columnWidth) => { - this.addText( - this.texts.medicationsSection.recommendationsTitle, - this.textStyles.bodyBold, - columnWidth, - ) - this.moveDown(this.textStyles.body.fontSize + 4) - this.addText( - this.texts.medicationsSection.recommendationsText, - this.textStyles.body, - columnWidth, - ) - this.moveDown(4) - this.addText( - this.texts.medicationsSection.recommendationsHint, - this.textStyles.bodyColoredBold, - columnWidth, - ) - this.moveDown(this.textStyles.body.fontSize) - }, - ) - - function colorForRecommendationType( - category: UserMedicationRecommendationType, - ): string | undefined { - switch (category) { - case UserMedicationRecommendationType.targetDoseReached: - return 'rgb(0,255,0)' - case UserMedicationRecommendationType.improvementAvailable: - return 'rgb(255,255,0)' - case UserMedicationRecommendationType.notStarted: - return 'rgb(211,211,211)' + const texts = this.texts.keyPointsSection.text({ + recommendations: this.data.recommendationsCategory, + symptomScore: this.data.symptomScoreCategory, + dizziness: this.data.dizzinessCategory, + weight: this.data.weightCategory, + }) + if (texts !== null) { + if (texts.length === 1) { + this.addText(texts[0], this.textStyles.bodyColored) + } else { + this.addList(texts, this.textStyles.bodyColored) } + } else { + this.addText( + this.texts.keyPointsSection.defaultText, + this.textStyles.bodyItalic, + ) } + } + + addCurrentMedicationSection() { + const currentMedication = this.data.recommendations.filter( + (recommendation) => + recommendation.displayInformation.dosageInformation.currentSchedule + .length > 0, + ) + if (currentMedication.length === 0) return + this.addSectionTitle(this.texts.currentMedicationsSection.title) + this.addText( + this.texts.currentMedicationsSection.description, + this.textStyles.bodyItalic, + ) const tableContent: CellDef[][] = [ [ - this.texts.medicationsTable.nameHeader, - this.texts.medicationsTable.doseHeader, - this.texts.medicationsTable.targetDoseHeader, - this.texts.medicationsTable.recommendationHeader, - this.texts.medicationsTable.commentsHeader, - ].map((title) => this.cell(title)), - ...this.data.recommendations.map((recommendation, index) => [ + this.texts.currentMedicationsSection.table.nameHeader, + this.texts.currentMedicationsSection.table.currentDoseHeader, + this.texts.currentMedicationsSection.table.targetDoseHeader, + ].map((title) => this.cell(title, { fontStyle: 'bold' })), + ...currentMedication.map((recommendation) => [ this.cell( '[ ] ' + recommendation.displayInformation.title.localize( @@ -253,7 +167,7 @@ class HealthSummaryPDFGenerator { this.cell( recommendation.displayInformation.dosageInformation.currentSchedule .map((schedule) => - this.texts.medicationsTable.doseSchedule( + this.texts.currentMedicationsSection.table.doseSchedule( schedule, recommendation.displayInformation.dosageInformation.unit, ), @@ -263,31 +177,13 @@ class HealthSummaryPDFGenerator { this.cell( recommendation.displayInformation.dosageInformation.targetSchedule .map((schedule) => - this.texts.medicationsTable.doseSchedule( + this.texts.currentMedicationsSection.table.doseSchedule( schedule, recommendation.displayInformation.dosageInformation.unit, ), ) .join('\n'), - { - fillColor: colorForRecommendationType( - recommendation.displayInformation.type, - ), - }, ), - this.cell( - recommendation.displayInformation.description.localize( - ...this.options.languages, - ), - ), - this.cell('', { - lineWidth: { - bottom: index === this.data.recommendations.length - 1 ? 0.5 : 0, - top: index == 0 ? 0.5 : 0, - left: 0.5, - right: 0.5, - }, - }), ]), ] @@ -295,171 +191,137 @@ class HealthSummaryPDFGenerator { this.moveDown(this.textStyles.body.fontSize) } - private addVitalsSection() { - this.addSectionTitle(this.texts.vitalsSection.title) - this.moveDown(4) - this.splitTwoColumns( - (columnWidth) => { - const avgSystolic = average( - this.data.vitals.systolicBloodPressure.map( - (observation) => observation.value, - ), - ) - this.addText( - this.texts.vitalsSection.averageSystolicText(avgSystolic ?? null), - this.textStyles.body, - columnWidth, - ) - this.moveDown(4) - const avgDiastolic = average( - this.data.vitals.diastolicBloodPressure.map( - (observation) => observation.value, - ), - ) - this.addText( - this.texts.vitalsSection.averageDiastolicText(avgDiastolic ?? null), - this.textStyles.body, - columnWidth, - ) - this.moveDown(4) - const avgHeartRate = average( - this.data.vitals.heartRate.map((observation) => observation.value), - ) - this.addText( - this.texts.vitalsSection.averageHeartRateText(avgHeartRate ?? null), - this.textStyles.body, - columnWidth, - ) - this.moveDown(this.textStyles.body.fontSize) - }, - (columnWidth) => { - const currentWeight = this.data.vitals.bodyWeight.at(0) - this.addText( - this.texts.vitalsSection.currentBodyWeightText(currentWeight ?? null), - this.textStyles.body, - columnWidth, - ) - this.moveDown(4) - const avgWeight = average( - this.data.vitals.bodyWeight.map((observation) => observation.value), - ) - this.addText( - this.texts.vitalsSection.averageBodyWeightText( - avgWeight !== undefined && currentWeight !== undefined ? - { value: avgWeight, unit: currentWeight.unit } - : null, - ), - this.textStyles.body, - columnWidth, - ) - this.moveDown(4) - this.addText( - this.texts.vitalsSection.dryWeightText( - this.data.vitals.dryWeight ?? null, - ), - this.textStyles.body, - columnWidth, + addMedicationRecommendationsSection() { + const optimizations = this.data.recommendations.filter((recommendation) => + [ + UserMedicationRecommendationType.improvementAvailable, + UserMedicationRecommendationType.notStarted, + ].includes(recommendation.displayInformation.type), + ) + if (optimizations.length === 0) return + this.addSectionTitle(this.texts.medicationRecommendationsSection.title) + + this.addList( + optimizations.map((recommendation) => { + const title = recommendation.displayInformation.title.localize( + ...this.options.languages, ) - this.moveDown(this.textStyles.body.fontSize) - }, + const description = + recommendation.displayInformation.description.localize( + ...this.options.languages, + ) + return `${title}: ${description}` + }), + this.textStyles.bodyColored, + ) + + this.moveDown(this.textStyles.body.fontSize / 2) + this.addText( + this.texts.medicationRecommendationsSection.description, + this.textStyles.bodyItalic, + ) + this.addText( + this.texts.medicationRecommendationsSection.hint, + this.textStyles.bodyColoredBoldItalic, ) } - private addSymptomScoresSection() { - this.addSectionTitle(this.texts.symptomScoresSection.title) - this.moveDown(4) + addSymptomScoresSummarySection() { + if (this.data.symptomScores.length === 0) return + this.addSectionTitle(this.texts.symptomScoresSummarySection.title) this.splitTwoColumns( (columnWidth) => { this.addSpeedometer(columnWidth) }, (columnWidth) => { - this.moveDown(this.textStyles.body.fontSize) - this.texts.symptomScoresSection.description - .split('\n') - .forEach((line) => { - this.addText(line, this.textStyles.body, columnWidth) - this.moveDown(4) - }) - - const currentScore = this.data.symptomScores.at(0) - const previousScore = this.data.symptomScores.at(1) - if ( - this.data.symptomScores.length >= 2 && - currentScore !== undefined && - previousScore !== undefined - ) { - let personalSummaryText = - this.texts.symptomScoresSection.personalSummary.title + ' ' - - if (currentScore.overallScore >= 90) { - if (currentScore.overallScore - previousScore.overallScore >= 10) { - personalSummaryText += - this.texts.symptomScoresSection.personalSummary.above90Improving - } else { - personalSummaryText = - this.texts.symptomScoresSection.personalSummary - .above90NotImproving - } - } else { - const improvement = - currentScore.overallScore - previousScore.overallScore - if (improvement >= 10) { - personalSummaryText += - this.texts.symptomScoresSection.personalSummary.below90Improving - } else if (improvement > -10) { - personalSummaryText += - this.texts.symptomScoresSection.personalSummary.below90Stable - } else { - personalSummaryText += - this.texts.symptomScoresSection.personalSummary.below90Worsening - } - } + this.addText( + this.texts.symptomScoresSummarySection.description, + this.textStyles.bodyItalic, + columnWidth, + ) + this.moveDown(this.textStyles.body.fontSize / 2) + const personalSummaryText = + this.texts.symptomScoresSummarySection.personalSummary({ + currentScore: this.data.latestSymptomScore, + previousScore: this.data.secondLatestSymptomScore, + }) + if (personalSummaryText !== undefined) { this.addText( personalSummaryText, this.textStyles.bodyColored, columnWidth, ) - this.moveDown(this.textStyles.body.fontSize) } }, ) + } + + addSymptomScoresTableSection() { + if (this.data.symptomScores.length === 0) return + this.addSectionTitle(this.texts.symptomScoresTableSection.title) + this.addText( + this.texts.symptomScoresTableSection.description, + this.textStyles.bodyItalic, + ) const tableContent: CellDef[][] = [ [ - this.texts.symptomScoresTable.dateHeader, - this.texts.symptomScoresTable.overallScoreHeader, - this.texts.symptomScoresTable.physicalLimitsScoreHeader, - this.texts.symptomScoresTable.socialLimitsScoreHeader, - this.texts.symptomScoresTable.qualityOfLifeScoreHeader, - this.texts.symptomScoresTable.symptomFrequencyScoreHeader, - this.texts.symptomScoresTable.dizzinessScoreHeader, + this.texts.symptomScoresTableSection.dateHeader, + this.texts.symptomScoresTableSection.overallScoreHeader, + this.texts.symptomScoresTableSection.physicalLimitsScoreHeader, + this.texts.symptomScoresTableSection.socialLimitsScoreHeader, + this.texts.symptomScoresTableSection.qualityOfLifeScoreHeader, + this.texts.symptomScoresTableSection.symptomFrequencyScoreHeader, + this.texts.symptomScoresTableSection.dizzinessScoreHeader, ].map((title) => this.cell(title)), - ...[...this.data.symptomScores].reverse().map((score, index) => [ - this.cell(this.texts.symptomScoresTable.formatDate(score.date), { - fontStyle: - index == this.data.symptomScores.length - 1 ? 'bold' : 'normal', + ...this.data.symptomScores.map((score, index) => [ + this.cell(this.texts.symptomScoresTableSection.formatDate(score.date), { + fontStyle: index === 0 ? 'bold' : 'normal', + }), + this.cell(score.overallScore.toFixed(0), { + fontStyle: index === 0 ? 'bold' : 'normal', + }), + this.cell(score.physicalLimitsScore?.toFixed(0) ?? '---', { + fontStyle: index === 0 ? 'bold' : 'normal', + }), + this.cell(score.socialLimitsScore?.toFixed(0) ?? '---', { + fontStyle: index === 0 ? 'bold' : 'normal', + }), + this.cell(score.qualityOfLifeScore?.toFixed(0) ?? '---', { + fontStyle: index === 0 ? 'bold' : 'normal', + }), + this.cell(score.symptomFrequencyScore?.toFixed(0) ?? '---', { + fontStyle: index === 0 ? 'bold' : 'normal', + }), + this.cell(score.dizzinessScore.toFixed(0), { + fontStyle: index === 0 ? 'bold' : 'normal', }), - this.cell(score.overallScore.toFixed(0)), - this.cell(score.physicalLimitsScore?.toFixed(0) ?? '---'), - this.cell(score.socialLimitsScore?.toFixed(0) ?? '---'), - this.cell(score.qualityOfLifeScore?.toFixed(0) ?? '---'), - this.cell(score.symptomFrequencyScore?.toFixed(0) ?? '---'), - this.cell(score.dizzinessScore.toFixed(0)), ]), ] this.addTable(tableContent) this.moveDown(this.textStyles.body.fontSize) } - private addVitalsChartsSection() { - this.addSectionTitle(this.texts.detailedVitalsSection.title) + addVitalsSection() { + this.addSectionTitle(this.texts.vitalsSection.title) + this.addWeightAndHeartRateVitalsPart() + this.addBloodPressureVitalsPart() + } + + private addWeightAndHeartRateVitalsPart() { + if ( + this.data.vitals.bodyWeight.length === 0 && + this.data.vitals.heartRate.length === 0 + ) + return this.splitTwoColumns( (columnWidth) => { + if (this.data.vitals.bodyWeight.length === 0) return this.addText( - this.texts.detailedVitalsSection.bodyWeightTitle, + this.texts.vitalsSection.bodyWeightTitle, this.textStyles.bodyBold, columnWidth, ) @@ -468,40 +330,28 @@ class HealthSummaryPDFGenerator { columnWidth, this.data.vitals.dryWeight?.value, ) - const bodyWeightValues = this.data.vitals.bodyWeight.map( - (observation) => observation.value, - ) - const avgWeight = average(bodyWeightValues) - const maxWeight = Math.max(...bodyWeightValues) - const minWeight = Math.min(...bodyWeightValues) - this.addTable( [ [ - this.texts.detailedVitalsSection.bodyWeightTable.titleHeader, - this.texts.detailedVitalsSection.bodyWeightTable.currentHeader, - this.texts.detailedVitalsSection.bodyWeightTable - .sevenDayAverageHeader, - this.texts.detailedVitalsSection.bodyWeightTable.lastVisitHeader, - this.texts.detailedVitalsSection.bodyWeightTable.rangeHeader, + this.texts.vitalsSection.bodyWeightTable.titleHeader, + this.texts.vitalsSection.bodyWeightTable.currentHeader, + this.texts.vitalsSection.bodyWeightTable.sevenDayAverageHeader, + this.texts.vitalsSection.bodyWeightTable.rangeHeader, ].map((title) => this.cell(title)), [ - this.texts.detailedVitalsSection.bodyWeightTable.rowTitle, - this.data.vitals.bodyWeight.at(0)?.value.toFixed(0) ?? '---', - avgWeight?.toFixed(0) ?? '---', - '-', - isFinite(maxWeight) && isFinite(minWeight) ? - (maxWeight - minWeight).toFixed(0) - : '---', + this.texts.vitalsSection.bodyWeightTable.rowTitle, + this.data.latestBodyWeight?.toFixed(0) ?? '---', + this.data.lastSevenDayAverageBodyWeight?.toFixed(0) ?? '---', + this.data.bodyWeightRange?.toFixed(0) ?? '---', ].map((title) => this.cell(title)), ], columnWidth, ) - this.moveDown(this.textStyles.body.fontSize * 2) }, (columnWidth) => { + if (this.data.vitals.heartRate.length === 0) return this.addText( - this.texts.detailedVitalsSection.heartRateTitle, + this.texts.vitalsSection.heartRateTitle, this.textStyles.bodyBold, columnWidth, ) @@ -514,16 +364,14 @@ class HealthSummaryPDFGenerator { this.addTable( [ [ - this.texts.detailedVitalsSection.heartRateTable.titleHeader, - this.texts.detailedVitalsSection.heartRateTable.medianHeader, - this.texts.detailedVitalsSection.heartRateTable.iqrHeader, - this.texts.detailedVitalsSection.heartRateTable - .percentageUnder50Header, - this.texts.detailedVitalsSection.heartRateTable - .percentageOver120Header, + this.texts.vitalsSection.heartRateTable.titleHeader, + this.texts.vitalsSection.heartRateTable.medianHeader, + this.texts.vitalsSection.heartRateTable.iqrHeader, + this.texts.vitalsSection.heartRateTable.percentageUnder50Header, + this.texts.vitalsSection.heartRateTable.percentageOver120Header, ].map((title) => this.cell(title)), [ - this.texts.detailedVitalsSection.heartRateTable.rowTitle, + this.texts.vitalsSection.heartRateTable.rowTitle, presortedMedian(values)?.toFixed(0) ?? '---', upperMedian && lowerMedian ? (upperMedian - lowerMedian).toFixed(0) @@ -534,22 +382,29 @@ class HealthSummaryPDFGenerator { ], columnWidth, ) - this.moveDown(this.textStyles.body.fontSize * 2) }, ) + this.moveDown(this.textStyles.body.fontSize * 2) + } + private addBloodPressureVitalsPart() { + const hasSystolic = this.data.vitals.systolicBloodPressure.length > 0 + const hasDiastolic = this.data.vitals.diastolicBloodPressure.length > 0 + if (!hasSystolic && !hasDiastolic) return this.splitTwoColumns( (columnWidth) => { + if (!hasSystolic) return this.addText( - this.texts.detailedVitalsSection.systolicBloodPressureTitle, + this.texts.vitalsSection.systolicBloodPressureTitle, this.textStyles.bodyBold, columnWidth, ) this.addChart(this.data.vitals.systolicBloodPressure, columnWidth) }, (columnWidth) => { + if (!hasDiastolic) return this.addText( - this.texts.detailedVitalsSection.diastolicBloodPressureTitle, + this.texts.vitalsSection.diastolicBloodPressureTitle, this.textStyles.bodyBold, columnWidth, ) @@ -575,95 +430,55 @@ class HealthSummaryPDFGenerator { this.addTable([ [ - this.texts.detailedVitalsSection.bloodPressureTable.titleHeader, - this.texts.detailedVitalsSection.bloodPressureTable.medianHeader, - this.texts.detailedVitalsSection.bloodPressureTable.iqrHeader, - this.texts.detailedVitalsSection.bloodPressureTable - .percentageUnder90Header, - this.texts.detailedVitalsSection.bloodPressureTable - .percentageOver180Header, - ].map((title) => this.cell(title)), - [ - this.texts.detailedVitalsSection.bloodPressureTable.systolicRowTitle, - presortedMedian(systolicValues)?.toFixed(0) ?? '---', - systolicUpperMedian && systolicLowerMedian ? - (systolicUpperMedian - systolicLowerMedian).toFixed(0) - : '---', - percentage(systolicValues, (value) => value < 90)?.toFixed(0) ?? '---', - percentage(systolicValues, (value) => value > 180)?.toFixed(0) ?? '---', - ].map((title) => this.cell(title)), - [ - this.texts.detailedVitalsSection.bloodPressureTable.diastolicRowTitle, - presortedMedian(diastolicValues)?.toFixed(0) ?? '---', - diastolicUpperMedian && diastolicLowerMedian ? - (diastolicUpperMedian - diastolicLowerMedian).toFixed(0) - : '---', - '-', - '-', + this.texts.vitalsSection.bloodPressureTable.titleHeader, + this.texts.vitalsSection.bloodPressureTable.medianHeader, + this.texts.vitalsSection.bloodPressureTable.iqrHeader, + this.texts.vitalsSection.bloodPressureTable.percentageUnder90Header, + this.texts.vitalsSection.bloodPressureTable.percentageOver180Header, ].map((title) => this.cell(title)), + (hasSystolic ? + [ + this.texts.vitalsSection.bloodPressureTable.systolicRowTitle, + presortedMedian(systolicValues)?.toFixed(0) ?? '---', + systolicUpperMedian && systolicLowerMedian ? + (systolicUpperMedian - systolicLowerMedian).toFixed(0) + : '---', + percentage(systolicValues, (value) => value < 90)?.toFixed(0) ?? + '---', + percentage(systolicValues, (value) => value > 180)?.toFixed(0) ?? + '---', + ] + : ['', '', '', '', ''] + ).map((title) => this.cell(title)), + (hasDiastolic ? + [ + this.texts.vitalsSection.bloodPressureTable.diastolicRowTitle, + presortedMedian(diastolicValues)?.toFixed(0) ?? '---', + diastolicUpperMedian && diastolicLowerMedian ? + (diastolicUpperMedian - diastolicLowerMedian).toFixed(0) + : '---', + '-', + '-', + ] + : ['', '', '', '', ''] + ).map((title) => this.cell(title)), ]) - this.moveDown(this.textStyles.body.fontSize * 2) - } - - private addPage() { - this.doc.addPage([this.pageWidth, this.pageHeight]) - this.cursor = { x: this.margins.left, y: this.margins.top } - this.addPageHeader() - } - - private addPageHeader() { - this.addText(this.texts.header.title, this.textStyles.h2) - this.moveDown(4) - this.addText(this.data.name ?? '---', this.textStyles.h1) - this.moveDown(4) - this.addText( - this.texts.header.dateOfBirthLine(this.data.dateOfBirth ?? null), - ) - this.moveDown(4) - this.addText(this.texts.header.providerLine(this.data.providerName ?? null)) - this.moveDown(4) - this.addText( - this.texts.header.nextAppointmentLine(this.data.nextAppointment ?? null), - ) - - const innerWidth = this.pageWidth - this.margins.left - this.margins.right - const pageNumberText = this.texts.header.pageNumberTitle( - this.doc.getNumberOfPages(), - ) - const pageNumberWidth = this.doc.getTextWidth(pageNumberText) - this.cursor.x = this.margins.left + innerWidth - pageNumberWidth - this.cursor.y -= this.textStyles.body.fontSize - this.addText(pageNumberText, this.textStyles.body, pageNumberWidth) - this.cursor.x = this.margins.left - - this.moveDown(8) - this.addLine( - { x: this.cursor.x, y: this.cursor.y }, - { x: this.cursor.x + innerWidth, y: this.cursor.y }, - this.colors.black, - 1, - ) - this.moveDown(8) + this.moveDown(this.textStyles.body.fontSize) } - private addSectionTitle(title: string) { - this.moveDown(8) - this.addText(title, this.textStyles.h3) - this.moveDown(4) - } + // Helpers private addChart(data: Observation[], maxWidth?: number, baseline?: number) { const width = maxWidth ?? this.pageWidth - this.cursor.x - this.margins.right - const height = width * 0.75 + const height = width * (9 / 16) const svg = generateChartSvg( data, { width: width, height: height }, - { top: 20, right: 40, bottom: 40, left: 40 }, + { top: 10, right: 20, bottom: 40, left: 20 }, baseline, ) - const img = this.convertSvgToPng(svg) - this.addPNG(img, width) + this.addSvg(svg, width) } private addSpeedometer(maxWidth?: number) { @@ -672,153 +487,6 @@ class HealthSummaryPDFGenerator { const svg = generateSpeedometerSvg(this.data.symptomScores, width, { languages: this.options.languages, }) - const img = this.convertSvgToPng(svg) - this.addPNG(img, width) - } - - private convertSvgToPng(svg: string): Buffer { - const options: ResvgRenderOptions = { - font: { - loadSystemFonts: false, - fontDirs: ['resources/fonts'], - defaultFontFamily: this.fontName, - serifFamily: this.fontName, - sansSerifFamily: this.fontName, - cursiveFamily: this.fontName, - fantasyFamily: this.fontName, - monospaceFamily: this.fontName, - }, - shapeRendering: 2, - textRendering: 1, - imageRendering: 0, - fitTo: { mode: 'zoom', value: 3 }, - } - return new Resvg(svg, options).render().asPng() - } - - private addPNG(data: Buffer, maxWidth?: number) { - const width = - maxWidth ?? this.pageWidth - this.margins.left - this.margins.right - const imgData = 'data:image/png;base64,' + data.toString('base64') - const imgProperties = this.doc.getImageProperties(imgData) - const height = width / (imgProperties.width / imgProperties.height) - this.doc.addImage( - imgData, - this.cursor.x, - this.cursor.y, - width, - height, - undefined, - 'FAST', - ) - this.moveDown(height) - } - - private addTable(rows: CellDef[][], maxWidth?: number) { - const textStyle = this.textStyles.body - const options: UserOptions = { - margin: { left: this.cursor.x }, - theme: 'grid', - startY: this.cursor.y, - tableWidth: - maxWidth ?? this.pageWidth - this.margins.left - this.margins.right, - body: rows, - styles: { - font: textStyle.fontName, - fontStyle: textStyle.fontStyle, - fontSize: textStyle.fontSize, - }, - } - ;(this.doc as any).autoTable(options) // eslint-disable-line - this.cursor.y = (this.doc as any).lastAutoTable.finalY // eslint-disable-line - } - - private addText( - text: string, - textStyle: TextStyle = this.textStyles.body, - maxWidth?: number, - ) { - this.doc.setFont( - textStyle.fontName, - textStyle.fontStyle, - textStyle.fontWeight, - ) - this.doc.setFontSize(textStyle.fontSize) - const previousTextColor = this.doc.getTextColor() - if (textStyle.color) { - this.doc.setTextColor(...textStyle.color) - } - const textWidth = - maxWidth ?? this.pageWidth - this.cursor.x - this.margins.right - const splitText = this.doc.splitTextToSize(text, textWidth) as string[] - for (const textSegment of splitText) { - this.doc.text( - textSegment, - this.cursor.x, - this.cursor.y + textStyle.fontSize / 2, - ) - this.cursor.y += textStyle.fontSize - } - if (textStyle.color) { - this.doc.setTextColor(previousTextColor) - } - } - - private addLine( - start: { x: number; y: number }, - end: { x: number; y: number }, - color: [number, number, number], - width: number, - ) { - this.doc.setLineWidth(width) - this.doc.setDrawColor(color[0], color[1], color[2]) - this.doc.line(start.x, start.y, end.x, end.y) - } - - private splitTwoColumns( - firstColumn: (width: number) => void, - secondColumn: (width: number) => void, - ) { - const cursorBeforeSplit = structuredClone(this.cursor) - const splitMargin = 8 - const innerWidth = this.pageWidth - this.margins.left - this.margins.right - const columnWidth = innerWidth / 2 - splitMargin - firstColumn(columnWidth) - const firstColumnMaxY = this.cursor.y - this.cursor = structuredClone(cursorBeforeSplit) - this.cursor.x += columnWidth + splitMargin - secondColumn(columnWidth) - this.cursor = { - x: cursorBeforeSplit.x, - y: Math.max(firstColumnMaxY, this.cursor.y), - } - } - - private moveDown(deltaY: number) { - this.cursor.y += deltaY - } - - private addFont(file: string, name: string, style: FontStyle) { - const fontFileContent = fs.readFileSync(file).toString('base64') - const fileName = file.split('/').at(-1) ?? file - this.doc.addFileToVFS(fileName, fontFileContent) - this.doc.addFont(fileName, name, style.toString()) - } - - private cell(title: string, styles: Partial = {}): CellDef { - styles.cellPadding = styles.cellPadding ?? { vertical: 0, horizontal: 0 } - styles.cellPadding = styles.fontSize ?? 4 - styles.lineWidth = styles.lineWidth ?? { - top: 0.5, - bottom: 0.5, - left: 0.5, - right: 0.5, - } - styles.textColor = styles.textColor ?? 'black' - styles.lineColor = styles.lineColor ?? this.colors.black - return { - styles: styles, - title: title, - } + this.addSvg(svg, width) } } diff --git a/functions/src/healthSummary/keyPointsMessage.test.ts b/functions/src/healthSummary/keyPointsMessage.test.ts new file mode 100644 index 00000000..68dede71 --- /dev/null +++ b/functions/src/healthSummary/keyPointsMessage.test.ts @@ -0,0 +1,121 @@ +// +// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import { LocalizedText } from '@stanfordbdhg/engagehf-models' +import { expect } from 'chai' +import { + HealthSummaryDizzinessCategory, + type HealthSummaryKeyPointMessage, + healthSummaryKeyPointMessages, + healthSummaryKeyPointTexts, + HealthSummaryMedicationRecommendationsCategory, + HealthSummarySymptomScoreCategory, + HealthSummaryWeightCategory, +} from './keyPointsMessage.js' +import { readCsv } from '../tests/helpers/csv.js' + +describe('keyPointsMessage', () => { + it('should generate the key point message json', () => { + const keyPointMessages: HealthSummaryKeyPointMessage[] = [] + readCsv('src/tests/resources/keyPointMessages.csv', 55, (line, index) => { + if (index === 0) return + + const recommendations = + Object.values(HealthSummaryMedicationRecommendationsCategory).find( + (category) => line[0] === category.toString(), + ) ?? null + const symptoms = + Object.values(HealthSummarySymptomScoreCategory).find( + (category) => line[1] === category.toString(), + ) ?? null + const dizziness = + Object.values(HealthSummaryDizzinessCategory).find( + (category) => line[2] === category.toString(), + ) ?? null + const weight = + Object.values(HealthSummaryWeightCategory).find( + (category) => line[3] === category.toString(), + ) ?? null + const texts = separateKeyPointTexts(line[4]).map( + (text) => new LocalizedText({ en: text }), + ) + + expect(recommendations, `recommendation: ${line[0]}`).to.not.be.null + expect(symptoms, `symptoms: ${line[1]}`).to.not.be.null + expect(dizziness, `dizziness: ${line[2]}`).to.not.be.null + expect(weight, `weight: ${line[3]}`).to.not.be.null + expect(texts).to.not.be.empty + + if ( + recommendations === null || + symptoms === null || + dizziness === null || + weight === null || + texts.length === 0 + ) + expect.fail('Invalid key point message') + + const message: HealthSummaryKeyPointMessage = { + recommendationsCategory: recommendations, + symptomScoreCategory: symptoms, + dizzinessCategory: dizziness, + weightCategory: weight, + texts, + } + + keyPointMessages.push(message) + }) + + expect( + keyPointMessages, + JSON.stringify( + keyPointMessages.map((message) => ({ + ...message, + texts: message.texts.map((text) => text.content), + })), + null, + 2, + ), + ).to.deep.equal(healthSummaryKeyPointMessages.value) + }) + + it('should cover all combinations', () => { + for (const recommendations of Object.values( + HealthSummaryMedicationRecommendationsCategory, + )) { + for (const symptomScore of Object.values( + HealthSummarySymptomScoreCategory, + )) { + for (const dizziness of Object.values(HealthSummaryDizzinessCategory)) { + for (const weight of Object.values(HealthSummaryWeightCategory)) { + const texts = healthSummaryKeyPointTexts({ + recommendations, + symptomScore, + dizziness, + weight, + }) + expect( + texts ?? [], + `${recommendations}, ${symptomScore}, ${dizziness}, ${weight}`, + ).to.not.be.empty + } + } + } + } + }) +}) + +function separateKeyPointTexts(string: string): string[] { + return string + .split('\n') + .map((line) => + (line.match(/[0-9]\.\) .*/g) ? line.substring(4) : line) + .replace(/\s+/g, ' ') + .trim(), + ) +} diff --git a/functions/src/healthSummary/keyPointsMessage.ts b/functions/src/healthSummary/keyPointsMessage.ts new file mode 100644 index 00000000..5854a266 --- /dev/null +++ b/functions/src/healthSummary/keyPointsMessage.ts @@ -0,0 +1,872 @@ +// +// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import { + Lazy, + type LocalizedText, + localizedTextConverter, +} from '@stanfordbdhg/engagehf-models' +import { z } from 'zod' + +export enum HealthSummarySymptomScoreCategory { + HIGH_STABLE_OR_IMPROVING = 'Change >-10 and KCCQ>=90', + LOW_STABLE_OR_IMPROVING = 'Change >-10 and KCCQ<90', + WORSENING = 'Change <-10', +} + +export enum HealthSummaryMedicationRecommendationsCategory { + OPTIMIZATIONS_AVAILABLE = 'Eligible meds for optimization', + OBSERVATIONS_REQUIRED = 'No eligible meds at optimization; measure BP', + AT_TARGET = 'No eligible meds at optimization; at target doses', +} + +export enum HealthSummaryWeightCategory { + INCREASING = 'Weight increase', + MISSING = 'No weight measured', + STABLE_OR_DECREASING = 'No weight gain but weight measured', +} + +export enum HealthSummaryDizzinessCategory { + WORSENING = 'Decrease <-25', + STABLE_OR_IMPROVING = 'No decrease <-25', +} + +export interface HealthSummaryKeyPointMessage { + recommendationsCategory: HealthSummaryMedicationRecommendationsCategory + symptomScoreCategory: HealthSummarySymptomScoreCategory + dizzinessCategory: HealthSummaryDizzinessCategory + weightCategory: HealthSummaryWeightCategory + texts: LocalizedText[] +} + +export function healthSummaryKeyPointTexts(input: { + recommendations: HealthSummaryMedicationRecommendationsCategory + symptomScore: HealthSummarySymptomScoreCategory + dizziness: HealthSummaryDizzinessCategory + weight: HealthSummaryWeightCategory +}): LocalizedText[] | null { + return ( + healthSummaryKeyPointMessages.value.find( + (message) => + message.recommendationsCategory === input.recommendations && + message.symptomScoreCategory === input.symptomScore && + message.dizzinessCategory === input.dizziness && + message.weightCategory === input.weight, + )?.texts ?? null + ) +} + +export const healthSummaryKeyPointMessages = new Lazy< + HealthSummaryKeyPointMessage[] +>(() => + z + .object({ + recommendationsCategory: z.nativeEnum( + HealthSummaryMedicationRecommendationsCategory, + ), + symptomScoreCategory: z.nativeEnum(HealthSummarySymptomScoreCategory), + dizzinessCategory: z.nativeEnum(HealthSummaryDizzinessCategory), + weightCategory: z.nativeEnum(HealthSummaryWeightCategory), + texts: z.array(localizedTextConverter.schema), + }) + .array() + .parse([ + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart.", + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart.", + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart.", + }, + { + en: 'Your heart symptoms worsened (see symptom report). Discuss with your care team how adjusting your medications can help you feel better.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Discuss with your care team potential options for helping you feel better.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart.", + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart.", + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart.", + }, + { + en: 'Your heart symptoms worsened (see symptom report). Discuss with your care team how adjusting your medications can help you feel better.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better.', + }, + { + en: 'You are noting your dizziness is more bothersome. Check your blood pressure and heart rate and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight gain but weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Discuss with your care team potential options for helping you feel better.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart.", + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart.", + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart.", + }, + { + en: 'Your heart symptoms worsened (see symptom report). Discuss with your care team how adjusting your medications can help you feel better.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Discuss with your care team potential options for helping you feel better.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart.", + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + { + en: 'Your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + { + en: 'Your dizziness is bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + { + en: 'Your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart.", + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart.', + }, + { + en: 'Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart.", + }, + { + en: 'Your heart symptoms worsened (see symptom report) and your weight increased. Discuss with your care team how adjusting your medications can help you feel better.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report) and your weight increased. Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better.', + }, + { + en: 'You are noting your dizziness is more bothersome. Check your blood pressure and heart rate and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'Weight increase', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report) and your weight increased. Discuss with your care team potential options for helping you feel better.', + }, + { + en: 'You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart.", + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart.", + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart.", + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team how adjusting your medications can help you feel better.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your blood pressure, heart rate, and weight, and discuss with your care team how adjusting your medicines can help you feel better.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'No decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team potential options for helping you feel better.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart.", + }, + { + en: 'You are noting your dizziness is more bothersome. Check your weight and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Check your blood pressure, heart rate, and weight more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ<90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart.', + }, + { + en: 'You are noting your dizziness is more bothersome. Check your weight and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart.", + }, + { + en: 'Your dizziness is more bothersome. Check your weight and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Check your blood pressure and heart rate multiple times a week to understand if your heart medicines can be adjusted to keep you feeling well and strengthen your heart.', + }, + { + en: 'Your dizziness is more bothersome. Check your blood pressure, heart rate, and weight more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change >-10 and KCCQ>=90', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart.', + }, + { + en: 'Your dizziness is more bothersome. Check your weight and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'Eligible meds for optimization', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: "There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart.", + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team how adjusting your meds can help you feel better.', + }, + { + en: 'Your dizziness is more bothersome. Discuss ways to improve your dizziness with your care team and watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: 'No eligible meds at optimization; measure BP', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your blood pressure, heart rate, and weight and discuss with your care team how adjusting your meds can help you feel better.', + }, + { + en: 'Your dizziness is more bothersome. Discuss ways to improve your dizziness with your care team and watch the dizziness educational video.', + }, + ], + }, + { + recommendationsCategory: + 'No eligible meds at optimization; at target doses', + symptomScoreCategory: 'Change <-10', + dizzinessCategory: 'Decrease <-25', + weightCategory: 'No weight measured', + texts: [ + { + en: 'Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart.', + }, + { + en: 'Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team options for helping you feel better.', + }, + { + en: 'Your dizziness is more bothersome. Discuss ways to improve your dizziness with your care team and watch the dizziness educational video.', + }, + ], + }, + ]), +) diff --git a/functions/src/healthSummary/pdfGenerator.ts b/functions/src/healthSummary/pdfGenerator.ts new file mode 100644 index 00000000..7108ca3f --- /dev/null +++ b/functions/src/healthSummary/pdfGenerator.ts @@ -0,0 +1,333 @@ +// +// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import fs from 'fs' +import { Resvg, type ResvgRenderOptions } from '@resvg/resvg-js' +import { logger } from 'firebase-functions' +import { jsPDF } from 'jspdf' +import 'jspdf-autotable' /* eslint-disable-line */ +import { + type Styles, + type CellDef, + type UserOptions, +} from 'jspdf-autotable' /* eslint-disable-line */ + +enum FontStyle { + normal = 'normal', + bold = 'bold', + italic = 'italic', + boldItalic = 'bolditalic', +} + +interface TextStyle { + fontName: string + fontStyle: FontStyle + fontWeight?: string + fontSize: number + color?: [number, number, number] +} + +export class PdfGenerator { + // Properties + + doc: jsPDF + pageWidth = 612 + pageHeight = 792 + margins = { top: 50, bottom: 50, left: 40, right: 40 } + cursor = { x: this.margins.left, y: this.margins.top } + + colors = { + black: [0, 0, 0] as [number, number, number], + primary: [0, 117, 116] as [number, number, number], + lightGray: [211, 211, 211] as [number, number, number], + } + + fontName = 'Open Sans' + textStyles = { + h1: { + fontName: this.fontName, + fontStyle: FontStyle.bold, + fontSize: 18, + } as TextStyle, + h2: { + fontName: this.fontName, + fontStyle: FontStyle.normal, + fontSize: 16, + color: this.colors.primary, + } as TextStyle, + h3: { + fontName: this.fontName, + fontStyle: FontStyle.normal, + fontSize: 15, + } as TextStyle, + body: { + fontName: this.fontName, + fontStyle: FontStyle.normal, + fontSize: 10, + } as TextStyle, + bodyItalic: { + fontName: this.fontName, + fontStyle: FontStyle.italic, + fontSize: 10, + } as TextStyle, + bodyColored: { + fontName: this.fontName, + fontStyle: FontStyle.normal, + fontSize: 10, + color: this.colors.primary, + } as TextStyle, + bodyColoredItalic: { + fontName: this.fontName, + fontStyle: FontStyle.italic, + fontSize: 10, + color: this.colors.primary, + } as TextStyle, + bodyColoredBold: { + fontName: this.fontName, + fontStyle: FontStyle.bold, + fontSize: 10, + color: this.colors.primary, + } as TextStyle, + bodyColoredBoldItalic: { + fontName: this.fontName, + fontStyle: FontStyle.boldItalic, + fontSize: 10, + color: this.colors.primary, + } as TextStyle, + bodyBold: { + fontName: this.fontName, + fontStyle: FontStyle.bold, + fontSize: 10, + } as TextStyle, + bodyBoldItalic: { + fontName: this.fontName, + fontStyle: FontStyle.boldItalic, + fontSize: 10, + } as TextStyle, + } + + // Constructor + + constructor() { + this.doc = new jsPDF('p', 'pt', [this.pageWidth, this.pageHeight], true) + this.addFont( + 'resources/fonts/OpenSans-Regular.ttf', + this.fontName, + FontStyle.normal, + ) + this.addFont( + 'resources/fonts/OpenSans-Bold.ttf', + this.fontName, + FontStyle.bold, + ) + this.addFont( + 'resources/fonts/OpenSans-Italic.ttf', + this.fontName, + FontStyle.italic, + ) + this.addFont( + 'resources/fonts/OpenSans-BoldItalic.ttf', + this.fontName, + FontStyle.boldItalic, + ) + } + + // Methods + + finish(): Buffer { + logger.debug( + `HealthSummaryPDFGenerator.finish: ${this.doc.getNumberOfPages()} pages total.`, + ) + return Buffer.from(this.doc.output('arraybuffer')) + } + + newPage() { + this.doc.addPage([this.pageWidth, this.pageHeight]) + this.cursor = { x: this.margins.left, y: this.margins.top } + } + + addSectionTitle(title: string) { + this.moveDown(8) + this.addText(title, this.textStyles.h3) + this.moveDown(4) + } + + addSvg(svg: string, maxWidth?: number) { + const img = this.convertSvgToPng(svg) + this.addPng(img, maxWidth) + } + + private convertSvgToPng(svg: string): Buffer { + const options: ResvgRenderOptions = { + font: { + loadSystemFonts: false, + fontDirs: ['resources/fonts'], + defaultFontFamily: this.fontName, + serifFamily: this.fontName, + sansSerifFamily: this.fontName, + cursiveFamily: this.fontName, + fantasyFamily: this.fontName, + monospaceFamily: this.fontName, + }, + shapeRendering: 2, + textRendering: 1, + imageRendering: 0, + fitTo: { mode: 'zoom', value: 3 }, + } + return new Resvg(svg, options).render().asPng() + } + + addPng(data: Buffer, maxWidth?: number) { + const width = + maxWidth ?? this.pageWidth - this.margins.left - this.margins.right + const imgData = 'data:image/png;base64,' + data.toString('base64') + const imgProperties = this.doc.getImageProperties(imgData) + const height = width / (imgProperties.width / imgProperties.height) + this.doc.addImage( + imgData, + this.cursor.x, + this.cursor.y, + width, + height, + undefined, + 'FAST', + ) + this.moveDown(height) + } + + addTable(rows: CellDef[][], maxWidth?: number) { + const textStyle = this.textStyles.body + const options: UserOptions = { + margin: { + left: this.cursor.x, + right: + maxWidth !== undefined ? + maxWidth - this.cursor.x + : this.margins.right, + }, + theme: 'grid', + startY: this.cursor.y, + tableWidth: 'wrap', + body: rows, + styles: { + font: textStyle.fontName, + fontStyle: textStyle.fontStyle, + fontSize: textStyle.fontSize, + }, + } + ;(this.doc as any).autoTable(options) // eslint-disable-line + this.cursor.y = (this.doc as any).lastAutoTable.finalY // eslint-disable-line + } + + addText( + text: string, + textStyle: TextStyle = this.textStyles.body, + maxWidth?: number, + ) { + this.doc.setFont( + textStyle.fontName, + textStyle.fontStyle, + textStyle.fontWeight, + ) + this.doc.setFontSize(textStyle.fontSize) + const previousTextColor = this.doc.getTextColor() + if (textStyle.color) { + this.doc.setTextColor(...textStyle.color) + } + const textWidth = + maxWidth ?? this.pageWidth - this.cursor.x - this.margins.right + const splitText = this.doc.splitTextToSize(text, textWidth) as string[] + for (const textSegment of splitText) { + this.doc.text( + textSegment, + this.cursor.x, + this.cursor.y + textStyle.fontSize / 2, + { + lineHeightFactor: 1.5, + }, + ) + this.cursor.y += textStyle.fontSize * 1.5 + } + if (textStyle.color) { + this.doc.setTextColor(previousTextColor) + } + } + + addList(texts: string[], textStyle: TextStyle) { + texts.forEach((text, index) => { + this.indent(1, textStyle) + this.addText(`${index + 1}.`, textStyle) + this.moveDown(-textStyle.fontSize * 1.5) + this.indent(1.5, textStyle) + this.addText(text, textStyle) + this.indent(-2.5, textStyle) + }) + } + + addLine( + start: { x: number; y: number }, + end: { x: number; y: number }, + color: [number, number, number], + width: number, + ) { + this.doc.setLineWidth(width) + this.doc.setDrawColor(color[0], color[1], color[2]) + this.doc.line(start.x, start.y, end.x, end.y) + } + + splitTwoColumns( + firstColumn: (width: number) => void, + secondColumn: (width: number) => void, + ) { + const cursorBeforeSplit = structuredClone(this.cursor) + const splitMargin = 8 + const innerWidth = this.pageWidth - this.margins.left - this.margins.right + const columnWidth = innerWidth / 2 - splitMargin + firstColumn(columnWidth) + const firstColumnMaxY = this.cursor.y + this.cursor = structuredClone(cursorBeforeSplit) + this.cursor.x += columnWidth + splitMargin + secondColumn(columnWidth) + this.cursor = { + x: cursorBeforeSplit.x, + y: Math.max(firstColumnMaxY, this.cursor.y), + } + } + + moveDown(deltaY: number) { + this.cursor.y += deltaY + } + + private addFont(file: string, name: string, style: FontStyle) { + const fontFileContent = fs.readFileSync(file).toString('base64') + const fileName = file.split('/').at(-1) ?? file + this.doc.addFileToVFS(fileName, fontFileContent) + this.doc.addFont(fileName, name, style.toString()) + } + + cell(title: string, styles: Partial = {}): CellDef { + styles.cellPadding = styles.cellPadding ?? { vertical: 0, horizontal: 0 } + styles.cellPadding = styles.fontSize ?? 4 + styles.lineWidth = styles.lineWidth ?? { + top: 0.5, + bottom: 0.5, + left: 0.5, + right: 0.5, + } + styles.textColor = styles.textColor ?? 'black' + styles.lineColor = styles.lineColor ?? this.colors.black + return { + styles: styles, + title: title, + } + } + + private indent(amount: number, textStyle: TextStyle) { + this.cursor.x += amount * textStyle.fontSize + } +} diff --git a/functions/src/models/healthSummaryData.ts b/functions/src/models/healthSummaryData.ts index fcd01b0f..112b7261 100644 --- a/functions/src/models/healthSummaryData.ts +++ b/functions/src/models/healthSummaryData.ts @@ -7,13 +7,33 @@ // import { + advanceDateByDays, + average, + UserMedicationRecommendationType, type FHIRAppointment, type Observation, type SymptomScore, type UserMedicationRecommendation, } from '@stanfordbdhg/engagehf-models' +import { + HealthSummaryDizzinessCategory, + HealthSummaryMedicationRecommendationsCategory, + HealthSummarySymptomScoreCategory, + HealthSummaryWeightCategory, +} from '../healthSummary/keyPointsMessage.js' + +export interface HealthSummaryVitals { + systolicBloodPressure: Observation[] + diastolicBloodPressure: Observation[] + heartRate: Observation[] + bodyWeight: Observation[] + + dryWeight?: Observation +} + +export class HealthSummaryData { + // Stored Properties -export interface HealthSummaryData { name?: string dateOfBirth?: Date providerName?: string @@ -21,13 +41,141 @@ export interface HealthSummaryData { recommendations: UserMedicationRecommendation[] vitals: HealthSummaryVitals symptomScores: SymptomScore[] -} + now: Date -export interface HealthSummaryVitals { - systolicBloodPressure: Observation[] - diastolicBloodPressure: Observation[] - heartRate: Observation[] - bodyWeight: Observation[] + // Computed Properties - Body Weight - dryWeight?: Observation + get latestBodyWeight(): number | null { + return this.vitals.bodyWeight.at(0)?.value ?? null + } + + get lastSevenDayAverageBodyWeight(): number | null { + const bodyWeightValues = this.vitals.bodyWeight + .filter( + (observation) => observation.date >= advanceDateByDays(this.now, -7), + ) + .map((observation) => observation.value) + return average(bodyWeightValues) ?? null + } + + get medianBodyWeight(): number | null { + return ( + average(this.vitals.bodyWeight.map((observation) => observation.value)) ?? + null + ) + } + + get bodyWeightRange(): number | null { + const bodyWeightValues = this.vitals.bodyWeight.map( + (observation) => observation.value, + ) + const minWeight = Math.min(...bodyWeightValues) + const maxWeight = Math.max(...bodyWeightValues) + return isFinite(minWeight) && isFinite(maxWeight) ? + maxWeight - minWeight + : null + } + + // Computed Properties - Symptom Scores + + get latestSymptomScore(): SymptomScore | null { + return this.symptomScores.at(0) ?? null + } + + get secondLatestSymptomScore(): SymptomScore | null { + return this.symptomScores.at(1) ?? null + } + + // Computed Properties - KeyPoints + + get dizzinessCategory(): HealthSummaryDizzinessCategory | null { + const latestScore = this.latestSymptomScore?.dizzinessScore ?? null + const secondLatestScore = + this.secondLatestSymptomScore?.dizzinessScore ?? null + + if (latestScore === null || secondLatestScore === null) return null + + return latestScore - secondLatestScore < 0 ? + HealthSummaryDizzinessCategory.WORSENING + : HealthSummaryDizzinessCategory.STABLE_OR_IMPROVING + } + + get recommendationsCategory(): HealthSummaryMedicationRecommendationsCategory | null { + const hasOptimizations = this.recommendations.some((recommendation) => + [ + UserMedicationRecommendationType.improvementAvailable, + UserMedicationRecommendationType.notStarted, + ].includes(recommendation.displayInformation.type), + ) + if (hasOptimizations) + return HealthSummaryMedicationRecommendationsCategory.OPTIMIZATIONS_AVAILABLE + + const hasObservationsRequired = this.recommendations.some( + (recommendation) => + [ + UserMedicationRecommendationType.moreLabObservationsRequired, + UserMedicationRecommendationType.morePatientObservationsRequired, + ].includes(recommendation.displayInformation.type), + ) + if (hasObservationsRequired) + return HealthSummaryMedicationRecommendationsCategory.OBSERVATIONS_REQUIRED + + const hasAtTarget = this.recommendations.some((recommendation) => + [ + UserMedicationRecommendationType.personalTargetDoseReached, + UserMedicationRecommendationType.targetDoseReached, + ].includes(recommendation.displayInformation.type), + ) + return hasAtTarget ? + HealthSummaryMedicationRecommendationsCategory.AT_TARGET + : null + } + + get symptomScoreCategory(): HealthSummarySymptomScoreCategory | null { + const latestScore = this.latestSymptomScore + const secondLatestScore = this.secondLatestSymptomScore + + if (latestScore === null || secondLatestScore === null) return null + + if (latestScore.overallScore - secondLatestScore.overallScore <= -10) + return HealthSummarySymptomScoreCategory.WORSENING + + if (latestScore.overallScore < 90) + return HealthSummarySymptomScoreCategory.LOW_STABLE_OR_IMPROVING + + return HealthSummarySymptomScoreCategory.HIGH_STABLE_OR_IMPROVING + } + + get weightCategory(): HealthSummaryWeightCategory { + const medianWeight = this.medianBodyWeight + const latestWeight = this.latestBodyWeight + if (medianWeight === null || latestWeight === null) + return HealthSummaryWeightCategory.MISSING + + return latestWeight - medianWeight >= 3 ? + HealthSummaryWeightCategory.INCREASING + : HealthSummaryWeightCategory.STABLE_OR_DECREASING + } + + // Initialization + + constructor(input: { + name?: string + dateOfBirth?: Date + providerName?: string + nextAppointment?: FHIRAppointment + recommendations: UserMedicationRecommendation[] + vitals: HealthSummaryVitals + symptomScores: SymptomScore[] + now: Date + }) { + this.name = input.name + this.dateOfBirth = input.dateOfBirth + this.providerName = input.providerName + this.nextAppointment = input.nextAppointment + this.recommendations = input.recommendations + this.vitals = input.vitals + this.symptomScores = input.symptomScores + this.now = input.now + } } diff --git a/functions/src/services/healthSummary/databaseHealthSummaryService.test.ts b/functions/src/services/healthSummary/databaseHealthSummaryService.test.ts index 367e97e8..511f1979 100644 --- a/functions/src/services/healthSummary/databaseHealthSummaryService.test.ts +++ b/functions/src/services/healthSummary/databaseHealthSummaryService.test.ts @@ -24,8 +24,10 @@ describe('HealthSummaryService', () => { it('should fetch health summary data', async () => { const actualData = await healthSummaryService.getHealthSummaryData( 'mockUser', + new Date(2024, 2, 2, 12, 30), QuantityUnit.lbs, ) + console.log('actualData:', actualData.nextAppointment?.start.toString()) const expectedData = await mockHealthSummaryData('mockUser') // TODO: Remove the next line to check whether medication optimizations also match the expected value. expectedData.recommendations = [] diff --git a/functions/src/services/healthSummary/databaseHealthSummaryService.ts b/functions/src/services/healthSummary/databaseHealthSummaryService.ts index faddf64e..41b834c9 100644 --- a/functions/src/services/healthSummary/databaseHealthSummaryService.ts +++ b/functions/src/services/healthSummary/databaseHealthSummaryService.ts @@ -12,8 +12,8 @@ import { } from '@stanfordbdhg/engagehf-models' import { type HealthSummaryService } from './healthSummaryService.js' import { + HealthSummaryData, type HealthSummaryVitals, - type HealthSummaryData, } from '../../models/healthSummaryData.js' import { type PatientService } from '../patient/patientService.js' import { type UserService } from '../user/userService.js' @@ -35,6 +35,7 @@ export class DefaultHealthSummaryService implements HealthSummaryService { async getHealthSummaryData( userId: string, + date: Date, weightUnit: QuantityUnit, ): Promise { const [ @@ -50,7 +51,7 @@ export class DefaultHealthSummaryService implements HealthSummaryService { this.patientService.getNextAppointment(userId), this.patientService.getMedicationRecommendations(userId), this.patientService.getSymptomScores(userId, { limit: 5 }), - this.getVitals(userId, advanceDateByDays(new Date(), -14), weightUnit), + this.getVitals(userId, advanceDateByDays(date, -14), weightUnit), ]) const providerName = @@ -59,7 +60,7 @@ export class DefaultHealthSummaryService implements HealthSummaryService { (await this.userService.getAuth(patient.content.clinician)).displayName : undefined) - return { + return new HealthSummaryData({ name: auth.displayName, dateOfBirth: patient?.content.dateOfBirth, providerName: providerName, @@ -67,7 +68,8 @@ export class DefaultHealthSummaryService implements HealthSummaryService { recommendations: recommendations.map((doc) => doc.content), vitals: vitals, symptomScores: symptomScores.map((doc) => doc.content), - } + now: date, + }) } // Helpers diff --git a/functions/src/services/healthSummary/healthSummaryService.mock.ts b/functions/src/services/healthSummary/healthSummaryService.mock.ts index 3d24f3cc..868a3e37 100644 --- a/functions/src/services/healthSummary/healthSummaryService.mock.ts +++ b/functions/src/services/healthSummary/healthSummaryService.mock.ts @@ -17,36 +17,29 @@ import { } from '@stanfordbdhg/engagehf-models' import { type HealthSummaryService } from './healthSummaryService.js' import { + HealthSummaryData, type HealthSummaryVitals, - type HealthSummaryData, } from '../../models/healthSummaryData.js' /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-vars */ export class MockHealthSummaryService implements HealthSummaryService { - // Properties - - private readonly startDate: Date - - // Constructor - - constructor(startDate: Date = new Date('2024-02-02')) { - this.startDate = startDate - } - // Methods - async getHealthSummaryData(userId: string): Promise { - return { + async getHealthSummaryData( + userId: string, + date: Date, + ): Promise { + return new HealthSummaryData({ name: 'John Doe', dateOfBirth: new Date('1970-01-02'), providerName: 'Dr. XXX', nextAppointment: FHIRAppointment.create({ userId, status: FHIRAppointmentStatus.booked, - created: this.startDateAdvancedByDays(-10), - start: this.startDateAdvancedByDays(1), + created: advanceDateByDays(date, -10), + start: advanceDateByDays(date, 1), durationInMinutes: 60, }), recommendations: [ @@ -98,7 +91,7 @@ export class MockHealthSummaryService implements HealthSummaryService { }, }, ], - vitals: await this.getVitals(userId), + vitals: await this.getVitals(date), symptomScores: [ { questionnaireResponseId: '4', @@ -108,7 +101,7 @@ export class MockHealthSummaryService implements HealthSummaryService { qualityOfLifeScore: 20, symptomFrequencyScore: 60, dizzinessScore: 3, - date: this.startDateAdvancedByDays(-9), + date: advanceDateByDays(date, -9), }, { questionnaireResponseId: '3', @@ -118,7 +111,7 @@ export class MockHealthSummaryService implements HealthSummaryService { qualityOfLifeScore: 37, symptomFrequencyScore: 72, dizzinessScore: 2, - date: this.startDateAdvancedByDays(-18), + date: advanceDateByDays(date, -18), }, { questionnaireResponseId: '2', @@ -128,7 +121,7 @@ export class MockHealthSummaryService implements HealthSummaryService { qualityOfLifeScore: 25, symptomFrequencyScore: 60, dizzinessScore: 1, - date: this.startDateAdvancedByDays(-34), + date: advanceDateByDays(date, -34), }, { questionnaireResponseId: '1', @@ -138,123 +131,121 @@ export class MockHealthSummaryService implements HealthSummaryService { qualityOfLifeScore: 60, symptomFrequencyScore: 80, dizzinessScore: 1, - date: this.startDateAdvancedByDays(-49), + date: advanceDateByDays(date, -49), }, ], - } + now: date, + }) } - async getVitals(userId: string): Promise { + // Helpers + + private async getVitals(date: Date): Promise { const [systolicBloodPressure, diastolicBloodPressure] = - await this.getBloodPressureObservations(userId, this.startDate) + await this.getBloodPressureObservations(date) return { systolicBloodPressure: systolicBloodPressure, diastolicBloodPressure: diastolicBloodPressure, - heartRate: await this.getHeartRateObservations(userId, this.startDate), - bodyWeight: await this.getBodyWeightObservations( - userId, - this.startDate, - QuantityUnit.lbs, - ), - dryWeight: await this.getMostRecentDryWeightObservation(userId), + heartRate: await this.getHeartRateObservations(date), + bodyWeight: await this.getBodyWeightObservations(date), + dryWeight: await this.getMostRecentDryWeightObservation(date), } } - async getBloodPressureObservations( - userId: string, - cutoffDate: Date, + private async getBloodPressureObservations( + date: Date, ): Promise<[Observation[], Observation[]]> { return [ [ { - date: this.startDateAdvancedByDays(-1), + date: advanceDateByDays(date, -1), value: 110, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-2), + date: advanceDateByDays(date, -2), value: 114, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-3), + date: advanceDateByDays(date, -3), value: 123, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-4), + date: advanceDateByDays(date, -4), value: 109, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-5), + date: advanceDateByDays(date, -5), value: 105, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-6), + date: advanceDateByDays(date, -6), value: 98, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-7), + date: advanceDateByDays(date, -7), value: 94, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-8), + date: advanceDateByDays(date, -8), value: 104, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-9), + date: advanceDateByDays(date, -9), value: 102, unit: QuantityUnit.mmHg, }, ], [ { - date: this.startDateAdvancedByDays(-1), + date: advanceDateByDays(date, -1), value: 70, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-2), + date: advanceDateByDays(date, -2), value: 82, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-3), + date: advanceDateByDays(date, -3), value: 75, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-4), + date: advanceDateByDays(date, -4), value: 77, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-5), + date: advanceDateByDays(date, -5), value: 72, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-6), + date: advanceDateByDays(date, -6), value: 68, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-7), + date: advanceDateByDays(date, -7), value: 65, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-8), + date: advanceDateByDays(date, -8), value: 72, unit: QuantityUnit.mmHg, }, { - date: this.startDateAdvancedByDays(-9), + date: advanceDateByDays(date, -9), value: 80, unit: QuantityUnit.mmHg, }, @@ -262,156 +253,113 @@ export class MockHealthSummaryService implements HealthSummaryService { ] } - async getHeartRateObservations( - userId: string, - cutoffDate: Date, - ): Promise { + private async getHeartRateObservations(date: Date): Promise { return [ { - date: this.startDateAdvancedByDays(-1), + date: advanceDateByDays(date, -1), value: 79, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-2), + date: advanceDateByDays(date, -2), value: 62, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-3), + date: advanceDateByDays(date, -3), value: 77, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-4), + date: advanceDateByDays(date, -4), value: 63, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-5), + date: advanceDateByDays(date, -5), value: 61, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-6), + date: advanceDateByDays(date, -6), value: 70, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-7), + date: advanceDateByDays(date, -7), value: 67, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-8), + date: advanceDateByDays(date, -8), value: 80, unit: QuantityUnit.bpm, }, { - date: this.startDateAdvancedByDays(-9), + date: advanceDateByDays(date, -9), value: 65, unit: QuantityUnit.bpm, }, ] } - async getBodyWeightObservations( - userId: string, - cutoffDate: Date, - unit: QuantityUnit, - ): Promise { + private async getBodyWeightObservations(date: Date): Promise { return [ { - date: this.startDateAdvancedByDays(-1), + date: advanceDateByDays(date, -1), value: 269, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-2), + date: advanceDateByDays(date, -2), value: 267, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-3), + date: advanceDateByDays(date, -3), value: 267, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-4), + date: advanceDateByDays(date, -4), value: 265, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-5), + date: advanceDateByDays(date, -5), value: 268, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-6), + date: advanceDateByDays(date, -6), value: 268, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-7), + date: advanceDateByDays(date, -7), value: 266, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-8), + date: advanceDateByDays(date, -8), value: 266, unit: QuantityUnit.lbs, }, { - date: this.startDateAdvancedByDays(-9), + date: advanceDateByDays(date, -9), value: 267, unit: QuantityUnit.lbs, }, ] } - async getMostRecentDryWeightObservation( - userId: string, + private async getMostRecentDryWeightObservation( + date: Date, ): Promise { return { - date: this.startDateAdvancedByDays(-4), + date: advanceDateByDays(date, -4), value: 267.5, unit: QuantityUnit.lbs, } } - - async getMostRecentCreatinineObservation( - userId: string, - ): Promise { - return { - date: this.startDateAdvancedByDays(-4), - value: 1.1, - unit: QuantityUnit.mg_dL, - } - } - - async getMostRecentPotassiumObservation( - userId: string, - ): Promise { - return { - date: this.startDateAdvancedByDays(-4), - value: 4.2, - unit: QuantityUnit.mEq_L, - } - } - - async getMostRecentEstimatedGlomerularFiltrationRateObservation( - userId: string, - ): Promise { - return { - date: this.startDateAdvancedByDays(-4), - value: 60, - unit: QuantityUnit.mL_min_173m2, - } - } - - // Helpers - - private startDateAdvancedByDays(days: number): Date { - return advanceDateByDays(this.startDate, days) - } } diff --git a/functions/src/services/healthSummary/healthSummaryService.ts b/functions/src/services/healthSummary/healthSummaryService.ts index 0eab74cf..7e52deea 100644 --- a/functions/src/services/healthSummary/healthSummaryService.ts +++ b/functions/src/services/healthSummary/healthSummaryService.ts @@ -12,6 +12,7 @@ import { type HealthSummaryData } from '../../models/healthSummaryData.js' export interface HealthSummaryService { getHealthSummaryData( userId: string, + date: Date, weightUnit: QuantityUnit, ): Promise } diff --git a/functions/src/services/patient/patientService.mock.ts b/functions/src/services/patient/patientService.mock.ts index b03d6c2c..83817442 100644 --- a/functions/src/services/patient/patientService.mock.ts +++ b/functions/src/services/patient/patientService.mock.ts @@ -33,7 +33,7 @@ export class MockPatientService implements PatientService { // Constructor - constructor(startDate: Date = new Date('2024-02-02')) { + constructor(startDate: Date = new Date(2024, 2, 2, 12, 30)) { this.startDate = startDate } @@ -120,15 +120,15 @@ export class MockPatientService implements PatientService { userId: string, ): Promise<[Observation[], Observation[]]> { const values = [ - this.bloodPressureObservations(110, 70, new Date('2024-02-01')), - this.bloodPressureObservations(114, 82, new Date('2024-01-31')), - this.bloodPressureObservations(123, 75, new Date('2024-01-30')), - this.bloodPressureObservations(109, 77, new Date('2024-01-29')), - this.bloodPressureObservations(105, 72, new Date('2024-01-28')), - this.bloodPressureObservations(98, 68, new Date('2024-01-27')), - this.bloodPressureObservations(94, 65, new Date('2024-01-26')), - this.bloodPressureObservations(104, 72, new Date('2024-01-25')), - this.bloodPressureObservations(102, 80, new Date('2024-01-24')), + this.bloodPressureObservations(110, 70, new Date(2024, 1, 30, 12, 30)), + this.bloodPressureObservations(114, 82, new Date(2024, 1, 29, 12, 30)), + this.bloodPressureObservations(123, 75, new Date(2024, 1, 28, 12, 30)), + this.bloodPressureObservations(109, 77, new Date(2024, 1, 27, 12, 30)), + this.bloodPressureObservations(105, 72, new Date(2024, 1, 26, 12, 30)), + this.bloodPressureObservations(98, 68, new Date(2024, 1, 25, 12, 30)), + this.bloodPressureObservations(94, 65, new Date(2024, 1, 24, 12, 30)), + this.bloodPressureObservations(104, 72, new Date(2024, 1, 23, 12, 30)), + this.bloodPressureObservations(102, 80, new Date(2024, 1, 22, 12, 30)), ] return [values.map((value) => value[0]), values.map((value) => value[1])] } @@ -154,15 +154,51 @@ export class MockPatientService implements PatientService { async getBodyWeightObservations(userId: string): Promise { return [ - this.bodyWeightObservation(269, QuantityUnit.lbs, new Date('2024-02-01')), - this.bodyWeightObservation(267, QuantityUnit.lbs, new Date('2024-01-31')), - this.bodyWeightObservation(267, QuantityUnit.lbs, new Date('2024-01-30')), - this.bodyWeightObservation(265, QuantityUnit.lbs, new Date('2024-01-29')), - this.bodyWeightObservation(268, QuantityUnit.lbs, new Date('2024-01-28')), - this.bodyWeightObservation(268, QuantityUnit.lbs, new Date('2024-01-27')), - this.bodyWeightObservation(266, QuantityUnit.lbs, new Date('2024-01-26')), - this.bodyWeightObservation(266, QuantityUnit.lbs, new Date('2024-01-25')), - this.bodyWeightObservation(267, QuantityUnit.lbs, new Date('2024-01-24')), + this.bodyWeightObservation( + 269, + QuantityUnit.lbs, + new Date(2024, 1, 30, 12, 30), + ), + this.bodyWeightObservation( + 267, + QuantityUnit.lbs, + new Date(2024, 1, 29, 12, 30), + ), + this.bodyWeightObservation( + 267, + QuantityUnit.lbs, + new Date(2024, 1, 28, 12, 30), + ), + this.bodyWeightObservation( + 265, + QuantityUnit.lbs, + new Date(2024, 1, 27, 12, 30), + ), + this.bodyWeightObservation( + 268, + QuantityUnit.lbs, + new Date(2024, 1, 26, 12, 30), + ), + this.bodyWeightObservation( + 268, + QuantityUnit.lbs, + new Date(2024, 1, 25, 12, 30), + ), + this.bodyWeightObservation( + 266, + QuantityUnit.lbs, + new Date(2024, 1, 24, 12, 30), + ), + this.bodyWeightObservation( + 266, + QuantityUnit.lbs, + new Date(2024, 1, 23, 12, 30), + ), + this.bodyWeightObservation( + 267, + QuantityUnit.lbs, + new Date(2024, 1, 22, 12, 30), + ), ] } @@ -180,15 +216,15 @@ export class MockPatientService implements PatientService { async getHeartRateObservations(userId: string): Promise { return [ - this.heartRateObservation(79, new Date('2024-02-01')), - this.heartRateObservation(62, new Date('2024-01-31')), - this.heartRateObservation(77, new Date('2024-01-30')), - this.heartRateObservation(63, new Date('2024-01-29')), - this.heartRateObservation(61, new Date('2024-01-28')), - this.heartRateObservation(70, new Date('2024-01-27')), - this.heartRateObservation(67, new Date('2024-01-26')), - this.heartRateObservation(80, new Date('2024-01-25')), - this.heartRateObservation(65, new Date('2024-01-24')), + this.heartRateObservation(79, new Date(2024, 1, 30, 12, 30)), + this.heartRateObservation(62, new Date(2024, 1, 29, 12, 30)), + this.heartRateObservation(77, new Date(2024, 1, 28, 12, 30)), + this.heartRateObservation(63, new Date(2024, 1, 27, 12, 30)), + this.heartRateObservation(61, new Date(2024, 1, 26, 12, 30)), + this.heartRateObservation(70, new Date(2024, 1, 25, 12, 30)), + this.heartRateObservation(67, new Date(2024, 1, 24, 12, 30)), + this.heartRateObservation(80, new Date(2024, 1, 23, 12, 30)), + this.heartRateObservation(65, new Date(2024, 1, 22, 12, 30)), ] } @@ -215,7 +251,7 @@ export class MockPatientService implements PatientService { unit: QuantityUnit, ): Promise { return { - date: new Date('2024-01-29'), + date: new Date(2024, 1, 27, 12, 30), value: 267.5, unit: QuantityUnit.lbs, } @@ -265,7 +301,7 @@ export class MockPatientService implements PatientService { qualityOfLifeScore: 20, symptomFrequencyScore: 60, dizzinessScore: 3, - date: new Date('2024-01-24'), + date: new Date(2024, 1, 22, 12, 30), }), new SymptomScore({ questionnaireResponseId: '3', @@ -275,7 +311,7 @@ export class MockPatientService implements PatientService { qualityOfLifeScore: 37, symptomFrequencyScore: 72, dizzinessScore: 2, - date: new Date('2024-01-15'), + date: new Date(2024, 1, 13, 12, 30), }), new SymptomScore({ questionnaireResponseId: '2', @@ -285,7 +321,7 @@ export class MockPatientService implements PatientService { qualityOfLifeScore: 25, symptomFrequencyScore: 60, dizzinessScore: 1, - date: new Date('2023-12-30'), + date: new Date(2023, 12, 28, 12, 30), }), new SymptomScore({ questionnaireResponseId: '1', @@ -295,7 +331,7 @@ export class MockPatientService implements PatientService { qualityOfLifeScore: 60, symptomFrequencyScore: 80, dizzinessScore: 1, - date: new Date('2023-12-15'), + date: new Date(2023, 12, 13, 12, 30), }), ] return values.map((value, index) => ({ diff --git a/functions/src/services/trigger/triggerService.ts b/functions/src/services/trigger/triggerService.ts index 58009c2b..a964bced 100644 --- a/functions/src/services/trigger/triggerService.ts +++ b/functions/src/services/trigger/triggerService.ts @@ -218,7 +218,7 @@ export class TriggerService { logger.debug( `TriggerService.userObservationWritten(${userId}, ${collection}): Most recent body weight is ${mostRecentBodyWeight} compared to a median of ${bodyWeightMedian}`, ) - if (mostRecentBodyWeight - bodyWeightMedian >= 7) { + if (mostRecentBodyWeight - bodyWeightMedian >= 3) { const messageService = this.factory.message() const messageDoc = await messageService.addMessage( userId, diff --git a/functions/src/tests/helpers/csv.ts b/functions/src/tests/helpers/csv.ts index d97aa865..85e6fde6 100644 --- a/functions/src/tests/helpers/csv.ts +++ b/functions/src/tests/helpers/csv.ts @@ -16,15 +16,27 @@ export function readCsv( ) { const fileContent = fs.readFileSync(path, 'utf8') const lines = fileContent - .replace(/"(.*?)"/g, (str) => - str.slice(1, -1).split(',').join('###COMMA###'), + .replace(/"([\s\S]*?)"/g, (str) => + str + .slice(1, -1) + .split(',') + .join('###COMMA###') + .split('\n') + .join('###NEWLINE###'), ) .split('\n') expect(lines).to.have.length(expectedLines) lines.forEach((line, index) => { const values = line .split(',') - .map((x) => x.split('###COMMA###').join(',').trim()) + .map((x) => + x + .split('###COMMA###') + .join(',') + .split('###NEWLINE###') + .join('\n') + .trim(), + ) perform(values, index) }) } diff --git a/functions/src/tests/mocks/healthSummaryData.ts b/functions/src/tests/mocks/healthSummaryData.ts index f5c3c9d9..f64a9c55 100644 --- a/functions/src/tests/mocks/healthSummaryData.ts +++ b/functions/src/tests/mocks/healthSummaryData.ts @@ -10,8 +10,8 @@ import { MockHealthSummaryService } from '../../services/healthSummary/healthSum export function mockHealthSummaryData( userId: string, - startDate: Date = new Date('2024-02-02'), + startDate: Date = new Date(2024, 2, 2, 12, 30), ): Promise { - const service = new MockHealthSummaryService(startDate) - return service.getHealthSummaryData(userId) + const service = new MockHealthSummaryService() + return service.getHealthSummaryData(userId, startDate) } diff --git a/functions/src/tests/resources/emptyHealthSummary.pdf b/functions/src/tests/resources/emptyHealthSummary.pdf index 273c4b83..11a830fd 100644 Binary files a/functions/src/tests/resources/emptyHealthSummary.pdf and b/functions/src/tests/resources/emptyHealthSummary.pdf differ diff --git a/functions/src/tests/resources/keyPointMessages.csv b/functions/src/tests/resources/keyPointMessages.csv new file mode 100644 index 00000000..41607002 --- /dev/null +++ b/functions/src/tests/resources/keyPointMessages.csv @@ -0,0 +1,116 @@ +Meds,Symptoms,Dizziness,Weight,Message, +Eligible meds for optimization,Change >-10 and KCCQ<90,No decrease <-25,No weight gain but weight measured,There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart. , +No eligible meds at optimization; measure BP,Change >-10 and KCCQ<90,No decrease <-25,No weight gain but weight measured,We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart. , +No eligible meds at optimization; at target doses,Change >-10 and KCCQ<90,No decrease <-25,No weight gain but weight measured,Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart. , +Eligible meds for optimization,Change >-10 and KCCQ>=90,No decrease <-25,No weight gain but weight measured,There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart. ,Minor change +No eligible meds at optimization; measure BP,Change >-10 and KCCQ>=90,No decrease <-25,No weight gain but weight measured,We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart. , +No eligible meds at optimization; at target doses,Change >-10 and KCCQ>=90,No decrease <-25,No weight gain but weight measured,Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart. , +Eligible meds for optimization,Change <-10,No decrease <-25,No weight gain but weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Discuss with your care team how adjusting your medications can help you feel better.",Minor change +No eligible meds at optimization; measure BP,Change <-10,No decrease <-25,No weight gain but weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better.", +No eligible meds at optimization; at target doses,Change <-10,No decrease <-25,No weight gain but weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Discuss with your care team potential options for helping you feel better.", +Eligible meds for optimization,Change >-10 and KCCQ<90,Decrease <-25,No weight gain but weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.", +No eligible meds at optimization; measure BP,Change >-10 and KCCQ<90,Decrease <-25,No weight gain but weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ<90,Decrease <-25,No weight gain but weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change >-10 and KCCQ>=90,Decrease <-25,No weight gain but weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.",Minor change +No eligible meds at optimization; measure BP,Change >-10 and KCCQ>=90,Decrease <-25,No weight gain but weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ>=90,Decrease <-25,No weight gain but weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change <-10,Decrease <-25,No weight gain but weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Discuss with your care team how adjusting your medications can help you feel better. +3.) You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.",Minor change +No eligible meds at optimization; measure BP,Change <-10,Decrease <-25,No weight gain but weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better. +3.) You are noting your dizziness is more bothersome. Check your blood pressure and heart rate and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change <-10,Decrease <-25,No weight gain but weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Discuss with your care team potential options for helping you feel better. +3.) You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change >-10 and KCCQ<90,No decrease <-25,Weight increase,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.", +No eligible meds at optimization; measure BP,Change >-10 and KCCQ<90,No decrease <-25,Weight increase,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ<90,No decrease <-25,Weight increase,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.", +Eligible meds for optimization,Change >-10 and KCCQ>=90,No decrease <-25,Weight increase,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.",Minor change +No eligible meds at optimization; measure BP,Change >-10 and KCCQ>=90,No decrease <-25,Weight increase,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart. +", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ>=90,No decrease <-25,Weight increase,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.", +Eligible meds for optimization,Change <-10,No decrease <-25,Weight increase,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Discuss with your care team how adjusting your medications can help you feel better. +3.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.",Minor change +No eligible meds at optimization; measure BP,Change <-10,No decrease <-25,Weight increase,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better. +3.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.", +No eligible meds at optimization; at target doses,Change <-10,No decrease <-25,Weight increase,"1.) Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Discuss with your care team potential options for helping you feel better. +3.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart.", +Eligible meds for optimization,Change >-10 and KCCQ<90,Decrease <-25,Weight increase,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart. +3.) Your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.", +No eligible meds at optimization; measure BP,Change >-10 and KCCQ<90,Decrease <-25,Weight increase,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart. +3.) Your dizziness is bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ<90,Decrease <-25,Weight increase,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart. +3.) Your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change >-10 and KCCQ>=90,Decrease <-25,Weight increase,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart. +3.) You are noting your dizziness is more bothersome. Would discuss options with your care team for improving your dizziness and watch the dizziness educational video.",Minor change +No eligible meds at optimization; measure BP,Change >-10 and KCCQ>=90,Decrease <-25,Weight increase,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Changing your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart. +3.) You are noting your dizziness is more bothersome. Check your blood pressure and heart rate more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ>=90,Decrease <-25,Weight increase,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart. +2.) Your weight is increasing. This is a sign that you may be retaining fluid. Discuss with your care team and watch the weight educational video. Taking your heart medicines can lower your risk of having fluid gain in the future by strengthening your heart. +3.) You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change <-10,Decrease <-25,Weight increase,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report) and your weight increased. Discuss with your care team how adjusting your medications can help you feel better. +3.) You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.",Minor change +No eligible meds at optimization; measure BP,Change <-10,Decrease <-25,Weight increase,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report) and your weight increased. Check your blood pressure and heart rate and discuss with your care team how adjusting your medicines can help you feel better. +3.) You are noting your dizziness is more bothersome. Check your blood pressure and heart rate and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change <-10,Decrease <-25,Weight increase,"1.) Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart. +2.) Your heart symptoms worsened (see symptom report) and your weight increased. Discuss with your care team potential options for helping you feel better. +3.) You are noting your dizziness is more bothersome. Would discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change >-10 and KCCQ<90,No decrease <-25,No weight measured,There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart. , +No eligible meds at optimization; measure BP,Change >-10 and KCCQ<90,No decrease <-25,No weight measured,We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart. , +No eligible meds at optimization; at target doses,Change >-10 and KCCQ<90,No decrease <-25,No weight measured,Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart. , +Eligible meds for optimization,Change >-10 and KCCQ>=90,No decrease <-25,No weight measured,There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart. ,Minor change +No eligible meds at optimization; measure BP,Change >-10 and KCCQ>=90,No decrease <-25,No weight measured,We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to keep you feeling well and strengthen your heart. , +No eligible meds at optimization; at target doses,Change >-10 and KCCQ>=90,No decrease <-25,No weight measured,Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart. , +Eligible meds for optimization,Change <-10,No decrease <-25,No weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team how adjusting your medications can help you feel better.",Minor change +No eligible meds at optimization; measure BP,Change <-10,No decrease <-25,No weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your blood pressure, heart rate, and weight, and discuss with your care team how adjusting your medicines can help you feel better.", +No eligible meds at optimization; at target doses,Change <-10,No decrease <-25,No weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team potential options for helping you feel better.", +Eligible meds for optimization,Change >-10 and KCCQ<90,Decrease <-25,No weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you feel better and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Check your weight and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.", +No eligible meds at optimization; measure BP,Change >-10 and KCCQ<90,Decrease <-25,No weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you feel better and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Check your blood pressure, heart rate, and weight more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ<90,Decrease <-25,No weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable and your weight is not rising. Make sure to keep taking your heart meds to help you feel better and strengthen your heart. +2.) You are noting your dizziness is more bothersome. Check your weight and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change >-10 and KCCQ>=90,Decrease <-25,No weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can keep you feeling well and strengthen your heart. +2.) Your dizziness is more bothersome. Check your weight and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.",Minor change +No eligible meds at optimization; measure BP,Change >-10 and KCCQ>=90,Decrease <-25,No weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Check your blood pressure and heart rate multiple times a week to understand if your heart medicines can be adjusted to keep you feeling well and strengthen your heart. +2.) Your dizziness is more bothersome. Check your blood pressure, heart rate, and weight more frequently and discuss options with your care team for improving your dizziness. Also watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change >-10 and KCCQ>=90,Decrease <-25,No weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Your symptoms are stable. Make sure to keep taking your heart meds to keep you feeling well and strengthen your heart. +2.) Your dizziness is more bothersome. Check your weight and discuss ways to improve your dizziness with your care team. Also watch the dizziness educational video.", +Eligible meds for optimization,Change <-10,Decrease <-25,No weight measured,"1.) There are possible options to improve your heart medicines. See the list of 'Potential Med Changes' below to discuss these options with your care team. These meds can help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team how adjusting your meds can help you feel better. +3.) Your dizziness is more bothersome. Discuss ways to improve your dizziness with your care team and watch the dizziness educational video.",Minor change +No eligible meds at optimization; measure BP,Change <-10,Decrease <-25,No weight measured,"1.) We are missing blood pressure and heart rate checks in the last two weeks. Try to check your blood pressure and heart rate multiple times a week to understand if your medications can be adjusted to help you start feeling better and strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your blood pressure, heart rate, and weight and discuss with your care team how adjusting your meds can help you feel better. +3.) Your dizziness is more bothersome. Discuss ways to improve your dizziness with your care team and watch the dizziness educational video.", +No eligible meds at optimization; at target doses,Change <-10,Decrease <-25,No weight measured,"1.) Great news! You are on the target dose for your heart medicines at this time. Make sure to keep taking your heart meds to strengthen your heart. +2.) Your heart symptoms worsened (see symptom report). Check your weight and discuss with your care team options for helping you feel better. +3.) Your dizziness is more bothersome. Discuss ways to improve your dizziness with your care team and watch the dizziness educational video.", \ No newline at end of file diff --git a/functions/src/tests/resources/keyPointMessages.csv.license b/functions/src/tests/resources/keyPointMessages.csv.license new file mode 100644 index 00000000..aecd0247 --- /dev/null +++ b/functions/src/tests/resources/keyPointMessages.csv.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2023 Stanford University +# SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/functions/src/tests/resources/mockChart.svg b/functions/src/tests/resources/mockChart.svg index 0750cdd9..6700d439 100644 --- a/functions/src/tests/resources/mockChart.svg +++ b/functions/src/tests/resources/mockChart.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68b76f9a7e5c95b9b82314c89a70a3dfeff71fa445f36876a1d5dc1374ae2ead -size 3698 +oid sha256:9d4b533357466f86fddb09dc4b68b2fb1af26976c764f8948a72defb47499f8a +size 3590 diff --git a/functions/src/tests/resources/mockHealthSummary.pdf b/functions/src/tests/resources/mockHealthSummary.pdf index 12772397..d512bf7c 100644 Binary files a/functions/src/tests/resources/mockHealthSummary.pdf and b/functions/src/tests/resources/mockHealthSummary.pdf differ