@striae-org/striae 4.2.0 → 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/LICENSE +1 -1
- package/app/components/actions/case-manage.ts +50 -17
- package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
- package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +6 -2
- package/app/components/colors/colors.module.css +4 -3
- 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/navbar/navbar.tsx +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +93 -73
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +737 -116
- package/app/components/sidebar/cases/cases.module.css +43 -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 +482 -177
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- 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-sidebar.tsx → notes-editor-form.tsx} +77 -76
- package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
- package/app/components/sidebar/notes/notes.module.css +262 -14
- package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
- package/app/components/sidebar/sidebar-container.tsx +2 -0
- package/app/components/sidebar/sidebar.tsx +15 -1
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/hooks/useOverlayDismiss.ts +6 -4
- package/app/root.tsx +1 -1
- package/app/routes/striae/striae.tsx +7 -0
- package/app/services/audit/audit.service.ts +2 -2
- package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
- package/app/types/annotations.ts +48 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/case-filters.ts +127 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +295 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/file-filters.ts +201 -0
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/forensics/export-verification.ts +40 -111
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +23 -22
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +7 -13
- package/scripts/deploy-primershear-emails.sh +1 -1
- package/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -124
- package/workers/pdf-worker/src/pdf-worker.example.ts +58 -61
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +23 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -52
- package/postcss.config.js +0 -6
- package/tailwind.config.ts +0 -22
- 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,19 +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
6
|
ACCOUNT_ID?: string;
|
|
7
|
-
CLOUDFLARE_ACCOUNT_ID?: string;
|
|
8
7
|
BROWSER_API_TOKEN?: string;
|
|
9
|
-
API_TOKEN?: string;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
const DEFAULT_REPORT_FORMAT = 'striae';
|
|
13
10
|
const BROWSER_PDF_TIMEOUT_MS = 90_000;
|
|
14
11
|
const BROWSER_RENDERING_API_BASE = 'https://api.cloudflare.com/client/v4/accounts';
|
|
15
12
|
|
|
16
|
-
const DEFAULT_PDF_OPTIONS = {
|
|
13
|
+
const DEFAULT_PDF_OPTIONS: ReportPdfOptions = {
|
|
17
14
|
printBackground: true,
|
|
18
15
|
format: 'letter',
|
|
19
16
|
margin: {
|
|
@@ -24,6 +21,17 @@ const DEFAULT_PDF_OPTIONS = {
|
|
|
24
21
|
},
|
|
25
22
|
};
|
|
26
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
|
+
|
|
27
35
|
const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
|
|
28
36
|
// Default Striae report format module
|
|
29
37
|
striae: () => import('./formats/format-striae'),
|
|
@@ -56,66 +64,55 @@ function jsonResponse(body: unknown, status: number): Response {
|
|
|
56
64
|
});
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
class MissingSecretError extends Error {
|
|
68
|
+
readonly secretKey: string;
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
constructor(key: string) {
|
|
71
|
+
super(`Worker is missing required secret: ${key}`);
|
|
72
|
+
this.name = 'MissingSecretError';
|
|
73
|
+
this.secretKey = key;
|
|
66
74
|
}
|
|
67
|
-
|
|
68
|
-
return '';
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
for (const candidate of candidates) {
|
|
75
|
-
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
76
|
-
return candidate.trim();
|
|
77
|
-
}
|
|
77
|
+
function getRequiredSecret(value: string | undefined, name: string): string {
|
|
78
|
+
if (typeof value !== 'string') {
|
|
79
|
+
throw new MissingSecretError(name);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
function normalizeReportFormat(format: unknown): string {
|
|
84
|
-
if (typeof format !== 'string') {
|
|
85
|
-
return DEFAULT_REPORT_FORMAT;
|
|
82
|
+
const normalized = value.trim();
|
|
83
|
+
if (!normalized) {
|
|
84
|
+
throw new MissingSecretError(name);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
return normalized || DEFAULT_REPORT_FORMAT;
|
|
87
|
+
return normalized;
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
function resolveReportRequest(payload: unknown):
|
|
93
|
-
if (!payload || typeof payload !== 'object') {
|
|
90
|
+
function resolveReportRequest(payload: unknown): PDFGenerationRequest {
|
|
91
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
94
92
|
throw new Error('Request body must be a JSON object');
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
const record = payload as Record<string, unknown>;
|
|
98
|
-
|
|
96
|
+
if (typeof record.reportFormat !== 'string' || record.reportFormat.trim().length === 0) {
|
|
97
|
+
throw new Error('Request body must include a non-empty reportFormat');
|
|
98
|
+
}
|
|
99
99
|
|
|
100
|
-
if (record.data
|
|
101
|
-
|
|
102
|
-
reportFormat,
|
|
103
|
-
data: record.data as PDFGenerationData,
|
|
104
|
-
};
|
|
100
|
+
if (!record.data || typeof record.data !== 'object' || Array.isArray(record.data)) {
|
|
101
|
+
throw new Error('Request body must include a data object');
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
const data = record.data as Record<string, unknown>;
|
|
105
|
+
if (typeof data.currentDate !== 'string' || data.currentDate.trim().length === 0) {
|
|
106
|
+
throw new Error('Request body data must include a non-empty currentDate');
|
|
107
|
+
}
|
|
111
108
|
|
|
112
109
|
return {
|
|
113
|
-
reportFormat,
|
|
114
|
-
data:
|
|
110
|
+
reportFormat: record.reportFormat.trim().toLowerCase(),
|
|
111
|
+
data: record.data as PDFGenerationData,
|
|
115
112
|
};
|
|
116
113
|
}
|
|
117
114
|
|
|
118
|
-
async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<string> {
|
|
115
|
+
async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<{ html: string; pdfOptions: ReportPdfOptions }> {
|
|
119
116
|
const loader = reportModuleLoaders[reportFormat];
|
|
120
117
|
|
|
121
118
|
if (!loader) {
|
|
@@ -124,28 +121,20 @@ async function renderReport(reportFormat: string, data: PDFGenerationData): Prom
|
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
const reportModule = await loader();
|
|
127
|
-
return
|
|
124
|
+
return {
|
|
125
|
+
html: reportModule.renderReport(data),
|
|
126
|
+
pdfOptions: resolvePdfOptions(reportModule.getPdfOptions?.(data)),
|
|
127
|
+
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
async function renderPdfViaRestEndpoint(env: Env, html: string): Promise<Response> {
|
|
131
|
-
const accountId =
|
|
132
|
-
const browserApiToken =
|
|
133
|
-
|
|
134
|
-
if (!accountId || !browserApiToken) {
|
|
135
|
-
return jsonResponse(
|
|
136
|
-
{
|
|
137
|
-
error: 'Missing required Browser Rendering credentials',
|
|
138
|
-
requiredSecrets: ['ACCOUNT_ID', 'BROWSER_API_TOKEN'],
|
|
139
|
-
note: 'Set ACCOUNT_ID and a Browser Rendering - Edit token (BROWSER_API_TOKEN) on this worker.',
|
|
140
|
-
},
|
|
141
|
-
502
|
|
142
|
-
);
|
|
143
|
-
}
|
|
130
|
+
async function renderPdfViaRestEndpoint(env: Env, html: string, pdfOptions: ReportPdfOptions): Promise<Response> {
|
|
131
|
+
const accountId = getRequiredSecret(env.ACCOUNT_ID, 'ACCOUNT_ID');
|
|
132
|
+
const browserApiToken = getRequiredSecret(env.BROWSER_API_TOKEN, 'BROWSER_API_TOKEN');
|
|
144
133
|
|
|
145
134
|
const endpoint = `${BROWSER_RENDERING_API_BASE}/${accountId}/browser-rendering/pdf`;
|
|
146
135
|
const requestBody = JSON.stringify({
|
|
147
136
|
html,
|
|
148
|
-
pdfOptions
|
|
137
|
+
pdfOptions,
|
|
149
138
|
});
|
|
150
139
|
|
|
151
140
|
let endpointResponse: Response;
|
|
@@ -217,12 +206,20 @@ export default {
|
|
|
217
206
|
|
|
218
207
|
if (request.method === 'POST') {
|
|
219
208
|
try {
|
|
220
|
-
const payload = await request.json() as
|
|
209
|
+
const payload = await request.json() as unknown;
|
|
221
210
|
const { reportFormat, data } = resolveReportRequest(payload);
|
|
222
211
|
const document = await renderReport(reportFormat, data);
|
|
223
212
|
|
|
224
|
-
return await renderPdfViaRestEndpoint(env, document);
|
|
213
|
+
return await renderPdfViaRestEndpoint(env, document.html, document.pdfOptions);
|
|
225
214
|
} catch (error) {
|
|
215
|
+
if (error instanceof MissingSecretError) {
|
|
216
|
+
console.error(`[pdf-worker] Configuration error: ${error.message}`);
|
|
217
|
+
return jsonResponse(
|
|
218
|
+
{ error: 'Worker configuration error', missing_secret: error.secretKey },
|
|
219
|
+
502
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
226
223
|
if (isTimeoutError(error)) {
|
|
227
224
|
const timeoutMessage = error instanceof Error ? error.message : 'PDF generation timed out';
|
|
228
225
|
return jsonResponse({ error: timeoutMessage }, 504);
|
|
@@ -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
|
+
}
|
|
@@ -52,7 +52,7 @@ export interface PDFGenerationData {
|
|
|
52
52
|
caseNumber?: string;
|
|
53
53
|
annotationData?: AnnotationData;
|
|
54
54
|
activeAnnotations?: string[];
|
|
55
|
-
currentDate
|
|
55
|
+
currentDate: string;
|
|
56
56
|
notesUpdatedFormatted?: string;
|
|
57
57
|
userCompany?: string;
|
|
58
58
|
userFirstName?: string;
|
|
@@ -61,12 +61,32 @@ export interface PDFGenerationData {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export interface PDFGenerationRequest {
|
|
64
|
-
reportFormat
|
|
65
|
-
data
|
|
64
|
+
reportFormat: string;
|
|
65
|
+
data: PDFGenerationData;
|
|
66
|
+
}
|
|
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;
|
|
66
83
|
}
|
|
67
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
|
}
|
|
@@ -433,6 +433,22 @@ async function deleteSingleCase(env: Env, userUid: string, caseNumber: string):
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
async function deleteUserConfirmationSummary(env: Env, userUid: string): Promise<void> {
|
|
437
|
+
const dataApiKey = env.R2_KEY_SECRET;
|
|
438
|
+
const dataWorkerBaseUrl = resolveDataWorkerBaseUrl(env);
|
|
439
|
+
const encodedUserId = encodeURIComponent(userUid);
|
|
440
|
+
const confirmationSummaryPath = `${dataWorkerBaseUrl}/${encodedUserId}/meta/confirmation-status.json`;
|
|
441
|
+
|
|
442
|
+
const response = await fetch(confirmationSummaryPath, {
|
|
443
|
+
method: 'DELETE',
|
|
444
|
+
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!response.ok && response.status !== 404) {
|
|
448
|
+
throw new Error(`Failed to delete confirmation summary metadata: ${response.status}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
436
452
|
async function executeUserDeletion(
|
|
437
453
|
env: Env,
|
|
438
454
|
userUid: string,
|
|
@@ -490,6 +506,7 @@ async function executeUserDeletion(
|
|
|
490
506
|
throw new Error(`Failed to fully delete all case data: ${caseCleanupErrors.join(' | ')}`);
|
|
491
507
|
}
|
|
492
508
|
|
|
509
|
+
await deleteUserConfirmationSummary(env, userUid);
|
|
493
510
|
await deleteFirebaseAuthUser(env, userUid);
|
|
494
511
|
|
|
495
512
|
// Delete the user account from the database
|
package/wrangler.toml.example
CHANGED
package/NOTICE
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
NOTICE
|
|
2
|
-
|
|
3
|
-
Striae – A Firearms Examiner’s Comparison Companion
|
|
4
|
-
|
|
5
|
-
Striae © 2025. All rights reserved.
|
|
6
|
-
|
|
7
|
-
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License in the repository’s LICENSE file.
|
|
8
|
-
|
|
9
|
-
Attributions for bundled components:
|
|
10
|
-
|
|
11
|
-
Portions of this product may include open-source software licensed under their respective licenses, including but not limited to: Remix, Cloudflare Workers, Firebase, Tailwind CSS, Vite, and other npm packages. License texts for third-party components are provided in their respective source files or in node_modules as applicable.
|
|
12
|
-
|
|
13
|
-
No trademark rights are granted for “Striae” or any associated names or logos.
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
-
import styles from './notes.module.css';
|
|
4
|
-
|
|
5
|
-
interface NotesModalProps {
|
|
6
|
-
isOpen: boolean;
|
|
7
|
-
onClose: () => void;
|
|
8
|
-
notes: string;
|
|
9
|
-
onSave: (notes: string) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps) => {
|
|
13
|
-
const [tempNotes, setTempNotes] = useState(notes);
|
|
14
|
-
const {
|
|
15
|
-
requestClose,
|
|
16
|
-
overlayProps,
|
|
17
|
-
getCloseButtonProps
|
|
18
|
-
} = useOverlayDismiss({
|
|
19
|
-
isOpen,
|
|
20
|
-
onClose
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
if (!isOpen) return null;
|
|
24
|
-
|
|
25
|
-
const handleSave = () => {
|
|
26
|
-
onSave(tempNotes);
|
|
27
|
-
requestClose();
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<div
|
|
32
|
-
className={styles.modalOverlay}
|
|
33
|
-
aria-label="Close notes dialog"
|
|
34
|
-
{...overlayProps}
|
|
35
|
-
>
|
|
36
|
-
<div className={styles.modal}>
|
|
37
|
-
<button {...getCloseButtonProps({ ariaLabel: 'Close notes dialog' })}>×</button>
|
|
38
|
-
<h5 className={styles.modalTitle}>Additional Notes</h5>
|
|
39
|
-
<textarea
|
|
40
|
-
value={tempNotes}
|
|
41
|
-
onChange={(e) => setTempNotes(e.target.value)}
|
|
42
|
-
className={styles.modalTextarea}
|
|
43
|
-
placeholder="Enter additional notes..."
|
|
44
|
-
/>
|
|
45
|
-
<div className={styles.modalButtons}>
|
|
46
|
-
<button onClick={handleSave} className={styles.saveButton}>Save</button>
|
|
47
|
-
<button onClick={requestClose} className={styles.cancelButton}>Cancel</button>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
);
|
|
52
|
-
};
|
package/postcss.config.js
DELETED
package/tailwind.config.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { Config } from "tailwindcss";
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
|
|
5
|
-
theme: {
|
|
6
|
-
extend: {
|
|
7
|
-
fontFamily: {
|
|
8
|
-
sans: [
|
|
9
|
-
"Inter",
|
|
10
|
-
"ui-sans-serif",
|
|
11
|
-
"system-ui",
|
|
12
|
-
"sans-serif",
|
|
13
|
-
"Apple Color Emoji",
|
|
14
|
-
"Segoe UI Emoji",
|
|
15
|
-
"Segoe UI Symbol",
|
|
16
|
-
"Noto Color Emoji",
|
|
17
|
-
],
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
plugins: [],
|
|
22
|
-
} satisfies Config;
|
|
Binary file
|
|
File without changes
|