@measurequick/measurequick-report-generator 1.5.180 → 1.5.181
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.
- package/base-64/mq-vitals-heatpump-heating.js +8 -0
- package/index.js +6 -0
- package/package.json +1 -1
- package/report-gen-scripts/ai-summary-page.js +293 -0
- package/report-gen-scripts/measurement-details.js +4 -1
- package/report-gen-scripts/mq-ai-summary-report.js +490 -0
- package/report-gen-scripts/vitals-cooling-report.js +6 -1
- package/report-gen-scripts/vitals-heating-report.js +6 -2
- package/report-gen-scripts/vitals-heatpump-heating-report.js +339 -0
- package/util.js +71 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Heat Pump Heating Vitals Report Template
|
|
2
|
+
// TODO: Replace with actual PDF template containing heat pump heating labels:
|
|
3
|
+
// - Page 1 measurements: Temp Rise, LLT, COP, OAT, Static Pressure, Airflow
|
|
4
|
+
// - Page 2 factors: Static Pressure (20pts), Air-Side Heat Delivery (20pts),
|
|
5
|
+
// Capacity Delivery (20pts), System Balance (20pts), Efficiency/COP (20pts)
|
|
6
|
+
//
|
|
7
|
+
// For now, this re-exports the cooling template as a placeholder
|
|
8
|
+
export { base64 } from "./mq-vitals-cooling.js";
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as photosReport from "./report-gen-scripts/photos-report.js";
|
|
|
7
7
|
import * as photosReportS3 from "./report-gen-scripts/photos-report-s3.js";
|
|
8
8
|
import * as ptcsHeatPumpReport from "./report-gen-scripts/ptcs-heat-pump-report.js";
|
|
9
9
|
import * as vitalsCoolingReport from "./report-gen-scripts/vitals-cooling-report.js";
|
|
10
|
+
import * as vitalsHeatPumpHeatingReport from "./report-gen-scripts/vitals-heatpump-heating-report.js";
|
|
10
11
|
import * as vitalsHeatingReport from "./report-gen-scripts/vitals-heating-report.js";
|
|
11
12
|
import * as vitalsHeatingReportNoCombustion from "./report-gen-scripts/vitals-heating-report-no-combustion.js";
|
|
12
13
|
import * as ductScreeningReport from "./report-gen-scripts/nci-report.js";
|
|
@@ -17,6 +18,7 @@ import * as energyStarCertificate from "./report-gen-scripts/energy-star-certifi
|
|
|
17
18
|
import * as franklinEnergyReport from "./report-gen-scripts/franklin-energy-report.js";
|
|
18
19
|
import * as nciCustomerReport from "./report-gen-scripts/nci-report-customer.js";
|
|
19
20
|
import * as nciContractorReport from "./report-gen-scripts/nci-report-contractor.js";
|
|
21
|
+
import * as mqAiSummaryReport from "./report-gen-scripts/mq-ai-summary-report.js";
|
|
20
22
|
|
|
21
23
|
export function getReport(payload) {
|
|
22
24
|
switch (payload.meta.report_type) {
|
|
@@ -38,6 +40,8 @@ export function getReport(payload) {
|
|
|
38
40
|
return ptcsHeatPumpReport.getReport(payload);
|
|
39
41
|
case "VitalsCoolingReport":
|
|
40
42
|
return vitalsCoolingReport.getReport(payload, false);
|
|
43
|
+
case "VitalsHeatPumpHeatingReport":
|
|
44
|
+
return vitalsHeatPumpHeatingReport.getReport(payload, false);
|
|
41
45
|
case "VitalsHeatingReport":
|
|
42
46
|
return vitalsHeatingReport.getReport(payload, false);
|
|
43
47
|
case "VitalsHeatingReportNoCombustion":
|
|
@@ -61,5 +65,7 @@ export function getReport(payload) {
|
|
|
61
65
|
return nciCustomerReport.getReport(payload);
|
|
62
66
|
case "NCIReportContractor":
|
|
63
67
|
return nciContractorReport.getReport(payload);
|
|
68
|
+
case "MqAiSummaryReport":
|
|
69
|
+
return mqAiSummaryReport.getReport(payload);
|
|
64
70
|
}
|
|
65
71
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
|
2
|
+
import * as base64 from '../base-64/icons.js';
|
|
3
|
+
import * as util from '../util.js';
|
|
4
|
+
|
|
5
|
+
// Color definitions matching MeasureQuick brand
|
|
6
|
+
const colors = {
|
|
7
|
+
headerBg: rgb(0.18, 0.31, 0.47), // Dark blue header
|
|
8
|
+
scoreGreen: rgb(0.18, 0.62, 0.35), // Green for A grades
|
|
9
|
+
scoreYellow: rgb(0.91, 0.73, 0.18), // Yellow for B grades
|
|
10
|
+
scoreOrange: rgb(0.91, 0.49, 0.18), // Orange for C grades
|
|
11
|
+
scoreRed: rgb(0.78, 0.22, 0.22), // Red for D/F grades
|
|
12
|
+
lightGray: rgb(0.95, 0.95, 0.95), // Light gray background
|
|
13
|
+
mediumGray: rgb(0.7, 0.7, 0.7), // Medium gray for borders
|
|
14
|
+
darkGray: rgb(0.3, 0.3, 0.3), // Dark gray for text
|
|
15
|
+
black: rgb(0, 0, 0),
|
|
16
|
+
white: rgb(1, 1, 1)
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getScoreColor(grade) {
|
|
20
|
+
if (!grade) return colors.mediumGray;
|
|
21
|
+
if (grade.includes('A')) return colors.scoreGreen;
|
|
22
|
+
if (grade.includes('B')) return colors.scoreYellow;
|
|
23
|
+
if (grade.includes('C')) return colors.scoreOrange;
|
|
24
|
+
return colors.scoreRed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add AI Summary page(s) to an existing PDF document
|
|
29
|
+
* @param {PDFDocument} pdfDoc - The existing PDF document to add pages to
|
|
30
|
+
* @param {Object} payload - The report payload containing AI summary and test data
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
export async function addAiSummaryPages(pdfDoc, payload) {
|
|
34
|
+
// Check if we should show the AI summary
|
|
35
|
+
if (!util.shouldShowAiSummary(payload)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const aiSummary = util.getAiSummary(payload);
|
|
40
|
+
if (!aiSummary) return;
|
|
41
|
+
|
|
42
|
+
// Embed fonts
|
|
43
|
+
const fontRegular = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
44
|
+
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
45
|
+
|
|
46
|
+
// Page dimensions (Letter size: 612 x 792 points)
|
|
47
|
+
const pageWidth = 612;
|
|
48
|
+
const pageHeight = 792;
|
|
49
|
+
const margin = 36;
|
|
50
|
+
const contentWidth = pageWidth - (margin * 2);
|
|
51
|
+
|
|
52
|
+
// Create the AI summary page
|
|
53
|
+
let page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
54
|
+
let yPos = pageHeight - margin;
|
|
55
|
+
|
|
56
|
+
// Load mQ logo
|
|
57
|
+
const mqLogoBytes = util._base64ToArrayBuffer(base64.mqLogo);
|
|
58
|
+
const mqLogo = await pdfDoc.embedPng(mqLogoBytes);
|
|
59
|
+
|
|
60
|
+
// Load company logo if available
|
|
61
|
+
let companyImage = null;
|
|
62
|
+
const companyImageType = util.checkCompanyLogo(payload.meta);
|
|
63
|
+
if (companyImageType === 'jpg') {
|
|
64
|
+
companyImage = await pdfDoc.embedJpg(util._base64ToArrayBuffer(payload.meta.profile_settings.companyLogoBase64));
|
|
65
|
+
} else if (companyImageType === 'png') {
|
|
66
|
+
companyImage = await pdfDoc.embedPng(util._base64ToArrayBuffer(payload.meta.profile_settings.companyLogoBase64));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// === HEADER SECTION ===
|
|
70
|
+
const headerHeight = 45;
|
|
71
|
+
page.drawRectangle({
|
|
72
|
+
x: margin,
|
|
73
|
+
y: yPos - headerHeight,
|
|
74
|
+
width: contentWidth,
|
|
75
|
+
height: headerHeight,
|
|
76
|
+
color: colors.headerBg
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Header title
|
|
80
|
+
page.drawText('mQ AI Summary', {
|
|
81
|
+
x: margin + 15,
|
|
82
|
+
y: yPos - 28,
|
|
83
|
+
size: 16,
|
|
84
|
+
font: fontBold,
|
|
85
|
+
color: colors.white
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Draw logo in header (right side)
|
|
89
|
+
const logoToUse = companyImage || mqLogo;
|
|
90
|
+
if (logoToUse) {
|
|
91
|
+
const logoHeight = 30;
|
|
92
|
+
const logoWidth = logoHeight * (logoToUse.width / logoToUse.height);
|
|
93
|
+
page.drawImage(logoToUse, {
|
|
94
|
+
x: pageWidth - margin - logoWidth - 10,
|
|
95
|
+
y: yPos - headerHeight + 7,
|
|
96
|
+
width: logoWidth,
|
|
97
|
+
height: logoHeight
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
yPos -= headerHeight + 15;
|
|
102
|
+
|
|
103
|
+
// === VITALS SCORES AND DATE/TIME SECTION ===
|
|
104
|
+
const hasTest1 = payload.tests && payload.tests[0] && payload.tests[0].testInfo && payload.tests[0].testInfo.data;
|
|
105
|
+
const hasTest2 = payload.tests && payload.tests[1] && payload.tests[1].testInfo && payload.tests[1].testInfo.data;
|
|
106
|
+
const hasTwoTests = hasTest1 && hasTest2;
|
|
107
|
+
|
|
108
|
+
// Get date/time from test
|
|
109
|
+
let dateTimeStr = '';
|
|
110
|
+
if (hasTest1 && payload.tests[0].testInfo.data.datetimeStart) {
|
|
111
|
+
const date = new Date(payload.tests[0].testInfo.data.datetimeStart);
|
|
112
|
+
dateTimeStr = date.toLocaleDateString('en-US', {
|
|
113
|
+
year: 'numeric',
|
|
114
|
+
month: 'short',
|
|
115
|
+
day: 'numeric',
|
|
116
|
+
hour: 'numeric',
|
|
117
|
+
minute: '2-digit'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Draw score and date/time in a compact row
|
|
122
|
+
const rowHeight = 35;
|
|
123
|
+
|
|
124
|
+
if (hasTest1) {
|
|
125
|
+
const score1 = payload.tests[0].testInfo.data.vitals_score;
|
|
126
|
+
const scorePercent1 = score1 ? Math.round(score1) + '%' : '--';
|
|
127
|
+
const grade1 = score1 ? util.getGradeFromScore(Math.round(score1)) : '';
|
|
128
|
+
const scoreColor1 = getScoreColor(grade1);
|
|
129
|
+
const scoreLabel1 = hasTwoTests ? 'Test In:' : 'Score:';
|
|
130
|
+
const scoreText1 = scorePercent1 + (grade1 ? ' ' + grade1 : '');
|
|
131
|
+
|
|
132
|
+
// Score label and value
|
|
133
|
+
page.drawText(scoreLabel1, {
|
|
134
|
+
x: margin,
|
|
135
|
+
y: yPos - 12,
|
|
136
|
+
size: 10,
|
|
137
|
+
font: fontRegular,
|
|
138
|
+
color: colors.darkGray
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
page.drawText(scoreText1, {
|
|
142
|
+
x: margin + 50,
|
|
143
|
+
y: yPos - 12,
|
|
144
|
+
size: 14,
|
|
145
|
+
font: fontBold,
|
|
146
|
+
color: scoreColor1
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Test Out score if exists
|
|
150
|
+
if (hasTwoTests) {
|
|
151
|
+
const score2 = payload.tests[1].testInfo.data.vitals_score;
|
|
152
|
+
const scorePercent2 = score2 ? Math.round(score2) + '%' : '--';
|
|
153
|
+
const grade2 = score2 ? util.getGradeFromScore(Math.round(score2)) : '';
|
|
154
|
+
const scoreColor2 = getScoreColor(grade2);
|
|
155
|
+
const scoreText2 = scorePercent2 + (grade2 ? ' ' + grade2 : '');
|
|
156
|
+
|
|
157
|
+
page.drawText('Test Out:', {
|
|
158
|
+
x: margin + 130,
|
|
159
|
+
y: yPos - 12,
|
|
160
|
+
size: 10,
|
|
161
|
+
font: fontRegular,
|
|
162
|
+
color: colors.darkGray
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
page.drawText(scoreText2, {
|
|
166
|
+
x: margin + 180,
|
|
167
|
+
y: yPos - 12,
|
|
168
|
+
size: 14,
|
|
169
|
+
font: fontBold,
|
|
170
|
+
color: scoreColor2
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Date/time on the right
|
|
175
|
+
if (dateTimeStr) {
|
|
176
|
+
const dateWidth = fontRegular.widthOfTextAtSize(dateTimeStr, 10);
|
|
177
|
+
page.drawText(dateTimeStr, {
|
|
178
|
+
x: pageWidth - margin - dateWidth,
|
|
179
|
+
y: yPos - 12,
|
|
180
|
+
size: 10,
|
|
181
|
+
font: fontRegular,
|
|
182
|
+
color: colors.darkGray
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
yPos -= rowHeight;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Draw separator line
|
|
190
|
+
page.drawLine({
|
|
191
|
+
start: { x: margin, y: yPos },
|
|
192
|
+
end: { x: pageWidth - margin, y: yPos },
|
|
193
|
+
thickness: 1,
|
|
194
|
+
color: colors.mediumGray
|
|
195
|
+
});
|
|
196
|
+
yPos -= 15;
|
|
197
|
+
|
|
198
|
+
// === AI SUMMARY CONTENT ===
|
|
199
|
+
const lineHeight = 14;
|
|
200
|
+
const summaryLines = util.wrapTextWithNewlines(aiSummary, fontRegular, 10, contentWidth);
|
|
201
|
+
|
|
202
|
+
for (const line of summaryLines) {
|
|
203
|
+
if (yPos < margin + 50) {
|
|
204
|
+
// Add new page if needed
|
|
205
|
+
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
206
|
+
yPos = pageHeight - margin;
|
|
207
|
+
|
|
208
|
+
// Add continuation header
|
|
209
|
+
page.drawText('mQ AI Summary (continued)', {
|
|
210
|
+
x: margin,
|
|
211
|
+
y: yPos,
|
|
212
|
+
size: 12,
|
|
213
|
+
font: fontBold,
|
|
214
|
+
color: colors.headerBg
|
|
215
|
+
});
|
|
216
|
+
yPos -= 20;
|
|
217
|
+
|
|
218
|
+
page.drawLine({
|
|
219
|
+
start: { x: margin, y: yPos },
|
|
220
|
+
end: { x: pageWidth - margin, y: yPos },
|
|
221
|
+
thickness: 1,
|
|
222
|
+
color: colors.mediumGray
|
|
223
|
+
});
|
|
224
|
+
yPos -= 15;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (line) {
|
|
228
|
+
page.drawText(line, {
|
|
229
|
+
x: margin,
|
|
230
|
+
y: yPos,
|
|
231
|
+
size: 10,
|
|
232
|
+
font: fontRegular,
|
|
233
|
+
color: colors.black
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
yPos -= lineHeight;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// === NOTES SECTION (if exists) ===
|
|
240
|
+
const notes = util.getAiNotes(payload);
|
|
241
|
+
if (notes && notes.trim().length > 0) {
|
|
242
|
+
yPos -= 15;
|
|
243
|
+
|
|
244
|
+
if (yPos < margin + 100) {
|
|
245
|
+
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
246
|
+
yPos = pageHeight - margin;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
page.drawText('Notes', {
|
|
250
|
+
x: margin,
|
|
251
|
+
y: yPos,
|
|
252
|
+
size: 12,
|
|
253
|
+
font: fontBold,
|
|
254
|
+
color: colors.headerBg
|
|
255
|
+
});
|
|
256
|
+
yPos -= 5;
|
|
257
|
+
|
|
258
|
+
page.drawLine({
|
|
259
|
+
start: { x: margin, y: yPos },
|
|
260
|
+
end: { x: pageWidth - margin, y: yPos },
|
|
261
|
+
thickness: 1,
|
|
262
|
+
color: colors.headerBg
|
|
263
|
+
});
|
|
264
|
+
yPos -= 15;
|
|
265
|
+
|
|
266
|
+
const notesLines = util.wrapTextWithNewlines(notes, fontRegular, 10, contentWidth);
|
|
267
|
+
|
|
268
|
+
for (const line of notesLines) {
|
|
269
|
+
if (yPos < margin + 50) {
|
|
270
|
+
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
271
|
+
yPos = pageHeight - margin;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (line) {
|
|
275
|
+
page.drawText(line, {
|
|
276
|
+
x: margin,
|
|
277
|
+
y: yPos,
|
|
278
|
+
size: 10,
|
|
279
|
+
font: fontRegular,
|
|
280
|
+
color: colors.black
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
yPos -= lineHeight;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if AI summary should be shown based on payload settings
|
|
290
|
+
*/
|
|
291
|
+
export function shouldShow(payload) {
|
|
292
|
+
return util.shouldShowAiSummary(payload);
|
|
293
|
+
}
|
|
@@ -622,7 +622,10 @@ export async function getReport(payload) {
|
|
|
622
622
|
) {
|
|
623
623
|
let v = measurement.value;
|
|
624
624
|
if (typeof v == "string") v = parseFloat(v);
|
|
625
|
-
if (v
|
|
625
|
+
if (v !== null && v !== undefined && !isNaN(v)) {
|
|
626
|
+
if (v > 9) rangeIcon = embeddedIcons.iconRangeHigh;
|
|
627
|
+
else rangeIcon = embeddedIcons.iconRangeNormal;
|
|
628
|
+
}
|
|
626
629
|
} else
|
|
627
630
|
rangeIcon = util.getRangeIcon(
|
|
628
631
|
test,
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
|
2
|
+
import fontkit from '@pdf-lib/fontkit';
|
|
3
|
+
import * as base64 from '../base-64/icons.js';
|
|
4
|
+
import * as util from '../util.js';
|
|
5
|
+
|
|
6
|
+
// Check if project is CoolMaxx or HeatMaxx (NCI branded)
|
|
7
|
+
function isNCIProject(payload) {
|
|
8
|
+
if (payload && payload.project && payload.project.specialProjectType) {
|
|
9
|
+
const type = payload.project.specialProjectType;
|
|
10
|
+
return type === 'CoolMaxx' || type === 'HeatMaxx';
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Color definitions matching MeasureQuick brand
|
|
16
|
+
const colors = {
|
|
17
|
+
headerBg: rgb(0.18, 0.31, 0.47), // Dark blue header
|
|
18
|
+
scoreGreen: rgb(0.18, 0.62, 0.35), // Green for A grades
|
|
19
|
+
scoreYellow: rgb(0.91, 0.73, 0.18), // Yellow for B grades
|
|
20
|
+
scoreOrange: rgb(0.91, 0.49, 0.18), // Orange for C grades
|
|
21
|
+
scoreRed: rgb(0.78, 0.22, 0.22), // Red for D/F grades
|
|
22
|
+
lightGray: rgb(0.95, 0.95, 0.95), // Light gray background
|
|
23
|
+
mediumGray: rgb(0.7, 0.7, 0.7), // Medium gray for borders
|
|
24
|
+
darkGray: rgb(0.3, 0.3, 0.3), // Dark gray for text
|
|
25
|
+
black: rgb(0, 0, 0),
|
|
26
|
+
white: rgb(1, 1, 1)
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function getScoreColor(grade) {
|
|
30
|
+
if (grade.includes('A')) return colors.scoreGreen;
|
|
31
|
+
if (grade.includes('B')) return colors.scoreYellow;
|
|
32
|
+
if (grade.includes('C')) return colors.scoreOrange;
|
|
33
|
+
return colors.scoreRed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Wrap text to fit within a given width
|
|
37
|
+
function wrapText(text, font, fontSize, maxWidth) {
|
|
38
|
+
if (!text) return [];
|
|
39
|
+
const words = text.split(' ');
|
|
40
|
+
const lines = [];
|
|
41
|
+
let currentLine = '';
|
|
42
|
+
|
|
43
|
+
for (const word of words) {
|
|
44
|
+
const testLine = currentLine ? currentLine + ' ' + word : word;
|
|
45
|
+
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
|
|
46
|
+
|
|
47
|
+
if (testWidth > maxWidth && currentLine) {
|
|
48
|
+
lines.push(currentLine);
|
|
49
|
+
currentLine = word;
|
|
50
|
+
} else {
|
|
51
|
+
currentLine = testLine;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (currentLine) {
|
|
56
|
+
lines.push(currentLine);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle newlines and wrap text
|
|
63
|
+
function wrapTextWithNewlines(text, font, fontSize, maxWidth) {
|
|
64
|
+
if (!text) return [];
|
|
65
|
+
const paragraphs = text.split('\n');
|
|
66
|
+
const allLines = [];
|
|
67
|
+
|
|
68
|
+
for (const paragraph of paragraphs) {
|
|
69
|
+
if (paragraph.trim() === '') {
|
|
70
|
+
allLines.push('');
|
|
71
|
+
} else {
|
|
72
|
+
const wrappedLines = wrapText(paragraph, font, fontSize, maxWidth);
|
|
73
|
+
allLines.push(...wrappedLines);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return allLines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getReport(payload) {
|
|
81
|
+
try {
|
|
82
|
+
// Create a new PDF document
|
|
83
|
+
const pdfDoc = await PDFDocument.create();
|
|
84
|
+
pdfDoc.registerFontkit(fontkit);
|
|
85
|
+
|
|
86
|
+
// Embed fonts
|
|
87
|
+
const fontRegular = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
88
|
+
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
89
|
+
|
|
90
|
+
// Page dimensions (Letter size: 612 x 792 points)
|
|
91
|
+
const pageWidth = 612;
|
|
92
|
+
const pageHeight = 792;
|
|
93
|
+
const margin = 36; // 0.5 inch margins
|
|
94
|
+
const contentWidth = pageWidth - (margin * 2);
|
|
95
|
+
|
|
96
|
+
// Create first page
|
|
97
|
+
let page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
98
|
+
|
|
99
|
+
// Load mQ logo
|
|
100
|
+
const mqLogoBytes = util._base64ToArrayBuffer(base64.mqLogo);
|
|
101
|
+
const mqLogo = await pdfDoc.embedPng(mqLogoBytes);
|
|
102
|
+
|
|
103
|
+
// Load NCI logo if applicable
|
|
104
|
+
let nciLogo = null;
|
|
105
|
+
const isNCI = isNCIProject(payload) && base64.nciLogo;
|
|
106
|
+
if (isNCI) {
|
|
107
|
+
const nciLogoBytes = util._base64ToArrayBuffer(base64.nciLogo);
|
|
108
|
+
nciLogo = await pdfDoc.embedPng(nciLogoBytes);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Load company logo and profile picture
|
|
112
|
+
let profileImage = null, companyImage = null;
|
|
113
|
+
const profileImageType = util.checkProfilePicture(payload.meta);
|
|
114
|
+
const companyImageType = util.checkCompanyLogo(payload.meta);
|
|
115
|
+
|
|
116
|
+
if (profileImageType) {
|
|
117
|
+
let profileData;
|
|
118
|
+
if (profileImageType.shape === 'square') {
|
|
119
|
+
profileData = payload.meta.profile_settings.profilePicSquareBase64;
|
|
120
|
+
} else if (profileImageType.shape === 'circle') {
|
|
121
|
+
profileData = payload.meta.profile_settings.profilePicCircleBase64;
|
|
122
|
+
}
|
|
123
|
+
if (profileData) {
|
|
124
|
+
if (profileImageType.type === 'jpg') {
|
|
125
|
+
profileImage = await pdfDoc.embedJpg(util._base64ToArrayBuffer(profileData));
|
|
126
|
+
} else if (profileImageType.type === 'png') {
|
|
127
|
+
profileImage = await pdfDoc.embedPng(util._base64ToArrayBuffer(profileData));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (companyImageType === 'jpg') {
|
|
133
|
+
companyImage = await pdfDoc.embedJpg(util._base64ToArrayBuffer(payload.meta.profile_settings.companyLogoBase64));
|
|
134
|
+
} else if (companyImageType === 'png') {
|
|
135
|
+
companyImage = await pdfDoc.embedPng(util._base64ToArrayBuffer(payload.meta.profile_settings.companyLogoBase64));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let yPos = pageHeight - margin;
|
|
139
|
+
|
|
140
|
+
// === HEADER SECTION ===
|
|
141
|
+
const headerHeight = 50;
|
|
142
|
+
page.drawRectangle({
|
|
143
|
+
x: margin,
|
|
144
|
+
y: yPos - headerHeight,
|
|
145
|
+
width: contentWidth,
|
|
146
|
+
height: headerHeight,
|
|
147
|
+
color: colors.headerBg
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Header title
|
|
151
|
+
page.drawText('mQ AI Summary & Notes', {
|
|
152
|
+
x: margin + 15,
|
|
153
|
+
y: yPos - 32,
|
|
154
|
+
size: 18,
|
|
155
|
+
font: fontBold,
|
|
156
|
+
color: colors.white
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Draw logo in header (right side)
|
|
160
|
+
const logoToUse = isNCI ? nciLogo : (companyImage || mqLogo);
|
|
161
|
+
if (logoToUse) {
|
|
162
|
+
const logoHeight = 35;
|
|
163
|
+
const logoWidth = logoHeight * (logoToUse.width / logoToUse.height);
|
|
164
|
+
page.drawImage(logoToUse, {
|
|
165
|
+
x: pageWidth - margin - logoWidth - 10,
|
|
166
|
+
y: yPos - headerHeight + 7,
|
|
167
|
+
width: logoWidth,
|
|
168
|
+
height: logoHeight
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
yPos -= headerHeight + 15;
|
|
173
|
+
|
|
174
|
+
// === CUSTOMER INFO SECTION ===
|
|
175
|
+
const customerName = payload.project && payload.project.customer_name ? payload.project.customer_name : '';
|
|
176
|
+
const address1 = payload.project && payload.project.address ? payload.project.address : '';
|
|
177
|
+
const city = payload.project && payload.project.city ? payload.project.city : '';
|
|
178
|
+
const state = payload.project && payload.project.state ? payload.project.state : '';
|
|
179
|
+
const zip = payload.project && payload.project.zip ? payload.project.zip : '';
|
|
180
|
+
const cityStateZip = [city, state, zip].filter(x => x).join(', ');
|
|
181
|
+
|
|
182
|
+
if (customerName) {
|
|
183
|
+
page.drawText(customerName, { x: margin, y: yPos, size: 11, font: fontBold, color: colors.darkGray });
|
|
184
|
+
yPos -= 14;
|
|
185
|
+
}
|
|
186
|
+
if (address1) {
|
|
187
|
+
page.drawText(address1, { x: margin, y: yPos, size: 10, font: fontRegular, color: colors.darkGray });
|
|
188
|
+
yPos -= 12;
|
|
189
|
+
}
|
|
190
|
+
if (cityStateZip) {
|
|
191
|
+
page.drawText(cityStateZip, { x: margin, y: yPos, size: 10, font: fontRegular, color: colors.darkGray });
|
|
192
|
+
yPos -= 12;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Date of service
|
|
196
|
+
let dateStr = '';
|
|
197
|
+
if (payload.tests && payload.tests[0] && payload.tests[0].testInfo && payload.tests[0].testInfo.data && payload.tests[0].testInfo.data.datetimeStart) {
|
|
198
|
+
const date = new Date(payload.tests[0].testInfo.data.datetimeStart);
|
|
199
|
+
dateStr = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
200
|
+
}
|
|
201
|
+
if (dateStr) {
|
|
202
|
+
page.drawText('Date: ' + dateStr, { x: margin, y: yPos, size: 10, font: fontRegular, color: colors.darkGray });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Tech name (right aligned)
|
|
206
|
+
const techFirstName = payload.meta && payload.meta.profile_settings && payload.meta.profile_settings.techFirstName ? payload.meta.profile_settings.techFirstName : '';
|
|
207
|
+
const techLastName = payload.meta && payload.meta.profile_settings && payload.meta.profile_settings.techLastName ? payload.meta.profile_settings.techLastName : '';
|
|
208
|
+
const techName = [techFirstName, techLastName].filter(x => x).join(' ');
|
|
209
|
+
if (techName) {
|
|
210
|
+
const techText = 'Technician: ' + techName;
|
|
211
|
+
const techWidth = fontRegular.widthOfTextAtSize(techText, 10);
|
|
212
|
+
page.drawText(techText, { x: pageWidth - margin - techWidth, y: yPos, size: 10, font: fontRegular, color: colors.darkGray });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
yPos -= 25;
|
|
216
|
+
|
|
217
|
+
// === VITALS SCORES SECTION ===
|
|
218
|
+
const hasTest1 = payload.tests && payload.tests[0] && payload.tests[0].testInfo && payload.tests[0].testInfo.data;
|
|
219
|
+
const hasTest2 = payload.tests && payload.tests[1] && payload.tests[1].testInfo && payload.tests[1].testInfo.data;
|
|
220
|
+
const hasTwoTests = hasTest1 && hasTest2;
|
|
221
|
+
|
|
222
|
+
const scoreBoxWidth = hasTwoTests ? (contentWidth - 20) / 2 : contentWidth;
|
|
223
|
+
const scoreBoxHeight = 60;
|
|
224
|
+
|
|
225
|
+
// Draw score box(es)
|
|
226
|
+
if (hasTest1) {
|
|
227
|
+
const score1 = payload.tests[0].testInfo.data.vitals_score;
|
|
228
|
+
const scorePercent1 = score1 ? Math.round(score1) + '%' : '--';
|
|
229
|
+
const grade1 = score1 ? util.getGradeFromScore(Math.round(score1)) : '';
|
|
230
|
+
const scoreColor1 = score1 ? getScoreColor(grade1) : colors.mediumGray;
|
|
231
|
+
const scoreLabel1 = hasTwoTests ? 'Test In Score' : 'System Vitals Score';
|
|
232
|
+
|
|
233
|
+
// Score box background
|
|
234
|
+
page.drawRectangle({
|
|
235
|
+
x: margin,
|
|
236
|
+
y: yPos - scoreBoxHeight,
|
|
237
|
+
width: scoreBoxWidth,
|
|
238
|
+
height: scoreBoxHeight,
|
|
239
|
+
color: colors.lightGray,
|
|
240
|
+
borderColor: scoreColor1,
|
|
241
|
+
borderWidth: 2
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Score label
|
|
245
|
+
page.drawText(scoreLabel1, {
|
|
246
|
+
x: margin + 10,
|
|
247
|
+
y: yPos - 18,
|
|
248
|
+
size: 10,
|
|
249
|
+
font: fontRegular,
|
|
250
|
+
color: colors.darkGray
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Score value
|
|
254
|
+
const scoreText1 = scorePercent1 + (grade1 ? ' ' + grade1 : '');
|
|
255
|
+
page.drawText(scoreText1, {
|
|
256
|
+
x: margin + 10,
|
|
257
|
+
y: yPos - 45,
|
|
258
|
+
size: 24,
|
|
259
|
+
font: fontBold,
|
|
260
|
+
color: scoreColor1
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (hasTwoTests) {
|
|
265
|
+
const score2 = payload.tests[1].testInfo.data.vitals_score;
|
|
266
|
+
const scorePercent2 = score2 ? Math.round(score2) + '%' : '--';
|
|
267
|
+
const grade2 = score2 ? util.getGradeFromScore(Math.round(score2)) : '';
|
|
268
|
+
const scoreColor2 = score2 ? getScoreColor(grade2) : colors.mediumGray;
|
|
269
|
+
|
|
270
|
+
// Score box background
|
|
271
|
+
page.drawRectangle({
|
|
272
|
+
x: margin + scoreBoxWidth + 20,
|
|
273
|
+
y: yPos - scoreBoxHeight,
|
|
274
|
+
width: scoreBoxWidth,
|
|
275
|
+
height: scoreBoxHeight,
|
|
276
|
+
color: colors.lightGray,
|
|
277
|
+
borderColor: scoreColor2,
|
|
278
|
+
borderWidth: 2
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Score label
|
|
282
|
+
page.drawText('Test Out Score', {
|
|
283
|
+
x: margin + scoreBoxWidth + 30,
|
|
284
|
+
y: yPos - 18,
|
|
285
|
+
size: 10,
|
|
286
|
+
font: fontRegular,
|
|
287
|
+
color: colors.darkGray
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Score value
|
|
291
|
+
const scoreText2 = scorePercent2 + (grade2 ? ' ' + grade2 : '');
|
|
292
|
+
page.drawText(scoreText2, {
|
|
293
|
+
x: margin + scoreBoxWidth + 30,
|
|
294
|
+
y: yPos - 45,
|
|
295
|
+
size: 24,
|
|
296
|
+
font: fontBold,
|
|
297
|
+
color: scoreColor2
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
yPos -= scoreBoxHeight + 20;
|
|
302
|
+
|
|
303
|
+
// === AI SUMMARY SECTION ===
|
|
304
|
+
page.drawText('AI Summary', {
|
|
305
|
+
x: margin,
|
|
306
|
+
y: yPos,
|
|
307
|
+
size: 14,
|
|
308
|
+
font: fontBold,
|
|
309
|
+
color: colors.headerBg
|
|
310
|
+
});
|
|
311
|
+
yPos -= 5;
|
|
312
|
+
|
|
313
|
+
// Draw line under header
|
|
314
|
+
page.drawLine({
|
|
315
|
+
start: { x: margin, y: yPos },
|
|
316
|
+
end: { x: pageWidth - margin, y: yPos },
|
|
317
|
+
thickness: 1,
|
|
318
|
+
color: colors.headerBg
|
|
319
|
+
});
|
|
320
|
+
yPos -= 15;
|
|
321
|
+
|
|
322
|
+
// AI Summary content
|
|
323
|
+
const aiSummary = payload.ai_summary || payload.project && payload.project.ai_summary || 'No AI summary available.';
|
|
324
|
+
const summaryLines = wrapTextWithNewlines(aiSummary, fontRegular, 10, contentWidth);
|
|
325
|
+
const lineHeight = 14;
|
|
326
|
+
|
|
327
|
+
for (const line of summaryLines) {
|
|
328
|
+
if (yPos < margin + 100) {
|
|
329
|
+
// Add new page if needed
|
|
330
|
+
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
331
|
+
yPos = pageHeight - margin;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (line) {
|
|
335
|
+
page.drawText(line, {
|
|
336
|
+
x: margin,
|
|
337
|
+
y: yPos,
|
|
338
|
+
size: 10,
|
|
339
|
+
font: fontRegular,
|
|
340
|
+
color: colors.black
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
yPos -= lineHeight;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
yPos -= 20;
|
|
347
|
+
|
|
348
|
+
// === NOTES SECTION ===
|
|
349
|
+
if (yPos < margin + 150) {
|
|
350
|
+
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
351
|
+
yPos = pageHeight - margin;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
page.drawText('Notes', {
|
|
355
|
+
x: margin,
|
|
356
|
+
y: yPos,
|
|
357
|
+
size: 14,
|
|
358
|
+
font: fontBold,
|
|
359
|
+
color: colors.headerBg
|
|
360
|
+
});
|
|
361
|
+
yPos -= 5;
|
|
362
|
+
|
|
363
|
+
// Draw line under header
|
|
364
|
+
page.drawLine({
|
|
365
|
+
start: { x: margin, y: yPos },
|
|
366
|
+
end: { x: pageWidth - margin, y: yPos },
|
|
367
|
+
thickness: 1,
|
|
368
|
+
color: colors.headerBg
|
|
369
|
+
});
|
|
370
|
+
yPos -= 15;
|
|
371
|
+
|
|
372
|
+
// Notes content - check multiple possible sources
|
|
373
|
+
let notes = payload.notes || '';
|
|
374
|
+
if (!notes && payload.project && payload.project.notes) {
|
|
375
|
+
notes = payload.project.notes;
|
|
376
|
+
}
|
|
377
|
+
if (!notes && payload.tests && payload.tests[0] && payload.tests[0].testInfo && payload.tests[0].testInfo.notes) {
|
|
378
|
+
notes = payload.tests[0].testInfo.notes;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!notes) {
|
|
382
|
+
notes = 'No notes available.';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const notesLines = wrapTextWithNewlines(notes, fontRegular, 10, contentWidth);
|
|
386
|
+
|
|
387
|
+
for (const line of notesLines) {
|
|
388
|
+
if (yPos < margin + 50) {
|
|
389
|
+
// Add new page if needed
|
|
390
|
+
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
391
|
+
yPos = pageHeight - margin;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (line) {
|
|
395
|
+
page.drawText(line, {
|
|
396
|
+
x: margin,
|
|
397
|
+
y: yPos,
|
|
398
|
+
size: 10,
|
|
399
|
+
font: fontRegular,
|
|
400
|
+
color: colors.black
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
yPos -= lineHeight;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// === FOOTER SECTION ===
|
|
407
|
+
const footerY = margin;
|
|
408
|
+
|
|
409
|
+
// Draw footer on all pages
|
|
410
|
+
const pages = pdfDoc.getPages();
|
|
411
|
+
for (let i = 0; i < pages.length; i++) {
|
|
412
|
+
const p = pages[i];
|
|
413
|
+
|
|
414
|
+
// Footer line
|
|
415
|
+
p.drawLine({
|
|
416
|
+
start: { x: margin, y: footerY + 25 },
|
|
417
|
+
end: { x: pageWidth - margin, y: footerY + 25 },
|
|
418
|
+
thickness: 0.5,
|
|
419
|
+
color: colors.mediumGray
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Profile picture (if available and not NCI)
|
|
423
|
+
if (!isNCI && profileImage) {
|
|
424
|
+
const profileSize = 20;
|
|
425
|
+
p.drawImage(profileImage, {
|
|
426
|
+
x: margin,
|
|
427
|
+
y: footerY,
|
|
428
|
+
width: profileSize,
|
|
429
|
+
height: profileSize
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Tech name next to profile
|
|
433
|
+
if (techName) {
|
|
434
|
+
p.drawText(techName, {
|
|
435
|
+
x: margin + profileSize + 5,
|
|
436
|
+
y: footerY + 6,
|
|
437
|
+
size: 8,
|
|
438
|
+
font: fontRegular,
|
|
439
|
+
color: colors.darkGray
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
} else if (techName) {
|
|
443
|
+
p.drawText(techName, {
|
|
444
|
+
x: margin,
|
|
445
|
+
y: footerY + 6,
|
|
446
|
+
size: 8,
|
|
447
|
+
font: fontRegular,
|
|
448
|
+
color: colors.darkGray
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Page number (center)
|
|
453
|
+
const pageNumText = 'Page ' + (i + 1) + ' of ' + pages.length;
|
|
454
|
+
const pageNumWidth = fontRegular.widthOfTextAtSize(pageNumText, 8);
|
|
455
|
+
p.drawText(pageNumText, {
|
|
456
|
+
x: (pageWidth - pageNumWidth) / 2,
|
|
457
|
+
y: footerY + 6,
|
|
458
|
+
size: 8,
|
|
459
|
+
font: fontRegular,
|
|
460
|
+
color: colors.darkGray
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// mQ logo or company logo (right side)
|
|
464
|
+
const footerLogo = companyImage || mqLogo;
|
|
465
|
+
if (footerLogo) {
|
|
466
|
+
const footerLogoHeight = 18;
|
|
467
|
+
const footerLogoWidth = footerLogoHeight * (footerLogo.width / footerLogo.height);
|
|
468
|
+
p.drawImage(footerLogo, {
|
|
469
|
+
x: pageWidth - margin - footerLogoWidth,
|
|
470
|
+
y: footerY,
|
|
471
|
+
width: footerLogoWidth,
|
|
472
|
+
height: footerLogoHeight
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Save and return
|
|
478
|
+
return {
|
|
479
|
+
status: 200,
|
|
480
|
+
data: await pdfDoc.saveAsBase64({ dataUri: true })
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
} catch (error) {
|
|
484
|
+
console.error('Error generating AI Summary report:', error);
|
|
485
|
+
return {
|
|
486
|
+
status: 400,
|
|
487
|
+
data: error.message || error
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -3,6 +3,7 @@ import * as base64 from "../base-64/icons.js";
|
|
|
3
3
|
import * as pdf from "../base-64/mq-vitals-cooling.js";
|
|
4
4
|
import * as systemInfoPage from "./system-info-page.js";
|
|
5
5
|
import * as util from "../util.js";
|
|
6
|
+
import * as aiSummaryPage from "./ai-summary-page.js";
|
|
6
7
|
|
|
7
8
|
// Check if project is CoolMaxx or HeatMaxx (NCI branded)
|
|
8
9
|
function isNCIProject(payload) {
|
|
@@ -420,8 +421,12 @@ export async function getReport(payload, _test) {
|
|
|
420
421
|
pdfDoc.insertPage(pdfDoc.getPages().length, _systemInfoPageDoc);
|
|
421
422
|
}
|
|
422
423
|
|
|
423
|
-
// flatten
|
|
424
|
+
// flatten form fields before adding AI summary pages
|
|
424
425
|
form.flatten();
|
|
426
|
+
|
|
427
|
+
// Add AI summary page if enabled and summary exists
|
|
428
|
+
await aiSummaryPage.addAiSummaryPages(pdfDoc, payload);
|
|
429
|
+
|
|
425
430
|
return { status: 200, data: await pdfDoc.saveAsBase64({ dataUri: true }) };
|
|
426
431
|
} catch (error) {
|
|
427
432
|
return { status: 400, data: error };
|
|
@@ -2,8 +2,8 @@ import { PDFDocument } from "pdf-lib";
|
|
|
2
2
|
import * as base64 from "../base-64/icons.js";
|
|
3
3
|
import * as pdf from "../base-64/mq-vitals-heating.js";
|
|
4
4
|
import * as systemInfoPage from "./system-info-page.js";
|
|
5
|
-
|
|
6
5
|
import * as util from "../util.js";
|
|
6
|
+
import * as aiSummaryPage from "./ai-summary-page.js";
|
|
7
7
|
|
|
8
8
|
// Check if project is CoolMaxx or HeatMaxx (NCI branded)
|
|
9
9
|
function isNCIProject(payload) {
|
|
@@ -628,8 +628,12 @@ export async function getReport(payload, _test) {
|
|
|
628
628
|
pdfDoc.insertPage(pdfDoc.getPages().length, _systemInfoPageDoc);
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
// flatten
|
|
631
|
+
// flatten form fields before adding AI summary pages
|
|
632
632
|
form.flatten();
|
|
633
|
+
|
|
634
|
+
// Add AI summary page if enabled and summary exists
|
|
635
|
+
await aiSummaryPage.addAiSummaryPages(pdfDoc, payload);
|
|
636
|
+
|
|
633
637
|
return { status: 200, data: await pdfDoc.saveAsBase64({ dataUri: true }) };
|
|
634
638
|
} catch (error) {
|
|
635
639
|
return { status: 400, data: error };
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
|
|
2
|
+
import * as base64 from "../base-64/icons.js";
|
|
3
|
+
import * as pdf from "../base-64/mq-vitals-heatpump-heating.js";
|
|
4
|
+
import * as systemInfoPage from "./system-info-page.js";
|
|
5
|
+
import * as util from "../util.js";
|
|
6
|
+
import * as aiSummaryPage from "./ai-summary-page.js";
|
|
7
|
+
|
|
8
|
+
// Check if project is CoolMaxx or HeatMaxx (NCI branded)
|
|
9
|
+
function isNCIProject(payload) {
|
|
10
|
+
if (payload && payload.project && payload.project.specialProjectType) {
|
|
11
|
+
const type = payload.project.specialProjectType;
|
|
12
|
+
return type === 'CoolMaxx' || type === 'HeatMaxx';
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function getReport(payload, _test) {
|
|
18
|
+
try {
|
|
19
|
+
// fetch and load form
|
|
20
|
+
const formPdfBytes = util._base64ToArrayBuffer(pdf.base64);
|
|
21
|
+
const pdfDoc = await PDFDocument.load(formPdfBytes);
|
|
22
|
+
|
|
23
|
+
// Embed font for custom text overlays
|
|
24
|
+
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
25
|
+
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
26
|
+
|
|
27
|
+
// fetch resources
|
|
28
|
+
const iconRangeGreenBytes = util._base64ToArrayBuffer(base64.iconRangeGreen);
|
|
29
|
+
const iconRangeRedBytes = util._base64ToArrayBuffer(base64.iconRangeRed);
|
|
30
|
+
const iconFlagBlackBytes = util._base64ToArrayBuffer(base64.iconFlagBlack);
|
|
31
|
+
const iconFlagGreenBytes = util._base64ToArrayBuffer(base64.iconFlagGreen);
|
|
32
|
+
const iconFlagRedBytes = util._base64ToArrayBuffer(base64.iconFlagRed);
|
|
33
|
+
const iconFlagYellowBytes = util._base64ToArrayBuffer(base64.iconFlagYellow);
|
|
34
|
+
const iconStabilityBytes = util._base64ToArrayBuffer(base64.iconStability);
|
|
35
|
+
const iconThumbprintGreenBytes = util._base64ToArrayBuffer(base64.iconThumbprintGreen);
|
|
36
|
+
const iconThumbprintRedBytes = util._base64ToArrayBuffer(base64.iconThumbprintRed);
|
|
37
|
+
const mqLogoBytes = util._base64ToArrayBuffer(base64.mqLogo);
|
|
38
|
+
|
|
39
|
+
// load resources
|
|
40
|
+
const iconRangeGreen = await pdfDoc.embedPng(iconRangeGreenBytes);
|
|
41
|
+
const iconRangeRed = await pdfDoc.embedPng(iconRangeRedBytes);
|
|
42
|
+
const iconFlagBlack = await pdfDoc.embedPng(iconFlagBlackBytes);
|
|
43
|
+
const iconFlagGreen = await pdfDoc.embedPng(iconFlagGreenBytes);
|
|
44
|
+
const iconFlagRed = await pdfDoc.embedPng(iconFlagRedBytes);
|
|
45
|
+
const iconFlagYellow = await pdfDoc.embedPng(iconFlagYellowBytes);
|
|
46
|
+
const iconStability = await pdfDoc.embedPng(iconStabilityBytes);
|
|
47
|
+
const iconThumbprintGreen = await pdfDoc.embedPng(iconThumbprintGreenBytes);
|
|
48
|
+
const iconThumbprintRed = await pdfDoc.embedPng(iconThumbprintRedBytes);
|
|
49
|
+
const mqLogo = await pdfDoc.embedPng(mqLogoBytes);
|
|
50
|
+
|
|
51
|
+
// embed company logo and profile picture
|
|
52
|
+
let profileImage, companyImage;
|
|
53
|
+
let profileImageType = util.checkProfilePicture(payload.meta);
|
|
54
|
+
let companyImageType = util.checkCompanyLogo(payload.meta);
|
|
55
|
+
if (profileImageType) {
|
|
56
|
+
if (profileImageType.shape == "square")
|
|
57
|
+
profileImage = payload.meta.profile_settings.profilePicSquareBase64;
|
|
58
|
+
else if (profileImageType.shape == "circle")
|
|
59
|
+
profileImage = payload.meta.profile_settings.profilePicCircleBase64;
|
|
60
|
+
if (profileImageType.type == "jpg")
|
|
61
|
+
profileImage = await pdfDoc.embedJpg(util._base64ToArrayBuffer(profileImage));
|
|
62
|
+
else if (profileImageType.type == "png")
|
|
63
|
+
profileImage = await pdfDoc.embedPng(util._base64ToArrayBuffer(profileImage));
|
|
64
|
+
}
|
|
65
|
+
if (companyImageType == "jpg")
|
|
66
|
+
companyImage = await pdfDoc.embedJpg(
|
|
67
|
+
util._base64ToArrayBuffer(payload.meta.profile_settings.companyLogoBase64)
|
|
68
|
+
);
|
|
69
|
+
else if (companyImageType == "png")
|
|
70
|
+
companyImage = await pdfDoc.embedPng(
|
|
71
|
+
util._base64ToArrayBuffer(payload.meta.profile_settings.companyLogoBase64)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// get form for filling
|
|
75
|
+
const form = pdfDoc.getForm();
|
|
76
|
+
|
|
77
|
+
// print system diagnostics section
|
|
78
|
+
let t = payload.tests[0].testInfo;
|
|
79
|
+
if (_test) t = _test.testInfo;
|
|
80
|
+
if (
|
|
81
|
+
t &&
|
|
82
|
+
t.diagnostics &&
|
|
83
|
+
t.diagnostics.multi_point_analysis &&
|
|
84
|
+
t.diagnostics.multi_point_analysis.length > 0
|
|
85
|
+
) {
|
|
86
|
+
for (let i = 0; i < 9; i++) {
|
|
87
|
+
let d = t.diagnostics.multi_point_analysis[i];
|
|
88
|
+
if (d) {
|
|
89
|
+
let diagnosticIcon = iconFlagBlack;
|
|
90
|
+
if (d.type === "stability") diagnosticIcon = iconStability;
|
|
91
|
+
else if (d.score > 1 && d.score < 10) diagnosticIcon = iconFlagRed;
|
|
92
|
+
else if (d.score >= 10 && d.score < 15) diagnosticIcon = iconFlagYellow;
|
|
93
|
+
form.getButton(`ImageDiagnostics${i + 1}_af_image`).setImage(diagnosticIcon);
|
|
94
|
+
form.getTextField(`SystemDiagnostics${i + 1}`).setText(`${d.title}`);
|
|
95
|
+
if (diagnosticIcon !== iconFlagYellow)
|
|
96
|
+
form.getTextField(`SystemDiagnostics${i + 1}a`).setText(`${d.score}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
form.getButton("ImageDiagnostics1_af_image").setImage(iconFlagGreen);
|
|
101
|
+
form.getTextField("SystemDiagnostics1").setText("No system-wide issues were detected.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Set header for heat pump heating
|
|
105
|
+
let header = payload.meta.header ? payload.meta.header : "Heat Pump Heating Vitals";
|
|
106
|
+
form.getTextField("Header").setText(header);
|
|
107
|
+
|
|
108
|
+
// Get text fields for heat pump heating
|
|
109
|
+
let textFields = getTextFields(payload, t);
|
|
110
|
+
|
|
111
|
+
// For NCI projects, handle logo placement
|
|
112
|
+
let isNCI = isNCIProject(payload) && base64.nciLogo;
|
|
113
|
+
if (isNCI) {
|
|
114
|
+
const nciLogoImage = await pdfDoc.embedPng(util._base64ToArrayBuffer(base64.nciLogo));
|
|
115
|
+
const pages = pdfDoc.getPages();
|
|
116
|
+
const profilePictureButton = form.getButton("ProfilePicture");
|
|
117
|
+
const widgets = profilePictureButton.acroField.getWidgets();
|
|
118
|
+
for (let i = 0; i < widgets.length; i++) {
|
|
119
|
+
const rect = widgets[i].getRectangle();
|
|
120
|
+
const page = pages[i] || pages[0];
|
|
121
|
+
const logoWidth = rect.width * 1.15;
|
|
122
|
+
const logoHeight = logoWidth * (nciLogoImage.height / nciLogoImage.width);
|
|
123
|
+
const x = rect.x + (rect.width - logoWidth) / 2;
|
|
124
|
+
const y = rect.y + (rect.height - logoHeight) / 2;
|
|
125
|
+
page.drawImage(nciLogoImage, { x, y, width: logoWidth, height: logoHeight });
|
|
126
|
+
}
|
|
127
|
+
if (companyImage) {
|
|
128
|
+
form.getButton("HalfWidthLogo").setImage(companyImage);
|
|
129
|
+
} else {
|
|
130
|
+
form.getButton("HalfWidthLogo").setImage(mqLogo);
|
|
131
|
+
}
|
|
132
|
+
if (payload.meta.profile_settings.techFirstName || payload.meta.profile_settings.techLastName)
|
|
133
|
+
form.getTextField("TechName").setText(
|
|
134
|
+
`${payload.meta.profile_settings.techFirstName} ${payload.meta.profile_settings.techLastName}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// print company logo and profile picture
|
|
139
|
+
if (!isNCI && profileImage && companyImage) {
|
|
140
|
+
form.getButton("ProfilePicture").setImage(profileImage);
|
|
141
|
+
form.getButton("HalfWidthLogo").setImage(companyImage);
|
|
142
|
+
if (payload.meta.profile_settings.techFirstName || payload.meta.profile_settings.techLastName)
|
|
143
|
+
form.getTextField("TechName").setText(
|
|
144
|
+
`${payload.meta.profile_settings.techFirstName} ${payload.meta.profile_settings.techLastName}`
|
|
145
|
+
);
|
|
146
|
+
} else if (!isNCI && profileImage) {
|
|
147
|
+
form.getButton("ProfilePicture").setImage(profileImage);
|
|
148
|
+
form.getButton("HalfWidthLogo").setImage(mqLogo);
|
|
149
|
+
if (payload.meta.profile_settings.techFirstName || payload.meta.profile_settings.techLastName)
|
|
150
|
+
form.getTextField("TechName").setText(
|
|
151
|
+
`${payload.meta.profile_settings.techFirstName} ${payload.meta.profile_settings.techLastName}`
|
|
152
|
+
);
|
|
153
|
+
} else if (!isNCI) {
|
|
154
|
+
if (companyImage) form.getButton("FullWidthLogo").setImage(companyImage);
|
|
155
|
+
else form.getButton("FullWidthLogo").setImage(mqLogo);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// print customer data
|
|
159
|
+
let date = new Date();
|
|
160
|
+
if (textFields.cName && textFields.address && textFields.city && textFields.state && textFields.zip) {
|
|
161
|
+
form.getTextField("Name").setText(`${textFields.cName}`);
|
|
162
|
+
form.getTextField("Address1").setText(`${textFields.address}`);
|
|
163
|
+
if (textFields.address2) {
|
|
164
|
+
form.getTextField("Address2").setText(`${textFields.address2}`);
|
|
165
|
+
form.getTextField("CityStateZip").setText(`${textFields.city}${textFields.state}${textFields.zip}`);
|
|
166
|
+
} else {
|
|
167
|
+
form.getTextField("Address2").setText(`${textFields.city}${textFields.state}${textFields.zip}`);
|
|
168
|
+
}
|
|
169
|
+
form.getTextField("DateOfServiceLabelUpper").setText("");
|
|
170
|
+
form.getTextField("TimeOfServiceLabelUpper").setText("");
|
|
171
|
+
form.getTextField("DateOfServiceLabelLower").setText("Date of Service:");
|
|
172
|
+
form.getTextField("TimeOfServiceLabelLower").setText("Time of Service:");
|
|
173
|
+
form.getTextField("DateOfServiceLower").setText(date.toLocaleDateString("en-US"));
|
|
174
|
+
form.getTextField("TimeOfServiceLower").setText(date.toLocaleTimeString("en-US"));
|
|
175
|
+
} else {
|
|
176
|
+
form.getTextField("DateOfServiceLabelLower").setText("");
|
|
177
|
+
form.getTextField("TimeOfServiceLabelLower").setText("");
|
|
178
|
+
form.getTextField("DateOfServiceLabelUpper").setText("Date of Service:");
|
|
179
|
+
form.getTextField("TimeOfServiceLabelUpper").setText("Time of Service:");
|
|
180
|
+
form.getTextField("DateOfServiceUpper").setText(date.toLocaleDateString("en-US"));
|
|
181
|
+
form.getTextField("TimeOfServiceUpper").setText(date.toLocaleTimeString("en-US"));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// System score
|
|
185
|
+
let systemScorePercentage = t.data.vitals_score ? +t.data.vitals_score.toFixed(0) : 0;
|
|
186
|
+
let systemScoreGrade = util.getGradeFromScore(systemScorePercentage);
|
|
187
|
+
let systemScoreColor = util.getColorFromGrade(systemScoreGrade);
|
|
188
|
+
if (!t.data.vitals_score) {
|
|
189
|
+
systemScorePercentage = "--";
|
|
190
|
+
systemScoreGrade = "";
|
|
191
|
+
} else {
|
|
192
|
+
systemScorePercentage += "%";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Print system score
|
|
196
|
+
form.getTextField(`YourSystemScore${systemScoreColor}`).setText(`${systemScorePercentage} ${systemScoreGrade}`);
|
|
197
|
+
|
|
198
|
+
// Heat pump heating measurements (using existing form fields with different data)
|
|
199
|
+
form.getTextField("Superheat").setText(`${textFields.tempRise} °${payload.units.temperature} Rise`);
|
|
200
|
+
form.getTextField("Subcooling").setText(`${textFields.llt} °${payload.units.temperature} LLT`);
|
|
201
|
+
form.getTextField("CondenserApproach").setText(`${textFields.cop} COP`);
|
|
202
|
+
form.getTextField("TemperatureSplit").setText(`${textFields.oat} °${payload.units.temperature} OAT`);
|
|
203
|
+
form.getTextField("TotalExternalStaticPressure").setText(`${textFields.tesp} inH2O`);
|
|
204
|
+
form.getTextField("FilterFaceVelocity").setText(`${textFields.airflow} CFM`);
|
|
205
|
+
|
|
206
|
+
// Heat pump heating scoring factors (points-based, not losses)
|
|
207
|
+
let ptsFilter = textFields.ptsFilter;
|
|
208
|
+
let ptsA = textFields.ptsA;
|
|
209
|
+
let ptsB = textFields.ptsB;
|
|
210
|
+
let ptsC = textFields.ptsC;
|
|
211
|
+
let ptsD = textFields.ptsD;
|
|
212
|
+
|
|
213
|
+
// Determine colors based on points (green if >= 15, red otherwise)
|
|
214
|
+
let filterColor = ptsFilter !== "--" && +ptsFilter >= 15 ? "Green" : "Red";
|
|
215
|
+
let aColor = ptsA !== "--" && +ptsA >= 15 ? "Green" : "Red";
|
|
216
|
+
let bColor = ptsB !== "--" && +ptsB >= 15 ? "Green" : "Red";
|
|
217
|
+
let cColor = ptsC !== "--" && +ptsC >= 15 ? "Green" : "Red";
|
|
218
|
+
let dColor = ptsD !== "--" && +ptsD >= 15 ? "Green" : "Red";
|
|
219
|
+
|
|
220
|
+
// Set scoring factor values
|
|
221
|
+
form.getTextField(`AgeLosses${filterColor}`).setText(`${ptsFilter}/20`);
|
|
222
|
+
form.getTextField(`TemperatureSplitLosses${aColor}`).setText(`${ptsA}/20`);
|
|
223
|
+
form.getTextField(`StaticPressureLosses${bColor}`).setText(`${ptsB}/20`);
|
|
224
|
+
form.getTextField(`ApproachLosses${cColor}`).setText(`${ptsC}/20`);
|
|
225
|
+
form.getTextField(`RefrigerantChargeIssues${dColor}`).setText(`${ptsD}/20`);
|
|
226
|
+
|
|
227
|
+
form.getTextField("YourSystemScorePage2").setText(`${systemScorePercentage} ${systemScoreGrade}`);
|
|
228
|
+
|
|
229
|
+
// Get pages for label overlays
|
|
230
|
+
const pages = pdfDoc.getPages();
|
|
231
|
+
|
|
232
|
+
// Overlay custom labels for heat pump heating on page 2 (factors page)
|
|
233
|
+
// Note: These positions may need adjustment based on actual PDF layout
|
|
234
|
+
if (pages.length >= 2) {
|
|
235
|
+
const page2 = pages[1];
|
|
236
|
+
const labelFontSize = 9;
|
|
237
|
+
const labelColor = rgb(0.2, 0.2, 0.2);
|
|
238
|
+
|
|
239
|
+
// Draw white rectangles to cover original labels, then draw new labels
|
|
240
|
+
// Factor 1: Static Pressure (was Age & Efficiency)
|
|
241
|
+
page2.drawRectangle({ x: 45, y: 635, width: 150, height: 12, color: rgb(1, 1, 1) });
|
|
242
|
+
page2.drawText("Static Pressure", { x: 47, y: 637, size: labelFontSize, font: helveticaBold, color: labelColor });
|
|
243
|
+
|
|
244
|
+
// Factor 2: Air-Side Heat Delivery (was Temperature Split)
|
|
245
|
+
page2.drawRectangle({ x: 45, y: 580, width: 150, height: 12, color: rgb(1, 1, 1) });
|
|
246
|
+
page2.drawText("Air-Side Heat Delivery", { x: 47, y: 582, size: labelFontSize, font: helveticaBold, color: labelColor });
|
|
247
|
+
|
|
248
|
+
// Factor 3: Capacity Delivery (was Static Pressure)
|
|
249
|
+
page2.drawRectangle({ x: 45, y: 525, width: 150, height: 12, color: rgb(1, 1, 1) });
|
|
250
|
+
page2.drawText("Capacity Delivery", { x: 47, y: 527, size: labelFontSize, font: helveticaBold, color: labelColor });
|
|
251
|
+
|
|
252
|
+
// Factor 4: System Balance (was Approach)
|
|
253
|
+
page2.drawRectangle({ x: 45, y: 470, width: 150, height: 12, color: rgb(1, 1, 1) });
|
|
254
|
+
page2.drawText("System Balance", { x: 47, y: 472, size: labelFontSize, font: helveticaBold, color: labelColor });
|
|
255
|
+
|
|
256
|
+
// Factor 5: Efficiency (COP) (was Refrigerant Charge)
|
|
257
|
+
page2.drawRectangle({ x: 45, y: 415, width: 150, height: 12, color: rgb(1, 1, 1) });
|
|
258
|
+
page2.drawText("Efficiency (COP)", { x: 47, y: 417, size: labelFontSize, font: helveticaBold, color: labelColor });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Print targets and range icons (using appropriate values for heat pump heating)
|
|
262
|
+
const measureLabels = ["Superheat", "Subcooling", "Condenser", "TempSplit", "Tesp", "FilterFace"];
|
|
263
|
+
for (let i = 0; i < measureLabels.length; i++) {
|
|
264
|
+
// For heat pump heating, show green icons for valid measurements
|
|
265
|
+
let icon = iconRangeGreen;
|
|
266
|
+
let iconPlacement = "Mid";
|
|
267
|
+
try {
|
|
268
|
+
form.getButton(`Image${measureLabels[i]}${iconPlacement}_af_image`).setImage(icon);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
// Field may not exist
|
|
271
|
+
}
|
|
272
|
+
// Clear target zone text for heat pump heating (different targets apply)
|
|
273
|
+
try {
|
|
274
|
+
form.getTextField(`${measureLabels[i]}Target`).setText("");
|
|
275
|
+
} catch (e) {
|
|
276
|
+
// Field may not exist
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Skip pass/fail subsystem review for heat pump heating
|
|
281
|
+
form.getTextField("SSR1").setText("Heat Pump Heating Mode");
|
|
282
|
+
|
|
283
|
+
// Add system info page
|
|
284
|
+
if (payload.meta.report_type != "FullReport") {
|
|
285
|
+
let systemInfoPageBase64 = await systemInfoPage.getReport(payload);
|
|
286
|
+
const systemInfoPageDoc = systemInfoPageBase64
|
|
287
|
+
? await PDFDocument.load(util._base64ToArrayBuffer(systemInfoPageBase64.data))
|
|
288
|
+
: false;
|
|
289
|
+
const [_systemInfoPageDoc] = systemInfoPageDoc
|
|
290
|
+
? await pdfDoc.copyPages(systemInfoPageDoc, [0])
|
|
291
|
+
: false;
|
|
292
|
+
if (_systemInfoPageDoc) pdfDoc.insertPage(pdfDoc.getPages().length, _systemInfoPageDoc);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// flatten form fields before adding AI summary pages
|
|
296
|
+
form.flatten();
|
|
297
|
+
|
|
298
|
+
// Add AI summary page if enabled and summary exists
|
|
299
|
+
await aiSummaryPage.addAiSummaryPages(pdfDoc, payload);
|
|
300
|
+
|
|
301
|
+
return { status: 200, data: await pdfDoc.saveAsBase64({ dataUri: true }) };
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error("[VitalsHeatPumpHeatingReport] Error:", error);
|
|
304
|
+
return { status: 400, data: error };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getTextFields(payload, test) {
|
|
309
|
+
// Calculate temperature rise
|
|
310
|
+
let tempRise = "--";
|
|
311
|
+
if (test.data.temperature_leaving_dry_bulb !== undefined && test.data.temperature_entering_dry_bulb !== undefined) {
|
|
312
|
+
tempRise = (test.data.temperature_leaving_dry_bulb - test.data.temperature_entering_dry_bulb).toFixed(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
cName:
|
|
317
|
+
payload.site.customer.first_name && payload.site.customer.last_name
|
|
318
|
+
? `${payload.site.customer.first_name} ${payload.site.customer.last_name}`
|
|
319
|
+
: "",
|
|
320
|
+
address: payload.site.location.address ? payload.site.location.address : "",
|
|
321
|
+
address2: payload.site.location.address2 ? payload.site.location.address2 : "",
|
|
322
|
+
city: payload.site.location.city ? payload.site.location.city : "",
|
|
323
|
+
state: payload.site.location.state ? `, ${payload.site.location.state}` : "",
|
|
324
|
+
zip: payload.site.location.zip ? ` ${payload.site.location.zip}` : "",
|
|
325
|
+
// Heat pump heating specific fields
|
|
326
|
+
tempRise: tempRise,
|
|
327
|
+
llt: test.data.temperature_liquid_line !== undefined ? (+test.data.temperature_liquid_line).toFixed(1) : "--",
|
|
328
|
+
cop: test.data.cop !== undefined ? (+test.data.cop).toFixed(2) : "--",
|
|
329
|
+
oat: test.data.temperature_outdoor_dry_bulb !== undefined ? (+test.data.temperature_outdoor_dry_bulb).toFixed(1) : "--",
|
|
330
|
+
tesp: test.data.pressure_static_total_external !== undefined ? (+test.data.pressure_static_total_external).toFixed(2) : "--",
|
|
331
|
+
airflow: test.data.airflow !== undefined ? (+test.data.airflow).toFixed(0) : "--",
|
|
332
|
+
// Heat pump heating scoring factors (points earned, not losses)
|
|
333
|
+
ptsFilter: test.data.hp_heating_pts_filter !== undefined ? (+test.data.hp_heating_pts_filter).toFixed(0) : "--",
|
|
334
|
+
ptsA: test.data.hp_heating_pts_a !== undefined ? (+test.data.hp_heating_pts_a).toFixed(0) : "--",
|
|
335
|
+
ptsB: test.data.hp_heating_pts_b !== undefined ? (+test.data.hp_heating_pts_b).toFixed(0) : "--",
|
|
336
|
+
ptsC: test.data.hp_heating_pts_c !== undefined ? (+test.data.hp_heating_pts_c).toFixed(0) : "--",
|
|
337
|
+
ptsD: test.data.hp_heating_pts_d !== undefined ? (+test.data.hp_heating_pts_d).toFixed(0) : "--",
|
|
338
|
+
};
|
|
339
|
+
}
|
package/util.js
CHANGED
|
@@ -1006,3 +1006,74 @@ function prettifyData(val, type, units) {
|
|
|
1006
1006
|
// reader.readAsDataURL(file);
|
|
1007
1007
|
// }
|
|
1008
1008
|
// }
|
|
1009
|
+
|
|
1010
|
+
// AI Summary helper functions
|
|
1011
|
+
export function wrapText(text, font, fontSize, maxWidth) {
|
|
1012
|
+
if (!text) return [];
|
|
1013
|
+
const words = text.split(' ');
|
|
1014
|
+
const lines = [];
|
|
1015
|
+
let currentLine = '';
|
|
1016
|
+
|
|
1017
|
+
for (const word of words) {
|
|
1018
|
+
const testLine = currentLine ? currentLine + ' ' + word : word;
|
|
1019
|
+
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
|
|
1020
|
+
|
|
1021
|
+
if (testWidth > maxWidth && currentLine) {
|
|
1022
|
+
lines.push(currentLine);
|
|
1023
|
+
currentLine = word;
|
|
1024
|
+
} else {
|
|
1025
|
+
currentLine = testLine;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (currentLine) {
|
|
1030
|
+
lines.push(currentLine);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return lines;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export function wrapTextWithNewlines(text, font, fontSize, maxWidth) {
|
|
1037
|
+
if (!text) return [];
|
|
1038
|
+
const paragraphs = text.split('\n');
|
|
1039
|
+
const allLines = [];
|
|
1040
|
+
|
|
1041
|
+
for (const paragraph of paragraphs) {
|
|
1042
|
+
if (paragraph.trim() === '') {
|
|
1043
|
+
allLines.push('');
|
|
1044
|
+
} else {
|
|
1045
|
+
const wrappedLines = wrapText(paragraph, font, fontSize, maxWidth);
|
|
1046
|
+
allLines.push(...wrappedLines);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return allLines;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export function shouldShowAiSummary(payload) {
|
|
1054
|
+
// Check if toggle is enabled in pdf_settings
|
|
1055
|
+
if (payload.meta && payload.meta.pdf_settings && payload.meta.pdf_settings.showAiSummaryOnReport === false) {
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
// Check if AI summary exists
|
|
1059
|
+
const aiSummary = getAiSummary(payload);
|
|
1060
|
+
return aiSummary && aiSummary.trim().length > 0 && aiSummary !== 'No AI summary available.';
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
export function getAiSummary(payload) {
|
|
1064
|
+
// Check multiple possible sources for AI summary
|
|
1065
|
+
if (payload.ai_summary) return payload.ai_summary;
|
|
1066
|
+
if (payload.project && payload.project.ai_summary) return payload.project.ai_summary;
|
|
1067
|
+
if (payload.project && payload.project.mq_ai_summary) return payload.project.mq_ai_summary;
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export function getAiNotes(payload) {
|
|
1072
|
+
// Check multiple possible sources for notes
|
|
1073
|
+
if (payload.notes) return payload.notes;
|
|
1074
|
+
if (payload.project && payload.project.notes) return payload.project.notes;
|
|
1075
|
+
if (payload.tests && payload.tests[0] && payload.tests[0].testInfo && payload.tests[0].testInfo.notes) {
|
|
1076
|
+
return payload.tests[0].testInfo.notes;
|
|
1077
|
+
}
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|