From 457b92bdba95a272c99587abb5f5c0144b341cf7 Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Fri, 6 Mar 2026 17:10:30 +0530 Subject: [PATCH 01/10] patient:profile management --- api-gateway/src/app.ts | 14 ++++++++++++-- api-gateway/src/middlewares/patient.guard.ts | 11 ++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/api-gateway/src/app.ts b/api-gateway/src/app.ts index d8a5f45..16147a7 100644 --- a/api-gateway/src/app.ts +++ b/api-gateway/src/app.ts @@ -25,8 +25,18 @@ app.use('/health', healthRoutes); app.use('/patients/public', patientAuthProxy); // PATIENT self routes -app.use('/patients', authenticate, requirePatientSelf, patientDataProxy); - +app.use( + '/patients', + authenticate, + requirePatientSelf, + (req: any, _res, next) => { + if (req.user?.sub) { + req.headers['x-user-id'] = String(req.user.sub); + } + next(); + }, + patientDataProxy +); // STAFF auth app.use('/staff/public', staffAuthProxy); app.use('/staff', authenticate, staffDataRouter); diff --git a/api-gateway/src/middlewares/patient.guard.ts b/api-gateway/src/middlewares/patient.guard.ts index 3c2b690..41f79a1 100644 --- a/api-gateway/src/middlewares/patient.guard.ts +++ b/api-gateway/src/middlewares/patient.guard.ts @@ -10,13 +10,22 @@ export function requirePatientSelf( return res.status(401).json({ error: 'Unauthorized' }); } - // URL will be /:id after /patients is stripped const patientIdFromPath = req.path.split('/')[1]; if (!patientIdFromPath) { return res.status(400).json({ error: 'Invalid patient path' }); } + /** + * Allow /patients/me directly + */ + if (patientIdFromPath === 'me') { + return next(); + } + + /** + * Protect /patients/:id + */ if (req.user.sub !== patientIdFromPath) { return res.status(403).json({ error: 'Access denied' }); } From 6f5591d6d4295dd908acb61d17f4b00f230b30cf Mon Sep 17 00:00:00 2001 From: linasliyakath Date: Fri, 6 Mar 2026 17:13:43 +0530 Subject: [PATCH 02/10] patient profile --- .../src/app/(patient)/dashboard/layout.tsx | 6 + .../app/(patient)/dashboard/profile/page.tsx | 23 ++ frontend/src/auth/auth.provider.tsx | 1 + .../src/features/patient/api/getProfile.ts | 6 + .../src/features/patient/api/updateProfile.ts | 6 + .../patient/components/ProfileForm.tsx | 289 ++++++++++++++++++ .../patient/hooks/usePatientProfile.ts | 9 + .../patient/hooks/useUpdateProfile.ts | 16 + services/patient-service/scripts/init-db.ts | 53 +++- .../modules/patients/patient.controller.ts | 32 ++ .../patients/patient.profile.repository.ts | 110 +++++++ .../src/modules/patients/patient.routes.ts | 14 +- .../src/modules/patients/patient.service.ts | 8 + 13 files changed, 568 insertions(+), 5 deletions(-) create mode 100644 frontend/src/app/(patient)/dashboard/profile/page.tsx create mode 100644 frontend/src/features/patient/api/getProfile.ts create mode 100644 frontend/src/features/patient/api/updateProfile.ts create mode 100644 frontend/src/features/patient/components/ProfileForm.tsx create mode 100644 frontend/src/features/patient/hooks/usePatientProfile.ts create mode 100644 frontend/src/features/patient/hooks/useUpdateProfile.ts create mode 100644 services/patient-service/src/modules/patients/patient.profile.repository.ts diff --git a/frontend/src/app/(patient)/dashboard/layout.tsx b/frontend/src/app/(patient)/dashboard/layout.tsx index 13dbeda..56bc6d9 100644 --- a/frontend/src/app/(patient)/dashboard/layout.tsx +++ b/frontend/src/app/(patient)/dashboard/layout.tsx @@ -52,6 +52,12 @@ export default function PatientDashboardLayout({ label="Book Appointment" active={pathname === '/dashboard/book'} /> + + diff --git a/frontend/src/app/(patient)/dashboard/profile/page.tsx b/frontend/src/app/(patient)/dashboard/profile/page.tsx new file mode 100644 index 0000000..fd7189a --- /dev/null +++ b/frontend/src/app/(patient)/dashboard/profile/page.tsx @@ -0,0 +1,23 @@ +'use client'; + +import ProfileForm from '@/src/features/patient/components/ProfileForm'; + +export default function ProfilePage() { + + return ( + +
+ +

+ + My Profile + +

+ + + +
+ + ); + +} \ No newline at end of file diff --git a/frontend/src/auth/auth.provider.tsx b/frontend/src/auth/auth.provider.tsx index c361883..1c6192c 100644 --- a/frontend/src/auth/auth.provider.tsx +++ b/frontend/src/auth/auth.provider.tsx @@ -24,6 +24,7 @@ type AuthContextType = { loginSuccess: (data: { accessToken: string; patient: Patient; + }) => void; logout: () => void; }; diff --git a/frontend/src/features/patient/api/getProfile.ts b/frontend/src/features/patient/api/getProfile.ts new file mode 100644 index 0000000..b321180 --- /dev/null +++ b/frontend/src/features/patient/api/getProfile.ts @@ -0,0 +1,6 @@ +import { api } from '@/src/lib/api'; + +export async function getProfile() { + const res = await api.get('/patients/me'); + return res.data; +} diff --git a/frontend/src/features/patient/api/updateProfile.ts b/frontend/src/features/patient/api/updateProfile.ts new file mode 100644 index 0000000..90fa15b --- /dev/null +++ b/frontend/src/features/patient/api/updateProfile.ts @@ -0,0 +1,6 @@ +import { api } from '@/src/lib/api'; + +export async function updateProfile(data: any) { + const res = await api.patch('/patients/me', data); + return res.data; +} diff --git a/frontend/src/features/patient/components/ProfileForm.tsx b/frontend/src/features/patient/components/ProfileForm.tsx new file mode 100644 index 0000000..3d6c2c2 --- /dev/null +++ b/frontend/src/features/patient/components/ProfileForm.tsx @@ -0,0 +1,289 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { usePatientProfile } from '../hooks/usePatientProfile'; +import { useUpdateProfile } from '../hooks/useUpdateProfile'; +import { + User, + Activity, + MapPin, + PhoneCall, + Pencil, + Save, + X, + Droplets, + Ruler, + Weight, + Calendar, + ShieldAlert, + Mail, + Phone, + Fingerprint +} from 'lucide-react'; + +export default function ProfileForm() { + const { data, isLoading } = usePatientProfile(); + const updateProfile = useUpdateProfile(); + const [isEditing, setIsEditing] = useState(false); + + const [form, setForm] = useState({ + name: '', + email: '', + phone: '', + dob: '', + blood_group: '', + height_cm: '', + weight_kg: '', + allergies: '', + chronic_conditions: '', + address_line1: '', + city: '', + state: '', + country: '', + pincode: '', + emergency_contact_name: '', + emergency_contact_phone: '', + emergency_contact_relation: '' + }); + + useEffect(() => { + if (!data) return; + setForm({ + name: data.name || '', + email: data.email || '', + phone: data.phone || '', + dob: data.dob || '', + blood_group: data.blood_group || '', + height_cm: data.height_cm || '', + weight_kg: data.weight_kg || '', + allergies: data.allergies || '', + chronic_conditions: data.chronic_conditions || '', + address_line1: data.address_line1 || '', + city: data.city || '', + state: data.state || '', + country: data.country || '', + pincode: data.pincode || '', + emergency_contact_name: data.emergency_contact_name || '', + emergency_contact_phone: data.emergency_contact_phone || '', + emergency_contact_relation: data.emergency_contact_relation || '' + }); + }, [data]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + const handleChange = (e: React.ChangeEvent) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + updateProfile.mutate(form, { + onSuccess: () => setIsEditing(false) + }); + }; + + const age = data?.dob ? new Date().getFullYear() - new Date(data.dob).getFullYear() : '—'; + + return ( +
+ + {/* HEADER SECTION */} +
+
+ +
+
+
+ {form.name?.charAt(0)} +
+
+

{form.name}

+
+ + Patient ID: {data.id?.slice(-6).toUpperCase() || 'H-OS-092'} + + + Account Active + +
+
+
+ +
+ {!isEditing ? ( + + ) : ( + + )} +
+
+ +
+ } /> + } /> + } /> + } /> +
+
+ +
+ + {/* LEFT COLUMN */} +
+ + {/* PERSONAL IDENTITY SECTION */} +
+
+
+

Personal Identity

+
+ +
+
+ } /> +
+ } /> + } /> +
+ } /> +
+
+
+ + {/* CLINICAL BACKGROUND */} +
+
+
+

Clinical Background

+
+ +
+
+ +
+ + + +
+
+ +
+ +