@striae-org/striae 4.2.1 → 4.3.0
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/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
- package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
- package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
- package/app/components/sidebar/cases/case-sidebar.tsx +49 -3
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +690 -110
- package/app/components/sidebar/cases/cases.module.css +23 -0
- package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
- package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
- package/app/components/sidebar/files/files-modal.module.css +285 -44
- package/app/components/sidebar/files/files-modal.tsx +452 -145
- package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
- package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
- package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
- package/app/components/sidebar/notes/class-details-shared.ts +239 -0
- package/app/components/sidebar/notes/notes-editor-form.tsx +43 -5
- package/app/components/sidebar/notes/notes.module.css +236 -4
- package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
- package/app/components/sidebar/sidebar-container.tsx +1 -0
- package/app/components/sidebar/sidebar.tsx +12 -1
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/routes/striae/striae.tsx +1 -0
- package/app/types/annotations.ts +48 -1
- package/app/utils/data/case-filters.ts +127 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +18 -2
- package/app/utils/data/file-filters.ts +201 -0
- package/functions/api/image/[[path]].ts +4 -0
- package/package.json +3 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
- package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +20 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { PDFGenerationData, ReportRenderer } from '../report-types';
|
|
1
|
+
import type { PDFGenerationData, ReportPdfOptionsBuilder, ReportRenderer } from '../report-types';
|
|
2
2
|
import { ICON_256 } from '../assets/generated-assets';
|
|
3
|
+
import { buildRepeatedChromePdfOptions, escapeHtml } from '../report-layout';
|
|
3
4
|
|
|
4
5
|
export const renderReport: ReportRenderer = (data: PDFGenerationData): string => {
|
|
5
|
-
const { imageUrl,
|
|
6
|
+
const { imageUrl, annotationData, activeAnnotations } = data;
|
|
6
7
|
const annotationsSet = new Set(activeAnnotations);
|
|
8
|
+
const hasImage = Boolean(imageUrl && imageUrl !== '/clear.jpg');
|
|
9
|
+
const safeText = (value: unknown): string => escapeHtml(String(value ?? ''));
|
|
7
10
|
|
|
8
11
|
// Programmatically determine if a color is dark and needs a light background
|
|
9
12
|
const needsLightBackground = (color: string | undefined): boolean => {
|
|
@@ -71,42 +74,23 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
71
74
|
<style>
|
|
72
75
|
html, body {
|
|
73
76
|
width: 100%;
|
|
74
|
-
height: 100%;
|
|
75
77
|
margin: 0;
|
|
76
78
|
font-family: Arial, sans-serif;
|
|
77
79
|
background-color: white;
|
|
78
|
-
display: flex;
|
|
79
|
-
flex-direction: column;
|
|
80
|
-
min-height: 100vh;
|
|
81
80
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
align-items: center;
|
|
85
|
-
margin-bottom: 15px;
|
|
86
|
-
border-bottom: 2px solid #333;
|
|
87
|
-
padding-bottom: 8px;
|
|
88
|
-
position: relative;
|
|
89
|
-
}
|
|
90
|
-
.header-content {
|
|
91
|
-
flex: 1;
|
|
92
|
-
display: flex;
|
|
93
|
-
align-items: center;
|
|
94
|
-
justify-content: space-between;
|
|
81
|
+
body {
|
|
82
|
+
color: #333;
|
|
95
83
|
}
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
.report-body {
|
|
85
|
+
width: 100%;
|
|
86
|
+
box-sizing: border-box;
|
|
99
87
|
}
|
|
100
|
-
.case-number {
|
|
101
|
-
font-size: 16px;
|
|
102
|
-
font-weight: bold;
|
|
103
|
-
color: #333;
|
|
104
|
-
text-align: right;
|
|
105
|
-
}
|
|
106
88
|
.image-container {
|
|
107
89
|
width: 100%;
|
|
108
|
-
margin:
|
|
90
|
+
margin: 0 0 10px;
|
|
109
91
|
position: relative;
|
|
92
|
+
page-break-inside: avoid;
|
|
93
|
+
break-inside: avoid;
|
|
110
94
|
}
|
|
111
95
|
.image-wrapper {
|
|
112
96
|
display: flex;
|
|
@@ -170,6 +154,8 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
170
154
|
width: 100%;
|
|
171
155
|
margin-top: 12px;
|
|
172
156
|
min-height: 50px;
|
|
157
|
+
page-break-inside: avoid;
|
|
158
|
+
break-inside: avoid;
|
|
173
159
|
}
|
|
174
160
|
.support-level-annotation {
|
|
175
161
|
flex: 1;
|
|
@@ -235,10 +221,21 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
235
221
|
}
|
|
236
222
|
.confirmation-section {
|
|
237
223
|
margin-top: 20px;
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: 16px;
|
|
227
|
+
}
|
|
228
|
+
.notes-page {
|
|
229
|
+
page-break-before: always;
|
|
230
|
+
break-before: page;
|
|
231
|
+
}
|
|
232
|
+
.confirmation-summary {
|
|
238
233
|
display: flex;
|
|
239
234
|
justify-content: flex-start;
|
|
240
235
|
align-items: flex-start;
|
|
241
236
|
gap: 20px;
|
|
237
|
+
page-break-inside: avoid;
|
|
238
|
+
break-inside: avoid;
|
|
242
239
|
}
|
|
243
240
|
.confirmation-box {
|
|
244
241
|
background: #ffffff;
|
|
@@ -247,6 +244,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
247
244
|
padding: 15px;
|
|
248
245
|
width: 280px;
|
|
249
246
|
font-family: 'Inter', Arial, sans-serif;
|
|
247
|
+
box-sizing: border-box;
|
|
250
248
|
}
|
|
251
249
|
.confirmation-label {
|
|
252
250
|
font-size: 14px;
|
|
@@ -274,6 +272,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
274
272
|
padding: 15px;
|
|
275
273
|
width: 280px;
|
|
276
274
|
font-family: 'Inter', Arial, sans-serif;
|
|
275
|
+
box-sizing: border-box;
|
|
277
276
|
}
|
|
278
277
|
.confirmation-title {
|
|
279
278
|
font-size: 14px;
|
|
@@ -316,64 +315,32 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
316
315
|
letter-spacing: 1px;
|
|
317
316
|
}
|
|
318
317
|
.additional-notes-section {
|
|
319
|
-
|
|
318
|
+
border: 1px solid #d7dbe0;
|
|
319
|
+
border-radius: 8px;
|
|
320
|
+
background: #fafbfc;
|
|
321
|
+
padding: 16px 18px;
|
|
322
|
+
box-sizing: border-box;
|
|
320
323
|
font-family: 'Inter', Arial, sans-serif;
|
|
321
|
-
font-size: 14px;
|
|
322
|
-
line-height: 1.6;
|
|
323
324
|
color: #333;
|
|
324
|
-
white-space: pre-wrap;
|
|
325
|
-
word-wrap: break-word;
|
|
326
|
-
text-indent: 0 !important;
|
|
327
|
-
padding: 0;
|
|
328
|
-
margin: 0;
|
|
329
|
-
margin-left: 20px;
|
|
330
|
-
flex-shrink: 0;
|
|
331
|
-
text-align: left;
|
|
332
|
-
display: block;
|
|
333
325
|
}
|
|
334
|
-
.
|
|
335
|
-
margin
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
align-items: center;
|
|
341
|
-
font-family: 'Inter', Arial, sans-serif;
|
|
342
|
-
font-size: 11px;
|
|
326
|
+
.additional-notes-title {
|
|
327
|
+
margin: 0 0 10px;
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
font-weight: 700;
|
|
330
|
+
letter-spacing: 0.08em;
|
|
331
|
+
text-transform: uppercase;
|
|
343
332
|
color: #666;
|
|
344
333
|
}
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
font-weight: 500;
|
|
356
|
-
flex: 1;
|
|
357
|
-
text-align: left;
|
|
358
|
-
display: flex;
|
|
359
|
-
align-items: center;
|
|
360
|
-
gap: 6px;
|
|
361
|
-
}
|
|
362
|
-
.footer-brand-icon {
|
|
363
|
-
width: 14px;
|
|
364
|
-
height: 14px;
|
|
365
|
-
object-fit: contain;
|
|
366
|
-
}
|
|
367
|
-
.footer-center {
|
|
368
|
-
font-weight: 600;
|
|
369
|
-
flex: 1;
|
|
370
|
-
text-align: center;
|
|
371
|
-
color: #333;
|
|
372
|
-
}
|
|
373
|
-
.footer-right {
|
|
374
|
-
font-style: italic;
|
|
375
|
-
flex: 1;
|
|
376
|
-
text-align: right;
|
|
334
|
+
.additional-notes-body {
|
|
335
|
+
margin: 0;
|
|
336
|
+
font-size: 14px;
|
|
337
|
+
line-height: 1.6;
|
|
338
|
+
white-space: pre-wrap;
|
|
339
|
+
overflow-wrap: anywhere;
|
|
340
|
+
word-break: break-word;
|
|
341
|
+
text-indent: 0 !important;
|
|
342
|
+
orphans: 3;
|
|
343
|
+
widows: 3;
|
|
377
344
|
}
|
|
378
345
|
.index-section {
|
|
379
346
|
text-align: center;
|
|
@@ -395,19 +362,12 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
395
362
|
</style>
|
|
396
363
|
</head>
|
|
397
364
|
<body>
|
|
398
|
-
<div class="
|
|
399
|
-
<div class="content-wrapper">
|
|
400
|
-
<div class="header">
|
|
401
|
-
<div class="header-content">
|
|
402
|
-
<div class="date">${currentDate}</div>
|
|
403
|
-
${caseNumber ? `<div class="case-number">${caseNumber}</div>` : '<div class="case-number"></div>'}
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
365
|
+
<div class="report-body">
|
|
406
366
|
|
|
407
|
-
${
|
|
367
|
+
${hasImage ? `
|
|
408
368
|
${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'number' && annotationData.indexNumber ? `
|
|
409
369
|
<div class="index-section">
|
|
410
|
-
Index: ${annotationData.indexNumber}
|
|
370
|
+
Index: ${safeText(annotationData.indexNumber)}
|
|
411
371
|
</div>
|
|
412
372
|
` : ''}
|
|
413
373
|
|
|
@@ -419,12 +379,12 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
419
379
|
<div class="annotations-overlay">
|
|
420
380
|
<div class="left-annotation" style="${needsLightBackground(annotationData.caseFontColor || '#FFDE21') ? 'background: rgba(255, 255, 255, 0.9); border: 2px solid rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : ''}">
|
|
421
381
|
<div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
|
|
422
|
-
${annotationData.leftCase}${annotationData.leftItem ? ` ${annotationData.leftItem}` : ''}
|
|
382
|
+
${safeText(annotationData.leftCase)}${annotationData.leftItem ? ` ${safeText(annotationData.leftItem)}` : ''}
|
|
423
383
|
</div>
|
|
424
384
|
</div>
|
|
425
385
|
<div class="right-annotation" style="${needsLightBackground(annotationData.caseFontColor || '#FFDE21') ? 'background: rgba(255, 255, 255, 0.9); border: 2px solid rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : ''}">
|
|
426
386
|
<div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
|
|
427
|
-
${annotationData.rightCase}${annotationData.rightItem ? ` ${annotationData.rightItem}` : ''}
|
|
387
|
+
${safeText(annotationData.rightCase)}${annotationData.rightItem ? ` ${safeText(annotationData.rightItem)}` : ''}
|
|
428
388
|
</div>
|
|
429
389
|
</div>
|
|
430
390
|
</div>
|
|
@@ -450,7 +410,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
450
410
|
${annotationData && annotationsSet?.has('id') ? `
|
|
451
411
|
<div class="support-level-annotation">
|
|
452
412
|
<div class="support-level-text" style="color: ${annotationData.supportLevel === 'ID' ? '#28a745' : annotationData.supportLevel === 'Exclusion' ? '#dc3545' : '#ffc107'}; background: ${annotationData.supportLevel === 'Inconclusive' ? 'rgba(120, 120, 120, 0.95)' : 'rgba(240, 240, 240, 0.95)'};">
|
|
453
|
-
${annotationData.supportLevel === 'ID' ? 'Identification' : annotationData.supportLevel}
|
|
413
|
+
${safeText(annotationData.supportLevel === 'ID' ? 'Identification' : annotationData.supportLevel)}
|
|
454
414
|
</div>
|
|
455
415
|
</div>
|
|
456
416
|
` : '<div class="support-level-annotation"></div>'}
|
|
@@ -458,7 +418,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
458
418
|
${annotationData && annotationsSet?.has('class') ? `
|
|
459
419
|
<div class="class-annotation">
|
|
460
420
|
<div class="class-text-annotation">
|
|
461
|
-
${annotationData.customClass || annotationData.classType}${annotationData.classNote ? ` (${annotationData.classNote})` : ''}
|
|
421
|
+
${safeText(annotationData.customClass || annotationData.classType)}${annotationData.classNote ? ` (${safeText(annotationData.classNote)})` : ''}
|
|
462
422
|
</div>
|
|
463
423
|
</div>
|
|
464
424
|
` : '<div class="class-annotation"></div>'}
|
|
@@ -477,20 +437,21 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
477
437
|
${annotationData && ((annotationData.includeConfirmation === true) || annotationData.additionalNotes) ? `
|
|
478
438
|
<div class="confirmation-section">
|
|
479
439
|
${annotationData && (annotationData.includeConfirmation === true) ? `
|
|
440
|
+
<div class="confirmation-summary">
|
|
480
441
|
${annotationData.confirmationData ? `
|
|
481
442
|
<div class="confirmation-data">
|
|
482
443
|
<div class="confirmation-title">IDENTIFICATION CONFIRMED</div>
|
|
483
444
|
<div class="confirmation-field">
|
|
484
|
-
<div class="confirmation-name">${annotationData.confirmationData.fullName}, ${annotationData.confirmationData.badgeId}</div>
|
|
445
|
+
<div class="confirmation-name">${safeText(annotationData.confirmationData.fullName)}, ${safeText(annotationData.confirmationData.badgeId)}</div>
|
|
485
446
|
</div>
|
|
486
447
|
<div class="confirmation-field">
|
|
487
|
-
<div class="confirmation-company">${annotationData.confirmationData.confirmedByCompany || 'N/A'}</div>
|
|
448
|
+
<div class="confirmation-company">${safeText(annotationData.confirmationData.confirmedByCompany || 'N/A')}</div>
|
|
488
449
|
</div>
|
|
489
450
|
<div class="confirmation-field">
|
|
490
|
-
<div class="confirmation-timestamp">${annotationData.confirmationData.timestamp}</div>
|
|
451
|
+
<div class="confirmation-timestamp">${safeText(annotationData.confirmationData.timestamp)}</div>
|
|
491
452
|
</div>
|
|
492
453
|
<div class="confirmation-field">
|
|
493
|
-
<div class="confirmation-id">ID: ${annotationData.confirmationData.confirmationId}</div>
|
|
454
|
+
<div class="confirmation-id">ID: ${safeText(annotationData.confirmationData.confirmationId)}</div>
|
|
494
455
|
</div>
|
|
495
456
|
</div>
|
|
496
457
|
` : `
|
|
@@ -501,30 +462,35 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
501
462
|
<div class="confirmation-line"></div>
|
|
502
463
|
</div>
|
|
503
464
|
`}
|
|
504
|
-
|
|
465
|
+
</div>
|
|
466
|
+
` : ''}
|
|
505
467
|
|
|
506
468
|
${annotationData && annotationsSet?.has('notes') && annotationData.additionalNotes && annotationData.additionalNotes.trim() ? `
|
|
507
|
-
<
|
|
508
|
-
|
|
469
|
+
<section class="additional-notes-section ${hasImage || annotationData.includeConfirmation === true ? 'notes-page' : ''}">
|
|
470
|
+
<h2 class="additional-notes-title">Additional Notes</h2>
|
|
471
|
+
<p class="additional-notes-body">${escapeHtml(annotationData.additionalNotes.trim())}</p>
|
|
472
|
+
</section>
|
|
473
|
+
` : ''}
|
|
509
474
|
</div>
|
|
510
475
|
` : ''}
|
|
511
476
|
|
|
512
477
|
</div>
|
|
513
|
-
</div>
|
|
514
|
-
|
|
515
|
-
<div class="footer">
|
|
516
|
-
<div class="footer-left">
|
|
517
|
-
<span>Notes formatted by Striae</span>
|
|
518
|
-
<img class="footer-brand-icon" src="${ICON_256}" alt="Striae icon" />
|
|
519
|
-
</div>
|
|
520
|
-
<div class="footer-center">
|
|
521
|
-
${userCompany ? userCompany : ''}
|
|
522
|
-
</div>
|
|
523
|
-
<div class="footer-right">
|
|
524
|
-
${notesUpdatedFormatted ? `Notes updated ${notesUpdatedFormatted}` : ''}
|
|
525
|
-
</div>
|
|
526
|
-
</div>
|
|
527
478
|
</body>
|
|
528
479
|
</html>
|
|
529
480
|
`;
|
|
530
|
-
};
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export const getPdfOptions: ReportPdfOptionsBuilder = (data: PDFGenerationData) => buildRepeatedChromePdfOptions({
|
|
484
|
+
headerLeft: data.currentDate,
|
|
485
|
+
headerRight: data.caseNumber,
|
|
486
|
+
headerDetailLeft: [data.annotationData?.leftCase, data.annotationData?.leftItem].filter(Boolean).join(' / ')
|
|
487
|
+
? `Left Case / Item: ${[data.annotationData?.leftCase, data.annotationData?.leftItem].filter(Boolean).join(' / ')}`
|
|
488
|
+
: undefined,
|
|
489
|
+
headerDetailRight: [data.annotationData?.rightCase, data.annotationData?.rightItem].filter(Boolean).join(' / ')
|
|
490
|
+
? `Right Case / Item: ${[data.annotationData?.rightCase, data.annotationData?.rightItem].filter(Boolean).join(' / ')}`
|
|
491
|
+
: undefined,
|
|
492
|
+
footerLeft: 'Notes formatted by Striae',
|
|
493
|
+
footerCenter: data.userCompany,
|
|
494
|
+
footerRight: data.notesUpdatedFormatted ? `Notes updated ${data.notesUpdatedFormatted}` : undefined,
|
|
495
|
+
footerLeftImageSrc: ICON_256,
|
|
496
|
+
});
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { PDFGenerationData, PDFGenerationRequest, ReportModule } from './report-types';
|
|
1
|
+
import type { PDFGenerationData, PDFGenerationRequest, ReportModule, ReportPdfOptions } from './report-types';
|
|
2
2
|
|
|
3
3
|
interface Env {
|
|
4
4
|
BROWSER: Fetcher;
|
|
5
5
|
PDF_WORKER_AUTH: string;
|
|
6
|
-
ACCOUNT_ID
|
|
7
|
-
BROWSER_API_TOKEN
|
|
6
|
+
ACCOUNT_ID?: string;
|
|
7
|
+
BROWSER_API_TOKEN?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const BROWSER_PDF_TIMEOUT_MS = 90_000;
|
|
11
11
|
const BROWSER_RENDERING_API_BASE = 'https://api.cloudflare.com/client/v4/accounts';
|
|
12
12
|
|
|
13
|
-
const DEFAULT_PDF_OPTIONS = {
|
|
13
|
+
const DEFAULT_PDF_OPTIONS: ReportPdfOptions = {
|
|
14
14
|
printBackground: true,
|
|
15
15
|
format: 'letter',
|
|
16
16
|
margin: {
|
|
@@ -21,6 +21,17 @@ const DEFAULT_PDF_OPTIONS = {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
function resolvePdfOptions(overrides?: Partial<ReportPdfOptions>): ReportPdfOptions {
|
|
25
|
+
return {
|
|
26
|
+
...DEFAULT_PDF_OPTIONS,
|
|
27
|
+
...overrides,
|
|
28
|
+
margin: {
|
|
29
|
+
...DEFAULT_PDF_OPTIONS.margin,
|
|
30
|
+
...overrides?.margin,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
|
|
25
36
|
// Default Striae report format module
|
|
26
37
|
striae: () => import('./formats/format-striae'),
|
|
@@ -63,7 +74,11 @@ class MissingSecretError extends Error {
|
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
function getRequiredSecret(value: string, name: string): string {
|
|
77
|
+
function getRequiredSecret(value: string | undefined, name: string): string {
|
|
78
|
+
if (typeof value !== 'string') {
|
|
79
|
+
throw new MissingSecretError(name);
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
const normalized = value.trim();
|
|
68
83
|
if (!normalized) {
|
|
69
84
|
throw new MissingSecretError(name);
|
|
@@ -97,7 +112,7 @@ function resolveReportRequest(payload: unknown): PDFGenerationRequest {
|
|
|
97
112
|
};
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<string> {
|
|
115
|
+
async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<{ html: string; pdfOptions: ReportPdfOptions }> {
|
|
101
116
|
const loader = reportModuleLoaders[reportFormat];
|
|
102
117
|
|
|
103
118
|
if (!loader) {
|
|
@@ -106,17 +121,20 @@ async function renderReport(reportFormat: string, data: PDFGenerationData): Prom
|
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
const reportModule = await loader();
|
|
109
|
-
return
|
|
124
|
+
return {
|
|
125
|
+
html: reportModule.renderReport(data),
|
|
126
|
+
pdfOptions: resolvePdfOptions(reportModule.getPdfOptions?.(data)),
|
|
127
|
+
};
|
|
110
128
|
}
|
|
111
129
|
|
|
112
|
-
async function renderPdfViaRestEndpoint(env: Env, html: string): Promise<Response> {
|
|
130
|
+
async function renderPdfViaRestEndpoint(env: Env, html: string, pdfOptions: ReportPdfOptions): Promise<Response> {
|
|
113
131
|
const accountId = getRequiredSecret(env.ACCOUNT_ID, 'ACCOUNT_ID');
|
|
114
132
|
const browserApiToken = getRequiredSecret(env.BROWSER_API_TOKEN, 'BROWSER_API_TOKEN');
|
|
115
133
|
|
|
116
134
|
const endpoint = `${BROWSER_RENDERING_API_BASE}/${accountId}/browser-rendering/pdf`;
|
|
117
135
|
const requestBody = JSON.stringify({
|
|
118
136
|
html,
|
|
119
|
-
pdfOptions
|
|
137
|
+
pdfOptions,
|
|
120
138
|
});
|
|
121
139
|
|
|
122
140
|
let endpointResponse: Response;
|
|
@@ -192,7 +210,7 @@ export default {
|
|
|
192
210
|
const { reportFormat, data } = resolveReportRequest(payload);
|
|
193
211
|
const document = await renderReport(reportFormat, data);
|
|
194
212
|
|
|
195
|
-
return await renderPdfViaRestEndpoint(env, document);
|
|
213
|
+
return await renderPdfViaRestEndpoint(env, document.html, document.pdfOptions);
|
|
196
214
|
} catch (error) {
|
|
197
215
|
if (error instanceof MissingSecretError) {
|
|
198
216
|
console.error(`[pdf-worker] Configuration error: ${error.message}`);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ReportPdfOptions } from './report-types';
|
|
2
|
+
|
|
3
|
+
interface ReportChromeTemplateConfig {
|
|
4
|
+
headerLeft?: string;
|
|
5
|
+
headerCenter?: string;
|
|
6
|
+
headerRight?: string;
|
|
7
|
+
headerDetailLeft?: string;
|
|
8
|
+
headerDetailRight?: string;
|
|
9
|
+
footerLeft?: string;
|
|
10
|
+
footerCenter?: string;
|
|
11
|
+
footerRight?: string;
|
|
12
|
+
footerLeftImageSrc?: string;
|
|
13
|
+
includePageNumbers?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const HEADER_TEMPLATE_STYLES = `
|
|
17
|
+
<style>
|
|
18
|
+
.report-header {
|
|
19
|
+
width: 100%;
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
padding: 0 0.5in 8px;
|
|
22
|
+
border-bottom: 2px solid #333333;
|
|
23
|
+
color: #333333;
|
|
24
|
+
font-family: Arial, sans-serif;
|
|
25
|
+
font-size: 18px;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
}
|
|
28
|
+
.report-header__content {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
width: 100%;
|
|
34
|
+
}
|
|
35
|
+
.report-header__details {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: flex-start;
|
|
38
|
+
justify-content: space-between;
|
|
39
|
+
gap: 12px;
|
|
40
|
+
width: 100%;
|
|
41
|
+
margin-top: 8px;
|
|
42
|
+
padding-top: 8px;
|
|
43
|
+
border-top: 1px solid #d9d9d9;
|
|
44
|
+
font-size: 10px;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
letter-spacing: 0.04em;
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
color: #666666;
|
|
49
|
+
}
|
|
50
|
+
.report-header__cell {
|
|
51
|
+
flex: 1 1 0;
|
|
52
|
+
min-width: 0;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
text-overflow: ellipsis;
|
|
56
|
+
}
|
|
57
|
+
.report-header__cell--left {
|
|
58
|
+
text-align: left;
|
|
59
|
+
}
|
|
60
|
+
.report-header__cell--center {
|
|
61
|
+
text-align: center;
|
|
62
|
+
}
|
|
63
|
+
.report-header__cell--right {
|
|
64
|
+
text-align: right;
|
|
65
|
+
}
|
|
66
|
+
.report-header__detail {
|
|
67
|
+
flex: 1 1 0;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
text-overflow: ellipsis;
|
|
72
|
+
}
|
|
73
|
+
.report-header__detail--left {
|
|
74
|
+
text-align: left;
|
|
75
|
+
}
|
|
76
|
+
.report-header__detail--right {
|
|
77
|
+
text-align: right;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
const FOOTER_TEMPLATE_STYLES = `
|
|
83
|
+
<style>
|
|
84
|
+
.report-footer {
|
|
85
|
+
width: 100%;
|
|
86
|
+
box-sizing: border-box;
|
|
87
|
+
padding: 8px 0.5in 0;
|
|
88
|
+
border-top: 1px solid #cccccc;
|
|
89
|
+
color: #666666;
|
|
90
|
+
font-family: Arial, sans-serif;
|
|
91
|
+
font-size: 9px;
|
|
92
|
+
}
|
|
93
|
+
.report-footer__content {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: space-between;
|
|
97
|
+
gap: 12px;
|
|
98
|
+
width: 100%;
|
|
99
|
+
}
|
|
100
|
+
.report-footer__cell {
|
|
101
|
+
flex: 1 1 0;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
text-overflow: ellipsis;
|
|
106
|
+
}
|
|
107
|
+
.report-footer__cell--left {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 6px;
|
|
111
|
+
text-align: left;
|
|
112
|
+
font-weight: 500;
|
|
113
|
+
}
|
|
114
|
+
.report-footer__cell--center {
|
|
115
|
+
text-align: center;
|
|
116
|
+
color: #333333;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
}
|
|
119
|
+
.report-footer__cell--right {
|
|
120
|
+
text-align: right;
|
|
121
|
+
font-style: italic;
|
|
122
|
+
}
|
|
123
|
+
.report-footer__page-count {
|
|
124
|
+
font-style: normal;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
color: #333333;
|
|
127
|
+
}
|
|
128
|
+
.report-footer__separator {
|
|
129
|
+
margin: 0 6px;
|
|
130
|
+
color: #999999;
|
|
131
|
+
font-style: normal;
|
|
132
|
+
}
|
|
133
|
+
.report-footer__icon {
|
|
134
|
+
width: 12px;
|
|
135
|
+
height: 12px;
|
|
136
|
+
object-fit: contain;
|
|
137
|
+
flex: 0 0 auto;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
export function escapeHtml(value: string | undefined): string {
|
|
143
|
+
if (!value) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return value
|
|
148
|
+
.replace(/&/g, '&')
|
|
149
|
+
.replace(/</g, '<')
|
|
150
|
+
.replace(/>/g, '>')
|
|
151
|
+
.replace(/"/g, '"')
|
|
152
|
+
.replace(/'/g, ''');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderTemplateCell(value: string | undefined, className: string): string {
|
|
156
|
+
const content = value && value.trim().length > 0 ? escapeHtml(value.trim()) : ' ';
|
|
157
|
+
return `<div class="${className}">${content}</div>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function buildRepeatedChromePdfOptions(config: ReportChromeTemplateConfig): Partial<ReportPdfOptions> {
|
|
161
|
+
const hasHeaderDetails = Boolean(
|
|
162
|
+
(config.headerDetailLeft && config.headerDetailLeft.trim().length > 0) ||
|
|
163
|
+
(config.headerDetailRight && config.headerDetailRight.trim().length > 0)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const headerDetails = hasHeaderDetails
|
|
167
|
+
? `
|
|
168
|
+
<div class="report-header__details">
|
|
169
|
+
${renderTemplateCell(config.headerDetailLeft, 'report-header__detail report-header__detail--left')}
|
|
170
|
+
${renderTemplateCell(config.headerDetailRight, 'report-header__detail report-header__detail--right')}
|
|
171
|
+
</div>
|
|
172
|
+
`
|
|
173
|
+
: '';
|
|
174
|
+
|
|
175
|
+
const headerTemplate = `
|
|
176
|
+
${HEADER_TEMPLATE_STYLES}
|
|
177
|
+
<div class="report-header">
|
|
178
|
+
<div class="report-header__content">
|
|
179
|
+
${renderTemplateCell(config.headerLeft, 'report-header__cell report-header__cell--left')}
|
|
180
|
+
${renderTemplateCell(config.headerCenter, 'report-header__cell report-header__cell--center')}
|
|
181
|
+
${renderTemplateCell(config.headerRight, 'report-header__cell report-header__cell--right')}
|
|
182
|
+
</div>
|
|
183
|
+
${headerDetails}
|
|
184
|
+
</div>
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
const footerLeftContent = config.footerLeft && config.footerLeft.trim().length > 0
|
|
188
|
+
? `<span>${escapeHtml(config.footerLeft.trim())}</span>`
|
|
189
|
+
: '<span> </span>';
|
|
190
|
+
|
|
191
|
+
const footerIcon = config.footerLeftImageSrc
|
|
192
|
+
? `<img class="report-footer__icon" src="${escapeHtml(config.footerLeftImageSrc)}" alt="" />`
|
|
193
|
+
: '';
|
|
194
|
+
|
|
195
|
+
const footerRightText = config.footerRight && config.footerRight.trim().length > 0
|
|
196
|
+
? `<span>${escapeHtml(config.footerRight.trim())}</span>`
|
|
197
|
+
: '';
|
|
198
|
+
|
|
199
|
+
const footerPageCount = config.includePageNumbers === false
|
|
200
|
+
? ''
|
|
201
|
+
: `<span class="report-footer__page-count">Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>`;
|
|
202
|
+
|
|
203
|
+
const footerRightContent = footerRightText && footerPageCount
|
|
204
|
+
? `${footerRightText}<span class="report-footer__separator">|</span>${footerPageCount}`
|
|
205
|
+
: footerRightText || footerPageCount || ' ';
|
|
206
|
+
|
|
207
|
+
const footerTemplate = `
|
|
208
|
+
${FOOTER_TEMPLATE_STYLES}
|
|
209
|
+
<div class="report-footer">
|
|
210
|
+
<div class="report-footer__content">
|
|
211
|
+
<div class="report-footer__cell report-footer__cell--left">${footerLeftContent}${footerIcon}</div>
|
|
212
|
+
${renderTemplateCell(config.footerCenter, 'report-footer__cell report-footer__cell--center')}
|
|
213
|
+
<div class="report-footer__cell report-footer__cell--right">${footerRightContent}</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
displayHeaderFooter: true,
|
|
220
|
+
headerTemplate,
|
|
221
|
+
footerTemplate,
|
|
222
|
+
margin: {
|
|
223
|
+
top: hasHeaderDetails ? '1.45in' : '1.15in',
|
|
224
|
+
bottom: '0.8in',
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -65,8 +65,28 @@ export interface PDFGenerationRequest {
|
|
|
65
65
|
data: PDFGenerationData;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export interface PDFMarginOptions {
|
|
69
|
+
top: string;
|
|
70
|
+
bottom: string;
|
|
71
|
+
left: string;
|
|
72
|
+
right: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ReportPdfOptions {
|
|
76
|
+
printBackground?: boolean;
|
|
77
|
+
format?: string;
|
|
78
|
+
margin?: Partial<PDFMarginOptions>;
|
|
79
|
+
displayHeaderFooter?: boolean;
|
|
80
|
+
headerTemplate?: string;
|
|
81
|
+
footerTemplate?: string;
|
|
82
|
+
preferCSSPageSize?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
export type ReportRenderer = (data: PDFGenerationData) => string;
|
|
69
86
|
|
|
87
|
+
export type ReportPdfOptionsBuilder = (data: PDFGenerationData) => Partial<ReportPdfOptions>;
|
|
88
|
+
|
|
70
89
|
export interface ReportModule {
|
|
71
90
|
renderReport: ReportRenderer;
|
|
91
|
+
getPdfOptions?: ReportPdfOptionsBuilder;
|
|
72
92
|
}
|