@measurequick/measurequick-report-generator 1.5.190 → 1.5.191

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@measurequick/measurequick-report-generator",
3
- "version": "1.5.190",
3
+ "version": "1.5.191",
4
4
  "description": "Generates PDF documents for various measureQuick applications.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,490 +1,10 @@
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
- }
1
+ import * as notesSummaryPage from './notes-summary-page.js';
79
2
 
3
+ /**
4
+ * Generate standalone AI Summary & Notes report using the notes-summary template
5
+ * @param {Object} payload - The report payload
6
+ * @returns {Promise<{status: number, data: string}>}
7
+ */
80
8
  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
- }
9
+ return notesSummaryPage.getReport(payload);
490
10
  }
@@ -3,7 +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
+ import * as notesSummaryPage from "./notes-summary-page.js";
7
7
 
8
8
  // Check if project is CoolMaxx or HeatMaxx (NCI branded)
9
9
  function isNCIProject(payload) {
@@ -425,7 +425,7 @@ export async function getReport(payload, _test) {
425
425
  form.flatten();
426
426
 
427
427
  // Add AI summary page if enabled and summary exists
428
- await aiSummaryPage.addAiSummaryPages(pdfDoc, payload);
428
+ await notesSummaryPage.addNotesSummaryPages(pdfDoc, payload);
429
429
 
430
430
  return { status: 200, data: await pdfDoc.saveAsBase64({ dataUri: true }) };
431
431
  } catch (error) {
@@ -3,7 +3,7 @@ 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
5
  import * as util from "../util.js";
6
- import * as aiSummaryPage from "./ai-summary-page.js";
6
+ import * as notesSummaryPage from "./notes-summary-page.js";
7
7
 
8
8
  // Check if project is CoolMaxx or HeatMaxx (NCI branded)
9
9
  function isNCIProject(payload) {
@@ -632,7 +632,7 @@ export async function getReport(payload, _test) {
632
632
  form.flatten();
633
633
 
634
634
  // Add AI summary page if enabled and summary exists
635
- await aiSummaryPage.addAiSummaryPages(pdfDoc, payload);
635
+ await notesSummaryPage.addNotesSummaryPages(pdfDoc, payload);
636
636
 
637
637
  return { status: 200, data: await pdfDoc.saveAsBase64({ dataUri: true }) };
638
638
  } catch (error) {
@@ -3,7 +3,7 @@ import * as base64 from "../base-64/icons.js";
3
3
  import * as pdf from "../base-64/mq-vitals-heatpump-heating.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
+ import * as notesSummaryPage from "./notes-summary-page.js";
7
7
 
8
8
  // Check if project is CoolMaxx or HeatMaxx (NCI branded)
9
9
  function isNCIProject(payload) {
@@ -267,7 +267,7 @@ export async function getReport(payload, _test) {
267
267
  form.flatten();
268
268
 
269
269
  // Add AI summary page if enabled and summary exists
270
- await aiSummaryPage.addAiSummaryPages(pdfDoc, payload);
270
+ await notesSummaryPage.addNotesSummaryPages(pdfDoc, payload);
271
271
 
272
272
  return { status: 200, data: await pdfDoc.saveAsBase64({ dataUri: true }) };
273
273
  } catch (error) {