@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,827 @@
|
|
|
1
|
+
import { User } from 'firebase/auth';
|
|
2
|
+
import {
|
|
3
|
+
exportCaseData,
|
|
4
|
+
exportAllCases,
|
|
5
|
+
downloadCaseAsJSON,
|
|
6
|
+
downloadCaseAsCSV,
|
|
7
|
+
downloadAllCasesAsJSON,
|
|
8
|
+
downloadAllCasesAsCSV,
|
|
9
|
+
downloadCaseAsZip
|
|
10
|
+
} from '../../actions/case-export';
|
|
11
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
12
|
+
import styles from './cases.module.css';
|
|
13
|
+
import { CasesModal } from './cases-modal';
|
|
14
|
+
import { FilesModal } from '../files/files-modal';
|
|
15
|
+
import { CaseExport, ExportFormat } from '../case-export/case-export';
|
|
16
|
+
import { ImageUploadZone } from '../upload/image-upload-zone';
|
|
17
|
+
import { UserAuditViewer } from '~/components/audit/user-audit-viewer';
|
|
18
|
+
import {
|
|
19
|
+
validateCaseNumber,
|
|
20
|
+
checkExistingCase,
|
|
21
|
+
createNewCase,
|
|
22
|
+
renameCase,
|
|
23
|
+
deleteCase,
|
|
24
|
+
} from '../../actions/case-manage';
|
|
25
|
+
import {
|
|
26
|
+
fetchFiles,
|
|
27
|
+
deleteFile,
|
|
28
|
+
} from '../../actions/image-manage';
|
|
29
|
+
import {
|
|
30
|
+
checkReadOnlyCaseExists
|
|
31
|
+
} from '../../actions/case-review';
|
|
32
|
+
import {
|
|
33
|
+
canCreateCase,
|
|
34
|
+
canUploadFile,
|
|
35
|
+
getLimitsDescription,
|
|
36
|
+
getUserData
|
|
37
|
+
} from '~/utils/permissions';
|
|
38
|
+
import { getFileAnnotations } from '~/utils/data-operations';
|
|
39
|
+
import { FileData, CaseActionType } from '~/types';
|
|
40
|
+
|
|
41
|
+
interface CaseSidebarProps {
|
|
42
|
+
user: User;
|
|
43
|
+
onImageSelect: (file: FileData) => void;
|
|
44
|
+
onCaseChange: (caseNumber: string) => void;
|
|
45
|
+
imageLoaded: boolean;
|
|
46
|
+
setImageLoaded: (loaded: boolean) => void;
|
|
47
|
+
onNotesClick: () => void;
|
|
48
|
+
files: FileData[];
|
|
49
|
+
setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
|
|
50
|
+
caseNumber: string;
|
|
51
|
+
setCaseNumber: (caseNumber: string) => void;
|
|
52
|
+
currentCase: string | null;
|
|
53
|
+
setCurrentCase: (caseNumber: string) => void;
|
|
54
|
+
error: string;
|
|
55
|
+
setError: (error: string) => void;
|
|
56
|
+
successAction: CaseActionType;
|
|
57
|
+
setSuccessAction: (action: CaseActionType) => void;
|
|
58
|
+
isReadOnly?: boolean;
|
|
59
|
+
isConfirmed?: boolean;
|
|
60
|
+
confirmationSaveVersion?: number;
|
|
61
|
+
selectedFileId?: string;
|
|
62
|
+
isUploading?: boolean;
|
|
63
|
+
onUploadStatusChange?: (isUploading: boolean) => void;
|
|
64
|
+
onUploadComplete?: (result: { successCount: number; failedFiles: string[] }) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SUCCESS_MESSAGE_TIMEOUT = 3000;
|
|
68
|
+
|
|
69
|
+
export const CaseSidebar = ({
|
|
70
|
+
user,
|
|
71
|
+
onImageSelect,
|
|
72
|
+
onCaseChange,
|
|
73
|
+
imageLoaded,
|
|
74
|
+
setImageLoaded,
|
|
75
|
+
onNotesClick,
|
|
76
|
+
files,
|
|
77
|
+
setFiles,
|
|
78
|
+
caseNumber,
|
|
79
|
+
setCaseNumber,
|
|
80
|
+
currentCase,
|
|
81
|
+
setCurrentCase,
|
|
82
|
+
error,
|
|
83
|
+
setError,
|
|
84
|
+
successAction,
|
|
85
|
+
setSuccessAction,
|
|
86
|
+
isReadOnly = false,
|
|
87
|
+
isConfirmed = false,
|
|
88
|
+
confirmationSaveVersion = 0,
|
|
89
|
+
selectedFileId,
|
|
90
|
+
isUploading = false,
|
|
91
|
+
onUploadStatusChange,
|
|
92
|
+
onUploadComplete
|
|
93
|
+
}: CaseSidebarProps) => {
|
|
94
|
+
|
|
95
|
+
const [isDeletingCase, setIsDeletingCase] = useState(false);
|
|
96
|
+
const [isRenaming, setIsRenaming] = useState(false);
|
|
97
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
98
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
99
|
+
const [fileError, setFileError] = useState('');
|
|
100
|
+
const [newCaseName, setNewCaseName] = useState('');
|
|
101
|
+
const [showCaseActions, setShowCaseActions] = useState(false);
|
|
102
|
+
const [canCreateNewCase, setCanCreateNewCase] = useState(true);
|
|
103
|
+
const [canUploadNewFile, setCanUploadNewFile] = useState(true);
|
|
104
|
+
const [createCaseError, setCreateCaseError] = useState('');
|
|
105
|
+
const [uploadFileError, setUploadFileError] = useState('');
|
|
106
|
+
const [limitsDescription, setLimitsDescription] = useState('');
|
|
107
|
+
const [permissionChecking, setPermissionChecking] = useState(false);
|
|
108
|
+
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
|
109
|
+
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
|
110
|
+
const [deletingFileId, setDeletingFileId] = useState<string | null>(null);
|
|
111
|
+
const [isAuditTrailOpen, setIsAuditTrailOpen] = useState(false);
|
|
112
|
+
const [fileConfirmationStatus, setFileConfirmationStatus] = useState<{
|
|
113
|
+
[fileId: string]: { includeConfirmation: boolean; isConfirmed: boolean }
|
|
114
|
+
}>({});
|
|
115
|
+
const [caseConfirmationStatus, setCaseConfirmationStatus] = useState<{
|
|
116
|
+
includeConfirmation: boolean;
|
|
117
|
+
isConfirmed: boolean;
|
|
118
|
+
}>({ includeConfirmation: false, isConfirmed: false });
|
|
119
|
+
|
|
120
|
+
const fileIdsKey = useMemo(
|
|
121
|
+
() => files.map((file) => file.id).sort().join('|'),
|
|
122
|
+
[files]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const calculateCaseConfirmationStatus = useCallback((
|
|
126
|
+
statuses: { [fileId: string]: { includeConfirmation: boolean; isConfirmed: boolean } }
|
|
127
|
+
) => {
|
|
128
|
+
const filesRequiringConfirmation = files
|
|
129
|
+
.map((file) => statuses[file.id] || { includeConfirmation: false, isConfirmed: false })
|
|
130
|
+
.filter((status) => status.includeConfirmation);
|
|
131
|
+
|
|
132
|
+
const allConfirmedFiles = filesRequiringConfirmation.every((status) => status.isConfirmed);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
includeConfirmation: filesRequiringConfirmation.length > 0,
|
|
136
|
+
isConfirmed: filesRequiringConfirmation.length > 0 ? allConfirmedFiles : false,
|
|
137
|
+
};
|
|
138
|
+
}, [files]);
|
|
139
|
+
|
|
140
|
+
// Check user permissions on mount and when user changes
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
checkUserPermissions();
|
|
143
|
+
}, [user]);
|
|
144
|
+
|
|
145
|
+
// Function to check user permissions (extracted for reuse)
|
|
146
|
+
const checkUserPermissions = async () => {
|
|
147
|
+
setPermissionChecking(true);
|
|
148
|
+
try {
|
|
149
|
+
const casePermission = await canCreateCase(user);
|
|
150
|
+
setCanCreateNewCase(casePermission.canCreate);
|
|
151
|
+
setCreateCaseError(casePermission.reason || '');
|
|
152
|
+
|
|
153
|
+
// Only show limits description for restricted accounts
|
|
154
|
+
const userData = await getUserData(user);
|
|
155
|
+
if (userData && !userData.permitted) {
|
|
156
|
+
const description = await getLimitsDescription(user);
|
|
157
|
+
setLimitsDescription(description);
|
|
158
|
+
} else {
|
|
159
|
+
setLimitsDescription(''); // Clear the description for permitted users
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error checking user permissions:', error);
|
|
163
|
+
setCreateCaseError('Unable to verify account permissions');
|
|
164
|
+
} finally {
|
|
165
|
+
setPermissionChecking(false);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Function to check file upload permissions (extracted for reuse)
|
|
170
|
+
const checkFileUploadPermissions = async (fileCount?: number) => {
|
|
171
|
+
if (currentCase) {
|
|
172
|
+
try {
|
|
173
|
+
// Use provided fileCount or fall back to current files.length
|
|
174
|
+
const currentFileCount = fileCount !== undefined ? fileCount : files.length;
|
|
175
|
+
const permission = await canUploadFile(user, currentFileCount);
|
|
176
|
+
setCanUploadNewFile(permission.canUpload);
|
|
177
|
+
setUploadFileError(permission.reason || '');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error checking file upload permission:', error);
|
|
180
|
+
setCanUploadNewFile(false);
|
|
181
|
+
setUploadFileError('Unable to verify upload permissions');
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
setCanUploadNewFile(true);
|
|
185
|
+
setUploadFileError('');
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Check file upload permissions when currentCase or files change
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
checkFileUploadPermissions();
|
|
192
|
+
}, [user, currentCase, files.length]);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (currentCase) {
|
|
196
|
+
setIsLoading(true);
|
|
197
|
+
fetchFiles(user, currentCase, { skipValidation: true })
|
|
198
|
+
.then(loadedFiles => {
|
|
199
|
+
setFiles(loadedFiles);
|
|
200
|
+
})
|
|
201
|
+
.catch(err => {
|
|
202
|
+
console.error('Failed to load files:', err);
|
|
203
|
+
setFileError(err instanceof Error ? err.message : 'Failed to load files');
|
|
204
|
+
})
|
|
205
|
+
.finally(() => {
|
|
206
|
+
setIsLoading(false);
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
setFiles([]);
|
|
210
|
+
}
|
|
211
|
+
}, [user, currentCase, setFiles]);
|
|
212
|
+
|
|
213
|
+
// Fetch confirmation status for all files when case/files change
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
let isCancelled = false;
|
|
216
|
+
|
|
217
|
+
const fetchConfirmationStatuses = async () => {
|
|
218
|
+
if (!currentCase || !user || files.length === 0) {
|
|
219
|
+
if (!isCancelled) {
|
|
220
|
+
setFileConfirmationStatus({});
|
|
221
|
+
setCaseConfirmationStatus({ includeConfirmation: false, isConfirmed: false });
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Fetch all annotations in parallel
|
|
227
|
+
const annotationPromises = files.map(async (file) => {
|
|
228
|
+
try {
|
|
229
|
+
const annotations = await getFileAnnotations(user, currentCase, file.id);
|
|
230
|
+
return {
|
|
231
|
+
fileId: file.id,
|
|
232
|
+
includeConfirmation: annotations?.includeConfirmation ?? false,
|
|
233
|
+
isConfirmed: !!annotations?.confirmationData,
|
|
234
|
+
};
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(`Error fetching annotations for file ${file.id}:`, err);
|
|
237
|
+
return {
|
|
238
|
+
fileId: file.id,
|
|
239
|
+
includeConfirmation: false,
|
|
240
|
+
isConfirmed: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Wait for all fetches to complete
|
|
246
|
+
const results = await Promise.all(annotationPromises);
|
|
247
|
+
|
|
248
|
+
// Build the statuses map from results
|
|
249
|
+
const statuses: { [fileId: string]: { includeConfirmation: boolean; isConfirmed: boolean } } = {};
|
|
250
|
+
results.forEach((result) => {
|
|
251
|
+
statuses[result.fileId] = {
|
|
252
|
+
includeConfirmation: result.includeConfirmation,
|
|
253
|
+
isConfirmed: result.isConfirmed,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (isCancelled) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setFileConfirmationStatus(statuses);
|
|
262
|
+
setCaseConfirmationStatus(calculateCaseConfirmationStatus(statuses));
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
fetchConfirmationStatuses();
|
|
266
|
+
|
|
267
|
+
return () => {
|
|
268
|
+
isCancelled = true;
|
|
269
|
+
};
|
|
270
|
+
}, [currentCase, fileIdsKey, user, calculateCaseConfirmationStatus]);
|
|
271
|
+
|
|
272
|
+
// Refresh only selected file confirmation status after confirmation-related data is persisted
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
let isCancelled = false;
|
|
275
|
+
|
|
276
|
+
const refreshSelectedFileConfirmationStatus = async () => {
|
|
277
|
+
if (!currentCase || !user || !selectedFileId || files.length === 0) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const annotations = await getFileAnnotations(user, currentCase, selectedFileId);
|
|
283
|
+
const selectedStatus = {
|
|
284
|
+
includeConfirmation: annotations?.includeConfirmation ?? false,
|
|
285
|
+
isConfirmed: !!annotations?.confirmationData,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (isCancelled) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
setFileConfirmationStatus((previous) => {
|
|
293
|
+
const next = {
|
|
294
|
+
...previous,
|
|
295
|
+
[selectedFileId]: selectedStatus,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
setCaseConfirmationStatus(calculateCaseConfirmationStatus(next));
|
|
299
|
+
return next;
|
|
300
|
+
});
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error(`Error refreshing confirmation status for file ${selectedFileId}:`, err);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
refreshSelectedFileConfirmationStatus();
|
|
307
|
+
|
|
308
|
+
return () => {
|
|
309
|
+
isCancelled = true;
|
|
310
|
+
};
|
|
311
|
+
}, [currentCase, fileIdsKey, user, selectedFileId, confirmationSaveVersion, calculateCaseConfirmationStatus]);
|
|
312
|
+
|
|
313
|
+
const handleCase = async () => {
|
|
314
|
+
setIsLoading(true);
|
|
315
|
+
setError('');
|
|
316
|
+
setCreateCaseError(''); // Clear permission errors when starting new operation
|
|
317
|
+
|
|
318
|
+
if (!validateCaseNumber(caseNumber)) {
|
|
319
|
+
setError('Invalid case number format');
|
|
320
|
+
setIsLoading(false);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const existingCase = await checkExistingCase(user, caseNumber);
|
|
326
|
+
|
|
327
|
+
if (existingCase) {
|
|
328
|
+
// Loading existing case - always allowed
|
|
329
|
+
setCurrentCase(caseNumber);
|
|
330
|
+
onCaseChange(caseNumber);
|
|
331
|
+
const files = await fetchFiles(user, caseNumber, { skipValidation: true });
|
|
332
|
+
setFiles(files);
|
|
333
|
+
setCaseNumber('');
|
|
334
|
+
setSuccessAction('loaded');
|
|
335
|
+
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check if a read-only case with this number exists
|
|
340
|
+
const existingReadOnlyCase = await checkReadOnlyCaseExists(user, caseNumber);
|
|
341
|
+
if (existingReadOnlyCase) {
|
|
342
|
+
setError(`Case "${caseNumber}" already exists as a read-only review case. You cannot create a case with the same number.`);
|
|
343
|
+
setIsLoading(false);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Creating new case - check permissions
|
|
348
|
+
if (!canCreateNewCase) {
|
|
349
|
+
setError(createCaseError || 'You cannot create more cases.');
|
|
350
|
+
setCreateCaseError(''); // Clear duplicate error
|
|
351
|
+
setIsLoading(false);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const newCase = await createNewCase(user, caseNumber);
|
|
356
|
+
setCurrentCase(newCase.caseNumber);
|
|
357
|
+
onCaseChange(newCase.caseNumber);
|
|
358
|
+
setFiles([]);
|
|
359
|
+
setCaseNumber('');
|
|
360
|
+
setSuccessAction('created');
|
|
361
|
+
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
362
|
+
|
|
363
|
+
// Refresh permissions after successful case creation
|
|
364
|
+
// This updates the UI for users with limited permissions
|
|
365
|
+
await checkUserPermissions();
|
|
366
|
+
} catch (err) {
|
|
367
|
+
setError(err instanceof Error ? err.message : 'Failed to load/create case');
|
|
368
|
+
console.error(err);
|
|
369
|
+
} finally {
|
|
370
|
+
setIsLoading(false);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
const handleFileDelete = async (fileId: string) => {
|
|
377
|
+
// Don't allow file deletion for read-only cases
|
|
378
|
+
if (isReadOnly) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!currentCase) return;
|
|
383
|
+
|
|
384
|
+
setFileError('');
|
|
385
|
+
setDeletingFileId(fileId);
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
await deleteFile(user, currentCase, fileId);
|
|
389
|
+
const updatedFiles = files.filter(f => f.id !== fileId);
|
|
390
|
+
setFiles(updatedFiles);
|
|
391
|
+
onImageSelect({ id: 'clear', originalFilename: '/clear.jpg', uploadedAt: '' });
|
|
392
|
+
setImageLoaded(false);
|
|
393
|
+
|
|
394
|
+
// Refresh file upload permissions after successful file deletion
|
|
395
|
+
// Pass the new file count directly to avoid state update timing issues
|
|
396
|
+
await checkFileUploadPermissions(updatedFiles.length);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
setFileError(err instanceof Error ? err.message : 'Delete failed');
|
|
399
|
+
} finally {
|
|
400
|
+
setDeletingFileId(null);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const handleRenameCase = async () => {
|
|
405
|
+
// Don't allow renaming read-only cases
|
|
406
|
+
if (isReadOnly) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!currentCase || !newCaseName) return;
|
|
411
|
+
|
|
412
|
+
if (!validateCaseNumber(newCaseName)) {
|
|
413
|
+
setError('Invalid new case number format');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
setIsRenaming(true);
|
|
418
|
+
setError('');
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
// Check if a read-only case with the new name exists
|
|
422
|
+
const existingReadOnlyCase = await checkReadOnlyCaseExists(user, newCaseName);
|
|
423
|
+
if (existingReadOnlyCase) {
|
|
424
|
+
setError(`Case "${newCaseName}" already exists as a read-only review case. You cannot rename to this case number.`);
|
|
425
|
+
setIsRenaming(false);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
await renameCase(user, currentCase, newCaseName);
|
|
430
|
+
setCurrentCase(newCaseName);
|
|
431
|
+
onCaseChange(newCaseName);
|
|
432
|
+
setNewCaseName('');
|
|
433
|
+
setSuccessAction('loaded');
|
|
434
|
+
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
setError(err instanceof Error ? err.message : 'Failed to rename case');
|
|
437
|
+
} finally {
|
|
438
|
+
setIsRenaming(false);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const handleDeleteCase = async () => {
|
|
443
|
+
// Don't allow deleting read-only cases
|
|
444
|
+
if (isReadOnly) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!currentCase) return;
|
|
449
|
+
|
|
450
|
+
const confirmed = window.confirm(
|
|
451
|
+
`Are you sure you want to delete case ${currentCase}? This will permanently delete all associated files and cannot be undone.`
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
if (!confirmed) return;
|
|
455
|
+
|
|
456
|
+
setIsDeletingCase(true);
|
|
457
|
+
setError('');
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
await deleteCase(user, currentCase);
|
|
461
|
+
setCurrentCase('');
|
|
462
|
+
onCaseChange('');
|
|
463
|
+
setFiles([]);
|
|
464
|
+
setSuccessAction('deleted');
|
|
465
|
+
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
466
|
+
|
|
467
|
+
// Refresh permissions after successful case deletion
|
|
468
|
+
// This allows users with limited permissions to create a new case
|
|
469
|
+
await checkUserPermissions();
|
|
470
|
+
} catch (err) {
|
|
471
|
+
setError(err instanceof Error ? err.message : 'Failed to delete case');
|
|
472
|
+
} finally {
|
|
473
|
+
setIsDeletingCase(false);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const handleImageSelect = (file: FileData) => {
|
|
478
|
+
onImageSelect(file);
|
|
479
|
+
// Prevent notes from opening against stale image state while selection loads.
|
|
480
|
+
setImageLoaded(false);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const selectedFileConfirmationState = selectedFileId
|
|
484
|
+
? fileConfirmationStatus[selectedFileId]
|
|
485
|
+
: undefined;
|
|
486
|
+
|
|
487
|
+
const isCheckingSelectedFileConfirmation = Boolean(
|
|
488
|
+
selectedFileId && !selectedFileConfirmationState
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const isSelectedFileConfirmed =
|
|
492
|
+
isConfirmed || !!selectedFileConfirmationState?.isConfirmed;
|
|
493
|
+
|
|
494
|
+
const isImageNotesDisabled =
|
|
495
|
+
!imageLoaded ||
|
|
496
|
+
isReadOnly ||
|
|
497
|
+
isSelectedFileConfirmed ||
|
|
498
|
+
isUploading ||
|
|
499
|
+
isCheckingSelectedFileConfirmation;
|
|
500
|
+
|
|
501
|
+
const imageNotesTitle = isUploading
|
|
502
|
+
? 'Cannot edit notes while uploading'
|
|
503
|
+
: isCheckingSelectedFileConfirmation
|
|
504
|
+
? 'Checking confirmation status...'
|
|
505
|
+
: isSelectedFileConfirmed
|
|
506
|
+
? 'Cannot edit notes for confirmed images'
|
|
507
|
+
: isReadOnly
|
|
508
|
+
? 'Cannot edit notes for read-only cases'
|
|
509
|
+
: !imageLoaded
|
|
510
|
+
? 'Select an image first'
|
|
511
|
+
: undefined;
|
|
512
|
+
|
|
513
|
+
const handleExport = async (exportCaseNumber: string, format: ExportFormat, includeImages?: boolean) => {
|
|
514
|
+
try {
|
|
515
|
+
if (includeImages) {
|
|
516
|
+
// ZIP export with images - only available for single case exports
|
|
517
|
+
await downloadCaseAsZip(user, exportCaseNumber, format);
|
|
518
|
+
} else {
|
|
519
|
+
// Standard data-only export
|
|
520
|
+
const exportData = await exportCaseData(user, exportCaseNumber, {
|
|
521
|
+
includeMetadata: true
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Download the exported data in the selected format
|
|
525
|
+
if (format === 'json') {
|
|
526
|
+
await downloadCaseAsJSON(user, exportData);
|
|
527
|
+
} else {
|
|
528
|
+
await downloadCaseAsCSV(user, exportData);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
} catch (error) {
|
|
533
|
+
console.error('Export failed:', error);
|
|
534
|
+
throw error; // Re-throw to be handled by the modal
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const handleExportAll = async (onProgress: (current: number, total: number, caseName: string) => void, format: ExportFormat) => {
|
|
539
|
+
try {
|
|
540
|
+
// Export all cases with progress callback
|
|
541
|
+
const exportData = await exportAllCases(user, {
|
|
542
|
+
includeMetadata: true
|
|
543
|
+
}, onProgress);
|
|
544
|
+
|
|
545
|
+
// Download the exported data in the selected format
|
|
546
|
+
if (format === 'json') {
|
|
547
|
+
await downloadAllCasesAsJSON(user, exportData);
|
|
548
|
+
} else {
|
|
549
|
+
await downloadAllCasesAsCSV(user, exportData);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
} catch (error) {
|
|
553
|
+
console.error('Export all failed:', error);
|
|
554
|
+
throw error; // Re-throw to be handled by the modal
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
return (
|
|
559
|
+
<div className={styles.caseSection}>
|
|
560
|
+
<div className={styles.caseSection}>
|
|
561
|
+
<h4>Case Management</h4>
|
|
562
|
+
{limitsDescription && (
|
|
563
|
+
<p className={styles.limitsInfo}>
|
|
564
|
+
{limitsDescription}
|
|
565
|
+
</p>
|
|
566
|
+
)}
|
|
567
|
+
<div className={`${styles.caseInput} mb-4`}>
|
|
568
|
+
<input
|
|
569
|
+
type="text"
|
|
570
|
+
value={caseNumber}
|
|
571
|
+
onChange={(e) => setCaseNumber(e.target.value)}
|
|
572
|
+
placeholder="Case #"
|
|
573
|
+
/>
|
|
574
|
+
</div>
|
|
575
|
+
<div className={`${styles.caseLoad} mb-4`}>
|
|
576
|
+
<button
|
|
577
|
+
onClick={handleCase}
|
|
578
|
+
disabled={isLoading || !caseNumber || permissionChecking || (isReadOnly && !!currentCase) || isUploading}
|
|
579
|
+
title={
|
|
580
|
+
isUploading
|
|
581
|
+
? "Cannot load/create cases while uploading files"
|
|
582
|
+
: (isReadOnly && currentCase)
|
|
583
|
+
? "Cannot load/create cases while reviewing a read-only case. Clear the current case first."
|
|
584
|
+
: (!canCreateNewCase ? createCaseError : undefined)
|
|
585
|
+
}
|
|
586
|
+
>
|
|
587
|
+
{isLoading ? 'Loading...' : permissionChecking ? 'Checking permissions...' : 'Load/Create Case'}
|
|
588
|
+
</button>
|
|
589
|
+
</div>
|
|
590
|
+
<div className={styles.caseInput}>
|
|
591
|
+
<button
|
|
592
|
+
onClick={() => setIsModalOpen(true)}
|
|
593
|
+
className={styles.listButton}
|
|
594
|
+
disabled={isUploading}
|
|
595
|
+
title={isUploading ? "Cannot list cases while uploading files" : undefined}
|
|
596
|
+
>
|
|
597
|
+
List All Cases
|
|
598
|
+
</button>
|
|
599
|
+
</div>
|
|
600
|
+
{error && <p className={styles.error}>{error}</p>}
|
|
601
|
+
{successAction && (
|
|
602
|
+
<p className={styles.success}>
|
|
603
|
+
Case {currentCase} {successAction} successfully!
|
|
604
|
+
</p>
|
|
605
|
+
)}
|
|
606
|
+
<CasesModal
|
|
607
|
+
isOpen={isModalOpen}
|
|
608
|
+
onClose={() => setIsModalOpen(false)}
|
|
609
|
+
onSelectCase={setCaseNumber}
|
|
610
|
+
currentCase={currentCase || ''}
|
|
611
|
+
user={user}
|
|
612
|
+
/>
|
|
613
|
+
|
|
614
|
+
<CaseExport
|
|
615
|
+
isOpen={isExportModalOpen}
|
|
616
|
+
onClose={() => setIsExportModalOpen(false)}
|
|
617
|
+
onExport={handleExport}
|
|
618
|
+
onExportAll={handleExportAll}
|
|
619
|
+
currentCaseNumber={currentCase || ''}
|
|
620
|
+
isReadOnly={isReadOnly}
|
|
621
|
+
/>
|
|
622
|
+
|
|
623
|
+
<FilesModal
|
|
624
|
+
isOpen={isFilesModalOpen}
|
|
625
|
+
onClose={() => setIsFilesModalOpen(false)}
|
|
626
|
+
onFileSelect={handleImageSelect}
|
|
627
|
+
currentCase={currentCase}
|
|
628
|
+
files={files}
|
|
629
|
+
setFiles={setFiles}
|
|
630
|
+
isReadOnly={isReadOnly}
|
|
631
|
+
selectedFileId={selectedFileId}
|
|
632
|
+
/>
|
|
633
|
+
|
|
634
|
+
<div className={styles.filesSection}>
|
|
635
|
+
<div className={isReadOnly && currentCase ? styles.readOnlyContainer : styles.caseHeader}>
|
|
636
|
+
<h4 className={`${styles.caseNumber} ${
|
|
637
|
+
currentCase && caseConfirmationStatus.includeConfirmation
|
|
638
|
+
? caseConfirmationStatus.isConfirmed
|
|
639
|
+
? styles.caseConfirmed
|
|
640
|
+
: styles.caseNotConfirmed
|
|
641
|
+
: ''
|
|
642
|
+
}`}>
|
|
643
|
+
{currentCase || 'No Case Selected'}
|
|
644
|
+
</h4>
|
|
645
|
+
{isReadOnly && currentCase && (
|
|
646
|
+
<div className={styles.readOnlyBadge}>(Read-Only)</div>
|
|
647
|
+
)}
|
|
648
|
+
</div>
|
|
649
|
+
{currentCase && (
|
|
650
|
+
<ImageUploadZone
|
|
651
|
+
user={user}
|
|
652
|
+
currentCase={currentCase}
|
|
653
|
+
isReadOnly={isReadOnly}
|
|
654
|
+
canUploadNewFile={canUploadNewFile}
|
|
655
|
+
uploadFileError={uploadFileError}
|
|
656
|
+
onFilesChanged={setFiles}
|
|
657
|
+
onUploadPermissionCheck={checkFileUploadPermissions}
|
|
658
|
+
currentFiles={files}
|
|
659
|
+
onUploadStatusChange={onUploadStatusChange}
|
|
660
|
+
onUploadComplete={onUploadComplete}
|
|
661
|
+
/>
|
|
662
|
+
)}
|
|
663
|
+
|
|
664
|
+
{/* Files Modal Button - positioned between upload and file list */}
|
|
665
|
+
{currentCase && (
|
|
666
|
+
<div className={styles.filesModalSection}>
|
|
667
|
+
<button
|
|
668
|
+
className={styles.filesModalButton}
|
|
669
|
+
onClick={() => setIsFilesModalOpen(true)}
|
|
670
|
+
disabled={files.length === 0 || isUploading}
|
|
671
|
+
title={isUploading ? "Cannot view files while uploading" : files.length === 0 ? "No files to view" : "View all files in modal"}
|
|
672
|
+
>
|
|
673
|
+
View All Files ({files.length})
|
|
674
|
+
</button>
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
677
|
+
|
|
678
|
+
{!currentCase ? (
|
|
679
|
+
<p className={styles.emptyState}>Create or select a case to view files</p>
|
|
680
|
+
) : files.length === 0 ? (
|
|
681
|
+
<p className={styles.emptyState}>No files found for {currentCase}</p>
|
|
682
|
+
) : (
|
|
683
|
+
<>
|
|
684
|
+
{!canUploadNewFile && (
|
|
685
|
+
<div className={styles.limitReached}>
|
|
686
|
+
<p>Upload limit reached for this case</p>
|
|
687
|
+
</div>
|
|
688
|
+
)}
|
|
689
|
+
<ul className={styles.fileList}>
|
|
690
|
+
{files.map((file) => {
|
|
691
|
+
const confirmationStatus = fileConfirmationStatus[file.id];
|
|
692
|
+
let confirmationClass = '';
|
|
693
|
+
|
|
694
|
+
if (confirmationStatus?.includeConfirmation) {
|
|
695
|
+
confirmationClass = confirmationStatus.isConfirmed
|
|
696
|
+
? styles.fileItemConfirmed
|
|
697
|
+
: styles.fileItemNotConfirmed;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return (
|
|
701
|
+
<li key={file.id}
|
|
702
|
+
className={`${styles.fileItem} ${selectedFileId === file.id ? styles.active : ''} ${confirmationClass}`}>
|
|
703
|
+
<button
|
|
704
|
+
className={styles.fileButton}
|
|
705
|
+
onClick={() => handleImageSelect(file)}
|
|
706
|
+
onKeyDown={(e) => e.key === 'Enter' && handleImageSelect(file)}
|
|
707
|
+
disabled={isUploading}
|
|
708
|
+
title={isUploading ? "Cannot select files while uploading" : undefined}
|
|
709
|
+
>
|
|
710
|
+
<span className={styles.fileName}>{file.originalFilename}</span>
|
|
711
|
+
</button>
|
|
712
|
+
<button
|
|
713
|
+
onClick={() => {
|
|
714
|
+
if (window.confirm('Are you sure you want to delete this file? This action cannot be undone.')) {
|
|
715
|
+
handleFileDelete(file.id);
|
|
716
|
+
}
|
|
717
|
+
}}
|
|
718
|
+
className={styles.deleteButton}
|
|
719
|
+
aria-label="Delete file"
|
|
720
|
+
disabled={isReadOnly || deletingFileId === file.id || isUploading}
|
|
721
|
+
style={{ opacity: (isReadOnly || isUploading) ? 0.5 : 1, cursor: (isReadOnly || isUploading) ? 'not-allowed' : 'pointer' }}
|
|
722
|
+
title={isUploading ? "Cannot delete while uploading" : undefined}
|
|
723
|
+
>
|
|
724
|
+
{deletingFileId === file.id ? '⏳' : '×'}
|
|
725
|
+
</button>
|
|
726
|
+
</li>
|
|
727
|
+
);
|
|
728
|
+
})}
|
|
729
|
+
</ul>
|
|
730
|
+
</>
|
|
731
|
+
)}
|
|
732
|
+
</div>
|
|
733
|
+
<div className={`${styles.sidebarToggle} mb-4`}>
|
|
734
|
+
<button
|
|
735
|
+
onClick={onNotesClick}
|
|
736
|
+
disabled={isImageNotesDisabled}
|
|
737
|
+
title={imageNotesTitle}
|
|
738
|
+
>
|
|
739
|
+
Image Notes
|
|
740
|
+
</button>
|
|
741
|
+
</div>
|
|
742
|
+
{currentCase && (
|
|
743
|
+
<div className={styles.caseActionsSection}>
|
|
744
|
+
<button
|
|
745
|
+
onClick={() => setShowCaseActions(!showCaseActions)}
|
|
746
|
+
className={styles.caseActionsButton}
|
|
747
|
+
disabled={isUploading}
|
|
748
|
+
title={isUploading ? "Cannot access case actions while uploading" : undefined}
|
|
749
|
+
>
|
|
750
|
+
{showCaseActions ? 'Hide Case Actions' : 'Case Actions'}
|
|
751
|
+
</button>
|
|
752
|
+
|
|
753
|
+
{showCaseActions && !isUploading && (
|
|
754
|
+
<div className={styles.caseActionsContent}>
|
|
755
|
+
{/* Export Case Data Section */}
|
|
756
|
+
<div className={styles.exportSection}>
|
|
757
|
+
<button
|
|
758
|
+
onClick={() => setIsExportModalOpen(true)}
|
|
759
|
+
className={styles.exportButton}
|
|
760
|
+
disabled={isUploading}
|
|
761
|
+
title={isUploading ? "Cannot export while uploading" : undefined}
|
|
762
|
+
>
|
|
763
|
+
Export Case Data
|
|
764
|
+
</button>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
{/* Audit Trail Section - Available for all cases */}
|
|
768
|
+
<div className={styles.auditTrailSection}>
|
|
769
|
+
<button
|
|
770
|
+
onClick={() => setIsAuditTrailOpen(true)}
|
|
771
|
+
className={styles.auditTrailButton}
|
|
772
|
+
disabled={isUploading}
|
|
773
|
+
title={isUploading ? "Cannot view audit trail while uploading" : undefined}
|
|
774
|
+
>
|
|
775
|
+
Audit Trail
|
|
776
|
+
</button>
|
|
777
|
+
</div>
|
|
778
|
+
|
|
779
|
+
{/* Rename/Delete Section - Only for owned cases */}
|
|
780
|
+
{!isReadOnly && (
|
|
781
|
+
<div className={styles.renameDeleteSection}>
|
|
782
|
+
<div className={`${styles.caseRename} mb-4`}>
|
|
783
|
+
<input
|
|
784
|
+
type="text"
|
|
785
|
+
value={newCaseName}
|
|
786
|
+
onChange={(e) => setNewCaseName(e.target.value)}
|
|
787
|
+
placeholder="New Case Number"
|
|
788
|
+
disabled={isUploading}
|
|
789
|
+
/>
|
|
790
|
+
<button
|
|
791
|
+
onClick={handleRenameCase}
|
|
792
|
+
disabled={isRenaming || !newCaseName || isUploading}
|
|
793
|
+
title={isUploading ? "Cannot rename while uploading" : undefined}
|
|
794
|
+
>
|
|
795
|
+
{isRenaming ? 'Renaming...' : 'Rename Case'}
|
|
796
|
+
</button>
|
|
797
|
+
</div>
|
|
798
|
+
|
|
799
|
+
<div className={styles.deleteCaseSection}>
|
|
800
|
+
<button
|
|
801
|
+
onClick={handleDeleteCase}
|
|
802
|
+
disabled={isDeletingCase || isUploading}
|
|
803
|
+
className={styles.deleteWarningButton}
|
|
804
|
+
title={isUploading ? "Cannot delete while uploading" : undefined}
|
|
805
|
+
>
|
|
806
|
+
{isDeletingCase ? 'Deleting...' : 'Delete Case'}
|
|
807
|
+
</button>
|
|
808
|
+
</div>
|
|
809
|
+
</div>
|
|
810
|
+
)}
|
|
811
|
+
</div>
|
|
812
|
+
)}
|
|
813
|
+
</div>
|
|
814
|
+
)}
|
|
815
|
+
|
|
816
|
+
{/* Unified Audit Viewer */}
|
|
817
|
+
<UserAuditViewer
|
|
818
|
+
caseNumber={currentCase || ''}
|
|
819
|
+
isOpen={isAuditTrailOpen}
|
|
820
|
+
onClose={() => setIsAuditTrailOpen(false)}
|
|
821
|
+
title={`Audit Trail - Case ${currentCase}`}
|
|
822
|
+
/>
|
|
823
|
+
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
};
|