@striae-org/striae 4.1.0 → 4.2.1
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/LICENSE +1 -1
- 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 +463 -8
- package/app/components/actions/confirm-export.ts +9 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +19 -8
- package/app/components/audit/user-audit.module.css +21 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
- package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- 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 +14 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +12 -14
- package/app/components/colors/colors.module.css +4 -3
- 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 +402 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +8 -46
- package/app/components/sidebar/case-import/case-import.module.css +23 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -16
- 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 +68 -588
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +82 -43
- package/app/components/sidebar/cases/cases.module.css +82 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +49 -52
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
- package/app/components/sidebar/notes/notes.module.css +170 -1
- package/app/components/sidebar/sidebar-container.tsx +16 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +27 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/user/inactivity-warning.module.css +1 -0
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.tsx +23 -10
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useOverlayDismiss.ts +54 -4
- package/app/root.tsx +1 -1
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +475 -30
- package/app/services/audit/audit.service.ts +173 -27
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +4 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/data/permissions.ts +16 -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 +426 -22
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +20 -23
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +7 -12
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +3 -3
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +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/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +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/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
- package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
- package/workers/pdf-worker/src/report-types.ts +3 -3
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- 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/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -53
- package/postcss.config.js +0 -6
- package/public/.well-known/keybase.txt +0 -56
- package/tailwind.config.ts +0 -22
|
@@ -1,53 +1,29 @@
|
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
getFileAnnotations
|
|
12
|
+
ensureCaseConfirmationSummary,
|
|
13
|
+
getCaseConfirmationSummary
|
|
31
14
|
} from '~/utils/data';
|
|
32
|
-
import { type FileData
|
|
15
|
+
import { type FileData } from '~/types';
|
|
33
16
|
|
|
34
17
|
interface CaseSidebarProps {
|
|
35
18
|
user: User;
|
|
36
19
|
onImageSelect: (file: FileData) => void;
|
|
37
|
-
|
|
20
|
+
onOpenCase: () => void;
|
|
38
21
|
imageLoaded: boolean;
|
|
39
22
|
setImageLoaded: (loaded: boolean) => void;
|
|
40
23
|
onNotesClick: () => void;
|
|
41
24
|
files: FileData[];
|
|
42
25
|
setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
|
|
43
|
-
caseNumber: string;
|
|
44
|
-
setCaseNumber: (caseNumber: string) => void;
|
|
45
26
|
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
27
|
isReadOnly?: boolean;
|
|
52
28
|
isConfirmed?: boolean;
|
|
53
29
|
confirmationSaveVersion?: number;
|
|
@@ -57,37 +33,16 @@ interface CaseSidebarProps {
|
|
|
57
33
|
onUploadComplete?: (result: { successCount: number; failedFiles: string[] }) => void;
|
|
58
34
|
}
|
|
59
35
|
|
|
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
36
|
export const CaseSidebar = ({
|
|
75
37
|
user,
|
|
76
38
|
onImageSelect,
|
|
77
|
-
|
|
39
|
+
onOpenCase,
|
|
78
40
|
imageLoaded,
|
|
79
41
|
setImageLoaded,
|
|
80
42
|
onNotesClick,
|
|
81
43
|
files,
|
|
82
44
|
setFiles,
|
|
83
|
-
caseNumber,
|
|
84
|
-
setCaseNumber,
|
|
85
45
|
currentCase,
|
|
86
|
-
setCurrentCase,
|
|
87
|
-
error,
|
|
88
|
-
setError,
|
|
89
|
-
successAction,
|
|
90
|
-
setSuccessAction,
|
|
91
46
|
isReadOnly = false,
|
|
92
47
|
isConfirmed = false,
|
|
93
48
|
confirmationSaveVersion = 0,
|
|
@@ -97,27 +52,11 @@ export const CaseSidebar = ({
|
|
|
97
52
|
onUploadComplete
|
|
98
53
|
}: CaseSidebarProps) => {
|
|
99
54
|
|
|
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
55
|
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
56
|
const [canUploadNewFile, setCanUploadNewFile] = useState(true);
|
|
113
|
-
const [createCaseError, setCreateCaseError] = useState('');
|
|
114
57
|
const [uploadFileError, setUploadFileError] = useState('');
|
|
115
|
-
const [limitsDescription, setLimitsDescription] = useState('');
|
|
116
|
-
const [permissionChecking, setPermissionChecking] = useState(false);
|
|
117
|
-
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
|
118
58
|
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
|
119
59
|
const [deletingFileId, setDeletingFileId] = useState<string | null>(null);
|
|
120
|
-
const [isAuditTrailOpen, setIsAuditTrailOpen] = useState(false);
|
|
121
60
|
const [fileConfirmationStatus, setFileConfirmationStatus] = useState<{
|
|
122
61
|
[fileId: string]: { includeConfirmation: boolean; isConfirmed: boolean }
|
|
123
62
|
}>({});
|
|
@@ -131,45 +70,6 @@ export const CaseSidebar = ({
|
|
|
131
70
|
[files]
|
|
132
71
|
);
|
|
133
72
|
|
|
134
|
-
const calculateCaseConfirmationStatus = useCallback((
|
|
135
|
-
statuses: { [fileId: string]: { includeConfirmation: boolean; isConfirmed: boolean } }
|
|
136
|
-
) => {
|
|
137
|
-
const filesRequiringConfirmation = files
|
|
138
|
-
.map((file) => statuses[file.id] || { includeConfirmation: false, isConfirmed: false })
|
|
139
|
-
.filter((status) => status.includeConfirmation);
|
|
140
|
-
|
|
141
|
-
const allConfirmedFiles = filesRequiringConfirmation.every((status) => status.isConfirmed);
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
includeConfirmation: filesRequiringConfirmation.length > 0,
|
|
145
|
-
isConfirmed: filesRequiringConfirmation.length > 0 ? allConfirmedFiles : false,
|
|
146
|
-
};
|
|
147
|
-
}, [files]);
|
|
148
|
-
|
|
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
73
|
// Function to check file upload permissions (extracted for reuse)
|
|
174
74
|
const checkFileUploadPermissions = useCallback(async (fileCount?: number) => {
|
|
175
75
|
if (currentCase) {
|
|
@@ -190,11 +90,6 @@ export const CaseSidebar = ({
|
|
|
190
90
|
}
|
|
191
91
|
}, [currentCase, files.length, user]);
|
|
192
92
|
|
|
193
|
-
// Check user permissions on mount and when user changes
|
|
194
|
-
useEffect(() => {
|
|
195
|
-
checkUserPermissions();
|
|
196
|
-
}, [checkUserPermissions]);
|
|
197
|
-
|
|
198
93
|
// Check file upload permissions when currentCase or files change
|
|
199
94
|
useEffect(() => {
|
|
200
95
|
checkFileUploadPermissions();
|
|
@@ -202,7 +97,6 @@ export const CaseSidebar = ({
|
|
|
202
97
|
|
|
203
98
|
useEffect(() => {
|
|
204
99
|
if (currentCase) {
|
|
205
|
-
setIsLoading(true);
|
|
206
100
|
fetchFiles(user, currentCase, { skipValidation: true })
|
|
207
101
|
.then(loadedFiles => {
|
|
208
102
|
setFiles(loadedFiles);
|
|
@@ -210,9 +104,6 @@ export const CaseSidebar = ({
|
|
|
210
104
|
.catch(err => {
|
|
211
105
|
console.error('Failed to load files:', err);
|
|
212
106
|
setFileError(err instanceof Error ? err.message : 'Failed to load files');
|
|
213
|
-
})
|
|
214
|
-
.finally(() => {
|
|
215
|
-
setIsLoading(false);
|
|
216
107
|
});
|
|
217
108
|
} else {
|
|
218
109
|
setFiles([]);
|
|
@@ -232,43 +123,24 @@ export const CaseSidebar = ({
|
|
|
232
123
|
return;
|
|
233
124
|
}
|
|
234
125
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const annotations = await getFileAnnotations(user, currentCase, file.id);
|
|
239
|
-
return {
|
|
240
|
-
fileId: file.id,
|
|
241
|
-
includeConfirmation: annotations?.includeConfirmation ?? false,
|
|
242
|
-
isConfirmed: !!annotations?.confirmationData,
|
|
243
|
-
};
|
|
244
|
-
} catch (err) {
|
|
245
|
-
console.error(`Error fetching annotations for file ${file.id}:`, err);
|
|
246
|
-
return {
|
|
247
|
-
fileId: file.id,
|
|
248
|
-
includeConfirmation: false,
|
|
249
|
-
isConfirmed: false,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
126
|
+
const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files).catch((error) => {
|
|
127
|
+
console.error(`Error fetching confirmation summary for case ${currentCase}:`, error);
|
|
128
|
+
return null;
|
|
252
129
|
});
|
|
253
130
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// Build the statuses map from results
|
|
258
|
-
const statuses: { [fileId: string]: { includeConfirmation: boolean; isConfirmed: boolean } } = {};
|
|
259
|
-
results.forEach((result) => {
|
|
260
|
-
statuses[result.fileId] = {
|
|
261
|
-
includeConfirmation: result.includeConfirmation,
|
|
262
|
-
isConfirmed: result.isConfirmed,
|
|
263
|
-
};
|
|
264
|
-
});
|
|
131
|
+
if (!caseSummary) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
265
134
|
|
|
266
135
|
if (isCancelled) {
|
|
267
136
|
return;
|
|
268
137
|
}
|
|
269
138
|
|
|
270
|
-
setFileConfirmationStatus(
|
|
271
|
-
setCaseConfirmationStatus(
|
|
139
|
+
setFileConfirmationStatus(caseSummary.filesById);
|
|
140
|
+
setCaseConfirmationStatus({
|
|
141
|
+
includeConfirmation: caseSummary.includeConfirmation,
|
|
142
|
+
isConfirmed: caseSummary.isConfirmed
|
|
143
|
+
});
|
|
272
144
|
};
|
|
273
145
|
|
|
274
146
|
fetchConfirmationStatuses();
|
|
@@ -276,7 +148,7 @@ export const CaseSidebar = ({
|
|
|
276
148
|
return () => {
|
|
277
149
|
isCancelled = true;
|
|
278
150
|
};
|
|
279
|
-
}, [currentCase, fileIdsKey, user, files
|
|
151
|
+
}, [currentCase, fileIdsKey, user, files]);
|
|
280
152
|
|
|
281
153
|
// Refresh only selected file confirmation status after confirmation-related data is persisted
|
|
282
154
|
useEffect(() => {
|
|
@@ -288,24 +160,18 @@ export const CaseSidebar = ({
|
|
|
288
160
|
}
|
|
289
161
|
|
|
290
162
|
try {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
isConfirmed: !!annotations?.confirmationData,
|
|
295
|
-
};
|
|
163
|
+
const caseSummary =
|
|
164
|
+
await getCaseConfirmationSummary(user, currentCase) ||
|
|
165
|
+
await ensureCaseConfirmationSummary(user, currentCase, files);
|
|
296
166
|
|
|
297
167
|
if (isCancelled) {
|
|
298
168
|
return;
|
|
299
169
|
}
|
|
300
170
|
|
|
301
|
-
setFileConfirmationStatus(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
setCaseConfirmationStatus(calculateCaseConfirmationStatus(next));
|
|
308
|
-
return next;
|
|
171
|
+
setFileConfirmationStatus(caseSummary.filesById);
|
|
172
|
+
setCaseConfirmationStatus({
|
|
173
|
+
includeConfirmation: caseSummary.includeConfirmation,
|
|
174
|
+
isConfirmed: caseSummary.isConfirmed
|
|
309
175
|
});
|
|
310
176
|
} catch (err) {
|
|
311
177
|
console.error(`Error refreshing confirmation status for file ${selectedFileId}:`, err);
|
|
@@ -317,90 +183,7 @@ export const CaseSidebar = ({
|
|
|
317
183
|
return () => {
|
|
318
184
|
isCancelled = true;
|
|
319
185
|
};
|
|
320
|
-
}, [currentCase, fileIdsKey, user, selectedFileId, confirmationSaveVersion, files
|
|
321
|
-
|
|
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
|
-
|
|
186
|
+
}, [currentCase, fileIdsKey, user, selectedFileId, confirmationSaveVersion, files]);
|
|
404
187
|
|
|
405
188
|
const handleFileDelete = async (fileId: string) => {
|
|
406
189
|
// Don't allow file deletion for read-only cases
|
|
@@ -414,11 +197,15 @@ export const CaseSidebar = ({
|
|
|
414
197
|
setDeletingFileId(fileId);
|
|
415
198
|
|
|
416
199
|
try {
|
|
417
|
-
await deleteFile(user, currentCase, fileId);
|
|
200
|
+
const deleteResult = await deleteFile(user, currentCase, fileId);
|
|
418
201
|
const updatedFiles = files.filter(f => f.id !== fileId);
|
|
419
202
|
setFiles(updatedFiles);
|
|
420
203
|
onImageSelect({ id: 'clear', originalFilename: '/clear.jpg', uploadedAt: '' });
|
|
421
204
|
setImageLoaded(false);
|
|
205
|
+
|
|
206
|
+
if (deleteResult.imageMissing) {
|
|
207
|
+
setFileError(`File record deleted. Image asset "${deleteResult.fileName}" was not found and was skipped.`);
|
|
208
|
+
}
|
|
422
209
|
|
|
423
210
|
// Refresh file upload permissions after successful file deletion
|
|
424
211
|
// Pass the new file count directly to avoid state update timing issues
|
|
@@ -430,79 +217,6 @@ export const CaseSidebar = ({
|
|
|
430
217
|
}
|
|
431
218
|
};
|
|
432
219
|
|
|
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
220
|
const handleImageSelect = (file: FileData) => {
|
|
507
221
|
onImageSelect(file);
|
|
508
222
|
// Prevent notes from opening against stale image state while selection loads.
|
|
@@ -539,155 +253,10 @@ const handleImageSelect = (file: FileData) => {
|
|
|
539
253
|
? 'Select an image first'
|
|
540
254
|
: undefined;
|
|
541
255
|
|
|
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
256
|
return (
|
|
607
257
|
<>
|
|
608
258
|
<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
|
-
|
|
259
|
+
|
|
691
260
|
<FilesModal
|
|
692
261
|
isOpen={isFilesModalOpen}
|
|
693
262
|
onClose={() => setIsFilesModalOpen(false)}
|
|
@@ -697,21 +266,29 @@ return (
|
|
|
697
266
|
setFiles={setFiles}
|
|
698
267
|
isReadOnly={isReadOnly}
|
|
699
268
|
selectedFileId={selectedFileId}
|
|
269
|
+
confirmationSaveVersion={confirmationSaveVersion}
|
|
700
270
|
/>
|
|
701
271
|
|
|
702
272
|
<div className={styles.filesSection}>
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
?
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
273
|
+
<div className={currentCase ? (isReadOnly ? styles.readOnlyContainer : styles.caseHeader) : styles.emptyCaseHeader}>
|
|
274
|
+
{currentCase ? (
|
|
275
|
+
<h4 className={`${styles.caseNumber} ${
|
|
276
|
+
caseConfirmationStatus.includeConfirmation
|
|
277
|
+
? caseConfirmationStatus.isConfirmed
|
|
278
|
+
? styles.caseConfirmed
|
|
279
|
+
: styles.caseNotConfirmed
|
|
280
|
+
: ''
|
|
281
|
+
}`}>
|
|
282
|
+
{currentCase}
|
|
283
|
+
</h4>
|
|
284
|
+
) : (
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
className={styles.openCaseButton}
|
|
288
|
+
onClick={onOpenCase}
|
|
289
|
+
>
|
|
290
|
+
Open Case
|
|
291
|
+
</button>
|
|
715
292
|
)}
|
|
716
293
|
</div>
|
|
717
294
|
{currentCase && (
|
|
@@ -743,25 +320,18 @@ return (
|
|
|
743
320
|
</div>
|
|
744
321
|
)}
|
|
745
322
|
|
|
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) => {
|
|
323
|
+
{currentCase ? (
|
|
324
|
+
<ul className={styles.fileList}>
|
|
325
|
+
{files.length === 0 ? (
|
|
326
|
+
<li className={styles.fileListMessage}>No files found for {currentCase}</li>
|
|
327
|
+
) : (
|
|
328
|
+
files.map((file) => {
|
|
759
329
|
const confirmationStatus = fileConfirmationStatus[file.id];
|
|
760
330
|
let confirmationClass = '';
|
|
761
|
-
|
|
331
|
+
|
|
762
332
|
if (confirmationStatus?.includeConfirmation) {
|
|
763
|
-
confirmationClass = confirmationStatus.isConfirmed
|
|
764
|
-
? styles.fileItemConfirmed
|
|
333
|
+
confirmationClass = confirmationStatus.isConfirmed
|
|
334
|
+
? styles.fileItemConfirmed
|
|
765
335
|
: styles.fileItemNotConfirmed;
|
|
766
336
|
}
|
|
767
337
|
|
|
@@ -776,11 +346,11 @@ return (
|
|
|
776
346
|
title={isUploading ? "Cannot select files while uploading" : undefined}
|
|
777
347
|
>
|
|
778
348
|
<span className={styles.fileName}>{file.originalFilename}</span>
|
|
779
|
-
</button>
|
|
349
|
+
</button>
|
|
780
350
|
<button
|
|
781
351
|
onClick={() => {
|
|
782
352
|
if (window.confirm('Are you sure you want to delete this file? This action cannot be undone.')) {
|
|
783
|
-
handleFileDelete(file.id);
|
|
353
|
+
handleFileDelete(file.id);
|
|
784
354
|
}
|
|
785
355
|
}}
|
|
786
356
|
className={styles.deleteButton}
|
|
@@ -793,12 +363,14 @@ return (
|
|
|
793
363
|
</button>
|
|
794
364
|
</li>
|
|
795
365
|
);
|
|
796
|
-
})
|
|
797
|
-
|
|
798
|
-
|
|
366
|
+
})
|
|
367
|
+
)}
|
|
368
|
+
</ul>
|
|
369
|
+
) : (
|
|
370
|
+
<div className={styles.fileListPlaceholder}>Select a case to view files</div>
|
|
799
371
|
)}
|
|
800
372
|
</div>
|
|
801
|
-
<div className={
|
|
373
|
+
<div className={styles.sidebarToggle}>
|
|
802
374
|
<button
|
|
803
375
|
onClick={onNotesClick}
|
|
804
376
|
disabled={isImageNotesDisabled}
|
|
@@ -807,99 +379,7 @@ return (
|
|
|
807
379
|
Image Notes
|
|
808
380
|
</button>
|
|
809
381
|
</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
382
|
</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
383
|
</>
|
|
904
384
|
);
|
|
905
385
|
};
|