Skip to content

Commit

Permalink
Add Export Health Summary (#27)
Browse files Browse the repository at this point in the history
# Add Export Health Summary

## ♻️ Current situation & Problem
As a clinician, I want to be able to export Health Summary report.



### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
arkadiuszbachorski authored Aug 12, 2024
1 parent 465a113 commit aba5cca
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 8 deletions.
69 changes: 69 additions & 0 deletions app/(dashboard)/patients/[id]/GenerateHealthSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// This source file is part of the Stanford Biodesign Digital Health ENGAGE-HF open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//
'use client'
import { kebabCase } from 'es-toolkit'
import { Download } from 'lucide-react'
import { useState } from 'react'
import { callables } from '@/modules/firebase/clientApp'
import { type ResourceType } from '@/modules/firebase/utils'
import { Button } from '@/packages/design-system/src/components/Button'
import { toast } from '@/packages/design-system/src/components/Toaster'
import { Tooltip } from '@/packages/design-system/src/components/Tooltip'
import {
base64ToBlob,
downloadFile,
} from '@/packages/design-system/src/utils/file'

interface GenerateHealthSummaryProps {
userId: string
userName: string
resourceType: ResourceType
}

export const GenerateHealthSummary = ({
userId,
resourceType,
userName,
}: GenerateHealthSummaryProps) => {
const [isPending, setIsPending] = useState(false)

const downloadHealthSummary = async () => {
setIsPending(true)
try {
const exportHealthPromise = callables.exportHealthSummary({ userId })
toast.promise(exportHealthPromise, {
loading: `Generating health summary for ${userName}...`,
success: `Health summary for ${userName} has been downloaded.`,
error: `Generating health summary for ${userName} failed. Please try later.`,
})
const response = await exportHealthPromise
const blob = base64ToBlob(response.data.content, 'application/pdf')
downloadFile(blob, `health-summary-${kebabCase(userName)}.pdf`)
} finally {
setIsPending(false)
}
}

return (
<Tooltip
open={resourceType === 'invitation' ? undefined : false}
tooltip="This user has not logged in to the application yet"
>
<Button
type="submit"
disabled={resourceType === 'invitation'}
onClick={downloadHealthSummary}
className="disabled:pointer-events-auto"
isPending={isPending}
>
<Download />
Export Health Summary
</Button>
</Tooltip>
)
}
12 changes: 11 additions & 1 deletion app/(dashboard)/patients/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
} from '@/packages/design-system/src/components/Tabs'
import { getUserName } from '@/packages/design-system/src/modules/auth/user'
import { PageTitle } from '@/packages/design-system/src/molecules/DashboardLayout'
import { GenerateHealthSummary } from './GenerateHealthSummary'
import { DashboardLayout } from '../../DashboardLayout'
import { Medications, type MedicationsFormSchema } from '../Medications'

Expand Down Expand Up @@ -148,15 +149,24 @@ const PatientPage = async ({ params }: PatientPageProps) => {
})
}

const userName = getUserName(authUser) ?? ''

return (
<DashboardLayout
title={
<PageTitle
title="Edit patient"
subTitle={getUserName(authUser)}
subTitle={userName}
icon={<Contact />}
/>
}
actions={
<GenerateHealthSummary
userId={userId}
resourceType={resourceType}
userName={userName}
/>
}
>
<Tabs defaultValue={Tab.information}>
<TabsList className="mb-6 w-full">
Expand Down
9 changes: 8 additions & 1 deletion modules/firebase/clientApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
// SPDX-License-Identifier: MIT
//
import { initializeApp } from '@firebase/app'
import { connectFunctionsEmulator, getFunctions } from '@firebase/functions'
import { OAuthProvider, getAuth, connectAuthEmulator } from 'firebase/auth'
import { env } from '@/env'
import { getCallables } from '@/modules/firebase/utils'
import { firebaseConfig } from './config'

export const app = initializeApp(firebaseConfig)
Expand All @@ -18,8 +20,13 @@ export const authProvider = {
}

export const auth = getAuth()
if (env.NEXT_PUBLIC_EMULATOR && !auth.emulatorConfig) {
const enableEmulation = env.NEXT_PUBLIC_EMULATOR && !auth.emulatorConfig
if (enableEmulation) {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', {
disableWarnings: true,
})
}
const functions = getFunctions(app)
if (enableEmulation) connectFunctionsEmulator(functions, '127.0.0.1', 5001)

export const callables = getCallables(functions)
6 changes: 6 additions & 0 deletions modules/firebase/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ export const getCallables = (functions: Functions) => ({
},
undefined
>(functions, 'updateUserInformation'),
exportHealthSummary: httpsCallable<
{
userId: string
},
{ content: string }
>(functions, 'exportHealthSummary'),
})

export const getDocData = async <T>(reference: DocumentReference<T>) => {
Expand Down
8 changes: 7 additions & 1 deletion modules/user/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,13 @@ export const getUserData = async (userId: string) => {
}
const invitation = await getDocData(docRefs.invitation(userId))
return {
user: invitation?.user,
user:
invitation?.user ?
{
...invitation.user,
invitationCode: invitation.id,
}
: undefined,
authUser:
invitation?.auth ?
{
Expand Down
4 changes: 3 additions & 1 deletion packages/design-system/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
>
{isPending !== undefined ?
<ButtonPending isPending={isPending}>{children}</ButtonPending>
<ButtonPending size={size} isPending={isPending}>
{children}
</ButtonPending>
: children}
</Comp>
)
Expand Down
23 changes: 19 additions & 4 deletions packages/design-system/src/components/Button/ButtonPending.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import { Loader2 } from 'lucide-react'
import { forwardRef, type HTMLAttributes } from 'react'
import { cn } from '../../utils/className'
import type { ButtonProps } from '../Button'

interface ButtonPendingProps extends HTMLAttributes<HTMLSpanElement> {
interface ButtonPendingProps
extends HTMLAttributes<HTMLSpanElement>,
Pick<ButtonProps, 'size'> {
isPending?: boolean
}

Expand All @@ -18,8 +21,12 @@ interface ButtonPendingProps extends HTMLAttributes<HTMLSpanElement> {
* It's separated from Button to prevent redundant markup when unnecessary
* */
export const ButtonPending = forwardRef<HTMLSpanElement, ButtonPendingProps>(
({ children, isPending, className, ...props }, ref) => (
<span className={cn('relative', className)} ref={ref} {...props}>
({ children, isPending, className, size, ...props }, ref) => (
<span
className={cn('inline-flex-center relative', className)}
ref={ref}
{...props}
>
{isPending && (
<div
className="absolute -top-0.5 left-1/2 -translate-x-1/2"
Expand All @@ -29,7 +36,15 @@ export const ButtonPending = forwardRef<HTMLSpanElement, ButtonPendingProps>(
<Loader2 className="animate-spin" />
</div>
)}
<span className={cn(isPending && 'invisible')}>{children}</span>
<span
className={cn(
'inline-flex-center',
size === 'lg' ? 'gap-2.5' : 'gap-2',
isPending && 'invisible ',
)}
>
{children}
</span>
</span>
),
)
Expand Down
44 changes: 44 additions & 0 deletions packages/design-system/src/utils/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// This source file is part of the Stanford Biodesign Digital Health ENGAGE-HF open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

export const downloadFile = (
src: File | Blob | MediaSource,
fileName: string,
) => {
const url = URL.createObjectURL(src)

const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()

URL.revokeObjectURL(url)
}

export const base64ToBlob = (
base64Data: string,
contentType = '',
sliceSize = 512,
) => {
const byteCharacters = atob(base64Data)
const byteArrays = []

for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize)

const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}

const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}

return new Blob(byteArrays, { type: contentType })
}

0 comments on commit aba5cca

Please sign in to comment.