218 lines
6.6 KiB
TypeScript
218 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useMemo } from 'react'
|
|
import { format } from 'date-fns'
|
|
import { X, FileText, Download, Share2 } from 'lucide-react'
|
|
|
|
import { Button, showToast } from '@/components/ui'
|
|
|
|
interface Symptom {
|
|
id: string
|
|
type: string
|
|
customName?: string
|
|
severity: number
|
|
notes?: string
|
|
recordedAt: string
|
|
durationMinutes?: number
|
|
triggers?: string
|
|
medicationTaken?: string
|
|
reliefNotes?: string
|
|
}
|
|
|
|
interface SymptomReportGeneratorProps {
|
|
symptoms: Symptom[]
|
|
workspaceId: string
|
|
timeRange: number
|
|
onClose: () => void
|
|
}
|
|
|
|
const SYMPTOM_LABELS: Record<string, string> = {
|
|
FATIGUE: 'Fatigue',
|
|
NAUSEA: 'Nausea',
|
|
PAIN: 'Pain',
|
|
APPETITE: 'Appetite Loss',
|
|
SLEEP: 'Sleep Issues',
|
|
MOOD: 'Mood Changes',
|
|
CUSTOM: 'Other',
|
|
}
|
|
|
|
export function SymptomReportGenerator({
|
|
symptoms,
|
|
workspaceId,
|
|
timeRange,
|
|
onClose
|
|
}: SymptomReportGeneratorProps) {
|
|
const [generating, setGenerating] = useState(false)
|
|
|
|
const reportData = useMemo(() => {
|
|
// Group by type
|
|
const byType: Record<string, Symptom[]> = {}
|
|
symptoms.forEach(s => {
|
|
if (!byType[s.type]) byType[s.type] = []
|
|
byType[s.type].push(s)
|
|
})
|
|
|
|
// Calculate averages
|
|
const averages = Object.entries(byType).map(([type, items]) => ({
|
|
type,
|
|
label: SYMPTOM_LABELS[type] || type,
|
|
count: items.length,
|
|
avgSeverity: (items.reduce((sum, s) => sum + s.severity, 0) / items.length).toFixed(1),
|
|
maxSeverity: Math.max(...items.map(s => s.severity)),
|
|
}))
|
|
|
|
// Sort by frequency
|
|
averages.sort((a, b) => b.count - a.count)
|
|
|
|
// Daily breakdown
|
|
const byDay: Record<string, Symptom[]> = {}
|
|
symptoms.forEach(s => {
|
|
const day = format(new Date(s.recordedAt), 'yyyy-MM-dd')
|
|
if (!byDay[day]) byDay[day] = []
|
|
byDay[day].push(s)
|
|
})
|
|
|
|
return { byType, averages, byDay }
|
|
}, [symptoms])
|
|
|
|
const generatePDF = async () => {
|
|
setGenerating(true)
|
|
try {
|
|
const response = await fetch(`/api/workspaces/${workspaceId}/symptoms/report`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
symptoms,
|
|
timeRange,
|
|
generatedAt: new Date().toISOString()
|
|
})
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Failed to generate report')
|
|
|
|
const blob = await response.blob()
|
|
const url = window.URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `symptom-report-${format(new Date(), 'yyyy-MM-dd')}.pdf`
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
window.URL.revokeObjectURL(url)
|
|
document.body.removeChild(a)
|
|
|
|
showToast('Report downloaded', 'success')
|
|
} catch {
|
|
showToast('Failed to generate report', 'error')
|
|
} finally {
|
|
setGenerating(false)
|
|
}
|
|
}
|
|
|
|
const copySummary = () => {
|
|
const summary = `
|
|
Symptom Summary (Last ${timeRange} Days)
|
|
Generated: ${format(new Date(), 'MMM d, yyyy')}
|
|
|
|
Total Symptoms Logged: ${symptoms.length}
|
|
|
|
By Symptom Type:
|
|
${reportData.averages.map(a =>
|
|
`- ${a.label}: ${a.count} times (avg severity: ${a.avgSeverity}/5)`
|
|
).join('\n')}
|
|
|
|
Most Common: ${reportData.averages[0]?.label || 'N/A'}
|
|
Average Severity: ${(symptoms.reduce((s, x) => s + x.severity, 0) / symptoms.length || 0).toFixed(1)}/5
|
|
`.trim()
|
|
|
|
navigator.clipboard.writeText(summary)
|
|
showToast('Summary copied to clipboard', 'success')
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-2xl max-w-md w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
<div className="flex items-center gap-2">
|
|
<FileText className="w-5 h-5 text-primary-500" />
|
|
<h2 className="font-semibold text-secondary-900">Symptom Report</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-secondary-100 rounded-full"
|
|
>
|
|
<X className="w-5 h-5 text-secondary-500" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-auto p-4 space-y-4">
|
|
{/* Summary */}
|
|
<div className="bg-primary-50 rounded-lg p-4">
|
|
<h3 className="font-medium text-primary-900 mb-2">Summary</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-2xl font-bold text-primary-700">{symptoms.length}</p>
|
|
<p className="text-xs text-primary-600">Total Logged</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold text-primary-700">
|
|
{(symptoms.reduce((s, x) => s + x.severity, 0) / symptoms.length || 0).toFixed(1)}
|
|
</p>
|
|
<p className="text-xs text-primary-600">Avg Severity</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Breakdown */}
|
|
<div>
|
|
<h3 className="font-medium text-secondary-900 mb-2">By Symptom Type</h3>
|
|
<div className="space-y-2">
|
|
{reportData.averages.map(item => (
|
|
<div
|
|
key={item.type}
|
|
className="flex items-center justify-between p-3 bg-secondary-50 rounded-lg"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-secondary-900">{item.label}</p>
|
|
<p className="text-xs text-secondary-500">{item.count} occurrences</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-semibold text-secondary-900">{item.avgSeverity}/5</p>
|
|
<p className="text-xs text-secondary-500">avg</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<p className="text-xs text-secondary-500 text-center">
|
|
Share this report with your doctor at your next appointment
|
|
</p>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="p-4 border-t border-border space-y-2">
|
|
<Button
|
|
onClick={generatePDF}
|
|
loading={generating}
|
|
fullWidth
|
|
>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Download PDF
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={copySummary}
|
|
fullWidth
|
|
>
|
|
<Share2 className="w-4 h-4 mr-2" />
|
|
Copy Text Summary
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|