From 4406b2013d9f71839d97d965ffb22c0745da0dc5 Mon Sep 17 00:00:00 2001 From: bolade Date: Wed, 26 Nov 2025 22:17:30 +0100 Subject: [PATCH] added minimal report --- app/main.py | 6 + app/report_gen/page_17_minimal.html | 35 + app/report_gen/page_19_20_minimal.html | 470 + app/report_gen/page_2_minimal.html | 85 + app/report_gen/page_5_minimal.html | 114 + .../context_generator.cpython-312.pyc | Bin 47507 -> 51847 bytes .../report_generator.cpython-312.pyc | Bin 23058 -> 24001 bytes app/services/context_generator.py | 270 +- app/services/report_generator.py | 50 +- app/templates/upload.html | 39 + minimal_report.pdf | 22648 ++++++++++++++++ notebooks/graphs.ipynb | 2 +- 12 files changed, 23639 insertions(+), 80 deletions(-) create mode 100644 app/report_gen/page_17_minimal.html create mode 100644 app/report_gen/page_19_20_minimal.html create mode 100644 app/report_gen/page_2_minimal.html create mode 100644 app/report_gen/page_5_minimal.html create mode 100644 minimal_report.pdf diff --git a/app/main.py b/app/main.py index d35bcb8..67acd05 100644 --- a/app/main.py +++ b/app/main.py @@ -111,6 +111,7 @@ async def upload_files( 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), @@ -199,12 +200,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"] @@ -408,6 +411,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), @@ -417,6 +422,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 diff --git a/app/report_gen/page_17_minimal.html b/app/report_gen/page_17_minimal.html new file mode 100644 index 0000000..7390967 --- /dev/null +++ b/app/report_gen/page_17_minimal.html @@ -0,0 +1,35 @@ +
+ + +
+ +

Glossary

+ + +
+

Body Fat Percentage:

+

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.

+
+ + +
+

Metabolic Rate:

+

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.

+
+ + +
+

Fuel Source:

+

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.

+

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.

+
+ + +
+

NEAT (Non-Exercise Activity Thermogenesis)

+

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.

+
+
+ +
+ diff --git a/app/report_gen/page_19_20_minimal.html b/app/report_gen/page_19_20_minimal.html new file mode 100644 index 0000000..80d87a5 --- /dev/null +++ b/app/report_gen/page_19_20_minimal.html @@ -0,0 +1,470 @@ +
+ +
+ +
+

+ Body Fat Percent Master Chart +

+
+ Body Fat Percentage +
+
+ + +
+

+ Resting Heart Rate +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Age (M) + + Poor + + Below Average + + Average + + Above Average + + Good + + Excellent + + Athlete +
+ 18-25 + + 85bpm + + + 79-84bpm + + 74-78bpm + + 70-73bpm + + 66-69bpm + + 61-65bpm + + 40-60bpm +
+ 26-35 + + 83bpm + + + 77-82bpm + + 73-76bpm + + 69-72bpm + + 65-68bpm + + 60-64bpm + + 42-59bpm +
+ 36-45 + + 85bpm + + + 79-84bpm + + 74-78bpm + + 70-73bpm + + 65-69bpm + + 60-64bpm + + 45-59bpm +
+ 46-55 + + 84bpm + + + 78-83bpm + + 74-77bpm + + 70-73bpm + + 66-69bpm + + 61-65bpm + + 48-60bpm +
+ 56-65 + + 84bpm + + + 78-83bpm + + 74-77bpm + + 70-73bpm + + 65-69bpm + + 60-64bpm + + 50-59bpm +
+ 65+ + + 84bpm + + + 77-83bpm + + 73-76bpm + + 70-73bpm + + 65-69bpm + + 60-64bpm + + 52-59bpm +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Age (F) + + Poor + + Below Average + + Average + + Above Average + + Good + + Excellent + + Athlete +
+ 18-25 + + 82bpm + + + 74-81bpm + + 70-73bpm + + 66-69bpm + + 62-65bpm + + 56-61bpm + + 40-55bpm +
+ 26-35 + + 82bpm + + + 75-81bpm + + 71-74bpm + + 66-70bpm + + 62-65bpm + + 55-61bpm + + 44-54bpm +
+ 36-45 + + 83bpm + + + 76-82bpm + + 71-75bpm + + 67-70bpm + + 63-66bpm + + 57-62bpm + + 47-56bpm +
+ 46-55 + + 84bpm + + + 77-83bpm + + 72-76bpm + + 68-71bpm + + 64-67bpm + + 58-63bpm + + 49-57bpm +
+ 56-65 + + 82bpm + + + 76-81bpm + + 72-75bpm + + 68-71bpm + + 62-67bpm + + 57-61bpm + + 51-56bpm +
+ 65+ + + 80bpm + + + 74-79bpm + + 70-73bpm + + 66-69bpm + + 62-65bpm + + 56-61bpm + + 52-55bpm +
+
+
+
+
+ diff --git a/app/report_gen/page_2_minimal.html b/app/report_gen/page_2_minimal.html new file mode 100644 index 0000000..c693707 --- /dev/null +++ b/app/report_gen/page_2_minimal.html @@ -0,0 +1,85 @@ +
+
+ +
+

+ TABLE OF CONTENTS +

+
+
+ + +
+ +
+
+ 3 +
+
+

+ Nutrition Guidelines +

+

+ Ultrasound & Body Composition +

+

+ Resting Metabolic Rate Assessment +

+
+
+ + +
+
+ 4 +
+
+

+ Nutrition Recommendations +

+
+
+ + +
+
+ 5 +
+
+

+ Next Steps +

+
+ +
+
+
+ + +
+
+ 6 +
+
+

+ Glossary +

+
+ +
+
+
+
+
+
+ diff --git a/app/report_gen/page_5_minimal.html b/app/report_gen/page_5_minimal.html new file mode 100644 index 0000000..4311a02 --- /dev/null +++ b/app/report_gen/page_5_minimal.html @@ -0,0 +1,114 @@ +
+ + + +
+ +

Nutrition Guidelines

+ + +

+ Resting Metabolic Rate Assessment +

+

+ 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. +

+ + +
+
+ Slow vs Fast Metabolism Chart +
+
+ + +
+
+ Fuel Source Chart +
+
+ + +
+

+ Caloric Intake +

+ + +
+ +
+
+ {{ resting_calories }}kCals +
+
+
Resting
+
Metabolic
+
+
+ + +
+
+ + +
+
+ {{ neat_calories }}kCals +
+
NEAT
+
+ + +
-
+ + +
+
+ {{ weight_loss_calories }}kCals +
+
+
to lose {{ weight_loss_rate }}lbs
+
per week
+
+
+ + +
=
+ + +
+
+ ~{{ total_calories }}kCals +
+
+
+
+ + + {% if rhr_table %} +
+

+ Resting Heart Rate +

+
+ Resting Heart Rate Table +
+
+ {% endif %} +
+
+ diff --git a/app/services/__pycache__/context_generator.cpython-312.pyc b/app/services/__pycache__/context_generator.cpython-312.pyc index 4691d54ba38f8e3ce9856750fd1a8631dc8b297c..c159332922eda96d5b999838b4f5e7856a20f04d 100644 GIT binary patch delta 10871 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 t$>}nUcUKtmL#kZ!cR6fc;Q-X&J!jXQXHTO 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 U{5HDs-JG~pNqov@98zZTe}0s^^Z)<= diff --git a/app/services/__pycache__/report_generator.cpython-312.pyc b/app/services/__pycache__/report_generator.cpython-312.pyc index e67ecbf77aa1b1e29e9b4aa7f88e08e331a33de5..32ff5d8e8bb3b6584cfb456e9be2c424b471e791 100644 GIT binary patch delta 4765 zcmaJ^YfxLq72egoSGsz?(F+L)fkA*k2muBg2!6!DB*YF$Y!ZVkxB`PgP$8Fu-fJ8u z({VaXOT6h6*QO<%X~&J@j1!!Ur}Ez`+z;t4&7#sUn++XseGpRe%cJ~Tlh&{Q? zJ)C#u>Y%oaq(%IjtO||F~?c7mb|nCUjF)k0UUnRCnkkBxbyKfN#?>?sq=GELEV~00~k>f`XT0 z9Od%hN-&Oc#nwb@cPTTr7_AhesRU(0{oo+CfcB#R?7rQb2Y8R6$~qhmhie5VqJ%6ys}(OH#IGP5d5}|%0AEhz%*3!F#H0Dpz3LR0X(xX|{#M2r*(U{Vj>NTJ@WoZjiKkix57B%5u zabT?#IhNwwZkjQdWJJSJMhuS}&H&osY;wd$h|aJAvVswh&HUNQa#Z`l8HxR zJ<%l9AB~}x@*KXTJQ_PNAg)Nfl|ayagS|1ICGH|oI|=C`ghTJ;xgF^V4n&5AdSg9F z*qfAN=P(I6jBe!B?4^3S7n1tHcyCW{EYdG3!q_6IPDBTW`thoVW08SqQn$P+_YGKa z;QOO5CfSVX@?=PW-dH%jflz6FF0`PX`9^feB|}_(13QG5`w&RE(5d{6mXragER()V zO7Cq}o~?QH@w;|VvHl!97d%^XhmvG#{t>J{xpP845uI$DteA|>n)0S2mvn_^N>UV1 z+fo3V++~5;I;omBjecT77RriAcrz(#ylxc3aSkHII?M|eK8wrdbtn)pORRF#P-#WsfXQg# zt(@~M*C|pQ@FUoG1^Riw18wN-a*y8jW0T=>j#ik^_Hw^j$Lo262(x_C3Jwf2UhaVo z5ytVOFu4R~N6?IRR&1o!Bn+z1e}nlI*<`IU>4tza@hUEVBOdMjMd1oFdcny%mzQIx zLaTTFXlUQj3Xi#%cRm;Z<%a4|Lyf}bTD5KbiApg`nD8!iIFt`v=wzr7(|?M*uJU=K ziwoj(lJblnA&1Z5Rp?-41m>cp%3@>g8m*W2Vy&yHz?=tx1n_yM(r!S==kxovk&6or zRH-0~Myq~i!K;e1Tz^8!=ZEu_4FR8zRMk$n4n0z7L5jg%PMz*K+QEppiKHOs`Wa$0%&5RFU%M6a0W#DCR zHJu`N3Vwtl9`rpU4jZ4PT%mYXz7&}S;)}WMWbv@+E@En?_#zC_ys`yLc=}KsiLBnr zJ)Vgy_=#lm6I{!Rq?CJ-_xI(9k=GC|i`$X0!WaDxz7*4)Q;b04n7hT{<9 zDZUJaYV*}4tGcx^)LrW@C|en~0*}1)1l0<>pd1Xt5!^Pk0f_%5q~piXUuyFmMjXdF zMP)qjR<#M{HZZ}Azk%-6I^aFDccW#4eN|i!Z+~#_aXBPEZGe*r_Jelpos;H9posGJ z11R2yqU5eYp~vUfpuGG5%DRW5hzD(DEcE#6Yf!cjl$zBuDtai2?+0UfCXIs5Y;>sY zV#Rd{Zrr$F}|l{$Jkr zf8M*sfAd5AA0hr?QQGtaq_Rw|M!*YH8yJDrU;>VRbebB0zNcq&^?#%aNe6h79Lb~< zH;r60o|JY+ahDhsJHjLk;4Y?fINmd}*l3~6A7aCF(26NdD-XVmq1C$D%Lb zpFGjw1nykI9BxHMZJP&UiAYz%ePGz#jl0m{u6Y#_yiN&f}SS4 zoe&O_q^c(#8QM1-?ixH0OZZ?~C`0p)UVw+tueTk7hf&sJ|Ev5haUeZk{7p=Jfpn`y z>RUXQkWND0B1FW0n{XW3+T?|^sJAH~6+7dz==V*3gNumU{&G<4m&GpaYhp{T{-k~= zlIV@b5@N#@CY^TtbE5nTli`5aK?Rx}xQk*?b923JGj0HepO*vh9r*qbxLZGSta+Do z*Mi4?*;7thmEhLdZJqO;u3K8ROmT$1%(~`S*WcM<+;Ew!myNzTqwlh@Vb0jFpt3Hg zEDKKmLbm&Ic3>_$uweD3l)&@|yaS{r#Su1TK*6WV*iZM*T72`C=MeYQX}EvJ(E0%! z>x4kw@Sou%9?aoQj?Oyxp-V+*QRN;b^HI(wAO84QuH0hz{AY?rw!IE~ZV0vqsEah=FRBRC#X?m_2wV&WI|`^T*0GrXqO=UlzjUd>#o)`L zVAwTAeU;7PkFWA_u;OcalYg%n%$2M0$6P~o!~-r>DKS4U*$g|C;2Uad2!DNB4LfV; zZ`T_!e?`gcG0U&$Si)QFd#b1_L5lE|knmL^U&FLhvMcpuu!~Z8yPsOr8ruu0#a!ta zHeD=ah{#V&7XwUtt!%N9Ok%N4-jPRLl^Q#8sH-+9)?UqFh$N3#UM*lc%4Jte$h5A8 zH zP2DhAJA>4X5>Xpuh@_hCtV8cSTdapkn(Rs+-O|z$r}2uTrDyl)#j`<04+$7IDcHM#z^ zSV}I66u|CGDMeNV9Ny{BN$OmJ(y&qoU9p z>6)lGvhP*bT_e`&6mbukCHd{>%ZjrQ{cCp*Tte+_yBrFfk+hcF;`i8%1B19WMeE}R Sh`w$MX%DhcaT^dSTKym5vZqb} delta 3671 zcmaJ@Yj9J?72eg}E8VNNEXjIVvgNmIjGw}9uy`fJU`SKK1Z*p^uP``ZKsH0;+{+`c zI;k^pvXjIF+_*FCOj<|>>U5?_rju8PG$AA*WRi@*5BSkZGwoEw{AijV^z5}I>_B@p z_vq|*&VJ|Y?%Cb@?pyfz*RlD$$)rc{y!>kZ?uMV7HS-$1jkV#K`ziHnyx2qQG{2;i znxc>p=XPo(%j4X3vt*5Pk_}!#$^gs`m_{P9$;jz z+}-*8uV*)Pocv<6cak ztGCnb?oq-G5L#JbkMIM0s*B8@!t?rC^sKg9(}TJ-9Y%x{tt3i%$slVb;~+Rlrjy)r znqf1C6rG}_W1doMrsZD0pG);u1X%|h;sVDq;1E?m3-hBbO=1%eEC&L;BHl_sGRpjX zh*O$WA|1!n1gx_51RB#Q23Z4xh}76khTSOTHBT<8K9X&WECkV48zJS4ht?vL$d~N0 zZ~{pV*$C1p>(#-)da~&SBy&usm}JsxV%#=UY9JU5A4s8g<0AAi4wk3a!n~_V3RhrZ zWHYdN#)x8(E%(6$Vt5tpp_C>bLLqN#=fs>Z7t^T!m^%*f6Fp4qVq!NFy&$NVUBM?R zm{y;?!8*zBkM+c(gZ%Pc1AF)UT{AJ3eNMYxm?S;%fl0C_-Z#l7VtqaFL>BazGy)nO zi0|FMCpHkD)F$G6Qaq8kkD0jMeY^WY)(Bv=t9UzV-x04~%w?JTi1Q9SH@32hZGlX3d#{pOrOVa;^NzweF&8-8(&(Tx~;l z&swY_l}{g}mqU4$;H$CU7F_gHeCDo9B2q=gk|J#>$_OPZXkAH-FG2*b{`>E;NS3}I zsuaCjZCh{W-m9rx@1CF~*EQNF+30>wpDw!}yC<%eoxqtoKvECWAA@)9e)@cQr$slg zQ2S8dMAp%pK}hz~Xl()U_pYEZ-cDbxU55SigW3X+|E%3%_e)hUiwmcj5|9J3f$pe_;UK*i zmi&u4hdsE&BUjD^k00uaoOu{&keqjFA&`a z$M%ng90+V`RK#>(`p3pr@G!_c#LBduUVkdJ&>`rym8Hw}Pq1Xsgo&vsl}n_%*dl@F zHs)y}awt7jrS#b*zv1q5dl}m!whhufX{1bQm6y?(CYOC<3WK9}qg)1Z6BV0-mgS3< ztW91{>zfNqrMIpdKptu?D_Wk;M}&+vpCtF8JXDGL@gX?8A3zv>Bii97=-bT&z6{`Y zkEIWMC^e7bcmR#?!_Uy0%|84$dT)!X(X%LSQ1;wjT2eL}_X9}MDW1a~=xvzS4q=t$ z<=a^D?#Pn6goPV^dkM?F+gJ+k$f9nPbS~WR(IqSujHQ0@h)V9r68cdtk$D#Ss}_%? zMe(TTXF$o8vwK&u>a;33izX>7d*HzJl&z3+?l4)^OQ^~47q_Sz1Ipsm>hG9Z-H-ae zk?!wXv$*$~JN9m2z10(C^^bUb3p_VloOsKGGx8gPd+1whJ!W=^PvQ!=m%I8CJ14HK zb#czu0Gq$()hm9CHD96T4ZjPpFMyCHG0G4x2)MlyvHiRHquu)s_6^YRJ@4ZL{r;Y# zxStNT-mG|n`PrFx0Ys>L{w9Bs0l7@5clt|AQyGub-*3#rZ_>{YqULm(w#vUOL|9qub1Y z;>G*l!bdtW5?B2IpBo50y4u$j!tVu)OqPUq)bj6VuMR=uoM>q0&^bpn=pP6XU3(S! ztEH;FfP9Dr&_C2igpLsUD5I()i=5|}e%{Ea&KDaybI|#ks!lukxKIH7 zxT}!6oNs4(E#HpC%S{64S2%BbF1ZpQOb=qF=c;-E-(Ie}Qo@qGQYChH$dyKWhl5O+ zwMW2p%E1FPsMcDsE00`5?ydm2 z=4RGw0iGdw9Oxl#&>Y*pzdRA&zb`Qm?SXGqEmSz&wB62;wzf8UVEZncTGgze6YRSz z@hzg?ZujFoRM;`*Sf*g!{dHE(xP82P+_0^ Xp7_ed4~QP_sIf!^tp5%%se1hv26S$- diff --git a/app/services/context_generator.py b/app/services/context_generator.py index 8690fe9..d261260 100644 --- a/app/services/context_generator.py +++ b/app/services/context_generator.py @@ -1112,9 +1112,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,60 +1141,156 @@ 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), + } + + # 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"] + ) + + # 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, + ) + ) - # 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 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%", + } - # Page 7 - contexts["page_7"] = { + # 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}", @@ -1410,32 +1514,60 @@ 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): - page_num = i + 13 - contexts[f"page_{page_num}"] = { + # 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": page_num, - } - # Add next_testing_date to page 16 - if page_num == 16: - contexts["page_16"]["next_testing_date"] = self.patient_info.get( + "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 diff --git a/app/services/report_generator.py b/app/services/report_generator.py index 3f8b499..f851277 100644 --- a/app/services/report_generator.py +++ b/app/services/report_generator.py @@ -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"""
@@ -214,7 +242,7 @@ class ReportGeneratorService: {template}
- {footer_html_list[i - 1]} + {footer_html_list[idx]}
""" @@ -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. @@ -535,6 +564,7 @@ class ReportGeneratorService: graphs_dict, metric_overrides=metric_overrides, graph_generator=self.graph_generator, + report_type=report_type, ) # Step 5: Calculate analysis metrics @@ -542,7 +572,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: diff --git a/app/templates/upload.html b/app/templates/upload.html index 5e394ef..fb44f06 100644 --- a/app/templates/upload.html +++ b/app/templates/upload.html @@ -146,6 +146,45 @@ Generator{% endblock %} {% block content %} 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" />
+
+ +
+
+ + +
+
+ + +
+
+