@striae-org/striae 3.2.1 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/app/components/actions/case-export/core-export.ts +2 -2
  2. package/app/components/actions/case-export/data-processing.ts +19 -4
  3. package/app/components/actions/case-export/download-handlers.ts +6 -5
  4. package/app/components/actions/case-export/metadata-helpers.ts +1 -1
  5. package/app/components/actions/case-import/annotation-import.ts +2 -2
  6. package/app/components/actions/case-import/confirmation-import.ts +3 -3
  7. package/app/components/actions/case-import/image-operations.ts +1 -1
  8. package/app/components/actions/case-import/orchestrator.ts +4 -4
  9. package/app/components/actions/case-import/storage-operations.ts +7 -7
  10. package/app/components/actions/case-import/validation.ts +3 -3
  11. package/app/components/actions/case-import/zip-processing.ts +3 -3
  12. package/app/components/actions/case-manage.ts +3 -3
  13. package/app/components/actions/confirm-export.ts +3 -3
  14. package/app/components/actions/generate-pdf.ts +3 -3
  15. package/app/components/actions/image-manage.ts +3 -3
  16. package/app/components/actions/notes-manage.ts +3 -3
  17. package/app/components/actions/signout.tsx +1 -1
  18. package/app/components/audit/user-audit-viewer.tsx +2 -3
  19. package/app/components/auth/auth-provider.tsx +2 -2
  20. package/app/components/auth/mfa-enrollment.tsx +3 -3
  21. package/app/components/auth/mfa-verification.tsx +4 -4
  22. package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
  23. package/app/components/canvas/canvas.tsx +1 -1
  24. package/app/components/canvas/confirmation/confirmation.tsx +1 -1
  25. package/app/components/sidebar/case-import/case-import.tsx +2 -2
  26. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
  27. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
  28. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +3 -3
  29. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
  30. package/app/components/sidebar/cases/case-sidebar.tsx +5 -4
  31. package/app/components/sidebar/cases/cases-modal.tsx +1 -1
  32. package/app/components/sidebar/files/files-modal.tsx +3 -2
  33. package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
  34. package/app/components/sidebar/sidebar-container.tsx +4 -3
  35. package/app/components/sidebar/sidebar.tsx +2 -2
  36. package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
  37. package/app/components/theme-provider/theme-provider.tsx +1 -1
  38. package/app/components/user/delete-account.tsx +1 -1
  39. package/app/components/user/manage-profile.tsx +2 -2
  40. package/app/components/user/mfa-phone-update.tsx +2 -2
  41. package/app/contexts/auth.context.ts +1 -1
  42. package/app/routes/auth/emailActionHandler.tsx +2 -2
  43. package/app/routes/auth/emailVerification.tsx +2 -2
  44. package/app/routes/auth/login.tsx +5 -5
  45. package/app/routes/auth/passwordReset.tsx +2 -2
  46. package/app/routes/striae/striae.tsx +2 -2
  47. package/app/services/audit/audit-console-logger.ts +46 -0
  48. package/app/services/audit/audit-export-csv.ts +126 -0
  49. package/app/services/audit/audit-export-report.ts +174 -0
  50. package/app/services/audit/audit-export-signing.ts +85 -0
  51. package/app/services/audit/audit-export.service.ts +334 -0
  52. package/app/services/audit/audit-file-type.ts +13 -0
  53. package/app/services/audit/audit-query-helpers.ts +88 -0
  54. package/app/services/audit/audit-worker-client.ts +95 -0
  55. package/app/services/audit/audit.service.ts +990 -0
  56. package/app/services/audit/builders/audit-entry-builder.ts +32 -0
  57. package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
  58. package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
  59. package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
  60. package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
  61. package/app/services/audit/builders/index.ts +40 -0
  62. package/app/services/audit/index.ts +2 -0
  63. package/app/types/case.ts +2 -2
  64. package/app/types/exceljs-bare.d.ts +3 -1
  65. package/app/types/user.ts +1 -1
  66. package/app/utils/audit-export-signature.ts +2 -2
  67. package/app/utils/confirmation-signature.ts +3 -3
  68. package/app/utils/data-operations.ts +5 -5
  69. package/app/utils/mfa-phone.ts +1 -1
  70. package/app/utils/mfa.ts +1 -1
  71. package/app/utils/permissions.ts +2 -2
  72. package/package.json +7 -8
  73. package/worker-configuration.d.ts +4435 -562
  74. package/workers/data-worker/src/data-worker.example.ts +3 -3
  75. package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
  76. package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
  77. package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
  78. package/app/services/audit-export.service.ts +0 -755
  79. package/app/services/audit.service.ts +0 -1474
  80. /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
  81. /package/app/services/{firebase.ts → firebase/index.ts} +0 -0
@@ -1,535 +1,535 @@
1
- import type { PDFGenerationData, ReportRenderer } from './report-types';
2
- import { ICON_256 } from './generated-assets';
3
-
4
- export const renderReport: ReportRenderer = (data: PDFGenerationData): string => {
5
- const { imageUrl, caseNumber, annotationData, activeAnnotations, currentDate, notesUpdatedFormatted, userCompany } = data;
6
- const annotationsSet = new Set(activeAnnotations);
7
-
8
- // Programmatically determine if a color is dark and needs a light background
9
- const needsLightBackground = (color: string | undefined): boolean => {
10
- if (!color) return false;
11
-
12
- // Handle named colors
13
- const namedColors: Record<string, string> = {
14
- 'black': '#000000',
15
- 'white': '#ffffff',
16
- 'red': '#ff0000',
17
- 'green': '#008000',
18
- 'blue': '#0000ff',
19
- 'yellow': '#ffff00',
20
- 'cyan': '#00ffff',
21
- 'magenta': '#ff00ff',
22
- 'silver': '#c0c0c0',
23
- 'gray': '#808080',
24
- 'maroon': '#800000',
25
- 'olive': '#808000',
26
- 'lime': '#00ff00',
27
- 'aqua': '#00ffff',
28
- 'teal': '#008080',
29
- 'navy': '#000080',
30
- 'fuchsia': '#ff00ff',
31
- 'purple': '#800080'
32
- };
33
-
34
- let hexColor = color.toLowerCase().trim();
35
-
36
- // Convert named color to hex
37
- if (namedColors[hexColor]) {
38
- hexColor = namedColors[hexColor];
39
- }
40
-
41
- // Remove # if present
42
- hexColor = hexColor.replace('#', '');
43
-
44
- // Handle 3-digit hex codes
45
- if (hexColor.length === 3) {
46
- hexColor = hexColor.split('').map(char => char + char).join('');
47
- }
48
-
49
- // Validate hex color
50
- if (!/^[0-9a-f]{6}$/i.test(hexColor)) {
51
- return false; // Invalid color, don't apply background
52
- }
53
-
54
- // Convert to RGB
55
- const r = parseInt(hexColor.substr(0, 2), 16);
56
- const g = parseInt(hexColor.substr(2, 2), 16);
57
- const b = parseInt(hexColor.substr(4, 2), 16);
58
-
59
- // Calculate relative luminance using WCAG formula
60
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
61
-
62
- // Colors with luminance < 0.5 are considered dark
63
- return luminance < 0.5;
64
- };
65
-
66
- // Use passed currentDate or generate fallback
67
- const displayDate = currentDate || (() => {
68
- const now = new Date();
69
- return `${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getDate().toString().padStart(2, '0')}/${now.getFullYear()}`;
70
- })();
71
-
72
- return `
73
- <!DOCTYPE html>
74
- <html lang="en">
75
- <head>
76
- <meta charset="utf-8" />
77
- <style>
78
- html, body {
79
- width: 100%;
80
- height: 100%;
81
- margin: 0;
82
- font-family: Arial, sans-serif;
83
- background-color: white;
84
- display: flex;
85
- flex-direction: column;
86
- min-height: 100vh;
87
- }
88
- .header {
89
- display: flex;
90
- align-items: center;
91
- margin-bottom: 15px;
92
- border-bottom: 2px solid #333;
93
- padding-bottom: 8px;
94
- position: relative;
95
- }
96
- .header-content {
97
- flex: 1;
98
- display: flex;
99
- align-items: center;
100
- justify-content: space-between;
101
- }
102
- .date {
103
- font-size: 16px;
104
- font-weight: bold;
105
- }
106
- .case-number {
107
- font-size: 16px;
108
- font-weight: bold;
109
- color: #333;
110
- text-align: right;
111
- }
112
- .image-container {
113
- width: 100%;
114
- margin: 10px 0;
115
- position: relative;
116
- }
117
- .image-wrapper {
118
- display: flex;
119
- justify-content: center;
120
- align-items: center;
121
- width: 100%;
122
- position: relative;
123
- }
124
- .image-container img {
125
- width: 100%;
126
- max-height: 65vh;
127
- height: auto;
128
- display: block;
129
- box-sizing: border-box;
130
- object-fit: contain;
131
- }
132
- .image-with-border {
133
- max-width: calc(100% - 10px);
134
- max-height: calc(100% - 10px);
135
- margin: 0 auto;
136
- }
137
- .annotations-overlay {
138
- position: absolute;
139
- top: 0;
140
- left: 0;
141
- width: 100%;
142
- height: 100%;
143
- pointer-events: none;
144
- z-index: 10;
145
- }
146
- .left-annotation,
147
- .right-annotation {
148
- position: absolute;
149
- padding: 12px 16px;
150
- background: rgba(0, 0, 0, 0.7);
151
- border-radius: 6px;
152
- backdrop-filter: blur(4px);
153
- border: 2px solid rgba(255, 255, 255, 0.2);
154
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
155
- }
156
- .left-annotation {
157
- top: 2%;
158
- left: 4%;
159
- }
160
- .right-annotation {
161
- top: 2%;
162
- right: 4%;
163
- }
164
- .case-text {
165
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
166
- font-size: 18px;
167
- font-weight: 700;
168
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
169
- white-space: nowrap;
170
- letter-spacing: 0.5px;
171
- }
172
- .below-image-annotations {
173
- display: flex;
174
- justify-content: space-between;
175
- align-items: flex-start;
176
- width: 100%;
177
- margin-top: 12px;
178
- min-height: 50px;
179
- }
180
- .support-level-annotation {
181
- flex: 1;
182
- display: flex;
183
- justify-content: flex-start;
184
- }
185
- .class-annotation {
186
- flex: 1;
187
- display: flex;
188
- justify-content: center;
189
- }
190
- .subclass-annotation {
191
- flex: 1;
192
- display: flex;
193
- justify-content: flex-end;
194
- }
195
- .support-level-text {
196
- padding: 10px 20px;
197
- background: rgba(240, 240, 240, 0.95);
198
- border-radius: 6px;
199
- backdrop-filter: blur(6px);
200
- border: 2px solid rgba(200, 200, 200, 0.6);
201
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
202
- font-family: 'Inter', Arial, sans-serif;
203
- font-size: 14px;
204
- font-weight: 700;
205
- text-align: center;
206
- letter-spacing: 0.5px;
207
- text-shadow: none;
208
- white-space: nowrap;
209
- }
210
- .class-text-annotation {
211
- padding: 10px 20px;
212
- background: rgba(0, 0, 0, 0.8);
213
- color: #ffffff;
214
- border-radius: 6px;
215
- backdrop-filter: blur(6px);
216
- border: 2px solid rgba(255, 255, 255, 0.2);
217
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
218
- font-family: 'Inter', Arial, sans-serif;
219
- font-size: 14px;
220
- font-weight: 600;
221
- text-align: center;
222
- letter-spacing: 0.5px;
223
- text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
224
- white-space: nowrap;
225
- }
226
- .subclass-text {
227
- padding: 12px 24px;
228
- background: rgba(220, 53, 69, 0.9);
229
- color: #ffffff;
230
- border-radius: 8px;
231
- backdrop-filter: blur(6px);
232
- border: 2px solid rgba(255, 255, 255, 0.3);
233
- box-shadow: 0 4px 16px rgba(220, 53, 69, 0.4);
234
- font-family: 'Inter', Arial, sans-serif;
235
- font-size: 14px;
236
- font-weight: 700;
237
- text-align: center;
238
- letter-spacing: 0.5px;
239
- text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
240
- white-space: nowrap;
241
- }
242
- .confirmation-section {
243
- margin-top: 20px;
244
- display: flex;
245
- justify-content: space-between;
246
- align-items: flex-start;
247
- }
248
- .confirmation-box {
249
- background: #ffffff;
250
- border: 2px solid #333;
251
- border-radius: 6px;
252
- padding: 15px;
253
- width: 280px;
254
- font-family: 'Inter', Arial, sans-serif;
255
- }
256
- .confirmation-label {
257
- font-size: 14px;
258
- font-weight: 600;
259
- color: #333;
260
- margin-bottom: 8px;
261
- }
262
- .confirmation-line {
263
- border-bottom: 1px solid #333;
264
- height: 18px;
265
- margin-bottom: 15px;
266
- width: 100%;
267
- }
268
- .confirmation-date-label {
269
- font-size: 14px;
270
- font-weight: 600;
271
- color: #333;
272
- margin-bottom: 8px;
273
- margin-top: 10px;
274
- }
275
- .confirmation-data {
276
- background: #f8f9fa;
277
- border: 2px solid #28a745;
278
- border-radius: 6px;
279
- padding: 15px;
280
- width: 280px;
281
- font-family: 'Inter', Arial, sans-serif;
282
- }
283
- .confirmation-title {
284
- font-size: 14px;
285
- font-weight: 700;
286
- color: #28a745;
287
- margin-bottom: 12px;
288
- text-align: center;
289
- border-bottom: 1px solid #28a745;
290
- padding-bottom: 6px;
291
- }
292
- .confirmation-field {
293
- margin-bottom: 8px;
294
- font-size: 13px;
295
- line-height: 1.4;
296
- }
297
- .confirmation-name {
298
- font-weight: 700;
299
- color: #333;
300
- font-size: 14px;
301
- }
302
- .confirmation-badge {
303
- color: #666;
304
- font-weight: 600;
305
- }
306
- .confirmation-company {
307
- color: #333;
308
- font-weight: 500;
309
- font-style: italic;
310
- }
311
- .confirmation-timestamp {
312
- color: #555;
313
- font-size: 12px;
314
- font-weight: 500;
315
- }
316
- .confirmation-id {
317
- color: #28a745;
318
- font-weight: 700;
319
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
320
- font-size: 12px;
321
- letter-spacing: 1px;
322
- }
323
- .additional-notes-section {
324
- max-width: 400px;
325
- font-family: 'Inter', Arial, sans-serif;
326
- font-size: 14px;
327
- line-height: 1.6;
328
- color: #333;
329
- white-space: pre-wrap;
330
- word-wrap: break-word;
331
- text-indent: 0 !important;
332
- padding: 0;
333
- margin: 0;
334
- margin-left: 20px;
335
- flex-shrink: 0;
336
- text-align: left;
337
- display: block;
338
- }
339
- .footer {
340
- margin-top: auto;
341
- padding-top: 15px;
342
- border-top: 1px solid #ccc;
343
- display: flex;
344
- justify-content: space-between;
345
- align-items: center;
346
- font-family: 'Inter', Arial, sans-serif;
347
- font-size: 11px;
348
- color: #666;
349
- }
350
- .main-content {
351
- flex: 1;
352
- display: flex;
353
- flex-direction: column;
354
- }
355
- .content-wrapper {
356
- flex-grow: 0;
357
- flex-shrink: 0;
358
- }
359
- .footer-left {
360
- font-weight: 500;
361
- flex: 1;
362
- text-align: left;
363
- display: flex;
364
- align-items: center;
365
- gap: 6px;
366
- }
367
- .footer-brand-icon {
368
- width: 14px;
369
- height: 14px;
370
- object-fit: contain;
371
- }
372
- .footer-center {
373
- font-weight: 600;
374
- flex: 1;
375
- text-align: center;
376
- color: #333;
377
- }
378
- .footer-right {
379
- font-style: italic;
380
- flex: 1;
381
- text-align: right;
382
- }
383
- .index-section {
384
- text-align: center;
385
- margin: 15px 0 8px 0;
386
- font-family: 'Inter', Arial, sans-serif;
387
- font-size: 14px;
388
- font-weight: 600;
389
- color: #333;
390
- }
391
- .box-annotation {
392
- position: absolute;
393
- box-sizing: border-box;
394
- pointer-events: none;
395
- background: transparent;
396
- border-width: 2px;
397
- border-style: solid;
398
- opacity: 0.8;
399
- }
400
- </style>
401
- </head>
402
- <body>
403
- <div class="main-content">
404
- <div class="content-wrapper">
405
- <div class="header">
406
- <div class="header-content">
407
- <div class="date">${displayDate}</div>
408
- ${caseNumber ? `<div class="case-number">${caseNumber}</div>` : '<div class="case-number"></div>'}
409
- </div>
410
- </div>
411
-
412
- ${imageUrl && imageUrl !== '/clear.jpg' ? `
413
- ${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'number' && annotationData.indexNumber ? `
414
- <div class="index-section">
415
- Index: ${annotationData.indexNumber}
416
- </div>
417
- ` : ''}
418
-
419
- <div class="image-container">
420
- <div class="image-wrapper">
421
- <img src="${imageUrl}" alt="Comparison Image" ${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'color' && annotationData.indexColor ? `class="image-with-border" style="border: 5px solid ${annotationData.indexColor};"` : ''} />
422
-
423
- ${annotationData && annotationsSet?.has('number') ? `
424
- <div class="annotations-overlay">
425
- <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);' : ''}">
426
- <div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
427
- ${annotationData.leftCase}${annotationData.leftItem ? ` ${annotationData.leftItem}` : ''}
428
- </div>
429
- </div>
430
- <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);' : ''}">
431
- <div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
432
- ${annotationData.rightCase}${annotationData.rightItem ? ` ${annotationData.rightItem}` : ''}
433
- </div>
434
- </div>
435
- </div>
436
- ` : ''}
437
-
438
- ${annotationData && annotationsSet?.has('box') && annotationData.boxAnnotations ? `
439
- <div class="annotations-overlay">
440
- ${annotationData.boxAnnotations.map(box => `
441
- <div class="box-annotation" style="
442
- left: ${box.x}%;
443
- top: ${box.y}%;
444
- width: ${box.width}%;
445
- height: ${box.height}%;
446
- border-color: ${box.color};
447
- "></div>
448
- `).join('')}
449
- </div>
450
- ` : ''}
451
- </div>
452
- </div>
453
-
454
- <div class="below-image-annotations">
455
- ${annotationData && annotationsSet?.has('id') ? `
456
- <div class="support-level-annotation">
457
- <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)'};">
458
- ${annotationData.supportLevel === 'ID' ? 'Identification' : annotationData.supportLevel}
459
- </div>
460
- </div>
461
- ` : '<div class="support-level-annotation"></div>'}
462
-
463
- ${annotationData && annotationsSet?.has('class') ? `
464
- <div class="class-annotation">
465
- <div class="class-text-annotation">
466
- ${annotationData.customClass || annotationData.classType}${annotationData.classNote ? ` (${annotationData.classNote})` : ''}
467
- </div>
468
- </div>
469
- ` : '<div class="class-annotation"></div>'}
470
-
471
- ${annotationData && annotationsSet?.has('class') && annotationData.hasSubclass ? `
472
- <div class="subclass-annotation">
473
- <div class="subclass-text">
474
- POTENTIAL SUBCLASS
475
- </div>
476
- </div>
477
- ` : '<div class="subclass-annotation"></div>'}
478
- </div>
479
- </div>
480
- ` : ''}
481
-
482
- ${annotationData && ((annotationData.includeConfirmation === true) || annotationData.additionalNotes) ? `
483
- <div class="confirmation-section">
484
- ${annotationData && (annotationData.includeConfirmation === true) ? `
485
- ${annotationData.confirmationData ? `
486
- <div class="confirmation-data">
487
- <div class="confirmation-title">IDENTIFICATION CONFIRMED</div>
488
- <div class="confirmation-field">
489
- <div class="confirmation-name">${annotationData.confirmationData.fullName}, ${annotationData.confirmationData.badgeId}</div>
490
- </div>
491
- <div class="confirmation-field">
492
- <div class="confirmation-company">${annotationData.confirmationData.confirmedByCompany || 'N/A'}</div>
493
- </div>
494
- <div class="confirmation-field">
495
- <div class="confirmation-timestamp">${annotationData.confirmationData.timestamp}</div>
496
- </div>
497
- <div class="confirmation-field">
498
- <div class="confirmation-id">ID: ${annotationData.confirmationData.confirmationId}</div>
499
- </div>
500
- </div>
501
- ` : `
502
- <div class="confirmation-box">
503
- <div class="confirmation-label">Confirmation by:</div>
504
- <div class="confirmation-line"></div>
505
- <div class="confirmation-date-label">Date:</div>
506
- <div class="confirmation-line"></div>
507
- </div>
508
- `}
509
- ` : '<div></div>'}
510
-
511
- ${annotationData && annotationsSet?.has('notes') && annotationData.additionalNotes && annotationData.additionalNotes.trim() ? `
512
- <div class="additional-notes-section">${annotationData.additionalNotes.trim()}</div>
513
- ` : '<div></div>'}
514
- </div>
515
- ` : ''}
516
-
517
- </div>
518
- </div>
519
-
520
- <div class="footer">
521
- <div class="footer-left">
522
- <span>Notes formatted by Striae</span>
523
- <img class="footer-brand-icon" src="${ICON_256}" alt="Striae icon" />
524
- </div>
525
- <div class="footer-center">
526
- ${userCompany ? userCompany : ''}
527
- </div>
528
- <div class="footer-right">
529
- ${notesUpdatedFormatted ? `Notes updated ${notesUpdatedFormatted}` : ''}
530
- </div>
531
- </div>
532
- </body>
533
- </html>
534
- `;
535
- };
1
+ import type { PDFGenerationData, ReportRenderer } from '../report-types';
2
+ import { ICON_256 } from '../assets/generated-assets';
3
+
4
+ export const renderReport: ReportRenderer = (data: PDFGenerationData): string => {
5
+ const { imageUrl, caseNumber, annotationData, activeAnnotations, currentDate, notesUpdatedFormatted, userCompany } = data;
6
+ const annotationsSet = new Set(activeAnnotations);
7
+
8
+ // Programmatically determine if a color is dark and needs a light background
9
+ const needsLightBackground = (color: string | undefined): boolean => {
10
+ if (!color) return false;
11
+
12
+ // Handle named colors
13
+ const namedColors: Record<string, string> = {
14
+ 'black': '#000000',
15
+ 'white': '#ffffff',
16
+ 'red': '#ff0000',
17
+ 'green': '#008000',
18
+ 'blue': '#0000ff',
19
+ 'yellow': '#ffff00',
20
+ 'cyan': '#00ffff',
21
+ 'magenta': '#ff00ff',
22
+ 'silver': '#c0c0c0',
23
+ 'gray': '#808080',
24
+ 'maroon': '#800000',
25
+ 'olive': '#808000',
26
+ 'lime': '#00ff00',
27
+ 'aqua': '#00ffff',
28
+ 'teal': '#008080',
29
+ 'navy': '#000080',
30
+ 'fuchsia': '#ff00ff',
31
+ 'purple': '#800080'
32
+ };
33
+
34
+ let hexColor = color.toLowerCase().trim();
35
+
36
+ // Convert named color to hex
37
+ if (namedColors[hexColor]) {
38
+ hexColor = namedColors[hexColor];
39
+ }
40
+
41
+ // Remove # if present
42
+ hexColor = hexColor.replace('#', '');
43
+
44
+ // Handle 3-digit hex codes
45
+ if (hexColor.length === 3) {
46
+ hexColor = hexColor.split('').map(char => char + char).join('');
47
+ }
48
+
49
+ // Validate hex color
50
+ if (!/^[0-9a-f]{6}$/i.test(hexColor)) {
51
+ return false; // Invalid color, don't apply background
52
+ }
53
+
54
+ // Convert to RGB
55
+ const r = parseInt(hexColor.substr(0, 2), 16);
56
+ const g = parseInt(hexColor.substr(2, 2), 16);
57
+ const b = parseInt(hexColor.substr(4, 2), 16);
58
+
59
+ // Calculate relative luminance using WCAG formula
60
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
61
+
62
+ // Colors with luminance < 0.5 are considered dark
63
+ return luminance < 0.5;
64
+ };
65
+
66
+ // Use passed currentDate or generate fallback
67
+ const displayDate = currentDate || (() => {
68
+ const now = new Date();
69
+ return `${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getDate().toString().padStart(2, '0')}/${now.getFullYear()}`;
70
+ })();
71
+
72
+ return `
73
+ <!DOCTYPE html>
74
+ <html lang="en">
75
+ <head>
76
+ <meta charset="utf-8" />
77
+ <style>
78
+ html, body {
79
+ width: 100%;
80
+ height: 100%;
81
+ margin: 0;
82
+ font-family: Arial, sans-serif;
83
+ background-color: white;
84
+ display: flex;
85
+ flex-direction: column;
86
+ min-height: 100vh;
87
+ }
88
+ .header {
89
+ display: flex;
90
+ align-items: center;
91
+ margin-bottom: 15px;
92
+ border-bottom: 2px solid #333;
93
+ padding-bottom: 8px;
94
+ position: relative;
95
+ }
96
+ .header-content {
97
+ flex: 1;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: space-between;
101
+ }
102
+ .date {
103
+ font-size: 16px;
104
+ font-weight: bold;
105
+ }
106
+ .case-number {
107
+ font-size: 16px;
108
+ font-weight: bold;
109
+ color: #333;
110
+ text-align: right;
111
+ }
112
+ .image-container {
113
+ width: 100%;
114
+ margin: 10px 0;
115
+ position: relative;
116
+ }
117
+ .image-wrapper {
118
+ display: flex;
119
+ justify-content: center;
120
+ align-items: center;
121
+ width: 100%;
122
+ position: relative;
123
+ }
124
+ .image-container img {
125
+ width: 100%;
126
+ max-height: 65vh;
127
+ height: auto;
128
+ display: block;
129
+ box-sizing: border-box;
130
+ object-fit: contain;
131
+ }
132
+ .image-with-border {
133
+ max-width: calc(100% - 10px);
134
+ max-height: calc(100% - 10px);
135
+ margin: 0 auto;
136
+ }
137
+ .annotations-overlay {
138
+ position: absolute;
139
+ top: 0;
140
+ left: 0;
141
+ width: 100%;
142
+ height: 100%;
143
+ pointer-events: none;
144
+ z-index: 10;
145
+ }
146
+ .left-annotation,
147
+ .right-annotation {
148
+ position: absolute;
149
+ padding: 12px 16px;
150
+ background: rgba(0, 0, 0, 0.7);
151
+ border-radius: 6px;
152
+ backdrop-filter: blur(4px);
153
+ border: 2px solid rgba(255, 255, 255, 0.2);
154
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
155
+ }
156
+ .left-annotation {
157
+ top: 2%;
158
+ left: 4%;
159
+ }
160
+ .right-annotation {
161
+ top: 2%;
162
+ right: 4%;
163
+ }
164
+ .case-text {
165
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
166
+ font-size: 18px;
167
+ font-weight: 700;
168
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
169
+ white-space: nowrap;
170
+ letter-spacing: 0.5px;
171
+ }
172
+ .below-image-annotations {
173
+ display: flex;
174
+ justify-content: space-between;
175
+ align-items: flex-start;
176
+ width: 100%;
177
+ margin-top: 12px;
178
+ min-height: 50px;
179
+ }
180
+ .support-level-annotation {
181
+ flex: 1;
182
+ display: flex;
183
+ justify-content: flex-start;
184
+ }
185
+ .class-annotation {
186
+ flex: 1;
187
+ display: flex;
188
+ justify-content: center;
189
+ }
190
+ .subclass-annotation {
191
+ flex: 1;
192
+ display: flex;
193
+ justify-content: flex-end;
194
+ }
195
+ .support-level-text {
196
+ padding: 10px 20px;
197
+ background: rgba(240, 240, 240, 0.95);
198
+ border-radius: 6px;
199
+ backdrop-filter: blur(6px);
200
+ border: 2px solid rgba(200, 200, 200, 0.6);
201
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
202
+ font-family: 'Inter', Arial, sans-serif;
203
+ font-size: 14px;
204
+ font-weight: 700;
205
+ text-align: center;
206
+ letter-spacing: 0.5px;
207
+ text-shadow: none;
208
+ white-space: nowrap;
209
+ }
210
+ .class-text-annotation {
211
+ padding: 10px 20px;
212
+ background: rgba(0, 0, 0, 0.8);
213
+ color: #ffffff;
214
+ border-radius: 6px;
215
+ backdrop-filter: blur(6px);
216
+ border: 2px solid rgba(255, 255, 255, 0.2);
217
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
218
+ font-family: 'Inter', Arial, sans-serif;
219
+ font-size: 14px;
220
+ font-weight: 600;
221
+ text-align: center;
222
+ letter-spacing: 0.5px;
223
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
224
+ white-space: nowrap;
225
+ }
226
+ .subclass-text {
227
+ padding: 12px 24px;
228
+ background: rgba(220, 53, 69, 0.9);
229
+ color: #ffffff;
230
+ border-radius: 8px;
231
+ backdrop-filter: blur(6px);
232
+ border: 2px solid rgba(255, 255, 255, 0.3);
233
+ box-shadow: 0 4px 16px rgba(220, 53, 69, 0.4);
234
+ font-family: 'Inter', Arial, sans-serif;
235
+ font-size: 14px;
236
+ font-weight: 700;
237
+ text-align: center;
238
+ letter-spacing: 0.5px;
239
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
240
+ white-space: nowrap;
241
+ }
242
+ .confirmation-section {
243
+ margin-top: 20px;
244
+ display: flex;
245
+ justify-content: space-between;
246
+ align-items: flex-start;
247
+ }
248
+ .confirmation-box {
249
+ background: #ffffff;
250
+ border: 2px solid #333;
251
+ border-radius: 6px;
252
+ padding: 15px;
253
+ width: 280px;
254
+ font-family: 'Inter', Arial, sans-serif;
255
+ }
256
+ .confirmation-label {
257
+ font-size: 14px;
258
+ font-weight: 600;
259
+ color: #333;
260
+ margin-bottom: 8px;
261
+ }
262
+ .confirmation-line {
263
+ border-bottom: 1px solid #333;
264
+ height: 18px;
265
+ margin-bottom: 15px;
266
+ width: 100%;
267
+ }
268
+ .confirmation-date-label {
269
+ font-size: 14px;
270
+ font-weight: 600;
271
+ color: #333;
272
+ margin-bottom: 8px;
273
+ margin-top: 10px;
274
+ }
275
+ .confirmation-data {
276
+ background: #f8f9fa;
277
+ border: 2px solid #28a745;
278
+ border-radius: 6px;
279
+ padding: 15px;
280
+ width: 280px;
281
+ font-family: 'Inter', Arial, sans-serif;
282
+ }
283
+ .confirmation-title {
284
+ font-size: 14px;
285
+ font-weight: 700;
286
+ color: #28a745;
287
+ margin-bottom: 12px;
288
+ text-align: center;
289
+ border-bottom: 1px solid #28a745;
290
+ padding-bottom: 6px;
291
+ }
292
+ .confirmation-field {
293
+ margin-bottom: 8px;
294
+ font-size: 13px;
295
+ line-height: 1.4;
296
+ }
297
+ .confirmation-name {
298
+ font-weight: 700;
299
+ color: #333;
300
+ font-size: 14px;
301
+ }
302
+ .confirmation-badge {
303
+ color: #666;
304
+ font-weight: 600;
305
+ }
306
+ .confirmation-company {
307
+ color: #333;
308
+ font-weight: 500;
309
+ font-style: italic;
310
+ }
311
+ .confirmation-timestamp {
312
+ color: #555;
313
+ font-size: 12px;
314
+ font-weight: 500;
315
+ }
316
+ .confirmation-id {
317
+ color: #28a745;
318
+ font-weight: 700;
319
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
320
+ font-size: 12px;
321
+ letter-spacing: 1px;
322
+ }
323
+ .additional-notes-section {
324
+ max-width: 400px;
325
+ font-family: 'Inter', Arial, sans-serif;
326
+ font-size: 14px;
327
+ line-height: 1.6;
328
+ color: #333;
329
+ white-space: pre-wrap;
330
+ word-wrap: break-word;
331
+ text-indent: 0 !important;
332
+ padding: 0;
333
+ margin: 0;
334
+ margin-left: 20px;
335
+ flex-shrink: 0;
336
+ text-align: left;
337
+ display: block;
338
+ }
339
+ .footer {
340
+ margin-top: auto;
341
+ padding-top: 15px;
342
+ border-top: 1px solid #ccc;
343
+ display: flex;
344
+ justify-content: space-between;
345
+ align-items: center;
346
+ font-family: 'Inter', Arial, sans-serif;
347
+ font-size: 11px;
348
+ color: #666;
349
+ }
350
+ .main-content {
351
+ flex: 1;
352
+ display: flex;
353
+ flex-direction: column;
354
+ }
355
+ .content-wrapper {
356
+ flex-grow: 0;
357
+ flex-shrink: 0;
358
+ }
359
+ .footer-left {
360
+ font-weight: 500;
361
+ flex: 1;
362
+ text-align: left;
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 6px;
366
+ }
367
+ .footer-brand-icon {
368
+ width: 14px;
369
+ height: 14px;
370
+ object-fit: contain;
371
+ }
372
+ .footer-center {
373
+ font-weight: 600;
374
+ flex: 1;
375
+ text-align: center;
376
+ color: #333;
377
+ }
378
+ .footer-right {
379
+ font-style: italic;
380
+ flex: 1;
381
+ text-align: right;
382
+ }
383
+ .index-section {
384
+ text-align: center;
385
+ margin: 15px 0 8px 0;
386
+ font-family: 'Inter', Arial, sans-serif;
387
+ font-size: 14px;
388
+ font-weight: 600;
389
+ color: #333;
390
+ }
391
+ .box-annotation {
392
+ position: absolute;
393
+ box-sizing: border-box;
394
+ pointer-events: none;
395
+ background: transparent;
396
+ border-width: 2px;
397
+ border-style: solid;
398
+ opacity: 0.8;
399
+ }
400
+ </style>
401
+ </head>
402
+ <body>
403
+ <div class="main-content">
404
+ <div class="content-wrapper">
405
+ <div class="header">
406
+ <div class="header-content">
407
+ <div class="date">${displayDate}</div>
408
+ ${caseNumber ? `<div class="case-number">${caseNumber}</div>` : '<div class="case-number"></div>'}
409
+ </div>
410
+ </div>
411
+
412
+ ${imageUrl && imageUrl !== '/clear.jpg' ? `
413
+ ${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'number' && annotationData.indexNumber ? `
414
+ <div class="index-section">
415
+ Index: ${annotationData.indexNumber}
416
+ </div>
417
+ ` : ''}
418
+
419
+ <div class="image-container">
420
+ <div class="image-wrapper">
421
+ <img src="${imageUrl}" alt="Comparison Image" ${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'color' && annotationData.indexColor ? `class="image-with-border" style="border: 5px solid ${annotationData.indexColor};"` : ''} />
422
+
423
+ ${annotationData && annotationsSet?.has('number') ? `
424
+ <div class="annotations-overlay">
425
+ <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);' : ''}">
426
+ <div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
427
+ ${annotationData.leftCase}${annotationData.leftItem ? ` ${annotationData.leftItem}` : ''}
428
+ </div>
429
+ </div>
430
+ <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);' : ''}">
431
+ <div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
432
+ ${annotationData.rightCase}${annotationData.rightItem ? ` ${annotationData.rightItem}` : ''}
433
+ </div>
434
+ </div>
435
+ </div>
436
+ ` : ''}
437
+
438
+ ${annotationData && annotationsSet?.has('box') && annotationData.boxAnnotations ? `
439
+ <div class="annotations-overlay">
440
+ ${annotationData.boxAnnotations.map(box => `
441
+ <div class="box-annotation" style="
442
+ left: ${box.x}%;
443
+ top: ${box.y}%;
444
+ width: ${box.width}%;
445
+ height: ${box.height}%;
446
+ border-color: ${box.color};
447
+ "></div>
448
+ `).join('')}
449
+ </div>
450
+ ` : ''}
451
+ </div>
452
+ </div>
453
+
454
+ <div class="below-image-annotations">
455
+ ${annotationData && annotationsSet?.has('id') ? `
456
+ <div class="support-level-annotation">
457
+ <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)'};">
458
+ ${annotationData.supportLevel === 'ID' ? 'Identification' : annotationData.supportLevel}
459
+ </div>
460
+ </div>
461
+ ` : '<div class="support-level-annotation"></div>'}
462
+
463
+ ${annotationData && annotationsSet?.has('class') ? `
464
+ <div class="class-annotation">
465
+ <div class="class-text-annotation">
466
+ ${annotationData.customClass || annotationData.classType}${annotationData.classNote ? ` (${annotationData.classNote})` : ''}
467
+ </div>
468
+ </div>
469
+ ` : '<div class="class-annotation"></div>'}
470
+
471
+ ${annotationData && annotationsSet?.has('class') && annotationData.hasSubclass ? `
472
+ <div class="subclass-annotation">
473
+ <div class="subclass-text">
474
+ POTENTIAL SUBCLASS
475
+ </div>
476
+ </div>
477
+ ` : '<div class="subclass-annotation"></div>'}
478
+ </div>
479
+ </div>
480
+ ` : ''}
481
+
482
+ ${annotationData && ((annotationData.includeConfirmation === true) || annotationData.additionalNotes) ? `
483
+ <div class="confirmation-section">
484
+ ${annotationData && (annotationData.includeConfirmation === true) ? `
485
+ ${annotationData.confirmationData ? `
486
+ <div class="confirmation-data">
487
+ <div class="confirmation-title">IDENTIFICATION CONFIRMED</div>
488
+ <div class="confirmation-field">
489
+ <div class="confirmation-name">${annotationData.confirmationData.fullName}, ${annotationData.confirmationData.badgeId}</div>
490
+ </div>
491
+ <div class="confirmation-field">
492
+ <div class="confirmation-company">${annotationData.confirmationData.confirmedByCompany || 'N/A'}</div>
493
+ </div>
494
+ <div class="confirmation-field">
495
+ <div class="confirmation-timestamp">${annotationData.confirmationData.timestamp}</div>
496
+ </div>
497
+ <div class="confirmation-field">
498
+ <div class="confirmation-id">ID: ${annotationData.confirmationData.confirmationId}</div>
499
+ </div>
500
+ </div>
501
+ ` : `
502
+ <div class="confirmation-box">
503
+ <div class="confirmation-label">Confirmation by:</div>
504
+ <div class="confirmation-line"></div>
505
+ <div class="confirmation-date-label">Date:</div>
506
+ <div class="confirmation-line"></div>
507
+ </div>
508
+ `}
509
+ ` : '<div></div>'}
510
+
511
+ ${annotationData && annotationsSet?.has('notes') && annotationData.additionalNotes && annotationData.additionalNotes.trim() ? `
512
+ <div class="additional-notes-section">${annotationData.additionalNotes.trim()}</div>
513
+ ` : '<div></div>'}
514
+ </div>
515
+ ` : ''}
516
+
517
+ </div>
518
+ </div>
519
+
520
+ <div class="footer">
521
+ <div class="footer-left">
522
+ <span>Notes formatted by Striae</span>
523
+ <img class="footer-brand-icon" src="${ICON_256}" alt="Striae icon" />
524
+ </div>
525
+ <div class="footer-center">
526
+ ${userCompany ? userCompany : ''}
527
+ </div>
528
+ <div class="footer-right">
529
+ ${notesUpdatedFormatted ? `Notes updated ${notesUpdatedFormatted}` : ''}
530
+ </div>
531
+ </div>
532
+ </body>
533
+ </html>
534
+ `;
535
+ };