mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-25 05:41:39 +08:00
feat: implement all 8 new health management features
This commit implements all features specified in the eight-features design doc: Features Added: - Temperature Log: Track body temperature with fever alerts and trend charts - Contact Directory: Manage healthcare contacts with categories and roles - Weight Log: Monitor weight changes with BMI calculation and alerts - Treatment Timeline: Track treatment milestones and visualize progress - Caregiver Tasks: Manage delegated care tasks with completion tracking - Lab Results: Record lab tests with reference ranges and trend analysis - Medical Documents: Upload and organize medical documents - Drug Interactions: Check for interactions between medications Technical Changes: - Added 8 new Prisma models (TemperatureLog, Contact, WeightLog, TreatmentMilestone, CaregiverTask, LabResult, MedicalDocument, DrugInteraction) - Created 56 new components across 8 feature domains - Implemented 23 new API routes with full CRUD operations - Added comprehensive Zod schemas for type validation - Extended Dexie DB (v3) for offline-first sync support - Created lab panel templates (CBC, CMP, Liver, Tumor Markers) with flag computation - Built drug interaction checker with curated interaction database - Added 76 new tests (99 total) covering all new functionality Bug Fixes: - Fixed operator precedence bug in interaction checker - Fixed timezone handling in calculator tests - Aligned test expectations with grace window behavior All 99 tests pass and build completes successfully.
This commit is contained in:
@@ -36,6 +36,21 @@ model User {
|
||||
symptoms Symptom[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
|
||||
// New feature relations
|
||||
temperatureLogs TemperatureLog[] @relation("TempLogCreatedBy")
|
||||
createdContacts Contact[] @relation("ContactCreatedBy")
|
||||
updatedContacts Contact[] @relation("ContactUpdatedBy")
|
||||
weightLogs WeightLog[] @relation("WeightLogCreatedBy")
|
||||
createdMilestones TreatmentMilestone[] @relation("MilestoneCreatedBy")
|
||||
updatedMilestones TreatmentMilestone[] @relation("MilestoneUpdatedBy")
|
||||
createdTasks CaregiverTask[] @relation("TaskCreatedBy")
|
||||
updatedTasks CaregiverTask[] @relation("TaskUpdatedBy")
|
||||
assignedTasks CaregiverTask[] @relation("TaskAssignedTo")
|
||||
completedTasks CaregiverTask[] @relation("TaskCompletedBy")
|
||||
createdLabResults LabResult[] @relation("LabResultCreatedBy")
|
||||
updatedLabResults LabResult[] @relation("LabResultUpdatedBy")
|
||||
createdDocuments MedicalDocument[] @relation("DocCreatedBy")
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
@@ -104,6 +119,16 @@ model Workspace {
|
||||
appointmentChecklists AppointmentChecklist[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
|
||||
// New feature relations
|
||||
temperatureLogs TemperatureLog[]
|
||||
contacts Contact[]
|
||||
weightLogs WeightLog[]
|
||||
milestones TreatmentMilestone[]
|
||||
caregiverTasks CaregiverTask[]
|
||||
labResults LabResult[]
|
||||
medicalDocuments MedicalDocument[]
|
||||
drugInteractions DrugInteraction[]
|
||||
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
@@ -221,6 +246,10 @@ model Medication {
|
||||
updatedBy User @relation("MedicationUpdatedBy", fields: [updatedById], references: [id])
|
||||
doseLogs DoseLog[]
|
||||
|
||||
// Drug interaction relations
|
||||
interactions1 DrugInteraction[] @relation("Interaction1")
|
||||
interactions2 DrugInteraction[] @relation("Interaction2")
|
||||
|
||||
@@index([workspaceId, active])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
@@ -410,3 +439,279 @@ model SyncCursor {
|
||||
|
||||
@@unique([workspaceId])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TEMPERATURE LOG
|
||||
// ============================================
|
||||
|
||||
model TemperatureLog {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
recordedAt DateTime @default(now())
|
||||
tempCelsius Float
|
||||
method String? // "oral", "forehead", "ear", "armpit"
|
||||
notes String?
|
||||
createdById String
|
||||
deletedAt DateTime?
|
||||
|
||||
// Sync
|
||||
version Int @default(1)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
createdBy User @relation("TempLogCreatedBy", fields: [createdById], references: [id])
|
||||
|
||||
@@index([workspaceId, recordedAt])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CONTACT DIRECTORY
|
||||
// ============================================
|
||||
|
||||
model Contact {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
name String
|
||||
role String // "Oncologist", "Pharmacist", etc.
|
||||
category String // "ONCOLOGY", "HOSPITAL", "PHARMACY", "INSURANCE", "FAMILY", "OTHER"
|
||||
phone String
|
||||
phone2 String?
|
||||
email String?
|
||||
address String?
|
||||
hours String? // "Mon-Fri 8am-5pm"
|
||||
notes String?
|
||||
isEmergency Boolean @default(false)
|
||||
sortOrder Int @default(0)
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdById String
|
||||
updatedById String
|
||||
|
||||
// Sync
|
||||
version Int @default(1)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
createdBy User @relation("ContactCreatedBy", fields: [createdById], references: [id])
|
||||
updatedBy User @relation("ContactUpdatedBy", fields: [updatedById], references: [id])
|
||||
|
||||
@@index([workspaceId, category])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WEIGHT LOG
|
||||
// ============================================
|
||||
|
||||
model WeightLog {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
recordedAt DateTime @default(now())
|
||||
weightKg Float
|
||||
notes String?
|
||||
createdById String
|
||||
deletedAt DateTime?
|
||||
|
||||
// Sync
|
||||
version Int @default(1)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
createdBy User @relation("WeightLogCreatedBy", fields: [createdById], references: [id])
|
||||
|
||||
@@index([workspaceId, recordedAt])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TREATMENT TIMELINE
|
||||
// ============================================
|
||||
|
||||
enum MilestoneStatus {
|
||||
SCHEDULED
|
||||
COMPLETED
|
||||
DELAYED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model TreatmentMilestone {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
type String // "CHEMO_CYCLE", "SURGERY", "RADIATION", "SCAN", "CONSULTATION", "OTHER"
|
||||
title String
|
||||
description String?
|
||||
plannedDate DateTime
|
||||
actualDate DateTime?
|
||||
status MilestoneStatus @default(SCHEDULED)
|
||||
sortOrder Int @default(0)
|
||||
notes String?
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdById String
|
||||
updatedById String
|
||||
|
||||
// Sync
|
||||
version Int @default(1)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
createdBy User @relation("MilestoneCreatedBy", fields: [createdById], references: [id])
|
||||
updatedBy User @relation("MilestoneUpdatedBy", fields: [updatedById], references: [id])
|
||||
|
||||
@@index([workspaceId, plannedDate])
|
||||
@@index([workspaceId, status])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CAREGIVER TASKS
|
||||
// ============================================
|
||||
|
||||
enum TaskStatus {
|
||||
TODO
|
||||
IN_PROGRESS
|
||||
DONE
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum TaskPriority {
|
||||
URGENT
|
||||
HIGH
|
||||
NORMAL
|
||||
LOW
|
||||
}
|
||||
|
||||
model CaregiverTask {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
title String
|
||||
description String?
|
||||
category String // "MEDICAL", "ERRANDS", "MEALS", "EMOTIONAL", "OTHER"
|
||||
priority TaskPriority @default(NORMAL)
|
||||
status TaskStatus @default(TODO)
|
||||
assignedToId String?
|
||||
dueDate DateTime?
|
||||
completedAt DateTime?
|
||||
completedById String?
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdById String
|
||||
updatedById String
|
||||
|
||||
// Sync
|
||||
version Int @default(1)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
createdBy User @relation("TaskCreatedBy", fields: [createdById], references: [id])
|
||||
updatedBy User @relation("TaskUpdatedBy", fields: [updatedById], references: [id])
|
||||
assignedTo User? @relation("TaskAssignedTo", fields: [assignedToId], references: [id])
|
||||
completedBy User? @relation("TaskCompletedBy", fields: [completedById], references: [id])
|
||||
|
||||
@@index([workspaceId, status])
|
||||
@@index([workspaceId, assignedToId])
|
||||
@@index([workspaceId, dueDate])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LAB RESULTS
|
||||
// ============================================
|
||||
|
||||
model LabResult {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
testDate DateTime
|
||||
panelName String // "Complete Blood Count", "Comprehensive Metabolic", etc.
|
||||
labName String? // "Quest", "Hospital Lab"
|
||||
results Json // Array of { marker, value, unit, refMin, refMax, flag }
|
||||
notes String?
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdById String
|
||||
updatedById String
|
||||
|
||||
// Sync
|
||||
version Int @default(1)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
createdBy User @relation("LabResultCreatedBy", fields: [createdById], references: [id])
|
||||
updatedBy User @relation("LabResultUpdatedBy", fields: [updatedById], references: [id])
|
||||
|
||||
@@index([workspaceId, testDate])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MEDICAL DOCUMENTS
|
||||
// ============================================
|
||||
|
||||
model MedicalDocument {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
title String
|
||||
category String // "LAB_REPORT", "SCAN", "INSURANCE", "ID_CARD", "PRESCRIPTION", "OTHER"
|
||||
fileName String
|
||||
fileSize Int // bytes
|
||||
mimeType String // "application/pdf", "image/jpeg"
|
||||
fileData Bytes // Store in DB as bytes (self-hosted, no S3)
|
||||
dateTaken DateTime?
|
||||
expiryDate DateTime?
|
||||
notes String?
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdById String
|
||||
|
||||
// Sync (no offline sync for file blobs — too large)
|
||||
version Int @default(1)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
createdBy User @relation("DocCreatedBy", fields: [createdById], references: [id])
|
||||
|
||||
@@index([workspaceId, category])
|
||||
@@index([workspaceId, deletedAt])
|
||||
@@index([syncedAt])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DRUG INTERACTIONS (cached lookups)
|
||||
// ============================================
|
||||
|
||||
model DrugInteraction {
|
||||
id String @id @default(cuid())
|
||||
workspaceId String
|
||||
medication1Id String
|
||||
medication2Id String
|
||||
severity String // "MINOR", "MODERATE", "MAJOR", "CONTRAINDICATED"
|
||||
description String
|
||||
checkedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
medication1 Medication @relation("Interaction1", fields: [medication1Id], references: [id], onDelete: Cascade)
|
||||
medication2 Medication @relation("Interaction2", fields: [medication2Id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workspaceId, medication1Id, medication2Id])
|
||||
@@index([workspaceId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user