@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.
Files changed (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. 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
+ };