Skip to content

Commit

Permalink
Add user actions (#16)
Browse files Browse the repository at this point in the history
# Add user actions

## ♻️ Current situation & Problem
Users need to be able to create/delete/update Users and Patients 


### 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 1, 2024
1 parent 720a0df commit 13821c1
Show file tree
Hide file tree
Showing 66 changed files with 4,718 additions and 1,313 deletions.
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ SPDX-License-Identifier: MIT
[![Deployment](https://github.com/StanfordBDHG/ENGAGE-HF-Web-Frontend/actions/workflows/main.yml/badge.svg)](https://github.com/StanfordBDHG/ENGAGE-HF-Web-Frontend/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/StanfordBDHG/ENGAGE-HF-Web-Frontend/graph/badge.svg?token=PsKyNz7Woe)](https://codecov.io/gh/StanfordBDHG/ENGAGE-HF-Web-Frontend)


## How To Use ENGAGE-HF Web Frontend

The ENGAGE-HF Web Frontend repository contains a Next.js project providing automated GitHub Actions and setups for code linting, testing & test coverage reports, docker deployments, a docker compose setup, local packages for modular deployment.


## Getting Started

You can run the project using the following command. You will need to install Node.js and npm, e.g., using [homebrew (recommended for macOS)](https://formulae.brew.sh/formula/node) or the official [Node.js installer](https://nodejs.org/en/download).
Expand Down Expand Up @@ -53,7 +51,6 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the

You can edit the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.


## Docker

1. [Install Docker](https://docs.docker.com/get-docker/) on your machine.
Expand All @@ -67,23 +64,21 @@ The `docker-compose.yml` setup contains a production-ready setup using a reverse

Every version of the application on the `main` branch is automatically packaged into docker images using the `main` tag. Every release is also published using the `latest` and respective version tags.


## Deployment

This repository contains all necessary files to deploy the web frontend to Google Cloud Firebase ([Stanford mHealth Platform](https://med.stanford.edu/mhealth.html)).


### Deployment Configuration

...


### Stanford SSO Setup

The ENGAGE-HF web page uses Stanford single sign on (SSO) as a mechanism to allow clinicians and admins to log into the web page.

The [Stanford SAML and OIDC Configuration Manager](https://spdb-prod.iam.stanford.edu) needs to be configured using an OIDC configuration.
You will use the Client ID and Client secret from the configuration to set up the OIDC authentication in Firebase Authentication.

- Subject Type: `public`
- Token Endpoint Auth: `client_secret_basic`
- Grant Type: `refresh_token (authorization_code always enabled)`
Expand All @@ -92,13 +87,13 @@ You will use the Client ID and Client secret from the configuration to set up th

You will need to configure [Firebase Authentication with Identity Platform to use OpenID connect in web apps](https://firebase.google.com/docs/auth/web/openid-connect).
You need to configure the OpenID Connect Sign-in provider as follows:

- Grant Type: `Code flow`
- Name: `Stanford`
- Client ID: Client ID obtained from your OIDC configuration from the [Stanford SAML and OIDC Configuration Manager](https://spdb-prod.iam.stanford.edu).
- Issuer (URL): `https://login.stanford.edu`
- Client secret: Client ID obtained from your OIDC configuration from the [Stanford SAML and OIDC Configuration Manager](https://spdb-prod.iam.stanford.edu).


## Learn More

To learn more about Next.js, take a look at the following resources:
Expand All @@ -108,12 +103,10 @@ To learn more about Next.js, take a look at the following resources:

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!


## License

This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordBDHG/ENGAGE-HF-Web-Frontend/tree/main/LICENSES) for more information.


## Contributors

This project is developed as part of the Stanford Byers Center for Biodesign at Stanford University.
Expand Down
12 changes: 6 additions & 6 deletions app/(dashboard)/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//
import Link from 'next/link'
import { LogoType } from '@/components/icons/LogoType'
import { getAuthenticatedOnlyApp, getUserRole } from '@/modules/firebase/guards'
import { getAuthenticatedOnlyApp } from '@/modules/firebase/guards'
import { getUserInfo } from '@stanfordbdhg/design-system/modules/auth/user'
import {
DashboardLayout as DashboardLayoutBase,
Expand All @@ -17,11 +17,11 @@ import { MenuLinks } from './MenuLinks'
import { User } from './User'

interface DashboardLayoutProps
extends Pick<DashboardLayoutPropsBase, 'children' | 'title'> {}
extends Omit<DashboardLayoutPropsBase, 'aside' | 'mobile'> {}

export const DashboardLayout = async (props: DashboardLayoutProps) => {
const { currentUser } = await getAuthenticatedOnlyApp()
const { role: userRole } = await getUserRole()
const { currentUser, user: userDoc } = await getAuthenticatedOnlyApp()
const role = userDoc.type
const user = <User user={getUserInfo(currentUser)} />

return (
Expand All @@ -32,15 +32,15 @@ export const DashboardLayout = async (props: DashboardLayoutProps) => {
<LogoType className="!h-auto !w-full px-2 xl:px-8" />
</Link>
<nav className="mt-9 flex flex-col gap-1 xl:w-full">
<MenuLinks role={userRole} />
<MenuLinks userType={role} />
</nav>
{user}
</>
}
mobile={
<>
<nav className="mt-9 flex flex-col gap-1 px-4">
<MenuLinks role={userRole} />
<MenuLinks userType={role} />
</nav>
{user}
</>
Expand Down
17 changes: 11 additions & 6 deletions app/(dashboard)/MenuLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
'use client'
import { Home, Users, Contact } from 'lucide-react'
import { usePathname } from 'next/navigation'
import { Role } from '@/modules/firebase/role'
import { UserType } from '@/modules/firebase/utils'
import { routes } from '@/modules/routes'
import { MenuItem } from '@stanfordbdhg/design-system/molecules/DashboardLayout'

interface MenuLinksProps {
role: Role
userType: UserType
}

export const MenuLinks = ({ role }: MenuLinksProps) => {
export const MenuLinks = ({ userType }: MenuLinksProps) => {
const pathname = usePathname()

const hrefProps = (href: string) => ({
Expand All @@ -26,11 +27,15 @@ export const MenuLinks = ({ role }: MenuLinksProps) => {
return (
<>
<MenuItem {...hrefProps('/')} label="Home" icon={<Home />} />
{role === Role.admin && (
<MenuItem {...hrefProps('/users')} label="Users" icon={<Users />} />
{[UserType.admin, UserType.owner].includes(userType) && (
<MenuItem
{...hrefProps(routes.users.index)}
label="Users"
icon={<Users />}
/>
)}
<MenuItem
{...hrefProps('/patients')}
{...hrefProps(routes.patients.index)}
label="Patients"
icon={<Contact />}
/>
Expand Down
35 changes: 35 additions & 0 deletions app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// 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
//
import { type ReactNode } from 'react'
import { getAuthenticatedOnlyApp } from '@/modules/firebase/guards'
import { UserContextProvider } from '@/modules/firebase/UserProvider'
import { getDocDataOrThrow } from '@/modules/firebase/utils'
import { getUserInfo } from '@/packages/design-system/src/modules/auth/user'

interface DashboardLayoutProps {
children?: ReactNode
}

export const dynamic = 'force-dynamic'

const DashboardLayout = async ({ children }: DashboardLayoutProps) => {
const { currentUser, docRefs } = await getAuthenticatedOnlyApp()
const user = await getDocDataOrThrow(docRefs.user(currentUser.uid))
return (
<UserContextProvider
user={{
auth: getUserInfo(currentUser),
user,
}}
>
{children}
</UserContextProvider>
)
}

export default DashboardLayout
116 changes: 116 additions & 0 deletions app/(dashboard)/patients/PatientForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// 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 { z } from 'zod'
import { type User } from '@/modules/firebase/utils'
import { Button } from '@/packages/design-system/src/components/Button'
import { Input } from '@/packages/design-system/src/components/Input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/packages/design-system/src/components/Select'
import { Field } from '@/packages/design-system/src/forms/Field'
import { useForm } from '@/packages/design-system/src/forms/useForm'
import {
getUserName,
type UserInfo,
} from '@/packages/design-system/src/modules/auth/user'

export const patientFormSchema = z.object({
email: z.string().min(1, 'Email is required'),
displayName: z.string(),
invitationCode: z.string(),
clinician: z.string(),
})

export type PatientFormSchema = z.infer<typeof patientFormSchema>

interface PatientFormProps {
clinicians: Array<{
id: string
displayName: string | null
email: string | null
}>
userInfo?: Pick<UserInfo, 'email' | 'displayName' | 'uid'>
user?: Pick<User, 'organization' | 'invitationCode' | 'clinician'>
onSubmit: (data: PatientFormSchema) => Promise<void>
clinicianPreselectId?: string
}

export const PatientForm = ({
user,
clinicians,
userInfo,
onSubmit,
clinicianPreselectId,
}: PatientFormProps) => {
const isEdit = !!user
const form = useForm({
formSchema: patientFormSchema,
defaultValues: {
email: userInfo?.email ?? '',
displayName: userInfo?.displayName ?? '',
invitationCode: user?.invitationCode ?? '',
clinician: user?.clinician ?? clinicianPreselectId ?? '',
},
})

const handleSubmit = form.handleSubmit(async (data) => {
await onSubmit(data)
})

return (
<form onSubmit={handleSubmit} className="mx-auto w-full max-w-2xl">
<Field
control={form.control}
name="email"
label="Email"
render={({ field }) => <Input type="email" {...field} />}
/>
<Field
control={form.control}
name="displayName"
label="Display name"
render={({ field }) => <Input {...field} />}
/>
{isEdit && (
<Field
control={form.control}
name="invitationCode"
label="Invitation code"
render={({ field }) => <Input {...field} />}
/>
)}
<Field
control={form.control}
name="clinician"
label="Clinician"
render={({ field }) => (
<Select onValueChange={field.onChange} {...field}>
<SelectTrigger>
<SelectValue placeholder="Clinician" />
</SelectTrigger>
<SelectContent>
{clinicians.map((clinician) => (
<SelectItem value={clinician.id} key={clinician.id}>
{getUserName(clinician)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<Button type="submit" isPending={form.formState.isSubmitting}>
{isEdit ? 'Edit' : 'Invite'} patient
</Button>
</form>
)
}
64 changes: 64 additions & 0 deletions app/(dashboard)/patients/PatientMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// 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 { Pencil, Trash } from 'lucide-react'
import Link from 'next/link'
import {
deletePatient,
deleteInvitation,
} from '@/app/(dashboard)/patients/actions'
import type { Patient } from '@/app/(dashboard)/patients/page'
import { routes } from '@/modules/routes'
import { RowDropdownMenu } from '@/packages/design-system/src/components/DataTable'
import { DropdownMenuItem } from '@/packages/design-system/src/components/DropdownMenu'
import { getUserName } from '@/packages/design-system/src/modules/auth/user'
import { ConfirmDeleteDialog } from '@/packages/design-system/src/molecules/ConfirmDeleteDialog'
import { useOpenState } from '@/packages/design-system/src/utils/useOpenState'

interface PatientMenuProps {
patient: Patient
}

export const PatientMenu = ({ patient }: PatientMenuProps) => {
const deleteConfirm = useOpenState()

const handleDelete = async () => {
if (patient.resourceType === 'user') {
await deletePatient({ userId: patient.resourceId })
} else {
await deleteInvitation({ invitationId: patient.resourceId })
}
deleteConfirm.close()
}

return (
<>
<ConfirmDeleteDialog
open={deleteConfirm.isOpen}
onOpenChange={deleteConfirm.setIsOpen}
entityName="patient"
itemName={getUserName(patient)}
onDelete={handleDelete}
/>
<RowDropdownMenu>
{patient.resourceType === 'user' && (
<DropdownMenuItem asChild>
<Link href={routes.patients.patient(patient.resourceId)}>
<Pencil />
Edit
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={deleteConfirm.open}>
<Trash />
Delete
</DropdownMenuItem>
</RowDropdownMenu>
</>
)
}
Loading

0 comments on commit 13821c1

Please sign in to comment.