Compare commits
7 Commits
47e6e69eb7
...
dynamic
| Author | SHA1 | Date | |
|---|---|---|---|
| 35ea522283 | |||
| fc62b64624 | |||
| e66b9e6c29 | |||
| f0e90aa772 | |||
| 4406b2013d | |||
| 9d61ebb533 | |||
| 100b47e947 |
Binary file not shown.
Binary file not shown.
+137
-56
@@ -9,6 +9,7 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
|
||||
@@ -109,7 +110,8 @@ async def upload_files(
|
||||
gender: str = Form(...),
|
||||
fat_percentage: float = Form(...),
|
||||
focus: str = Form(default="Endurance"),
|
||||
session_id: str = Form(default="default"),
|
||||
next_testing_date: str = Form(...),
|
||||
report_type: str = Form(default="full"),
|
||||
spirometry_pdf: UploadFile = File(...),
|
||||
pnoe_csv: UploadFile = File(...),
|
||||
oxygenation_csv: UploadFile = File(None),
|
||||
@@ -176,6 +178,11 @@ async def upload_files(
|
||||
|
||||
# Prepare patient information
|
||||
patient_name = f"{first_name} {last_name}"
|
||||
print(f"DEBUG: Received next_testing_date: '{next_testing_date}'")
|
||||
|
||||
# Generate session_id internally using timestamp for unique identification
|
||||
session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
patient_info = {
|
||||
"patient_name": patient_name,
|
||||
"first_name": first_name,
|
||||
@@ -187,6 +194,7 @@ async def upload_files(
|
||||
"fat_percentage": fat_percentage,
|
||||
"focus": focus,
|
||||
"session_id": session_id,
|
||||
"next_testing_date": next_testing_date,
|
||||
}
|
||||
|
||||
# Generate report
|
||||
@@ -196,12 +204,14 @@ async def upload_files(
|
||||
pnoe_csv_path=str(pnoe_path),
|
||||
patient_info=patient_info,
|
||||
oxygenation_csv_path=oxygenation_csv_path,
|
||||
report_type=report_type,
|
||||
)
|
||||
|
||||
# Store in session
|
||||
request.session["patient_info"] = patient_info
|
||||
request.session["temp_dir"] = str(session_temp_dir)
|
||||
request.session["report_path"] = result["report_path"]
|
||||
request.session["report_type"] = report_type
|
||||
request.session["graphs_generated"] = result["graphs_generated"]
|
||||
request.session["analysis_data"] = result["analysis_data"]
|
||||
|
||||
@@ -284,8 +294,18 @@ async def upload_files(
|
||||
@app.get("/preview", response_class=HTMLResponse)
|
||||
async def preview(request: Request):
|
||||
"""Preview generated report"""
|
||||
# Check for required session data
|
||||
if not request.session.get("report_path"):
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
# Ensure metrics exist in session, initialize if missing
|
||||
if "metrics" not in request.session:
|
||||
request.session["metrics"] = {"pnoe": {}, "spirometry": {}}
|
||||
|
||||
# Ensure patient_info exists
|
||||
if "patient_info" not in request.session:
|
||||
request.session["patient_info"] = {}
|
||||
|
||||
return render_template(
|
||||
"preview.html", {"request": request, "session": request.session}
|
||||
)
|
||||
@@ -303,8 +323,16 @@ async def serve_graph(filename: str):
|
||||
@app.get("/edit", response_class=HTMLResponse)
|
||||
async def edit_form(request: Request):
|
||||
"""Display edit metrics form"""
|
||||
if not request.session.get("metrics"):
|
||||
# Check for required session data
|
||||
if not request.session.get("report_path") or not request.session.get(
|
||||
"patient_info"
|
||||
):
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
# Ensure metrics exist in session, initialize if missing
|
||||
if "metrics" not in request.session:
|
||||
request.session["metrics"] = {"pnoe": {}, "spirometry": {}}
|
||||
|
||||
return render_template(
|
||||
"edit.html", {"request": request, "session": request.session}
|
||||
)
|
||||
@@ -319,69 +347,117 @@ async def edit_metrics(request: Request):
|
||||
# Get form data
|
||||
form_data = await request.form()
|
||||
|
||||
# Helper function to safely convert form values to float
|
||||
def safe_float(value):
|
||||
"""Convert form value to float, return None if empty or invalid"""
|
||||
if not value or value.strip() == "":
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
# Build metric overrides
|
||||
metric_overrides = {"pnoe": {}, "spirometry": {}}
|
||||
|
||||
# Pnoe overrides
|
||||
if form_data.get("vo2_max"):
|
||||
metric_overrides["pnoe"]["vo2_max"] = float(form_data["vo2_max"])
|
||||
if form_data.get("vo2_max_per_kg"):
|
||||
metric_overrides["pnoe"]["vo2_max_per_kg"] = float(form_data["vo2_max_per_kg"])
|
||||
if form_data.get("peak_vt"):
|
||||
metric_overrides["pnoe"]["peak_vt"] = float(form_data["peak_vt"])
|
||||
if form_data.get("peak_vt_hr"):
|
||||
metric_overrides["pnoe"]["peak_vt_hr"] = float(form_data["peak_vt_hr"])
|
||||
if form_data.get("fat_max_value"):
|
||||
metric_overrides["pnoe"]["fat_max_value"] = float(form_data["fat_max_value"])
|
||||
if form_data.get("fat_max_hr"):
|
||||
metric_overrides["pnoe"]["fat_max_hr"] = float(form_data["fat_max_hr"])
|
||||
# Pnoe overrides - only add if value is provided and valid
|
||||
vo2_max_val = safe_float(form_data.get("vo2_max"))
|
||||
if vo2_max_val is not None:
|
||||
metric_overrides["pnoe"]["vo2_max"] = vo2_max_val
|
||||
|
||||
# VT1 and VT2 overrides
|
||||
if (
|
||||
form_data.get("vt1_hr")
|
||||
or form_data.get("vt1_speed")
|
||||
or form_data.get("vt1_time")
|
||||
):
|
||||
metric_overrides["pnoe"]["vt1"] = {
|
||||
"HeartRate": float(form_data.get("vt1_hr", 0)),
|
||||
"Speed": float(form_data.get("vt1_speed", 0)),
|
||||
"Time": float(form_data.get("vt1_time", 0)),
|
||||
vo2_max_per_kg_val = safe_float(form_data.get("vo2_max_per_kg"))
|
||||
if vo2_max_per_kg_val is not None:
|
||||
metric_overrides["pnoe"]["vo2_max_per_kg"] = vo2_max_per_kg_val
|
||||
|
||||
peak_vt_val = safe_float(form_data.get("peak_vt"))
|
||||
if peak_vt_val is not None:
|
||||
metric_overrides["pnoe"]["peak_vt"] = peak_vt_val
|
||||
|
||||
peak_vt_hr_val = safe_float(form_data.get("peak_vt_hr"))
|
||||
if peak_vt_hr_val is not None:
|
||||
metric_overrides["pnoe"]["peak_vt_hr"] = peak_vt_hr_val
|
||||
|
||||
fat_max_value_val = safe_float(form_data.get("fat_max_value"))
|
||||
if fat_max_value_val is not None:
|
||||
metric_overrides["pnoe"]["fat_max_value"] = fat_max_value_val
|
||||
|
||||
fat_max_hr_val = safe_float(form_data.get("fat_max_hr"))
|
||||
if fat_max_hr_val is not None:
|
||||
metric_overrides["pnoe"]["fat_max_hr"] = fat_max_hr_val
|
||||
|
||||
# VT1 and VT2 overrides - use existing values if not provided
|
||||
existing_metrics = request.session.get("metrics", {})
|
||||
existing_pnoe = existing_metrics.get("pnoe", {})
|
||||
existing_vt1 = existing_pnoe.get("vt1", {})
|
||||
existing_vt2 = existing_pnoe.get("vt2", {})
|
||||
|
||||
vt1_hr_val = safe_float(form_data.get("vt1_hr"))
|
||||
vt1_speed_val = safe_float(form_data.get("vt1_speed"))
|
||||
vt1_time_val = safe_float(form_data.get("vt1_time"))
|
||||
|
||||
if vt1_hr_val is not None or vt1_speed_val is not None or vt1_time_val is not None:
|
||||
vt1_dict = {
|
||||
"HeartRate": vt1_hr_val
|
||||
if vt1_hr_val is not None
|
||||
else existing_vt1.get("HeartRate", 0),
|
||||
"Speed": vt1_speed_val
|
||||
if vt1_speed_val is not None
|
||||
else existing_vt1.get("Speed", 0),
|
||||
"Time": vt1_time_val
|
||||
if vt1_time_val is not None
|
||||
else existing_vt1.get("Time", 0),
|
||||
}
|
||||
metric_overrides["pnoe"]["vt1"] = vt1_dict
|
||||
|
||||
if (
|
||||
form_data.get("vt2_hr")
|
||||
or form_data.get("vt2_speed")
|
||||
or form_data.get("vt2_time")
|
||||
):
|
||||
metric_overrides["pnoe"]["vt2"] = {
|
||||
"HeartRate": float(form_data.get("vt2_hr", 0)),
|
||||
"Speed": float(form_data.get("vt2_speed", 0)),
|
||||
"Time": float(form_data.get("vt2_time", 0)),
|
||||
vt2_hr_val = safe_float(form_data.get("vt2_hr"))
|
||||
vt2_speed_val = safe_float(form_data.get("vt2_speed"))
|
||||
vt2_time_val = safe_float(form_data.get("vt2_time"))
|
||||
|
||||
if vt2_hr_val is not None or vt2_speed_val is not None or vt2_time_val is not None:
|
||||
vt2_dict = {
|
||||
"HeartRate": vt2_hr_val
|
||||
if vt2_hr_val is not None
|
||||
else existing_vt2.get("HeartRate", 0),
|
||||
"Speed": vt2_speed_val
|
||||
if vt2_speed_val is not None
|
||||
else existing_vt2.get("Speed", 0),
|
||||
"Time": vt2_time_val
|
||||
if vt2_time_val is not None
|
||||
else existing_vt2.get("Time", 0),
|
||||
}
|
||||
metric_overrides["pnoe"]["vt2"] = vt2_dict
|
||||
|
||||
# Heart rate zones
|
||||
# Heart rate zones - only add if value is provided
|
||||
for i in range(1, 6):
|
||||
zone_key = f"zone{i}_bpm"
|
||||
if form_data.get(zone_key):
|
||||
metric_overrides["pnoe"][zone_key] = form_data[zone_key]
|
||||
zone_val = form_data.get(zone_key)
|
||||
if zone_val and zone_val.strip():
|
||||
metric_overrides["pnoe"][zone_key] = zone_val.strip()
|
||||
|
||||
# Spirometry overrides
|
||||
if form_data.get("fvc_best"):
|
||||
metric_overrides["spirometry"]["fvc_best"] = float(form_data["fvc_best"])
|
||||
if form_data.get("fvc_pred"):
|
||||
metric_overrides["spirometry"]["fvc_pred"] = float(form_data["fvc_pred"])
|
||||
if form_data.get("fev1_best"):
|
||||
metric_overrides["spirometry"]["fev1_best"] = float(form_data["fev1_best"])
|
||||
if form_data.get("fev1_pred"):
|
||||
metric_overrides["spirometry"]["fev1_pred"] = float(form_data["fev1_pred"])
|
||||
if form_data.get("fev1_fvc_pct_best"):
|
||||
metric_overrides["spirometry"]["fev1_fvc_pct_best"] = float(
|
||||
form_data["fev1_fvc_pct_best"]
|
||||
)
|
||||
if form_data.get("fev1_fvc_pct_pred"):
|
||||
metric_overrides["spirometry"]["fev1_fvc_pct_pred"] = float(
|
||||
form_data["fev1_fvc_pct_pred"]
|
||||
)
|
||||
# Spirometry overrides - only add if value is provided and valid
|
||||
fvc_best_val = safe_float(form_data.get("fvc_best"))
|
||||
if fvc_best_val is not None:
|
||||
metric_overrides["spirometry"]["fvc_best"] = fvc_best_val
|
||||
|
||||
fvc_pred_val = safe_float(form_data.get("fvc_pred"))
|
||||
if fvc_pred_val is not None:
|
||||
metric_overrides["spirometry"]["fvc_pred"] = fvc_pred_val
|
||||
|
||||
fev1_best_val = safe_float(form_data.get("fev1_best"))
|
||||
if fev1_best_val is not None:
|
||||
metric_overrides["spirometry"]["fev1_best"] = fev1_best_val
|
||||
|
||||
fev1_pred_val = safe_float(form_data.get("fev1_pred"))
|
||||
if fev1_pred_val is not None:
|
||||
metric_overrides["spirometry"]["fev1_pred"] = fev1_pred_val
|
||||
|
||||
fev1_fvc_pct_best_val = safe_float(form_data.get("fev1_fvc_pct_best"))
|
||||
if fev1_fvc_pct_best_val is not None:
|
||||
metric_overrides["spirometry"]["fev1_fvc_pct_best"] = fev1_fvc_pct_best_val
|
||||
|
||||
fev1_fvc_pct_pred_val = safe_float(form_data.get("fev1_fvc_pct_pred"))
|
||||
if fev1_fvc_pct_pred_val is not None:
|
||||
metric_overrides["spirometry"]["fev1_fvc_pct_pred"] = fev1_fvc_pct_pred_val
|
||||
|
||||
try:
|
||||
# Get file paths from session
|
||||
@@ -405,6 +481,8 @@ async def edit_metrics(request: Request):
|
||||
raise ValueError("Could not find all required uploaded files")
|
||||
|
||||
# Regenerate report with overrides
|
||||
# Get report_type from session or default to "full"
|
||||
report_type = request.session.get("report_type", "full")
|
||||
oxygenation_csv_path = str(oxygenation_path) if oxygenation_path else None
|
||||
result = await report_service.generate_report(
|
||||
spirometry_pdf_path=str(spirometry_path),
|
||||
@@ -414,6 +492,7 @@ async def edit_metrics(request: Request):
|
||||
if (metric_overrides["pnoe"] or metric_overrides["spirometry"])
|
||||
else None,
|
||||
oxygenation_csv_path=oxygenation_csv_path,
|
||||
report_type=report_type,
|
||||
)
|
||||
|
||||
# Update session with new report
|
||||
@@ -459,6 +538,7 @@ async def edit_metrics(request: Request):
|
||||
"fat_percentage": patient_info.get("fat_percentage", 0),
|
||||
"gender": patient_info.get("gender", "female"),
|
||||
}
|
||||
# Calculate fat_mass and lean_mass (extract_patient_info does this when no SECA file)
|
||||
context_gen.extract_patient_info(patient_info.get("last_name", ""))
|
||||
|
||||
spirometry_overrides = metric_overrides.get("spirometry", {})
|
||||
@@ -505,7 +585,6 @@ async def generate_report(
|
||||
height: str = Form(..., description="Patient height (e.g., 5'4\")"),
|
||||
weight: str = Form(..., description="Patient weight (e.g., 123lbs)"),
|
||||
focus: str = Form(default="Endurance", description="Training focus"),
|
||||
session_id: str = Form(default="default", description="Session ID"),
|
||||
spirometry_pdf: UploadFile = File(..., description="Spirometry PDF file"),
|
||||
pnoe_csv: UploadFile = File(..., description="Pnoe CSV file"),
|
||||
seca_excel: UploadFile = File(..., description="SECA Excel file"),
|
||||
@@ -525,7 +604,6 @@ async def generate_report(
|
||||
height: Patient height
|
||||
weight: Patient weight
|
||||
focus: Training focus (default: Endurance)
|
||||
session_id: Session identifier (default: default)
|
||||
|
||||
Returns:
|
||||
ReportResponse with report path, graphs generated, and analysis data
|
||||
@@ -562,6 +640,9 @@ async def generate_report(
|
||||
with open(seca_path, "wb") as f:
|
||||
shutil.copyfileobj(seca_excel.file, f)
|
||||
|
||||
# Generate session_id internally using timestamp for unique identification
|
||||
session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# Prepare patient information
|
||||
patient_info = {
|
||||
"patient_name": patient_name,
|
||||
|
||||
+52
-57
@@ -26,56 +26,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Summary Grid -->
|
||||
<div class="grid grid-cols-2 gap-6 mb-6">
|
||||
<!-- Metrics Summary Grid: Left and Right as two side-by-side panels, fields as two columns inside each -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<!-- Left Leg Metrics -->
|
||||
<div class="bg-blue-50 p-4 rounded-lg border-2 border-blue-200">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4 text-center">
|
||||
<div class="bg-blue-50 p-3 rounded-lg border-l-4 border-blue-300 h-full flex flex-col">
|
||||
<h3 class="text-base font-bold text-gray-900 mb-3 text-center">
|
||||
Left Leg Analysis
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<!-- Baseline SmO2 -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Baseline SmO₂
|
||||
</div>
|
||||
<div class="text-lg font-bold text-gray-900">
|
||||
<div class="text-base font-bold text-gray-900">
|
||||
{{ left_baseline_smo2 | default('75.4%') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<!-- Minimum SmO2 -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Minimum SmO₂
|
||||
</div>
|
||||
<div class="text-lg font-bold text-gray-900">
|
||||
<div class="text-base font-bold text-gray-900">
|
||||
{{ left_minimum_smo2 | default('69.3%') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ left_minimum_lap | default('Lap 6') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<!-- Oxygen Drop -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Oxygen Drop
|
||||
</div>
|
||||
<div class="text-lg font-bold text-gray-900">
|
||||
<div class="text-base font-bold text-gray-900">
|
||||
{{ left_oxygen_drop | default('6.0%') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ left_drop_percentage | default('8% decrease') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<!-- Recovery -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Recovery
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mb-1">
|
||||
"Optimal >100%"
|
||||
</div>
|
||||
<div class="text-lg font-bold text-green-600">
|
||||
<div class="text-xs text-gray-500">Optimal >100%</div>
|
||||
<div class="text-base font-bold text-green-600">
|
||||
{{ left_recovery_percentage | default('109%') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,53 +81,51 @@
|
||||
</div>
|
||||
|
||||
<!-- Right Leg Metrics -->
|
||||
<div class="bg-purple-50 p-4 rounded-lg border-2 border-purple-200">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4 text-center">
|
||||
<div class="bg-purple-50 p-3 rounded-lg border-l-4 border-purple-300 h-full flex flex-col">
|
||||
<h3 class="text-base font-bold text-gray-900 mb-3 text-center">
|
||||
Right Leg Analysis
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<!-- Baseline SmO2 -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Baseline SmO₂
|
||||
</div>
|
||||
<div class="text-lg font-bold text-gray-900">
|
||||
<div class="text-base font-bold text-gray-900">
|
||||
{{ right_baseline_smo2 | default('82.9%') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<!-- Minimum SmO2 -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Minimum SmO₂
|
||||
</div>
|
||||
<div class="text-lg font-bold text-gray-900">
|
||||
<div class="text-base font-bold text-gray-900">
|
||||
{{ right_minimum_smo2 | default('73.7%') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ right_minimum_lap | default('Lap 6') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<!-- Oxygen Drop -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Oxygen Drop
|
||||
</div>
|
||||
<div class="text-lg font-bold text-gray-900">
|
||||
<div class="text-base font-bold text-gray-900">
|
||||
{{ right_oxygen_drop | default('9.3%') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ right_drop_percentage | default('11% decrease') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-3 rounded shadow-sm">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-1">
|
||||
<!-- Recovery -->
|
||||
<div class="bg-white p-2 rounded shadow-sm col-span-2 md:col-span-1">
|
||||
<div class="text-xs font-semibold text-gray-700">
|
||||
Recovery
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mb-1">
|
||||
"Optimal >100%"
|
||||
</div>
|
||||
<div class="text-lg font-bold text-blue-600">
|
||||
<div class="text-xs text-gray-500">Optimal >100%</div>
|
||||
<div class="text-base font-bold text-blue-600">
|
||||
{{ right_recovery_percentage | default('97%') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,19 +134,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Key Findings Summary -->
|
||||
<div class="bg-gray-100 p-4 rounded-lg">
|
||||
<h3 class="text-base font-bold text-gray-900 mb-3">Key Findings</h3>
|
||||
<div class="text-sm text-gray-700 space-y-2">
|
||||
<div class="bg-gray-100 p-3 rounded-lg">
|
||||
<h3 class="text-sm font-bold text-gray-900 mb-2">Key Findings</h3>
|
||||
<div class="text-xs text-gray-700 space-y-1">
|
||||
<p>
|
||||
• <strong>Left leg</strong> showed better oxygen maintenance
|
||||
during high-intensity work
|
||||
</p>
|
||||
<p>
|
||||
•
|
||||
<strong
|
||||
>{{ recovery_assessment | default('Excellent recovery
|
||||
capacity') }}</strong
|
||||
>
|
||||
<strong>
|
||||
{{ recovery_assessment | default('Excellent recovery capacity') }}
|
||||
</strong>
|
||||
- both legs recovered well
|
||||
</p>
|
||||
<p>
|
||||
|
||||
+521
-208
@@ -1,216 +1,529 @@
|
||||
<div class="w-full page bg-white">
|
||||
<!-- Header Section -->
|
||||
<!-- Main Content -->
|
||||
<div class="px-8 py-6">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-3xl font-bold text-black mb-8 text-center">Training Recommendations</h1>
|
||||
<!-- Header Section -->
|
||||
<!-- Main Content -->
|
||||
<div class="px-8 py-6">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-3xl font-bold text-black mb-8 text-center">
|
||||
Training Recommendations
|
||||
</h1>
|
||||
|
||||
<!-- Training Recommendations Section -->
|
||||
<div class="grid grid-cols-2 gap-8 mb-8">
|
||||
<!-- Left Side: Zone Recommendations -->
|
||||
<div class="bg-gray-200 p-6 rounded-lg">
|
||||
<!-- Zone 2 Recommendations -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-bold text-black mb-3">Zone 2 {{ zone2_frequency | default('3-4x/week') }}:</h3>
|
||||
<ul class="text-sm text-black space-y-1 list-disc list-inside">
|
||||
<li>{{ zone2_duration | default('40+ minutes') }} of Steady State Cardio (HR {{ zone2_hr_range | default('____') }} bpm)</li>
|
||||
<li>{{ zone2_speed | default('____ mph') }} at {{ zone2_incline | default('2% Incline') }}</li>
|
||||
</ul>
|
||||
<!-- Training Recommendations Section -->
|
||||
<div class="grid grid-cols-2 gap-8 mb-8">
|
||||
<!-- Left Side: Zone Recommendations -->
|
||||
<div class="bg-gray-200 p-4 rounded-lg border-2 border-gray-300">
|
||||
<!-- Zone 2 Recommendations -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-sm font-bold text-black mb-2">
|
||||
Zone 2 {{ zone2_frequency | default('3-4x/week') }}:
|
||||
</h3>
|
||||
<ul
|
||||
class="text-xs text-black space-y-0.5 list-disc list-inside ml-2"
|
||||
>
|
||||
<li>
|
||||
{{ zone2_duration | default('40+ minutes') }} of
|
||||
Steady State Cardio (HR
|
||||
<span class="border-b border-black"
|
||||
>{{ zone2_hr_range | default('____') }}</span
|
||||
>
|
||||
bpm)
|
||||
</li>
|
||||
<li>
|
||||
<span class="border-b border-black"
|
||||
>{{ zone2_speed | default('___') }}</span
|
||||
>
|
||||
mph at {{ zone2_incline | default('2% Incline') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Zone 3 Recommendations -->
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-black mb-2">
|
||||
Zone 3: {{ zone3_frequency | default('1-2x/week') }}:
|
||||
</h3>
|
||||
<ul
|
||||
class="text-xs text-black space-y-0.5 list-disc list-inside ml-2"
|
||||
>
|
||||
<li>
|
||||
{{ zone3_duration | default('10-20 minutes') }} in
|
||||
zone 3 (HR
|
||||
<span class="border-b border-black"
|
||||
>{{ zone3_hr_range | default('____') }}</span
|
||||
>
|
||||
bpm)
|
||||
</li>
|
||||
<li>
|
||||
<span class="border-b border-black"
|
||||
>{{ zone3_speed | default('___') }}</span
|
||||
>mph + at {{ zone3_incline | default('2% Incline')
|
||||
}}
|
||||
</li>
|
||||
<li class="text-orange-500">
|
||||
Slow down cadence until HR reaches
|
||||
<span class="border-b border-orange-500"
|
||||
>{{ zone3_target_hr | default('___') }}</span
|
||||
>bpm
|
||||
</li>
|
||||
<li>
|
||||
<span class="border-b border-black"
|
||||
>{{ zone3_recovery_speed | default('____')
|
||||
}}</span
|
||||
>mph at {{ zone3_recovery_incline | default('2%
|
||||
Incline') }}
|
||||
</li>
|
||||
<li class="text-orange-500">
|
||||
Maintain HR in zone 1 (<span
|
||||
class="border-b border-orange-500"
|
||||
>{{ zone1_hr_range | default('____') }}</span
|
||||
>bpm) for {{ zone1_duration | default('4-8 minutes')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
Repeat {{ zone3_repeats | default('2-3 times') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Training Table -->
|
||||
<div>
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="bg-cyan-300 border border-gray-400 px-2 py-1.5 text-black font-bold text-xs"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
class="bg-cyan-300 border border-gray-400 px-2 py-1.5 text-black font-bold text-xs"
|
||||
>
|
||||
Sets
|
||||
</th>
|
||||
<th
|
||||
class="bg-cyan-300 border border-gray-400 px-2 py-1.5 text-black font-bold text-xs"
|
||||
>
|
||||
Effort Duration
|
||||
</th>
|
||||
<th
|
||||
class="bg-cyan-300 border border-gray-400 px-2 py-1.5 text-black font-bold text-xs"
|
||||
>
|
||||
Zone
|
||||
</th>
|
||||
<th
|
||||
class="bg-cyan-300 border border-gray-400 px-2 py-1.5 text-black font-bold text-xs"
|
||||
>
|
||||
RPE
|
||||
</th>
|
||||
<th
|
||||
class="bg-cyan-300 border border-gray-400 px-2 py-1.5 text-black font-bold text-xs"
|
||||
>
|
||||
Recovery Duration
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Short Row -->
|
||||
<tr>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black font-medium text-xs"
|
||||
>
|
||||
Short
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ short_sets | default('8-10') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ short_duration | default('10-30 seconds') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ short_zone | default('5') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ short_rpe | default('10') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ short_recovery | default('20-60 seconds') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Medium Row -->
|
||||
<tr>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black font-medium text-xs"
|
||||
>
|
||||
Medium
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ medium_sets | default('6-8') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ medium_duration | default('30-90 seconds') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ medium_zone | default('4') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ medium_rpe | default('8-9') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ medium_recovery | default('30-90 seconds') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Long Row -->
|
||||
<tr>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black font-medium text-xs"
|
||||
>
|
||||
Long
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ long_sets | default('4-6') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ long_duration | default('5-10 minutes') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ long_zone | default('3/4') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ long_rpe | default('7-8') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ long_recovery | default('2.5-5 minutes') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Tempo Row -->
|
||||
<tr>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black font-medium text-xs"
|
||||
>
|
||||
Tempo
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ tempo_sets | default('2-3') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ tempo_duration | default('10-20 minutes') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ tempo_zone | default('3') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ tempo_rpe | default('6-7') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ tempo_recovery | default('4-8 minutes') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Cardio Row -->
|
||||
<tr>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black font-medium text-xs"
|
||||
>
|
||||
Cardio
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ cardio_sets | default('1') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ cardio_duration | default('>40 minutes') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ cardio_zone | default('2') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ cardio_rpe | default('4-5') }}
|
||||
</td>
|
||||
<td
|
||||
class="bg-gray-100 border border-gray-400 px-2 py-1.5 text-center text-black text-xs"
|
||||
>
|
||||
{{ cardio_recovery | default('N/A') }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zone 3 Recommendations -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-bold text-black mb-3">Zone 3 {{ zone3_frequency | default('1-2x/week') }}:</h3>
|
||||
<ul class="text-sm text-black space-y-1 list-disc list-inside">
|
||||
<li>{{ zone3_duration | default('10-20 minutes') }} in zone 3 (HR {{ zone3_hr_range | default('____ bpm') }})</li>
|
||||
<li>{{ zone3_speed | default('____mph') }} + at {{ zone3_incline | default('2% Incline') }}</li>
|
||||
<li>Slow down cadence until HR reaches {{ zone3_target_hr | default('___ bpm') }}</li>
|
||||
<li>{{ zone3_recovery_speed | default('____mph') }} at {{ zone3_recovery_incline | default('2% Incline') }}</li>
|
||||
<li>Maintain HR in zone 1 ({{ zone1_hr_range | default('____bpm') }}) for {{ zone1_duration | default('4-8 minutes') }}</li>
|
||||
<li>Repeat {{ zone3_repeats | default('2-3 times') }}</li>
|
||||
</ul>
|
||||
<!-- Training Week Example Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-black mb-6 text-center">
|
||||
Training Week Example with Progression
|
||||
</h2>
|
||||
|
||||
<!-- Week 1 -->
|
||||
<div class="mb-4">
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<!-- Monday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Monday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tuesday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Tuesday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Wednesday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Wednesday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Thursday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Thursday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Friday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Friday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Saturday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Saturday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sunday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Sunday
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-1 mt-1">
|
||||
<!-- Monday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_mon_zone | default('Zone 2') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_mon_duration | default('45 mins') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tuesday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_tue_zone | default('Zone 2') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_tue_duration | default('45 mins') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Wednesday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_wed_zone | default('Zone 3') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_wed_duration1 | default('10mins On') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_wed_duration2 | default('8mins Rest') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_wed_sets | default('x2') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Thursday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_thu_content | default('') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Friday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_fri_zone | default('Zone 2') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_fri_duration | default('45 mins') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Saturday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_sat_content | default('') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sunday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week1_sun_content | default('') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 2 -->
|
||||
<div class="mb-4">
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<!-- Monday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Monday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tuesday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Tuesday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Wednesday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Wednesday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Thursday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Thursday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Friday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Friday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Saturday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Saturday
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sunday -->
|
||||
<div class="bg-cyan-300 p-2 text-center">
|
||||
<div class="font-bold text-black text-sm mb-1">
|
||||
Sunday
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-1 mt-1">
|
||||
<!-- Monday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_mon_zone | default('Zone 2') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_mon_duration | default('50 mins') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tuesday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_tue_zone | default('Zone 2') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_tue_duration | default('50 mins') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Wednesday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_wed_zone | default('Zone 3') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_wed_duration1 | default('10mins On') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_wed_duration2 | default('6mins Rest') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_wed_sets | default('x2') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Thursday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_thu_content | default('') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Friday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_fri_zone | default('Zone 2') }}
|
||||
</div>
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_fri_duration | default('50 mins') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Saturday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_sat_content | default('') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sunday Content -->
|
||||
<div class="bg-gray-200 p-2 text-center min-h-[60px]">
|
||||
<div class="text-xs text-black">
|
||||
{{ week2_sun_content | default('') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Training Table -->
|
||||
<div>
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Type</th>
|
||||
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Sets</th>
|
||||
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Effort Duration</th>
|
||||
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone</th>
|
||||
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">RPE</th>
|
||||
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Recovery Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Short Row -->
|
||||
<tr>
|
||||
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Short</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ short_sets | default('8-10') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ short_duration | default('10-30 seconds') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ short_zone | default('5') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ short_rpe | default('10') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ short_recovery | default('20-60 seconds') }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- Medium Row -->
|
||||
<tr>
|
||||
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Medium</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_sets | default('6-8') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_duration | default('30-90 seconds') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_zone | default('4') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_rpe | default('8-9') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_recovery | default('30-90 seconds') }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- Long Row -->
|
||||
<tr>
|
||||
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Long</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ long_sets | default('4-6') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ long_duration | default('5-10 minutes') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ long_zone | default('3/4') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ long_rpe | default('7-8') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ long_recovery | default('2.5-5 minutes') }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- Tempo Row -->
|
||||
<tr>
|
||||
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Tempo</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_sets | default('2-3') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_duration | default('10-20 minutes') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_zone | default('3') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_rpe | default('6-7') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_recovery | default('4-8 minutes') }}</td>
|
||||
</tr>
|
||||
|
||||
<!-- Cardio Row -->
|
||||
<tr>
|
||||
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Cardio</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_sets | default('1') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_duration | default('>40 minutes') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_zone | default('2') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_rpe | default('4-5') }}</td>
|
||||
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_recovery | default('N/A') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training Week Example Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-black mb-6 text-center">Training Week Example with Progression</h2>
|
||||
|
||||
<!-- Week 1 -->
|
||||
<div class="mb-6">
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
<!-- Monday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Monday</div>
|
||||
<div class="text-sm text-black">{{ week1_mon_zone | default('Zone 2') }}</div>
|
||||
<div class="text-sm text-black">{{ week1_mon_duration | default('45 mins') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Tuesday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Tuesday</div>
|
||||
<div class="text-sm text-black">{{ week1_tue_zone | default('Zone 2') }}</div>
|
||||
<div class="text-sm text-black">{{ week1_tue_duration | default('45 mins') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Wednesday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Wednesday</div>
|
||||
<div class="text-sm text-black">{{ week1_wed_zone | default('Zone 3') }}</div>
|
||||
<div class="text-sm text-black">{{ week1_wed_duration1 | default('10mins On') }}</div>
|
||||
<div class="text-sm text-black">{{ week1_wed_duration2 | default('8mins Rest') }}</div>
|
||||
<div class="text-sm text-black">{{ week1_wed_sets | default('x2') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Thursday -->
|
||||
<div class="bg-gray-200 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Thursday</div>
|
||||
<div class="text-sm text-black">{{ week1_thu_content | default('') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Friday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Friday</div>
|
||||
<div class="text-sm text-black">{{ week1_fri_zone | default('Zone 2') }}</div>
|
||||
<div class="text-sm text-black">{{ week1_fri_duration | default('45 mins') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Saturday -->
|
||||
<div class="bg-gray-200 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Saturday</div>
|
||||
<div class="text-sm text-black">{{ week1_sat_content | default('') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Sunday -->
|
||||
<div class="bg-gray-200 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Sunday</div>
|
||||
<div class="text-sm text-black">{{ week1_sun_content | default('') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 2 -->
|
||||
<div class="mb-6">
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
<!-- Monday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Monday</div>
|
||||
<div class="text-sm text-black">{{ week2_mon_zone | default('Zone 2') }}</div>
|
||||
<div class="text-sm text-black">{{ week2_mon_duration | default('50 mins') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Tuesday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Tuesday</div>
|
||||
<div class="text-sm text-black">{{ week2_tue_zone | default('Zone 2') }}</div>
|
||||
<div class="text-sm text-black">{{ week2_tue_duration | default('50 mins') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Wednesday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Wednesday</div>
|
||||
<div class="text-sm text-black">{{ week2_wed_zone | default('Zone 3') }}</div>
|
||||
<div class="text-sm text-black">{{ week2_wed_duration1 | default('10mins On') }}</div>
|
||||
<div class="text-sm text-black">{{ week2_wed_duration2 | default('6mins Rest') }}</div>
|
||||
<div class="text-sm text-black">{{ week2_wed_sets | default('x2') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Thursday -->
|
||||
<div class="bg-gray-200 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Thursday</div>
|
||||
<div class="text-sm text-black">{{ week2_thu_content | default('') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Friday -->
|
||||
<div class="bg-cyan-300 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Friday</div>
|
||||
<div class="text-sm text-black">{{ week2_fri_zone | default('Zone 2') }}</div>
|
||||
<div class="text-sm text-black">{{ week2_fri_duration | default('50 mins') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Saturday -->
|
||||
<div class="bg-gray-200 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Saturday</div>
|
||||
<div class="text-sm text-black">{{ week2_sat_content | default('') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Sunday -->
|
||||
<div class="bg-gray-200 p-3 text-center rounded-lg">
|
||||
<div class="font-bold text-black mb-1">Sunday</div>
|
||||
<div class="text-sm text-black">{{ week2_sun_content | default('') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<!-- Footer Section -->
|
||||
</div>
|
||||
|
||||
+103
-64
@@ -1,70 +1,109 @@
|
||||
<div class="w-full page bg-white">
|
||||
<!-- Main Content -->
|
||||
<div class="px-8 py-6">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-3xl font-bold text-black mb-8">Next Steps:</h1>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="px-8 py-6">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-3xl font-bold text-black mb-8">Next Steps:</h1>
|
||||
<!-- Calorie Tracking Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4">Calorie Tracking</h2>
|
||||
<ul class="text-black space-y-2 list-disc list-inside ml-4">
|
||||
<li>
|
||||
Download and create an account with My Fitness Pal (or
|
||||
preferred nutrition tracker)
|
||||
</li>
|
||||
<li>
|
||||
Fill out the "My Profile" section with your goals (ie:
|
||||
height, weight, target weight)
|
||||
<ul class="mt-2 ml-6 space-y-1 list-disc list-inside">
|
||||
<li>Input your Macros</li>
|
||||
<li>Click the three dots on the bottom right corner</li>
|
||||
<li>Click "Goals"</li>
|
||||
<li>
|
||||
Click "Calorie, Carbs, Protein and Fat Goals" under
|
||||
the Nutrition Goals
|
||||
</li>
|
||||
<li>
|
||||
Set the Calories, Carbs, Protein, and Fat to the
|
||||
recommended macro outlined above.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Once completed fill out your food intake from each meal on
|
||||
the main page
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Calorie Tracking Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4">Calorie Tracking</h2>
|
||||
<ul class="text-black space-y-2 list-disc list-inside ml-4">
|
||||
<li>Download and create an account with My Fitness Pal (or preferred nutrition tracker)</li>
|
||||
<li>Fill out the "My Profile" section with your goals (ie: height, weight, target weight)
|
||||
<ul class="mt-2 ml-6 space-y-1 list-disc list-inside">
|
||||
<li>Input your Macros</li>
|
||||
<li>Click the three dots on the bottom right corner</li>
|
||||
<li>Click "Goals"</li>
|
||||
<li>Click "Calorie, Carbs, Protein and Fat Goals" under the Nutrition Goals</li>
|
||||
<li>Set the Calories, Carbs, Protein, and Fat to the recommended macro outlined above.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Once completed fill out your food intake from each meal on the main page</li>
|
||||
</ul>
|
||||
<!-- Recommendation Note -->
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-black italic font-semibold">
|
||||
It's highly recommended to purchase a weight and food scale
|
||||
for more accurate results.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommendation Note -->
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-black italic font-semibold">It's highly recommended to purchase a weight and food scale for more accurate results.</p>
|
||||
</div>
|
||||
<!-- Daily Tasks Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4">Daily Tasks</h2>
|
||||
<ul class="text-black space-y-2 list-disc list-inside ml-4">
|
||||
<li>
|
||||
Weigh yourself in the morning, after your first bowel
|
||||
movement, and naked
|
||||
</li>
|
||||
<li>Log your weight into your my fitness pal app</li>
|
||||
<li>
|
||||
Track calories in grams - FOLLOW YOUR PERSONAL
|
||||
RECOMMENDATIONS.
|
||||
</li>
|
||||
<li>
|
||||
Log in a diary:
|
||||
<ul class="mt-2 ml-6 space-y-1 list-disc list-inside">
|
||||
<li>
|
||||
Log any additional prescribed recommendation (i.e
|
||||
breath work)
|
||||
</li>
|
||||
<li>
|
||||
Complete the prescribed training recommendations
|
||||
(i.e Zone 2 Training)
|
||||
</li>
|
||||
<li>
|
||||
Log additional physical activity (i.e Monday -
|
||||
Strength Training 1 hour)
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Two weeks after Appointment Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4">
|
||||
Two weeks after Appointment
|
||||
</h2>
|
||||
<ul class="text-black space-y-2 list-disc list-inside ml-4">
|
||||
<li>
|
||||
Should you find the macronutrient breakdown difficult to
|
||||
follow, reach out to us to discuss a change within your
|
||||
caloric parameters
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="mb-12 text-center">
|
||||
<p class="text-lg font-bold text-black">
|
||||
Should you have any questions or concerns please contact us!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Recommended Next Testing Date -->
|
||||
<div class="mb-8 text-center">
|
||||
<h2 class="text-2xl font-bold text-black">
|
||||
<span class="underline">Recommended Next Testing Date:</span>
|
||||
<span class="ml-2">{{ next_testing_date }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Daily Tasks Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4">Daily Tasks</h2>
|
||||
<ul class="text-black space-y-2 list-disc list-inside ml-4">
|
||||
<li>Weigh yourself in the morning, after your first bowel movement, and naked</li>
|
||||
<li>Log your weight into your my fitness pal app</li>
|
||||
<li>Track calories in grams - FOLLOW YOUR PERSONAL RECOMMENDATIONS.</li>
|
||||
<li>Log in a diary:
|
||||
<ul class="mt-2 ml-6 space-y-1 list-disc list-inside">
|
||||
<li>Log any additional prescribed recommendation (i.e breath work)</li>
|
||||
<li>Complete the prescribed training recommendations (i.e Zone 2 Training)</li>
|
||||
<li>Log additional physical activity (i.e Monday - Strength Training 1 hour)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Two weeks after Appointment Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4">Two weeks after Appointment</h2>
|
||||
<ul class="text-black space-y-2 list-disc list-inside ml-4">
|
||||
<li>Should you find the macronutrient breakdown difficult to follow, reach out to us to discuss a change within your caloric parameters</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="mb-12 text-center">
|
||||
<p class="text-lg font-bold text-black">Should you have any questions or concerns please contact us!</p>
|
||||
</div>
|
||||
|
||||
<!-- Recommended Next Testing Date -->
|
||||
<div class="mb-8 text-center">
|
||||
<h2 class="text-2xl font-bold text-black">
|
||||
<span class="underline">Recommended Next Testing Date:</span>
|
||||
<span class="ml-2">October 2025</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="w-full page bg-white">
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="px-8 py-6">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-3xl font-bold text-black mb-6">Glossary</h1>
|
||||
|
||||
<!-- Body Fat Percentage -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-bold text-black mb-2">Body Fat Percentage:</h2>
|
||||
<p class="text-sm text-black leading-relaxed">The percentage of your overall body weight that is composed of fat cells. Body fat percentage can be reduced by either losing weight from fat mass, while maintaining lean mass, or maintaining fat mass while increasing lean mass.</p>
|
||||
</div>
|
||||
|
||||
<!-- Metabolic Rate -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-bold text-black mb-2">Metabolic Rate:</h2>
|
||||
<p class="text-sm text-black leading-relaxed">Metabolic Rate measures the number of calories your body burns for basic functions and movement, based on factors like weight, age, gender, and height. A higher metabolic rate helps prevent weight gain and supports weight loss by ensuring you burn enough calories. Tracking metabolic rate is key for managing weight and preventing conditions linked to metabolic dysfunction. Positive influences include resistance exercise, proper sleep, and adequate protein, while negative factors include extreme dieting, yo-yo dieting, and excessive cardio. Improving it involves resistance training and optimal nutrition.</p>
|
||||
</div>
|
||||
|
||||
<!-- Fuel Source -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-bold text-black mb-2">Fuel Source:</h2>
|
||||
<p class="text-sm text-black leading-relaxed mb-2">Fat-burning efficiency measures your cells' ability to use fat as fuel, reflecting mitochondrial and cellular health. It indicates how well your body balances fat and carbohydrate usage to support energy needs, assessed by analyzing oxygen and carbon dioxide in your breath. High fat-burning efficiency suggests strong metabolic and mitochondrial function, linked to better weight management and longevity.</p>
|
||||
<p class="text-sm text-black leading-relaxed">To improve fat-burning efficiency, focus on Zone 2 endurance training and potentially intermittent fasting to enhance oxygen absorption and cellular function. Zone 5 interval training will also help improve fat burning mitochondrial density and capillarization. Factors that reduce fat burning ability include diets high in processed foods, alcohol, and large meals before bed. Conditions related to metabolic stress also hinder fat burning abilities.</p>
|
||||
</div>
|
||||
|
||||
<!-- NEAT -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-bold text-black mb-2">NEAT (Non-Exercise Activity Thermogenesis)</h2>
|
||||
<p class="text-sm text-black leading-relaxed">refers to the energy expended for all activities that are not deliberate exercise or structured physical activity. This includes daily movements such as walking, fidgeting, standing, cleaning, typing, and even simple tasks like cooking or shopping. NEAT contributes significantly to the total caloric expenditure and plays a key role in maintaining body weight and overall energy balance. It varies widely among individuals, depending on lifestyle, occupation, and habits.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
<div class="w-full page bg-white">
|
||||
<!-- Main Content -->
|
||||
<div class="p-8">
|
||||
<!-- Body Fat Percent Master Chart Section -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold mb-4 text-center">
|
||||
Body Fat Percent Master Chart
|
||||
</h1>
|
||||
<div class="w-full max-w-5xl mx-auto">
|
||||
<img
|
||||
src="data:image/png;base64,{{ body_fat_percentage_chart }}"
|
||||
alt="Body Fat Percentage"
|
||||
class="w-full h-auto object-contain chart-large"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resting Heart Rate Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4 text-center">
|
||||
Resting Heart Rate
|
||||
</h2>
|
||||
|
||||
<!-- Male Table -->
|
||||
<div class="mb-4">
|
||||
<table
|
||||
class="w-full border-collapse border border-gray-300 text-xs"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-blue-300">
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Age (M)
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Poor
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Below Average
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Average
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Above Average
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Good
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Excellent
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Athlete
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
18-25
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
85bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
79-84bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
74-78bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
70-73bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
66-69bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
61-65bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
40-60bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
26-35
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
83bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
77-82bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
73-76bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
69-72bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
65-68bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
60-64bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
42-59bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
36-45
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
85bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
79-84bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
74-78bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
70-73bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
65-69bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
60-64bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
45-59bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
46-55
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
84bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
78-83bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
74-77bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
70-73bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
66-69bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
61-65bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
48-60bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
56-65
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
84bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
78-83bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
74-77bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
70-73bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
65-69bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
60-64bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
50-59bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
65+
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
84bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
77-83bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
73-76bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
70-73bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
65-69bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
60-64bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
52-59bpm
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Female Table -->
|
||||
<div class="mb-4">
|
||||
<table
|
||||
class="w-full border-collapse border border-gray-300 text-xs"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-blue-300">
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Age (F)
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Poor
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Below Average
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Average
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Above Average
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Good
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Excellent
|
||||
</th>
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
Athlete
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
18-25
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
82bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
74-81bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
70-73bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
66-69bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
62-65bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
56-61bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
40-55bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
26-35
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
82bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
75-81bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
71-74bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
66-70bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
62-65bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
55-61bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
44-54bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
36-45
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
83bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
76-82bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
71-75bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
67-70bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
63-66bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
57-62bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
47-56bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
46-55
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
84bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
77-83bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
72-76bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
68-71bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
64-67bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
58-63bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
49-57bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
56-65
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
82bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
76-81bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
72-75bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
68-71bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
62-67bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
57-61bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
51-56bpm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="border border-gray-300 p-1 font-medium text-center"
|
||||
>
|
||||
65+
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
80bpm +
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
74-79bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
70-73bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
66-69bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
62-65bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
56-61bpm
|
||||
</td>
|
||||
<td class="border border-gray-300 p-1 text-center">
|
||||
52-55bpm
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
<!-- Lung Analysis -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
3
|
||||
</div>
|
||||
@@ -35,7 +36,8 @@
|
||||
<!-- Cardio Metrics -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
4
|
||||
</div>
|
||||
@@ -52,7 +54,8 @@
|
||||
<!-- Fuel Utilization -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
5
|
||||
</div>
|
||||
@@ -66,7 +69,8 @@
|
||||
<!-- Local Muscle Activity -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
9
|
||||
</div>
|
||||
@@ -80,7 +84,8 @@
|
||||
<!-- Training Recommendations -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
10
|
||||
</div>
|
||||
@@ -94,7 +99,8 @@
|
||||
<!-- Next Steps -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
12
|
||||
</div>
|
||||
@@ -111,7 +117,8 @@
|
||||
<!-- Glossary -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
13
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
class="w-full border-collapse border border-gray-300 text-xs"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-cyan-200">
|
||||
<tr class="bg-blue-300">
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
@@ -238,7 +238,7 @@
|
||||
class="w-full border-collapse border border-gray-300 text-xs"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-cyan-200">
|
||||
<tr class="bg-blue-300">
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
@@ -467,7 +467,7 @@
|
||||
class="w-full border-collapse border border-gray-300 text-xs"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-cyan-200">
|
||||
<tr class="bg-blue-300">
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
@@ -641,7 +641,7 @@
|
||||
class="w-full border-collapse border border-gray-300 text-xs"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-cyan-200">
|
||||
<tr class="bg-blue-300">
|
||||
<th
|
||||
class="border border-gray-300 p-1 font-bold text-center"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<div class="bg-white w-full page m-0 px-10">
|
||||
<div class="px-16 py-10">
|
||||
<!-- Table of Contents Header -->
|
||||
<div class="mb-8">
|
||||
<h1
|
||||
class="text-5xl font-bold text-black mb-6 tracking-wide border-b-4 border-blue-500 pb-2 text-center"
|
||||
>
|
||||
TABLE OF CONTENTS
|
||||
</h1>
|
||||
<div class="w-full h-1 bg-cyan-400"></div>
|
||||
</div>
|
||||
|
||||
<!-- Table of Contents Items -->
|
||||
<div class="flex flex-col justify-between space-y-6 py-6">
|
||||
<!-- Nutrition Guidelines -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 py-1 justify-center h-full">
|
||||
<h2 class="text-2xl font-semibold text-black">
|
||||
Nutrition Guidelines
|
||||
</h2>
|
||||
<p class="text-gray-600 text-base">
|
||||
Ultrasound & Body Composition
|
||||
</p>
|
||||
<p class="text-gray-600 text-base">
|
||||
Resting Metabolic Rate Assessment
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nutrition Recommendations -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
4
|
||||
</div>
|
||||
<div class="flex flex-col py-1 flex-1 justify-center h-full">
|
||||
<h2 class="text-2xl font-semibold text-black mb-3">
|
||||
Nutrition Recommendations
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next Steps -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
5
|
||||
</div>
|
||||
<div class="flex flex-col h-full justify-center flex-1">
|
||||
<h2 class="text-2xl font-semibold text-black">
|
||||
Next Steps
|
||||
</h2>
|
||||
<div class="space-y-2">
|
||||
<!-- No sub-items -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Glossary -->
|
||||
<div class="flex items-start bg-gray-200 h-24">
|
||||
<div
|
||||
class="bg-black text-white text-4xl font-extrabold w-24 h-24 flex items-center justify-center mr-8 flex-shrink-0"
|
||||
style="border-radius: 0;"
|
||||
>
|
||||
6
|
||||
</div>
|
||||
<div class="flex flex-col h-full justify-center flex-1">
|
||||
<h2 class="text-2xl font-semibold text-black">
|
||||
Glossary
|
||||
</h2>
|
||||
<div class="space-y-2">
|
||||
<!-- No sub-items -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,114 @@
|
||||
<div class="w-full bg-white">
|
||||
<!-- Header Section -->
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="px-8 py-2">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-3xl font-bold text-black mb-3">Nutrition Guidelines</h1>
|
||||
|
||||
<!-- Section Title -->
|
||||
<h2 class="text-xl font-bold text-black mb-2">
|
||||
Resting Metabolic Rate Assessment
|
||||
</h2>
|
||||
<p class="text-gray-700 text-sm mb-4">
|
||||
The resting metabolic rate assessment determines the number of
|
||||
calories that you burn at rest, and metabolic health. It is also an
|
||||
indicator of overall health and well-being.
|
||||
</p>
|
||||
|
||||
<!-- Slow vs Fast Metabolism Section -->
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
src="data:image/png;base64,{{ metabolism_chart }}"
|
||||
alt="Slow vs Fast Metabolism Chart"
|
||||
class="max-w-full h-auto max-h-40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fuel Source Section -->
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
src="data:image/png;base64,{{ fuel_source_chart }}"
|
||||
alt="Fuel Source Chart"
|
||||
class="max-w-full h-auto max-h-40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Caloric Intake Section -->
|
||||
<div class="px-6 mb-6">
|
||||
<h3 class="text-2xl font-bold text-black mb-4 text-center">
|
||||
Caloric Intake
|
||||
</h3>
|
||||
|
||||
<!-- Calculation Formula -->
|
||||
<div class="flex items-center justify-center space-x-4 text-center">
|
||||
<!-- Resting Metabolic -->
|
||||
<div class="bg-gray-200 p-3 rounded-lg">
|
||||
<div class="text-lg font-bold text-black">
|
||||
{{ resting_calories }}kCals
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div>Resting</div>
|
||||
<div>Metabolic</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plus sign -->
|
||||
<div class="text-2xl font-bold text-black">+</div>
|
||||
|
||||
<!-- NEAT -->
|
||||
<div class="bg-gray-200 p-3 rounded-lg">
|
||||
<div class="text-lg font-bold text-black">
|
||||
{{ neat_calories }}kCals
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">NEAT</div>
|
||||
</div>
|
||||
|
||||
<!-- Minus sign -->
|
||||
<div class="text-2xl font-bold text-black">-</div>
|
||||
|
||||
<!-- Weight Loss -->
|
||||
<div class="bg-gray-200 p-3 rounded-lg">
|
||||
<div class="text-lg font-bold text-black">
|
||||
{{ weight_loss_calories }}kCals
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div>to lose {{ weight_loss_rate }}lbs</div>
|
||||
<div>per week</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equals sign -->
|
||||
<div class="text-2xl font-bold text-black">=</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div class="bg-gray-200 p-3 rounded-lg">
|
||||
<div class="text-lg font-bold text-black">
|
||||
~{{ total_calories }}kCals
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resting Heart Rate Table Section -->
|
||||
{% if rhr_table %}
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-bold text-black mb-4 text-center">
|
||||
Resting Heart Rate
|
||||
</h2>
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
src="data:image/png;base64, {{ rhr_table }}"
|
||||
alt="Resting Heart Rate Table"
|
||||
class="table-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+127
-206
@@ -1,219 +1,140 @@
|
||||
<div class="w-full page bg-white">
|
||||
<!-- Header Section -->
|
||||
<!-- main content -->
|
||||
<div class="px-8 py-6">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-3xl font-bold text-black mb-8 text-center">Weekly Meal Plan Breakdown</h1>
|
||||
<div class="px-1 py-6">
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-2xl font-bold text-black mb-6 text-center">
|
||||
Weekly Meal Plan Breakdown
|
||||
</h1>
|
||||
|
||||
<!-- Caloric Deficit Example Section -->
|
||||
<div class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-black mb-6 text-center">Caloric Deficit Example</h2>
|
||||
<!-- Caloric Deficit Example Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4 text-center">
|
||||
Caloric Deficit Example
|
||||
</h2>
|
||||
|
||||
<!-- Weekly Grid -->
|
||||
<div class="grid grid-cols-7 gap-3 mb-8">
|
||||
<!-- Monday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Monday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
<!-- Weekly Grid -->
|
||||
<div class="grid grid-cols-7 gap-0 mb-6">
|
||||
{% set days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] %}
|
||||
{% for i in range(7) %}
|
||||
<div
|
||||
class="border border-gray-300 text-center
|
||||
{% if i == 0 %} rounded-tl-lg rounded-bl-lg {% endif %}
|
||||
{% if i == 6 %} rounded-tr-lg rounded-br-lg {% endif %}
|
||||
{% if i < 5 %} bg-cyan-300 {% else %} bg-white {% endif %}"
|
||||
style="padding: 0"
|
||||
>
|
||||
<!-- Day Row (Always blue) -->
|
||||
<div class="bg-blue-300
|
||||
{% if i == 0 %} rounded-tl-lg {% endif %}
|
||||
{% if i == 6 %} rounded-tr-lg {% endif %}
|
||||
text-black text-sm font-bold px-2 py-1"
|
||||
style="border-top-left-radius: {% if i == 0 %}0.5rem{% else %}0{% endif %}; border-top-right-radius: {% if i == 6 %}0.5rem{% else %}0{% endif %};"
|
||||
>
|
||||
{{ days[i] }}
|
||||
</div>
|
||||
<!-- Macro Body (fills to the bottom, cyan or white) -->
|
||||
<div class="flex flex-col items-center py-1 px-2">
|
||||
<div class="font-bold text-sm text-black mb-1">
|
||||
{{ deficit_calories | default('1725KCals') }} KCals
|
||||
</div>
|
||||
<div class="text-xs text-black leading-tight text-left">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tuesday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Tuesday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div class="text-xs text-black">{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
<!-- Caloric Deficit with Maintenance/Refeed Example Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-black mb-4 text-center">
|
||||
Caloric Deficit with Maintenance/Refeed Example
|
||||
</h2>
|
||||
|
||||
<!-- Weekly Grid -->
|
||||
<div class="grid grid-cols-7 gap-0 mb-6">
|
||||
{% for i in range(7) %}
|
||||
<div class="border border-gray-300 text-center
|
||||
{% if i == 0 %} rounded-tl-lg rounded-bl-lg {% endif %}
|
||||
{% if i == 6 %} rounded-tr-lg rounded-br-lg {% endif %}
|
||||
{% if i < 5 %} bg-cyan-300 {% else %} bg-white {% endif %}"
|
||||
style="padding: 0"
|
||||
>
|
||||
<!-- Day Row (Always blue) -->
|
||||
<div class="bg-blue-300
|
||||
{% if i == 0 %} rounded-tl-lg {% endif %}
|
||||
{% if i == 6 %} rounded-tr-lg {% endif %}
|
||||
text-black text-sm font-bold px-2 py-1"
|
||||
style="border-top-left-radius: {% if i == 0 %}0.5rem{% else %}0{% endif %}; border-top-right-radius: {% if i == 6 %}0.5rem{% else %}0{% endif %};"
|
||||
>
|
||||
{{ days[i] }}
|
||||
</div>
|
||||
<!-- Macro Body (fills to the bottom, cyan or white) -->
|
||||
{% if i < 5 %}
|
||||
<div class="flex flex-col items-center py-1 px-2">
|
||||
<div class="font-bold text-sm text-black mb-1">
|
||||
{{ refeed_weekday_calories | default('1615KCals') }} KCals
|
||||
</div>
|
||||
<div class="text-xs text-black leading-tight text-left">
|
||||
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
|
||||
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
|
||||
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex flex-col items-center py-1 px-2">
|
||||
<div class="font-bold text-black mb-1">
|
||||
{{ refeed_weekend_calories | default('2000KCals') }} KCals
|
||||
</div>
|
||||
<div class="text-xs text-black leading-tight text-left">
|
||||
<div>{{ refeed_weekend_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekend_carbs | default('190g Carbs') }}</div>
|
||||
<div>{{ refeed_weekend_fat | default('84g Fat') }}</div>
|
||||
<div>{{ refeed_weekend_fiber | default('30g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wednesday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Wednesday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Macronutrients Recommendations Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-black mb-8 text-center">
|
||||
Macronutrients Recommendations
|
||||
</h2>
|
||||
|
||||
<!-- Thursday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Thursday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Macronutrient Boxes -->
|
||||
<div class="flex justify-center space-x-12">
|
||||
<!-- Protein -->
|
||||
<div class="bg-gray-200 p-6 rounded-lg text-center">
|
||||
<div class="text-3xl font-bold text-black mb-2">
|
||||
{{ protein_percentage | default('28%') }}
|
||||
</div>
|
||||
<div class="text-lg font-semibold text-black">Protein</div>
|
||||
</div>
|
||||
|
||||
<!-- Friday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Friday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Carbs -->
|
||||
<div class="bg-gray-200 p-6 rounded-lg text-center">
|
||||
<div class="text-3xl font-bold text-black mb-2">
|
||||
{{ carbs_percentage | default('36%') }}
|
||||
</div>
|
||||
<div class="text-lg font-semibold text-black">Carbs</div>
|
||||
</div>
|
||||
|
||||
<!-- Saturday -->
|
||||
<div class="p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Saturday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
<!-- Fats -->
|
||||
<div class="bg-gray-200 p-6 rounded-lg text-center">
|
||||
<div class="text-3xl font-bold text-black mb-2">
|
||||
{{ fats_percentage | default('36%') }}
|
||||
</div>
|
||||
<div class="text-lg font-semibold text-black">Fats</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sunday -->
|
||||
<div class="p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Sunday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ deficit_protein | default('120g Protein') }}</div>
|
||||
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
|
||||
<div>{{ deficit_fat | default('69g Fat') }}</div>
|
||||
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Caloric Deficit with Maintenance/Refeed Example Section -->
|
||||
<div class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-black mb-6 text-center">Caloric Deficit with Maintenance/Refeed Example</h2>
|
||||
|
||||
<!-- Weekly Grid -->
|
||||
<div class="grid grid-cols-7 gap-2 mb-8">
|
||||
<!-- Monday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Monday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
|
||||
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
|
||||
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tuesday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Tuesday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
|
||||
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
|
||||
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wednesday -->
|
||||
<div class="bg-cyan-300 p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Wednesday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
|
||||
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
|
||||
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thursday -->
|
||||
<div class="p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Thursday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
|
||||
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
|
||||
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Friday -->
|
||||
<div class="p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Friday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
|
||||
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
|
||||
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Saturday -->
|
||||
<div class="p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Saturday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekend_calories | default('2000KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ refeed_weekend_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekend_carbs | default('190g Carbs') }}</div>
|
||||
<div>{{ refeed_weekend_fat | default('84g Fat') }}</div>
|
||||
<div>{{ refeed_weekend_fiber | default('30g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sunday -->
|
||||
<div class="p-3 text-left rounded-lg">
|
||||
<div class="font-bold text-black mb-2">Sunday</div>
|
||||
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekend_calories | default('2000KCals') }}</div>
|
||||
<div class="text-xs text-black space-y-1">
|
||||
<div>{{ refeed_weekend_protein | default('120g Protein') }}</div>
|
||||
<div>{{ refeed_weekend_carbs | default('190g Carbs') }}</div>
|
||||
<div>{{ refeed_weekend_fat | default('84g Fat') }}</div>
|
||||
<div>{{ refeed_weekend_fiber | default('30g Fibre') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Macronutrients Recommendations Section -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-black mb-8 text-center">Macronutrients Recommendations</h2>
|
||||
|
||||
<!-- Macronutrient Boxes -->
|
||||
<div class="flex justify-center space-x-12">
|
||||
<!-- Protein -->
|
||||
<div class="bg-gray-200 p-6 rounded-lg text-center">
|
||||
<div class="text-3xl font-bold text-black mb-2">{{ protein_percentage | default('28%') }}</div>
|
||||
<div class="text-lg font-semibold text-black">Protein</div>
|
||||
</div>
|
||||
|
||||
<!-- Carbs -->
|
||||
<div class="bg-gray-200 p-6 rounded-lg text-center">
|
||||
<div class="text-3xl font-bold text-black mb-2">{{ carbs_percentage | default('36%') }}</div>
|
||||
<div class="text-lg font-semibold text-black">Carbs</div>
|
||||
</div>
|
||||
|
||||
<!-- Fats -->
|
||||
<div class="bg-gray-200 p-6 rounded-lg text-center">
|
||||
<div class="text-3xl font-bold text-black mb-2">{{ fats_percentage | default('36%') }}</div>
|
||||
<div class="text-lg font-semibold text-black">Fats</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<!-- Indications Box -->
|
||||
<div class="bg-gray-200 rounded-lg p-4 text-center mb-2">
|
||||
<h3 class="font-semibold text-lg mb-2">Indications</h3>
|
||||
<p class="text-gray-700">{{ indication }}</p>
|
||||
<p >{{ indication | default('No Respiratory Capacity Limitation')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<div class="w-full page bg-white">
|
||||
<!-- Main Content -->
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<!-- Title -->
|
||||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-6">
|
||||
Fuel Utilization Report - Institute of Science, Health and
|
||||
Performance
|
||||
</h1>
|
||||
|
||||
<!-- Fuel Utilization Chart -->
|
||||
<div class="w-full max-w-5xl">
|
||||
<img
|
||||
@@ -13,8 +19,8 @@
|
||||
<!-- Chart Information -->
|
||||
<div class="mt-8 text-center">
|
||||
<p class="text-gray-700 text-sm">
|
||||
Client: {{ client_name | default('Keirstyn Moran') }} |
|
||||
Assessment Date: {{ assessment_date | default('July 29 2025') }}
|
||||
Client: {{ client_name }} | Assessment Date: {{ assessment_date
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -232,10 +232,15 @@ class ContextGenerator:
|
||||
if zone_key in metric_overrides:
|
||||
metrics[zone_key] = metric_overrides[zone_key]
|
||||
else:
|
||||
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
|
||||
fat_max_row = self.pnoe_df.loc[fat_max_idx]
|
||||
# Use optimal fat burning zone (highest fat:carb ratio) - same as _calculate_zone_metrics
|
||||
# This ensures consistency between zone calculations and zone metrics
|
||||
self.pnoe_df["fat_carb_ratio"] = self.pnoe_df["FAT_smoothed"] / (
|
||||
self.pnoe_df["CHO_smoothed"] + 0.00000001
|
||||
)
|
||||
optimal_fat_idx = self.pnoe_df["fat_carb_ratio"].idxmax()
|
||||
optimal_row = self.pnoe_df.loc[optimal_fat_idx]
|
||||
zones = self._calculate_hr_zones(
|
||||
metrics["vt1"], metrics["vt2"], fat_max_row
|
||||
metrics["vt1"], metrics["vt2"], optimal_row
|
||||
)
|
||||
metrics.update(zones)
|
||||
|
||||
@@ -280,29 +285,46 @@ class ContextGenerator:
|
||||
return vt1, vt2
|
||||
|
||||
def _calculate_hr_zones(
|
||||
self, vt1: Optional[Dict], vt2: Optional[Dict], fat_max_row: pd.Series
|
||||
self, vt1: Optional[Dict], vt2: Optional[Dict], optimal_row: pd.Series
|
||||
) -> Dict:
|
||||
"""Calculate heart rate zones based on thresholds"""
|
||||
"""Calculate heart rate zones based on thresholds
|
||||
|
||||
Uses optimal fat burning zone (highest fat:carb ratio) to match _calculate_zone_metrics.
|
||||
This ensures consistency between zone string calculations and zone metrics table.
|
||||
"""
|
||||
import math
|
||||
|
||||
zones = {}
|
||||
if vt1 and vt2:
|
||||
zone_1_start = fat_max_row["HR(bpm)_smoothed"] - 15
|
||||
zone_2_start = fat_max_row["HR(bpm)_smoothed"]
|
||||
zone_3_start = vt1["HeartRate"]
|
||||
zone_4_start = vt2["HeartRate"] - 10
|
||||
zone_5_start = vt2["HeartRate"] + 10
|
||||
# Use same zone boundary calculation as _calculate_zone_metrics
|
||||
zone_1_start = math.floor(optimal_row["HR(bpm)_smoothed"] - 15)
|
||||
zone_2_start = math.floor(optimal_row["HR(bpm)_smoothed"])
|
||||
zone_3_start = math.floor(vt1["HeartRate"])
|
||||
zone_4_start = math.floor(vt2["HeartRate"] - 10)
|
||||
zone_5_start = math.floor(vt2["HeartRate"])
|
||||
# zone_5_end is calculated for consistency with _calculate_zone_metrics
|
||||
# (not used in string format since zone 5 is open-ended: "+bpm")
|
||||
zone_5_end = math.floor(vt2["HeartRate"] + 10) # noqa: F841
|
||||
|
||||
zones["zone1_bpm"] = f"{int(zone_1_start)}-{int(zone_2_start)}bpm"
|
||||
zones["zone2_bpm"] = f"{int(zone_2_start)}-{int(vt1['HeartRate'])}bpm"
|
||||
zones["zone3_bpm"] = f"{int(zone_3_start)}-{int(zone_4_start)}bpm"
|
||||
zones["zone4_bpm"] = f"{int(zone_4_start)}-{int(zone_5_start)}bpm"
|
||||
zones["zone5_bpm"] = f"{int(zone_5_start)}+bpm"
|
||||
# Calculate zone ends to match _calculate_zone_metrics exactly
|
||||
zone_1_end = zone_2_start
|
||||
zone_2_end = math.floor(vt1["HeartRate"])
|
||||
zone_3_end = zone_4_start
|
||||
zone_4_end = zone_5_start
|
||||
|
||||
# Format zones to match _calculate_zone_metrics output
|
||||
zones["zone1_bpm"] = f"{int(zone_1_start)}-{int(zone_1_end)}bpm"
|
||||
zones["zone2_bpm"] = f"{int(zone_2_start)}-{int(zone_2_end)}bpm"
|
||||
zones["zone3_bpm"] = f"{int(zone_3_start)}-{int(zone_3_end)}bpm"
|
||||
zones["zone4_bpm"] = f"{int(zone_4_start)}-{int(zone_4_end)}bpm"
|
||||
zones["zone5_bpm"] = f"{int(zone_5_start)}-{int(zone_5_end)}bpm"
|
||||
else:
|
||||
max_hr = 220 - self.patient_info["age"]
|
||||
zones["zone1_bpm"] = f"{int(max_hr * 0.55)}-{int(max_hr * 0.65)}bpm"
|
||||
zones["zone2_bpm"] = f"{int(max_hr * 0.65)}-{int(max_hr * 0.75)}bpm"
|
||||
zones["zone3_bpm"] = f"{int(max_hr * 0.75)}-{int(max_hr * 0.85)}bpm"
|
||||
zones["zone4_bpm"] = f"{int(max_hr * 0.85)}-{int(max_hr * 0.95)}bpm"
|
||||
zones["zone5_bpm"] = f"{int(max_hr * 0.95)}+bpm"
|
||||
zones["zone5_bpm"] = f"{int(max_hr * 0.95)}-{int(max_hr * 1.05)}bpm"
|
||||
return zones
|
||||
|
||||
def _calculate_vo2_drop_points(self, pnoe_metrics: Dict) -> Dict:
|
||||
@@ -1112,9 +1134,17 @@ class ContextGenerator:
|
||||
graphs: Dict[str, str],
|
||||
metric_overrides: Optional[Dict] = None,
|
||||
graph_generator: Optional[Any] = None,
|
||||
report_type: str = "full",
|
||||
) -> Dict[str, Dict]:
|
||||
"""Main method to generate all page contexts
|
||||
|
||||
Args:
|
||||
patient_name: Patient name
|
||||
graphs: Dictionary of graph data
|
||||
metric_overrides: Optional metric overrides
|
||||
graph_generator: Optional graph generator instance
|
||||
report_type: Type of report ("full" or "minimal")
|
||||
|
||||
Returns:
|
||||
Dictionary with keys 'page_1', 'page_2', etc., each containing context data for that page
|
||||
"""
|
||||
@@ -1133,66 +1163,168 @@ class ContextGenerator:
|
||||
|
||||
contexts = {}
|
||||
|
||||
# Define which pages to generate based on report type
|
||||
if report_type == "minimal":
|
||||
# Minimal report only needs pages: 1, 2, 4, 5, 6, 16, 17, 19, 20
|
||||
# But we'll generate contexts for all needed pages and combine 19+20
|
||||
pages_to_generate = [1, 2, 4, 5, 6, 16, 17, 19, 20]
|
||||
else:
|
||||
# Full report needs all pages 1-20
|
||||
pages_to_generate = list(range(1, 21))
|
||||
|
||||
# Page 1
|
||||
contexts["page_1"] = {
|
||||
"name": self.patient_info["name"],
|
||||
"surname": self.patient_info["last_name"],
|
||||
"date": datetime.now().strftime("%B %d, %Y"),
|
||||
}
|
||||
|
||||
# Page 2
|
||||
contexts["page_2"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"test_date": datetime.now().strftime("%B %d, %Y"),
|
||||
}
|
||||
|
||||
# Pages 3, 6 (pages 4 and 5 are handled separately)
|
||||
for i in [0, 3]: # Skip indices 1 and 2 which are pages 4 and 5
|
||||
contexts[f"page_{i + 3}"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": i + 3,
|
||||
if 1 in pages_to_generate:
|
||||
contexts["page_1"] = {
|
||||
"name": self.patient_info["name"],
|
||||
"surname": self.patient_info["last_name"],
|
||||
"date": datetime.now().strftime("%B %d, %Y"),
|
||||
}
|
||||
|
||||
# Page 2
|
||||
if 2 in pages_to_generate:
|
||||
contexts["page_2"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"test_date": datetime.now().strftime("%B %d, %Y"),
|
||||
}
|
||||
|
||||
# Pages 3, 6 (pages 4 and 5 are handled separately)
|
||||
if report_type == "full":
|
||||
for i in [0, 3]: # Skip indices 1 and 2 which are pages 4 and 5
|
||||
contexts[f"page_{i + 3}"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": i + 3,
|
||||
}
|
||||
|
||||
# Page 4 - Nutrition Guidelines with Body Composition
|
||||
contexts["page_4"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 4,
|
||||
"fat_percentage": f"{self.patient_info['fat_percentage']:.1f}",
|
||||
"body_composition_chart": graphs.get("body_composition", ""),
|
||||
"body_fat_chart": graphs.get("body_fat_percent", ""), # Alias for template
|
||||
"body_fat_percent_chart": graphs.get(
|
||||
"body_fat_percent", ""
|
||||
), # Keep for consistency
|
||||
}
|
||||
if 4 in pages_to_generate:
|
||||
contexts["page_4"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 4,
|
||||
"fat_percentage": f"{self.patient_info['fat_percentage']:.1f}",
|
||||
"body_composition_chart": graphs.get("body_composition", ""),
|
||||
"body_fat_chart": graphs.get(
|
||||
"body_fat_percent", ""
|
||||
), # Alias for template
|
||||
"body_fat_percent_chart": graphs.get(
|
||||
"body_fat_percent", ""
|
||||
), # Keep for consistency
|
||||
}
|
||||
|
||||
# Page 5 - Resting Metabolic Rate Assessment
|
||||
contexts["page_5"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 5,
|
||||
"metabolism_chart": graphs.get("metabolism_chart", ""),
|
||||
"fuel_source_chart": graphs.get("fuel_source_chart", ""),
|
||||
"resting_calories": rmr_metrics.get("resting_calories", 1500),
|
||||
"neat_calories": rmr_metrics.get("neat_calories", 375),
|
||||
"weight_loss_calories": rmr_metrics.get("weight_loss_calories", 500),
|
||||
"weight_loss_rate": rmr_metrics.get("weight_loss_rate", 1.0),
|
||||
"total_calories": rmr_metrics.get("total_calories", 1375),
|
||||
}
|
||||
if 5 in pages_to_generate:
|
||||
contexts["page_5"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 5,
|
||||
"metabolism_chart": graphs.get("metabolism_chart", ""),
|
||||
"fuel_source_chart": graphs.get("fuel_source_chart", ""),
|
||||
"resting_calories": rmr_metrics.get("resting_calories", 1500),
|
||||
"neat_calories": rmr_metrics.get("neat_calories", 375),
|
||||
"weight_loss_calories": rmr_metrics.get("weight_loss_calories", 500),
|
||||
"weight_loss_rate": rmr_metrics.get("weight_loss_rate", 1.0),
|
||||
"total_calories": rmr_metrics.get("total_calories", 1375),
|
||||
}
|
||||
|
||||
# Calculate FEV1 percentage for page 7
|
||||
fev1_percentage = 0
|
||||
if spirometry_metrics.get("fvc_best"):
|
||||
fev1_percentage = (
|
||||
pnoe_metrics["peak_vt"] / spirometry_metrics["fvc_best"]
|
||||
) * 100
|
||||
# For minimal reports, also generate resting heart rate table for page_5
|
||||
if report_type == "minimal" and graph_generator:
|
||||
resting_hr_metrics = self._calculate_resting_heart_rate_metrics()
|
||||
rhr_table_info = self._calculate_rhr_table_data(
|
||||
self.patient_info["age"], self.patient_info["gender"]
|
||||
)
|
||||
|
||||
# Page 7
|
||||
contexts["page_7"] = {
|
||||
"peak_vt": f"{pnoe_metrics['peak_vt']:.2f}",
|
||||
"peak_vt_bpm": f"{int(pnoe_metrics['peak_vt_hr'])}",
|
||||
"fev1_percentage": f"{fev1_percentage:.1f}",
|
||||
"lung_analysis_chart": graphs.get("spirometry_chart", ""),
|
||||
"respiratory_analysis_chart": graphs.get("respiratory", ""),
|
||||
}
|
||||
# Get resting heart rate value and determine category
|
||||
rhr_value_str = resting_hr_metrics.get("resting_heart_rate", "0bpm")
|
||||
rhr_value = float(rhr_value_str.replace("bpm", "").strip())
|
||||
|
||||
category = self._determine_rhr_category(
|
||||
rhr_value,
|
||||
self.patient_info["age"],
|
||||
self.patient_info["gender"],
|
||||
)
|
||||
|
||||
gender_label = (
|
||||
"F" if self.patient_info["gender"].lower().startswith("f") else "M"
|
||||
)
|
||||
age_range_label = f"{rhr_table_info['age_range']} ({gender_label})"
|
||||
|
||||
rhr_columns = [
|
||||
"Age",
|
||||
"Poor",
|
||||
"Below Average",
|
||||
"Average",
|
||||
"Above Average",
|
||||
"Good",
|
||||
"Excellent",
|
||||
"Athlete",
|
||||
]
|
||||
rhr_data = [
|
||||
[
|
||||
age_range_label,
|
||||
rhr_table_info["ranges"]["Poor"],
|
||||
rhr_table_info["ranges"]["Below Average"],
|
||||
rhr_table_info["ranges"]["Average"],
|
||||
rhr_table_info["ranges"]["Above Average"],
|
||||
rhr_table_info["ranges"]["Good"],
|
||||
rhr_table_info["ranges"]["Excellent"],
|
||||
rhr_table_info["ranges"]["Athlete"],
|
||||
]
|
||||
]
|
||||
|
||||
contexts["page_5"]["rhr_table"] = (
|
||||
graph_generator.generate_resting_heart_rate_table(
|
||||
data=rhr_data,
|
||||
columns=rhr_columns,
|
||||
rhr_value=rhr_value,
|
||||
category=category,
|
||||
save_as_base64=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Page 6 - Meal Plan (needed for both full and minimal)
|
||||
if 6 in pages_to_generate:
|
||||
contexts["page_6"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 6,
|
||||
"deficit_calories": rmr_metrics.get("total_calories", 1600),
|
||||
"deficit_protein": f"{int(rmr_metrics.get('total_calories', 1600) * 0.22 / 4)}g Protein",
|
||||
"deficit_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 0.39 / 4)}g Carbs",
|
||||
"deficit_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 0.39 / 9)}g Fat",
|
||||
"deficit_fiber": "24g Fibre",
|
||||
"refeed_weekday_calories": int(
|
||||
rmr_metrics.get("total_calories", 1600) * 0.85
|
||||
),
|
||||
"refeed_weekday_protein": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.22 / 4)}g Protein",
|
||||
"refeed_weekday_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.39 / 4)}g Carbs",
|
||||
"refeed_weekday_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.39 / 9)}g Fat",
|
||||
"refeed_weekday_fiber": "20g Fibre",
|
||||
"refeed_weekend_calories": int(
|
||||
rmr_metrics.get("total_calories", 1600) * 1.375
|
||||
),
|
||||
"refeed_weekend_protein": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.22 / 4)}g Protein",
|
||||
"refeed_weekend_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.39 / 4)}g Carbs",
|
||||
"refeed_weekend_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.39 / 9)}g Fat",
|
||||
"refeed_weekend_fiber": "33g Fibre",
|
||||
"protein_percentage": "22%",
|
||||
"carbs_percentage": "39%",
|
||||
"fats_percentage": "39%",
|
||||
}
|
||||
|
||||
# Only generate pages 7-15 and 18 for full reports
|
||||
if report_type == "full":
|
||||
# Calculate FEV1 percentage for page 7
|
||||
fev1_percentage = 0
|
||||
if spirometry_metrics.get("fvc_best"):
|
||||
fev1_percentage = (
|
||||
pnoe_metrics["peak_vt"] / spirometry_metrics["fvc_best"]
|
||||
) * 100
|
||||
|
||||
# Page 7
|
||||
contexts["page_7"] = {
|
||||
"peak_vt": f"{pnoe_metrics['peak_vt']:.2f}",
|
||||
"peak_vt_bpm": f"{int(pnoe_metrics['peak_vt_hr'])}",
|
||||
"fev1_percentage": f"{fev1_percentage:.1f}",
|
||||
"lung_analysis_chart": graphs.get("spirometry_chart", ""),
|
||||
"respiratory_analysis_chart": graphs.get("respiratory", ""),
|
||||
}
|
||||
|
||||
# Page 8
|
||||
contexts["page_8"] = {
|
||||
@@ -1297,6 +1429,8 @@ class ContextGenerator:
|
||||
|
||||
# Page 9
|
||||
contexts["page_9"] = {
|
||||
"client_name": self.patient_info["name"],
|
||||
"assessment_date": datetime.now().strftime("%B %d %Y"),
|
||||
"fat_max_value": f"{pnoe_metrics['fat_max_value']:.2f}",
|
||||
"fat_max_hr": f"{int(pnoe_metrics['fat_max_hr'])}",
|
||||
"fuel_utilization_chart": graphs.get("fuel_utilization", ""),
|
||||
@@ -1408,26 +1542,64 @@ class ContextGenerator:
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not generate muscle oxygenation chart: {e}")
|
||||
|
||||
# Pages 14-18 (previously 13-17)
|
||||
for i in range(1, 6):
|
||||
contexts[f"page_{i + 13}"] = {
|
||||
# Pages 14-18 (previously 13-17)
|
||||
for i in range(1, 6):
|
||||
page_num = i + 13
|
||||
contexts[f"page_{page_num}"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": page_num,
|
||||
}
|
||||
# Add next_testing_date to page 16
|
||||
if page_num == 16:
|
||||
contexts["page_16"]["next_testing_date"] = self.patient_info.get(
|
||||
"next_testing_date", "Contact us for scheduling"
|
||||
)
|
||||
|
||||
# Page 16 - Next Steps (needed for both full and minimal)
|
||||
if 16 in pages_to_generate:
|
||||
contexts["page_16"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": i + 13,
|
||||
"page_number": 16,
|
||||
"next_testing_date": self.patient_info.get(
|
||||
"next_testing_date", "Contact us for scheduling"
|
||||
),
|
||||
}
|
||||
|
||||
# Page 19 - Glossary with Body Fat Percentage Master Chart (previously page 18)
|
||||
contexts["page_19"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 19,
|
||||
"body_fat_percentage_chart": graphs.get(
|
||||
"body_fat_percentage_master_chart", ""
|
||||
),
|
||||
}
|
||||
# Page 17 - Glossary (needed for both full and minimal, but minimal uses different template)
|
||||
if 17 in pages_to_generate:
|
||||
contexts["page_17"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 17,
|
||||
}
|
||||
|
||||
# Page 20 (previously page 19)
|
||||
contexts["page_20"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 20,
|
||||
}
|
||||
# Page 19 - Glossary with Body Fat Percentage Master Chart
|
||||
if 19 in pages_to_generate:
|
||||
contexts["page_19"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 19,
|
||||
"body_fat_percentage_chart": graphs.get(
|
||||
"body_fat_percentage_master_chart", ""
|
||||
),
|
||||
}
|
||||
|
||||
# Page 20 - Resting Heart Rate Table
|
||||
if 20 in pages_to_generate:
|
||||
contexts["page_20"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 20,
|
||||
}
|
||||
|
||||
# For minimal reports, create combined context for page_19_20_minimal
|
||||
if (
|
||||
report_type == "minimal"
|
||||
and 19 in pages_to_generate
|
||||
and 20 in pages_to_generate
|
||||
):
|
||||
contexts["page_19_20_minimal"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"body_fat_percentage_chart": graphs.get(
|
||||
"body_fat_percentage_master_chart", ""
|
||||
),
|
||||
}
|
||||
|
||||
return contexts
|
||||
|
||||
+224
-85
@@ -1124,75 +1124,163 @@ class GraphGenerator:
|
||||
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
|
||||
|
||||
def generate_metabolism_chart(
|
||||
self, rmr_kcal: float, save_as_base64: bool = True
|
||||
self,
|
||||
rmr_kcal: float,
|
||||
weight_kg: float = None,
|
||||
height_cm: float = None,
|
||||
age_years: int = None,
|
||||
sex: str = None,
|
||||
save_as_base64: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Generate metabolism chart (Slow vs Fast Metabolism).
|
||||
Matches the notebook implementation with ratio-based scale (0.3 to 1.9).
|
||||
|
||||
Args:
|
||||
rmr_kcal: Resting metabolic rate in kcal/day
|
||||
rmr_kcal: Resting metabolic rate in kcal/day (measured RMR)
|
||||
weight_kg: Weight in kg (optional, for calculating ratio)
|
||||
height_cm: Height in cm (optional, for calculating ratio)
|
||||
age_years: Age in years (optional, for calculating ratio)
|
||||
sex: Sex ("male" or "female", optional, for calculating ratio)
|
||||
save_as_base64: If True, return base64 string, else return file path
|
||||
|
||||
Returns:
|
||||
Base64 string or file path
|
||||
"""
|
||||
from matplotlib.patches import FancyBboxPatch
|
||||
from matplotlib.patches import Rectangle
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 2.5))
|
||||
fig, ax = plt.subplots(figsize=(11.5, 2.5))
|
||||
|
||||
# Chart data and positions
|
||||
categories = ["Very Slow", "Slow", "Average", "Fast", "Very Fast"]
|
||||
positions = [1500, 3000, 4500, 6000, 7500]
|
||||
indicator_pos = rmr_kcal
|
||||
highlight_end = rmr_kcal
|
||||
# Calculate ratio if we have all required parameters
|
||||
ratio = None
|
||||
if all([weight_kg, height_cm, age_years, sex]):
|
||||
# Mifflin-St Jeor equation
|
||||
if sex.lower() == "male":
|
||||
mifflin_rmr = 10 * weight_kg + 6.25 * height_cm - 5 * age_years + 5
|
||||
elif sex.lower() == "female":
|
||||
mifflin_rmr = 10 * weight_kg + 6.25 * height_cm - 5 * age_years - 161
|
||||
else:
|
||||
mifflin_rmr = None
|
||||
|
||||
# Main Bar (Background)
|
||||
main_bar = FancyBboxPatch(
|
||||
(0, 0.4),
|
||||
9000,
|
||||
0.2,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
ec="none",
|
||||
fc="#E0E0E0",
|
||||
if mifflin_rmr and mifflin_rmr > 0:
|
||||
ratio = rmr_kcal / mifflin_rmr
|
||||
|
||||
# Bar setup - using ratio scale from 0.3 to 1.9 (as in notebook)
|
||||
scale_edges = [0.3, 0.7, 0.9, 1.1, 1.3, 1.5, 1.9]
|
||||
scale_labels = ["Very Slow", "Slow", "Average", "Fast", "Very Fast"]
|
||||
tick_edges = scale_edges[1:-1] # Remove first and last tick (omit 0.3 and 1.9)
|
||||
|
||||
x_start = scale_edges[0]
|
||||
x_end = scale_edges[-1]
|
||||
# Make the bar THICKER by increasing bar_height and adjusting y_bar
|
||||
bar_height = 0.36
|
||||
y_bar = 0.48
|
||||
|
||||
color_before = "#B2FFC8"
|
||||
color_after = "#ECEDF2"
|
||||
gray_color = "#606060"
|
||||
|
||||
# If we have a ratio, use it; otherwise map rmr_kcal to the scale
|
||||
if ratio is not None:
|
||||
highlight_end = min(max(ratio, x_start), x_end)
|
||||
else:
|
||||
# Fallback: map rmr_kcal to scale (assuming typical range 1000-3000 kcal/day)
|
||||
# Map to 0.3-1.9 scale
|
||||
min_rmr = 1000
|
||||
max_rmr = 3000
|
||||
normalized = (rmr_kcal - min_rmr) / (max_rmr - min_rmr)
|
||||
highlight_end = x_start + normalized * (x_end - x_start)
|
||||
highlight_end = min(max(highlight_end, x_start), x_end)
|
||||
|
||||
# Draw plain rectangle bar (no rounding)
|
||||
ax.add_patch(
|
||||
Rectangle(
|
||||
(x_start, y_bar),
|
||||
x_end - x_start,
|
||||
bar_height,
|
||||
ec="none",
|
||||
fc=color_after,
|
||||
lw=0,
|
||||
)
|
||||
)
|
||||
ax.add_patch(main_bar)
|
||||
|
||||
# Highlighted Bar
|
||||
highlight_bar = FancyBboxPatch(
|
||||
(0, 0.4),
|
||||
highlight_end,
|
||||
0.2,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
ec="none",
|
||||
fc="#B2FFC8",
|
||||
)
|
||||
ax.add_patch(highlight_bar)
|
||||
# Highlighted rectangle
|
||||
if highlight_end > x_start:
|
||||
ax.add_patch(
|
||||
Rectangle(
|
||||
(x_start, y_bar),
|
||||
highlight_end - x_start,
|
||||
bar_height,
|
||||
ec="none",
|
||||
fc=color_before,
|
||||
lw=0,
|
||||
)
|
||||
)
|
||||
|
||||
# Text and Labels
|
||||
# kCals label, left-aligned, bold inside green, TEXT COLOR gray
|
||||
ax.text(
|
||||
highlight_end / 2,
|
||||
0.5,
|
||||
f"{rmr_kcal:.0f}kCals",
|
||||
ha="center",
|
||||
x_start + 0.07,
|
||||
y_bar + bar_height / 2,
|
||||
f"{int(round(rmr_kcal))}kCals",
|
||||
ha="left",
|
||||
va="center",
|
||||
color="#006400",
|
||||
color=gray_color,
|
||||
fontsize=12,
|
||||
weight="bold",
|
||||
bbox=dict(boxstyle="round,pad=0.14", ec="none", fc="#B2FFC8", alpha=1.0),
|
||||
)
|
||||
|
||||
# Triangle marker above highlight end, gray
|
||||
ax.plot(
|
||||
[highlight_end],
|
||||
[y_bar + bar_height + 0.08],
|
||||
marker="v",
|
||||
markersize=14,
|
||||
color=gray_color,
|
||||
clip_on=False,
|
||||
)
|
||||
|
||||
# Draw ticks – omit leftmost/rightmost (thicker and below bar), color gray
|
||||
tick_width = 4.1
|
||||
tick_bottom = y_bar - 0.07 # further below bar
|
||||
tick_top = y_bar # at the base of bar
|
||||
for edge in tick_edges:
|
||||
ax.plot(
|
||||
[edge, edge],
|
||||
[tick_bottom, tick_top],
|
||||
color=gray_color,
|
||||
lw=tick_width,
|
||||
solid_capstyle="butt",
|
||||
clip_on=False,
|
||||
zorder=2,
|
||||
)
|
||||
|
||||
# Label locations (place directly under each tick), text color gray
|
||||
label_y = tick_bottom - 0.08
|
||||
for label, tick in zip(scale_labels, tick_edges):
|
||||
ax.text(
|
||||
tick,
|
||||
label_y,
|
||||
label,
|
||||
ha="center",
|
||||
va="top",
|
||||
fontsize=11,
|
||||
weight="bold",
|
||||
color=gray_color,
|
||||
)
|
||||
|
||||
# Axis title: bold, with extra gap above the graph
|
||||
ax.text(
|
||||
x_start,
|
||||
y_bar + bar_height + 0.5,
|
||||
"Slow vs Fast Metabolism",
|
||||
ha="left",
|
||||
va="bottom",
|
||||
fontsize=14,
|
||||
weight="bold",
|
||||
)
|
||||
|
||||
# Indicator Triangle
|
||||
ax.plot(indicator_pos, 0.65, "v", markersize=15, color="#606060", clip_on=False)
|
||||
|
||||
# Ticks and Labels
|
||||
for pos, label in zip(positions, categories):
|
||||
ax.text(
|
||||
pos, 0.15, label, ha="center", va="center", fontsize=12, color="#333333"
|
||||
)
|
||||
ax.plot([pos, pos], [0.35, 0.39], color="grey", lw=5)
|
||||
|
||||
# Chart Styling
|
||||
ax.set_title("Slow vs Fast Metabolism", fontsize=18, weight="bold", loc="left")
|
||||
ax.set_xlim(0, 9000)
|
||||
ax.set_xlim(x_start, x_end)
|
||||
ax.set_ylim(0, 1)
|
||||
ax.axis("off")
|
||||
|
||||
@@ -1209,6 +1297,7 @@ class GraphGenerator:
|
||||
) -> str:
|
||||
"""
|
||||
Generate fuel source chart (Fats vs Carbs).
|
||||
Matches the notebook implementation with proper tick styling.
|
||||
|
||||
Args:
|
||||
fat_percentage: Fat percentage at rest
|
||||
@@ -1219,83 +1308,133 @@ class GraphGenerator:
|
||||
"""
|
||||
from matplotlib.patches import FancyBboxPatch
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 2.5))
|
||||
fig, ax = plt.subplots(figsize=(11.5, 2.5))
|
||||
|
||||
carb_percentage = 100 - fat_percentage
|
||||
optimal_point = 75
|
||||
|
||||
# Main Bars (Fats and Carbs)
|
||||
# Fats bar (yellow)
|
||||
# Let the bars be a bit thicker as well: increase bar height and y
|
||||
fats_bar = FancyBboxPatch(
|
||||
(0, 0.4),
|
||||
(0, 0.36),
|
||||
fat_percentage,
|
||||
0.2,
|
||||
0.28,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
ec="none",
|
||||
fc="#FEEAAB",
|
||||
)
|
||||
ax.add_patch(fats_bar)
|
||||
|
||||
# Carbs bar (blue) - starts where the fats bar ends
|
||||
carbs_bar = FancyBboxPatch(
|
||||
(fat_percentage, 0.4),
|
||||
(fat_percentage, 0.36),
|
||||
carb_percentage,
|
||||
0.2,
|
||||
0.28,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
ec="none",
|
||||
fc="#A7F5FF",
|
||||
)
|
||||
ax.add_patch(carbs_bar)
|
||||
|
||||
# Text and Labels
|
||||
# Style: match font weight/color/size with other chart
|
||||
label_fontprops = dict(fontsize=12, weight="bold", color="#333333")
|
||||
|
||||
ax.text(
|
||||
fat_percentage / 2,
|
||||
0.5,
|
||||
f"Fats\n{fat_percentage:.1f}%",
|
||||
f"Fats\n{fat_percentage:.0f}%",
|
||||
ha="center",
|
||||
va="center",
|
||||
color="#333333",
|
||||
fontsize=12,
|
||||
weight="bold",
|
||||
**label_fontprops,
|
||||
)
|
||||
ax.text(
|
||||
fat_percentage + carb_percentage / 2,
|
||||
0.5,
|
||||
f"Carbs\n{carb_percentage:.1f}%",
|
||||
f"Carbs\n{100 - fat_percentage:.0f}%",
|
||||
ha="center",
|
||||
va="center",
|
||||
color="#333333",
|
||||
fontsize=12,
|
||||
weight="bold",
|
||||
**label_fontprops,
|
||||
)
|
||||
|
||||
# Add 'Optimal' label
|
||||
ax.text(optimal_point, 0.75, "Optimal", ha="center", va="center", fontsize=12)
|
||||
|
||||
# Indicator Triangle
|
||||
ax.plot(
|
||||
fat_percentage, 0.65, "v", markersize=15, color="#606060", clip_on=False
|
||||
ax.text(
|
||||
optimal_point,
|
||||
0.9,
|
||||
"Optimal",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=12,
|
||||
weight="bold",
|
||||
color="#606060",
|
||||
)
|
||||
|
||||
# Ticks and Labels
|
||||
# Optimal point line
|
||||
ax.plot([optimal_point, optimal_point], [0.65, 0.8], color="#606060", lw=3)
|
||||
|
||||
# Indicator Triangle
|
||||
ax.plot(fat_percentage, 0.7, "v", markersize=15, color="#606060", clip_on=False)
|
||||
|
||||
# Ticks and Labels - matching notebook implementation
|
||||
positions = [0, 25, 50, 75, 100]
|
||||
tick_color = "#606060"
|
||||
for pos in positions:
|
||||
ax.text(
|
||||
pos,
|
||||
0.15,
|
||||
str(pos),
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=12,
|
||||
color="#333333",
|
||||
)
|
||||
ax.plot([pos, pos], [0.35, 0.39], color="grey", lw=5)
|
||||
# Smallest ticks (first and last) are thicker
|
||||
if pos == 0:
|
||||
ax.text(
|
||||
pos + 0.5,
|
||||
0.15,
|
||||
str(pos),
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=12,
|
||||
color="#333333",
|
||||
weight="bold",
|
||||
)
|
||||
ax.plot(
|
||||
[pos, pos],
|
||||
[0.25, 0.37],
|
||||
color=tick_color,
|
||||
lw=14,
|
||||
solid_capstyle="butt",
|
||||
)
|
||||
elif pos == 100:
|
||||
ax.text(
|
||||
pos - 0.5,
|
||||
0.15,
|
||||
str(pos),
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=12,
|
||||
color="#333333",
|
||||
weight="bold",
|
||||
)
|
||||
ax.plot(
|
||||
[pos, pos],
|
||||
[0.25, 0.37],
|
||||
color=tick_color,
|
||||
lw=14,
|
||||
solid_capstyle="butt",
|
||||
)
|
||||
else:
|
||||
ax.text(
|
||||
pos,
|
||||
0.15,
|
||||
str(pos),
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=12,
|
||||
color="#333333",
|
||||
weight="bold",
|
||||
)
|
||||
ax.plot(
|
||||
[pos, pos],
|
||||
[0.25, 0.37],
|
||||
color=tick_color,
|
||||
lw=8,
|
||||
solid_capstyle="butt",
|
||||
)
|
||||
|
||||
# Add a special tick for the 'Optimal' point
|
||||
ax.plot([optimal_point, optimal_point], [0.6, 0.7], color="black", lw=2)
|
||||
|
||||
# Chart Styling
|
||||
ax.set_title("Fuel Source", fontsize=18, weight="bold", loc="left")
|
||||
# Chart Styling - uniform style for title
|
||||
ax.set_title("Fuel Source", fontsize=14, weight="bold", loc="left", pad=22)
|
||||
ax.set_xlim(0, 100)
|
||||
ax.set_ylim(0, 1)
|
||||
ax.axis("off")
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ class ReportGeneratorService:
|
||||
}
|
||||
|
||||
def generate_html(
|
||||
self, patient_info: Dict[str, Any], contexts: Dict[str, Dict[str, Any]]
|
||||
self, patient_info: Dict[str, Any], contexts: Dict[str, Dict[str, Any]], report_type: str = "full"
|
||||
) -> str:
|
||||
"""
|
||||
Generate HTML content for the report.
|
||||
@@ -160,6 +160,7 @@ class ReportGeneratorService:
|
||||
patient_info: Dictionary containing patient information
|
||||
(patient_name, age, height, weight, focus)
|
||||
contexts: Dictionary with keys 'page_1', 'page_2', etc., each containing context data
|
||||
report_type: Type of report to generate ("full" or "minimal")
|
||||
|
||||
Returns:
|
||||
Complete HTML document as string
|
||||
@@ -175,8 +176,28 @@ class ReportGeneratorService:
|
||||
"focus": patient_info.get("focus", "Endurance"),
|
||||
}
|
||||
|
||||
# Get total number of pages
|
||||
num_pages = len(contexts)
|
||||
# Define page mappings for full vs minimal reports
|
||||
if report_type == "minimal":
|
||||
# Minimal report: pages 1, 2, 4, 5, 6, 16, 17, 19, 20
|
||||
# Map to minimal report pages 1-8
|
||||
# Page mapping: (original_page_num, template_name, minimal_page_num)
|
||||
page_mapping = [
|
||||
(1, "page_1.html", 1),
|
||||
(2, "page_2_minimal.html", 2),
|
||||
(4, "page_4.html", 3),
|
||||
(5, "page_5_minimal.html", 4),
|
||||
(6, "page_6.html", 5),
|
||||
(16, "page_16.html", 6),
|
||||
(17, "page_17_minimal.html", 7),
|
||||
(19, "page_19_20_minimal.html", 8), # Combined page
|
||||
]
|
||||
else:
|
||||
# Full report: all pages 1-20
|
||||
page_mapping = [
|
||||
(i, f"page_{i}.html", i) for i in range(1, 21)
|
||||
]
|
||||
|
||||
num_pages = len(page_mapping)
|
||||
|
||||
# Footer context
|
||||
footer_context = [
|
||||
@@ -198,13 +219,20 @@ class ReportGeneratorService:
|
||||
for context in footer_context
|
||||
]
|
||||
|
||||
# Render pages - iterate through pages in order
|
||||
for i in range(1, num_pages + 1):
|
||||
page_key = f"page_{i}"
|
||||
# Render pages based on mapping
|
||||
for idx, (original_page_num, template_name, minimal_page_num) in enumerate(page_mapping):
|
||||
# For combined page_19_20_minimal, use the combined context
|
||||
if template_name == "page_19_20_minimal.html":
|
||||
page_key = "page_19_20_minimal"
|
||||
else:
|
||||
page_key = f"page_{original_page_num}"
|
||||
context = contexts.get(page_key, {})
|
||||
template = self.env.get_template(f"page_{i}.html").render(context)
|
||||
template = self.env.get_template(template_name).render(context)
|
||||
|
||||
if i > 2:
|
||||
# Pages 1 and 2 don't have headers/footers in full report
|
||||
# In minimal report, only page 1 doesn't have header/footer
|
||||
page_num_in_report = minimal_page_num if report_type == "minimal" else original_page_num
|
||||
if page_num_in_report > 2:
|
||||
full_html = f"""
|
||||
<div class="page flex flex-col justify-between">
|
||||
<div>
|
||||
@@ -214,7 +242,7 @@ class ReportGeneratorService:
|
||||
{template}
|
||||
</main>
|
||||
<div class="border-t text-center text-sm text-gray-600">
|
||||
{footer_html_list[i - 1]}
|
||||
{footer_html_list[idx]}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
@@ -300,6 +328,7 @@ class ReportGeneratorService:
|
||||
output_filename: str = None,
|
||||
metric_overrides: Optional[Dict[str, Any]] = None,
|
||||
oxygenation_csv_path: Optional[str] = None,
|
||||
report_type: str = "full",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate complete medical report from uploaded files.
|
||||
@@ -495,9 +524,36 @@ class ReportGeneratorService:
|
||||
}
|
||||
rmr_metrics = temp_context_gen.calculate_rmr_and_fuel_source()
|
||||
|
||||
# Generate metabolism chart
|
||||
# Convert height to cm if available
|
||||
height_cm = None
|
||||
height_str = patient_info.get("height", "")
|
||||
if height_str:
|
||||
try:
|
||||
# Try to parse height string (e.g., "5'4"", "165cm", "165")
|
||||
import re
|
||||
# Check if it's in feet'inches" format
|
||||
feet_inches_match = re.match(r"(\d+)'(\d+)\"", height_str)
|
||||
if feet_inches_match:
|
||||
feet = int(feet_inches_match.group(1))
|
||||
inches = int(feet_inches_match.group(2))
|
||||
height_cm = (feet * 12 + inches) * 2.54
|
||||
# Check if it ends with cm
|
||||
elif "cm" in height_str.lower():
|
||||
height_cm = float(re.sub(r"[^\d.]", "", height_str))
|
||||
# Otherwise try to parse as number (assume cm)
|
||||
else:
|
||||
height_cm = float(re.sub(r"[^\d.]", "", height_str))
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
# Generate metabolism chart with ratio calculation if we have all parameters
|
||||
metabolism_chart_b64 = self.graph_generator.generate_metabolism_chart(
|
||||
rmr_metrics["rmr_kcal"], save_as_base64=True
|
||||
rmr_metrics["rmr_kcal"],
|
||||
weight_kg=weight_kg,
|
||||
height_cm=height_cm,
|
||||
age_years=patient_info.get("age", None),
|
||||
sex=gender,
|
||||
save_as_base64=True,
|
||||
)
|
||||
graphs_dict["metabolism_chart"] = metabolism_chart_b64
|
||||
|
||||
@@ -528,12 +584,14 @@ class ReportGeneratorService:
|
||||
"weight": weight_kg,
|
||||
"fat_percentage": fat_pct,
|
||||
"gender": gender,
|
||||
"next_testing_date": patient_info.get("next_testing_date", "Contact us for scheduling"),
|
||||
}
|
||||
contexts = self.context_generator.generate_all_contexts(
|
||||
patient_name,
|
||||
graphs_dict,
|
||||
metric_overrides=metric_overrides,
|
||||
graph_generator=self.graph_generator,
|
||||
report_type=report_type,
|
||||
)
|
||||
|
||||
# Step 5: Calculate analysis metrics
|
||||
@@ -541,7 +599,7 @@ class ReportGeneratorService:
|
||||
analysis_data["graphs_count"] = len(graphs_generated)
|
||||
|
||||
# Step 6: Generate HTML
|
||||
html_content = self.generate_html(patient_info, contexts)
|
||||
html_content = self.generate_html(patient_info, contexts, report_type=report_type)
|
||||
|
||||
# Step 7: Generate PDF
|
||||
if output_filename is None:
|
||||
|
||||
+162
-74
@@ -1,24 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Report Preview - Report Generator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% extends "base.html" %} {% block title %}Report Preview - Report Generator{%
|
||||
endblock %} {% block content %}
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
{% if not session.get('report_path') %}
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||
<p class="text-yellow-800">No report found. Please <a href="/" class="underline">upload files</a> first.</p>
|
||||
<p class="text-yellow-800">
|
||||
No report found. Please
|
||||
<a href="/" class="underline">upload files</a> first.
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Generated Report Preview</h2>
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
Generated Report Preview
|
||||
</h2>
|
||||
<div class="flex space-x-3">
|
||||
<a href="/edit" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<a
|
||||
href="/edit"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Edit Metrics
|
||||
</a>
|
||||
<a href="/download-report/{{ session.report_path.split('/')[-1] }}" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
|
||||
<a
|
||||
href="/download-report/{{ session.report_path.split('/')[-1] }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
Download PDF
|
||||
</a>
|
||||
</div>
|
||||
@@ -26,23 +34,33 @@
|
||||
|
||||
<!-- Patient Information -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Patient Information</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Patient Information
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Name</p>
|
||||
<p class="text-base font-medium text-gray-900">{{ session.patient_info['patient_name'] }}</p>
|
||||
<p class="text-base font-medium text-gray-900">
|
||||
{{ session.patient_info['patient_name'] }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Age</p>
|
||||
<p class="text-base font-medium text-gray-900">{{ session.patient_info['age'] }}</p>
|
||||
<p class="text-base font-medium text-gray-900">
|
||||
{{ session.patient_info['age'] }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Height</p>
|
||||
<p class="text-base font-medium text-gray-900">{{ session.patient_info['height'] }}</p>
|
||||
<p class="text-base font-medium text-gray-900">
|
||||
{{ session.patient_info['height'] }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Weight</p>
|
||||
<p class="text-base font-medium text-gray-900">{{ session.patient_info['weight'] }}</p>
|
||||
<p class="text-base font-medium text-gray-900">
|
||||
{{ session.patient_info['weight'] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,56 +70,113 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Pnoe Metrics -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Pnoe Metrics</h3>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Pnoe Metrics
|
||||
</h3>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
{% if session.metrics.pnoe.get('vo2_max') %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-500">VO2 Max</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['vo2_max']) }} ml/min</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
"%.2f"|format(session.metrics.pnoe['vo2_max'])
|
||||
}} ml/min
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if session.metrics.pnoe.get('vo2_max_per_kg') %}
|
||||
{% endif %} {% if
|
||||
session.metrics.pnoe.get('vo2_max_per_kg') %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-500">VO2 Max per kg</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['vo2_max_per_kg']) }} ml/min/kg</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
"%.2f"|format(session.metrics.pnoe['vo2_max_per_kg'])
|
||||
}} ml/min/kg
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if session.metrics.pnoe.get('peak_vt') %}
|
||||
{% endif %} {% if session.metrics.pnoe.get('peak_vt') %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-500">Peak VT</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['peak_vt']) }} L</p>
|
||||
<p class="text-sm text-gray-500 mt-1">HR: {{ "%.0f"|format(session.metrics.pnoe['peak_vt_hr']) }} bpm</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
"%.2f"|format(session.metrics.pnoe['peak_vt'])
|
||||
}} L
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
HR: {{
|
||||
"%.0f"|format(session.metrics.pnoe['peak_vt_hr'])
|
||||
}} bpm
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if session.metrics.pnoe.get('fat_max_value') %}
|
||||
{% endif %} {% if
|
||||
session.metrics.pnoe.get('fat_max_value') %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-500">Fat Max Value</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.pnoe['fat_max_value']) }} kcal/min</p>
|
||||
<p class="text-sm text-gray-500 mt-1">HR: {{ "%.0f"|format(session.metrics.pnoe['fat_max_hr']) }} bpm</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
"%.2f"|format(session.metrics.pnoe['fat_max_value'])
|
||||
}} kcal/min
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
HR: {{
|
||||
"%.0f"|format(session.metrics.pnoe['fat_max_hr'])
|
||||
}} bpm
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VT1 and VT2 -->
|
||||
{% if session.metrics.pnoe.get('vt1') or session.metrics.pnoe.get('vt2') %}
|
||||
{% if session.metrics.pnoe.get('vt1') or
|
||||
session.metrics.pnoe.get('vt2') %}
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Ventilatory Thresholds</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Ventilatory Thresholds
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{% if session.metrics.pnoe.get('vt1') %}
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-blue-900 mb-2">VT1</p>
|
||||
<p class="text-sm text-blue-700">Heart Rate: {{ "%.0f"|format(session.metrics.pnoe['vt1']['HeartRate']) }} bpm</p>
|
||||
<p class="text-sm text-blue-700">Speed: {{ "%.2f"|format(session.metrics.pnoe['vt1']['Speed']) }} mph</p>
|
||||
<p class="text-sm text-blue-700">Time: {{ "%.0f"|format(session.metrics.pnoe['vt1']['Time']) }} sec</p>
|
||||
<p class="text-sm font-medium text-blue-900 mb-2">
|
||||
VT1
|
||||
</p>
|
||||
<p class="text-sm text-blue-700">
|
||||
Heart Rate: {{
|
||||
"%.0f"|format(session.metrics.pnoe['vt1']['HeartRate'])
|
||||
}} bpm
|
||||
</p>
|
||||
<p class="text-sm text-blue-700">
|
||||
Speed: {{
|
||||
"%.2f"|format(session.metrics.pnoe['vt1']['Speed'])
|
||||
}} mph
|
||||
</p>
|
||||
<p class="text-sm text-blue-700">
|
||||
Time: {{
|
||||
"%.0f"|format(session.metrics.pnoe['vt1']['Time'])
|
||||
}} sec
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if session.metrics.pnoe.get('vt2') %}
|
||||
{% endif %} {% if session.metrics.pnoe.get('vt2') %}
|
||||
<div class="bg-green-50 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-green-900 mb-2">VT2</p>
|
||||
<p class="text-sm text-green-700">Heart Rate: {{ "%.0f"|format(session.metrics.pnoe['vt2']['HeartRate']) }} bpm</p>
|
||||
<p class="text-sm text-green-700">Speed: {{ "%.2f"|format(session.metrics.pnoe['vt2']['Speed']) }} mph</p>
|
||||
<p class="text-sm text-green-700">Time: {{ "%.0f"|format(session.metrics.pnoe['vt2']['Time']) }} sec</p>
|
||||
<p class="text-sm font-medium text-green-900 mb-2">
|
||||
VT2
|
||||
</p>
|
||||
<p class="text-sm text-green-700">
|
||||
Heart Rate: {{
|
||||
"%.0f"|format(session.metrics.pnoe['vt2']['HeartRate'])
|
||||
}} bpm
|
||||
</p>
|
||||
<p class="text-sm text-green-700">
|
||||
Speed: {{
|
||||
"%.2f"|format(session.metrics.pnoe['vt2']['Speed'])
|
||||
}} mph
|
||||
</p>
|
||||
<p class="text-sm text-green-700">
|
||||
Time: {{
|
||||
"%.0f"|format(session.metrics.pnoe['vt2']['Time'])
|
||||
}} sec
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -111,17 +186,20 @@
|
||||
<!-- Heart Rate Zones -->
|
||||
{% if session.metrics.pnoe.get('zone1_bpm') %}
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Heart Rate Zones</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Heart Rate Zones
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-5">
|
||||
{% for i in range(1, 6) %}
|
||||
{% set zone_key = "zone" + i|string + "_bpm" %}
|
||||
{% if session.metrics.pnoe.get(zone_key) %}
|
||||
{% for i in range(1, 6) %} {% set zone_key = "zone" +
|
||||
i|string + "_bpm" %} {% if
|
||||
session.metrics.pnoe.get(zone_key) %}
|
||||
<div class="bg-gray-50 p-3 rounded-lg text-center">
|
||||
<p class="text-xs text-gray-500">Zone {{ i }}</p>
|
||||
<p class="text-sm font-medium text-gray-900">{{ session.metrics.pnoe[zone_key] }}</p>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
{{ session.metrics.pnoe[zone_key] }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %} {% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -129,27 +207,53 @@
|
||||
<!-- Spirometry Metrics -->
|
||||
{% if session.metrics.spirometry %}
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Spirometry Metrics</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Spirometry Metrics
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{% if session.metrics.spirometry.get('fvc_best') %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-500">FVC Best</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.spirometry['fvc_best']) }} L</p>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ "%.1f"|format(session.metrics.spirometry['fvc_pred']) }}% predicted</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
"%.2f"|format(session.metrics.spirometry['fvc_best'])
|
||||
}} L
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{
|
||||
"%.1f"|format(session.metrics.spirometry['fvc_pred'])
|
||||
}}% predicted
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if session.metrics.spirometry.get('fev1_best') %}
|
||||
{% endif %} {% if
|
||||
session.metrics.spirometry.get('fev1_best') %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-500">FEV1 Best</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.spirometry['fev1_best']) }} L</p>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ "%.1f"|format(session.metrics.spirometry['fev1_pred']) }}% predicted</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
"%.2f"|format(session.metrics.spirometry['fev1_best'])
|
||||
}} L
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{
|
||||
"%.1f"|format(session.metrics.spirometry['fev1_pred'])
|
||||
}}% predicted
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if session.metrics.spirometry.get('fev1_fvc_pct_best') %}
|
||||
{% endif %} {% if
|
||||
session.metrics.spirometry.get('fev1_fvc_pct_best') %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-500">FEV1/FVC%</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(session.metrics.spirometry['fev1_fvc_pct_best']) }}%</p>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ "%.1f"|format(session.metrics.spirometry['fev1_fvc_pct_pred']) }}% predicted</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
"%.2f"|format(session.metrics.spirometry['fev1_fvc_pct_best'])
|
||||
}}%
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{
|
||||
"%.1f"|format(session.metrics.spirometry['fev1_fvc_pct_pred'])
|
||||
}}% predicted
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -157,24 +261,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Graphs Section -->
|
||||
{% if session.graphs_generated %}
|
||||
<div class="mt-8">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Generated Graphs</h3>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{% for graph in session.graphs_generated %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-700 mb-2">{{ graph.name|replace('_', ' ')|title }}</p>
|
||||
<img src="/graphs/{{ graph.path.split('/')[-1] }}" alt="{{ graph.name }}" class="w-full h-auto rounded">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
+314
-55
@@ -1,12 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Upload Patient Data - Report Generator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% extends "base.html" %} {% block title %}Upload Patient Data - Report
|
||||
Generator{% endblock %} {% block content %}
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Upload Patient Data and Files</h2>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">
|
||||
Upload Patient Data and Files
|
||||
</h2>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||
@@ -14,42 +13,105 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/upload" method="post" enctype="multipart/form-data" class="space-y-6">
|
||||
<form
|
||||
id="upload-form"
|
||||
action="/upload"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="space-y-6"
|
||||
>
|
||||
<!-- Patient Information Section -->
|
||||
<div class="border-b border-gray-200 pb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Patient Information</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Patient Information
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="first_name" class="block text-sm font-medium text-gray-700">First Name</label>
|
||||
<input type="text" name="first_name" id="first_name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
</div>
|
||||
<div>
|
||||
<label for="last_name" class="block text-sm font-medium text-gray-700">Last Name</label>
|
||||
<input type="text" name="last_name" id="last_name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
</div>
|
||||
<div>
|
||||
<label for="age" class="block text-sm font-medium text-gray-700">Age</label>
|
||||
<input type="number" name="age" id="age" required min="1" max="120"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
</div>
|
||||
<div>
|
||||
<label for="height" class="block text-sm font-medium text-gray-700">Height (e.g., 5'4" or 165cm)</label>
|
||||
<input type="text" name="height" id="height" required
|
||||
<label
|
||||
for="first_name"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>First Name</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
id="first_name"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
placeholder="5'4"">
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="weight" class="block text-sm font-medium text-gray-700">Weight (e.g., 123lbs or 56kg)</label>
|
||||
<input type="text" name="weight" id="weight" required
|
||||
<label
|
||||
for="last_name"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Last Name</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
id="last_name"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
placeholder="123lbs">
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="gender" class="block text-sm font-medium text-gray-700">Gender</label>
|
||||
<select name="gender" id="gender" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
<label
|
||||
for="age"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Age</label
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
name="age"
|
||||
id="age"
|
||||
required
|
||||
min="1"
|
||||
max="120"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="height"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Height (e.g., 5'4" or 165cm)</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="height"
|
||||
id="height"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
placeholder="5'4""
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="weight"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Weight (e.g., 123lbs or 56kg)</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="weight"
|
||||
id="weight"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
placeholder="123lbs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="gender"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Gender</label
|
||||
>
|
||||
<select
|
||||
name="gender"
|
||||
id="gender"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
@@ -57,51 +119,249 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="focus" class="block text-sm font-medium text-gray-700">Training Focus</label>
|
||||
<input type="text" name="focus" id="focus" value="Endurance"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
<label
|
||||
for="focus"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Training Focus</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="focus"
|
||||
id="focus"
|
||||
value="Endurance"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="session_id" class="block text-sm font-medium text-gray-700">Session ID</label>
|
||||
<input type="text" name="session_id" id="session_id" value="default"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700 mb-2"
|
||||
>Report Type</label
|
||||
>
|
||||
<div class="mt-1 space-y-2">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="report_type"
|
||||
id="report_type_full"
|
||||
value="full"
|
||||
checked
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
/>
|
||||
<label
|
||||
for="report_type_full"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
Full Report
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="report_type"
|
||||
id="report_type_minimal"
|
||||
value="minimal"
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
/>
|
||||
<label
|
||||
for="report_type_minimal"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
Minimal Report
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Recommended Next Testing Date</label
|
||||
>
|
||||
<div class="mt-1 grid grid-cols-2 gap-3">
|
||||
<select
|
||||
id="next_testing_month"
|
||||
required
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
>
|
||||
<option value="">Month</option>
|
||||
<option value="January">January</option>
|
||||
<option value="February">February</option>
|
||||
<option value="March">March</option>
|
||||
<option value="April">April</option>
|
||||
<option value="May">May</option>
|
||||
<option value="June">June</option>
|
||||
<option value="July">July</option>
|
||||
<option value="August">August</option>
|
||||
<option value="September">September</option>
|
||||
<option value="October">October</option>
|
||||
<option value="November">November</option>
|
||||
<option value="December">December</option>
|
||||
</select>
|
||||
<select
|
||||
id="next_testing_year"
|
||||
required
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
>
|
||||
<option value="">Year</option>
|
||||
</select>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
name="next_testing_date"
|
||||
id="next_testing_date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<script>
|
||||
// Populate year dropdown
|
||||
(function () {
|
||||
const yearSelect =
|
||||
document.getElementById(
|
||||
"next_testing_year"
|
||||
);
|
||||
const currentYear = new Date().getFullYear();
|
||||
for (
|
||||
let i = currentYear;
|
||||
i <= currentYear + 10;
|
||||
i++
|
||||
) {
|
||||
const option =
|
||||
document.createElement("option");
|
||||
option.value = i;
|
||||
option.textContent = i;
|
||||
yearSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Combine month and year into hidden input
|
||||
const monthSelect =
|
||||
document.getElementById(
|
||||
"next_testing_month"
|
||||
);
|
||||
const dateInput =
|
||||
document.getElementById(
|
||||
"next_testing_date"
|
||||
);
|
||||
|
||||
function updateDateInput() {
|
||||
const month = monthSelect.value;
|
||||
const year = yearSelect.value;
|
||||
if (month && year) {
|
||||
dateInput.value = month + " " + year;
|
||||
} else {
|
||||
dateInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
monthSelect.addEventListener(
|
||||
"change",
|
||||
updateDateInput
|
||||
);
|
||||
yearSelect.addEventListener(
|
||||
"change",
|
||||
updateDateInput
|
||||
);
|
||||
|
||||
// Validate form submission
|
||||
const form =
|
||||
document.getElementById("upload-form");
|
||||
form.addEventListener("submit", function (e) {
|
||||
const month = monthSelect.value;
|
||||
const year = yearSelect.value;
|
||||
if (!month || !year) {
|
||||
e.preventDefault();
|
||||
alert(
|
||||
"Please select both month and year for the recommended next testing date."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Ensure hidden input is set before submission
|
||||
updateDateInput();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Section -->
|
||||
<div class="border-b border-gray-200 pb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Upload Files</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Upload Files
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="spirometry_pdf" class="block text-sm font-medium text-gray-700">Spirometry PDF</label>
|
||||
<input type="file" name="spirometry_pdf" id="spirometry_pdf" accept=".pdf" required
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
|
||||
<label
|
||||
for="spirometry_pdf"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Spirometry PDF</label
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="spirometry_pdf"
|
||||
id="spirometry_pdf"
|
||||
accept=".pdf"
|
||||
required
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pnoe_csv" class="block text-sm font-medium text-gray-700">Pnoe CSV</label>
|
||||
<input type="file" name="pnoe_csv" id="pnoe_csv" accept=".csv" required
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
|
||||
<label
|
||||
for="pnoe_csv"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Pnoe CSV</label
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="pnoe_csv"
|
||||
id="pnoe_csv"
|
||||
accept=".csv"
|
||||
required
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="fat_percentage" class="block text-sm font-medium text-gray-700">Body Fat Percentage (%)</label>
|
||||
<input type="number" step="0.1" name="fat_percentage" id="fat_percentage" required min="0" max="100"
|
||||
<label
|
||||
for="fat_percentage"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Body Fat Percentage (%)</label
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
name="fat_percentage"
|
||||
id="fat_percentage"
|
||||
required
|
||||
min="0"
|
||||
max="100"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border"
|
||||
placeholder="22.5">
|
||||
placeholder="22.5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="oxygenation_csv" class="block text-sm font-medium text-gray-700">Muscle Oxygenation CSV (Optional)</label>
|
||||
<input type="file" name="oxygenation_csv" id="oxygenation_csv" accept=".csv"
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
|
||||
<p class="mt-1 text-xs text-gray-500">Upload NIRS muscle oxygen CSV file to generate TSI graph</p>
|
||||
<label
|
||||
for="oxygenation_csv"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>Muscle Oxygenation CSV (Optional)</label
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="oxygenation_csv"
|
||||
id="oxygenation_csv"
|
||||
accept=".csv"
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Upload NIRS muscle oxygen CSV file to generate
|
||||
TSI graph
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
Generate Report
|
||||
</button>
|
||||
</div>
|
||||
@@ -110,4 +370,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
+22648
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+23
-20
@@ -2,7 +2,7 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 1,
|
||||
"id": "b18c1027",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 3,
|
||||
"id": "56a9d655",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -104,7 +104,10 @@
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"spirometry_df = pd.read_csv(\"data/spirometry_data.csv\")\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"base_dir = os.path.dirname(os.path.abspath('.'))\n",
|
||||
"spirometry_df = pd.read_csv(f\"{base_dir}/data/spirometry_data.csv\")\n",
|
||||
"\n",
|
||||
"fvc_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', 'Best'].values[0]\n",
|
||||
"fvc_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', '%Pred.'].values[0]\n",
|
||||
@@ -122,7 +125,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 4,
|
||||
"id": "990f4b4f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -136,7 +139,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"df = pd.read_csv('data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
|
||||
"df = pd.read_csv(f'{base_dir}/data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
|
||||
"peak_vt = df['VT(l)'].max()\n",
|
||||
"max_vt_row = df.loc[df['VT(l)'].idxmax()]\n",
|
||||
"print(f\"Peak VT: {peak_vt}\")\n",
|
||||
@@ -146,7 +149,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 19,
|
||||
"id": "041cbc3d",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -154,21 +157,21 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Peak VT: 2.3770000000000002\n",
|
||||
"HR at Peak VT: 171.525\n"
|
||||
"Peak VT: 2.3844444444444446\n",
|
||||
"HR at Peak VT: 172.80555555555554\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/tmp/ipykernel_69398/4157056299.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
|
||||
"/tmp/ipykernel_53922/361246798.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
|
||||
" df = df.apply(pd.to_numeric, errors='ignore')\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"df = pd.read_csv('data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
|
||||
"df = pd.read_csv(f'{base_dir}/data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
|
||||
"# Convert all columns to numeric where possible, coercing errors to NaN\n",
|
||||
"df = df.apply(pd.to_numeric, errors='ignore')\n",
|
||||
"df['VO2 Pulse'] = df['VO2(ml/min)'] / df['HR(bpm)'] # VO2 Pulse in mL/beat\n",
|
||||
@@ -176,7 +179,7 @@
|
||||
"df['CHO'] = df['EE(kcal/min)'] * df['CARBS(%)']/100\n",
|
||||
"df['FAT'] = df['EE(kcal/min)'] * df['FAT(%)']/100\n",
|
||||
"# Smooth key columns using rolling window\n",
|
||||
"window_size = 10\n",
|
||||
"window_size = 9\n",
|
||||
"\n",
|
||||
"# List of columns to smooth\n",
|
||||
"columns_to_smooth = ['VO2(ml/min)', 'VCO2(ml/min)', 'HR(bpm)', 'VT(l)', 'BF(bpm)', 'VE(l/min)', 'VO2 Pulse', 'VO2 Breath', 'CHO', 'FAT']\n",
|
||||
@@ -195,7 +198,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 20,
|
||||
"id": "de7cadd1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -203,7 +206,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Percent FEV: 72.91411042944786\n"
|
||||
"Percent FEV: 73.14246762099523\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -214,7 +217,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 21,
|
||||
"id": "cb972ed3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -311,13 +314,13 @@
|
||||
"[1 rows x 147 columns]"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"execution_count": 21,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"personal_df = pd.read_excel('data/SECA body comp for all patients.xlsx')\n",
|
||||
"personal_df = pd.read_excel(f'{base_dir}/data/SECA body comp for all patients.xlsx')\n",
|
||||
"\n",
|
||||
"keirstyn_data = personal_df[personal_df['LastName'].str.contains('Moran', case=False, na=False)]\n",
|
||||
"keirstyn_data"
|
||||
@@ -325,7 +328,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 22,
|
||||
"id": "98d9295a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -333,7 +336,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"VO2 Max: 47.906290322580645\n"
|
||||
"VO2 Max: 48.19062126642772\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -823,7 +826,7 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "report_generation",
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
@@ -837,7 +840,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
"version": "3.12.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def mifflin_st_jeor(weight_kg, height_cm, age_years, sex):
|
||||
"""
|
||||
Compute predicted RMR with Mifflin St Jeor.
|
||||
sex: 'male' or 'female'
|
||||
"""
|
||||
base = 10.0 * weight_kg + 6.25 * height_cm - 5.0 * age_years
|
||||
if sex.lower().startswith("m"):
|
||||
return base + 5.0
|
||||
else:
|
||||
return base - 161.0
|
||||
|
||||
|
||||
def classify_metabolism(measured_kcal_day, predicted_kcal_day):
|
||||
"""
|
||||
Classify metabolic rate relative to prediction.
|
||||
Returns (label, ratio).
|
||||
"""
|
||||
ratio = measured_kcal_day / predicted_kcal_day
|
||||
|
||||
if ratio < 0.70:
|
||||
label = "very slow"
|
||||
elif ratio < 0.90:
|
||||
label = "slow"
|
||||
elif ratio <= 1.10:
|
||||
label = "average"
|
||||
elif ratio <= 1.30:
|
||||
label = "fast"
|
||||
else:
|
||||
label = "very fast"
|
||||
|
||||
return label, ratio
|
||||
|
||||
|
||||
def find_sampling_window(df):
|
||||
"""
|
||||
Derive number of samples that represent about 2 minutes.
|
||||
"""
|
||||
dt = df["T(sec)"].diff().median()
|
||||
if dt is None or dt <= 0:
|
||||
raise ValueError("Invalid time step in T(sec)")
|
||||
|
||||
samples = int(round(120.0 / dt))
|
||||
if samples < 1:
|
||||
samples = 1
|
||||
return samples
|
||||
|
||||
|
||||
def rolling_stable_window(df, window_samples):
|
||||
"""
|
||||
Find the most stable 2-minute window using rolling standard deviation.
|
||||
Returns:
|
||||
means_series, t_start, t_end
|
||||
"""
|
||||
cols_mean = [
|
||||
"VO2(ml/min)",
|
||||
"VCO2(ml/min)",
|
||||
"VE(l/min)",
|
||||
"VT(l)",
|
||||
"BF(bpm)",
|
||||
"EE(kcal/min)",
|
||||
"RER",
|
||||
"CARBS(%)",
|
||||
"FAT(%)",
|
||||
]
|
||||
|
||||
cols_std = [
|
||||
"VO2(ml/min)",
|
||||
"VCO2(ml/min)",
|
||||
"VE(l/min)",
|
||||
"VT(l)",
|
||||
"BF(bpm)",
|
||||
]
|
||||
|
||||
roll_mean = df[cols_mean].rolling(window_samples, min_periods=window_samples).mean()
|
||||
roll_std = df[cols_std].rolling(window_samples, min_periods=window_samples).std()
|
||||
|
||||
# Sum std devs to get stability score; use skipna=False to preserve NaN for incomplete windows
|
||||
stability_score = roll_std.sum(axis=1, skipna=False)
|
||||
|
||||
# Find index with lowest stability score (dropna to ignore incomplete windows)
|
||||
best_idx = stability_score.dropna().idxmin()
|
||||
|
||||
means_series = roll_mean.loc[best_idx].copy()
|
||||
|
||||
start_idx = max(best_idx - window_samples + 1, 0)
|
||||
end_idx = best_idx
|
||||
|
||||
t_start = float(df["T(sec)"].iloc[start_idx])
|
||||
t_end = float(df["T(sec)"].iloc[end_idx])
|
||||
|
||||
return means_series, t_start, t_end
|
||||
|
||||
|
||||
def manual_window_means(df, t_start, t_end):
|
||||
"""
|
||||
Compute mean values inside a user-selected time window.
|
||||
"""
|
||||
mask = (df["T(sec)"] >= t_start) & (df["T(sec)"] <= t_end)
|
||||
slice_df = df.loc[mask].copy()
|
||||
|
||||
if slice_df.empty:
|
||||
raise ValueError("Manual window has no rows inside T(sec) range")
|
||||
|
||||
cols = [
|
||||
"VO2(ml/min)",
|
||||
"VCO2(ml/min)",
|
||||
"VE(l/min)",
|
||||
"VT(l)",
|
||||
"BF(bpm)",
|
||||
"EE(kcal/min)",
|
||||
"RER",
|
||||
"CARBS(%)",
|
||||
"FAT(%)",
|
||||
]
|
||||
|
||||
means = slice_df[cols].mean()
|
||||
return means, float(t_start), float(t_end)
|
||||
|
||||
|
||||
def load_pnoe_csv(path):
|
||||
"""
|
||||
Load and clean a PNOE CSV file.
|
||||
"""
|
||||
df = pd.read_csv(path, sep=";")
|
||||
|
||||
numeric_cols = [
|
||||
"T(sec)",
|
||||
"VO2(ml/min)",
|
||||
"VCO2(ml/min)",
|
||||
"RER",
|
||||
"VE(l/min)",
|
||||
"VT(l)",
|
||||
"BF(bpm)",
|
||||
"EE(kcal/min)",
|
||||
"CARBS(%)",
|
||||
"FAT(%)",
|
||||
]
|
||||
|
||||
for col in numeric_cols:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
|
||||
df = df.dropna(subset=["VO2(ml/min)", "EE(kcal/min)"]).reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
def analyze_pnoe_rmr(
|
||||
path,
|
||||
weight_kg,
|
||||
height_cm,
|
||||
age_years,
|
||||
sex,
|
||||
subject_name=None,
|
||||
test_date=None,
|
||||
manual_window=None,
|
||||
):
|
||||
"""
|
||||
Analyze resting RMR from a PNOE CSV file.
|
||||
|
||||
manual_window:
|
||||
None for automatic stable window
|
||||
or (t_start_sec, t_end_sec) for user-chosen window
|
||||
"""
|
||||
df = load_pnoe_csv(path)
|
||||
window_samples = find_sampling_window(df)
|
||||
|
||||
# Automatic stable window
|
||||
auto_means, auto_t_start, auto_t_end = rolling_stable_window(df, window_samples)
|
||||
|
||||
# Manual override if provided
|
||||
manual_means = None
|
||||
manual_t_start = None
|
||||
manual_t_end = None
|
||||
|
||||
if manual_window is not None:
|
||||
t_start_manual, t_end_manual = manual_window
|
||||
manual_means, manual_t_start, manual_t_end = manual_window_means(
|
||||
df, t_start_manual, t_end_manual
|
||||
)
|
||||
chosen_source = "manual"
|
||||
chosen_means = manual_means
|
||||
chosen_t_start = manual_t_start
|
||||
chosen_t_end = manual_t_end
|
||||
else:
|
||||
chosen_source = "auto"
|
||||
chosen_means = auto_means
|
||||
chosen_t_start = auto_t_start
|
||||
chosen_t_end = auto_t_end
|
||||
|
||||
kcal_per_min = float(chosen_means["EE(kcal/min)"])
|
||||
rmr_kcal_day = kcal_per_min * 1440.0
|
||||
|
||||
predicted_kcal_day = mifflin_st_jeor(weight_kg, height_cm, age_years, sex)
|
||||
label, ratio = classify_metabolism(rmr_kcal_day, predicted_kcal_day)
|
||||
|
||||
def pack_metrics(prefix, means, t_start, t_end):
|
||||
if means is None:
|
||||
return {}
|
||||
return {
|
||||
f"{prefix}_window_start_sec": t_start,
|
||||
f"{prefix}_window_end_sec": t_end,
|
||||
f"{prefix}_VO2_L_min": float(means["VO2(ml/min)"]) / 1000.0,
|
||||
f"{prefix}_VCO2_L_min": float(means["VCO2(ml/min)"]) / 1000.0,
|
||||
f"{prefix}_VE_L_min": float(means["VE(l/min)"]),
|
||||
f"{prefix}_VT_L": float(means["VT(l)"]),
|
||||
f"{prefix}_BF_bpm": float(means["BF(bpm)"]),
|
||||
f"{prefix}_RER": float(means["RER"]),
|
||||
f"{prefix}_Fat_percent": float(means["FAT(%)"]),
|
||||
f"{prefix}_Carb_percent": float(means["CARBS(%)"]),
|
||||
f"{prefix}_kcal_per_min": float(means["EE(kcal/min)"]),
|
||||
}
|
||||
|
||||
result = {
|
||||
"subject_name": subject_name,
|
||||
"test_date": test_date,
|
||||
"sex": sex,
|
||||
"weight_kg": weight_kg,
|
||||
"height_cm": height_cm,
|
||||
"age_years": age_years,
|
||||
"chosen_window_source": chosen_source,
|
||||
"chosen_window_start_sec": chosen_t_start,
|
||||
"chosen_window_end_sec": chosen_t_end,
|
||||
"RMR_kcal_day": rmr_kcal_day,
|
||||
"Mifflin_kcal_day": predicted_kcal_day,
|
||||
"Measured_to_Mifflin_ratio": ratio,
|
||||
"Metabolic_classification": label,
|
||||
}
|
||||
|
||||
result.update(pack_metrics("auto", auto_means, auto_t_start, auto_t_end))
|
||||
result.update(pack_metrics("manual", manual_means, manual_t_start, manual_t_end))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
result = analyze_pnoe_rmr(
|
||||
path="/home/oluwasanmi/Documents/Work/MKD/report_generation/data/Pnoe_20250729_1550-Moran_Keirstyn.csv",
|
||||
weight_kg=56,
|
||||
height_cm=162,
|
||||
age_years=34,
|
||||
sex="female",
|
||||
subject_name="Cullen Pacas",
|
||||
test_date="2025-11-12",
|
||||
manual_window=None, # or (t_start_sec, t_end_sec)
|
||||
)
|
||||
|
||||
for key, value in result.items():
|
||||
print(f"{key}: {value}")
|
||||
+214
-137
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user