Skip to content

Commit b398f0b

Browse files
committed
feat: add patient fetching on details
1 parent 06bf4ea commit b398f0b

11 files changed

Lines changed: 256 additions & 131 deletions

File tree

web/api/gql/generated.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,13 @@ export type GetOverviewDataQueryVariables = Exact<{ [key: string]: never; }>;
401401

402402
export type GetOverviewDataQuery = { __typename?: 'Query', recentPatients: Array<{ __typename?: 'PatientType', id: string, name: string, sex: Sex, birthdate: any, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null }>, recentTasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, updateDate?: any | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null } | null, patient: { __typename?: 'PatientType', id: string, name: string } }> };
403403

404+
export type GetPatientQueryVariables = Exact<{
405+
id: Scalars['ID']['input'];
406+
}>;
407+
408+
409+
export type GetPatientQuery = { __typename?: 'Query', patient?: { __typename?: 'PatientType', id: string, firstname: string, lastname: string, birthdate: any, sex: Sex, assignedLocation?: { __typename?: 'LocationNodeType', id: string } | null, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null }> } | null };
410+
404411
export type GetPatientsQueryVariables = Exact<{
405412
locationId?: InputMaybe<Scalars['ID']['input']>;
406413
}>;
@@ -612,6 +619,44 @@ export const useGetOverviewDataQuery = <
612619
}
613620
)};
614621

622+
export const GetPatientDocument = `
623+
query GetPatient($id: ID!) {
624+
patient(id: $id) {
625+
id
626+
firstname
627+
lastname
628+
birthdate
629+
sex
630+
assignedLocation {
631+
id
632+
}
633+
tasks {
634+
id
635+
title
636+
description
637+
done
638+
dueDate
639+
}
640+
}
641+
}
642+
`;
643+
644+
export const useGetPatientQuery = <
645+
TData = GetPatientQuery,
646+
TError = unknown
647+
>(
648+
variables: GetPatientQueryVariables,
649+
options?: Omit<UseQueryOptions<GetPatientQuery, TError, TData>, 'queryKey'> & { queryKey?: UseQueryOptions<GetPatientQuery, TError, TData>['queryKey'] }
650+
) => {
651+
652+
return useQuery<GetPatientQuery, TError, TData>(
653+
{
654+
queryKey: ['GetPatient', variables],
655+
queryFn: fetcher<GetPatientQuery, GetPatientQueryVariables>(GetPatientDocument, variables),
656+
...options
657+
}
658+
)};
659+
615660
export const GetPatientsDocument = `
616661
query GetPatients($locationId: ID) {
617662
patients(locationNodeId: $locationId) {

web/api/graphql/GetPatient.graphql

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
query GetPatient($id: ID!) {
2+
patient(id: $id) {
3+
id
4+
firstname
5+
lastname
6+
birthdate
7+
sex
8+
assignedLocation {
9+
id
10+
}
11+
tasks {
12+
id
13+
title
14+
description
15+
done
16+
dueDate
17+
}
18+
}
19+
}

web/components/patients/PatientDetailView.tsx

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import { useEffect, useState } from 'react'
22
import { useTasksTranslation } from '@/i18n/useTasksTranslation'
3-
import type { CreatePatientInput, TaskType, UpdatePatientInput } from '@/api/gql/generated'
4-
import { Sex, useCreatePatientMutation, useUpdatePatientMutation } from '@/api/gql/generated'
5-
import { Input, LoadingButton, Select, SelectOption, Tab, TabView } from '@helpwave/hightide'
3+
import type { CreatePatientInput, UpdatePatientInput } from '@/api/gql/generated'
4+
import { Sex, useCreatePatientMutation, useUpdatePatientMutation, useGetPatientQuery } from '@/api/gql/generated'
5+
import {
6+
Input,
7+
LoadingButton,
8+
Select,
9+
SelectOption,
10+
Tab,
11+
TabView,
12+
LoadingContainer,
13+
FormElementWrapper
14+
} from '@helpwave/hightide'
615
import { useTasksContext } from '@/hooks/useTasksContext'
7-
import { PopupDatePicker } from '@/components/ui/PopupDatePicker'
16+
import { DateInput } from '@/components/ui/DateInput'
817
import { CheckCircle2, Circle, Clock } from 'lucide-react'
918
import { PropertyList } from '@/components/PropertyList'
1019

1120
interface PatientDetailViewProps {
1221
patientId?: string,
13-
initialData?: Partial<CreatePatientInput> & {
14-
tasks?: TaskType[],
15-
name?: string,
16-
},
1722
onClose: () => void,
1823
onSuccess: () => void,
1924
}
@@ -39,37 +44,38 @@ const FormField = ({ label, children }: { label: string, children: React.ReactNo
3944
</div>
4045
)
4146

42-
export const PatientDetailView = ({ patientId, initialData, onClose, onSuccess }: PatientDetailViewProps) => {
47+
export const PatientDetailView = ({ patientId, onClose, onSuccess }: PatientDetailViewProps) => {
4348
const translation = useTasksTranslation()
4449
const { selectedLocationId } = useTasksContext()
4550
const isEditMode = !!patientId
4651

47-
const [fullName, setFullName] = useState(initialData?.name || '')
52+
const { data: patientData, isLoading: isLoadingPatient } = useGetPatientQuery(
53+
{ id: patientId! },
54+
{ enabled: isEditMode }
55+
)
4856

57+
const [fullName, setFullName] = useState('')
4958
const [formData, setFormData] = useState<CreatePatientInput>({
5059
firstname: '',
5160
lastname: '',
5261
sex: Sex.Female,
5362
assignedLocationId: selectedLocationId ?? '',
54-
...initialData,
55-
birthdate: initialData?.birthdate ? toISODate(initialData.birthdate) : getDefaultBirthdate()
63+
birthdate: getDefaultBirthdate()
5664
})
5765

5866
useEffect(() => {
59-
if (initialData) {
60-
setFormData(prev => ({
61-
...prev,
62-
...initialData,
63-
birthdate: initialData.birthdate ? toISODate(initialData.birthdate) : prev.birthdate
64-
}))
65-
66-
if (initialData.firstname && initialData.lastname) {
67-
setFullName(`${initialData.firstname} ${initialData.lastname}`)
68-
} else if (initialData.name) {
69-
setFullName(initialData.name)
70-
}
67+
if (patientData?.patient) {
68+
const { firstname, lastname, sex, birthdate, assignedLocation } = patientData.patient
69+
setFormData({
70+
firstname,
71+
lastname,
72+
sex,
73+
birthdate: toISODate(birthdate),
74+
assignedLocationId: assignedLocation?.id ?? selectedLocationId ?? ''
75+
})
76+
setFullName(`${firstname} ${lastname}`)
7177
}
72-
}, [initialData])
78+
}, [patientData, selectedLocationId])
7379

7480
const { mutate: createPatient, isLoading: isCreating } = useCreatePatientMutation({
7581
onSuccess: () => {
@@ -84,7 +90,6 @@ export const PatientDetailView = ({ patientId, initialData, onClose, onSuccess }
8490
}
8591
})
8692

87-
8893
const persistChanges = (updates: Partial<UpdatePatientInput>) => {
8994
if (updates.firstname !== undefined && !updates.firstname?.trim()) return
9095
if (updates.lastname !== undefined && !updates.lastname?.trim()) return
@@ -117,22 +122,26 @@ export const PatientDetailView = ({ patientId, initialData, onClose, onSuccess }
117122
{ label: translation('diverse'), value: Sex.Unknown }
118123
]
119124

120-
const tasks = initialData?.tasks || []
125+
const tasks = patientData?.patient?.tasks || []
121126
const openTasks = tasks.filter(t => !t.done)
122127
const closedTasks = tasks.filter(t => t.done)
123128

129+
if (isEditMode && isLoadingPatient) {
130+
return <LoadingContainer />
131+
}
132+
124133
return (
125-
<div className="flex flex-col h-full bg-surface">
134+
<div className="flex-col-0 h-full bg-surface">
126135
<div className="flex-none mb-6">
127136
<div className="typography-title-lg text-primary">
128-
{fullName}
137+
{fullName || translation('newPatient')}
129138
</div>
130139
</div>
131140

132-
<div className="flex-grow overflow-hidden flex flex-col">
133-
<TabView className="h-full flex flex-col">
141+
<div className="flex-col-0 flex-grow overflow-hidden">
142+
<TabView className="h-full flex-col-0">
134143
<Tab label={translation('overview')} className="h-full overflow-x-visible pr-2">
135-
<div className="flex flex-col gap-6 pt-4">
144+
<div className="flex-col-6 pt-4">
136145
<div className="grid grid-cols-2 gap-4">
137146
<FormField label={translation('firstName')}>
138147
<Input
@@ -160,15 +169,20 @@ export const PatientDetailView = ({ patientId, initialData, onClose, onSuccess }
160169
</FormField>
161170
</div>
162171

163-
<PopupDatePicker
164-
label={translation('birthdate')}
165-
date={new Date(formData.birthdate as string)}
166-
onDateChange={date => {
167-
const dateStr = toISODate(date)
168-
updateLocalState({ birthdate: dateStr })
169-
persistChanges({ birthdate: dateStr })
170-
}}
171-
/>
172+
<FormElementWrapper label={translation('birthdate')}>
173+
{(bag) => (
174+
<DateInput
175+
{...bag}
176+
date={new Date(formData.birthdate as string)}
177+
onValueChange={date => {
178+
const dateStr = toISODate(date)
179+
updateLocalState({ birthdate: dateStr })
180+
persistChanges({ birthdate: dateStr })
181+
}}
182+
/>
183+
)}
184+
</FormElementWrapper>
185+
172186

173187
<FormField label={translation('sex')}>
174188
<Select
@@ -253,4 +267,4 @@ export const PatientDetailView = ({ patientId, initialData, onClose, onSuccess }
253267
)}
254268
</div>
255269
)
256-
}
270+
}

web/components/ui/DateInput.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { HTMLAttributes } from 'react'
2+
import { useMemo } from 'react'
3+
import { useEffect, useRef, useState } from 'react'
4+
import type { InputProps } from '@helpwave/hightide'
5+
import { Button, DatePicker, Input, useLocale, useOutsideClick, useZIndexRegister } from '@helpwave/hightide'
6+
import { CalendarIcon } from 'lucide-react'
7+
import { formatAbsolute } from '@/utils/date'
8+
import clsx from 'clsx'
9+
10+
export type DateInputProps = InputProps & {
11+
date: Date,
12+
onValueChange: (date: Date) => void,
13+
containerProps?: HTMLAttributes<HTMLDivElement>,
14+
}
15+
16+
export const DateInput = ({ date, onValueChange, containerProps, ...props }: DateInputProps) => {
17+
const { locale } = useLocale()
18+
const [isOpen, setIsOpen] = useState(false)
19+
20+
const containerRef = useRef<HTMLDivElement>(null)
21+
22+
useOutsideClick([containerRef], () => setIsOpen(false))
23+
24+
const zIndex = useZIndexRegister(isOpen)
25+
26+
const isReadOnly = useMemo(() => props.readOnly || props.disabled, [props.readOnly, props.disabled])
27+
28+
useEffect(() => {
29+
if(isReadOnly) {
30+
setIsOpen(false)
31+
}
32+
}, [isReadOnly])
33+
34+
return (
35+
<>
36+
<div {...containerProps} className={clsx('relative w-full', containerProps?.className)}>
37+
<Input
38+
{...props}
39+
value={formatAbsolute(date, locale, false)}
40+
onClick={(event) => {
41+
setIsOpen(true)
42+
props.onClick?.(event)
43+
}}
44+
readOnly={true}
45+
className={clsx(
46+
'pr-10 w-full',
47+
{ 'hover:cursor-pointer': !isReadOnly },
48+
props.className
49+
)}
50+
/>
51+
<Button
52+
coloringStyle="text" layout="icon" color="neutral" size="small"
53+
className="absolute right-1 top-1/2 -translate-y-1/2"
54+
disabled={isReadOnly}
55+
onClick={() => setIsOpen(prevState => !prevState)}
56+
>
57+
<CalendarIcon className="size-5"/>
58+
</Button>
59+
</div>
60+
{isOpen && (
61+
<div
62+
ref={containerRef}
63+
className="absolute mt-1 left-0 rounded-lg shadow-xl border bg-surface text-on-surface border-divider p-2"
64+
style={{ zIndex }}
65+
>
66+
<DatePicker
67+
value={date}
68+
onChange={(newDate) => {
69+
onValueChange(newDate)
70+
setIsOpen(false)
71+
}}
72+
className="max-h-75.5 min-w-80"
73+
/>
74+
</div>
75+
)}
76+
</>
77+
)
78+
}

web/components/ui/PopupDatePicker.tsx

Lines changed: 0 additions & 48 deletions
This file was deleted.

web/i18n/translations.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export type TasksTranslationEntries = {
6363
'nBed': (values: { count: number }) => string,
6464
'nCurrentlyPatients': (values: { count: number }) => string,
6565
'newestAdmissions': string,
66+
'newPatient': string,
6667
'noClosedTasks': string,
6768
'noOpenTasks': string,
6869
'noPatient': string,
@@ -197,6 +198,7 @@ export const tasksTranslation: Translation<TasksTranslationLocales, Partial<Task
197198
return _out
198199
},
199200
'newestAdmissions': `Neueste Aufnahmen`,
201+
'newPatient': `Neuer Patient`,
200202
'noClosedTasks': `Keine erledigten Aufgaben`,
201203
'noOpenTasks': `Keine offenen Aufgaben`,
202204
'noPatient': `Kein Patient`,
@@ -397,6 +399,7 @@ export const tasksTranslation: Translation<TasksTranslationLocales, Partial<Task
397399
return _out
398400
},
399401
'newestAdmissions': `Newest admissions`,
402+
'newPatient': `New Patient`,
400403
'noClosedTasks': `No closed tasks`,
401404
'noOpenTasks': `No open tasks`,
402405
'noPatient': `No Patient`,

0 commit comments

Comments
 (0)