Skip to content

Commit 9e2ff2b

Browse files
committed
feat: add patient medical history fields
Add optional medical history fields to patient records: - Blood group (dropdown: A+, A-, B+, B-, AB+, AB-, O+, O-) - Allergies (tag-based input) - Pre-existing conditions (tag-based input) - Current medications (tag-based input) Also adds a link from edit patient page to view patient.
1 parent e9aa47e commit 9e2ff2b

File tree

7 files changed

+339
-12
lines changed

7 files changed

+339
-12
lines changed

src/main/db/migrations.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as m_005_backup_settings from './migrations/005_backup_settings'
77
import * as m_006_onboarding_setting from './migrations/006_onboarding_setting'
88
import * as m_007_activity_log from './migrations/007_activity_log'
99
import * as m_008_backup_frequency from './migrations/008_backup_frequency'
10+
import * as m_009_patient_medical_history from './migrations/009_patient_medical_history'
1011

1112
export default {
1213
'000_init': m_000_init,
@@ -17,5 +18,6 @@ export default {
1718
'005_backup_settings': m_005_backup_settings,
1819
'006_onboarding_setting': m_006_onboarding_setting,
1920
'007_activity_log': m_007_activity_log,
20-
'008_backup_frequency': m_008_backup_frequency
21+
'008_backup_frequency': m_008_backup_frequency,
22+
'009_patient_medical_history': m_009_patient_medical_history
2123
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { Kysely } from 'kysely'
3+
4+
export async function up(db: Kysely<any>): Promise<void> {
5+
await db.schema
6+
.alterTable('patients')
7+
.addColumn('blood_group', 'text') // A+, A-, B+, B-, AB+, AB-, O+, O-
8+
.execute()
9+
10+
await db.schema
11+
.alterTable('patients')
12+
.addColumn('allergies', 'text') // Comma-separated tags
13+
.execute()
14+
15+
await db.schema
16+
.alterTable('patients')
17+
.addColumn('conditions', 'text') // Comma-separated tags (pre-existing conditions)
18+
.execute()
19+
20+
await db.schema
21+
.alterTable('patients')
22+
.addColumn('medications', 'text') // Comma-separated tags (current medications)
23+
.execute()
24+
}
25+
26+
export async function down(db: Kysely<any>): Promise<void> {
27+
await db.schema.alterTable('patients').dropColumn('blood_group').execute()
28+
await db.schema.alterTable('patients').dropColumn('allergies').execute()
29+
await db.schema.alterTable('patients').dropColumn('conditions').execute()
30+
await db.schema.alterTable('patients').dropColumn('medications').execute()
31+
}

src/renderer/src/components/patient/NewPatientForm.tsx

Lines changed: 175 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ import {
3232
MapPin,
3333
Phone,
3434
FileText,
35-
Calendar
35+
Calendar,
36+
HeartPulse,
37+
Droplets,
38+
AlertTriangle,
39+
ClipboardList,
40+
Pill
3641
} from 'lucide-react'
42+
import { TagsInput } from '../ui/tags-input'
3743

3844
export interface CreatePatientFormProps {
3945
onRecordUpdated?: (patient: Patient) => void
@@ -77,6 +83,17 @@ const birthYearFromAge = (birth_year: number) => {
7783
return currentYear - birth_year
7884
}
7985

86+
const BLOOD_GROUPS = ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'] as const
87+
88+
// Helper functions to convert between arrays and comma-separated strings
89+
const tagsToString = (tags: string[]): string | null => {
90+
return tags.length > 0 ? tags.join(',') : null
91+
}
92+
93+
const stringToTags = (str: string | null): string[] => {
94+
return str ? str.split(',').filter((tag) => tag.trim()) : []
95+
}
96+
8097
const patientSchema = z.object({
8198
phn: z.string().min(1, {
8299
message: 'PHN is required'
@@ -94,7 +111,12 @@ const patientSchema = z.object({
94111
.regex(AGE_PATTERN, 'Age must be in the format(year, year month or month) ie: 20y, 10y 6m, 5m'),
95112
gender: z.enum(['M', 'F'], {
96113
message: 'Select Gender'
97-
})
114+
}),
115+
// Medical history fields
116+
blood_group: z.enum(BLOOD_GROUPS).optional().nullable(),
117+
allergies: z.array(z.string()).optional().default([]),
118+
conditions: z.array(z.string()).optional().default([]),
119+
medications: z.array(z.string()).optional().default([])
98120
})
99121

100122
type FormSchema = z.infer<typeof patientSchema>
@@ -117,7 +139,12 @@ export const NewPatientForm = forwardRef<NewPatientFormRef, CreatePatientFormPro
117139
emergency_contact: values?.emergency_contact || '',
118140
emergency_phone: values?.emergency_phone || '',
119141
gender: values?.gender || ('' as any),
120-
age: values && values.birth_year ? `${birthYearFromAge(values.birth_year)}y` : ''
142+
age: values && values.birth_year ? `${birthYearFromAge(values.birth_year)}y` : '',
143+
// Medical history fields
144+
blood_group: values?.blood_group as typeof BLOOD_GROUPS[number] | null || null,
145+
allergies: stringToTags(values?.allergies || null),
146+
conditions: stringToTags(values?.conditions || null),
147+
medications: stringToTags(values?.medications || null)
121148
}
122149
})
123150
const isUpdate = values && !!values.id
@@ -130,7 +157,11 @@ export const NewPatientForm = forwardRef<NewPatientFormRef, CreatePatientFormPro
130157
if (isUpdate) {
131158
form.reset({
132159
...values,
133-
age: values && values.birth_year ? `${birthYearFromAge(values.birth_year)}y` : ''
160+
age: values && values.birth_year ? `${birthYearFromAge(values.birth_year)}y` : '',
161+
blood_group: values?.blood_group as typeof BLOOD_GROUPS[number] | null || null,
162+
allergies: stringToTags(values?.allergies || null),
163+
conditions: stringToTags(values?.conditions || null),
164+
medications: stringToTags(values?.medications || null)
134165
})
135166

136167
return
@@ -145,8 +176,11 @@ export const NewPatientForm = forwardRef<NewPatientFormRef, CreatePatientFormPro
145176
emergency_contact: '',
146177
emergency_phone: '',
147178
remarks: '',
148-
149-
gender: '' as any
179+
gender: '' as any,
180+
blood_group: null,
181+
allergies: [],
182+
conditions: [],
183+
medications: []
150184
})
151185
}, [form, isUpdate, values])
152186

@@ -163,7 +197,11 @@ export const NewPatientForm = forwardRef<NewPatientFormRef, CreatePatientFormPro
163197
phone: data.phone,
164198
emergency_contact: data.emergency_contact,
165199
emergency_phone: data.emergency_phone,
166-
remarks: data.remarks
200+
remarks: data.remarks,
201+
blood_group: data.blood_group || null,
202+
allergies: tagsToString(data.allergies || []),
203+
conditions: tagsToString(data.conditions || []),
204+
medications: tagsToString(data.medications || [])
167205
})
168206

169207
if (error) {
@@ -199,7 +237,11 @@ export const NewPatientForm = forwardRef<NewPatientFormRef, CreatePatientFormPro
199237
phone: data.phone,
200238
emergency_contact: data.emergency_contact,
201239
emergency_phone: data.emergency_phone,
202-
remarks: data.remarks
240+
remarks: data.remarks,
241+
blood_group: data.blood_group || null,
242+
allergies: tagsToString(data.allergies || []),
243+
conditions: tagsToString(data.conditions || []),
244+
medications: tagsToString(data.medications || [])
203245
})
204246

205247
if (error) {
@@ -505,10 +547,134 @@ export const NewPatientForm = forwardRef<NewPatientFormRef, CreatePatientFormPro
505547
</CardContent>
506548
</Card>
507549

508-
{/* Row 4: Notes */}
550+
{/* Row 4: Medical History */}
509551
<Card
510552
className="bg-gradient-to-br from-card to-card/80 animate-fade-in-up"
511553
style={{ animationDelay: '300ms' }}
554+
>
555+
<CardHeader className="pb-3 pt-4">
556+
<div className="flex items-center gap-2.5">
557+
<div className="h-8 w-8 rounded-lg bg-rose-500/10 flex items-center justify-center">
558+
<HeartPulse className="h-4 w-4 text-rose-500" />
559+
</div>
560+
<CardTitle className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
561+
Medical History
562+
</CardTitle>
563+
</div>
564+
</CardHeader>
565+
<CardContent className="pt-0 space-y-4">
566+
{/* Blood Group Field */}
567+
<FormField
568+
control={form.control}
569+
name="blood_group"
570+
render={({ field }) => (
571+
<FormItem>
572+
<div className="flex items-center gap-2 mb-1.5">
573+
<div className="h-6 w-6 rounded-md bg-red-500/10 flex items-center justify-center">
574+
<Droplets className="h-3.5 w-3.5 text-red-500" />
575+
</div>
576+
<FormLabel className="text-sm font-medium">Blood Group</FormLabel>
577+
</div>
578+
<Select onValueChange={field.onChange} value={field.value || ''}>
579+
<FormControl>
580+
<SelectTrigger>
581+
<SelectValue placeholder="Select blood group..." />
582+
</SelectTrigger>
583+
</FormControl>
584+
<SelectContent>
585+
{BLOOD_GROUPS.map((group) => (
586+
<SelectItem key={group} value={group}>
587+
{group}
588+
</SelectItem>
589+
))}
590+
</SelectContent>
591+
</Select>
592+
<FormMessage />
593+
</FormItem>
594+
)}
595+
/>
596+
597+
{/* Allergies Field */}
598+
<FormField
599+
control={form.control}
600+
name="allergies"
601+
render={({ field }) => (
602+
<FormItem>
603+
<div className="flex items-center gap-2 mb-1.5">
604+
<div className="h-6 w-6 rounded-md bg-amber-500/10 flex items-center justify-center">
605+
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
606+
</div>
607+
<FormLabel className="text-sm font-medium">Known Allergies</FormLabel>
608+
</div>
609+
<FormControl>
610+
<TagsInput
611+
value={field.value || []}
612+
onChange={field.onChange}
613+
placeholder="Add allergy..."
614+
/>
615+
</FormControl>
616+
<FormDescription className="text-xs">
617+
Drug allergies, food allergies, environmental triggers
618+
</FormDescription>
619+
<FormMessage />
620+
</FormItem>
621+
)}
622+
/>
623+
624+
{/* Pre-existing Conditions Field */}
625+
<FormField
626+
control={form.control}
627+
name="conditions"
628+
render={({ field }) => (
629+
<FormItem>
630+
<div className="flex items-center gap-2 mb-1.5">
631+
<div className="h-6 w-6 rounded-md bg-violet-500/10 flex items-center justify-center">
632+
<ClipboardList className="h-3.5 w-3.5 text-violet-500" />
633+
</div>
634+
<FormLabel className="text-sm font-medium">Pre-existing Conditions</FormLabel>
635+
</div>
636+
<FormControl>
637+
<TagsInput
638+
value={field.value || []}
639+
onChange={field.onChange}
640+
placeholder="Add condition..."
641+
/>
642+
</FormControl>
643+
<FormMessage />
644+
</FormItem>
645+
)}
646+
/>
647+
648+
{/* Current Medications Field */}
649+
<FormField
650+
control={form.control}
651+
name="medications"
652+
render={({ field }) => (
653+
<FormItem>
654+
<div className="flex items-center gap-2 mb-1.5">
655+
<div className="h-6 w-6 rounded-md bg-emerald-500/10 flex items-center justify-center">
656+
<Pill className="h-3.5 w-3.5 text-emerald-500" />
657+
</div>
658+
<FormLabel className="text-sm font-medium">Current Medications</FormLabel>
659+
</div>
660+
<FormControl>
661+
<TagsInput
662+
value={field.value || []}
663+
onChange={field.onChange}
664+
placeholder="Add medication..."
665+
/>
666+
</FormControl>
667+
<FormMessage />
668+
</FormItem>
669+
)}
670+
/>
671+
</CardContent>
672+
</Card>
673+
674+
{/* Row 5: Notes */}
675+
<Card
676+
className="bg-gradient-to-br from-card to-card/80 animate-fade-in-up"
677+
style={{ animationDelay: '375ms' }}
512678
>
513679
<CardHeader className="pb-3 pt-4">
514680
<div className="flex items-center gap-2.5">

src/renderer/src/routes/patients/edit-patient.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ export const EditPatient = () => {
5757
<div>
5858
<h1 className="text-2xl font-bold tracking-tight">Edit Patient</h1>
5959
<p className="text-sm text-muted-foreground">
60-
Update details for {data?.name || data?.phn || 'patient'}
60+
Update details for{' '}
61+
{data ? (
62+
<Link to={`/patients/${data.id}`} className="text-primary hover:underline">
63+
{data.name || data.phn}
64+
</Link>
65+
) : (
66+
'patient'
67+
)}
6168
</p>
6269
</div>
6370
</div>

0 commit comments

Comments
 (0)