Add patient personalization: profile saved once, reused every chat turn
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import { Body, Controller, Get, Post, Req, UseGuards } from "@nestjs/common";
|
||||
import type { AuthUser, LoginResponse } from "@duoc-thu/shared-types";
|
||||
import { Body, Controller, Get, Post, Put, Req, UseGuards } from "@nestjs/common";
|
||||
import type {
|
||||
AuthUser,
|
||||
LoginResponse,
|
||||
PatientProfile,
|
||||
} from "@duoc-thu/shared-types";
|
||||
import type { Request } from "express";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { LoginDto } from "./dto/login.dto";
|
||||
import { PatientProfileDto } from "./dto/patient-profile.dto";
|
||||
import { RegisterDto } from "./dto/register.dto";
|
||||
import { JwtAuthGuard, JwtPayload } from "./jwt.guard";
|
||||
|
||||
@@ -26,4 +31,21 @@ export class AuthController {
|
||||
const payload = (request as Request & { user: JwtPayload }).user;
|
||||
return { username: payload.username, role: payload.role };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get("patient-profile")
|
||||
getPatientProfile(@Req() request: Request): Promise<PatientProfile> {
|
||||
const payload = (request as Request & { user: JwtPayload }).user;
|
||||
return this.auth.getPatientProfile(payload.sub);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put("patient-profile")
|
||||
savePatientProfile(
|
||||
@Req() request: Request,
|
||||
@Body() body: PatientProfileDto
|
||||
): Promise<PatientProfile> {
|
||||
const payload = (request as Request & { user: JwtPayload }).user;
|
||||
return this.auth.savePatientProfile(payload.sub, body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import {
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import type { AuthUser, LoginResponse, UserRole } from "@duoc-thu/shared-types";
|
||||
import type {
|
||||
AuthUser,
|
||||
LoginResponse,
|
||||
PatientProfile,
|
||||
PatientProfileUpdate,
|
||||
UserRole,
|
||||
} from "@duoc-thu/shared-types";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { Pool } from "pg";
|
||||
|
||||
@@ -17,6 +23,22 @@ interface UserRow {
|
||||
role: UserRole;
|
||||
}
|
||||
|
||||
interface PatientProfileRow {
|
||||
age_text: string | null;
|
||||
weight_kg: string | null; // numeric comes back as string from `pg`
|
||||
renal_function: string | null;
|
||||
hepatic_function: string | null;
|
||||
known_allergies: string | null;
|
||||
}
|
||||
|
||||
const EMPTY_PROFILE: PatientProfile = {
|
||||
ageText: null,
|
||||
weightKg: null,
|
||||
renalFunction: null,
|
||||
hepaticFunction: null,
|
||||
knownAllergies: null,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@@ -67,4 +89,51 @@ export class AuthService {
|
||||
});
|
||||
return { user, token };
|
||||
}
|
||||
|
||||
async getPatientProfile(userId: string): Promise<PatientProfile> {
|
||||
const result = await this.pool.query<PatientProfileRow>(
|
||||
`SELECT age_text, weight_kg, renal_function, hepatic_function, known_allergies
|
||||
FROM patient_profile WHERE user_id = $1`,
|
||||
[userId]
|
||||
);
|
||||
const row = result.rows[0];
|
||||
if (!row) return EMPTY_PROFILE;
|
||||
return {
|
||||
ageText: row.age_text,
|
||||
weightKg: row.weight_kg === null ? null : Number(row.weight_kg),
|
||||
renalFunction: row.renal_function,
|
||||
hepaticFunction: row.hepatic_function,
|
||||
knownAllergies: row.known_allergies,
|
||||
};
|
||||
}
|
||||
|
||||
// Upsert on the single row per user — a doctor has at most one saved
|
||||
// profile, and `PUT` is expected to be called with the full form each
|
||||
// time (undefined fields here keep whatever was already stored, since a
|
||||
// partial `PatientProfileDto` only sends the keys the form actually has).
|
||||
async savePatientProfile(
|
||||
userId: string,
|
||||
update: PatientProfileUpdate
|
||||
): Promise<PatientProfile> {
|
||||
await this.pool.query(
|
||||
`INSERT INTO patient_profile (user_id, age_text, weight_kg, renal_function, hepatic_function, known_allergies, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, now())
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
age_text = COALESCE($2, patient_profile.age_text),
|
||||
weight_kg = COALESCE($3, patient_profile.weight_kg),
|
||||
renal_function = COALESCE($4, patient_profile.renal_function),
|
||||
hepatic_function = COALESCE($5, patient_profile.hepatic_function),
|
||||
known_allergies = COALESCE($6, patient_profile.known_allergies),
|
||||
updated_at = now()`,
|
||||
[
|
||||
userId,
|
||||
update.ageText ?? null,
|
||||
update.weightKg ?? null,
|
||||
update.renalFunction ?? null,
|
||||
update.hepaticFunction ?? null,
|
||||
update.knownAllergies ?? null,
|
||||
]
|
||||
);
|
||||
return this.getPatientProfile(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IsNumber, IsOptional, IsString, Length, Max, Min } from "class-validator";
|
||||
|
||||
export class PatientProfileDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(0, 64)
|
||||
ageText?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(500)
|
||||
weightKg?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(0, 200)
|
||||
renalFunction?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(0, 200)
|
||||
hepaticFunction?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(0, 500)
|
||||
knownAllergies?: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user