@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,161 @@
1
+ import { useRef, useState, useEffect, useCallback } from 'react';
2
+ import { resetFileInput } from '../utils/file-validation';
3
+ import styles from '../case-import.module.css';
4
+
5
+ interface FileSelectorProps {
6
+ selectedFile: File | null;
7
+ onFileSelect: (event: React.ChangeEvent<HTMLInputElement>) => void;
8
+ isDisabled: boolean;
9
+ onClear?: () => void;
10
+ onFileSelectDirect?: (file: File) => void; // For drag and drop
11
+ }
12
+
13
+ export const FileSelector = ({
14
+ selectedFile,
15
+ onFileSelect,
16
+ isDisabled,
17
+ onClear,
18
+ onFileSelectDirect
19
+ }: FileSelectorProps) => {
20
+ const fileInputRef = useRef<HTMLInputElement>(null);
21
+ const isMountedRef = useRef(true);
22
+ const [isDragOver, setIsDragOver] = useState(false);
23
+
24
+ useEffect(() => {
25
+ return () => {
26
+ isMountedRef.current = false;
27
+ };
28
+ }, []);
29
+
30
+ const handleClear = () => {
31
+ resetFileInput(fileInputRef);
32
+ onClear?.();
33
+ };
34
+
35
+ const handleDragOver = useCallback((e: React.DragEvent) => {
36
+ e.preventDefault();
37
+ if (!isDisabled && isMountedRef.current) {
38
+ setIsDragOver(true);
39
+ }
40
+ }, [isDisabled]);
41
+
42
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
43
+ e.preventDefault();
44
+ const relatedTarget = e.relatedTarget as HTMLElement | null;
45
+ if (!relatedTarget || !e.currentTarget?.contains(relatedTarget)) {
46
+ if (isMountedRef.current) {
47
+ setIsDragOver(false);
48
+ }
49
+ }
50
+ }, []);
51
+
52
+ const handleDrop = useCallback((e: React.DragEvent) => {
53
+ e.preventDefault();
54
+ if (isMountedRef.current) {
55
+ setIsDragOver(false);
56
+ }
57
+ if (isDisabled || !isMountedRef.current) return;
58
+
59
+ const files = e.dataTransfer.files;
60
+ if (files.length > 0) {
61
+ const file = files[0];
62
+
63
+ // Check file type (same as input accept attribute)
64
+ const isValidType = file.name.toLowerCase().endsWith('.zip') ||
65
+ file.name.toLowerCase().endsWith('.json');
66
+
67
+ if (isValidType) {
68
+ if (onFileSelectDirect) {
69
+ onFileSelectDirect(file);
70
+ } else {
71
+ // Fallback: simulate file input change event
72
+ const dataTransfer = new DataTransfer();
73
+ dataTransfer.items.add(file);
74
+ if (fileInputRef.current) {
75
+ fileInputRef.current.files = dataTransfer.files;
76
+ const input = fileInputRef.current;
77
+ const event = new Event('change', { bubbles: true });
78
+ Object.defineProperty(event, 'target', { value: input, enumerable: true });
79
+ onFileSelect(event as unknown as React.ChangeEvent<HTMLInputElement>);
80
+ }
81
+ }
82
+ } else {
83
+ console.warn('Invalid file type dropped:', file.name);
84
+ }
85
+ }
86
+ }, [isDisabled, onFileSelectDirect, onFileSelect]);
87
+
88
+ return (
89
+ <div className={styles.fileSection}>
90
+ <div className={styles.fileInputGroup}>
91
+ <input
92
+ ref={fileInputRef}
93
+ type="file"
94
+ id="zipFile"
95
+ accept=".zip,.json"
96
+ onChange={onFileSelect}
97
+ disabled={isDisabled}
98
+ className={styles.fileInput}
99
+ aria-label="File picker for ZIP or JSON files"
100
+ />
101
+ <div
102
+ className={`${styles.fileLabel} ${isDragOver ? styles.fileLabelDragOver : ''}`}
103
+ onDragOver={handleDragOver}
104
+ onDragLeave={handleDragLeave}
105
+ onDrop={handleDrop}
106
+ onClick={() => {
107
+ if (!isDisabled) {
108
+ fileInputRef.current?.click();
109
+ }
110
+ }}
111
+ role="button"
112
+ tabIndex={isDisabled ? -1 : 0}
113
+ aria-disabled={isDisabled}
114
+ aria-label="File selection area. Drag and drop a ZIP file for case import or JSON file for confirmation import."
115
+ onKeyDown={(e) => {
116
+ if ((e.key === 'Enter' || e.key === ' ') && !isDisabled) {
117
+ if (e.key === ' ') {
118
+ e.preventDefault();
119
+ }
120
+ fileInputRef.current?.click();
121
+ }
122
+ }}
123
+ >
124
+ <div className={styles.fileLabelContent}>
125
+ <span className={styles.fileLabelIcon}>📁</span>
126
+ <span className={styles.fileLabelText}>
127
+ {selectedFile
128
+ ? selectedFile.name
129
+ : isDragOver
130
+ ? 'Drop file here...'
131
+ : 'Select ZIP or JSON file... or drag & drop'
132
+ }
133
+ </span>
134
+ </div>
135
+ </div>
136
+
137
+ {/* Clear button positioned in upper right corner */}
138
+ {selectedFile && onClear && (
139
+ <button
140
+ type="button"
141
+ onClick={handleClear}
142
+ className={styles.clearFileButton}
143
+ disabled={isDisabled}
144
+ title="Clear selected file"
145
+ aria-label="Clear selected file"
146
+ >
147
+ ×
148
+ </button>
149
+ )}
150
+ </div>
151
+
152
+ {selectedFile && (
153
+ <div className={styles.fileInfo}>
154
+ <span className={styles.fileSize}>
155
+ {(selectedFile.size / (1024 * 1024)).toFixed(2)} MB
156
+ </span>
157
+ </div>
158
+ )}
159
+ </div>
160
+ );
161
+ };
@@ -0,0 +1,46 @@
1
+ import styles from '../case-import.module.css';
2
+
3
+ interface ProgressState {
4
+ stage: string;
5
+ progress: number;
6
+ details?: string;
7
+ }
8
+
9
+ interface ProgressSectionProps {
10
+ importProgress: ProgressState | null;
11
+ }
12
+
13
+ export const ProgressSection = ({ importProgress }: ProgressSectionProps) => {
14
+ if (!importProgress) return null;
15
+
16
+ const progressPercent = Math.round(importProgress.progress);
17
+ const isComplete = importProgress.progress >= 100;
18
+
19
+ return (
20
+ <div className={styles.progressSection} role="region" aria-live="polite" aria-label="Import progress">
21
+ <div className={styles.progressText}>
22
+ {importProgress.stage}
23
+ {importProgress.details && (
24
+ <span className={styles.progressDetails}> - {importProgress.details}</span>
25
+ )}
26
+ </div>
27
+ <div
28
+ className={styles.progressBar}
29
+ role="progressbar"
30
+ aria-valuemin={0}
31
+ aria-valuemax={100}
32
+ aria-valuenow={importProgress.progress}
33
+ aria-valuetext={`${progressPercent}% complete${isComplete ? ' - Import finished' : ''}`}
34
+ aria-label="Case import progress"
35
+ >
36
+ <div
37
+ className={styles.progressFill}
38
+ style={{ width: `${importProgress.progress}%` }}
39
+ />
40
+ </div>
41
+ <div className={styles.progressPercent}>
42
+ {progressPercent}%
43
+ </div>
44
+ </div>
45
+ );
46
+ };
@@ -0,0 +1,101 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { User } from 'firebase/auth';
3
+ import { previewCaseImport } from '~/components/actions/case-review';
4
+ import { CaseImportPreview } from '~/types';
5
+ import { ConfirmationPreview } from '../components/ConfirmationPreviewSection';
6
+
7
+ interface UseFilePreviewReturn {
8
+ casePreview: CaseImportPreview | null;
9
+ confirmationPreview: ConfirmationPreview | null;
10
+ loadCasePreview: (file: File) => Promise<void>;
11
+ loadConfirmationPreview: (file: File) => Promise<void>;
12
+ clearPreviews: () => void;
13
+ }
14
+
15
+ /**
16
+ * Custom hook for handling file preview loading
17
+ */
18
+ export const useFilePreview = (
19
+ user: User | null,
20
+ setError: (error: string) => void,
21
+ setIsLoadingPreview: (loading: boolean) => void,
22
+ clearImportData: () => void
23
+ ): UseFilePreviewReturn => {
24
+ const [casePreview, setCasePreview] = useState<CaseImportPreview | null>(null);
25
+ const [confirmationPreview, setConfirmationPreview] = useState<ConfirmationPreview | null>(null);
26
+
27
+ const loadCasePreview = useCallback(async (file: File) => {
28
+ if (!user) {
29
+ setError('User authentication required');
30
+ return;
31
+ }
32
+
33
+ setIsLoadingPreview(true);
34
+ try {
35
+ const preview = await previewCaseImport(file, user);
36
+ setCasePreview(preview);
37
+ } catch (error) {
38
+ console.error('Error loading case preview:', error);
39
+ setError(`Failed to read case information: ${error instanceof Error ? error.message : 'Unknown error'}`);
40
+ clearImportData();
41
+ } finally {
42
+ setIsLoadingPreview(false);
43
+ }
44
+ }, [user, setError, setIsLoadingPreview, clearImportData]);
45
+
46
+ const loadConfirmationPreview = useCallback(async (file: File) => {
47
+ if (!user) {
48
+ setError('User authentication required');
49
+ return;
50
+ }
51
+
52
+ setIsLoadingPreview(true);
53
+ try {
54
+ const text = await file.text();
55
+ const data = JSON.parse(text);
56
+
57
+ // Extract confirmation IDs from the confirmations object
58
+ const confirmationIds: string[] = [];
59
+ if (data.confirmations) {
60
+ Object.values(data.confirmations).forEach((imageConfirmations: any) => {
61
+ if (Array.isArray(imageConfirmations)) {
62
+ imageConfirmations.forEach((confirmation: any) => {
63
+ if (confirmation.confirmationId) {
64
+ confirmationIds.push(confirmation.confirmationId);
65
+ }
66
+ });
67
+ }
68
+ });
69
+ }
70
+
71
+ const preview: ConfirmationPreview = {
72
+ caseNumber: data.metadata?.caseNumber || 'Unknown',
73
+ fullName: data.metadata?.exportedByName || 'Unknown',
74
+ exportDate: data.metadata?.exportDate || new Date().toISOString(),
75
+ totalConfirmations: data.metadata?.totalConfirmations || confirmationIds.length,
76
+ confirmationIds
77
+ };
78
+
79
+ setConfirmationPreview(preview);
80
+ } catch (error) {
81
+ console.error('Error loading confirmation preview:', error);
82
+ setError(`Failed to read confirmation data: ${error instanceof Error ? error.message : 'Invalid JSON format'}`);
83
+ clearImportData();
84
+ } finally {
85
+ setIsLoadingPreview(false);
86
+ }
87
+ }, [user, setError, setIsLoadingPreview, clearImportData]);
88
+
89
+ const clearPreviews = useCallback(() => {
90
+ setCasePreview(null);
91
+ setConfirmationPreview(null);
92
+ }, []);
93
+
94
+ return {
95
+ casePreview,
96
+ confirmationPreview,
97
+ loadCasePreview,
98
+ loadConfirmationPreview,
99
+ clearPreviews
100
+ };
101
+ };
@@ -0,0 +1,152 @@
1
+ import { useCallback } from 'react';
2
+ import { User } from 'firebase/auth';
3
+ import { importCaseForReview, importConfirmationData } from '~/components/actions/case-review';
4
+ import { ImportResult, ConfirmationImportResult } from '~/types';
5
+
6
+ interface ProgressState {
7
+ stage: string;
8
+ progress: number;
9
+ details?: string;
10
+ }
11
+
12
+ interface UseImportExecutionProps {
13
+ user: User | null;
14
+ selectedFile: File | null;
15
+ importType: 'case' | 'confirmation' | null;
16
+ setImportProgress: (progress: ProgressState | null) => void;
17
+ clearMessages: () => void;
18
+ setError: (error: string) => void;
19
+ setSuccess: (success: string) => void;
20
+ setIsImporting: (importing: boolean) => void;
21
+ onImportComplete?: (result: ImportResult | ConfirmationImportResult) => void;
22
+ onUpdateExistingCase: (caseNumber: string) => void;
23
+ onClose: () => void;
24
+ }
25
+
26
+ /**
27
+ * Custom hook for handling import execution logic
28
+ */
29
+ export const useImportExecution = ({
30
+ user,
31
+ selectedFile,
32
+ importType,
33
+ setImportProgress,
34
+ clearMessages,
35
+ setError,
36
+ setSuccess,
37
+ setIsImporting,
38
+ onImportComplete,
39
+ onUpdateExistingCase,
40
+ onClose
41
+ }: UseImportExecutionProps) => {
42
+
43
+ const executeImport = useCallback(async () => {
44
+ if (!user || !selectedFile || !importType) return;
45
+
46
+ // Clear any existing success and error messages when starting a new import
47
+ clearMessages();
48
+ setIsImporting(true);
49
+
50
+ try {
51
+ if (importType === 'case') {
52
+ // Handle case import
53
+ setImportProgress({ stage: 'Starting case import...', progress: 0 });
54
+
55
+ // Track if we're in cleanup phase to trigger UI reset
56
+ let hasTriggeredCleanupReset = false;
57
+
58
+ const result = await importCaseForReview(
59
+ user,
60
+ selectedFile,
61
+ { overwriteExisting: true },
62
+ (stage: string, progress: number, details?: string) => {
63
+ setImportProgress({ stage, progress, details });
64
+
65
+ // Detect cleanup phase and trigger UI reset once
66
+ if (!hasTriggeredCleanupReset &&
67
+ (stage.toLowerCase().includes('checking existing read-only cases') ||
68
+ stage.toLowerCase().includes('cleaning up existing case') ||
69
+ details?.toLowerCase().includes('cleaning up previous imports') ||
70
+ details?.toLowerCase().includes('removing existing case data'))) {
71
+ hasTriggeredCleanupReset = true;
72
+
73
+ // Trigger immediate UI reset during cleanup phase
74
+ onImportComplete?.({
75
+ success: true,
76
+ caseNumber: '',
77
+ isReadOnly: false,
78
+ filesImported: 0,
79
+ annotationsImported: 0,
80
+ errors: [],
81
+ warnings: []
82
+ });
83
+ }
84
+ }
85
+ );
86
+
87
+ if (result.success) {
88
+ setSuccess(`Successfully imported case "${result.caseNumber}" for review`);
89
+
90
+ // Update existing case status
91
+ onUpdateExistingCase(result.caseNumber);
92
+
93
+ // Call completion callback
94
+ onImportComplete?.(result);
95
+
96
+ // Auto-close after success
97
+ setTimeout(() => {
98
+ onClose();
99
+ }, 2000);
100
+
101
+ } else {
102
+ setError(result.errors?.join(', ') || 'Case import failed');
103
+ }
104
+
105
+ } else if (importType === 'confirmation') {
106
+ // Handle confirmation import
107
+ setImportProgress({ stage: 'Validating confirmation data...', progress: 50 });
108
+
109
+ const result = await importConfirmationData(user, selectedFile);
110
+
111
+ if (result.success) {
112
+ let message = `Successfully imported ${result.confirmationsImported} confirmation(s) for case "${result.caseNumber}"`;
113
+
114
+ // If there were blocked confirmations, include that information
115
+ if (result.errors && result.errors.length > 0) {
116
+ message += `\n\n${result.errors.length} confirmation(s) were blocked:\n\n${result.errors.join('\n\n')}`;
117
+ }
118
+
119
+ setSuccess(message);
120
+
121
+ // No auto-close for confirmation imports - let user read the details and close manually
122
+
123
+ } else {
124
+ setError(result.errors?.join(', ') || 'Confirmation import failed');
125
+ }
126
+ }
127
+
128
+ } catch (error) {
129
+ console.error('Import failed:', error);
130
+ setError(error instanceof Error ? error.message : 'Import failed. Please try again.');
131
+ } finally {
132
+ setIsImporting(false);
133
+ setImportProgress(null);
134
+ }
135
+ }, [
136
+ user,
137
+ selectedFile,
138
+ importType,
139
+ setImportProgress,
140
+ clearMessages,
141
+ setError,
142
+ setSuccess,
143
+ setIsImporting,
144
+ onImportComplete,
145
+ onUpdateExistingCase,
146
+ onClose
147
+ ]);
148
+
149
+ return {
150
+ executeImport
151
+ };
152
+ };
@@ -0,0 +1,88 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ // State interfaces
4
+ interface ImportState {
5
+ selectedFile: File | null;
6
+ isImporting: boolean;
7
+ isClearing: boolean;
8
+ isLoadingPreview: boolean;
9
+ showConfirmation: boolean;
10
+ importType: 'case' | 'confirmation' | null;
11
+ }
12
+
13
+ interface MessageState {
14
+ error: string;
15
+ success: string;
16
+ }
17
+
18
+ interface ProgressState {
19
+ stage: string;
20
+ progress: number;
21
+ details?: string;
22
+ }
23
+
24
+ /**
25
+ * Custom hook for managing import-related state
26
+ */
27
+ export const useImportState = () => {
28
+ const [importState, setImportState] = useState<ImportState>({
29
+ selectedFile: null,
30
+ isImporting: false,
31
+ isClearing: false,
32
+ isLoadingPreview: false,
33
+ showConfirmation: false,
34
+ importType: null
35
+ });
36
+
37
+ const [messages, setMessages] = useState<MessageState>({
38
+ error: '',
39
+ success: ''
40
+ });
41
+
42
+ const [importProgress, setImportProgress] = useState<ProgressState | null>(null);
43
+
44
+ // Helper functions
45
+ const clearMessages = useCallback(() => {
46
+ setMessages({ error: '', success: '' });
47
+ }, []);
48
+
49
+ const setError = useCallback((error: string) => {
50
+ setMessages({ error, success: '' });
51
+ }, []);
52
+
53
+ const setSuccess = useCallback((success: string) => {
54
+ setMessages({ error: '', success });
55
+ }, []);
56
+
57
+ const updateImportState = useCallback((updates: Partial<ImportState>) => {
58
+ setImportState(prev => ({ ...prev, ...updates }));
59
+ }, []);
60
+
61
+ const resetImportState = useCallback(() => {
62
+ setImportState({
63
+ selectedFile: null,
64
+ isImporting: false,
65
+ isClearing: false,
66
+ isLoadingPreview: false,
67
+ showConfirmation: false,
68
+ importType: null
69
+ });
70
+ clearMessages();
71
+ setImportProgress(null);
72
+ }, [clearMessages]);
73
+
74
+ return {
75
+ // State
76
+ importState,
77
+ messages,
78
+ importProgress,
79
+
80
+ // Actions
81
+ clearMessages,
82
+ setError,
83
+ setSuccess,
84
+ updateImportState,
85
+ resetImportState,
86
+ setImportProgress
87
+ };
88
+ };
@@ -0,0 +1,18 @@
1
+ // Components
2
+ export { FileSelector } from './components/FileSelector';
3
+ export { CasePreviewSection } from './components/CasePreviewSection';
4
+ export { ConfirmationPreviewSection } from './components/ConfirmationPreviewSection';
5
+ export { ProgressSection } from './components/ProgressSection';
6
+ export { ExistingCaseSection } from './components/ExistingCaseSection';
7
+ export { ConfirmationDialog } from './components/ConfirmationDialog';
8
+
9
+ // Hooks
10
+ export { useImportState } from './hooks/useImportState';
11
+ export { useFilePreview } from './hooks/useFilePreview';
12
+ export { useImportExecution } from './hooks/useImportExecution';
13
+
14
+ // Utils
15
+ export * from './utils/file-validation';
16
+
17
+ // Types
18
+ export type { ConfirmationPreview } from './components/ConfirmationPreviewSection';
@@ -0,0 +1,43 @@
1
+ import { isConfirmationDataFile } from '~/components/actions/case-review';
2
+
3
+ /**
4
+ * Check if a file is a valid ZIP file
5
+ */
6
+ export const isValidZipFile = (file: File): boolean => {
7
+ return file.type === 'application/zip' ||
8
+ file.type === 'application/x-zip-compressed' ||
9
+ file.name.toLowerCase().endsWith('.zip');
10
+ };
11
+
12
+ /**
13
+ * Check if a file is a valid confirmation JSON file
14
+ */
15
+ export const isValidConfirmationFile = (file: File): boolean => {
16
+ return file.type === 'application/json' &&
17
+ isConfirmationDataFile(file.name);
18
+ };
19
+
20
+ /**
21
+ * Check if a file is valid for import (either ZIP or confirmation JSON)
22
+ */
23
+ export const isValidImportFile = (file: File): boolean => {
24
+ return isValidZipFile(file) || isValidConfirmationFile(file);
25
+ };
26
+
27
+ /**
28
+ * Get import type based on file
29
+ */
30
+ export const getImportType = (file: File): 'case' | 'confirmation' | null => {
31
+ if (isValidZipFile(file)) return 'case';
32
+ if (isValidConfirmationFile(file)) return 'confirmation';
33
+ return null;
34
+ };
35
+
36
+ /**
37
+ * Reset file input element
38
+ */
39
+ export const resetFileInput = (ref: React.RefObject<HTMLInputElement | null>): void => {
40
+ if (ref.current) {
41
+ ref.current.value = '';
42
+ }
43
+ };