mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-24 21:31:43 +08:00
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.
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
'use client'
|
|
|
|
import { format, isPast, addDays } from 'date-fns'
|
|
import { FileText, Image as ImageIcon, File } from 'lucide-react'
|
|
|
|
interface DocumentData {
|
|
id: string
|
|
title: string
|
|
category: string
|
|
fileName: string
|
|
fileSize: number
|
|
mimeType: string
|
|
dateTaken: string | null
|
|
expiryDate: string | null
|
|
notes: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
interface DocumentCardProps {
|
|
document: DocumentData
|
|
onView: (doc: DocumentData) => void
|
|
}
|
|
|
|
const CATEGORY_BADGES: Record<string, string> = {
|
|
LAB_REPORT: 'bg-blue-100 text-blue-700',
|
|
SCAN: 'bg-purple-100 text-purple-700',
|
|
INSURANCE: 'bg-green-100 text-green-700',
|
|
ID_CARD: 'bg-orange-100 text-orange-700',
|
|
PRESCRIPTION: 'bg-pink-100 text-pink-700',
|
|
OTHER: 'bg-secondary-100 text-secondary-700',
|
|
}
|
|
|
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
LAB_REPORT: 'Lab Report',
|
|
SCAN: 'Scan',
|
|
INSURANCE: 'Insurance',
|
|
ID_CARD: 'ID Card',
|
|
PRESCRIPTION: 'Prescription',
|
|
OTHER: 'Other',
|
|
}
|
|
|
|
function FileIcon({ mimeType }: { mimeType: string }) {
|
|
if (mimeType === 'application/pdf') return <FileText className="w-6 h-6 text-red-500" />
|
|
if (mimeType.startsWith('image/')) return <ImageIcon className="w-6 h-6 text-blue-500" />
|
|
return <File className="w-6 h-6 text-secondary-500" />
|
|
}
|
|
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
}
|
|
|
|
export function DocumentCard({ document: doc, onView }: DocumentCardProps) {
|
|
const badge = CATEGORY_BADGES[doc.category] || CATEGORY_BADGES.OTHER
|
|
const label = CATEGORY_LABELS[doc.category] || doc.category
|
|
const isExpiringSoon = doc.expiryDate && !isPast(new Date(doc.expiryDate)) &&
|
|
isPast(addDays(new Date(), -30))
|
|
const isExpired = doc.expiryDate && isPast(new Date(doc.expiryDate))
|
|
|
|
return (
|
|
<div
|
|
onClick={() => onView(doc)}
|
|
className="bg-surface rounded-card border border-border p-4 cursor-pointer hover:shadow-card-hover transition-shadow"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center flex-shrink-0">
|
|
<FileIcon mimeType={doc.mimeType} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-secondary-900 truncate">{doc.title}</h3>
|
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
|
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${badge}`}>
|
|
{label}
|
|
</span>
|
|
<span className="text-xs text-secondary-400">
|
|
{formatFileSize(doc.fileSize)}
|
|
</span>
|
|
{doc.dateTaken && (
|
|
<span className="text-xs text-secondary-500">
|
|
{format(new Date(doc.dateTaken), 'MMM d, yyyy')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{/* Expiry indicators */}
|
|
{isExpired && (
|
|
<span className="inline-block mt-1.5 text-xs font-semibold px-2 py-0.5 rounded-full bg-red-100 text-red-700">
|
|
Expired
|
|
</span>
|
|
)}
|
|
{isExpiringSoon && !isExpired && (
|
|
<span className="inline-block mt-1.5 text-xs font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">
|
|
Expiring soon
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|