diff --git a/src/components/PatientPortal/HealthDataTab.jsx b/src/components/PatientPortal/HealthDataTab.jsx index b703a73..6e10412 100644 --- a/src/components/PatientPortal/HealthDataTab.jsx +++ b/src/components/PatientPortal/HealthDataTab.jsx @@ -41,6 +41,8 @@ const HealthDataTab = ({ patientId: propPatientId, isDoctorView = false }) => { const [aggregatedData, setAggregatedData] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); + // 'normal' | 'exercise' | 'heart-patient' + const [hrMode, setHrMode] = useState('normal'); // Load sessions from backend on mount and when returning to tab (e.g. after refresh) useEffect(() => { @@ -99,6 +101,28 @@ const HealthDataTab = ({ patientId: propPatientId, isDoctorView = false }) => { const healthData = currentSession?.healthData || null; const dataSource = currentSession?.source || ''; + // Previous session for trend comparison + const currentSessionIndex = currentSession + ? workoutSessions.findIndex(s => s.id === currentSession.id) + : -1; + const prevSession = currentSessionIndex >= 0 && currentSessionIndex < workoutSessions.length - 1 + ? workoutSessions[currentSessionIndex + 1] + : null; + const prevAggregatedData = prevSession ? aggregateHealthData(prevSession.healthData) : null; + + // Helper: compute % trend between current and previous value + const calcTrend = (current, previous) => { + if (!prevAggregatedData || !previous || previous === 0) { + return { trend: 'neutral', trendValue: 'No prev data' }; + } + const pct = Math.round(((current - previous) / previous) * 100); + if (pct === 0) return { trend: 'neutral', trendValue: 'No change' }; + return { + trend: pct > 0 ? 'up' : 'down', + trendValue: `${pct > 0 ? '+' : ''}${pct}%`, + }; + }; + const handleDataUploaded = async (jsonData, fileName, metadata) => { try { let parsedData; @@ -318,14 +342,94 @@ const HealthDataTab = ({ patientId: propPatientId, isDoctorView = false }) => {

Health Metrics

+ + {/* ─── Heart Rate Mode Selector ─── */} +
+ Mode: + {[ + { id: 'normal', label: '🧍 Normal', tip: 'Resting / daily activity' }, + { id: 'exercise', label: '🏃 Exercising', tip: 'During a workout session' }, + { id: 'heart-patient', label: '❤️‍🩹 Heart Patient', tip: 'Cardiac condition / post-surgery' }, + ].map(m => ( + + ))} +
+ + {/* ─── Critical Heart Rate Warning ─── */} + {(() => { + const avgHR = aggregatedData.today.avgHeartRate; + if (!avgHR) return null; + + // Thresholds per mode + const thresholds = { + 'normal': { warn: 100, critical: 120 }, + 'exercise': { warn: 150, critical: 170 }, + 'heart-patient': { warn: 110, critical: 120 }, + }; + const { warn, critical } = thresholds[hrMode]; + + if (avgHR <= warn) return null; + + const isCritical = avgHR > critical; + + const messages = { + normal: { + warn: 'Your resting heart rate is elevated. Rest and avoid exertion. Consult your doctor if this persists.', + critical: 'Critical resting heart rate! Stop any activity and seek medical attention immediately.', + }, + exercise: { + warn: `Heart rate is high for exercise (${avgHR} bpm). Slow down and catch your breath — you're approaching your limit.`, + critical: `Heart rate is dangerously high during exercise (${avgHR} bpm). Stop immediately, rest, and seek help if discomfort occurs.`, + }, + 'heart-patient': { + warn: `Heart rate exceeds your safe limit (${avgHR} bpm). Stop activity, rest, and take prescribed medication if needed.`, + critical: `Critical! Heart rate is ${avgHR} bpm — far above the safe range for cardiac patients. Seek immediate medical attention.`, + }, + }; + + const msg = isCritical ? messages[hrMode].critical : messages[hrMode].warn; + + return ( +
+
+ {isCritical ? '🚨' : '⚠️'} +
+
+

+ {isCritical ? 'Critical Heart Rate Detected!' : 'Elevated Heart Rate'} +

+

+ Avg HR: {avgHR} bpm — {msg} +

+

+ Safe range ({hrMode === 'normal' ? 'resting' : hrMode === 'exercise' ? 'exercise' : 'cardiac'}): + {' '}60–{warn} bpm +

+
+
+ ); + })()} +
{ value={aggregatedData.today.avgHeartRate} unit="bpm" icon={Heart} - trend="down" - trendValue="-2 bpm" + {...calcTrend(aggregatedData.today.avgHeartRate, prevAggregatedData?.today.avgHeartRate)} bgGradient="bg-gradient-to-br from-red-100 to-pink-200" iconBg="bg-red-500" chartData={generateChartData(healthData.heartRate)} - subtitle={`Resting: ${aggregatedData.today.avgHeartRate} bpm`} + subtitle={`Avg across session: ${aggregatedData.today.avgHeartRate} bpm`} /> { value={aggregatedData.today.avgSpo2 || 98} unit="%" icon={Droplets} - trend="up" - trendValue="+1%" + {...calcTrend(aggregatedData.today.avgSpo2, prevAggregatedData?.today.avgSpo2)} bgGradient="bg-gradient-to-br from-teal-100 to-cyan-200" iconBg="bg-teal-500" chartData={generateChartData(healthData.spo2)} @@ -363,8 +465,7 @@ const HealthDataTab = ({ patientId: propPatientId, isDoctorView = false }) => { value={aggregatedData.today.calories.toLocaleString()} unit="cal" icon={Flame} - trend="up" - trendValue="+8%" + {...calcTrend(aggregatedData.today.calories, prevAggregatedData?.today.calories)} bgGradient="bg-gradient-to-br from-orange-100 to-red-200" iconBg="bg-orange-500" chartData={generateChartData(healthData.calories)} @@ -376,8 +477,7 @@ const HealthDataTab = ({ patientId: propPatientId, isDoctorView = false }) => { value={aggregatedData.today.distance.toFixed(1)} unit="km" icon={TrendingUp} - trend="up" - trendValue="+5%" + {...calcTrend(aggregatedData.today.distance, prevAggregatedData?.today.distance)} bgGradient="bg-gradient-to-br from-purple-100 to-indigo-200" iconBg="bg-purple-500" chartData={generateChartData(healthData.distance)} @@ -389,7 +489,7 @@ const HealthDataTab = ({ patientId: propPatientId, isDoctorView = false }) => { value={aggregatedData.today.workouts} unit="sessions" icon={Activity} - trend="neutral" + {...calcTrend(aggregatedData.today.workouts, prevAggregatedData?.today.workouts)} bgGradient="bg-gradient-to-br from-green-100 to-emerald-200" iconBg="bg-green-500" chartData={healthData.workouts.slice(-7).map((workout, i) => ({ diff --git a/src/components/PatientPortal/HealthRiskCard.jsx b/src/components/PatientPortal/HealthRiskCard.jsx index 8082202..adad239 100644 --- a/src/components/PatientPortal/HealthRiskCard.jsx +++ b/src/components/PatientPortal/HealthRiskCard.jsx @@ -1,42 +1,189 @@ -import React from 'react'; -import { CheckCircle, AlertTriangle, Brain, TrendingUp } from 'lucide-react'; +import React, { useMemo } from 'react'; +import { CheckCircle, AlertTriangle, XCircle, Brain, TrendingUp, TrendingDown } from 'lucide-react'; -const HealthRiskCard = () => { - const healthPredictions = [ - { - id: 1, +/* ───────────────────────────────────────────────────────────── + Helper: parse the systolic value out of "120/80" or just "120" +───────────────────────────────────────────────────────────── */ +const parseSystolic = (bp) => { + if (!bp) return null; + const s = String(bp).split('/')[0]; + const n = parseFloat(s); + return isNaN(n) ? null : n; +}; + +/* ───────────────────────────────────────────────────────────── + computeRisks(vitals) + Returns array of { title, riskPct (0–100), status, ... } +───────────────────────────────────────────────────────────── */ +const computeRisks = (vitals) => { + if (!vitals) return null; + + const hr = parseFloat(vitals.heartRate); + const systolic = parseSystolic(vitals.bp); + const spo2 = parseFloat(vitals.spo2); + const temp = parseFloat(vitals.temp); + const sugar = parseFloat(vitals.sugarLevel); + + const risks = []; + + // ── Cardiac Event (based on HR + BP) ────────────────────── + if (!isNaN(hr) || systolic !== null) { + let riskPct = 5; + if (!isNaN(hr)) { + if (hr < 50 || hr > 120) riskPct += 35; + else if (hr < 60 || hr > 100) riskPct += 15; + } + if (systolic !== null) { + if (systolic > 160) riskPct += 35; + else if (systolic > 140) riskPct += 20; + else if (systolic > 130) riskPct += 10; + } + riskPct = Math.min(riskPct, 99); + risks.push({ + id: 'cardiac', title: 'Cardiac Event', - risk: '5%', - status: 'Normal', - icon: CheckCircle, - color: 'green', - bgColor: 'bg-green-50', - textColor: 'text-green-700', - iconColor: 'text-green-600' - }, - { - id: 2, + riskPct, + status: riskPct >= 40 ? 'High Risk' : riskPct >= 20 ? 'Monitor' : 'Normal', + }); + } + + // ── Arrhythmia (HR-based) ────────────────────────────────── + if (!isNaN(hr)) { + let riskPct = 5; + if (hr < 50 || hr > 120) riskPct = 55; + else if (hr < 60 || hr > 100) riskPct = 25; + riskPct = Math.min(riskPct, 99); + risks.push({ + id: 'arrhythmia', title: 'Arrhythmia', - risk: '8%', - status: 'Normal', - icon: CheckCircle, - color: 'green', - bgColor: 'bg-green-50', - textColor: 'text-green-700', - iconColor: 'text-green-600' - }, - { - id: 3, + riskPct, + status: riskPct >= 40 ? 'High Risk' : riskPct >= 20 ? 'Monitor' : 'Normal', + }); + } + + // ── Hypertension (BP-based) ──────────────────────────────── + if (systolic !== null) { + let riskPct = 5; + if (systolic > 160) riskPct = 75; + else if (systolic > 140) riskPct = 50; + else if (systolic > 130) riskPct = 30; + riskPct = Math.min(riskPct, 99); + risks.push({ + id: 'hypertension', title: 'Hypertension', - risk: '35%', - status: 'Monitor', - icon: AlertTriangle, - color: 'orange', - bgColor: 'bg-orange-50', - textColor: 'text-orange-700', - iconColor: 'text-orange-600' - } - ]; + riskPct, + status: riskPct >= 40 ? 'High Risk' : riskPct >= 20 ? 'Monitor' : 'Normal', + }); + } + + // ── Hypoxia (SpO2-based) ─────────────────────────────────── + if (!isNaN(spo2)) { + let riskPct = 3; + if (spo2 < 90) riskPct = 70; + else if (spo2 < 95) riskPct = 35; + riskPct = Math.min(riskPct, 99); + risks.push({ + id: 'hypoxia', + title: 'Hypoxia (Low SpO₂)', + riskPct, + status: riskPct >= 40 ? 'High Risk' : riskPct >= 20 ? 'Monitor' : 'Normal', + }); + } + + // ── Fever (temp-based) ──────────────────────────────────── + if (!isNaN(temp)) { + let riskPct = 5; + if (temp >= 39.5) riskPct = 60; + else if (temp >= 38.5) riskPct = 35; + else if (temp >= 37.5) riskPct = 18; + riskPct = Math.min(riskPct, 99); + risks.push({ + id: 'fever', + title: 'Fever / Infection', + riskPct, + status: riskPct >= 40 ? 'High Risk' : riskPct >= 20 ? 'Monitor' : 'Normal', + }); + } + + // ── Diabetes Risk (sugar-based) ──────────────────────────── + if (!isNaN(sugar)) { + let riskPct = 5; + if (sugar > 250 || sugar < 55) riskPct = 70; + else if (sugar > 200 || sugar < 70) riskPct = 45; + else if (sugar > 140) riskPct = 20; + riskPct = Math.min(riskPct, 99); + risks.push({ + id: 'diabetes', + title: 'Diabetes Risk', + riskPct, + status: riskPct >= 40 ? 'High Risk' : riskPct >= 20 ? 'Monitor' : 'Normal', + }); + } + + return risks; +}; + +/* ───────────────────────────────────────────────────────────── + computeHealthScore(risks) → 0-100 +───────────────────────────────────────────────────────────── */ +const computeHealthScore = (risks) => { + if (!risks || risks.length === 0) return null; + const avg = risks.reduce((sum, r) => sum + r.riskPct, 0) / risks.length; + return Math.round(100 - avg); +}; + +/* ───────────────────────────────────────────────────────────── + Styling helpers +───────────────────────────────────────────────────────────── */ +const statusStyle = (status) => { + switch (status) { + case 'High Risk': + return { + bgColor: 'bg-red-50', + textColor: 'text-red-700', + iconColor: 'text-red-600', + barColor: 'bg-red-500', + Icon: XCircle, + }; + case 'Monitor': + return { + bgColor: 'bg-orange-50', + textColor: 'text-orange-700', + iconColor: 'text-orange-600', + barColor: 'bg-orange-500', + Icon: AlertTriangle, + }; + default: + return { + bgColor: 'bg-green-50', + textColor: 'text-green-700', + iconColor: 'text-green-600', + barColor: 'bg-green-500', + Icon: CheckCircle, + }; + } +}; + +/* ───────────────────────────────────────────────────────────── + Component +───────────────────────────────────────────────────────────── */ +const HealthRiskCard = ({ medicalSummary }) => { + const vitals = medicalSummary?.latestVitals ?? null; + + const risks = useMemo(() => computeRisks(vitals), [vitals]); + const healthScore = useMemo(() => computeHealthScore(risks), [risks]); + + const scoreColor = + healthScore === null ? 'text-gray-400' + : healthScore >= 80 ? 'text-green-600' + : healthScore >= 60 ? 'text-orange-500' + : 'text-red-600'; + + const scoreBarColor = + healthScore === null ? 'bg-gray-300' + : healthScore >= 80 ? 'bg-green-500' + : healthScore >= 60 ? 'bg-orange-500' + : 'bg-red-500'; return (
@@ -50,73 +197,79 @@ const HealthRiskCard = () => {

AI Health Predictions ✨

-

ML-powered risk analysis

+

+ {vitals ? `Based on vitals recorded at ${new Date(vitals.recordedAt).toLocaleString()}` : 'ML-powered risk analysis'} +

- {/* Prediction Cards */} -
- {healthPredictions.map((prediction) => { - const Icon = prediction.icon; - return ( -
-
-
- -

{prediction.title}

-
+ {/* No data state */} + {!risks || risks.length === 0 ? ( +
+ +

No vitals data available

+

Add a VITALS medical event in Medical Records to see AI predictions.

+
+ ) : ( + <> + {/* Prediction Cards */} +
+ {risks.map((r) => { + const { bgColor, textColor, iconColor, barColor, Icon } = statusStyle(r.status); + return ( +
+
+
+ +

{r.title}

+
+
+
{r.riskPct}%
+
Risk
+
+
-
-
{prediction.risk}
-
Risk
+
+ Status: {r.status} +
+
+
+
-
+ ); + })} +
-
- - Status: {prediction.status} - - -
-
-
+ {/* Overall Health Score */} +
+
+

Overall Health Score

+
+ {healthScore >= 80 ? : } + {healthScore}/100
- ); - })} -
- - {/* Overall Health Score */} -
-
-

Overall Health Score

-
- - 92/100 +
+
+
+

+ {healthScore >= 80 ? 'Your vitals look great! Keep it up.' : healthScore >= 60 ? 'Some metrics need attention.' : 'Please consult your doctor.'} +

-
- -
-
-
-
+ + )} {/* AI Analysis Info */}

- AI analysis based on 24h vitals monitoring, historical trends, and clinical data patterns + AI analysis based on your latest vitals. Add regular VITALS events in Medical Records for more accurate predictions.

diff --git a/src/components/PatientPortal/HealthTipsCard.jsx b/src/components/PatientPortal/HealthTipsCard.jsx index 6a2ab05..da52bd3 100644 --- a/src/components/PatientPortal/HealthTipsCard.jsx +++ b/src/components/PatientPortal/HealthTipsCard.jsx @@ -1,33 +1,154 @@ -import React from "react"; -import { Heart, Droplets, Zap, Lightbulb, Info } from "lucide-react"; - -const HealthTipsCard = () => { - const tips = [ - { - id: 1, - title: "Heart Rate", - description: "Your heart rate is within normal range. Continue regular activity.", - icon: Heart, - iconBg: "bg-blue-100", - iconColor: "text-blue-600", - }, - { - id: 2, - title: "Hydration", - description: "Stay hydrated. Aim for 8 glasses of water daily.", - icon: Droplets, - iconBg: "bg-teal-100", - iconColor: "text-teal-600", - }, - { - id: 3, - title: "Activity", - description: "Light exercise recommended. 20-30 minutes daily walk is ideal.", - icon: Zap, - iconBg: "bg-green-100", - iconColor: "text-green-600", - }, - ]; +import React, { useMemo } from "react"; +import { Heart, Droplets, Zap, Lightbulb, Info, Thermometer, Activity } from "lucide-react"; + +/* ───────────────────────────────────────────────────────────── + Helper: parse systolic from "120/80" or "120" +───────────────────────────────────────────────────────────── */ +const parseSystolic = (bp) => { + if (!bp) return null; + const n = parseFloat(String(bp).split('/')[0]); + return isNaN(n) ? null : n; +}; + +/* ───────────────────────────────────────────────────────────── + generateTips(vitals) → array of tip objects +───────────────────────────────────────────────────────────── */ +const generateTips = (vitals) => { + if (!vitals) { + return [ + { id: 'default-hr', title: 'Heart Rate', description: 'Add a VITALS event to see personalized heart rate tips.', icon: Heart, iconBg: 'bg-blue-100', iconColor: 'text-blue-600' }, + { id: 'default-hydration', title: 'Hydration', description: 'Stay hydrated. Aim for 8 glasses of water daily.', icon: Droplets, iconBg: 'bg-teal-100', iconColor: 'text-teal-600' }, + { id: 'default-activity', title: 'Activity', description: 'Light exercise recommended. A 20–30 minute daily walk is ideal.', icon: Zap, iconBg: 'bg-green-100', iconColor: 'text-green-600' }, + ]; + } + + const tips = []; + const hr = parseFloat(vitals.heartRate); + const systolic = parseSystolic(vitals.bp); + const spo2 = parseFloat(vitals.spo2); + const temp = parseFloat(vitals.temp); + const sugar = parseFloat(vitals.sugarLevel); + + // Heart Rate tip + if (!isNaN(hr)) { + if (hr > 100) { + tips.push({ id: 'hr', title: 'Heart Rate', description: `Your heart rate is elevated (${hr} bpm). Rest, avoid caffeine and stress. If persistent, consult your doctor.`, icon: Heart, iconBg: 'bg-red-100', iconColor: 'text-red-600' }); + } else if (hr < 60) { + tips.push({ id: 'hr', title: 'Heart Rate', description: `Your heart rate is low (${hr} bpm). Monitor closely and consult your doctor if you feel dizzy or fatigued.`, icon: Heart, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' }); + } else { + tips.push({ id: 'hr', title: 'Heart Rate', description: `Your heart rate is in the normal range (${hr} bpm). Great job — keep up your routine!`, icon: Heart, iconBg: 'bg-blue-100', iconColor: 'text-blue-600' }); + } + } + + // Blood Pressure tip + if (systolic !== null) { + if (systolic > 140) { + tips.push({ id: 'bp', title: 'Blood Pressure', description: `Your BP is high (${vitals.bp} mmHg). Reduce salt intake, avoid stress, and follow up with your doctor.`, icon: Activity, iconBg: 'bg-red-100', iconColor: 'text-red-600' }); + } else if (systolic > 130) { + tips.push({ id: 'bp', title: 'Blood Pressure', description: `Your BP is slightly elevated (${vitals.bp}). Monitor it daily and reduce sodium intake.`, icon: Activity, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' }); + } else { + tips.push({ id: 'bp', title: 'Blood Pressure', description: `Your blood pressure looks good (${vitals.bp} mmHg). Keep monitoring regularly.`, icon: Activity, iconBg: 'bg-green-100', iconColor: 'text-green-600' }); + } + } + + // SpO2 tip + if (!isNaN(spo2)) { + if (spo2 < 90) { + tips.push({ id: 'spo2', title: 'Oxygen Level', description: `SpO₂ is critically low (${spo2}%). Seek immediate medical attention.`, icon: Droplets, iconBg: 'bg-red-100', iconColor: 'text-red-600' }); + } else if (spo2 < 95) { + tips.push({ id: 'spo2', title: 'Oxygen Level', description: `SpO₂ is slightly low (${spo2}%). Practice slow deep breathing exercises and avoid strenuous activity.`, icon: Droplets, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' }); + } else { + tips.push({ id: 'spo2', title: 'Oxygen Level', description: `Your oxygen saturation is excellent (${spo2}%). Keep breathing easy!`, icon: Droplets, iconBg: 'bg-teal-100', iconColor: 'text-teal-600' }); + } + } + + // Temperature tip + if (!isNaN(temp)) { + if (temp >= 38.5) { + tips.push({ id: 'temp', title: 'Temperature', description: `You have a fever (${temp}°C). Rest, stay hydrated, and take prescribed medication. See a doctor if it persists.`, icon: Thermometer, iconBg: 'bg-red-100', iconColor: 'text-red-600' }); + } else if (temp >= 37.5) { + tips.push({ id: 'temp', title: 'Temperature', description: `Slightly elevated temperature (${temp}°C). Stay hydrated and rest. Monitor for further increases.`, icon: Thermometer, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' }); + } else { + tips.push({ id: 'temp', title: 'Temperature', description: `Your temperature is normal (${temp}°C). Keep staying comfortable and well-rested.`, icon: Thermometer, iconBg: 'bg-green-100', iconColor: 'text-green-600' }); + } + } + + // Sugar tip + if (!isNaN(sugar)) { + if (sugar > 200 || sugar < 70) { + tips.push({ id: 'sugar', title: 'Blood Sugar', description: `Blood sugar out of range (${sugar} mg/dL). Eat appropriately and consult your doctor immediately.`, icon: Zap, iconBg: 'bg-red-100', iconColor: 'text-red-600' }); + } else if (sugar > 140) { + tips.push({ id: 'sugar', title: 'Blood Sugar', description: `Blood sugar slightly elevated (${sugar} mg/dL). Avoid sugary foods, follow your diet plan, and take your medication as prescribed.`, icon: Zap, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' }); + } else { + tips.push({ id: 'sugar', title: 'Blood Sugar', description: `Blood sugar is in a healthy range (${sugar} mg/dL). Excellent — maintain your dietary habits!`, icon: Zap, iconBg: 'bg-green-100', iconColor: 'text-green-600' }); + } + } + + // Add a general hydration tip if we have less than 2 tips + if (tips.length < 2) { + tips.push({ id: 'hydration', title: 'Hydration', description: 'Stay hydrated. Aim for at least 8 glasses of water daily.', icon: Droplets, iconBg: 'bg-teal-100', iconColor: 'text-teal-600' }); + } + + return tips; +}; + +/* ───────────────────────────────────────────────────────────── + computeHealthScore — same formula as HealthRiskCard +───────────────────────────────────────────────────────────── */ +const computeHealthScore = (vitals) => { + if (!vitals) return null; + const hr = parseFloat(vitals.heartRate); + const systolic = parseSystolic(vitals.bp); + const spo2 = parseFloat(vitals.spo2); + const temp = parseFloat(vitals.temp); + const sugar = parseFloat(vitals.sugarLevel); + + const scores = []; + if (!isNaN(hr)) scores.push(hr >= 60 && hr <= 100 ? 100 : hr >= 50 && hr <= 120 ? 65 : 30); + if (systolic !== null) scores.push(systolic <= 130 ? 100 : systolic <= 140 ? 70 : systolic <= 160 ? 45 : 20); + if (!isNaN(spo2)) scores.push(spo2 >= 95 ? 100 : spo2 >= 90 ? 60 : 20); + if (!isNaN(temp)) scores.push(temp < 37.5 ? 100 : temp < 38.5 ? 70 : 30); + if (!isNaN(sugar)) scores.push(sugar >= 70 && sugar <= 140 ? 100 : sugar <= 200 && sugar >= 55 ? 65 : 25); + + if (scores.length === 0) return null; + return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length); +}; + +/* ───────────────────────────────────────────────────────────── + Medication reminder string +───────────────────────────────────────────────────────────── */ +const buildReminder = (medications) => { + if (!medications || medications.length === 0) return null; + const med = medications[0]; // most recent + const name = med.name || 'your medication'; + const dose = med.dose ? ` ${med.dose}` : ''; + const time = med.timeOfDay ? ` at ${med.timeOfDay}` : ''; + const freq = med.frequency ? ` (${med.frequency})` : ''; + return `Remember to take ${name}${dose}${time}${freq}.`; +}; + +/* ───────────────────────────────────────────────────────────── + Component +───────────────────────────────────────────────────────────── */ +const HealthTipsCard = ({ medicalSummary }) => { + const vitals = medicalSummary?.latestVitals ?? null; + const medications = medicalSummary?.medications ?? []; + + const tips = useMemo(() => generateTips(vitals), [vitals]); + const healthScore = useMemo(() => computeHealthScore(vitals), [vitals]); + const reminder = useMemo(() => buildReminder(medications), [medications]); + + const scoreLabel = + healthScore === null ? { text: 'No Data', bg: 'bg-gray-200', color: 'text-gray-600' } + : healthScore >= 80 ? { text: 'Excellent', bg: 'bg-green-200', color: 'text-green-800' } + : healthScore >= 60 ? { text: 'Fair', bg: 'bg-orange-200', color: 'text-orange-800' } + : { text: 'Poor', bg: 'bg-red-200', color: 'text-red-800' }; + + const scoreBarColor = + healthScore === null ? 'bg-gray-300' + : healthScore >= 80 ? 'bg-green-500' + : healthScore >= 60 ? 'bg-orange-500' + : 'bg-red-500'; return (
@@ -39,7 +160,9 @@ const HealthTipsCard = () => {

Health Tips

-

Personalized recommendations

+

+ {vitals ? 'Personalised from your latest vitals' : 'General recommendations'} +

@@ -55,10 +178,9 @@ const HealthTipsCard = () => {
-
-

{tip.title}

-

{tip.description}

+

{tip.title}

+

{tip.description}

); @@ -66,7 +188,7 @@ const HealthTipsCard = () => {
{/* Divider */} -
+
{/* Reminders Section */}
@@ -75,31 +197,41 @@ const HealthTipsCard = () => {

Reminders

-
- Remember to take your medication at 2:00 PM -
+ {reminder ? ( +
+ {reminder} +
+ ) : ( +
+ No medication reminders — add a MEDICATION event in Medical Records. +
+ )}
{/* Daily Health Score */}
-

- Daily Health Score -

+

Daily Health Score

- 8.5 /10 + {healthScore !== null ? healthScore : '--'} + /100

- - - Excellent + + {scoreLabel.text}
- {/* Progress Bar */}
-
+
+ + {healthScore === null && ( +

Add vitals in Medical Records to calculate your score.

+ )}
); diff --git a/src/pages/PatientPortal/PatientPortal.jsx b/src/pages/PatientPortal/PatientPortal.jsx index 100a59c..8466a6a 100644 --- a/src/pages/PatientPortal/PatientPortal.jsx +++ b/src/pages/PatientPortal/PatientPortal.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import Header from "../../components/PatientPortal/Header"; import PatientInfoCard from "../../components/PatientPortal/PatientInfoCard"; @@ -30,10 +30,25 @@ const PatientPortal = () => { const [currentTab, setCurrentTab] = useState("Overview"); const [showManualEntry, setShowManualEntry] = useState(false); - // NEW: used to trigger vitals refresh after adding a new medical event + // used to trigger vitals refresh after adding a new medical event const [vitalsRefreshKey, setVitalsRefreshKey] = useState(0); + // medical-summary shared by VitalsDashboard, HealthRiskCard, HealthTipsCard + const [medicalSummary, setMedicalSummary] = useState(null); + const patientId = localStorage.getItem("patientId"); + const token = useMemo(() => localStorage.getItem("token"), []); + + useEffect(() => { + if (!patientId || !token) return; + const API_BASE = import.meta.env.VITE_API_URL; + fetch(`${API_BASE}/api/patients/${patientId}/medical-summary`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => (res.ok ? res.json() : null)) + .then((data) => { if (data) setMedicalSummary(data); }) + .catch(() => {}); + }, [patientId, token, vitalsRefreshKey]); return ( <> @@ -83,13 +98,13 @@ const PatientPortal = () => {
- {/* ECG + Health Tips */} + {/* AI Predictions + Health Tips */}
- +
- +