@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.
Files changed (118) hide show
  1. package/.env.example +8 -0
  2. package/app/components/actions/case-export/core-export.ts +14 -8
  3. package/app/components/actions/case-export/data-processing.ts +1 -0
  4. package/app/components/actions/case-export/download-handlers.ts +7 -0
  5. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  6. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  7. package/app/components/actions/case-import/orchestrator.ts +78 -32
  8. package/app/components/actions/case-import/storage-operations.ts +97 -8
  9. package/app/components/actions/case-import/zip-processing.ts +159 -86
  10. package/app/components/actions/case-manage.ts +430 -8
  11. package/app/components/actions/confirm-export.ts +13 -4
  12. package/app/components/actions/generate-pdf.ts +10 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +137 -945
  15. package/app/components/audit/user-audit.module.css +41 -0
  16. package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
  17. package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
  18. package/app/components/audit/viewer/audit-filters-panel.tsx +307 -0
  19. package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
  20. package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
  21. package/app/components/audit/viewer/audit-viewer-utils.ts +123 -0
  22. package/app/components/audit/viewer/types.ts +1 -0
  23. package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -0
  24. package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
  25. package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
  26. package/app/components/auth/mfa-enrollment.module.css +13 -5
  27. package/app/components/auth/mfa-verification.module.css +13 -5
  28. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  29. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  30. package/app/components/canvas/canvas.module.css +64 -54
  31. package/app/components/canvas/canvas.tsx +17 -16
  32. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  33. package/app/components/canvas/confirmation/confirmation.tsx +17 -47
  34. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  35. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  36. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  37. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  38. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  39. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  40. package/app/components/navbar/navbar.module.css +447 -0
  41. package/app/components/navbar/navbar.tsx +377 -0
  42. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +2 -0
  43. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
  44. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  45. package/app/components/sidebar/case-export/case-export.tsx +14 -77
  46. package/app/components/sidebar/case-import/case-import.module.css +25 -0
  47. package/app/components/sidebar/case-import/case-import.tsx +64 -40
  48. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  49. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  50. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  51. package/app/components/sidebar/cases/cases-modal.module.css +45 -9
  52. package/app/components/sidebar/cases/cases-modal.tsx +16 -16
  53. package/app/components/sidebar/cases/cases.module.css +62 -21
  54. package/app/components/sidebar/files/files-modal.module.css +46 -10
  55. package/app/components/sidebar/files/files-modal.tsx +22 -23
  56. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  57. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  58. package/app/components/sidebar/notes/notes-modal.tsx +18 -17
  59. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  60. package/app/components/sidebar/notes/notes.module.css +155 -0
  61. package/app/components/sidebar/sidebar-container.tsx +15 -28
  62. package/app/components/sidebar/sidebar.module.css +7 -71
  63. package/app/components/sidebar/sidebar.tsx +24 -125
  64. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  65. package/app/components/toast/toast.module.css +2 -1
  66. package/app/components/toast/toast.tsx +16 -11
  67. package/app/components/user/delete-account.tsx +10 -31
  68. package/app/components/user/inactivity-warning.module.css +9 -6
  69. package/app/components/user/inactivity-warning.tsx +15 -2
  70. package/app/components/user/manage-profile.module.css +2 -0
  71. package/app/components/user/manage-profile.tsx +108 -40
  72. package/app/hooks/useOverlayDismiss.ts +116 -0
  73. package/app/routes/auth/login.example.tsx +19 -8
  74. package/app/routes/auth/login.tsx +785 -774
  75. package/app/routes/auth/passwordReset.module.css +23 -13
  76. package/app/routes/striae/striae.module.css +10 -3
  77. package/app/routes/striae/striae.tsx +477 -31
  78. package/app/routes.ts +7 -0
  79. package/app/services/audit/audit-export-csv.ts +2 -0
  80. package/app/services/audit/audit.service.ts +202 -32
  81. package/app/services/audit/builders/audit-entry-builder.ts +2 -1
  82. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  83. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  84. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
  85. package/app/services/audit/builders/index.ts +1 -0
  86. package/app/types/audit.ts +5 -2
  87. package/app/types/case.ts +29 -0
  88. package/app/types/import.ts +3 -0
  89. package/app/types/user.ts +1 -0
  90. package/app/utils/data/permissions.ts +17 -1
  91. package/app/utils/forensics/audit-export-signature.ts +5 -1
  92. package/app/utils/forensics/confirmation-signature.ts +3 -0
  93. package/app/utils/forensics/export-verification.ts +497 -22
  94. package/functions/api/pdf/[[path]].ts +32 -1
  95. package/load-context.ts +9 -0
  96. package/package.json +6 -2
  97. package/primershear.emails.example +6 -0
  98. package/scripts/deploy-pages-secrets.sh +6 -0
  99. package/scripts/deploy-primershear-emails.sh +167 -0
  100. package/worker-configuration.d.ts +7493 -7491
  101. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  102. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  103. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  104. package/workers/data-worker/wrangler.jsonc.example +1 -1
  105. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  106. package/workers/image-worker/wrangler.jsonc.example +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  110. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
  111. package/workers/pdf-worker/src/report-types.ts +3 -0
  112. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  113. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  114. package/workers/user-worker/src/user-worker.example.ts +6 -1
  115. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  116. package/workers/user-worker/wrangler.jsonc.example +1 -1
  117. package/wrangler.toml.example +1 -1
  118. 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, type CaseActionType } from '~/types';
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
- {currentCase && !showCaseManagement ? (
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
- <div className={isReadOnly && currentCase ? styles.readOnlyContainer : styles.caseHeader}>
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
- {!currentCase ? (
747
- <p className={styles.emptyState}>Create or select a case to view files</p>
748
- ) : files.length === 0 ? (
749
- <p className={styles.emptyState}>No files found for {currentCase}</p>
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
- </ul>
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={`${styles.sidebarToggle} mb-4`}>
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
  };