@striae-org/striae 3.0.4
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/.env.example +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- package/wrangler.toml.example +8 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useContext, useCallback } from 'react';
|
|
2
|
+
import { AuthContext } from '~/contexts/auth.context';
|
|
3
|
+
import {
|
|
4
|
+
listReadOnlyCases,
|
|
5
|
+
deleteReadOnlyCase
|
|
6
|
+
} from '~/components/actions/case-review';
|
|
7
|
+
import {
|
|
8
|
+
ImportResult,
|
|
9
|
+
ConfirmationImportResult
|
|
10
|
+
} from '~/types';
|
|
11
|
+
import {
|
|
12
|
+
FileSelector,
|
|
13
|
+
CasePreviewSection,
|
|
14
|
+
ConfirmationPreviewSection,
|
|
15
|
+
ProgressSection,
|
|
16
|
+
ExistingCaseSection,
|
|
17
|
+
ConfirmationDialog,
|
|
18
|
+
useImportState,
|
|
19
|
+
useFilePreview,
|
|
20
|
+
useImportExecution,
|
|
21
|
+
isValidImportFile,
|
|
22
|
+
getImportType,
|
|
23
|
+
resetFileInput
|
|
24
|
+
} from './index';
|
|
25
|
+
import styles from './case-import.module.css';
|
|
26
|
+
|
|
27
|
+
interface CaseImportProps {
|
|
28
|
+
isOpen: boolean;
|
|
29
|
+
onClose: () => void;
|
|
30
|
+
onImportComplete?: (result: ImportResult | ConfirmationImportResult) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const CaseImport = ({
|
|
34
|
+
isOpen,
|
|
35
|
+
onClose,
|
|
36
|
+
onImportComplete
|
|
37
|
+
}: CaseImportProps) => {
|
|
38
|
+
const { user } = useContext(AuthContext);
|
|
39
|
+
|
|
40
|
+
// Use our custom hooks
|
|
41
|
+
const {
|
|
42
|
+
importState,
|
|
43
|
+
messages,
|
|
44
|
+
importProgress,
|
|
45
|
+
clearMessages,
|
|
46
|
+
setError,
|
|
47
|
+
setSuccess,
|
|
48
|
+
updateImportState,
|
|
49
|
+
resetImportState,
|
|
50
|
+
setImportProgress
|
|
51
|
+
} = useImportState();
|
|
52
|
+
|
|
53
|
+
const [existingReadOnlyCase, setExistingReadOnlyCase] = useState<string | null>(null);
|
|
54
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
55
|
+
|
|
56
|
+
// Clear import data helper
|
|
57
|
+
const clearImportData = useCallback(() => {
|
|
58
|
+
updateImportState({ selectedFile: null, importType: null });
|
|
59
|
+
clearPreviews();
|
|
60
|
+
resetFileInput(fileInputRef);
|
|
61
|
+
}, [updateImportState]);
|
|
62
|
+
|
|
63
|
+
// File preview hook
|
|
64
|
+
const {
|
|
65
|
+
casePreview,
|
|
66
|
+
confirmationPreview,
|
|
67
|
+
loadCasePreview,
|
|
68
|
+
loadConfirmationPreview,
|
|
69
|
+
clearPreviews
|
|
70
|
+
} = useFilePreview(
|
|
71
|
+
user,
|
|
72
|
+
setError,
|
|
73
|
+
(loading) => updateImportState({ isLoadingPreview: loading }),
|
|
74
|
+
clearImportData
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Import execution hook
|
|
78
|
+
const { executeImport } = useImportExecution({
|
|
79
|
+
user,
|
|
80
|
+
selectedFile: importState.selectedFile,
|
|
81
|
+
importType: importState.importType,
|
|
82
|
+
setImportProgress,
|
|
83
|
+
clearMessages,
|
|
84
|
+
setError,
|
|
85
|
+
setSuccess,
|
|
86
|
+
setIsImporting: (importing) => updateImportState({ isImporting: importing }),
|
|
87
|
+
onImportComplete,
|
|
88
|
+
onUpdateExistingCase: setExistingReadOnlyCase,
|
|
89
|
+
onClose
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Check for existing read-only cases
|
|
93
|
+
const checkForExistingReadOnlyCase = useCallback(async () => {
|
|
94
|
+
if (!user) return;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const readOnlyCases = await listReadOnlyCases(user);
|
|
98
|
+
setExistingReadOnlyCase(readOnlyCases.length > 0 ? readOnlyCases[0].caseNumber : null);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Error checking existing read-only cases:', error);
|
|
101
|
+
}
|
|
102
|
+
}, [user]);
|
|
103
|
+
|
|
104
|
+
// Clear existing read-only case
|
|
105
|
+
const clearExistingReadOnlyCase = useCallback(async () => {
|
|
106
|
+
if (!user || !existingReadOnlyCase) return;
|
|
107
|
+
|
|
108
|
+
updateImportState({ isClearing: true });
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await deleteReadOnlyCase(user, existingReadOnlyCase);
|
|
112
|
+
|
|
113
|
+
const clearedCaseName = existingReadOnlyCase;
|
|
114
|
+
setExistingReadOnlyCase(null);
|
|
115
|
+
setSuccess(`Removed read-only case "${clearedCaseName}"`);
|
|
116
|
+
|
|
117
|
+
onImportComplete?.({
|
|
118
|
+
success: true,
|
|
119
|
+
caseNumber: '',
|
|
120
|
+
isReadOnly: false,
|
|
121
|
+
filesImported: 0,
|
|
122
|
+
annotationsImported: 0,
|
|
123
|
+
errors: [],
|
|
124
|
+
warnings: []
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error clearing existing read-only case:', error);
|
|
129
|
+
setError(error instanceof Error ? error.message : 'Failed to clear existing case');
|
|
130
|
+
} finally {
|
|
131
|
+
updateImportState({ isClearing: false });
|
|
132
|
+
}
|
|
133
|
+
}, [user, existingReadOnlyCase, updateImportState, setSuccess, setError, onImportComplete]);
|
|
134
|
+
|
|
135
|
+
// Handle file selection
|
|
136
|
+
const handleFileSelect = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
137
|
+
const file = event.target.files?.[0];
|
|
138
|
+
if (!file) return;
|
|
139
|
+
|
|
140
|
+
// Clear any existing messages when selecting a new file
|
|
141
|
+
clearMessages();
|
|
142
|
+
|
|
143
|
+
if (!isValidImportFile(file)) {
|
|
144
|
+
setError('Only ZIP files (case imports) or JSON files (confirmation imports) are allowed. Please select a valid file.');
|
|
145
|
+
clearImportData();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const importType = getImportType(file);
|
|
150
|
+
updateImportState({
|
|
151
|
+
selectedFile: file,
|
|
152
|
+
importType
|
|
153
|
+
});
|
|
154
|
+
clearPreviews();
|
|
155
|
+
|
|
156
|
+
// Load preview based on import type
|
|
157
|
+
if (importType === 'case') {
|
|
158
|
+
await loadCasePreview(file);
|
|
159
|
+
} else if (importType === 'confirmation') {
|
|
160
|
+
await loadConfirmationPreview(file);
|
|
161
|
+
}
|
|
162
|
+
}, [clearMessages, clearImportData, setError, updateImportState, clearPreviews, loadCasePreview, loadConfirmationPreview]);
|
|
163
|
+
|
|
164
|
+
// Handle direct file selection (for drag and drop)
|
|
165
|
+
const handleFileSelectDirect = useCallback(async (file: File) => {
|
|
166
|
+
// Clear any existing messages when selecting a new file
|
|
167
|
+
clearMessages();
|
|
168
|
+
|
|
169
|
+
if (!isValidImportFile(file)) {
|
|
170
|
+
setError('Only ZIP files (case imports) or JSON files (confirmation imports) are allowed. Please select a valid file.');
|
|
171
|
+
clearImportData();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const importType = getImportType(file);
|
|
176
|
+
updateImportState({
|
|
177
|
+
selectedFile: file,
|
|
178
|
+
importType
|
|
179
|
+
});
|
|
180
|
+
clearPreviews();
|
|
181
|
+
|
|
182
|
+
// Load preview based on import type
|
|
183
|
+
if (importType === 'case') {
|
|
184
|
+
await loadCasePreview(file);
|
|
185
|
+
} else if (importType === 'confirmation') {
|
|
186
|
+
await loadConfirmationPreview(file);
|
|
187
|
+
}
|
|
188
|
+
}, [clearMessages, clearImportData, setError, updateImportState, clearPreviews, loadCasePreview, loadConfirmationPreview]);
|
|
189
|
+
|
|
190
|
+
// Handle import action
|
|
191
|
+
const handleImport = useCallback(() => {
|
|
192
|
+
if (!user || !importState.selectedFile || !importState.importType) return;
|
|
193
|
+
|
|
194
|
+
// For case imports, show confirmation dialog with preview
|
|
195
|
+
// For confirmation imports, proceed directly to import
|
|
196
|
+
if (importState.importType === 'case') {
|
|
197
|
+
if (!casePreview) return;
|
|
198
|
+
updateImportState({ showConfirmation: true });
|
|
199
|
+
} else {
|
|
200
|
+
// Direct import for confirmations
|
|
201
|
+
executeImport();
|
|
202
|
+
}
|
|
203
|
+
}, [user, importState.selectedFile, importState.importType, casePreview, updateImportState, executeImport]);
|
|
204
|
+
|
|
205
|
+
const handleCancelImport = useCallback(() => {
|
|
206
|
+
updateImportState({ showConfirmation: false });
|
|
207
|
+
clearImportData();
|
|
208
|
+
}, [updateImportState, clearImportData]);
|
|
209
|
+
|
|
210
|
+
const handleModalCancel = useCallback(() => {
|
|
211
|
+
clearImportData();
|
|
212
|
+
onClose();
|
|
213
|
+
}, [clearImportData, onClose]);
|
|
214
|
+
|
|
215
|
+
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
|
|
216
|
+
if (e.target === e.currentTarget && !importState.isImporting && !importState.isClearing) {
|
|
217
|
+
onClose();
|
|
218
|
+
}
|
|
219
|
+
}, [importState.isImporting, importState.isClearing, onClose]);
|
|
220
|
+
|
|
221
|
+
// Effects
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (user && isOpen) {
|
|
224
|
+
checkForExistingReadOnlyCase();
|
|
225
|
+
}
|
|
226
|
+
}, [user, isOpen, checkForExistingReadOnlyCase]);
|
|
227
|
+
|
|
228
|
+
// Handle keyboard events
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
const handleEscapeKey = (event: KeyboardEvent) => {
|
|
231
|
+
if (event.key === 'Escape' && isOpen && !importState.isImporting && !importState.isClearing) {
|
|
232
|
+
onClose();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (isOpen) {
|
|
237
|
+
document.addEventListener('keydown', handleEscapeKey);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return () => {
|
|
241
|
+
document.removeEventListener('keydown', handleEscapeKey);
|
|
242
|
+
};
|
|
243
|
+
}, [isOpen, onClose, importState.isImporting, importState.isClearing]);
|
|
244
|
+
|
|
245
|
+
// Reset state when modal closes
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (!isOpen) {
|
|
248
|
+
resetImportState();
|
|
249
|
+
}
|
|
250
|
+
}, [isOpen, resetImportState]);
|
|
251
|
+
|
|
252
|
+
// Handle confirmation import
|
|
253
|
+
const handleConfirmImport = useCallback(() => {
|
|
254
|
+
executeImport();
|
|
255
|
+
updateImportState({ showConfirmation: false });
|
|
256
|
+
}, [executeImport, updateImportState]);
|
|
257
|
+
|
|
258
|
+
if (!isOpen) return null;
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<>
|
|
262
|
+
<div className={styles.overlay} onClick={handleOverlayClick}>
|
|
263
|
+
<div className={styles.modal}>
|
|
264
|
+
<div className={styles.header}>
|
|
265
|
+
<h2 className={styles.title}>Import RO Case or Confirmations</h2>
|
|
266
|
+
<button
|
|
267
|
+
className={styles.closeButton}
|
|
268
|
+
onClick={onClose}
|
|
269
|
+
aria-label="Close modal"
|
|
270
|
+
disabled={importState.isImporting || importState.isClearing}
|
|
271
|
+
>
|
|
272
|
+
×
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div className={styles.content}>
|
|
277
|
+
<div className={styles.fieldGroup}>
|
|
278
|
+
|
|
279
|
+
{/* Existing read-only case section */}
|
|
280
|
+
<ExistingCaseSection
|
|
281
|
+
existingReadOnlyCase={existingReadOnlyCase}
|
|
282
|
+
selectedFile={importState.selectedFile}
|
|
283
|
+
onClear={clearExistingReadOnlyCase}
|
|
284
|
+
isClearing={importState.isClearing}
|
|
285
|
+
isImporting={importState.isImporting}
|
|
286
|
+
/>
|
|
287
|
+
|
|
288
|
+
{/* File selector */}
|
|
289
|
+
<FileSelector
|
|
290
|
+
selectedFile={importState.selectedFile}
|
|
291
|
+
onFileSelect={handleFileSelect}
|
|
292
|
+
onFileSelectDirect={handleFileSelectDirect}
|
|
293
|
+
isDisabled={importState.isImporting || importState.isClearing}
|
|
294
|
+
onClear={clearImportData}
|
|
295
|
+
/>
|
|
296
|
+
|
|
297
|
+
{/* Import type indicator and preview */}
|
|
298
|
+
{importState.selectedFile && importState.importType && (
|
|
299
|
+
<div className={styles.importTypeSection}>
|
|
300
|
+
<div className={styles.importTypeIndicator}>
|
|
301
|
+
<strong>Import Type:</strong> {importState.importType === 'case' ? 'Case Import' : 'Confirmation Import'}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{importState.importType === 'case' && (
|
|
305
|
+
<CasePreviewSection
|
|
306
|
+
casePreview={casePreview}
|
|
307
|
+
isLoadingPreview={importState.isLoadingPreview}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
|
|
311
|
+
{importState.importType === 'confirmation' && (
|
|
312
|
+
<ConfirmationPreviewSection
|
|
313
|
+
confirmationPreview={confirmationPreview}
|
|
314
|
+
isLoadingPreview={importState.isLoadingPreview}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
{/* Import progress */}
|
|
321
|
+
<ProgressSection importProgress={importProgress} />
|
|
322
|
+
|
|
323
|
+
{/* Hash validation warning */}
|
|
324
|
+
{casePreview?.hashValid === false && (
|
|
325
|
+
<div className={styles.hashWarning}>
|
|
326
|
+
<strong>⚠️ Import Blocked:</strong> Data hash validation failed.
|
|
327
|
+
This file may have been tampered with or corrupted and cannot be imported.
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{/* Success message */}
|
|
332
|
+
{messages.success && (
|
|
333
|
+
<div className={styles.success}>
|
|
334
|
+
{messages.success}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{/* Error message */}
|
|
339
|
+
{messages.error && (
|
|
340
|
+
<div className={styles.error}>
|
|
341
|
+
{messages.error}
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{/* Action buttons */}
|
|
346
|
+
<div className={styles.buttonGroup}>
|
|
347
|
+
<button
|
|
348
|
+
className={styles.importButton}
|
|
349
|
+
onClick={handleImport}
|
|
350
|
+
disabled={
|
|
351
|
+
!importState.selectedFile ||
|
|
352
|
+
!importState.importType ||
|
|
353
|
+
importState.isImporting ||
|
|
354
|
+
importState.isClearing ||
|
|
355
|
+
importState.isLoadingPreview ||
|
|
356
|
+
(importState.importType === 'case' && (!casePreview || casePreview.hashValid !== true))
|
|
357
|
+
}
|
|
358
|
+
>
|
|
359
|
+
{importState.isImporting ? 'Importing...' :
|
|
360
|
+
importState.importType === 'confirmation' ? 'Import Confirmations' : 'Import Case'}
|
|
361
|
+
</button>
|
|
362
|
+
|
|
363
|
+
<button
|
|
364
|
+
className={styles.cancelButton}
|
|
365
|
+
onClick={handleModalCancel}
|
|
366
|
+
disabled={importState.isImporting || importState.isClearing}
|
|
367
|
+
>
|
|
368
|
+
Cancel
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{/* Instructions */}
|
|
373
|
+
<div className={styles.instructions}>
|
|
374
|
+
<h3 className={styles.instructionsTitle}>Case Review Instructions:</h3>
|
|
375
|
+
<ul className={styles.instructionsList}>
|
|
376
|
+
<li>Only ZIP files (.zip) exported with the JSON data format from Striae are accepted</li>
|
|
377
|
+
<li>Only one case can be reviewed at a time</li>
|
|
378
|
+
<li>Imported cases are read-only and cannot be modified</li>
|
|
379
|
+
<li>Importing will automatically replace any existing review case</li>
|
|
380
|
+
</ul>
|
|
381
|
+
<br />
|
|
382
|
+
<h3 className={styles.instructionsTitle}>Confirmation Import Instructions:</h3>
|
|
383
|
+
<ul className={styles.instructionsList}>
|
|
384
|
+
<li>Only JSON files (.json) with confirmation data exported from Striae are accepted</li>
|
|
385
|
+
<li>Only one confirmation file can be imported at a time</li>
|
|
386
|
+
<li>Confirmed images will become read-only and cannot be modified</li>
|
|
387
|
+
<li>If an image has a pre-existing confirmation, it will be skipped</li>
|
|
388
|
+
</ul>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
{/* Confirmation Dialog */}
|
|
396
|
+
<ConfirmationDialog
|
|
397
|
+
showConfirmation={importState.showConfirmation}
|
|
398
|
+
casePreview={casePreview}
|
|
399
|
+
onConfirm={handleConfirmImport}
|
|
400
|
+
onCancel={handleCancelImport}
|
|
401
|
+
/>
|
|
402
|
+
</>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CaseImportPreview } from '~/types';
|
|
2
|
+
import styles from '../case-import.module.css';
|
|
3
|
+
|
|
4
|
+
interface CasePreviewSectionProps {
|
|
5
|
+
casePreview: CaseImportPreview | null;
|
|
6
|
+
isLoadingPreview: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const CasePreviewSection = ({ casePreview, isLoadingPreview }: CasePreviewSectionProps) => {
|
|
10
|
+
if (isLoadingPreview) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={styles.previewSection}>
|
|
13
|
+
<div className={styles.previewLoading}>
|
|
14
|
+
Loading case information...
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!casePreview) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{/* Case Information - Always Blue */}
|
|
25
|
+
<div className={styles.previewSection}>
|
|
26
|
+
<h3 className={styles.previewTitle}>Case Information</h3>
|
|
27
|
+
<div className={styles.previewGrid}>
|
|
28
|
+
<div className={styles.previewItem}>
|
|
29
|
+
<span className={styles.previewLabel}>Case Number:</span>
|
|
30
|
+
<span className={styles.previewValue}>{casePreview.caseNumber}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div className={styles.previewItem}>
|
|
33
|
+
<span className={styles.previewLabel}>Exported by:</span>
|
|
34
|
+
<span className={styles.previewValue}>
|
|
35
|
+
{casePreview.exportedByName || casePreview.exportedBy || 'N/A'}
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div className={styles.previewItem}>
|
|
39
|
+
<span className={styles.previewLabel}>Lab/Company:</span>
|
|
40
|
+
<span className={styles.previewValue}>{casePreview.exportedByCompany || 'N/A'}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div className={styles.previewItem}>
|
|
43
|
+
<span className={styles.previewLabel}>Export Date:</span>
|
|
44
|
+
<span className={styles.previewValue}>
|
|
45
|
+
{new Date(casePreview.exportDate).toLocaleDateString()}
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className={styles.previewItem}>
|
|
49
|
+
<span className={styles.previewLabel}>Total Images:</span>
|
|
50
|
+
<span className={styles.previewValue}>{casePreview.totalFiles}</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Data Integrity Checks - Green/Red Based on Validation */}
|
|
56
|
+
{casePreview.hashValid !== undefined && (
|
|
57
|
+
<div className={`${styles.validationSection} ${casePreview.hashValid ? styles.validationSectionValid : styles.validationSectionInvalid}`}>
|
|
58
|
+
<h3 className={styles.validationTitle}>Data Integrity Validation</h3>
|
|
59
|
+
<div className={styles.validationItem}>
|
|
60
|
+
<span className={`${styles.validationValue} ${casePreview.hashValid ? styles.validationSuccess : styles.validationError}`}>
|
|
61
|
+
{casePreview.hashValid ? (
|
|
62
|
+
<>✓ Verified (SHA-256: {casePreview.expectedHash})</>
|
|
63
|
+
) : (
|
|
64
|
+
<>✗ FAILED - {casePreview.hashError}</>
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CaseImportPreview } from '~/types';
|
|
2
|
+
import styles from '../case-import.module.css';
|
|
3
|
+
|
|
4
|
+
interface ConfirmationDialogProps {
|
|
5
|
+
showConfirmation: boolean;
|
|
6
|
+
casePreview: CaseImportPreview | null;
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ConfirmationDialog = ({
|
|
12
|
+
showConfirmation,
|
|
13
|
+
casePreview,
|
|
14
|
+
onConfirm,
|
|
15
|
+
onCancel
|
|
16
|
+
}: ConfirmationDialogProps) => {
|
|
17
|
+
if (!showConfirmation || !casePreview) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.confirmationOverlay} onClick={(e) => e.stopPropagation()}>
|
|
21
|
+
<div className={styles.confirmationModal}>
|
|
22
|
+
<div className={styles.confirmationContent}>
|
|
23
|
+
<h3 className={styles.confirmationTitle}>Confirm Case Import</h3>
|
|
24
|
+
<p className={styles.confirmationText}>
|
|
25
|
+
Are you sure you want to import this case for review?
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<div className={styles.confirmationDetails}>
|
|
29
|
+
<div className={styles.confirmationItem}>
|
|
30
|
+
<strong>Case Number:</strong> {casePreview.caseNumber}
|
|
31
|
+
</div>
|
|
32
|
+
<div className={styles.confirmationItem}>
|
|
33
|
+
<strong>Exported by:</strong> {casePreview.exportedByName || casePreview.exportedBy || 'N/A'}
|
|
34
|
+
</div>
|
|
35
|
+
<div className={styles.confirmationItem}>
|
|
36
|
+
<strong>Lab/Company:</strong> {casePreview.exportedByCompany || 'N/A'}
|
|
37
|
+
</div>
|
|
38
|
+
<div className={styles.confirmationItem}>
|
|
39
|
+
<strong>Export Date:</strong> {new Date(casePreview.exportDate).toLocaleDateString()}
|
|
40
|
+
</div>
|
|
41
|
+
<div className={styles.confirmationItem}>
|
|
42
|
+
<strong>Total Images:</strong> {casePreview.totalFiles}
|
|
43
|
+
</div>
|
|
44
|
+
{casePreview.hashValid !== undefined && (
|
|
45
|
+
<div className={`${styles.confirmationItem} ${casePreview.hashValid ? styles.confirmationItemValid : styles.confirmationItemInvalid}`}>
|
|
46
|
+
<strong>Data Integrity:</strong>
|
|
47
|
+
<span className={casePreview.hashValid ? styles.confirmationSuccess : styles.confirmationError}>
|
|
48
|
+
{casePreview.hashValid ? '✓ Verified' : '✗ Failed'}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className={styles.confirmationButtons}>
|
|
55
|
+
<button
|
|
56
|
+
className={styles.confirmButton}
|
|
57
|
+
onClick={onConfirm}
|
|
58
|
+
>
|
|
59
|
+
Confirm Import
|
|
60
|
+
</button>
|
|
61
|
+
<button
|
|
62
|
+
className={styles.cancelButton}
|
|
63
|
+
onClick={onCancel}
|
|
64
|
+
>
|
|
65
|
+
Cancel
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import styles from '../case-import.module.css';
|
|
2
|
+
|
|
3
|
+
// Confirmation preview interface
|
|
4
|
+
export interface ConfirmationPreview {
|
|
5
|
+
caseNumber: string;
|
|
6
|
+
fullName: string;
|
|
7
|
+
exportDate: string;
|
|
8
|
+
totalConfirmations: number;
|
|
9
|
+
confirmationIds: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ConfirmationPreviewSectionProps {
|
|
13
|
+
confirmationPreview: ConfirmationPreview | null;
|
|
14
|
+
isLoadingPreview: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ConfirmationPreviewSection = ({ confirmationPreview, isLoadingPreview }: ConfirmationPreviewSectionProps) => {
|
|
18
|
+
if (isLoadingPreview) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.previewSection}>
|
|
21
|
+
<div className={styles.previewLoading}>
|
|
22
|
+
Loading confirmation information...
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!confirmationPreview) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={styles.previewSection}>
|
|
32
|
+
<h3 className={styles.previewTitle}>Confirmation Data Information</h3>
|
|
33
|
+
<div className={styles.previewGrid}>
|
|
34
|
+
<div className={styles.previewItem}>
|
|
35
|
+
<span className={styles.previewLabel}>Case Number:</span>
|
|
36
|
+
<span className={styles.previewValue}>{confirmationPreview.caseNumber}</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div className={styles.previewItem}>
|
|
39
|
+
<span className={styles.previewLabel}>Exported by:</span>
|
|
40
|
+
<span className={styles.previewValue}>{confirmationPreview.fullName}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div className={styles.previewItem}>
|
|
43
|
+
<span className={styles.previewLabel}>Export Date:</span>
|
|
44
|
+
<span className={styles.previewValue}>
|
|
45
|
+
{new Date(confirmationPreview.exportDate).toLocaleDateString(undefined, {
|
|
46
|
+
year: 'numeric',
|
|
47
|
+
month: 'long',
|
|
48
|
+
day: 'numeric',
|
|
49
|
+
hour: '2-digit',
|
|
50
|
+
minute: '2-digit',
|
|
51
|
+
timeZoneName: 'short'
|
|
52
|
+
})}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className={styles.previewItem}>
|
|
56
|
+
<span className={styles.previewLabel}>Total Confirmations:</span>
|
|
57
|
+
<span className={styles.previewValue}>{confirmationPreview.totalConfirmations}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className={styles.previewItem}>
|
|
60
|
+
<span className={styles.previewLabel}>Confirmation IDs:</span>
|
|
61
|
+
<span className={styles.previewValue}>
|
|
62
|
+
{confirmationPreview.confirmationIds.length > 0
|
|
63
|
+
? confirmationPreview.confirmationIds.join(', ')
|
|
64
|
+
: 'None'
|
|
65
|
+
}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import styles from '../case-import.module.css';
|
|
2
|
+
|
|
3
|
+
interface ExistingCaseSectionProps {
|
|
4
|
+
existingReadOnlyCase: string | null;
|
|
5
|
+
selectedFile: File | null;
|
|
6
|
+
onClear: () => void;
|
|
7
|
+
isClearing: boolean;
|
|
8
|
+
isImporting: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ExistingCaseSection = ({
|
|
12
|
+
existingReadOnlyCase,
|
|
13
|
+
selectedFile,
|
|
14
|
+
onClear,
|
|
15
|
+
isClearing,
|
|
16
|
+
isImporting
|
|
17
|
+
}: ExistingCaseSectionProps) => {
|
|
18
|
+
if (!existingReadOnlyCase) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={styles.warningSection}>
|
|
22
|
+
<div className={styles.warningText}>
|
|
23
|
+
<strong>Current Review Case:</strong> "{existingReadOnlyCase}"
|
|
24
|
+
<p className={styles.warningSubtext}>
|
|
25
|
+
{selectedFile
|
|
26
|
+
? 'Importing a new case will automatically replace the existing one.'
|
|
27
|
+
: 'You can clear this case or import a new one to replace it.'
|
|
28
|
+
}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
<button
|
|
32
|
+
className={styles.clearButton}
|
|
33
|
+
onClick={onClear}
|
|
34
|
+
disabled={isClearing || isImporting}
|
|
35
|
+
>
|
|
36
|
+
{isClearing ? 'Clearing...' : 'Clear Case'}
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|