Skip to content

Commit

Permalink
List users and patients (#14)
Browse files Browse the repository at this point in the history
# List users and patients

## ♻️ Current situation & Problem
Users and patients need to be listed. 

## ⚙️ Release Notes 
* Add features to DataTable
* Create "Users" list page
* Create "Patients list" page

## ✅ Testing
Patients and users views will be covered with E2E tests later, hence
drop in code coverage


### 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 Jul 23, 2024
1 parent ceaa0ba commit 720a0df
Show file tree
Hide file tree
Showing 66 changed files with 2,935 additions and 415 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ yarn-error.log*

# local env files
.env*.local
# Firebase admin credentials
admin-creds.json

# vercel
.vercel
Expand Down
37 changes: 11 additions & 26 deletions app/(dashboard)/layout.tsx → app/(dashboard)/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,23 @@
//
// SPDX-License-Identifier: MIT
//
import { Home } from 'lucide-react'
import Link from 'next/link'
import { type ReactNode } from 'react'
import { LogoType } from '@/components/icons/LogoType'
import { getAuthenticatedOnlyApp } from '@/modules/firebase/guards'
import { getAuthenticatedOnlyApp, getUserRole } from '@/modules/firebase/guards'
import { getUserInfo } from '@stanfordbdhg/design-system/modules/auth/user'
import {
DashboardLayout as DashboardLayoutBase,
MenuItem,
PageTitle,
type DashboardLayoutProps as DashboardLayoutPropsBase,
} from '@stanfordbdhg/design-system/molecules/DashboardLayout'
import { MenuLinks } from './MenuLinks'
import { User } from './User'

interface DashboardLayoutProps {
children?: ReactNode
}

export const dynamic = 'force-dynamic'

const MenuLinks = () => (
<>
<MenuItem href="/" label="Home" icon={<Home />} isActive />
</>
)
interface DashboardLayoutProps
extends Pick<DashboardLayoutPropsBase, 'children' | 'title'> {}

const DashboardLayout = async ({ children }: DashboardLayoutProps) => {
export const DashboardLayout = async (props: DashboardLayoutProps) => {
const { currentUser } = await getAuthenticatedOnlyApp()

const { role: userRole } = await getUserRole()
const user = <User user={getUserInfo(currentUser)} />

return (
Expand All @@ -43,24 +32,20 @@ const DashboardLayout = async ({ children }: 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 />
<MenuLinks role={userRole} />
</nav>
{user}
</>
}
mobile={
<>
<nav className="mt-9 flex flex-col gap-1 px-4">
<MenuLinks />
<MenuLinks role={userRole} />
</nav>
{user}
</>
}
title={<PageTitle icon={<Home />} title="Home" />}
>
{children}
</DashboardLayoutBase>
{...props}
/>
)
}

export default DashboardLayout
39 changes: 39 additions & 0 deletions app/(dashboard)/MenuLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// 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 { Home, Users, Contact } from 'lucide-react'
import { usePathname } from 'next/navigation'
import { Role } from '@/modules/firebase/role'
import { MenuItem } from '@stanfordbdhg/design-system/molecules/DashboardLayout'

interface MenuLinksProps {
role: Role
}

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

const hrefProps = (href: string) => ({
href,
isActive: pathname === href,
})

return (
<>
<MenuItem {...hrefProps('/')} label="Home" icon={<Home />} />
{role === Role.admin && (
<MenuItem {...hrefProps('/users')} label="Users" icon={<Users />} />
)}
<MenuItem
{...hrefProps('/patients')}
label="Patients"
icon={<Contact />}
/>
</>
)
}
2 changes: 1 addition & 1 deletion app/(dashboard)/User.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const User = ({ user }: UserProps) => (
await auth.signOut()
}}
>
<LogOut className="size-4" />
<LogOut />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
Expand Down
7 changes: 5 additions & 2 deletions app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
//
// SPDX-License-Identifier: MIT
//
import { Home } from 'lucide-react'
import { PageTitle } from '@/packages/design-system/src/molecules/DashboardLayout'
import { DashboardLayout } from './DashboardLayout'

const DashboardPage = () => (
<div className="text-center">
<DashboardLayout title={<PageTitle title="Home" icon={<Home />} />}>
<h1 className="text-2xl">Dashboard</h1>
</div>
</DashboardLayout>
)

export default DashboardPage
58 changes: 58 additions & 0 deletions app/(dashboard)/patients/PatientsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// 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 { createColumnHelper } from '@tanstack/table-core'
import { Pencil, Trash } from 'lucide-react'
import { CopyText } from '@/packages/design-system/src/components/CopyText'
import {
DataTable,
RowDropdownMenu,
} from '@/packages/design-system/src/components/DataTable'
import { DropdownMenuItem } from '@/packages/design-system/src/components/DropdownMenu'
import type { Patient } from './page'

const columnHelper = createColumnHelper<Patient>()

const columns = [
columnHelper.accessor('uid', {
header: 'Id',
cell: (props) => (
<CopyText className="max-w-[7rem]">{props.getValue()}</CopyText>
),
}),
columnHelper.accessor('displayName', {
header: 'Name',
cell: (props) => props.getValue() ?? '-',
}),
columnHelper.accessor('email', { header: 'Email' }),
columnHelper.accessor('gender', { header: 'Gender' }),
columnHelper.display({
id: 'actions',
cell: () => (
// TODO: Actions
<RowDropdownMenu>
<DropdownMenuItem>
<Pencil />
Edit
</DropdownMenuItem>
<DropdownMenuItem>
<Trash />
Delete
</DropdownMenuItem>
</RowDropdownMenu>
),
}),
]

interface PatientsDataTableProps {
data: Patient[]
}

export const PatientsTable = ({ data }: PatientsDataTableProps) => (
<DataTable columns={columns} data={data} entityName="patients" />
)
74 changes: 74 additions & 0 deletions app/(dashboard)/patients/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// 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 { getDocs, query, where } from 'firebase/firestore'
import { Contact } from 'lucide-react'
import { getAuthenticatedOnlyApp, getUserRole } from '@/modules/firebase/guards'
import { Role } from '@/modules/firebase/role'
import { mapUserData } from '@/modules/firebase/user'
import { PageTitle } from '@/packages/design-system/src/molecules/DashboardLayout'
import { PatientsTable } from './PatientsTable'
import { DashboardLayout } from '../DashboardLayout'

const getPatientsQuery = async () => {
const { refs } = await getAuthenticatedOnlyApp()
const userRole = await getUserRole()
if (userRole.role === Role.admin) return refs.users()
if (userRole.role === Role.owner) {
const organizationIds = userRole.organizations.docs.map((doc) => doc.id)
return query(refs.users(), where('organization', 'in', organizationIds))
}
if (userRole.role === Role.clinician) {
const organizationId = userRole.clinician.data().organization
if (!organizationId) {
// TODO: Check if there is any reason for organization not to be defined
throw new Error('')
}
return query(refs.users(), where('organization', '==', organizationId))
}
// Other roles can't reach this point, so this should never execute
throw new Error()
}

const listPatients = async () => {
const patientsQuery = await getPatientsQuery()
const patients = await getDocs(patientsQuery)
const userIdsToGet = patients.docs.map((patient) => patient.id)
const patientsById = new Map(
patients.docs.map(
(patient) => [patient.id, { id: patient.id, ...patient.data() }] as const,
),
)

return mapUserData(userIdsToGet, (authData, id) => {
const patient = patientsById.get(id)
if (!patient) {
console.error(`No patient found for user id ${id}`)
return null
}
return {
uid: id,
email: authData.email,
displayName: authData.displayName,
gender: patient.GenderIdentityKey,
}
})
}

export type Patient = Awaited<ReturnType<typeof listPatients>>[number]

const PatientsPage = async () => {
const patients = await listPatients()

return (
<DashboardLayout title={<PageTitle title="Patients" icon={<Contact />} />}>
<PatientsTable data={patients} />
</DashboardLayout>
)
}

export default PatientsPage
58 changes: 58 additions & 0 deletions app/(dashboard)/users/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// 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 { createColumnHelper } from '@tanstack/table-core'
import { Pencil, Trash } from 'lucide-react'
import { CopyText } from '@/packages/design-system/src/components/CopyText'
import {
DataTable,
RowDropdownMenu,
} from '@/packages/design-system/src/components/DataTable'
import { DropdownMenuItem } from '@/packages/design-system/src/components/DropdownMenu'
import type { User } from './page'

const columnHelper = createColumnHelper<User>()

const columns = [
columnHelper.accessor('uid', {
header: 'Id',
cell: (props) => (
<CopyText className="max-w-[7rem]">{props.getValue()}</CopyText>
),
}),
columnHelper.accessor('displayName', {
header: 'Name',
cell: (props) => props.getValue() ?? '-',
}),
columnHelper.accessor('email', { header: 'Email' }),
columnHelper.accessor('role', { header: 'Role' }),
columnHelper.display({
id: 'actions',
cell: () => (
// TODO: Actions
<RowDropdownMenu>
<DropdownMenuItem>
<Pencil />
Edit
</DropdownMenuItem>
<DropdownMenuItem>
<Trash />
Delete
</DropdownMenuItem>
</RowDropdownMenu>
),
}),
]

interface UsersDataTableProps {
data: User[]
}

export const UsersTable = ({ data }: UsersDataTableProps) => (
<DataTable columns={columns} data={data} entityName="users" />
)
Loading

0 comments on commit 720a0df

Please sign in to comment.