@striae-org/striae 4.0.3 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +430 -8
- package/app/components/actions/confirm-export.ts +13 -4
- package/app/components/actions/generate-pdf.ts +10 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +137 -945
- package/app/components/audit/user-audit.module.css +41 -0
- package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +307 -0
- package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
- package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +123 -0
- package/app/components/audit/viewer/types.ts +1 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -0
- package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
- package/app/components/auth/mfa-enrollment.module.css +13 -5
- package/app/components/auth/mfa-verification.module.css +13 -5
- package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +17 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +17 -47
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +377 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +2 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +14 -77
- package/app/components/sidebar/case-import/case-import.module.css +25 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -40
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
- package/app/components/sidebar/cases/cases-modal.module.css +45 -9
- package/app/components/sidebar/cases/cases-modal.tsx +16 -16
- package/app/components/sidebar/cases/cases.module.css +62 -21
- package/app/components/sidebar/files/files-modal.module.css +46 -10
- package/app/components/sidebar/files/files-modal.tsx +22 -23
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
- package/app/components/sidebar/notes/notes-modal.tsx +18 -17
- package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
- package/app/components/sidebar/notes/notes.module.css +155 -0
- package/app/components/sidebar/sidebar-container.tsx +15 -28
- package/app/components/sidebar/sidebar.module.css +7 -71
- package/app/components/sidebar/sidebar.tsx +24 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/toast/toast.module.css +2 -1
- package/app/components/toast/toast.tsx +16 -11
- package/app/components/user/delete-account.tsx +10 -31
- package/app/components/user/inactivity-warning.module.css +9 -6
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.module.css +2 -0
- package/app/components/user/manage-profile.tsx +108 -40
- package/app/hooks/useOverlayDismiss.ts +116 -0
- package/app/routes/auth/login.example.tsx +19 -8
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/auth/passwordReset.module.css +23 -13
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +477 -31
- package/app/routes.ts +7 -0
- package/app/services/audit/audit-export-csv.ts +2 -0
- package/app/services/audit/audit.service.ts +202 -32
- package/app/services/audit/builders/audit-entry-builder.ts +2 -1
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +5 -2
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/types/user.ts +1 -0
- package/app/utils/data/permissions.ts +17 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +497 -22
- package/functions/api/pdf/[[path]].ts +32 -1
- package/load-context.ts +9 -0
- package/package.json +6 -2
- package/primershear.emails.example +6 -0
- package/scripts/deploy-pages-secrets.sh +6 -0
- package/scripts/deploy-primershear-emails.sh +167 -0
- package/worker-configuration.d.ts +7493 -7491
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
- package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
- package/workers/pdf-worker/src/report-types.ts +3 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/user-worker.example.ts +6 -1
- package/workers/user-worker/worker-configuration.d.ts +7448 -11323
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/public/.well-known/keybase.txt +0 -56
|
@@ -1,53 +1,27 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import type * as CaseExportActions from '../../actions/case-export';
|
|
3
2
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
3
|
import styles from './cases.module.css';
|
|
5
|
-
import { Toast } from '~/components/toast/toast';
|
|
6
|
-
import { CasesModal } from './cases-modal';
|
|
7
4
|
import { FilesModal } from '../files/files-modal';
|
|
8
|
-
import { CaseExport, type ExportFormat } from '../case-export/case-export';
|
|
9
5
|
import { ImageUploadZone } from '../upload/image-upload-zone';
|
|
10
|
-
import { UserAuditViewer } from '~/components/audit/user-audit-viewer';
|
|
11
|
-
import {
|
|
12
|
-
validateCaseNumber,
|
|
13
|
-
checkExistingCase,
|
|
14
|
-
createNewCase,
|
|
15
|
-
renameCase,
|
|
16
|
-
deleteCase,
|
|
17
|
-
} from '../../actions/case-manage';
|
|
18
6
|
import {
|
|
19
7
|
fetchFiles,
|
|
20
8
|
deleteFile,
|
|
21
9
|
} from '../../actions/image-manage';
|
|
22
10
|
import {
|
|
23
|
-
checkReadOnlyCaseExists
|
|
24
|
-
} from '../../actions/case-review';
|
|
25
|
-
import {
|
|
26
|
-
canCreateCase,
|
|
27
11
|
canUploadFile,
|
|
28
|
-
getLimitsDescription,
|
|
29
|
-
getUserData,
|
|
30
12
|
getFileAnnotations
|
|
31
13
|
} from '~/utils/data';
|
|
32
|
-
import { type FileData
|
|
14
|
+
import { type FileData } from '~/types';
|
|
33
15
|
|
|
34
16
|
interface CaseSidebarProps {
|
|
35
17
|
user: User;
|
|
36
18
|
onImageSelect: (file: FileData) => void;
|
|
37
|
-
onCaseChange: (caseNumber: string) => void;
|
|
38
19
|
imageLoaded: boolean;
|
|
39
20
|
setImageLoaded: (loaded: boolean) => void;
|
|
40
21
|
onNotesClick: () => void;
|
|
41
22
|
files: FileData[];
|
|
42
23
|
setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
|
|
43
|
-
caseNumber: string;
|
|
44
|
-
setCaseNumber: (caseNumber: string) => void;
|
|
45
24
|
currentCase: string | null;
|
|
46
|
-
setCurrentCase: (caseNumber: string) => void;
|
|
47
|
-
error: string;
|
|
48
|
-
setError: (error: string) => void;
|
|
49
|
-
successAction: CaseActionType;
|
|
50
|
-
setSuccessAction: (action: CaseActionType) => void;
|
|
51
25
|
isReadOnly?: boolean;
|
|
52
26
|
isConfirmed?: boolean;
|
|
53
27
|
confirmationSaveVersion?: number;
|
|
@@ -57,37 +31,15 @@ interface CaseSidebarProps {
|
|
|
57
31
|
onUploadComplete?: (result: { successCount: number; failedFiles: string[] }) => void;
|
|
58
32
|
}
|
|
59
33
|
|
|
60
|
-
const SUCCESS_MESSAGE_TIMEOUT = 3000;
|
|
61
|
-
|
|
62
|
-
type CaseExportActionsModule = typeof CaseExportActions;
|
|
63
|
-
|
|
64
|
-
let caseExportActionsPromise: Promise<CaseExportActionsModule> | null = null;
|
|
65
|
-
|
|
66
|
-
const loadCaseExportActions = (): Promise<CaseExportActionsModule> => {
|
|
67
|
-
if (!caseExportActionsPromise) {
|
|
68
|
-
caseExportActionsPromise = import('../../actions/case-export');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return caseExportActionsPromise;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
34
|
export const CaseSidebar = ({
|
|
75
35
|
user,
|
|
76
36
|
onImageSelect,
|
|
77
|
-
onCaseChange,
|
|
78
37
|
imageLoaded,
|
|
79
38
|
setImageLoaded,
|
|
80
39
|
onNotesClick,
|
|
81
40
|
files,
|
|
82
41
|
setFiles,
|
|
83
|
-
caseNumber,
|
|
84
|
-
setCaseNumber,
|
|
85
42
|
currentCase,
|
|
86
|
-
setCurrentCase,
|
|
87
|
-
error,
|
|
88
|
-
setError,
|
|
89
|
-
successAction,
|
|
90
|
-
setSuccessAction,
|
|
91
43
|
isReadOnly = false,
|
|
92
44
|
isConfirmed = false,
|
|
93
45
|
confirmationSaveVersion = 0,
|
|
@@ -97,27 +49,11 @@ export const CaseSidebar = ({
|
|
|
97
49
|
onUploadComplete
|
|
98
50
|
}: CaseSidebarProps) => {
|
|
99
51
|
|
|
100
|
-
const [isDeletingCase, setIsDeletingCase] = useState(false);
|
|
101
|
-
const [isRenaming, setIsRenaming] = useState(false);
|
|
102
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
103
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
104
52
|
const [, setFileError] = useState('');
|
|
105
|
-
const [newCaseName, setNewCaseName] = useState('');
|
|
106
|
-
const [showCaseActions, setShowCaseActions] = useState(false);
|
|
107
|
-
const [showCaseManagement, setShowCaseManagement] = useState(false);
|
|
108
|
-
const [canCreateNewCase, setCanCreateNewCase] = useState(true);
|
|
109
|
-
const [isToastVisible, setIsToastVisible] = useState(false);
|
|
110
|
-
const [toastMessage, setToastMessage] = useState('');
|
|
111
|
-
const [toastType, setToastType] = useState<'success' | 'error' | 'warning'>('success');
|
|
112
53
|
const [canUploadNewFile, setCanUploadNewFile] = useState(true);
|
|
113
|
-
const [createCaseError, setCreateCaseError] = useState('');
|
|
114
54
|
const [uploadFileError, setUploadFileError] = useState('');
|
|
115
|
-
const [limitsDescription, setLimitsDescription] = useState('');
|
|
116
|
-
const [permissionChecking, setPermissionChecking] = useState(false);
|
|
117
|
-
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
|
118
55
|
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
|
119
56
|
const [deletingFileId, setDeletingFileId] = useState<string | null>(null);
|
|
120
|
-
const [isAuditTrailOpen, setIsAuditTrailOpen] = useState(false);
|
|
121
57
|
const [fileConfirmationStatus, setFileConfirmationStatus] = useState<{
|
|
122
58
|
[fileId: string]: { includeConfirmation: boolean; isConfirmed: boolean }
|
|
123
59
|
}>({});
|
|
@@ -146,30 +82,6 @@ export const CaseSidebar = ({
|
|
|
146
82
|
};
|
|
147
83
|
}, [files]);
|
|
148
84
|
|
|
149
|
-
// Function to check user permissions (extracted for reuse)
|
|
150
|
-
const checkUserPermissions = useCallback(async () => {
|
|
151
|
-
setPermissionChecking(true);
|
|
152
|
-
try {
|
|
153
|
-
const casePermission = await canCreateCase(user);
|
|
154
|
-
setCanCreateNewCase(casePermission.canCreate);
|
|
155
|
-
setCreateCaseError(casePermission.reason || '');
|
|
156
|
-
|
|
157
|
-
// Only show limits description for restricted accounts
|
|
158
|
-
const userData = await getUserData(user);
|
|
159
|
-
if (userData && !userData.permitted) {
|
|
160
|
-
const description = await getLimitsDescription(user);
|
|
161
|
-
setLimitsDescription(description);
|
|
162
|
-
} else {
|
|
163
|
-
setLimitsDescription(''); // Clear the description for permitted users
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
console.error('Error checking user permissions:', error);
|
|
167
|
-
setCreateCaseError('Unable to verify account permissions');
|
|
168
|
-
} finally {
|
|
169
|
-
setPermissionChecking(false);
|
|
170
|
-
}
|
|
171
|
-
}, [user]);
|
|
172
|
-
|
|
173
85
|
// Function to check file upload permissions (extracted for reuse)
|
|
174
86
|
const checkFileUploadPermissions = useCallback(async (fileCount?: number) => {
|
|
175
87
|
if (currentCase) {
|
|
@@ -190,11 +102,6 @@ export const CaseSidebar = ({
|
|
|
190
102
|
}
|
|
191
103
|
}, [currentCase, files.length, user]);
|
|
192
104
|
|
|
193
|
-
// Check user permissions on mount and when user changes
|
|
194
|
-
useEffect(() => {
|
|
195
|
-
checkUserPermissions();
|
|
196
|
-
}, [checkUserPermissions]);
|
|
197
|
-
|
|
198
105
|
// Check file upload permissions when currentCase or files change
|
|
199
106
|
useEffect(() => {
|
|
200
107
|
checkFileUploadPermissions();
|
|
@@ -202,7 +109,6 @@ export const CaseSidebar = ({
|
|
|
202
109
|
|
|
203
110
|
useEffect(() => {
|
|
204
111
|
if (currentCase) {
|
|
205
|
-
setIsLoading(true);
|
|
206
112
|
fetchFiles(user, currentCase, { skipValidation: true })
|
|
207
113
|
.then(loadedFiles => {
|
|
208
114
|
setFiles(loadedFiles);
|
|
@@ -210,9 +116,6 @@ export const CaseSidebar = ({
|
|
|
210
116
|
.catch(err => {
|
|
211
117
|
console.error('Failed to load files:', err);
|
|
212
118
|
setFileError(err instanceof Error ? err.message : 'Failed to load files');
|
|
213
|
-
})
|
|
214
|
-
.finally(() => {
|
|
215
|
-
setIsLoading(false);
|
|
216
119
|
});
|
|
217
120
|
} else {
|
|
218
121
|
setFiles([]);
|
|
@@ -319,89 +222,6 @@ export const CaseSidebar = ({
|
|
|
319
222
|
};
|
|
320
223
|
}, [currentCase, fileIdsKey, user, selectedFileId, confirmationSaveVersion, files.length, calculateCaseConfirmationStatus]);
|
|
321
224
|
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
if (error) {
|
|
324
|
-
setToastMessage(error);
|
|
325
|
-
setToastType('error');
|
|
326
|
-
setIsToastVisible(true);
|
|
327
|
-
}
|
|
328
|
-
}, [error]);
|
|
329
|
-
|
|
330
|
-
useEffect(() => {
|
|
331
|
-
if (successAction) {
|
|
332
|
-
setToastMessage(`Case ${currentCase} ${successAction} successfully!`);
|
|
333
|
-
setToastType('success');
|
|
334
|
-
setIsToastVisible(true);
|
|
335
|
-
}
|
|
336
|
-
// currentCase intentionally omitted: we capture its value at the time successAction changes
|
|
337
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
338
|
-
}, [successAction]);
|
|
339
|
-
|
|
340
|
-
const handleCase = async () => {
|
|
341
|
-
setIsLoading(true);
|
|
342
|
-
setError('');
|
|
343
|
-
setCreateCaseError(''); // Clear permission errors when starting new operation
|
|
344
|
-
|
|
345
|
-
if (!validateCaseNumber(caseNumber)) {
|
|
346
|
-
setError('Invalid case number format');
|
|
347
|
-
setIsLoading(false);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
const existingCase = await checkExistingCase(user, caseNumber);
|
|
353
|
-
|
|
354
|
-
if (existingCase) {
|
|
355
|
-
// Loading existing case - always allowed
|
|
356
|
-
setCurrentCase(caseNumber);
|
|
357
|
-
onCaseChange(caseNumber);
|
|
358
|
-
const files = await fetchFiles(user, caseNumber, { skipValidation: true });
|
|
359
|
-
setFiles(files);
|
|
360
|
-
setCaseNumber('');
|
|
361
|
-
setSuccessAction('loaded');
|
|
362
|
-
setShowCaseManagement(false);
|
|
363
|
-
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Check if a read-only case with this number exists
|
|
368
|
-
const existingReadOnlyCase = await checkReadOnlyCaseExists(user, caseNumber);
|
|
369
|
-
if (existingReadOnlyCase) {
|
|
370
|
-
setError(`Case "${caseNumber}" already exists as a read-only review case. You cannot create a case with the same number.`);
|
|
371
|
-
setIsLoading(false);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Creating new case - check permissions
|
|
376
|
-
if (!canCreateNewCase) {
|
|
377
|
-
setError(createCaseError || 'You cannot create more cases.');
|
|
378
|
-
setCreateCaseError(''); // Clear duplicate error
|
|
379
|
-
setIsLoading(false);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const newCase = await createNewCase(user, caseNumber);
|
|
384
|
-
setCurrentCase(newCase.caseNumber);
|
|
385
|
-
onCaseChange(newCase.caseNumber);
|
|
386
|
-
setFiles([]);
|
|
387
|
-
setCaseNumber('');
|
|
388
|
-
setSuccessAction('created');
|
|
389
|
-
setShowCaseManagement(false);
|
|
390
|
-
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
391
|
-
|
|
392
|
-
// Refresh permissions after successful case creation
|
|
393
|
-
// This updates the UI for users with limited permissions
|
|
394
|
-
await checkUserPermissions();
|
|
395
|
-
} catch (err) {
|
|
396
|
-
setError(err instanceof Error ? err.message : 'Failed to load/create case');
|
|
397
|
-
console.error(err);
|
|
398
|
-
} finally {
|
|
399
|
-
setIsLoading(false);
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
225
|
const handleFileDelete = async (fileId: string) => {
|
|
406
226
|
// Don't allow file deletion for read-only cases
|
|
407
227
|
if (isReadOnly) {
|
|
@@ -414,11 +234,15 @@ export const CaseSidebar = ({
|
|
|
414
234
|
setDeletingFileId(fileId);
|
|
415
235
|
|
|
416
236
|
try {
|
|
417
|
-
await deleteFile(user, currentCase, fileId);
|
|
237
|
+
const deleteResult = await deleteFile(user, currentCase, fileId);
|
|
418
238
|
const updatedFiles = files.filter(f => f.id !== fileId);
|
|
419
239
|
setFiles(updatedFiles);
|
|
420
240
|
onImageSelect({ id: 'clear', originalFilename: '/clear.jpg', uploadedAt: '' });
|
|
421
241
|
setImageLoaded(false);
|
|
242
|
+
|
|
243
|
+
if (deleteResult.imageMissing) {
|
|
244
|
+
setFileError(`File record deleted. Image asset "${deleteResult.fileName}" was not found and was skipped.`);
|
|
245
|
+
}
|
|
422
246
|
|
|
423
247
|
// Refresh file upload permissions after successful file deletion
|
|
424
248
|
// Pass the new file count directly to avoid state update timing issues
|
|
@@ -430,79 +254,6 @@ export const CaseSidebar = ({
|
|
|
430
254
|
}
|
|
431
255
|
};
|
|
432
256
|
|
|
433
|
-
const handleRenameCase = async () => {
|
|
434
|
-
// Don't allow renaming read-only cases
|
|
435
|
-
if (isReadOnly) {
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (!currentCase || !newCaseName) return;
|
|
440
|
-
|
|
441
|
-
if (!validateCaseNumber(newCaseName)) {
|
|
442
|
-
setError('Invalid new case number format');
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
setIsRenaming(true);
|
|
447
|
-
setError('');
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
// Check if a read-only case with the new name exists
|
|
451
|
-
const existingReadOnlyCase = await checkReadOnlyCaseExists(user, newCaseName);
|
|
452
|
-
if (existingReadOnlyCase) {
|
|
453
|
-
setError(`Case "${newCaseName}" already exists as a read-only review case. You cannot rename to this case number.`);
|
|
454
|
-
setIsRenaming(false);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
await renameCase(user, currentCase, newCaseName);
|
|
459
|
-
setCurrentCase(newCaseName);
|
|
460
|
-
onCaseChange(newCaseName);
|
|
461
|
-
setNewCaseName('');
|
|
462
|
-
setSuccessAction('loaded');
|
|
463
|
-
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
464
|
-
} catch (err) {
|
|
465
|
-
setError(err instanceof Error ? err.message : 'Failed to rename case');
|
|
466
|
-
} finally {
|
|
467
|
-
setIsRenaming(false);
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const handleDeleteCase = async () => {
|
|
472
|
-
// Don't allow deleting read-only cases
|
|
473
|
-
if (isReadOnly) {
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (!currentCase) return;
|
|
478
|
-
|
|
479
|
-
const confirmed = window.confirm(
|
|
480
|
-
`Are you sure you want to delete case ${currentCase}? This will permanently delete all associated files and cannot be undone.`
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
if (!confirmed) return;
|
|
484
|
-
|
|
485
|
-
setIsDeletingCase(true);
|
|
486
|
-
setError('');
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
await deleteCase(user, currentCase);
|
|
490
|
-
setCurrentCase('');
|
|
491
|
-
onCaseChange('');
|
|
492
|
-
setFiles([]);
|
|
493
|
-
setSuccessAction('deleted');
|
|
494
|
-
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
495
|
-
|
|
496
|
-
// Refresh permissions after successful case deletion
|
|
497
|
-
// This allows users with limited permissions to create a new case
|
|
498
|
-
await checkUserPermissions();
|
|
499
|
-
} catch (err) {
|
|
500
|
-
setError(err instanceof Error ? err.message : 'Failed to delete case');
|
|
501
|
-
} finally {
|
|
502
|
-
setIsDeletingCase(false);
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
|
|
506
257
|
const handleImageSelect = (file: FileData) => {
|
|
507
258
|
onImageSelect(file);
|
|
508
259
|
// Prevent notes from opening against stale image state while selection loads.
|
|
@@ -539,155 +290,10 @@ const handleImageSelect = (file: FileData) => {
|
|
|
539
290
|
? 'Select an image first'
|
|
540
291
|
: undefined;
|
|
541
292
|
|
|
542
|
-
const handleExport = async (exportCaseNumber: string, format: ExportFormat, includeImages?: boolean, onProgress?: (progress: number, label: string) => void) => {
|
|
543
|
-
try {
|
|
544
|
-
const caseExportActions = await loadCaseExportActions();
|
|
545
|
-
|
|
546
|
-
if (includeImages) {
|
|
547
|
-
// ZIP export with images - only available for single case exports
|
|
548
|
-
await caseExportActions.downloadCaseAsZip(user, exportCaseNumber, format, (progress) => {
|
|
549
|
-
const label = progress < 30 ? 'Loading case data' :
|
|
550
|
-
progress < 50 ? 'Preparing archive' :
|
|
551
|
-
progress < 80 ? 'Adding images' :
|
|
552
|
-
progress < 96 ? 'Finalizing' : 'Downloading';
|
|
553
|
-
onProgress?.(Math.round(progress), label);
|
|
554
|
-
});
|
|
555
|
-
} else {
|
|
556
|
-
// Standard data-only export
|
|
557
|
-
onProgress?.(5, 'Loading case data');
|
|
558
|
-
const exportData = await caseExportActions.exportCaseData(
|
|
559
|
-
user,
|
|
560
|
-
exportCaseNumber,
|
|
561
|
-
{ includeMetadata: true },
|
|
562
|
-
(current, total, label) => {
|
|
563
|
-
const p = total > 0 ? Math.round(10 + (current / total) * 60) : 10;
|
|
564
|
-
onProgress?.(p, label);
|
|
565
|
-
}
|
|
566
|
-
);
|
|
567
|
-
onProgress?.(75, 'Preparing download');
|
|
568
|
-
|
|
569
|
-
// Download the exported data in the selected format
|
|
570
|
-
if (format === 'json') {
|
|
571
|
-
await caseExportActions.downloadCaseAsJSON(user, exportData);
|
|
572
|
-
} else {
|
|
573
|
-
await caseExportActions.downloadCaseAsCSV(user, exportData);
|
|
574
|
-
}
|
|
575
|
-
onProgress?.(100, 'Complete');
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
} catch (error) {
|
|
579
|
-
console.error('Export failed:', error);
|
|
580
|
-
throw error; // Re-throw to be handled by the modal
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
const handleExportAll = async (onProgress: (current: number, total: number, caseName: string) => void, format: ExportFormat) => {
|
|
585
|
-
try {
|
|
586
|
-
const caseExportActions = await loadCaseExportActions();
|
|
587
|
-
|
|
588
|
-
// Export all cases with progress callback
|
|
589
|
-
const exportData = await caseExportActions.exportAllCases(user, {
|
|
590
|
-
includeMetadata: true
|
|
591
|
-
}, onProgress);
|
|
592
|
-
|
|
593
|
-
// Download the exported data in the selected format
|
|
594
|
-
if (format === 'json') {
|
|
595
|
-
await caseExportActions.downloadAllCasesAsJSON(user, exportData);
|
|
596
|
-
} else {
|
|
597
|
-
await caseExportActions.downloadAllCasesAsCSV(user, exportData);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
} catch (error) {
|
|
601
|
-
console.error('Export all failed:', error);
|
|
602
|
-
throw error; // Re-throw to be handled by the modal
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
|
|
606
293
|
return (
|
|
607
294
|
<>
|
|
608
295
|
<div className={styles.caseSection}>
|
|
609
|
-
|
|
610
|
-
<div className={`${styles.caseLoad} mb-4`}>
|
|
611
|
-
<button
|
|
612
|
-
className={styles.switchCaseButton}
|
|
613
|
-
onClick={() => setShowCaseManagement(true)}
|
|
614
|
-
disabled={isUploading}
|
|
615
|
-
title={isUploading ? "Cannot switch cases while uploading files" : undefined}
|
|
616
|
-
>
|
|
617
|
-
Switch Case
|
|
618
|
-
</button>
|
|
619
|
-
</div>
|
|
620
|
-
) : (
|
|
621
|
-
<>
|
|
622
|
-
<h4>Case Management</h4>
|
|
623
|
-
{limitsDescription && (
|
|
624
|
-
<p className={styles.limitsInfo}>
|
|
625
|
-
{limitsDescription}
|
|
626
|
-
</p>
|
|
627
|
-
)}
|
|
628
|
-
<div className={`${styles.caseInput} mb-4`}>
|
|
629
|
-
<input
|
|
630
|
-
type="text"
|
|
631
|
-
value={caseNumber}
|
|
632
|
-
onChange={(e) => setCaseNumber(e.target.value)}
|
|
633
|
-
placeholder="Case #"
|
|
634
|
-
/>
|
|
635
|
-
</div>
|
|
636
|
-
<div className={`${styles.caseLoad} mb-4`}>
|
|
637
|
-
<button
|
|
638
|
-
onClick={handleCase}
|
|
639
|
-
disabled={isLoading || !caseNumber || permissionChecking || (isReadOnly && !!currentCase) || isUploading}
|
|
640
|
-
title={
|
|
641
|
-
isUploading
|
|
642
|
-
? "Cannot load/create cases while uploading files"
|
|
643
|
-
: (isReadOnly && currentCase)
|
|
644
|
-
? "Cannot load/create cases while reviewing a read-only case. Clear the current case first."
|
|
645
|
-
: (!canCreateNewCase ? createCaseError : undefined)
|
|
646
|
-
}
|
|
647
|
-
>
|
|
648
|
-
{isLoading ? 'Loading...' : permissionChecking ? 'Checking permissions...' : 'Load/Create Case'}
|
|
649
|
-
</button>
|
|
650
|
-
</div>
|
|
651
|
-
<div className={styles.caseInput}>
|
|
652
|
-
<button
|
|
653
|
-
onClick={() => setIsModalOpen(true)}
|
|
654
|
-
className={styles.listButton}
|
|
655
|
-
disabled={isUploading}
|
|
656
|
-
title={isUploading ? "Cannot list cases while uploading files" : undefined}
|
|
657
|
-
>
|
|
658
|
-
List All Cases
|
|
659
|
-
</button>
|
|
660
|
-
</div>
|
|
661
|
-
{currentCase && (
|
|
662
|
-
<div className="mb-4">
|
|
663
|
-
<button
|
|
664
|
-
className={styles.cancelSwitchButton}
|
|
665
|
-
onClick={() => setShowCaseManagement(false)}
|
|
666
|
-
disabled={isUploading}
|
|
667
|
-
>
|
|
668
|
-
Cancel
|
|
669
|
-
</button>
|
|
670
|
-
</div>
|
|
671
|
-
)}
|
|
672
|
-
</>
|
|
673
|
-
)}
|
|
674
|
-
<CasesModal
|
|
675
|
-
isOpen={isModalOpen}
|
|
676
|
-
onClose={() => setIsModalOpen(false)}
|
|
677
|
-
onSelectCase={setCaseNumber}
|
|
678
|
-
currentCase={currentCase || ''}
|
|
679
|
-
user={user}
|
|
680
|
-
/>
|
|
681
|
-
|
|
682
|
-
<CaseExport
|
|
683
|
-
isOpen={isExportModalOpen}
|
|
684
|
-
onClose={() => setIsExportModalOpen(false)}
|
|
685
|
-
onExport={handleExport}
|
|
686
|
-
onExportAll={handleExportAll}
|
|
687
|
-
currentCaseNumber={currentCase || ''}
|
|
688
|
-
isReadOnly={isReadOnly}
|
|
689
|
-
/>
|
|
690
|
-
|
|
296
|
+
|
|
691
297
|
<FilesModal
|
|
692
298
|
isOpen={isFilesModalOpen}
|
|
693
299
|
onClose={() => setIsFilesModalOpen(false)}
|
|
@@ -700,7 +306,7 @@ return (
|
|
|
700
306
|
/>
|
|
701
307
|
|
|
702
308
|
<div className={styles.filesSection}>
|
|
703
|
-
|
|
309
|
+
<div className={currentCase ? (isReadOnly ? styles.readOnlyContainer : styles.caseHeader) : styles.emptyCaseHeader}>
|
|
704
310
|
<h4 className={`${styles.caseNumber} ${
|
|
705
311
|
currentCase && caseConfirmationStatus.includeConfirmation
|
|
706
312
|
? caseConfirmationStatus.isConfirmed
|
|
@@ -710,9 +316,6 @@ return (
|
|
|
710
316
|
}`}>
|
|
711
317
|
{currentCase || 'No Case Selected'}
|
|
712
318
|
</h4>
|
|
713
|
-
{isReadOnly && currentCase && (
|
|
714
|
-
<div className={styles.readOnlyBadge}>(Read-Only)</div>
|
|
715
|
-
)}
|
|
716
319
|
</div>
|
|
717
320
|
{currentCase && (
|
|
718
321
|
<ImageUploadZone
|
|
@@ -743,25 +346,18 @@ return (
|
|
|
743
346
|
</div>
|
|
744
347
|
)}
|
|
745
348
|
|
|
746
|
-
{
|
|
747
|
-
<
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
{!canUploadNewFile && (
|
|
753
|
-
<div className={styles.limitReached}>
|
|
754
|
-
<p>Upload limit reached for this case</p>
|
|
755
|
-
</div>
|
|
756
|
-
)}
|
|
757
|
-
<ul className={styles.fileList}>
|
|
758
|
-
{files.map((file) => {
|
|
349
|
+
{currentCase ? (
|
|
350
|
+
<ul className={styles.fileList}>
|
|
351
|
+
{files.length === 0 ? (
|
|
352
|
+
<li className={styles.fileListMessage}>No files found for {currentCase}</li>
|
|
353
|
+
) : (
|
|
354
|
+
files.map((file) => {
|
|
759
355
|
const confirmationStatus = fileConfirmationStatus[file.id];
|
|
760
356
|
let confirmationClass = '';
|
|
761
|
-
|
|
357
|
+
|
|
762
358
|
if (confirmationStatus?.includeConfirmation) {
|
|
763
|
-
confirmationClass = confirmationStatus.isConfirmed
|
|
764
|
-
? styles.fileItemConfirmed
|
|
359
|
+
confirmationClass = confirmationStatus.isConfirmed
|
|
360
|
+
? styles.fileItemConfirmed
|
|
765
361
|
: styles.fileItemNotConfirmed;
|
|
766
362
|
}
|
|
767
363
|
|
|
@@ -776,11 +372,11 @@ return (
|
|
|
776
372
|
title={isUploading ? "Cannot select files while uploading" : undefined}
|
|
777
373
|
>
|
|
778
374
|
<span className={styles.fileName}>{file.originalFilename}</span>
|
|
779
|
-
</button>
|
|
375
|
+
</button>
|
|
780
376
|
<button
|
|
781
377
|
onClick={() => {
|
|
782
378
|
if (window.confirm('Are you sure you want to delete this file? This action cannot be undone.')) {
|
|
783
|
-
handleFileDelete(file.id);
|
|
379
|
+
handleFileDelete(file.id);
|
|
784
380
|
}
|
|
785
381
|
}}
|
|
786
382
|
className={styles.deleteButton}
|
|
@@ -793,12 +389,14 @@ return (
|
|
|
793
389
|
</button>
|
|
794
390
|
</li>
|
|
795
391
|
);
|
|
796
|
-
})
|
|
797
|
-
|
|
798
|
-
|
|
392
|
+
})
|
|
393
|
+
)}
|
|
394
|
+
</ul>
|
|
395
|
+
) : (
|
|
396
|
+
<div className={styles.fileListPlaceholder}>Select a case to view files</div>
|
|
799
397
|
)}
|
|
800
398
|
</div>
|
|
801
|
-
<div className={
|
|
399
|
+
<div className={styles.sidebarToggle}>
|
|
802
400
|
<button
|
|
803
401
|
onClick={onNotesClick}
|
|
804
402
|
disabled={isImageNotesDisabled}
|
|
@@ -807,99 +405,7 @@ return (
|
|
|
807
405
|
Image Notes
|
|
808
406
|
</button>
|
|
809
407
|
</div>
|
|
810
|
-
{currentCase && (
|
|
811
|
-
<div className={styles.caseActionsSection}>
|
|
812
|
-
<button
|
|
813
|
-
onClick={() => setShowCaseActions(!showCaseActions)}
|
|
814
|
-
className={styles.caseActionsButton}
|
|
815
|
-
disabled={isUploading}
|
|
816
|
-
title={isUploading ? "Cannot access case actions while uploading" : undefined}
|
|
817
|
-
>
|
|
818
|
-
{showCaseActions ? 'Hide Case Actions' : 'Case Actions'}
|
|
819
|
-
</button>
|
|
820
|
-
|
|
821
|
-
{showCaseActions && !isUploading && (
|
|
822
|
-
<div className={styles.caseActionsContent}>
|
|
823
|
-
{/* Export Case Data Section */}
|
|
824
|
-
<div className={styles.exportSection}>
|
|
825
|
-
<button
|
|
826
|
-
onClick={() => setIsExportModalOpen(true)}
|
|
827
|
-
className={styles.exportButton}
|
|
828
|
-
disabled={isUploading}
|
|
829
|
-
title={isUploading ? "Cannot export while uploading" : undefined}
|
|
830
|
-
>
|
|
831
|
-
Export Case Data
|
|
832
|
-
</button>
|
|
833
|
-
</div>
|
|
834
|
-
|
|
835
|
-
{/* Audit Trail Section - Available for all cases */}
|
|
836
|
-
<div className={styles.auditTrailSection}>
|
|
837
|
-
<button
|
|
838
|
-
onClick={() => setIsAuditTrailOpen(true)}
|
|
839
|
-
className={styles.auditTrailButton}
|
|
840
|
-
disabled={isUploading}
|
|
841
|
-
title={isUploading ? "Cannot view audit trail while uploading" : undefined}
|
|
842
|
-
>
|
|
843
|
-
Audit Trail
|
|
844
|
-
</button>
|
|
845
|
-
</div>
|
|
846
|
-
|
|
847
|
-
{/* Rename/Delete Section - Only for owned cases */}
|
|
848
|
-
{!isReadOnly && (
|
|
849
|
-
<div className={styles.renameDeleteSection}>
|
|
850
|
-
<div className={`${styles.caseRename} mb-4`}>
|
|
851
|
-
<input
|
|
852
|
-
type="text"
|
|
853
|
-
value={newCaseName}
|
|
854
|
-
onChange={(e) => setNewCaseName(e.target.value)}
|
|
855
|
-
placeholder="New Case Number"
|
|
856
|
-
disabled={isUploading}
|
|
857
|
-
/>
|
|
858
|
-
<button
|
|
859
|
-
onClick={handleRenameCase}
|
|
860
|
-
disabled={isRenaming || !newCaseName || isUploading}
|
|
861
|
-
title={isUploading ? "Cannot rename while uploading" : undefined}
|
|
862
|
-
>
|
|
863
|
-
{isRenaming ? 'Renaming...' : 'Rename Case'}
|
|
864
|
-
</button>
|
|
865
|
-
</div>
|
|
866
|
-
|
|
867
|
-
<div className={styles.deleteCaseSection}>
|
|
868
|
-
<button
|
|
869
|
-
onClick={handleDeleteCase}
|
|
870
|
-
disabled={isDeletingCase || isUploading}
|
|
871
|
-
className={styles.deleteWarningButton}
|
|
872
|
-
title={isUploading ? "Cannot delete while uploading" : undefined}
|
|
873
|
-
>
|
|
874
|
-
{isDeletingCase ? 'Deleting...' : 'Delete Case'}
|
|
875
|
-
</button>
|
|
876
|
-
</div>
|
|
877
|
-
</div>
|
|
878
|
-
)}
|
|
879
|
-
</div>
|
|
880
|
-
)}
|
|
881
|
-
</div>
|
|
882
|
-
)}
|
|
883
|
-
|
|
884
|
-
{/* Unified Audit Viewer */}
|
|
885
|
-
<UserAuditViewer
|
|
886
|
-
caseNumber={currentCase || ''}
|
|
887
|
-
isOpen={isAuditTrailOpen}
|
|
888
|
-
onClose={() => setIsAuditTrailOpen(false)}
|
|
889
|
-
title={`Audit Trail - Case ${currentCase}`}
|
|
890
|
-
/>
|
|
891
|
-
|
|
892
408
|
</div>
|
|
893
|
-
<Toast
|
|
894
|
-
message={toastMessage}
|
|
895
|
-
type={toastType}
|
|
896
|
-
isVisible={isToastVisible}
|
|
897
|
-
onClose={() => {
|
|
898
|
-
setIsToastVisible(false);
|
|
899
|
-
setError('');
|
|
900
|
-
setSuccessAction(null);
|
|
901
|
-
}}
|
|
902
|
-
/>
|
|
903
409
|
</>
|
|
904
410
|
);
|
|
905
411
|
};
|