@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,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
|
+
};
|