Files
nextstep/src/components/documents/DocumentCard.tsx
Tony0410 f0f674945c 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.
2026-03-02 11:17:38 +00:00

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>
)
}