@medicus.ai/medicus-report-pdf-generator 1.0.402 → 1.0.404

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.vscode/settings.json +4 -0
  2. package/assets/jquery-2.1.0.min.js +3 -3
  3. package/assets/pha/labcrop-logo.png +0 -0
  4. package/assets/raphael-min.js +10 -10
  5. package/assets/wellbeing/css/nasco_report.css +32 -0
  6. package/assets/wellbeing/css/nasco_report_rtl.css +5 -0
  7. package/config/Pha.json +48 -0
  8. package/config/test.txt +5 -0
  9. package/index.js +27 -11
  10. package/lib/wellbeing_report_generator.js +1350 -963
  11. package/locales/ar-AE.json +168 -168
  12. package/locales/ar.json +199 -199
  13. package/locales/fr.json +166 -167
  14. package/package.json +1 -1
  15. package/templates/Mediclinic/blocks/first-section.html +162 -219
  16. package/templates/Mediclinic/blocks/header-template.html +4 -4
  17. package/templates/Mediclinic/blocks/header.html +6 -6
  18. package/templates/Mediclinic/blocks/pdf-header.html +2 -2
  19. package/templates/Mediclinic/rtl_no_pages.html +3 -3
  20. package/templates/Pha/blocks/first-section.html +208 -0
  21. package/templates/Pha/blocks/footer.html +8 -0
  22. package/templates/Pha/blocks/header-template.html +6 -0
  23. package/templates/Pha/blocks/header.html +20 -0
  24. package/templates/Pha/blocks/patient-table.html +38 -0
  25. package/templates/Pha/blocks/pdf-header.html +8 -0
  26. package/templates/Pha/blocks/tips.html +9 -0
  27. package/templates/Pha/ltr_no_pages.html +37 -0
  28. package/templates/Pha/rtl_no_pages.html +42 -0
  29. package/templates/Pha/wellbeing_template.html +42 -0
  30. package/templates/blocks/ar-first-page-header.html +3 -6
  31. package/templates/blocks/ar-header.html +1 -1
  32. package/templates/blocks/biomarker-compact-ar.html +2 -0
  33. package/templates/blocks/biomarker-compact.html +2 -0
  34. package/templates/blocks/biomarker-details.html +1 -1
  35. package/templates/blocks/biomarker-insight.html +14 -14
  36. package/templates/blocks/header-first-page.html +3 -6
  37. package/templates/blocks/header.html +1 -1
  38. package/templates/blocks/signature.html +1 -1
  39. package/templates/blocks/summary-insight.html +6 -6
  40. package/templates/corporate_report/ltr_no_pages.html +1 -2
  41. package/templates/corporate_report/rtl_no_pages.html +1 -2
  42. package/templates/first_page_head.html +3 -1
  43. package/templates/ltr.html +31 -79
  44. package/templates/no_pages.html +48 -85
  45. package/templates/popup/popup-template.html +15 -18
  46. package/templates/sanusx/blocks/personal-details.html +54 -78
  47. package/templates/sanusx/blocks/predictions-section.html +3 -3
  48. package/templates/sanusx/blocks/super-power-section.html +2 -1
  49. package/templates/sanusx/ltr_no_pages.html +1 -2
  50. package/templates/sanusx/rtl_no_pages.html +2 -3
  51. package/templates/template.html +1373 -1421
  52. package/templates/wellbeing/blocks/first-section.html +14 -46
  53. package/templates/wellbeing/blocks/header.html +4 -5
  54. package/templates/wellbeing/ltr_no_pages.html +1 -2
  55. package/templates/wellbeing/rtl_no_pages.html +2 -3
  56. package/test.js +18 -9
@@ -1,24 +1,19 @@
1
1
  "use strict";
2
2
  const fs = require("fs");
3
- const {promisify} = require('util');
4
- const {JSDOM} = require("jsdom");
5
- const fetch = require("node-fetch");
3
+ const { promisify } = require('util');
4
+ const { JSDOM } = require("jsdom");
6
5
  const path = require("path");
7
6
  const LocaleService = require('../app/services/localeService.js');
8
7
  const i18n = require('../app/i18n_wellbeing.config.js');
9
8
  const puppeteer = require("puppeteer");
10
- const {log} = require("util");
11
- const {renderReportItems, find, renderSummary, renderReportSummary, renderHTMLTemplate} = require("./template.js");
12
- const {cairoFontBase64} = require("../base64/cairo-font");
9
+ const { renderReportItems, find, renderSummary, renderReportSummary, renderHTMLTemplate, renderAllergyClasses } = require("./template.js");
13
10
 
14
11
 
15
12
  var localeService = new LocaleService(i18n);
16
- let isRTL = false;
17
13
 
18
14
 
19
15
  let OUT_FILE = 'output/nasco-pdf.html';
20
16
  let Pdf_file = '';
21
- const appDir = path.dirname(require.main.filename);
22
17
 
23
18
 
24
19
  let debug = true;
@@ -30,6 +25,125 @@ let debugLog = (val1, val2 = "") => {
30
25
  }
31
26
 
32
27
 
28
+ // Config object to hold all configuration values
29
+ let config = {
30
+ logo: "",
31
+ title: "",
32
+ bulbIcon: "",
33
+ bulb_icon_font_color: "",
34
+ first_level_color: "",
35
+ second_level_color: "",
36
+ third_level_color: "",
37
+ forth_level_color: "",
38
+ fifth_level_color: "",
39
+ general_font_color: "",
40
+ disclaimer_text_color: "",
41
+ top_box_background: "",
42
+ footer_background: "",
43
+ lifestyleImage: path.resolve(__dirname + '/../assets/wellbeing/images/lifestyle-section.png'),
44
+ mindImage: path.resolve(__dirname + '/../assets/wellbeing/images/mind-section.png'),
45
+ bodyImage: path.resolve(__dirname + '/../assets/wellbeing/images/body-section.png'),
46
+ body_font_color: "",
47
+ list_icons_font_color: "",
48
+ insights_font_color: "",
49
+ section_title_border_color: ""
50
+ };
51
+ /**
52
+ * Load color theme based on client name
53
+ * @param {string} clientName - Client name (mediclinic, pha, nasco, etc.)
54
+ * @returns {object} Color theme object
55
+ */
56
+ const loadColorTheme = (clientName = 'default') => {
57
+ const themes = {
58
+ mediclinic: {
59
+ headerText: '#3B3B3B',
60
+ headerBackground: '#f2f5f8',
61
+ headerDivider: '#0092D2',
62
+ footerText: '#B5B5B5',
63
+ listIcon: '#8b8ba7',
64
+ insightPrimary: '#124374',
65
+ insightSecondary: '#000000',
66
+ warningIcon: '#ffc107'
67
+ },
68
+ pha: {
69
+ headerText: '#2C3E50',
70
+ headerBackground: '#ECF0F1',
71
+ headerDivider: '#0F2062',
72
+ footerText: '#95A5A6',
73
+ listIcon: '#7F8C8D',
74
+ insightPrimary: '#0F2062',
75
+ insightSecondary: '#2C3E50',
76
+ warningIcon: '#F39C12'
77
+ },
78
+ nasco: {
79
+ headerText: '#34495E',
80
+ headerBackground: '#EBF5FB',
81
+ headerDivider: '#5DADE2',
82
+ footerText: '#AAB7B8',
83
+ listIcon: '#85929E',
84
+ insightPrimary: '#1F618D',
85
+ insightSecondary: '#1C2833',
86
+ warningIcon: '#F4D03F'
87
+ },
88
+
89
+ };
90
+ return themes[clientName?.toLowerCase()] || themes.mediclinic;
91
+ };
92
+ /**
93
+ * Load and apply configuration for a specific client
94
+ * @param {string} clientName - The client name to load config for
95
+ * @returns {object} The loaded configuration
96
+ */
97
+ const loadClientConfig = (clientName = 'default') => {
98
+ const pathFile = path.resolve(__dirname + '/../config/' + clientName + '.json');
99
+ let clientConfig = require('../config/default.json');
100
+
101
+ if (fs.existsSync(pathFile) && clientName !== 'default') {
102
+ clientConfig = require('../config/' + clientName + '.json');
103
+ }
104
+
105
+ // Map config values with defaults
106
+ config.logo = clientConfig['logo'] || "";
107
+ config.labs_logos = clientConfig['labs-logo'] || [];
108
+ config.title = clientConfig['title'] || "";
109
+ config.bulbIcon = clientConfig['bulb_icon'] || "";
110
+ config.bulb_icon_font_color = clientConfig['bulb_icon_font_color'] || "";
111
+ config.first_level_color = clientConfig['first-level-color'] || "";
112
+ config.second_level_color = clientConfig['second-level-color'] || "";
113
+ config.third_level_color = clientConfig['third-level-color'] || "";
114
+ config.forth_level_color = clientConfig['forth-level-color'] || "";
115
+ config.fifth_level_color = clientConfig['fifth-level-color'] || "";
116
+ config.general_font_color = clientConfig['general-font-color'] || "";
117
+ config.disclaimer_text_color = clientConfig['disclaimer-text-color'] || "";
118
+ config.top_box_background = clientConfig['top-box-background'] || "";
119
+ config.footer_background = clientConfig['footer-background'] || "";
120
+ config.footer_border_color = clientConfig['footer-border-color'] || clientConfig['section-title-border-color'] || "";
121
+
122
+ // Handle image paths with fallbacks
123
+ config.lifestyleImage = clientConfig['lifestyle_icon'] || path.resolve(__dirname + '/../assets/wellbeing/images/lifestyle-section.png');
124
+ config.mindImage = clientConfig['mind_icon'] || path.resolve(__dirname + '/../assets/wellbeing/images/mind-section.png');
125
+ config.bodyImage = clientConfig['body_icon'] || path.resolve(__dirname + '/../assets/wellbeing/images/body-section.png');
126
+ config.body_font_color = clientConfig['body_font_color'] || "";
127
+ config.list_icons_font_color = clientConfig['list_icons_font_color'] || "";
128
+ config.insights_font_color = clientConfig['insights_font_color'] || "";
129
+
130
+ config.score_header_color = clientConfig['score-header-color'] || clientConfig['first-level-color'] || "";
131
+ config.score_description_color = clientConfig['score-description-color'] || clientConfig['first-level-color'] || "";
132
+ config.svg_icon_color = clientConfig['svg-icon-color'] || clientConfig['first-level-color'] || "";
133
+ config.low_score_text_color = clientConfig['low-score-text-color'] || clientConfig['first-level-color'] || "";
134
+ config.section_title_border_color =
135
+ clientConfig['section-title-border-color'] ||
136
+ (clientConfig['corporate_report'] ?
137
+ clientConfig['corporate_report']['section_title_border_color'] : null) ||
138
+ clientConfig['forth-level-color'] ||
139
+ "";
140
+ config.first_section_scores_border_color = clientConfig['first-section-scores-border-color'] || clientConfig['fifth-level-color'] || "";
141
+
142
+ config.colors = loadColorTheme(clientName);
143
+
144
+ return config;
145
+ };
146
+
33
147
  const icons = {
34
148
  "wellbeing_element_alcohol": "M 10.667969 5.144531 L 19.332031 5.144531 C 19.5 5.144531 19.65625 5.238281 19.710938 5.375 C 21.007812 8.671875 21.394531 10.714844 21.394531 12.316406 C 21.394531 15.285156 18.53125 17.695312 15 17.695312 C 11.46875 17.695312 8.605469 15.285156 8.605469 12.316406 C 8.605469 10.714844 8.988281 8.671875 10.289062 5.375 C 10.34375 5.238281 10.5 5.144531 10.667969 5.144531 Z M 9.804688 12.316406 C 9.804688 14.648438 11.785156 16.09375 15 16.339844 C 17.796875 16.550781 20.195312 14.71875 20.195312 12.316406 L 20.195312 11.929688 L 9.804688 11.929688 Z M 9.804688 12.316406 M 17.796875 24.140625 L 17.398438 24.140625 C 16.296875 24.140625 15.398438 23.378906 15.398438 22.445312 L 15.394531 18.28125 C 15.394531 17.945312 15.6875 17.667969 16.078125 17.613281 C 17.324219 17.433594 18.445312 16.953125 19.332031 16.261719 C 19.332031 16.261719 18.003906 17.015625 15 17.015625 C 11.996094 17.015625 10.667969 16.261719 10.667969 16.261719 C 11.558594 16.953125 12.675781 17.433594 13.921875 17.613281 C 14.3125 17.667969 14.605469 17.945312 14.601562 18.28125 L 14.601562 22.445312 C 14.601562 23.378906 13.703125 24.140625 12.601562 24.140625 L 12.203125 24.140625 C 11.980469 24.140625 11.804688 24.292969 11.804688 24.480469 C 11.804688 24.667969 11.980469 24.820312 12.203125 24.820312 L 17.796875 24.820312 C 18.019531 24.820312 18.195312 24.667969 18.195312 24.480469 C 18.195312 24.292969 18.019531 24.140625 17.796875 24.140625 Z M 17.796875 24.140625",
35
149
  "wellbeing_element_blue_mood": "M 19.351562 7.859375 C 19.640625 6.664062 20.308594 5.144531 21.085938 5.144531 C 22.121094 5.144531 22.972656 7.855469 22.972656 8.882812 C 22.972656 9.371094 22.78125 9.8125 22.472656 10.148438 C 24.007812 11.851562 24.859375 14.066406 24.855469 16.363281 C 24.855469 21.527344 20.636719 25.714844 15.429688 25.714844 C 10.222656 25.714844 6 21.527344 6 16.363281 C 6 11.199219 10.222656 7.011719 15.429688 7.011719 C 16.828125 7.011719 18.15625 7.316406 19.351562 7.859375 Z M 12.191406 15.734375 C 12.636719 15.734375 13 15.375 13 14.933594 C 13 14.488281 12.636719 14.128906 12.191406 14.128906 C 11.75 14.128906 11.386719 14.488281 11.382812 14.933594 C 11.382812 15.375 11.746094 15.734375 12.191406 15.734375 Z M 18.835938 15.734375 C 19.28125 15.734375 19.644531 15.375 19.644531 14.933594 C 19.644531 14.488281 19.28125 14.128906 18.835938 14.128906 C 18.394531 14.128906 18.03125 14.488281 18.027344 14.933594 C 18.027344 15.375 18.390625 15.734375 18.835938 15.734375 Z M 12.929688 20.742188 C 13.726562 20.632812 14.59375 20.574219 15.53125 20.574219 C 16.464844 20.574219 17.3125 20.628906 18.066406 20.742188 C 17.785156 20.328125 17.5 20.023438 17.207031 19.835938 C 16.765625 19.550781 16.429688 19.3125 15.601562 19.3125 C 14.773438 19.3125 14.234375 19.554688 13.777344 19.835938 C 13.472656 20.019531 13.1875 20.324219 12.929688 20.742188 Z M 19.351562 7.859375 C 19.253906 8.257812 19.199219 8.625 19.199219 8.882812 C 19.199219 9.910156 20.046875 10.753906 21.085938 10.753906 C 21.632812 10.753906 22.128906 10.519531 22.472656 10.148438 C 21.601562 9.175781 20.539062 8.398438 19.351562 7.859375 Z M 9.941406 13.4375 L 10.421875 14.148438 L 12.554688 12.710938 L 12.074219 12 Z M 18.355469 12.832031 L 20.488281 14.269531 L 20.96875 13.558594 L 18.835938 12.121094 Z M 18.355469 12.832031",
@@ -68,27 +182,6 @@ const icons = {
68
182
  "wellbeing_element_vitamin_balance": "M9.28358 5H18.2836C18.6866 5 19.0896 5.26866 19.0896 5.80597V8.08955C19.0896 8.49254 18.8209 8.89552 18.2836 8.89552H18.0149H18.4179V9.97015C18.4179 10.6418 18.8209 11.3134 19.4925 11.5821C19.7612 11.7164 20.1642 11.8507 20.2985 12.2537C20.5672 12.5224 20.5672 12.9254 20.5672 13.3284V14H7V13.194C7 12.5224 7.40299 11.8507 8.07463 11.5821C8.47761 11.4478 8.8806 11.1791 9.01493 10.9104C9.28358 10.6418 9.28358 10.2388 9.28358 9.97015V8.89552C8.8806 8.89552 8.61194 8.49254 8.61194 8.08955V5.80597C8.61194 5.26866 8.8806 5 9.28358 5ZM7 14.2687H20.5672V16.6866C19.4925 17.2239 18.6866 18.2985 18.6866 19.5075V23.806H7V14.2687ZM18.8209 24.0746L7.13433 24.209V25.6866C7.13433 26.4925 7.80597 27.1642 8.61194 27.1642H13.5821H19.2239C19.6269 27.1642 20.0299 27.0299 20.2985 26.6269C20.8358 26.8955 21.2388 27.0298 21.9104 27.0298C23.6567 27.0298 25 25.6866 25 23.9403V19.3731C25 18.5672 24.7313 17.7612 24.0597 17.2239C23.1194 16.2836 21.9104 16.0149 20.8358 16.5522V23.806V25.6866C20.8358 26.0896 20.7015 26.3582 20.4328 26.6269C19.3582 26.2239 18.8209 25.1493 18.8209 24.0746ZM13.0448 21.9254C13.1791 22.194 13.5821 22.4627 13.9851 22.4627C14.3881 22.4627 14.6567 22.194 14.9254 21.9254L17.209 17.0896C17.3433 16.8209 17.3433 16.4179 17.0746 16.1493C16.806 15.8806 16.5373 15.6119 16.2687 15.7463C15.8657 15.7463 15.597 15.8806 15.3284 16.2836L13.8507 19.3731L12.3731 16.2836C12.1045 15.7463 11.5672 15.6119 11.0299 15.8806C10.4925 16.1493 10.3582 16.6866 10.6269 17.2239L13.0448 21.9254Z"
69
183
  };
70
184
 
71
- let logo = ""
72
- let title = ""
73
-
74
- let bulbIcon = ""
75
- let bulb_icon_font_color = ""
76
- let first_level_color = ""
77
- let second_level_color = ""
78
- let third_level_color = ""
79
- let forth_level_color = ""
80
- let fifth_level_color = ""
81
- let general_font_color = ""
82
- let disclaimer_text_color = ""
83
- let top_box_background = ""
84
- let footer_background = ""
85
- let lifestyleImage = path.resolve(__dirname + '/../assets/wellbeing/images/lifestyle-section.png')
86
- let mindImage = path.resolve(__dirname + '/../assets/wellbeing/images/mind-section.png')
87
- let bodyImage = path.resolve(__dirname + '/../assets/wellbeing/images/body-section.png')
88
- let body_font_color = ""
89
- let list_icons_font_color = ""
90
- let insights_font_color = ""
91
-
92
185
  const escapeHTML = (text) => {
93
186
  return text
94
187
  .replace(/&/g, '&') // Must come first
@@ -110,373 +203,448 @@ const wrapLTR = (text) => {
110
203
  }
111
204
  return `<span style="unicode-bidi: bidi-override;" dir="ltr">${text}</span>`;
112
205
  };
113
- /***
114
- * HTML file generator to fill the html template
115
- * based on the json wellbeingData from the POST request
116
- */
117
- let generateHTMLWellbeingReport = async (data, isDebugging, clientName, language) => {
118
206
 
119
- debug = isDebugging;
120
- OUT_FILE = path.resolve(__dirname + '/../output/nasco-pdf' + Date.now() + '.html');
121
- Pdf_file = '/output/nasco-pdf' + Date.now() + '.html'
122
- if (empty(language)) {
123
- language = 'en'
124
- }
125
-
126
- let isRtl = false;
127
- if (language.indexOf('ar') !== -1) {
128
- language = 'ar-SA';
129
- isRtl = true;
130
- }
131
207
 
132
208
 
133
- let client = clientName ? clientName : 'default'
134
- let pathFile = path.resolve(__dirname + '/../config/' + client + '.json');
135
- let defaultConfig = require('../config/default.json');
136
- if (fs.existsSync(pathFile)) {
137
- defaultConfig = require('../config/' + client + '.json');
209
+ /**
210
+ * Calculate score metrics for visualization
211
+ * @param {number} score - The score value (0-100)
212
+ * @param {string} client - Client name for specific color logic
213
+ * @returns {object} Score metrics including color, amount, and rotation
214
+ */
215
+ const calculateScoreMetrics = (score, client = 'default') => {
216
+ const scoreValue = parseFloat(score);
217
+
218
+ // Determine amount based on score range
219
+ let amount;
220
+ if (scoreValue > 40 && scoreValue < 70) {
221
+ amount = 355;
222
+ } else if (scoreValue >= 70) {
223
+ amount = 360;
224
+ } else {
225
+ amount = 214;
138
226
  }
139
227
 
140
- logo = defaultConfig['logo'];
141
- bulbIcon = defaultConfig['bulb_icon'];
142
- bulb_icon_font_color = defaultConfig['bulb_icon_font_color'];
143
- first_level_color = defaultConfig['first-level-color']
144
- second_level_color = defaultConfig['second-level-color']
145
- third_level_color = defaultConfig['third-level-color']
146
- forth_level_color = defaultConfig['forth-level-color']
147
- fifth_level_color = defaultConfig['fifth-level-color']
148
- general_font_color = defaultConfig['general-font-color']
149
- disclaimer_text_color = defaultConfig['disclaimer-text-color']
150
- top_box_background = defaultConfig['top-box-background']
151
- footer_background = defaultConfig['footer-background']
152
- lifestyleImage = defaultConfig['lifestyle_icon'] ? defaultConfig['lifestyle_icon'] : path.resolve(__dirname + '/../assets/wellbeing/images/lifestyle-section.png')
153
- mindImage = defaultConfig['mind_icon'] ? defaultConfig['mind_icon'] : path.resolve(__dirname + '/../assets/wellbeing/images/mind-section.png')
154
- bodyImage = defaultConfig['body_icon'] ? defaultConfig['body_icon'] : path.resolve(__dirname + '/../assets/wellbeing/images/body-section.png')
155
- body_font_color = defaultConfig['body_font_color']
156
- list_icons_font_color = defaultConfig['list_icons_font_color']
157
- insights_font_color = defaultConfig['insights_font_color']
158
-
159
-
160
- /*check if the language is an RTL language*/
161
- localeService.setLocale(language);
162
- debugLog("pdf language:", language);
163
-
164
- /*choose the right template*/
165
- let html = !isRtl ? fs.readFileSync(path.resolve(__dirname + '/../templates/wellbeing/ltr_no_pages.html'), 'utf8') : fs.readFileSync(path.resolve(__dirname + '/../templates/wellbeing/rtl_no_pages.html'), 'utf8');
166
-
167
- let headerTemplate = fs.readFileSync(path.resolve(__dirname + '/../templates/wellbeing/blocks/header.html'), 'utf8');
168
-
169
- let footerTemplate = fs.readFileSync(path.resolve(__dirname + '/../templates/wellbeing/blocks/footer.html'), 'utf8');
228
+ // Determine color based on score range
229
+ let color;
230
+ if (scoreValue > 40 && scoreValue < 70) {
231
+ color = config.third_level_color;
232
+ } else if (scoreValue >= 70) {
233
+ color = config.fifth_level_color;
234
+ } else {
235
+ color = client === 'nasco' ? config.second_level_color : config.first_level_color;
236
+ }
170
237
 
171
- let firstSectionTemplate = fs.readFileSync(path.resolve(__dirname + '/../templates/wellbeing/blocks/first-section.html'), 'utf8');
238
+ // Calculate rotation amount
239
+ let rotateAmount = (scoreValue / 100) * amount;
240
+ if (rotateAmount > 336) {
241
+ rotateAmount = 336;
242
+ }
172
243
 
173
- let tipsTemplate = fs.readFileSync(path.resolve(__dirname + '/../templates/wellbeing/blocks/tips.html'), 'utf8');
244
+ return {
245
+ score: scoreValue,
246
+ amount,
247
+ color,
248
+ rotateAmount,
249
+ scoreInteger: Math.floor(scoreValue),
250
+ scoreDecimal: parseFloat((scoreValue % 1).toFixed(1)).toString().substring(1)
251
+ };
252
+ };
174
253
 
175
- /*create the dom element*/
176
- const dom = new JSDOM(html);
177
- let historyData = [];
178
- debugLog("templates loaded");
179
- let nascReport = true
254
+ /**
255
+ * Apply inactive classes to score sections based on score ranges
256
+ * @param {object} $ - jQuery instance
257
+ * @param {number} score - The score value
258
+ * @param {string} sectionClass - CSS class of the section
259
+ */
260
+ const applyScoreInactiveClasses = ($, score, sectionClass) => {
261
+ const scoreValue = parseFloat(score);
262
+ const selector = `.first-section-scores .${sectionClass}`;
263
+
264
+ if (scoreValue > 40 && scoreValue < 70) {
265
+ $(`${selector} .part-1, ${selector} .part-3`).addClass('inactive');
266
+ } else if (scoreValue >= 70) {
267
+ $(`${selector} .part-3, ${selector} .part-2`).addClass('inactive');
268
+ } else {
269
+ $(`${selector} .part-1, ${selector} .part-2`).addClass('inactive');
270
+ }
271
+ };
180
272
 
181
- let img = ""
182
- if (nascReport) {
183
- img = logo;
273
+ /**
274
+ * Calculate extended score metrics for special score types (depression, anxiety)
275
+ * @param {number} score - The score value
276
+ * @param {string} scoreType - Type of score ('depression' or 'anxiety')
277
+ * @param {string} client - Client name
278
+ * @returns {object} Score metrics
279
+ */
280
+ const calculateExtendedScoreMetrics = (score, scoreType, client = 'default') => {
281
+ const scoreValue = parseFloat(score);
282
+ let amount = 0;
283
+
284
+ if (scoreType === 'depression') {
285
+ if (scoreValue >= 0 && scoreValue <= 14.8) amount = 550;
286
+ else if (scoreValue >= 18.5 && scoreValue <= 33.3) amount = 400;
287
+ else if (scoreValue >= 37 && scoreValue <= 51.9) amount = 380;
288
+ else if (scoreValue >= 55.6 && scoreValue <= 70.4) amount = 370;
289
+ else if (scoreValue >= 74.1 && scoreValue <= 100) amount = 330;
290
+ } else if (scoreType === 'anxiety') {
291
+ if (scoreValue >= 0 && scoreValue <= 19) amount = 460;
292
+ else if (scoreValue >= 23.8 && scoreValue <= 42.9) amount = 400;
293
+ else if (scoreValue >= 47.6 && scoreValue <= 66.7) amount = 360;
294
+ else if (scoreValue >= 71.4 && scoreValue <= 100) amount = 330;
184
295
  }
185
296
 
186
- let genderId = data.genderId
187
- let userImage = data.photo;
188
- if (empty(userImage)) {
189
- if (genderId === 90) {
190
- userImage = path.resolve(__dirname + '/../assets/wellbeing/images/user-woman.png');
191
- } else {
192
- userImage = path.resolve(__dirname + '/../assets/wellbeing/images/user-man.png');
193
- }
297
+ // Calculate color
298
+ let color;
299
+ if (scoreValue > 40 && scoreValue < 70) {
300
+ color = config.third_level_color;
301
+ } else if (scoreValue >= 70) {
302
+ color = config.fifth_level_color;
303
+ } else {
304
+ color = client === 'nasco' ? config.second_level_color : config.first_level_color;
194
305
  }
195
306
 
196
- const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
197
-
198
- let formattedDate = new Date();
199
- let day = formattedDate.getDate();
200
- let month = formattedDate.getMonth();
201
- month += 1;
202
- /* JavaScript months are 0-11 */
203
- let year = formattedDate.getFullYear();
204
- let Hours = (formattedDate.getHours() < 10 ? '0' : '') + formattedDate.getHours();
205
- let m = (formattedDate.getMinutes() < 10 ? '0' : '') + formattedDate.getMinutes();
206
- /* only -12 from hours if it is greater than 12 (if not back at mid night) */
207
- let hours12 = (Hours > 12) ? Hours - 12 : Hours;
208
- /* if 00 then it is 12 am */
209
- hours12 = (hours12 == 0) ? 12 : hours12;
210
-
211
-
212
- let topInsight = data.lingualReading ? data.lingualReading.statement : localeService.t('reportHeaderDescription')
213
-
214
- headerTemplate = headerTemplate
215
- .replace("{{logo}}", img)
216
- .replace("{{userImage}}", userImage)
217
- .replace("{{topBoxBg}}", top_box_background)
218
- .replace("{{scoresNote}}", localeService.t('scoresNote'))
219
- .replace("{{bulbIcon}}", bulbIcon)
220
- .replace("{{reportHeader}}", localeService.t('wellbeingReport'))
221
- .replace("{{reportHeaderDescription}}", topInsight)
222
- .replace("{{patientName}}", data.name)
223
- .replace("{{patientOld}}", data.age)
224
- .replace("{{yearsOld}}", localeService.t('yearsOld'))
225
- .replace("{{patientGender}}", data.gender)
226
- .replace("{{reportDate}}", reportDate);
227
-
228
- let iconColor = !empty(bulb_icon_font_color) ? bulb_icon_font_color : general_font_color
229
- /*get the window from the dom to be used as a jquery function*/
230
- const $ = (require('jquery'))(dom.window);
231
- $('#content').append(headerTemplate);
232
- $('#content').append('<style>' + '.tips-text{color: ' + insights_font_color + ' !important;}' + '.first-level-font-color{color:' + general_font_color + ' !important;}' + '.first-section-scores{border-color:' + fifth_level_color + ' !important;}' + '.disclaimer-text-color{color:' + disclaimer_text_color + ' !important;}' + '.first-level-border-color{border-color:' + general_font_color + ' !important;}' + '.scores-note .first-level-font-color{color:' + iconColor + ' !important;}' + '.top-box-bg{background-color: ' + top_box_background + ' !important;}' + '.footer-background{background-color: ' + footer_background + ' !important;}' + '.report-date, .patient-info,.score-description,.list-text,.report-header,.list-score,.score-details-title{color: ' + body_font_color + ' !important;}' + '.list-item g, .list-item path{fill: ' + list_icons_font_color + ' !important;stroke: ' + list_icons_font_color + ' !important;}' + '.fifth-level-font-color{color:' + fifth_level_color + '!important;}' + '</style>');
233
- if (empty(data.photo)) {
307
+ const rotateAmount = (scoreValue / 100) * amount;
234
308
 
235
- $('#content').append('<style>' + '.user-profile-img{display: none !important;}' + '</style>');
236
- if (genderId === 90) {
237
- userImage = path.resolve(__dirname + '/../assets/wellbeing/images/user-woman.png');
238
- } else {
239
- userImage = path.resolve(__dirname + '/../assets/wellbeing/images/user-man.png');
240
- }
309
+ return {
310
+ score: scoreValue,
311
+ amount,
312
+ color,
313
+ rotateAmount,
314
+ scoreInteger: Math.floor(scoreValue),
315
+ scoreDecimal: parseFloat((scoreValue % 1).toFixed(1)).toString().substring(1)
316
+ };
317
+ };
318
+
319
+ /**
320
+ * Calculate physical score metrics with special handling for low scores
321
+ * @param {number} score - The score value
322
+ * @param {string} client - Client name
323
+ * @returns {object} Score metrics
324
+ */
325
+ const calculatePhysicalScoreMetrics = (score, client = 'default') => {
326
+ const scoreValue = parseFloat(score);
327
+
328
+ // Use different amounts for this specific case
329
+ let amount;
330
+ if (scoreValue > 40 && scoreValue < 70) {
331
+ amount = 310;
332
+ } else if (scoreValue >= 70) {
333
+ amount = 310;
334
+ } else {
335
+ amount = 300;
241
336
  }
242
- $("#chartsData").append("<script> var RANGE_COLORS = ['" + first_level_color + "','" + second_level_color + "','" + third_level_color + "','" + forth_level_color + "','" + fifth_level_color + "'];</script>");
243
337
 
244
- /* First section */
245
- let physicalScore = data.calculation.physicalScore?.toFixed(1)
246
- let physicalAmount = ((physicalScore > 40 && physicalScore < 70) ? 355 : (physicalScore >= 70) ? 360 : 214);
247
- let physicalColor = (physicalScore > 40 && physicalScore < 70) ? third_level_color : (physicalScore >= 70) ? fifth_level_color : client === 'nasco' ? second_level_color : first_level_color;
338
+ // Determine color
339
+ let color;
340
+ if (scoreValue > 40 && scoreValue < 70) {
341
+ color = config.third_level_color;
342
+ } else if (scoreValue >= 70) {
343
+ color = config.fifth_level_color;
344
+ } else {
345
+ color = client === 'nasco' ? config.second_level_color : config.first_level_color;
346
+ }
248
347
 
249
- let rotatePhysicalAmount = (physicalScore / 100) * physicalAmount;
250
- if (rotatePhysicalAmount > 336) {
251
- rotatePhysicalAmount = 336
348
+ // Calculate rotation with special handling for low scores
349
+ let rotateAmount = (scoreValue / 100) * amount;
350
+ if (scoreValue <= 20) {
351
+ rotateAmount += 25;
252
352
  }
253
- let psychologicalScore = data.calculation.psychologicalScore?.toFixed(1)
254
- let psychologicalAmount = ((psychologicalScore > 40 && psychologicalScore < 70) ? 355 : (psychologicalScore >= 70) ? 360 : 214);
255
- let psychologicalColor = (psychologicalScore > 40 && psychologicalScore < 70) ? third_level_color : (psychologicalScore >= 70) ? fifth_level_color : client === 'nasco' ? second_level_color : first_level_color;
256
- let rotatePsychologicalAmount = (psychologicalScore / 100) * psychologicalAmount;
257
- if (rotatePsychologicalAmount > 336) {
258
- rotatePsychologicalAmount = 336
353
+ if (rotateAmount > 336) {
354
+ rotateAmount = 336;
259
355
  }
260
356
 
261
- let firstSectionlHtml = firstSectionTemplate.replace("{{physicalScore}}", parseFloat(Math.floor(data.calculation.physicalScore)))
262
- .replace("{{physicalScoreDecimalPart}}", parseFloat((data.calculation.physicalScore)?.toFixed(1) % 1)?.toFixed(1).substring(1))
263
- .replace("{{physicalScoreStatement}}", data.calculation.physicalScoreStatement)
357
+ return {
358
+ score: scoreValue,
359
+ amount,
360
+ color,
361
+ rotateAmount,
362
+ scoreInteger: Math.floor(scoreValue),
363
+ scoreDecimal: parseFloat((scoreValue % 1).toFixed(1)).toString().substring(1)
364
+ };
365
+ };
366
+
367
+ /**
368
+ * Render the first section with physical and psychological scores
369
+ * @param {object} template - The HTML template
370
+ * @param {object} calculationData - Data containing scores and statements
371
+ * @param {object} physicalMetrics - Calculated physical score metrics
372
+ * @param {object} psychologicalMetrics - Calculated psychological score metrics
373
+ * @param {string} client - Client name
374
+ * @returns {string} Rendered HTML
375
+ */
376
+ const renderFirstSection = (template, calculationData, physicalMetrics, psychologicalMetrics, client) => {
377
+ return template
378
+ .replace("{{physicalScore}}", physicalMetrics.scoreInteger)
379
+ .replace("{{physicalScoreDecimalPart}}", physicalMetrics.scoreDecimal)
380
+ .replace("{{physicalScoreStatement}}", calculationData.physicalScoreStatement)
264
381
  .replace("{{PhysicalWord}}", localeService.t('physical'))
265
- .replace(/{{rotatePhysicalAmount}}/gi, rotatePhysicalAmount)
266
- .replace(/{{physicalColor}}/gi, physicalColor)
267
- .replace("{{psychologicalScore}}", Math.floor(data.calculation.psychologicalScore))
268
- .replace("{{psychologicalScoreDecimalPart}}", parseFloat((data.calculation.psychologicalScore)?.toFixed(1) % 1)?.toFixed(1).substring(1))
382
+ .replace(/{{rotatePhysicalAmount}}/gi, physicalMetrics.rotateAmount)
383
+ .replace(/{{physicalColor}}/gi, physicalMetrics.color)
384
+ .replace("{{psychologicalScore}}", psychologicalMetrics.scoreInteger)
385
+ .replace("{{psychologicalScoreDecimalPart}}", psychologicalMetrics.scoreDecimal)
269
386
  .replace("{{psychologicalWord}}", localeService.t('psychological'))
270
- .replace("{{psychologicalScoreStatement}}", data.calculation.psychologicalScoreStatement)
271
- .replace(/{{rotatePsychologicalAmount}}/gi, rotatePsychologicalAmount)
272
- .replace(/{{fifthLevelColor}}/g, fifth_level_color)
273
- .replace(/{{thirdLevelColor}}/g, third_level_color)
274
- .replace(/{{firstLevelColor}}/g, client === 'nasco' ? second_level_color : first_level_color)
275
- .replace("{{psychologicalColor}}", psychologicalColor)
276
- .replace(/{{listIconsFontColor}}/g, list_icons_font_color)
277
-
278
- let footerTemplateHTML = footerTemplate.replace("{{Disclaimer}}", localeService.t('Disclaimer'))
279
- .replace("{{Disclaimer Text}}", localeService.t('Disclaimer Text'));
387
+ .replace("{{psychologicalScoreStatement}}", calculationData.psychologicalScoreStatement)
388
+ .replace(/{{rotatePsychologicalAmount}}/gi, psychologicalMetrics.rotateAmount)
389
+ .replace(/{{fifthLevelColor}}/g, config.fifth_level_color)
390
+ .replace(/{{thirdLevelColor}}/g, config.third_level_color)
391
+ .replace(/{{firstLevelColor}}/g, client === 'nasco' ? config.second_level_color : config.first_level_color)
392
+ .replace("{{psychologicalColor}}", psychologicalMetrics.color)
393
+ .replace(/{{scoreHeaderColor}}/gi, physicalMetrics.color)
394
+ .replace(/{{svgIconColor}}/gi, physicalMetrics.color)
395
+ .replace(/{{lowScoreTextColor}}/gi, physicalMetrics.color)
396
+ .replace(/{{scoreDescriptionColor}}/gi, physicalMetrics.color)
397
+ .replace(/{{listIconsFontColor}}/g, config.list_icons_font_color);
398
+ };
399
+
400
+ /**
401
+ * Render extended first section with depression and anxiety scores
402
+ * @param {object} template - The HTML template
403
+ * @param {object} calculationData - Data containing all scores and statements
404
+ * @param {object} metrics - Object containing all calculated metrics
405
+ * @param {string} client - Client name
406
+ * @returns {string} Rendered HTML
407
+ */
408
+ const renderExtendedFirstSection = (template, calculationData, metrics, client) => {
409
+ return template
410
+ .replace("{{depressionRiskScore}}", metrics.depression.scoreInteger)
411
+ .replace("{{depressionRiskScoreDecimalPart}}", metrics.depression.scoreDecimal)
412
+ .replace("{{depressionRiskScoreStatement}}", calculationData.depressionRiskScoreStatement)
413
+ .replace("{{DepressionRiskWord}}", localeService.t('depressionRisk'))
414
+ .replace(/{{rotateDepressionRiskAmount}}/gi, metrics.depression.rotateAmount)
415
+ .replace(/{{depressionRiskColor}}/gi, metrics.depression.color)
416
+ .replace("{{anxietyRiskScore}}", metrics.anxiety.scoreInteger)
417
+ .replace("{{anxietyRiskScoreDecimalPart}}", metrics.anxiety.scoreDecimal)
418
+ .replace("{{anxietyRiskScoreStatement}}", calculationData.anxietyRiskScoreStatement)
419
+ .replace("{{AnxietyRiskWord}}", localeService.t('anxietyRisk'))
420
+ .replace(/{{rotateAnxietyRiskAmount}}/gi, metrics.anxiety.rotateAmount)
421
+ .replace(/{{anxietyRiskColor}}/gi, metrics.anxiety.color)
422
+ .replace("{{physicalScore}}", metrics.physical.scoreInteger)
423
+ .replace("{{physicalScoreDecimalPart}}", metrics.physical.scoreDecimal)
424
+ .replace("{{physicalScoreStatement}}", calculationData.physicalScoreStatement)
425
+ .replace("{{PhysicalWord}}", localeService.t('physicalScore'))
426
+ .replace(/{{rotatePhysicalAmount}}/gi, metrics.physical.rotateAmount)
427
+ .replace(/{{physicalColor}}/gi, metrics.physical.color)
428
+ .replace("{{ScoreText}}", localeService.t('scoreText'))
429
+ .replace("{{lowScoreText}}", localeService.t('lowScoreText'))
430
+ .replace("{{PhysicalWellbeingText}}", localeService.t('PhysicalWellbeingText'))
431
+ .replace("{{PsychologicalWellbeingText}}", localeService.t('PsychologicalWellbeingText'))
432
+ .replace("{{psychologicalScore}}", metrics.psychological.scoreInteger)
433
+ .replace("{{psychologicalScoreDecimalPart}}", metrics.psychological.scoreDecimal)
434
+ .replace("{{psychologicalWord}}", localeService.t('psychological'))
435
+ .replace("{{psychologicalScoreStatement}}", calculationData.psychologicalScoreStatement)
436
+ .replace(/{{rotatePsychologicalAmount}}/gi, metrics.psychological.rotateAmount)
437
+ .replace(/{{fifthLevelColor}}/g, config.fifth_level_color)
438
+ .replace(/{{thirdLevelColor}}/g, config.third_level_color)
439
+ .replace(/{{secondLevelColor}}/g, config.second_level_color)
440
+ .replace(/{{forthLevelColor}}/g, config.forth_level_color)
441
+ .replace(/{{firstLevelColor}}/g, client === 'nasco' ? config.second_level_color : config.first_level_color)
442
+ .replace(/{{psychologicalColor}}/gi, metrics.psychological.color)
443
+ .replace(/{{depressionRiskColor}}/gi, metrics.depression.color)
444
+ .replace(/{{anxietyRiskColor}}/gi, metrics.anxiety.color)
445
+ .replace(/{{scoreHeaderColor}}/gi, config.score_header_color)
446
+ .replace(/{{svgIconColor}}/gi, config.svg_icon_color)
447
+ .replace(/{{lowScoreTextColor}}/gi, config.low_score_text_color)
448
+ .replace(/{{scoreDescriptionColor}}/gi, config.score_description_color)
449
+ .replace(/{{listIconsFontColor}}/g, config.list_icons_font_color)
450
+ .replace(/{{generalFontColor}}/g, config.general_font_color)
451
+ .replace(/{{sectionTitleBorderColor}}/g, config.section_title_border_color);
452
+ };
280
453
 
281
- $('.first-section-scores').html(firstSectionlHtml);
454
+ /**
455
+ * Load wellbeing templates based on language direction
456
+ * @param {boolean} isRtl - Whether the language is RTL
457
+ * @param {string} templatePath - Base template path (default: 'wellbeing')
458
+ * @returns {object} Object containing all loaded templates
459
+ */
460
+ const loadWellbeingTemplates = (isRtl, templatePath = 'wellbeing') => {
461
+ const basePath = path.resolve(__dirname + `/../templates/${templatePath}`);
462
+ return {
463
+ html: fs.readFileSync(
464
+ path.resolve(basePath, isRtl ? 'rtl_no_pages.html' : 'ltr_no_pages.html'),
465
+ 'utf8'
466
+ ),
467
+ header: fs.readFileSync(path.resolve(basePath, 'blocks/header.html'), 'utf8'),
468
+ footer: fs.readFileSync(path.resolve(basePath, 'blocks/footer.html'), 'utf8'),
469
+ firstSection: fs.readFileSync(path.resolve(basePath, 'blocks/first-section.html'), 'utf8'),
470
+ tips: fs.readFileSync(path.resolve(basePath, 'blocks/tips.html'), 'utf8')
471
+ };
472
+ };
282
473
 
283
- if (parseFloat(psychologicalScore) > 40 && parseFloat(psychologicalScore) < 70) {
284
- $(".first-section-scores .psychological-score-section .part-1,.first-section-scores .psychological-score-section .part-3").addClass('inactive')
285
- } else if (psychologicalScore >= 70) {
286
- $(".first-section-scores .psychological-score-section .part-3,.first-section-scores .psychological-score-section .part-2").addClass('inactive')
287
- } else {
288
- $(".first-section-scores .psychological-score-section .part-1,.first-section-scores .psychological-score-section .part-2").addClass('inactive')
474
+ /**
475
+ * Get user image based on gender and photo availability
476
+ * @param {string} photo - User photo URL
477
+ * @param {number} genderId - Gender ID (90 for female)
478
+ * @returns {string} Path to user image
479
+ */
480
+ const getUserImage = (photo, genderId) => {
481
+ if (!empty(photo)) {
482
+ return photo;
289
483
  }
290
484
 
485
+ const imageFolder = path.resolve(__dirname + '/../assets/wellbeing/images');
486
+ return genderId === 90
487
+ ? path.resolve(imageFolder, 'user-woman.png')
488
+ : path.resolve(imageFolder, 'user-man.png');
489
+ };
291
490
 
292
- if (parseFloat(physicalScore) > 40 && parseFloat(physicalScore) < 70) {
293
- $(".first-section-scores .physical-score-section .part-1,.first-section-scores .physical-score-section .part-3").addClass('inactive')
294
- } else if (physicalScore >= 70) {
295
- $(".first-section-scores .physical-score-section .part-3,.first-section-scores .physical-score-section .part-2").addClass('inactive')
296
- } else {
297
- $(".first-section-scores .physical-score-section .part-1,.first-section-scores .physical-score-section .part-2").addClass('inactive')
298
- }
299
- $(".score-main").addClass(client);
300
- $(".score-main").addClass(language);
301
- /* Second section*/
302
- let partScores = data.partsScore;
303
- let allScores = {'lifeStyle': [], 'body': [], 'mind': [], healthRisk: []}
304
- let tempScore = {
305
- 'lifeStyle': partScores.lifeStyle,
306
- 'body': partScores.body,
307
- 'mind': partScores.mind,
308
- 'healthRisk': partScores.healthRisk
491
+ /**
492
+ * Generate dynamic CSS styles for the report
493
+ * @param {string} iconColor - Color for icon elements
494
+ * @returns {string} CSS style string
495
+ */
496
+ const generateReportStyles = (iconColor) => {
497
+ const styles = {
498
+ '.tips-text': `color: ${config.insights_font_color} !important;`,
499
+ '.first-level-font-color': `color: ${config.general_font_color} !important;`,
500
+ '.first-section-scores': `border-color: ${config.first_section_scores_border_color} !important;`,
501
+ '.disclaimer-text-color': `color: ${config.disclaimer_text_color} !important;`,
502
+ '.first-level-border-color': `border-color: ${config.general_font_color} !important;`,
503
+ '.scores-note .first-level-font-color': `color: ${iconColor} !important;`,
504
+ '.top-box-bg': `background-color: ${config.top_box_background} !important;`,
505
+ '.footer-background': `background-color: ${config.footer_background} !important;`,
506
+ '.report-date, .patient-info, .score-description, .list-text, .report-header, .list-score, .score-details-title': `color: ${config.body_font_color} !important;`,
507
+ '.list-item g, .list-item path': `fill: ${config.list_icons_font_color} !important; stroke: ${config.list_icons_font_color} !important;`,
508
+ '.fifth-level-font-color': `color: ${config.fifth_level_color} !important;`
509
+ };
510
+
511
+ return '<style>' + Object.entries(styles).map(([selector, rules]) => `${selector}{${rules}}`).join('') + '</style>';
512
+ };
513
+
514
+ /**
515
+ * Process part scores and organize by category
516
+ * @param {object} partScores - Raw part scores data
517
+ * @returns {object} Organized scores by category
518
+ */
519
+ const processPartScores = (partScores) => {
520
+ const categories = {
521
+ lifeStyle: [],
522
+ body: [],
523
+ mind: [],
524
+ healthRisk: []
309
525
  };
310
- let body = [];
311
- let lifeStyle = [];
312
- let mind = [];
313
- let healthRisk = []
314
- Object.entries(partScores).map(([key, value], index) => {
526
+
527
+ Object.entries(partScores).forEach(([key, items]) => {
315
528
  if (key === 'body' || key === 'lifeStyle' || key === 'mind') {
316
- partScores[key].map((item, index) => {
317
- let iconName = item['icon'].split(" ").join("")
318
- let name = iconName.split(".png", [1]);
319
- let value = icons[name];
320
- if (empty(value)) {
321
- console.log("missing icon", name)
322
- }
529
+ items.forEach(item => {
530
+ const iconName = item['icon'].replace(/\s/g, '');
531
+ const name = iconName.split('.png')[0];
532
+ const iconPath = icons[name];
323
533
 
324
- let tempScore = {
325
- percentage: Math.round(item['score'] / 2), icon: value, scoreText: item['scoreText'],
534
+ if (empty(iconPath)) {
535
+ console.log('Missing icon:', name);
536
+ }
326
537
 
538
+ const scoreData = {
539
+ percentage: Math.round(item['score'] / 2),
540
+ icon: iconPath,
541
+ scoreText: item['scoreText']
327
542
  };
328
- if (key === 'body') {
329
- body.push(tempScore)
330
- } else if (key === 'lifeStyle') {
331
- lifeStyle.push(tempScore)
332
- } else if (key === 'mind') {
333
- mind.push(tempScore)
334
- } else {
335
- healthRisk.push(tempScore)
336
- }
337
543
 
338
- })
339
- }
340
- });
341
- tempScore['lifeStyle'] = lifeStyle;
342
- tempScore['body'] = body;
343
- tempScore['mind'] = mind;
344
- tempScore['healthRisk'] = healthRisk;
345
- // $('#content').append("<div class='row scores-parts'>" + renderItems(allScores, partScores) + "</div>");
346
- debugLog("after signature");
347
- /*Footer*/
348
-
349
- $('#content').after("<h5 class='recommended-tips-title first-level-font-color'>" + localeService.t('weRecommend') + "</h5>");
350
- $('.footer').append(footerTemplateHTML);
351
- /*tips*/
352
- let tips = data.insights.tips
353
- let allTips = []
354
- let lifeStyleTips = []
355
- let mindTips = []
356
- let bodyTips = []
357
- let i = 0;
358
- tips.map((key, index) => {
359
- if (key.partId === 1 || key.partId === 2 || key.partId === 3) {
360
- if (key.partId === 1) {
361
- key.type = 'lifeStyleInsights'
362
- lifeStyleTips.push(key)
363
- }
364
- if (key.partId === 2) {
365
- key.type = 'bodyInsights'
366
- bodyTips.push(key)
367
- }
368
- if (key.partId === 3) {
369
- key.type = 'mindInsights'
370
- mindTips.push(key)
371
- }
544
+ categories[key].push(scoreData);
545
+ });
372
546
  }
373
547
  });
374
548
 
375
- allTips.push(lifeStyleTips)
376
- allTips.push(bodyTips)
377
- allTips.push(mindTips)
378
- $('.all-tips').append(tipsTemplate)
379
- allTips.forEach((tip, index) => {
380
- $(".tips-section .three-column").append("<div class='left-align'>" + renderInsightTips(tip) + "</div>");
381
- })
549
+ return categories;
550
+ };
382
551
 
383
- $("#chartsData").append("<script> var allScores = " + JSON.stringify(tempScore) + "</script>");
552
+ /**
553
+ * Organize tips by part ID
554
+ * @param {array} tips - Array of tip objects
555
+ * @returns {object} Tips organized by category
556
+ */
557
+ const organizeTipsByPart = (tips) => {
558
+ const organized = {
559
+ lifeStyle: [],
560
+ body: [],
561
+ mind: []
562
+ };
384
563
 
385
- let OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output`);
386
- if (!fs.existsSync(OUTPUT_DIRECTORY)) {
387
- fs.mkdirSync(OUTPUT_DIRECTORY)
388
- }
389
- await promisify(fs.writeFile)(OUT_FILE, dom.serialize());
564
+ const partMapping = {
565
+ 1: 'lifeStyle',
566
+ 2: 'body',
567
+ 3: 'mind'
568
+ };
390
569
 
570
+ tips.forEach(tip => {
571
+ const category = partMapping[tip.partId];
572
+ if (category) {
573
+ tip.type = `${category}Insights`;
574
+ organized[category].push(tip);
575
+ }
576
+ });
391
577
 
392
- let REQUEST_OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output/request`);
393
- if (!fs.existsSync(REQUEST_OUTPUT_DIRECTORY)) {
394
- fs.mkdirSync(REQUEST_OUTPUT_DIRECTORY)
395
- }
578
+ return organized;
579
+ };
396
580
 
397
- return {
398
- "html": dom.serialize(),
399
- "outputFile": OUT_FILE,
400
- "header": headerTemplate,
401
- "footer": footerTemplate,
402
- "historyData": historyData,
581
+ /**
582
+ * Render header template with data
583
+ * @param {string} template - Header template
584
+ * @param {object} data - Data object containing patient info
585
+ * @param {string} logo - Logo path
586
+ * @param {string} userImage - User image path
587
+ * @param {string} topInsight - Top insight text
588
+ * @returns {string} Rendered header HTML
589
+ */
590
+ const renderHeaderTemplate = (template, data, logo, userImage, topInsight) => {
591
+ return template
592
+ .replace('{{logo}}', logo)
593
+ .replace('{{userImage}}', userImage)
594
+ .replace('{{topBoxBg}}', config.top_box_background)
595
+ .replace('{{scoresNote}}', localeService.t('scoresNote'))
596
+ .replace('{{bulbIcon}}', config.bulbIcon)
597
+ .replace('{{reportHeader}}', localeService.t('wellbeingReport'))
598
+ .replace('{{reportHeaderDescription}}', topInsight)
599
+ .replace('{{patientName}}', data.name)
600
+ .replace('{{patientOld}}', data.age)
601
+ .replace('{{yearsOld}}', localeService.t('yearsOld'))
602
+ .replace('{{patientGender}}', data.gender);
603
+ };
604
+
605
+ /**
606
+ * Load extended templates for smart report
607
+ * @param {boolean} isRtl - Whether the language is RTL
608
+ * @param {string} clientName - Client name for template path
609
+ * @param {boolean} hasProfileInfo - Whether profile info exists
610
+ * @returns {object} Object containing all loaded templates
611
+ */
612
+ const loadExtendedTemplates = (isRtl, clientName, hasProfileInfo = false) => {
613
+ const basePath = path.resolve(__dirname + `/../templates/${clientName}`);
614
+
615
+ const templates = {
616
+ html: fs.readFileSync(
617
+ path.resolve(basePath, isRtl ? 'rtl_no_pages.html' : 'ltr_no_pages.html'),
618
+ 'utf8'
619
+ ),
620
+ header: fs.readFileSync(path.resolve(basePath, 'blocks/header.html'), 'utf8'),
621
+ pdfHeader: fs.readFileSync(path.resolve(basePath, 'blocks/header-template.html'), 'utf8'),
622
+ footer: fs.readFileSync(path.resolve(basePath, 'blocks/footer.html'), 'utf8'),
623
+ firstSection: fs.readFileSync(path.resolve(basePath, 'blocks/first-section.html'), 'utf8'),
624
+ tips: fs.readFileSync(path.resolve(basePath, 'blocks/tips.html'), 'utf8'),
625
+ patientTable: ''
403
626
  };
404
- }
405
627
 
406
- let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clientName, language) => {
407
- debug = isDebugging;
408
- OUT_FILE = path.resolve(__dirname + `/../output/${clientName}-pdf` + Date.now() + '.html');
409
- Pdf_file = `/output/${clientName}-pdf` + Date.now() + '.html'
410
- let wellbeingData = data.wellbeing ? data.wellbeing : {}
411
- let profileInfo = data.profileInfo ? data.profileInfo : {}
412
- let doctorData = data.IsDoctor ? data.Profile : []
413
- let smartReport = data.SR ? JSON.parse(data.SR) : {}
414
- if (empty(language)) {
415
- language = 'en'
416
- }
417
- let isRtl = false;
418
- if (language.indexOf('ar') !== -1) {
419
- language = 'ar-SA';
420
- isRtl = true;
628
+ if (hasProfileInfo) {
629
+ templates.patientTable = fs.readFileSync(path.resolve(basePath, 'blocks/patient-table.html'), 'utf8');
421
630
  }
422
- let client = clientName ? clientName : 'default'
423
- let pathFile = path.resolve(__dirname + '/../config/' + client + '.json');
424
- let defaultConfig = require('../config/default.json');
425
- if (fs.existsSync(pathFile)) {
426
- defaultConfig = require('../config/' + client + '.json');
427
- }
428
- logo = defaultConfig['logo'];
429
- title = defaultConfig['title'];
430
- bulbIcon = defaultConfig['bulb_icon'];
431
- bulb_icon_font_color = defaultConfig['bulb_icon_font_color'];
432
- first_level_color = defaultConfig['first-level-color']
433
- second_level_color = defaultConfig['second-level-color']
434
- third_level_color = defaultConfig['third-level-color']
435
- forth_level_color = defaultConfig['forth-level-color']
436
- fifth_level_color = defaultConfig['fifth-level-color']
437
- general_font_color = defaultConfig['general-font-color']
438
- disclaimer_text_color = defaultConfig['disclaimer-text-color']
439
- top_box_background = defaultConfig['top-box-background']
440
- footer_background = defaultConfig['footer-background']
441
- lifestyleImage = path.resolve(__dirname + '/../assets/wellbeing/images/lifestyle-section.png')
442
- mindImage = path.resolve(__dirname + '/../assets/wellbeing/images/mind-section.png')
443
- bodyImage = path.resolve(__dirname + '/../assets/wellbeing/images/body-section.png')
444
- body_font_color = defaultConfig['body_font_color']
445
- list_icons_font_color = defaultConfig['list_icons_font_color']
446
- insights_font_color = defaultConfig['insights_font_color']
447
- /*check if the language is an RTL language*/
448
- localeService.setLocale(language);
449
- debugLog("pdf language:", language);
450
- /*choose the right template*/
451
- let html = !isRtl ? fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/ltr_no_pages.html`), 'utf8') : fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/rtl_no_pages.html`), 'utf8');
452
- let headerTemplate = fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/blocks/header.html`), 'utf8');
453
- let pdfHeader = fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/blocks/header-template.html`), 'utf8');
454
- let patientTableTemplate = ''
455
- if (!empty(profileInfo)) patientTableTemplate = fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/blocks/patient-table.html`), 'utf8');
456
-
457
- let footerTemplate = fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/blocks/footer.html`), 'utf8');
458
- let firstSectionTemplate = fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/blocks/first-section.html`), 'utf8');
459
- let tipsTemplate = fs.readFileSync(path.resolve(__dirname + `/../templates/${clientName}/blocks/tips.html`), 'utf8');
460
- /*create the dom element*/
461
- const dom = new JSDOM(html);
462
- let historyData = [];
463
- debugLog("templates loaded");
464
- let img = logo;
465
-
466
- pdfHeader = pdfHeader.replace("{{logo}}", img)
467
- .replace("{{clientName}}", localeService.t(title))
468
- .replace("{{notificationTitle}}", localeService.t('notificationTitle'));
469
-
470
- headerTemplate = headerTemplate
471
- .replace("{{logo}}", img)
472
- .replace("{{clientName}}", localeService.t(title))
473
- .replace("{{notificationTitle}}", localeService.t('notificationTitle'));
474
- let pdfHeaderData = profileInfo;
475
- let headerData = []
476
- for (let index = 0; index < 4; index++) {
477
- const element = profileInfo[index];
631
+
632
+ return templates;
633
+ };
634
+
635
+ /**
636
+ * Generate header info HTML from profile data
637
+ * @param {array} profileInfo - Array of profile information
638
+ * @param {number} count - Number of items to process (default: 4)
639
+ * @returns {string} Generated HTML
640
+ */
641
+ const generateHeaderInfo = (profileInfo, count = 4) => {
642
+ let html = '';
643
+
644
+ for (let i = 0; i < Math.min(count, profileInfo.length); i++) {
645
+ const element = profileInfo[i];
478
646
  if (element) {
479
- headerData += `
647
+ html += `
480
648
  <div class="info-group">
481
649
  <div class="info-title">${element.title}:</div>
482
650
  <div class="info-value">${element.value}</div>
@@ -484,222 +652,124 @@ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clien
484
652
  `;
485
653
  }
486
654
  }
487
- const $ = (require('jquery'))(dom.window);
488
- $('#content').append(headerTemplate);
489
- $('.report-info').html(headerData);
490
- let iconColor = !empty(bulb_icon_font_color) ? bulb_icon_font_color : general_font_color
491
- /*get the window from the dom to be used as a jquery function*/
492
- $('#content').append('<style>' + '.tips-text{color: ' + insights_font_color + ' !important;}' + '.first-level-font-color{color:' + general_font_color + ' !important;}' + '.first-section-scores{border-color:' + fifth_level_color + ' !important;}' + '.disclaimer-text-color{color:' + disclaimer_text_color + ' !important;}' + '.first-level-border-color{border-color:' + general_font_color + ' !important;}' + '.scores-note .first-level-font-color{color:' + iconColor + ' !important;}' + '.top-box-bg{background-color: ' + top_box_background + ' !important;}' + '.footer-background{background-color: ' + footer_background + ' !important;}' + '.report-date, .patient-info,.score-description,.list-text,.report-header,.list-score,.score-details-title{color: ' + body_font_color + ' !important;}' + '.list-item g, .list-item path{fill: ' + list_icons_font_color + ' !important;stroke: ' + list_icons_font_color + ' !important;}' + '.fifth-level-font-color{color:' + fifth_level_color + '!important;}' + '</style>');
493
- if (empty(wellbeingData.photo)) {
494
-
495
- $('#content').append('<style>' + '.user-profile-img{display: none !important;}' + '</style>');
496
-
497
- }
498
- $("#chartsData").append("<script> var RANGE_COLORS = ['" + first_level_color + "','" + second_level_color + "','" + third_level_color + "','" + forth_level_color + "','" + fifth_level_color + "'];</script>");
499
- let firstSectionlHtml = ''
500
- if (!empty(wellbeingData)) {
501
- /* First section */
502
- let physicalScore = wellbeingData.calculation.physicalScore?.toFixed(1)
503
- let physicalAmount = ((physicalScore > 40 && physicalScore < 70) ? 310 : (physicalScore >= 70) ? 310 : 300);
504
- let physicalColor = (physicalScore > 40 && physicalScore < 70) ? third_level_color : (physicalScore >= 70) ? fifth_level_color : client === 'nasco' ? second_level_color : first_level_color;
505
-
506
- let rotatePhysicalAmount = (physicalScore / 100) * physicalAmount;
507
- if (physicalScore <= 20) {
508
- rotatePhysicalAmount += 25
509
- }
510
655
 
511
- if (rotatePhysicalAmount > 336) {
512
- rotatePhysicalAmount = 336
513
- }
514
-
515
- let depressionRiskScore = wellbeingData.calculation.depressionRiskScore?.toFixed(1)
516
- const depressionRiskAmount = (depressionRiskScore >= 0 && depressionRiskScore <= 14.8) ? 550 : (depressionRiskScore >= 18.5 && depressionRiskScore <= 33.3) ? 400 : (depressionRiskScore >= 37 && depressionRiskScore <= 51.9) ? 380 : (depressionRiskScore >= 55.6 && depressionRiskScore <= 70.4) ? 370 : (depressionRiskScore >= 74.1 && depressionRiskScore <= 100) ? 330 : 0;
517
- let depressionRiskColor = (depressionRiskScore > 40 && depressionRiskScore < 70) ? third_level_color : (depressionRiskScore >= 70) ? fifth_level_color : client === 'nasco' ? second_level_color : first_level_color;
518
-
519
- let rotateDepressionRiskAmount = (depressionRiskScore / 100) * depressionRiskAmount;
656
+ return html;
657
+ };
520
658
 
659
+ /**
660
+ * Generate extended report styles with additional properties
661
+ * @param {string} iconColor - Color for icon elements
662
+ * @returns {string} CSS style string
663
+ */
664
+ const generateExtendedReportStyles = (iconColor) => {
665
+ const styles = {
666
+ '.tips-text': `color: ${config.insights_font_color} !important;`,
667
+ '.first-level-font-color': `color: ${config.general_font_color} !important;`,
668
+ '.first-section-scores': `border-color: ${config.section_title_border_color} !important;`,
669
+ '.physical-score-section': `border-color: ${config.section_title_border_color} !important;`,
670
+ '.psychological-score-section, .psychological-score-section1, .psychological-score-section2': `border-color: ${config.body_font_color} !important;`,
671
+ '.row.score-section': `border-color: ${config.section_title_border_color} !important;`,
672
+ '.disclaimer-text-color': `color: ${config.disclaimer_text_color} !important;`,
673
+ '.first-level-border-color': `border-color: ${config.general_font_color} !important;`,
674
+ '.scores-note .first-level-font-color': `color: ${iconColor} !important;`,
675
+ '.top-box-bg': `background-color: ${config.top_box_background} !important;`,
676
+ '.footer-background': `background-color: ${config.footer_background} !important;`,
677
+ '.print-page-footer': `border-color: ${config.footer_border_color} !important;`,
678
+ '.report-date, .patient-info, .score-description, .list-text, .report-header, .list-score, .score-details-title': `color: ${config.body_font_color} !important;`,
679
+ '.list-item g, .list-item path': `fill: ${config.list_icons_font_color} !important; stroke: ${config.list_icons_font_color} !important;`,
680
+ '.fifth-level-font-color': `color: ${config.fifth_level_color} !important;`
681
+ };
521
682
 
522
- let anxietyRiskScore = wellbeingData.calculation.anxietyRiskScore?.toFixed(1)
523
- const anxietyRiskAmount = (anxietyRiskScore >= 0 && anxietyRiskScore <= 19) ? 460 : (anxietyRiskScore >= 23.8 && anxietyRiskScore <= 42.9) ? 400 : (anxietyRiskScore >= 47.6 && anxietyRiskScore <= 66.7) ? 360 : (anxietyRiskScore >= 71.4 && anxietyRiskScore <= 100) ? 330 : 0;
683
+ return '<style>' + Object.entries(styles).map(([selector, rules]) => `${selector}{${rules}}`).join('') + '</style>';
684
+ };
524
685
 
525
- let anxietyRiskColor = (anxietyRiskScore > 40 && anxietyRiskScore < 70) ? third_level_color : (anxietyRiskScore >= 70) ? fifth_level_color : client === 'nasco' ? second_level_color : first_level_color;
686
+ /**
687
+ * Process extended part scores with base division
688
+ * @param {object} partScores - Raw part scores data
689
+ * @returns {object} Organized scores by category
690
+ */
691
+ const processExtendedPartScores = (partScores) => {
692
+ const categories = {
693
+ lifeStyle: [],
694
+ body: [],
695
+ mind: [],
696
+ healthRisk: []
697
+ };
526
698
 
527
- let rotateAnxietyRiskAmount = (anxietyRiskScore / 100) * anxietyRiskAmount;
699
+ Object.entries(partScores).forEach(([key, items]) => {
700
+ if (key === 'body' || key === 'lifeStyle' || key === 'mind') {
701
+ items.forEach(item => {
702
+ const iconName = item['icon'].replace(/\s/g, '');
703
+ const name = iconName.split('.png')[0];
704
+ const iconPath = icons[name];
528
705
 
529
- let psychologicalScore = wellbeingData.calculation.psychologicalScore?.toFixed(1)
530
- let psychologicalAmount = ((psychologicalScore > 40 && psychologicalScore < 70) ? 355 : (psychologicalScore >= 70) ? 360 : 214);
531
- let psychologicalColor = (psychologicalScore > 40 && psychologicalScore < 70) ? third_level_color : (psychologicalScore >= 70) ? fifth_level_color : client === 'nasco' ? second_level_color : first_level_color;
532
- let rotatePsychologicalAmount = (psychologicalScore / 100) * psychologicalAmount;
533
- if (rotatePsychologicalAmount > 336) {
534
- rotatePsychologicalAmount = 336
535
- }
706
+ if (empty(iconPath)) {
707
+ console.log('Missing icon:', name);
708
+ }
536
709
 
537
- firstSectionlHtml = firstSectionTemplate.replace("{{depressionRiskScore}}", parseFloat(Math.floor(wellbeingData.calculation.depressionRiskScore)))
538
- .replace("{{depressionRiskScoreDecimalPart}}", parseFloat((wellbeingData.calculation.depressionRiskScore)?.toFixed(1) % 1)?.toFixed(1).substring(1))
539
- .replace("{{depressionRiskScoreStatement}}", wellbeingData.calculation.depressionRiskScoreStatement)
540
- .replace("{{DepressionRiskWord}}", localeService.t('depressionRisk'))
541
- .replace(/{{rotateDepressionRiskAmount}}/gi, rotateDepressionRiskAmount)
542
- .replace(/{{depressionRiskColor}}/gi, depressionRiskColor)
543
- .replace("{{anxietyRiskScore}}", parseFloat(Math.floor(wellbeingData.calculation.anxietyRiskScore)))
544
- .replace("{{anxietyRiskScoreDecimalPart}}", parseFloat((wellbeingData.calculation.anxietyRiskScore)?.toFixed(1) % 1)?.toFixed(1).substring(1))
545
- .replace("{{anxietyRiskScoreStatement}}", wellbeingData.calculation.anxietyRiskScoreStatement)
546
- .replace("{{AnxietyRiskWord}}", localeService.t('anxietyRisk'))
547
- .replace(/{{rotateAnxietyRiskAmount}}/gi, rotateAnxietyRiskAmount)
548
- .replace(/{{anxietyRiskColor}}/gi, anxietyRiskColor)
549
- .replace("{{physicalScore}}", parseFloat(Math.floor(wellbeingData.calculation.physicalScore)))
550
- .replace("{{physicalScoreDecimalPart}}", parseFloat((wellbeingData.calculation.physicalScore)?.toFixed(1) % 1)?.toFixed(1).substring(1))
551
- .replace("{{physicalScoreStatement}}", wellbeingData.calculation.physicalScoreStatement)
552
- .replace("{{PhysicalWord}}", localeService.t('physicalScore'))
553
- .replace(/{{rotatePhysicalAmount}}/gi, rotatePhysicalAmount)
554
- .replace(/{{physicalColor}}/gi, physicalColor)
555
- .replace("{{ScoreText}}", localeService.t('scoreText'))
556
- .replace("{{lowScoreText}}", localeService.t('lowScoreText'))
557
- .replace("{{PhysicalWellbeingText}}", localeService.t('PhysicalWellbeingText'))
558
- .replace("{{PsychologicalWellbeingText}}", localeService.t('PsychologicalWellbeingText'))
559
- .replace("{{psychologicalScore}}", Math.floor(wellbeingData.calculation.psychologicalScore))
560
- .replace("{{psychologicalScoreDecimalPart}}", parseFloat((wellbeingData.calculation.psychologicalScore)?.toFixed(1) % 1)?.toFixed(1).substring(1))
561
- .replace("{{psychologicalWord}}", localeService.t('psychological'))
562
- .replace("{{psychologicalScoreStatement}}", wellbeingData.calculation.psychologicalScoreStatement)
563
- .replace(/{{rotatePsychologicalAmount}}/gi, rotatePsychologicalAmount)
564
- .replace(/{{fifthLevelColor}}/g, fifth_level_color)
565
- .replace(/{{thirdLevelColor}}/g, third_level_color)
566
- .replace(/{{secondLevelColor}}/g, second_level_color)
567
- .replace(/{{forthLevelColor}}/g, forth_level_color)
568
- .replace(/{{firstLevelColor}}/g, client === 'nasco' ? second_level_color : first_level_color)
569
- .replace("{{psychologicalColor}}", psychologicalColor)
570
- .replace(/{{listIconsFontColor}}/g, list_icons_font_color);
571
- $('.first-section-scores').html(firstSectionlHtml);
572
- if (parseFloat(psychologicalScore) > 40 && parseFloat(psychologicalScore) < 70) {
573
- $(".first-section-scores .psychological-score-section .part-1,.first-section-scores .psychological-score-section .part-3").addClass('inactive')
574
- } else if (psychologicalScore >= 70) {
575
- $(".first-section-scores .psychological-score-section .part-3,.first-section-scores .psychological-score-section .part-2").addClass('inactive')
576
- } else {
577
- $(".first-section-scores .psychological-score-section .part-1,.first-section-scores .psychological-score-section .part-2").addClass('inactive')
578
- }
710
+ const scoreData = {
711
+ percentage: Math.round(item['score'] / (item['base'] / 5)),
712
+ icon: iconPath,
713
+ scoreText: item['scoreText'],
714
+ key
715
+ };
579
716
 
580
- if (parseFloat(depressionRiskScore) > 40 && parseFloat(depressionRiskScore) < 70) {
581
- $(".first-section-scores .psychological-score-section1 .part-1,.first-section-scores .psychological-score-section1 .part-3").addClass('inactive')
582
- } else if (depressionRiskScore >= 70) {
583
- $(".first-section-scores .psychological-score-section1 .part-3,.first-section-scores .psychological-score-section1 .part-2").addClass('inactive')
584
- } else {
585
- $(".first-section-scores .psychological-score-section1 .part-1,.first-section-scores .psychological-score-section1 .part-2").addClass('inactive')
717
+ categories[key].push(scoreData);
718
+ });
586
719
  }
720
+ });
587
721
 
588
- if (parseFloat(anxietyRiskScore) > 40 && parseFloat(anxietyRiskScore) < 70) {
589
-
590
- $(".first-section-scores .psychological-score-section2 .part-1,.first-section-scores .psychological-score-section2 .part-3").addClass('inactive')
591
- } else if (anxietyRiskScore >= 70) {
592
- $(".first-section-scores .psychological-score-section2 .part-3,.first-section-scores .psychological-score-section2 .part-2").addClass('inactive')
593
- } else {
594
- $(".first-section-scores .psychological-score-section2 .part-1,.first-section-scores .psychological-score-section2 .part-2").addClass('inactive')
595
- }
722
+ return categories;
723
+ };
596
724
 
597
- if (parseFloat(physicalScore) > 40 && parseFloat(physicalScore) < 70) {
598
- $(".first-section-scores .physical-score-section .part-1,.first-section-scores .physical-score-section .part-3").addClass('inactive')
599
- } else if (physicalScore >= 70) {
600
- $(".first-section-scores .physical-score-section .part-3,.first-section-scores .physical-score-section .part-2").addClass('inactive')
601
- } else {
602
- $(".first-section-scores .physical-score-section .part-1,.first-section-scores .physical-score-section .part-2").addClass('inactive')
603
- }
725
+ /**
726
+ * Organize tips by part ID for extended report
727
+ * @param {array} tips - Array of tip objects
728
+ * @returns {object} Tips organized by category
729
+ */
730
+ const organizeExtendedTipsByPart = (tips) => {
731
+ const organized = {
732
+ lifeStyle: [],
733
+ body: [],
734
+ mind: []
735
+ };
604
736
 
605
- $(".score-main").addClass(client);
606
- $(".score-main").addClass(language);
607
- /* Second section*/
608
- let partScores = wellbeingData.partsScore;
609
- let allScores = {'lifeStyle': [], 'body': [], 'mind': [], healthRisk: []}
610
- let tempScore = {
611
- 'lifeStyle': partScores.lifeStyle,
612
- 'body': partScores.body,
613
- 'mind': partScores.mind,
614
- 'healthRisk': partScores.healthRisk
615
- };
616
- let body = [];
617
- let lifeStyle = [];
618
- let mind = [];
619
- let healthRisk = []
620
- Object.entries(partScores).map(([key, value], index) => {
621
- if (key === 'body' || key === 'lifeStyle' || key === 'mind') {
622
- partScores[key].map((item, index) => {
623
- let iconName = item['icon'].split(" ").join("")
624
- let name = iconName.split(".png", [1]);
625
- let value = icons[name];
626
- if (empty(value)) {
627
- console.log("missing icon", name)
628
- }
629
- let tempScore = {
630
- percentage: Math.round(item['score'] / (item['base'] / 5)),
631
- icon: value,
632
- scoreText: item['scoreText'],
633
- key
634
- };
635
- if (key === 'body') {
636
- body.push(tempScore)
637
- } else if (key === 'lifeStyle') {
638
- lifeStyle.push(tempScore)
639
- } else if (key === 'mind') {
640
- mind.push(tempScore)
641
- } else {
642
- healthRisk.push(tempScore)
643
- }
644
- })
645
- }
646
- });
647
- tempScore['lifeStyle'] = lifeStyle;
648
- tempScore['body'] = body;
649
- tempScore['mind'] = mind;
650
- tempScore['healthRisk'] = healthRisk;
651
- $('#content').append("<div class='row scores-parts' style='width:99.5% !important;'>" + renderItems(allScores, partScores) + "</div>");
652
- debugLog("after signature");
653
- let tips = wellbeingData.insights.tips
654
- if (!empty(tips)) $('#content').after("<h5 class='recommended-tips-title'>" + localeService.t('weRecommend') + "</h5>");
655
- let allTips = []
656
- let lifeStyleTips = []
657
- let mindTips = []
658
- let bodyTips = []
659
- let i = 0;
660
- if (!empty(tips)) {
661
- tips.map((key, index) => {
662
- if (key.partId === 1 || key.partId === 2 || key.partId === 3) {
663
- if (key.partId === 1) {
664
- key.type = 'lifeStyleInsights'
665
- lifeStyleTips.push(key)
666
- }
667
- if (key.partId === 3) {
668
- key.type = 'bodyInsights'
669
- bodyTips.push(key)
670
- }
671
- if (key.partId === 2) {
672
- key.type = 'mindInsights'
673
- mindTips.push(key)
674
- }
675
- }
676
- });
737
+ const partMapping = {
738
+ 1: 'lifeStyle',
739
+ 3: 'body',
740
+ 2: 'mind'
741
+ };
677
742
 
678
- allTips.push(lifeStyleTips)
679
- allTips.push(bodyTips)
680
- allTips.push(mindTips)
681
- $('.all-tips').append(tipsTemplate)
682
- allTips.forEach((tip, index) => {
683
- $(".tips-section .three-column").append("<div class='left-align'>" + renderInsightTips(tip, client) + "</div>");
684
- })
743
+ tips.forEach(tip => {
744
+ const category = partMapping[tip.partId];
745
+ if (category) {
746
+ tip.type = `${category}Insights`;
747
+ organized[category].push(tip);
685
748
  }
686
- $("#chartsData").append("<script> var allScores = " + JSON.stringify(tempScore) + "</script>");
687
-
688
- }
689
- if (empty(wellbeingData) || empty(wellbeingData?.insights?.tips)) {
749
+ });
690
750
 
691
- $('.footer-background').css('display', 'none')
692
- if (empty(wellbeingData)) $('.first-section-scores').css('display', 'none')
751
+ return organized;
752
+ };
693
753
 
754
+ /**
755
+ * Render patient table from profile info
756
+ * @param {array} profileInfo - Profile information array
757
+ * @param {number} startIndex - Start index for processing (default: 4)
758
+ * @returns {string} Rendered patient table HTML
759
+ */
760
+ const renderPatientTable = (profileInfo, startIndex = 4) => {
761
+ if (empty(profileInfo) || profileInfo.length <= startIndex) {
762
+ return '';
694
763
  }
695
764
 
696
- if (!empty(profileInfo)) {
697
- let details1HTML = "";
698
- let details2HTML = "";
699
-
765
+ let details1HTML = '';
766
+ let details2HTML = '';
767
+ const midPoint = Math.min(8, profileInfo.length);
700
768
 
701
- for (let i = 4; i < 8; i++) {
702
- const item = profileInfo[i];
769
+ // First column (items 4-7)
770
+ for (let i = startIndex; i < midPoint; i++) {
771
+ const item = profileInfo[i];
772
+ if (item) {
703
773
  const title = escapeHTML(item.title);
704
774
  details1HTML += `
705
775
  <div class="details-row">
@@ -708,342 +778,700 @@ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clien
708
778
  </div>
709
779
  `;
710
780
  }
781
+ }
711
782
 
712
- for (let i = 8; i < profileInfo.length; i++) {
713
- const item = profileInfo[i];
783
+ // Second column (items 8+)
784
+ for (let i = midPoint; i < profileInfo.length; i++) {
785
+ const item = profileInfo[i];
786
+ if (item) {
714
787
  const title = escapeHTML(item.title);
715
788
  details2HTML += `
716
789
  <div class="details-row">
717
- <div class="details-label" style=" unicode-bidi: isolate;">${wrapLTR(title)}</div>
790
+ <div class="details-label" style="unicode-bidi: isolate;">${wrapLTR(title)}</div>
718
791
  <div class="details-value">${item.value}</div>
719
792
  </div>
720
793
  `;
721
794
  }
722
- $('.patient-table').html(`<div class="patient-details"> <div class="details-column"> ${details1HTML} </div> <div class="details-column" style="margin-left:1%"> ${details2HTML} </div> </div>`);
723
795
  }
724
796
 
725
- if (!empty(doctorData)) {
726
- let details1HTML = "";
727
- let details2HTML = "";
797
+ return `
798
+ <div class="patient-details">
799
+ <div class="details-column">${details1HTML}</div>
800
+ <div class="details-column" style="margin-left:1%">${details2HTML}</div>
801
+ </div>
802
+ `;
803
+ };
804
+
805
+ /**
806
+ * Render doctor quiz details
807
+ * @param {array} doctorData - Doctor data array
808
+ * @returns {string} Rendered doctor details HTML
809
+ */
810
+ const renderDoctorDetails = (doctorData) => {
811
+ if (empty(doctorData)) {
812
+ return '';
813
+ }
728
814
 
729
- const halfLength = Math.ceil(doctorData.length / 2);
815
+ let details1HTML = '';
816
+ let details2HTML = '';
817
+ const halfLength = Math.ceil(doctorData.length / 2);
730
818
 
731
- for (let i = 0; i < halfLength; i++) {
732
- const item = doctorData[i];
733
- details1HTML += `
819
+ // First column
820
+ for (let i = 0; i < halfLength; i++) {
821
+ const item = doctorData[i];
822
+ details1HTML += `
734
823
  <div class="details-row page-break">
735
824
  <div class="question-title">${item.title}</div>
736
825
  <div class="details-value">${item.value}</div>
737
826
  </div>
738
827
  `;
739
- }
828
+ }
740
829
 
741
- for (let i = halfLength; i < doctorData.length; i++) {
742
- const item = doctorData[i];
743
- details2HTML += `
830
+ // Second column
831
+ for (let i = halfLength; i < doctorData.length; i++) {
832
+ const item = doctorData[i];
833
+ details2HTML += `
744
834
  <div class="details-row page-break">
745
835
  <div class="question-title">${item.title}</div>
746
836
  <div class="details-value">${item.value}</div>
747
837
  </div>
748
838
  `;
749
- }
839
+ }
750
840
 
751
- const finalHTML = `
752
- <p class="resume-title"> Quiz Answers</p>
753
- <div class="details-column">
754
- ${details1HTML}
755
- </div>
756
- <div class="details-column" style="margin-left: 1%">
757
- ${details2HTML}
758
- </div>
841
+ return `
842
+ <p class="resume-title">Quiz Answers</p>
843
+ <div class="details-column">${details1HTML}</div>
844
+ <div class="details-column" style="margin-left: 1%">${details2HTML}</div>
759
845
  `;
846
+ };
847
+
848
+ /**
849
+ * Render PDF header template
850
+ * @param {string} template - PDF header template
851
+ * @param {string} logo - Logo path
852
+ * @param {string} title - Client title
853
+ * @returns {string} Rendered PDF header
854
+ */
855
+ const renderPdfHeaderTemplate = (template, logo, title) => {
856
+ return template
857
+ .replace('{{logo}}', logo)
858
+ .replace('{{clientName}}', localeService.t(title))
859
+ .replace('{{notificationTitle}}', localeService.t('notificationTitle'))
860
+ .replace(/{{sectionTitleBorderColor}}/g, config.section_title_border_color);
861
+ };
862
+
863
+ /**
864
+ * Render basic header template for extended report
865
+ * @param {string} template - Header template
866
+ * @param {string} logo - Logo path
867
+ * @param {string} title - Client title
868
+ * @returns {string} Rendered header
869
+ */
870
+ const renderBasicHeaderTemplate = (template, logo, title) => {
871
+ return template
872
+ .replace('{{logo}}', logo)
873
+ .replace('{{clientName}}', localeService.t(title))
874
+ .replace('{{notificationTitle}}', localeService.t('notificationTitle'));
875
+ };
876
+
877
+ /***
878
+ * HTML file generator to fill the html template
879
+ * based on the json wellbeingData from the POST request
880
+ */
881
+ /**
882
+ * Generate HTML wellbeing report
883
+ * @param {object} data - Report data
884
+ * @param {boolean} isDebugging - Debug mode flag
885
+ * @param {string} clientName - Client name for configuration
886
+ * @param {string} language - Report language
887
+ * @returns {object} Generated report data
888
+ */
889
+ let generateHTMLWellbeingReport = async (data, isDebugging, clientName, language) => {
890
+ // Setup
891
+ debug = isDebugging;
892
+ const client = clientName || 'default';
893
+ OUT_FILE = path.resolve(__dirname + `/../output/${client}-pdf-` + Date.now() + '.html');
894
+ Pdf_file = `/output/${client}-pdf-` + Date.now() + '.html';
895
+
896
+ // Language configuration
897
+ language = empty(language) ? 'en' : language;
898
+ const isRtl = language.indexOf('ar') !== -1;
899
+ if (isRtl) language = 'ar-SA';
900
+
901
+ // Load client configuration
902
+ loadClientConfig(client);
903
+ localeService.setLocale(language);
904
+ debugLog("pdf language:", language);
760
905
 
761
- $('.doctor-details').html(finalHTML);
906
+ // Load templates
907
+ const templates = loadWellbeingTemplates(isRtl);
908
+ debugLog("templates loaded");
909
+
910
+ // Setup DOM
911
+ const dom = new JSDOM(templates.html);
912
+ const $ = (require('jquery'))(dom.window);
913
+ const historyData = [];
914
+
915
+ // Prepare data
916
+ const userImage = getUserImage(data.photo, data.genderId);
917
+ const topInsight = data.lingualReading?.statement || localeService.t('reportHeaderDescription');
918
+
919
+ // Render header
920
+ const renderedHeader = renderHeaderTemplate(
921
+ templates.header,
922
+ data,
923
+ config.logo,
924
+ userImage,
925
+ topInsight
926
+ );
927
+
928
+ // Apply styles
929
+ const iconColor = config.bulb_icon_font_color || config.general_font_color;
930
+ const dynamicStyles = generateReportStyles(iconColor);
931
+
932
+ $('#content').append(renderedHeader);
933
+ $('#content').append(dynamicStyles);
934
+
935
+ // Handle missing photo
936
+ if (empty(data.photo)) {
937
+ $('#content').append('<style>.user-profile-img{display: none !important;}</style>');
762
938
  }
763
- $('.report-info').html(headerData);
764
939
 
765
- if (!empty(smartReport)) $('#smart-report').append(generateReportHtml(smartReport, isRtl, false, false, language).html);
940
+ // Add chart colors
941
+ $("#chartsData").append(
942
+ `<script>var RANGE_COLORS = ['${config.first_level_color}','${config.second_level_color}','${config.third_level_color}','${config.forth_level_color}','${config.fifth_level_color}'];</script>`
943
+ );
944
+
945
+ // Render first section
946
+ const physicalMetrics = calculateScoreMetrics(data.calculation.physicalScore, client);
947
+ const psychologicalMetrics = calculateScoreMetrics(data.calculation.psychologicalScore, client);
948
+
949
+ const firstSectionHtml = renderFirstSection(
950
+ templates.firstSection,
951
+ data.calculation,
952
+ physicalMetrics,
953
+ psychologicalMetrics,
954
+ client
955
+ );
956
+
957
+ $('.first-section-scores').html(firstSectionHtml);
958
+ applyScoreInactiveClasses($, psychologicalMetrics.score, 'psychological-score-section');
959
+ applyScoreInactiveClasses($, physicalMetrics.score, 'physical-score-section');
960
+ $(".score-main").addClass(client).addClass(language);
961
+
962
+ // Process part scores
963
+ const processedScores = processPartScores(data.partsScore);
964
+ debugLog("after signature");
965
+
966
+ // Render footer
967
+ const footerHtml = templates.footer
968
+ .replace("{{Disclaimer}}", localeService.t('Disclaimer'))
969
+ .replace("{{Disclaimer Text}}", localeService.t('Disclaimer Text'));
970
+
971
+ $('#content').after(`<h5 class='recommended-tips-title first-level-font-color'>${localeService.t('weRecommend')}</h5>`);
972
+ $('.footer').append(footerHtml);
766
973
 
767
- let OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output`);
974
+ // Process and render tips
975
+ const organizedTips = organizeTipsByPart(data.insights.tips);
976
+ $('.all-tips').append(templates.tips);
977
+
978
+ Object.values(organizedTips).forEach(tipCategory => {
979
+ $(".tips-section .three-column").append(
980
+ `<div class='left-align'>${renderInsightTips(tipCategory)}</div>`
981
+ );
982
+ });
983
+
984
+ // Add scores data for charts
985
+ $("#chartsData").append(`<script>var allScores = ${JSON.stringify(processedScores)};</script>`);
986
+
987
+ // Ensure output directories exist
988
+ const OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output`);
768
989
  if (!fs.existsSync(OUTPUT_DIRECTORY)) {
769
- fs.mkdirSync(OUTPUT_DIRECTORY)
990
+ fs.mkdirSync(OUTPUT_DIRECTORY);
770
991
  }
771
- await promisify(fs.writeFile)(OUT_FILE, dom.serialize());
772
992
 
773
- let REQUEST_OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output/request`);
993
+ const REQUEST_OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output/request`);
774
994
  if (!fs.existsSync(REQUEST_OUTPUT_DIRECTORY)) {
775
- fs.mkdirSync(REQUEST_OUTPUT_DIRECTORY)
995
+ fs.mkdirSync(REQUEST_OUTPUT_DIRECTORY);
776
996
  }
997
+
998
+ // Replace footer border color placeholder for PHA
999
+ let finalHtml = dom.serialize();
1000
+ if (client.toLowerCase() === 'pha') {
1001
+ finalHtml = finalHtml.replace(/\{\{footerBorderColor\}\}/g, config.footer_border_color);
1002
+ }
1003
+
1004
+ // Write file
1005
+ await promisify(fs.writeFile)(OUT_FILE, finalHtml);
1006
+
777
1007
  return {
778
- "html": dom.serialize(),
779
- "outputFile": OUT_FILE,
780
- "header": headerTemplate,
781
- "footer": footerTemplate,
782
- "historyData": historyData,
783
- "profileInfo": profileInfo,
784
- "language": language,
785
- "pdfHeader": cleanStyledHeader(pdfHeader, profileInfo, language),
1008
+ html: finalHtml,
1009
+ outputFile: OUT_FILE,
1010
+ header: renderedHeader,
1011
+ footer: footerHtml,
1012
+ historyData
786
1013
  };
787
- }
1014
+ };
788
1015
 
789
- let generatePDFWellbeingReport = async (data) => {
790
- try {
791
- let contentHtml = data.html;
792
- OUT_FILE = data.outputFile;
793
- let browser = await puppeteer.launch({
794
- args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
795
- headless: true,
796
- defaultViewport: null
797
- });
798
- let page = await browser.newPage();
799
- debugLog("page launched!");
1016
+ /**
1017
+ * Generate HTML wellbeing report with smart report integration
1018
+ * @param {object} data - Report data including wellbeing, profile, and smart report
1019
+ * @param {boolean} isDebugging - Debug mode flag
1020
+ * @param {string} clientName - Client name for configuration
1021
+ * @param {string} language - Report language
1022
+ * @param {array} selectedLabs - Selected labs default is []
1023
+ * @param {boolean} showHeaderLogo - Show header logo default is true
1024
+ * @returns {object} Generated report data
1025
+ */
1026
+ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clientName, language, selectedLabs = [], showHeaderLogo = true) => {
1027
+ // Setup
1028
+ debug = isDebugging;
1029
+ const client = clientName || 'default';
1030
+ OUT_FILE = path.resolve(__dirname + `/../output/${client}-pdf-` + Date.now() + '.html');
1031
+ Pdf_file = `/output/${client}-pdf-` + Date.now() + '.html';
1032
+
1033
+ // Extract data
1034
+ const wellbeingData = data.wellbeing || {};
1035
+ const profileInfo = data.profileInfo || {};
1036
+ const doctorData = data.IsDoctor ? data.Profile : [];
1037
+ const smartReport = data.SR ? JSON.parse(data.SR) : {};
1038
+
1039
+ // Language configuration
1040
+ language = empty(language) ? 'en' : language;
1041
+ const isRtl = language.indexOf('ar') !== -1;
1042
+ if (isRtl) language = 'ar-SA';
1043
+
1044
+ // Load client configuration
1045
+ loadClientConfig(client);
1046
+ localeService.setLocale(language);
1047
+ debugLog("pdf language:", language);
800
1048
 
801
- await page.setContent(contentHtml, {waitUntil: 'networkidle2'});
1049
+ // Load templates
1050
+ const templates = loadExtendedTemplates(isRtl, client, !empty(profileInfo));
1051
+ debugLog("templates loaded");
802
1052
 
803
- await page.evaluateHandle('document.fonts.ready');
1053
+ // Setup DOM
1054
+ const dom = new JSDOM(templates.html);
1055
+ const $ = (require('jquery'))(dom.window);
1056
+ const historyData = [];
804
1057
 
805
- await page.addScriptTag({url: 'https://code.jquery.com/jquery-3.3.1.min.js'});
806
- debugLog("fonts loaded!");
1058
+ // Render templates
1059
+ const renderedPdfHeader = renderPdfHeaderTemplate(templates.pdfHeader, config.logo, config.title);
1060
+ const renderedHeader = renderBasicHeaderTemplate(templates.header, config.logo, config.title);
1061
+ const headerInfoHtml = generateHeaderInfo(profileInfo, 4);
807
1062
 
808
- await page.setContent(contentHtml, {waitUntil: 'networkidle2'});
809
- await page.goto("file:///" + path.resolve(__dirname + `/../${Pdf_file}`), {
810
- timeout: 30000000, waitUntil: 'networkidle2'
811
- }).catch(e => {
812
- console.log("ERROR IS File path: " + e.message);
813
- });
1063
+ // Apply content and styles
1064
+ $('#content').append(renderedHeader);
1065
+ $('.report-info').html(headerInfoHtml);
1066
+
1067
+ const iconColor = config.bulb_icon_font_color || config.general_font_color;
1068
+ const dynamicStyles = generateExtendedReportStyles(iconColor);
1069
+ $('#content').append(dynamicStyles);
1070
+
1071
+ // Handle missing photo
1072
+ if (empty(wellbeingData.photo)) {
1073
+ $('#content').append('<style>.user-profile-img{display: none !important;}</style>');
1074
+ }
1075
+
1076
+ // Add chart colors
1077
+ $("#chartsData").append(
1078
+ `<script>var RANGE_COLORS = ['${config.first_level_color}','${config.second_level_color}','${config.third_level_color}','${config.forth_level_color}','${config.fifth_level_color}'];</script>`
1079
+ );
1080
+
1081
+ // Render wellbeing data if available
1082
+ let firstSectionHtml = '';
1083
+ if (!empty(wellbeingData)) {
1084
+ // Calculate all metrics
1085
+ const metrics = {
1086
+ physical: calculatePhysicalScoreMetrics(wellbeingData.calculation.physicalScore, client),
1087
+ psychological: calculateScoreMetrics(wellbeingData.calculation.psychologicalScore, client),
1088
+ depression: calculateExtendedScoreMetrics(wellbeingData.calculation.depressionRiskScore, 'depression', client),
1089
+ anxiety: calculateExtendedScoreMetrics(wellbeingData.calculation.anxietyRiskScore, 'anxiety', client)
1090
+ };
1091
+
1092
+ // Render first section
1093
+ firstSectionHtml = renderExtendedFirstSection(
1094
+ templates.firstSection,
1095
+ wellbeingData.calculation,
1096
+ metrics,
1097
+ client
1098
+ );
1099
+
1100
+ $('.first-section-scores').html(firstSectionHtml);
1101
+
1102
+ // Apply inactive classes
1103
+ applyScoreInactiveClasses($, metrics.psychological.score, 'psychological-score-section');
1104
+ applyScoreInactiveClasses($, metrics.depression.score, 'psychological-score-section1');
1105
+ applyScoreInactiveClasses($, metrics.anxiety.score, 'psychological-score-section2');
1106
+ applyScoreInactiveClasses($, metrics.physical.score, 'physical-score-section');
1107
+
1108
+ $(".score-main").addClass(client).addClass(language);
1109
+
1110
+ // Process part scores
1111
+ const processedScores = processExtendedPartScores(wellbeingData.partsScore);
1112
+ const allScores = { lifeStyle: [], body: [], mind: [], healthRisk: [] };
1113
+
1114
+ $('#content').append(
1115
+ `<div class='row scores-parts' style='width:99.5% !important;'>${renderItems(allScores, wellbeingData.partsScore)}</div>`
1116
+ );
1117
+ debugLog("after signature");
1118
+
1119
+ // Process and render tips
1120
+ const tips = wellbeingData.insights.tips;
1121
+ if (!empty(tips)) {
1122
+ $('#content').after(`<h5 class='recommended-tips-title'>${localeService.t('weRecommend')}</h5>`);
814
1123
 
1124
+ const organizedTips = organizeExtendedTipsByPart(tips);
1125
+ $('.all-tips').append(templates.tips);
1126
+
1127
+ Object.values(organizedTips).forEach(tipCategory => {
1128
+ $(".tips-section .three-column").append(
1129
+ `<div class='left-align'>${renderInsightTips(tipCategory, client)}</div>`
1130
+ );
1131
+ });
1132
+ }
1133
+
1134
+ // Add scores data for charts
1135
+ $("#chartsData").append(`<script>var allScores = ${JSON.stringify(processedScores)};</script>`);
1136
+ }
1137
+
1138
+ // Handle empty wellbeing data
1139
+ if (empty(wellbeingData) || empty(wellbeingData?.insights?.tips)) {
1140
+ $('.footer-background').css('display', 'none');
1141
+ if (empty(wellbeingData)) {
1142
+ $('.first-section-scores').css('display', 'none');
1143
+ }
1144
+ }
1145
+
1146
+ // Render patient table
1147
+ if (!empty(profileInfo)) {
1148
+ const patientTableHtml = renderPatientTable(profileInfo, 4);
1149
+ $('.patient-table').html(patientTableHtml);
1150
+ }
1151
+
1152
+ // Render doctor details
1153
+ if (!empty(doctorData)) {
1154
+ const doctorDetailsHtml = renderDoctorDetails(doctorData);
1155
+ $('.doctor-details').html(doctorDetailsHtml);
1156
+ }
1157
+
1158
+ // Update report info
1159
+ $('.report-info').html(headerInfoHtml);
1160
+
1161
+ // Add smart report if available
1162
+ if (!empty(smartReport)) {
1163
+ $('#smart-report').append(generateReportHtml(smartReport, isRtl, false, false, language).html);
1164
+ }
1165
+
1166
+ // Ensure output directories exist
1167
+ const OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output`);
1168
+ if (!fs.existsSync(OUTPUT_DIRECTORY)) {
1169
+ fs.mkdirSync(OUTPUT_DIRECTORY);
1170
+ }
1171
+
1172
+ const REQUEST_OUTPUT_DIRECTORY = path.resolve(`${__dirname}/../output/request`);
1173
+ if (!fs.existsSync(REQUEST_OUTPUT_DIRECTORY)) {
1174
+ fs.mkdirSync(REQUEST_OUTPUT_DIRECTORY);
1175
+ }
1176
+
1177
+ // Replace footer border color placeholder for PHA
1178
+ let finalHtml = dom.serialize();
1179
+ if (client.toLowerCase() === 'pha') {
1180
+ finalHtml = finalHtml.replace(/\{\{footerBorderColor\}\}/g, config.footer_border_color);
1181
+ }
1182
+
1183
+ // Write file
1184
+ await promisify(fs.writeFile)(OUT_FILE, finalHtml);
1185
+
1186
+ return {
1187
+ html: finalHtml,
1188
+ outputFile: OUT_FILE,
1189
+ header: renderedHeader,
1190
+ footer: templates.footer,
1191
+ historyData,
1192
+ profileInfo,
1193
+ language,
1194
+ pdfHeader: cleanStyledHeader(renderedPdfHeader, profileInfo, language, clientName, selectedLabs, showHeaderLogo)
1195
+ };
1196
+ };
1197
+
1198
+ /**
1199
+ * Launch Puppeteer browser with standard configuration
1200
+ * @returns {object} Browser instance
1201
+ */
1202
+ const launchBrowser = async () => {
1203
+ return await puppeteer.launch({
1204
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
1205
+ headless: true,
1206
+ defaultViewport: null
1207
+ });
1208
+ };
1209
+
1210
+ /**
1211
+ * Setup page with content and dependencies
1212
+ * @param {object} page - Puppeteer page instance
1213
+ * @param {string} contentHtml - HTML content to load
1214
+ * @param {string} filePath - File path for navigation
1215
+ * @returns {object} Configured page
1216
+ */
1217
+ const setupPageContent = async (page, contentHtml, filePath) => {
1218
+ await page.setContent(contentHtml, { waitUntil: 'networkidle2' });
1219
+ await page.evaluateHandle('document.fonts.ready');
1220
+ await page.addScriptTag({ url: 'https://code.jquery.com/jquery-3.3.1.min.js' });
1221
+
1222
+ debugLog("fonts loaded!");
1223
+
1224
+ await page.setContent(contentHtml, { waitUntil: 'networkidle2' });
1225
+ await page.goto(`file:///${path.resolve(__dirname + `/../${filePath}`)}`, {
1226
+ timeout: 30000000,
1227
+ waitUntil: 'networkidle2'
1228
+ }).catch(e => {
1229
+ console.log("ERROR IS File path: " + e.message);
1230
+ });
1231
+
1232
+ return page;
1233
+ };
1234
+
1235
+ /**
1236
+ * Generate PDF from HTML content (simple wellbeing report)
1237
+ * @param {object} data - Data containing HTML and output file info
1238
+ * @returns {Buffer} PDF buffer
1239
+ */
1240
+ let generatePDFWellbeingReport = async (data) => {
1241
+ try {
1242
+ const contentHtml = data.html;
1243
+ OUT_FILE = data.outputFile;
1244
+
1245
+ // Launch browser and setup page
1246
+ const browser = await launchBrowser();
1247
+ const page = await browser.newPage();
1248
+ debugLog("page launched!");
1249
+
1250
+ // Setup content
1251
+ await setupPageContent(page, contentHtml, Pdf_file);
815
1252
  debugLog("page opened!");
816
- let buffer1 = await page.pdf({
1253
+
1254
+ // Generate PDF
1255
+ const buffer = await page.pdf({
817
1256
  width: "210mm",
818
1257
  height: "297mm",
819
1258
  pageRanges: '1',
820
1259
  printBackground: true,
821
1260
  displayHeaderFooter: true,
822
1261
  margin: {
823
- top: 0, bottom: 0
1262
+ top: 0,
1263
+ bottom: 0
824
1264
  }
825
1265
  });
826
1266
 
827
- debugLog("Buffer1 loaded!");
1267
+ debugLog("Buffer loaded!");
828
1268
 
829
- let buffer = buffer1;
830
1269
  await browser.close();
831
1270
  return buffer;
832
-
833
-
834
1271
  } catch (e) {
835
1272
  debugLog("ERROR!: ", e);
836
1273
  return "";
837
1274
  }
838
1275
  };
839
1276
 
840
-
841
- // function cleanStyledHeader(html, language = 'ar') {
842
- // const dom = new JSDOM(html);
843
- // const { document } = dom.window;
844
- // const fontFamily = language.startsWith('ar') ? 'Cairo' : 'Open Sans';
845
- // const header = document.querySelector(".report-header");
846
- // if (!header) return "";
847
- //
848
- // const styleEl = document.createElement("style");
849
- // styleEl.textContent = `
850
- // @media print {
851
- // @page {
852
- // size: A4 portrait;
853
- // }
854
- // .report-header-wrapper {
855
- // position: fixed;
856
- // top: 0;
857
- // }
858
- // }
859
- // .report-header-wrapper * {
860
- // font-family: '${fontFamily}', sans-serif !important;
861
- // }
862
- // `;
863
- // document.head.insertBefore(styleEl, document.head.firstChild);
864
- //
865
- // const wrapper = document.createElement("div");
866
- // wrapper.className = "report-header-wrapper";
867
- // wrapper.setAttribute("style", `
868
- // display:flex;
869
- // align-items:center;
870
- // justify-content:space-between;
871
- // width:100%;
872
- // padding:10px 15px;
873
- // box-sizing:border-box;
874
- // margin-bottom:10px;
875
- // font-family:'${fontFamily}', sans-serif;
876
- // font-size:10px;
877
- // `);
878
- //
879
- // const top = header.querySelector(".report-top");
880
- // if (top) {
881
- // const cleanTop = document.createElement("div");
882
- // cleanTop.setAttribute("style", `
883
- // display:flex;
884
- // justify-content:space-between;
885
- // flex-direction:row-reverse;
886
- // align-items:center;
887
- // width:100%;
888
- // `);
889
- //
890
- // const name = top.querySelector(".client-name");
891
- // if (name) {
892
- // const cleanName = document.createElement("div");
893
- // cleanName.textContent = name.textContent.trim();
894
- // cleanName.setAttribute("style", `
895
- // color:#555;
896
- // font-size:10px;
897
- // font-weight:bold;
898
- // text-transform:uppercase;
899
- // overflow-wrap:break-word;
900
- // `);
901
- // cleanTop.appendChild(cleanName);
902
- // }
903
- //
904
- // const logo = top.querySelector("img.report-logo");
905
- // if (logo) {
906
- // const logoWrapper = document.createElement("div");
907
- // logoWrapper.setAttribute("style", `
908
- // display:inline-block;
909
- // width:80px;
910
- // height:40px;
911
- // overflow:hidden;
912
- // `);
913
- // const cleanImg = document.createElement("img");
914
- // cleanImg.src = logo.src;
915
- // cleanImg.alt = logo.alt || "";
916
- // cleanImg.setAttribute("style", "width:100%; height:auto; display:block;");
917
- // logoWrapper.appendChild(cleanImg);
918
- // cleanTop.appendChild(logoWrapper);
919
- // }
920
- //
921
- // wrapper.appendChild(cleanTop);
922
- // }
923
- //
924
- // return wrapper.outerHTML;
925
- // }
926
-
927
- const cleanStyledHeader = (html, profileInfo, language = 'ar') => {
1277
+ const cleanStyledHeader = (html, profileInfo, language = 'ar', clientName = 'mediclinic', selectedLabs = [], showLogoInHeader = true) => {
928
1278
  const dom = new JSDOM(html);
929
1279
  const document = dom.window.document;
930
-
931
1280
  const logo = document.querySelector("img.report-logo");
1281
+ const allClientLabs = config.labs_logos;
1282
+ console.log('selected labs : ',selectedLabs);
1283
+ const addedLogos = Object.keys(allClientLabs).filter(lab =>
1284
+ selectedLabs?.some(otherLab => otherLab.toLowerCase() === lab.toLowerCase())
1285
+ );
1286
+ console.log('added logos',addedLogos);
932
1287
  const logoSrc = logo?.src || "";
933
- const clientName = document.querySelector(".client-name")?.textContent?.trim() || "";
1288
+ const headerTitle = document.querySelector(".client-name")?.textContent?.trim() || "";
934
1289
  const fontFamily = language.startsWith('ar') ? 'Cairo' : 'Open Sans';
935
1290
  const dir = language.startsWith('ar') ? 'rtl' : 'ltr';
936
- const textAlign = dir === 'rtl' ? 'right' : 'left';
937
- const justifyContent = dir === 'rtl' ? 'flex-end' : 'flex-start';
938
-
939
1291
  let pdfHeaderData = "";
940
1292
 
1293
+ const headerStylesCustomization = () => {
1294
+ let logoStyle = 'width:130px;height:40px;margin:0 8px;';
1295
+ let titleStyle = `font-family: ${fontFamily};font-weight: 700;font-style: Bold;font-size: 14px;line-height: 100%;letter-spacing: 0;text-transform: uppercase;`;
1296
+
1297
+ if (clientName.toLocaleLowerCase() == 'mediclinic') {
1298
+ logoStyle = 'width: 92px;height: 28px;';
1299
+ titleStyle = titleStyle;
1300
+ } else if (clientName.toLocaleLowerCase() == 'pha') {
1301
+ // Fixed height with flexible width, but constrained
1302
+ logoStyle = 'max-height: 32px; max-width: 60px; width: auto; object-fit: contain; display: block;margin:0 4px;';
1303
+ titleStyle = `font-family: ${fontFamily};font-weight: 500;font-style: Bold;font-size: 12px;line-height: 100%;letter-spacing: 0;text-transform: uppercase;`;
1304
+ }
1305
+
1306
+ return {
1307
+ logoStyle,
1308
+ titleStyle
1309
+ };
1310
+ };
1311
+
941
1312
  for (let index = 0; index < 4; index++) {
942
1313
  const element = profileInfo[index];
943
1314
  if (element) {
944
1315
  pdfHeaderData += `
945
- <div style="display: flex; align-items: center; justify-content: center; text-align: center;padding: 4px;">
946
- <span style="font-weight: 500;font-style: normal;font-size: 8px;leading-trim: NONE;line-height: 100%;letter-spacing: 0;">
947
- ${element.title}:
948
- </span>
949
- <span style="width: 4px;"></span>
950
- <span style="font-weight: 300;font-size: 8px;color: #3B3B3B;leading-trim: NONE;line-height: 100%;letter-spacing: 0;">
951
- ${element.value}
952
- </span>
953
- </div>
954
- `;
1316
+ <div style="display: flex; align-items: center; justify-content: center; text-align: center;padding: 4px;">
1317
+ <span style="font-weight: 500;font-style: normal;font-size: 8px;leading-trim: NONE;line-height: 100%;letter-spacing: 0;">
1318
+ ${element.title}:
1319
+ </span>
1320
+ <span style="width: 4px;"></span>
1321
+ <span style="font-weight: 300;font-size: 8px;color: ${config.colors.headerText};leading-trim: NONE;line-height: 100%;letter-spacing: 0;">
1322
+ ${element.value}
1323
+ </span>
1324
+ </div>
1325
+ `;
955
1326
  }
956
1327
  }
957
1328
 
1329
+ // the images should be in the same height and width 92x61
1330
+ const styles = headerStylesCustomization();
1331
+ const logoSection = showLogoInHeader && logoSrc ? `
1332
+ <div style="display: flex; gap: 12px; align-items: center;">
1333
+ <img src="${logoSrc}" alt="Logo" style="${styles?.logoStyle || ""}" />
1334
+ ${addedLogos.length > 0 ? addedLogos.map(lab =>
1335
+ (!!lab && !!allClientLabs[lab])
1336
+ ? `<img src="${allClientLabs[lab]}" style="${styles?.logoStyle || ""}" />`
1337
+ : ''
1338
+ ).join('') : ''}
1339
+ </div>
1340
+ ` : '';
1341
+
1342
+ return `
1343
+ <style>
1344
+ html { -webkit-print-color-adjust: exact; margin-bottom: 10px}
1345
+ </style>
1346
+ <div style="font-family: '${fontFamily}', sans-serif; direction: ${dir}; width: 100%;margin-left: 20px;margin-right: 20px;margin-bottom: 20px;">
1347
+ <div style="width: 100%; display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;gap: 12px;">
1348
+ ${logoSection}
1349
+ <div style="${styles?.titleStyle || ""}">
1350
+ ${headerTitle.toUpperCase()}
1351
+ </div>
1352
+ </div>
1353
+ <!-- Info Row -->
1354
+ <div style="
1355
+ padding: 2px 10px;
1356
+ background-color: ${config.colors.headerBackground};
1357
+ display: flex;
1358
+ justify-content: space-between;
1359
+ align-items: center;
1360
+ overflow: hidden;
1361
+ margin-bottom: 5px;
1362
+ ">
1363
+ ${pdfHeaderData}
1364
+ </div>
1365
+ <div style="width: 100%; height: 1px; background-color: ${config.colors.headerDivider}; margin: 6px 0 10px 0;"></div>
1366
+ </div>
1367
+ `;
1368
+ };
1369
+
1370
+ /**
1371
+ * Get PDF footer template HTML
1372
+ * @returns {string} Footer template HTML
1373
+ */
1374
+ const getPdfFooterTemplate = () => {
958
1375
  return `
959
- <style>
960
- html { -webkit-print-color-adjust: exact; margin-bottom: 10px}
961
- </style>
962
- <div style="font-family: '${fontFamily}', sans-serif; direction: ${dir}; width: 100%;margin-left: 20px;margin-right: 20px;margin-bottom: 20px;">
963
- <!-- Top Row -->
964
- <div style="width: 100%; display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
965
- ${logoSrc ? `
966
- <div>
967
- <img src="${logoSrc}" alt="Logo" style="width: 92px;height: 28px;" />
968
- </div>` : ''}
969
- <div style="font-family: '${fontFamily}';font-weight: 700;font-style: Bold;font-size: 14px;line-height: 100%;letter-spacing: 0;text-transform: uppercase;">
970
- ${clientName.toUpperCase()}
971
- </div>
972
- </div>
973
-
974
- <!-- Info Row -->
975
- <div style="
976
- padding: 2px 10px;
977
- background-color: #f2f5f8;
978
- display: flex;
979
- justify-content: space-between;
980
- align-items: center;
981
- overflow: hidden;
982
- margin-bottom: 5px;
983
- ">
984
- ${pdfHeaderData}
985
- </div>
986
-
987
- <!-- Divider -->
988
- <div style="width: 100%; height: 1px; background-color: #0092D2; margin: 6px 0 10px 0;"></div>
989
- </div>
990
- `;
1376
+ <div style="width: 100%; font-size:7.3px; color: ${config.colors.footerText}; padding: 0 20px; box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; font-family: 'Open Sans', sans-serif;">
1377
+ <div style="display: flex; align-items: center;font-family: 'Open Sans', sans-serif;">
1378
+ <span style="font-style: italic; font-family: 'Open Sans', sans-serif;">Page
1379
+ <span class="pageNumber" style="font-family: 'Open Sans', sans-serif; font-feature-settings: 'tnum';"></span>
1380
+ of
1381
+ <span class="totalPages" style="font-family: 'Open Sans', sans-serif; font-feature-settings: 'tnum';"></span>
1382
+ </span>
1383
+ </div>
1384
+ <div style="display: flex; flex-direction: column; align-items: flex-end; font-family: 'Open Sans', sans-serif;">
1385
+ <div style="font-family: 'Open Sans', sans-serif; font-feature-settings: 'tnum';">Telephone: 800 20 33</div>
1386
+ <div style="font-family: 'Open Sans', sans-serif;">Email: info@mediclinic.ae</div>
1387
+ </div>
1388
+ </div>
1389
+ `;
1390
+ };
1391
+
1392
+ /**
1393
+ * Launch browser for extended PDF with font optimization
1394
+ * @returns {object} Browser instance
1395
+ */
1396
+ const launchExtendedBrowser = async () => {
1397
+ return await puppeteer.launch({
1398
+ args: [
1399
+ "--no-sandbox",
1400
+ "--disable-setuid-sandbox",
1401
+ "--font-render-hinting=none",
1402
+ "--force-color-profile=srgb",
1403
+ "--disable-web-security"
1404
+ ],
1405
+ headless: true,
1406
+ defaultViewport: null
1407
+ });
1408
+ };
1409
+
1410
+ /**
1411
+ * Setup page for extended PDF report
1412
+ * @param {object} page - Puppeteer page instance
1413
+ * @param {string} contentHtml - HTML content
1414
+ * @param {string} filePath - File path for navigation
1415
+ * @returns {object} Configured page
1416
+ */
1417
+ const setupExtendedPageContent = async (page, contentHtml, filePath) => {
1418
+ await page.evaluateHandle('document.fonts.ready');
1419
+ await page.evaluate(() => {
1420
+ const usedFonts = Array.from(document.fonts).map(f => f.family);
1421
+ console.log('Fonts in use:', usedFonts);
1422
+ });
1423
+ await page.screenshot({ encoding: 'binary' });
1424
+
1425
+ debugLog("page launched & fonts loaded!");
1426
+
1427
+ await page.addScriptTag({ url: 'https://code.jquery.com/jquery-3.3.1.min.js' });
1428
+ await page.setContent(contentHtml, { waitUntil: 'networkidle2', timeout: 30000 });
1429
+ await page.goto(`file:///${path.resolve(__dirname + `/../${filePath}`)}`, {
1430
+ timeout: 30000000,
1431
+ waitUntil: 'networkidle2'
1432
+ }).catch(e => {
1433
+ console.log("ERROR IS File path: " + e.message);
1434
+ });
1435
+
1436
+ return page;
991
1437
  };
992
1438
 
1439
+ /**
1440
+ * Generate PDF from HTML content (extended report with header/footer)
1441
+ * @param {object} data - Data containing HTML, header, and output file info
1442
+ * @returns {Buffer} PDF buffer
1443
+ */
993
1444
  const generatePDFReport = async (data) => {
994
1445
  try {
995
- let contentHtml = data.html;
996
-
997
- const footerTemplate = `
998
- <div style="width: 100%; font-size:7.3px; color: #B5B5B5; padding: 0 20px; box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; font-family: 'Open Sans', sans-serif;">
999
- <div style="display: flex; align-items: center;font-family: 'Open Sans', sans-serif;">
1000
- <span style="font-style: italic; font-family: 'Open Sans', sans-serif;">Page
1001
- <span class="pageNumber" style="font-family: 'Open Sans', sans-serif; font-feature-settings: 'tnum';"></span>
1002
- of
1003
- <span class="totalPages" style="font-family: 'Open Sans', sans-serif; font-feature-settings: 'tnum';"></span></span>
1004
- </div>
1005
- <div style="display: flex; flex-direction: column; align-items: flex-end; font-family: 'Open Sans', sans-serif;">
1006
- <div style="font-family: 'Open Sans', sans-serif; font-feature-settings: 'tnum';">Telephone: 800 20 33</div>
1007
- <div style="font-family: 'Open Sans', sans-serif;">Email: info@mediclinic.ae</div>
1008
- </div>
1009
- </div>`;
1010
-
1446
+ const contentHtml = data.html;
1011
1447
  const OUT_FILE = data.outputFile;
1012
- const browser = await puppeteer.launch({
1013
- args: ["--no-sandbox", "--disable-setuid-sandbox", "--font-render-hinting=none", "--force-color-profile=srgb", "--disable-web-security"],
1014
- headless: true,
1015
- defaultViewport: null
1016
- });
1448
+ const footerTemplate = getPdfFooterTemplate();
1449
+
1450
+ // Launch browser and setup page
1451
+ const browser = await launchExtendedBrowser();
1017
1452
  const page = await browser.newPage();
1018
- await page.evaluateHandle('document.fonts.ready');
1019
- await page.evaluate(() => {
1020
- const usedFonts = Array.from(document.fonts).map(f => f.family);
1021
- console.log('Fonts in use:', usedFonts);
1022
- });
1023
- await page.screenshot({encoding: 'binary'});
1024
- debugLog("page launched & fonts loaded!");
1025
- await page.addScriptTag({url: 'https://code.jquery.com/jquery-3.3.1.min.js'});
1026
- await page.setContent(contentHtml, {waitUntil: 'networkidle2', timeout: 30000});
1027
- await page.goto("file:///" + path.resolve(__dirname + `/../${Pdf_file}`), {
1028
- timeout: 30000000, waitUntil: 'networkidle2'
1029
- }).catch(e => {
1030
- console.log("ERROR IS File path: " + e.message);
1031
- });
1032
- const buffer1 = await page.pdf({
1453
+
1454
+ // Setup content
1455
+ await setupExtendedPageContent(page, contentHtml, Pdf_file);
1456
+
1457
+ // Generate PDF with header and footer
1458
+ const buffer = await page.pdf({
1033
1459
  format: 'A4',
1034
1460
  displayHeaderFooter: true,
1035
- footerTemplate: footerTemplate,
1461
+ footerTemplate,
1036
1462
  headerTemplate: data.pdfHeader,
1037
1463
  width: "220mm",
1038
1464
  margin: {
1039
- top: '125px', bottom: '50px', left: '20px', right: '20px'
1465
+ top: '125px',
1466
+ bottom: '50px',
1467
+ left: '20px',
1468
+ right: '20px'
1040
1469
  },
1041
- printBackground: true,
1470
+ printBackground: true
1042
1471
  });
1043
- const buffer = buffer1;
1472
+
1044
1473
  await browser.close();
1045
1474
  return buffer;
1046
-
1047
1475
  } catch (e) {
1048
1476
  debugLog("ERROR!: ", e);
1049
1477
  return "";
@@ -1066,211 +1494,170 @@ const renderDetails = (list) => {
1066
1494
  return (`<li>${list[0].content.substring(list[0].sepCount, list[0].content.length + 1)}</li>`)
1067
1495
  }
1068
1496
 
1069
- let columns = []
1070
- let isParent = false
1071
-
1072
- list.map((child, index) => {
1073
- if (child.parent_id === "0") isParent = true
1497
+ const columns = []
1074
1498
 
1499
+ list.forEach((child) => {
1075
1500
  let div = '';
1076
1501
  if (empty(child.children)) {
1077
- div = `<li>` + `${child.content.substring(child.sepCount, child.content.length + 1)}` + `</li>`
1502
+ div = `<li>${child.content.substring(child.sepCount, child.content.length + 1)}</li>`
1078
1503
  } else {
1079
- div = `<li>` + `${child.content.substring(child.sepCount, child.content.length + 1)}` + `</li>` + `<ul>` + `${renderDetails(child.children)}` + `</ul>`
1504
+ div = `<li>${child.content.substring(child.sepCount, child.content.length + 1)}</li><ul>${renderDetails(child.children)}</ul>`
1080
1505
  }
1081
1506
 
1507
+ if (!empty(div)) {
1508
+ columns.push(div);
1509
+ }
1510
+ });
1082
1511
 
1083
- if (!empty(div)) columns.push(div)
1084
-
1085
- })
1086
-
1087
- let results = ''
1088
- columns.map((tabel) => {
1089
- if (!empty(tabel)) results += tabel
1090
- })
1091
- return !empty(columns) ? `${results}` : null
1512
+ return !empty(columns) ? columns.join('') : null;
1092
1513
  }
1093
1514
 
1094
1515
 
1095
- let renderScoreListElements = (key, partScores) => {
1096
- let elementsHtml = ''
1097
- let tempElements = {'body': [], 'lifeStyle': [], 'mind': [], 'healthRisk': []};
1098
- let body = [];
1099
- let lifeStyle = [];
1100
- let mind = [];
1101
- let healthRisk = []
1102
- let i = 0;
1103
- if (!isEmpty(partScores)) {
1104
- partScores[key].map((item, index) => {
1105
- let name = item['icon'].split(".png", [1]);
1106
- let icon = icons[name];
1107
- let score_segments = item['score'] + "/" + item['base']; // get score between 1 and 5
1108
- let color = '#8b8ba7';
1109
- let tempElement = {
1110
- id: i,
1111
- icon: icon,
1112
- name: item['name'],
1113
- color: color,
1114
- score: key === 'healthRisk' ? item['scoreText'] : score_segments
1115
- };
1116
- if (key === 'body') {
1117
- body.push(tempElement)
1118
- } else if (key === 'lifeStyle') {
1119
- lifeStyle.push(tempElement)
1120
- } else if (key === 'mind') {
1121
- mind.push(tempElement)
1122
- } else {
1123
- healthRisk.push(tempElement)
1124
- }
1125
- i++;
1126
- })
1127
- tempElements['lifeStyle'] = lifeStyle;
1128
- tempElements['mind'] = mind;
1129
- tempElements['body'] = body;
1130
-
1131
- tempElements['healthRisk'] = healthRisk;
1132
-
1133
- tempElements[key].map((item, index) => {
1134
- elementsHtml += "<li class='list-item'>\n" + " <svg fill-rule=\"evenodd\" clip-rule=\"evenodd\" xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 30 30\">\n" + " <g fill=\"none\" fillRule=\"evenodd\">\n" + " <g fill='" + item.color + "' stroke='" + item.color + "' strokeOpacity={0.2} fillRule=\"nonzero\">\n" + " <path d='" + item.icon + "'/>\n" + " </g>\n" + " </g>\n" + " </svg>\n" + " <div class='list-text'>" + item.name + "</div>\n" + " <div class='list-score'>" + item.score + "</div>\n" + " </li>"
1135
- })
1136
-
1516
+ /**
1517
+ * Render score list elements for a given category
1518
+ * @param {string} key - Category key (body, lifeStyle, mind, healthRisk)
1519
+ * @param {object} partScores - Part scores data
1520
+ * @returns {string} Rendered score list HTML
1521
+ */
1522
+ const renderScoreListElements = (key, partScores) => {
1523
+ if (isEmpty(partScores) || !partScores[key]) {
1524
+ return '<ul class="score-list"></ul>';
1137
1525
  }
1138
- let parts = "<ul class='score-list'>" + elementsHtml + "</ul>"
1139
-
1140
- return parts;
1141
- }
1142
-
1143
1526
 
1144
- let renderItems = (allScores, partScores) => {
1145
- let partsScoreHtml = ''
1146
- if (!isEmpty(allScores)) {
1147
- Object.entries(allScores).map(([key, value], index) => {
1148
- let numberOfE = 1
1149
- let top = 30 * Object.entries(allScores[key]).length
1150
- top = top / 2
1151
- top = `calc( 50% - ${top}px)`;
1152
-
1153
- if (Object.entries(allScores[key]).length === 1) numberOfE = "one"
1154
-
1155
- if (Object.entries(allScores[key]).length === 2) numberOfE = "two"
1156
-
1157
- if (Object.entries(allScores[key]).length === 3) numberOfE = "three"
1158
-
1159
- if (Object.entries(allScores[key]).length === 4) numberOfE = "four"
1160
-
1161
- if (Object.entries(allScores[key]).length === 5) numberOfE = "five"
1527
+ const elements = partScores[key].map((item) => {
1528
+ const iconName = item['icon'].split(".png")[0];
1529
+ const icon = icons[iconName];
1530
+ const color = config.colors.listIcon;
1531
+ const score = key === 'healthRisk'
1532
+ ? item['scoreText']
1533
+ : `${item['score']}/${item['base']}`;
1534
+
1535
+ return `
1536
+ <li class='list-item'>
1537
+ <svg fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 30 30">
1538
+ <g fill="none" fillRule="evenodd">
1539
+ <g fill='${color}' stroke='${color}' strokeOpacity={0.2} fillRule="nonzero">
1540
+ <path d='${icon}'/>
1541
+ </g>
1542
+ </g>
1543
+ </svg>
1544
+ <div class='list-text'>${item.name}</div>
1545
+ <div class='list-score'>${score}</div>
1546
+ </li>
1547
+ `;
1548
+ }).join('');
1162
1549
 
1163
- if (Object.entries(allScores[key]).length === 6) numberOfE = "six"
1550
+ return `<ul class='score-list'>${elements}</ul>`;
1551
+ };
1164
1552
 
1165
- if (Object.entries(allScores[key]).length === 7) numberOfE = "seven"
1166
1553
 
1167
- if (Object.entries(allScores[key]).length === 8) numberOfE = "eight"
1554
+ /**
1555
+ * Render all score items with charts
1556
+ * @param {object} allScores - All scores object
1557
+ * @param {object} partScores - Part scores data
1558
+ * @returns {string} Rendered HTML for all score items
1559
+ */
1560
+ const renderItems = (allScores, partScores) => {
1561
+ if (isEmpty(allScores)) {
1562
+ return '';
1563
+ }
1168
1564
 
1169
- if (Object.entries(allScores[key]).length === 9) numberOfE = "nine"
1565
+ const numberWords = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
1170
1566
 
1171
- if (Object.entries(allScores[key]).length === 10) numberOfE = "ten"
1567
+ const itemsHtml = Object.entries(allScores).map(([key, value]) => {
1568
+ if (empty(partScores[key])) {
1569
+ return '';
1570
+ }
1172
1571
 
1173
- if (!empty(partScores[key])) {
1174
- partsScoreHtml += "<div\n" + " key='" + key + "'\n" + " class='percentage-meter-content center'>";
1175
- partsScoreHtml += "<div class='svg-container'>";
1176
- if (key !== "healthRisk") {
1177
- partsScoreHtml += "<div id='" + key + "' class=\"circle-container\"></div>";
1178
- }
1572
+ const itemCount = Object.entries(allScores[key]).length;
1573
+ const numberOfE = numberWords[itemCount] || itemCount.toString();
1574
+ const topOffset = itemCount * 30 / 2;
1575
+ const top = `calc(50% - ${topOffset}px)`;
1576
+
1577
+ const circleContainer = key !== "healthRisk"
1578
+ ? `<div id='${key}' class="circle-container"></div>`
1579
+ : '';
1580
+
1581
+ return `
1582
+ <div key='${key}' class='percentage-meter-content center'>
1583
+ <div class='svg-container'>
1584
+ ${circleContainer}
1585
+ <div class='chart-title'>
1586
+ <span class='score-details-title'>${localeService.t(key)}</span>
1587
+ </div>
1588
+ </div>
1589
+ <div class='element-list ${numberOfE}' style='top:${top}'>
1590
+ ${renderScoreListElements(key, partScores)}
1591
+ </div>
1592
+ </div>
1593
+ `;
1594
+ }).join('');
1179
1595
 
1180
- partsScoreHtml += "<div class='chart-title'>\n" + " <span\n" + " class='score-details-title'>" + localeService.t(key) + "</span>\n" + " </div>\n" + "\n" + " </div>\n" + "\n" + " <div class='element-list " + numberOfE + "' style='top:" + top + "'>" + renderScoreListElements(key, partScores) + "</div>\n" + " </div>"
1181
- }
1596
+ return itemsHtml;
1597
+ };
1182
1598
 
1183
- });
1599
+ /**
1600
+ * Render insight tips for a specific category
1601
+ * @param {array} tips - Array of tip objects
1602
+ * @param {string} clientName - Client type (e.g., 'Mediclinic')
1603
+ * @returns {string} Rendered tips HTML
1604
+ */
1605
+ const renderInsightTips = (tips, clientName) => {
1606
+ if (empty(tips)) {
1607
+ return '';
1184
1608
  }
1185
- return partsScoreHtml;
1186
- };
1187
1609
 
1188
- let renderInsightTips = (tips, type) => {
1189
- console.log(tips, 'tips');
1610
+ const partImages = {
1611
+ 1: config.lifestyleImage,
1612
+ 2: config.mindImage,
1613
+ 3: config.bodyImage
1614
+ };
1190
1615
 
1191
- if (type === 'Mediclinic') {
1192
- let tipsHtml = '';
1616
+ const partTitles = {
1617
+ 1: localeService.t('lifeStyle'),
1618
+ 2: localeService.t('mind'),
1619
+ 3: localeService.t('body')
1620
+ };
1193
1621
 
1194
- if (!empty(tips)) {
1195
- let partImages = {
1196
- 1: lifestyleImage, 3: bodyImage, 2: mindImage
1197
- };
1198
-
1199
- let partTitles = {
1200
- 1: localeService.t('lifeStyle'), 3: localeService.t('body'), 2: localeService.t('mind')
1201
- };
1202
-
1203
- let tip = tips[0];
1204
- let tipImage = partImages[tip.partId] || '';
1205
- let translatedTitle = partTitles[tip.partId] || '';
1206
-
1207
- let textColor = tip.isPositive ? '#124374' : 'black';
1208
- let tipContents = ''
1209
- tips.map((t, index) => {
1210
- if (index == 0) {
1211
- tipContents += `
1212
-
1213
- <div class="insight-content" style="color: #124374;">${t.content}</div>
1214
- `
1215
- } else {
1216
- tipContents += ` <div class="insight-content" style="color: #000000;">${t.content}</div>`
1622
+ if (clientName === 'Mediclinic' || clientName === 'pha') {
1623
+ const tip = tips[0];
1624
+ const tipImage = partImages[tip.partId] || '';
1625
+ const translatedTitle = partTitles[tip.partId] || '';
1217
1626
 
1218
- }
1219
- })
1627
+ const tipContents = tips.map((t, index) => {
1628
+ const color = index === 0 ? config.colors.insightPrimary : config.colors.insightSecondary;
1629
+ return `<div class="insight-content" style="color: ${color};">${t.content}</div>`;
1630
+ }).join('');
1220
1631
 
1221
- tipsHtml += `
1632
+ return `
1222
1633
  <div class="insight-wrapper">
1223
1634
  <div class="insight-header">
1224
1635
  <div class="insight-icon">
1225
- <img src="${tipImage}" alt="${translatedTitle}" />
1226
-
1636
+ <img src="${tipImage}" alt="${translatedTitle}" />
1227
1637
  </div>
1228
1638
  <div class="insight-title">${translatedTitle}</div>
1229
1639
  </div>
1230
- ${tipContents}
1640
+ ${tipContents}
1231
1641
  </div>
1232
1642
  `;
1233
-
1234
- return tipsHtml;
1235
- }
1236
-
1237
- return tipsHtml;
1238
- } else {
1239
-
1240
-
1241
- let tipsHtml = ''
1242
- if (!empty(tips)) {
1243
- tips.map((tip, index) => {
1244
- let tipImage = ''
1245
- let insightClass = 'insight-container-without-img';
1246
- let tipsTextClass = ''
1247
- if (index === 0) {
1248
- if (tip.partId === 1) {
1249
- tipImage = lifestyleImage
1250
- }
1251
- if (tip.partId === 2) {
1252
- tipImage = bodyImage
1253
- }
1254
- if (tip.partId === 3) {
1255
- tipImage = mindImage
1256
- }
1257
- insightClass = "insight-container-with-img";
1258
- tipsTextClass = "first-level-font-color"
1259
- }
1260
-
1261
- let tex = tip.content
1262
-
1263
- tipsHtml += "<div class='insight-container " + insightClass + "'>" + "<div class='tips-text " + tipsTextClass + "'>" + tex + "</div>";
1264
- if (index === 0) {
1265
- tipsHtml += "<div class='chunk-image'><img class='chunk-image-mind' src='" + tipImage + "'/></div>"
1266
-
1267
- tipsHtml += "</div>"
1268
- }
1269
- })
1270
- }
1271
- return tipsHtml;
1272
1643
  }
1273
- }
1644
+
1645
+ // Default rendering for other clients
1646
+ return tips.map((tip, index) => {
1647
+ const isFirst = index === 0;
1648
+ const tipImage = isFirst ? (partImages[tip.partId] || '') : '';
1649
+ const insightClass = isFirst ? 'insight-container-with-img' : 'insight-container-without-img';
1650
+ const tipsTextClass = isFirst ? 'first-level-font-color' : '';
1651
+ const imageHtml = isFirst ? `<div class='chunk-image'><img class='chunk-image-mind' src='${tipImage}'/></div>` : '';
1652
+
1653
+ return `
1654
+ <div class='insight-container ${insightClass}'>
1655
+ <div class='tips-text ${tipsTextClass}'>${tip.content}</div>
1656
+ ${imageHtml}
1657
+ </div>
1658
+ `;
1659
+ }).join('');
1660
+ };
1274
1661
 
1275
1662
  function generateReportHtml(data, isRtl, isRectified, isIMCLab, currentLanguage) {
1276
1663
  let detailsViewClass = data.showCompactView ? "" : "one-page";
@@ -1285,7 +1672,7 @@ function generateReportHtml(data, isRtl, isRectified, isIMCLab, currentLanguage)
1285
1672
  <td>
1286
1673
  <svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 12" width="20" height="20">
1287
1674
  <style>
1288
- .s0 { fill: #ffc107 }
1675
+ .s0 { fill: ${config.colors.warningIcon} }
1289
1676
  </style>
1290
1677
  <path id="Layer" class="s0" d="m0.7 0.6q-0.1 0-0.1 0.1v8.8q0 0.1 0.1 0.1h3.1l-0.6 0.6h-2.5c-0.4 0-0.7-0.3-0.7-0.7v-8.8c0-0.4 0.3-0.7 0.7-0.7h6.2c0.4 0 0.7 0.3 0.7 0.7v5l-0.6 0.6v-5.6q0-0.1-0.1-0.1z" />
1291
1678
  <path id="Layer" class="s0" d="m9.9 5.4c0.1 0.2 0.1 0.4 0 0.5l-5.5 5.5l-1.6 0.6l0.6-1.6l5.5-5.6c0.1-0.1 0.3-0.1 0.4 0z" />