@striae-org/striae 3.2.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -1
- package/app/components/actions/case-export/core-export.ts +5 -2
- package/app/components/actions/case-export/download-handlers.ts +51 -3
- package/app/components/actions/case-import/confirmation-import.ts +65 -40
- package/app/components/actions/case-import/confirmation-package.ts +86 -0
- package/app/components/actions/case-import/image-operations.ts +20 -49
- package/app/components/actions/case-import/index.ts +1 -0
- package/app/components/actions/case-import/orchestrator.ts +13 -3
- package/app/components/actions/case-import/storage-operations.ts +54 -89
- package/app/components/actions/case-import/validation.ts +7 -111
- package/app/components/actions/case-import/zip-processing.ts +44 -2
- package/app/components/actions/case-manage.ts +15 -27
- package/app/components/actions/confirm-export.ts +44 -13
- package/app/components/actions/generate-pdf.ts +3 -7
- package/app/components/actions/image-manage.ts +63 -129
- package/app/components/button/button.module.css +12 -8
- package/app/components/form/form-button.tsx +1 -1
- package/app/components/form/form.module.css +9 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
- package/app/components/sidebar/case-export/case-export.tsx +13 -60
- package/app/components/sidebar/case-import/case-import.tsx +18 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
- package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +122 -52
- package/app/components/sidebar/cases/cases.module.css +101 -18
- package/app/components/sidebar/notes/notes.module.css +33 -13
- package/app/components/sidebar/sidebar.module.css +0 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/components/user/manage-profile.tsx +1 -1
- package/app/components/user/mfa-phone-update.tsx +15 -12
- package/app/config-example/config.json +2 -8
- package/app/hooks/useInactivityTimeout.ts +2 -5
- package/app/root.tsx +96 -65
- package/app/routes/auth/login.tsx +132 -11
- package/app/routes/auth/route.ts +4 -3
- package/app/routes/striae/striae.tsx +4 -8
- package/app/services/audit/audit-api-client.ts +40 -0
- package/app/services/audit/audit-worker-client.ts +14 -17
- package/app/styles/root.module.css +13 -101
- package/app/tailwind.css +9 -2
- package/app/utils/SHA256.ts +5 -1
- package/app/utils/auth.ts +5 -32
- package/app/utils/confirmation-signature.ts +5 -1
- package/app/utils/data-api-client.ts +43 -0
- package/app/utils/data-operations.ts +59 -75
- package/app/utils/export-verification.ts +353 -0
- package/app/utils/image-api-client.ts +130 -0
- package/app/utils/pdf-api-client.ts +43 -0
- package/app/utils/permissions.ts +10 -23
- package/app/utils/signature-utils.ts +74 -4
- package/app/utils/user-api-client.ts +90 -0
- package/functions/api/_shared/firebase-auth.ts +255 -0
- package/functions/api/audit/[[path]].ts +150 -0
- package/functions/api/data/[[path]].ts +141 -0
- package/functions/api/image/[[path]].ts +127 -0
- package/functions/api/pdf/[[path]].ts +110 -0
- package/functions/api/user/[[path]].ts +196 -0
- package/package.json +8 -4
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +39 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/react-router.config.ts +5 -0
- package/scripts/deploy-all.sh +22 -8
- package/scripts/deploy-config.sh +143 -148
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -8
- package/workers/data-worker/wrangler.jsonc.example +1 -8
- package/workers/image-worker/wrangler.jsonc.example +1 -8
- package/workers/keys-worker/wrangler.jsonc.example +2 -9
- package/workers/pdf-worker/scripts/generate-assets.js +94 -0
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -8
- package/workers/user-worker/src/user-worker.example.ts +121 -41
- package/workers/user-worker/wrangler.jsonc.example +1 -8
- package/wrangler.toml.example +1 -1
- package/app/styles/legal-pages.module.css +0 -113
- package/public/favicon.svg +0 -9
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
useFilePreview,
|
|
20
20
|
useImportExecution,
|
|
21
21
|
isValidImportFile,
|
|
22
|
-
|
|
22
|
+
resolveImportType,
|
|
23
23
|
resetFileInput
|
|
24
24
|
} from './index';
|
|
25
25
|
import styles from './case-import.module.css';
|
|
@@ -146,12 +146,18 @@ export const CaseImport = ({
|
|
|
146
146
|
clearMessages();
|
|
147
147
|
|
|
148
148
|
if (!isValidImportFile(file)) {
|
|
149
|
-
setError('Only ZIP files
|
|
149
|
+
setError('Only Striae case ZIP files, confirmation ZIP files, or confirmation JSON files are allowed.');
|
|
150
|
+
clearImportData();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const importType = await resolveImportType(file);
|
|
155
|
+
if (!importType) {
|
|
156
|
+
setError('The selected file is not a supported Striae case or confirmation import package.');
|
|
150
157
|
clearImportData();
|
|
151
158
|
return;
|
|
152
159
|
}
|
|
153
160
|
|
|
154
|
-
const importType = getImportType(file);
|
|
155
161
|
updateImportState({
|
|
156
162
|
selectedFile: file,
|
|
157
163
|
importType
|
|
@@ -172,12 +178,18 @@ export const CaseImport = ({
|
|
|
172
178
|
clearMessages();
|
|
173
179
|
|
|
174
180
|
if (!isValidImportFile(file)) {
|
|
175
|
-
setError('Only ZIP files
|
|
181
|
+
setError('Only Striae case ZIP files, confirmation ZIP files, or confirmation JSON files are allowed.');
|
|
182
|
+
clearImportData();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const importType = await resolveImportType(file);
|
|
187
|
+
if (!importType) {
|
|
188
|
+
setError('The selected file is not a supported Striae case or confirmation import package.');
|
|
176
189
|
clearImportData();
|
|
177
190
|
return;
|
|
178
191
|
}
|
|
179
192
|
|
|
180
|
-
const importType = getImportType(file);
|
|
181
193
|
updateImportState({
|
|
182
194
|
selectedFile: file,
|
|
183
195
|
importType
|
|
@@ -404,7 +416,7 @@ export const CaseImport = ({
|
|
|
404
416
|
<br />
|
|
405
417
|
<h3 className={styles.instructionsTitle}>Confirmation Import Instructions:</h3>
|
|
406
418
|
<ul className={styles.instructionsList}>
|
|
407
|
-
<li>
|
|
419
|
+
<li>Confirmation imports accept either confirmation JSON files or confirmation ZIP packages exported from Striae</li>
|
|
408
420
|
<li>Only one confirmation file can be imported at a time</li>
|
|
409
421
|
<li>Confirmed images will become read-only and cannot be modified</li>
|
|
410
422
|
<li>If an image has a pre-existing confirmation, it will be skipped</li>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import type { User } from 'firebase/auth';
|
|
3
|
-
import { previewCaseImport } from '~/components/actions/case-review';
|
|
3
|
+
import { previewCaseImport, extractConfirmationImportPackage } from '~/components/actions/case-review';
|
|
4
4
|
import { type CaseImportPreview } from '~/types';
|
|
5
5
|
import { type ConfirmationPreview } from '../components/ConfirmationPreviewSection';
|
|
6
6
|
|
|
@@ -56,8 +56,8 @@ export const useFilePreview = (
|
|
|
56
56
|
|
|
57
57
|
setIsLoadingPreview(true);
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
60
|
-
const parsed =
|
|
59
|
+
const { confirmationData } = await extractConfirmationImportPackage(file);
|
|
60
|
+
const parsed = confirmationData as unknown;
|
|
61
61
|
|
|
62
62
|
if (!isRecord(parsed)) {
|
|
63
63
|
throw new Error('Invalid confirmation data format');
|
|
@@ -104,7 +104,9 @@ export const useFilePreview = (
|
|
|
104
104
|
setConfirmationPreview(preview);
|
|
105
105
|
} catch (error) {
|
|
106
106
|
console.error('Error loading confirmation preview:', error);
|
|
107
|
-
setError(
|
|
107
|
+
setError(
|
|
108
|
+
`Failed to read confirmation data: ${error instanceof Error ? error.message : 'Invalid confirmation package format'}`
|
|
109
|
+
);
|
|
108
110
|
clearImportData();
|
|
109
111
|
} finally {
|
|
110
112
|
setIsLoadingPreview(false);
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { isConfirmationDataFile } from '~/components/actions/case-review';
|
|
2
2
|
|
|
3
|
+
const CASE_EXPORT_DATA_FILE_REGEX = /_data\.(json|csv)$/i;
|
|
4
|
+
const CONFIRMATION_EXPORT_FILE_REGEX = /^confirmation-data-.*\.json$/i;
|
|
5
|
+
const FORENSIC_MANIFEST_FILE_NAME = 'forensic_manifest.json';
|
|
6
|
+
|
|
7
|
+
function getLeafFileName(path: string): string {
|
|
8
|
+
const segments = path.split('/').filter(Boolean);
|
|
9
|
+
return segments.length > 0 ? segments[segments.length - 1] : path;
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
* Check if a file is a valid ZIP file
|
|
5
14
|
*/
|
|
@@ -13,8 +22,10 @@ export const isValidZipFile = (file: File): boolean => {
|
|
|
13
22
|
* Check if a file is a valid confirmation JSON file
|
|
14
23
|
*/
|
|
15
24
|
export const isValidConfirmationFile = (file: File): boolean => {
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
const lowerName = file.name.toLowerCase();
|
|
26
|
+
const jsonType = file.type === 'application/json' || file.type === '';
|
|
27
|
+
|
|
28
|
+
return lowerName.endsWith('.json') && jsonType && isConfirmationDataFile(file.name);
|
|
18
29
|
};
|
|
19
30
|
|
|
20
31
|
/**
|
|
@@ -33,6 +44,50 @@ export const getImportType = (file: File): 'case' | 'confirmation' | null => {
|
|
|
33
44
|
return null;
|
|
34
45
|
};
|
|
35
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Resolve import type, including ZIP package inspection.
|
|
49
|
+
* Case ZIPs are identified by case data files or FORENSIC_MANIFEST.json.
|
|
50
|
+
* Confirmation ZIPs are identified by confirmation-data-*.json.
|
|
51
|
+
*/
|
|
52
|
+
export const resolveImportType = async (file: File): Promise<'case' | 'confirmation' | null> => {
|
|
53
|
+
if (isValidConfirmationFile(file)) {
|
|
54
|
+
return 'confirmation';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isValidZipFile(file)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const JSZip = (await import('jszip')).default;
|
|
63
|
+
const zip = await JSZip.loadAsync(file);
|
|
64
|
+
const fileEntries = Object.keys(zip.files).filter((path) => !zip.files[path].dir);
|
|
65
|
+
|
|
66
|
+
const hasCaseData = fileEntries.some((path) =>
|
|
67
|
+
CASE_EXPORT_DATA_FILE_REGEX.test(getLeafFileName(path))
|
|
68
|
+
);
|
|
69
|
+
const hasManifest = fileEntries.some(
|
|
70
|
+
(path) => getLeafFileName(path).toLowerCase() === FORENSIC_MANIFEST_FILE_NAME
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (hasCaseData || hasManifest) {
|
|
74
|
+
return 'case';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hasConfirmationData = fileEntries.some((path) =>
|
|
78
|
+
CONFIRMATION_EXPORT_FILE_REGEX.test(getLeafFileName(path))
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (hasConfirmationData) {
|
|
82
|
+
return 'confirmation';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
36
91
|
/**
|
|
37
92
|
* Reset file input element
|
|
38
93
|
*/
|
|
@@ -2,6 +2,7 @@ import type { User } from 'firebase/auth';
|
|
|
2
2
|
import type * as CaseExportActions from '../../actions/case-export';
|
|
3
3
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
4
|
import styles from './cases.module.css';
|
|
5
|
+
import { Toast } from '~/components/toast/toast';
|
|
5
6
|
import { CasesModal } from './cases-modal';
|
|
6
7
|
import { FilesModal } from '../files/files-modal';
|
|
7
8
|
import { CaseExport, type ExportFormat } from '../case-export/case-export';
|
|
@@ -103,7 +104,11 @@ export const CaseSidebar = ({
|
|
|
103
104
|
const [, setFileError] = useState('');
|
|
104
105
|
const [newCaseName, setNewCaseName] = useState('');
|
|
105
106
|
const [showCaseActions, setShowCaseActions] = useState(false);
|
|
107
|
+
const [showCaseManagement, setShowCaseManagement] = useState(false);
|
|
106
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');
|
|
107
112
|
const [canUploadNewFile, setCanUploadNewFile] = useState(true);
|
|
108
113
|
const [createCaseError, setCreateCaseError] = useState('');
|
|
109
114
|
const [uploadFileError, setUploadFileError] = useState('');
|
|
@@ -313,6 +318,24 @@ export const CaseSidebar = ({
|
|
|
313
318
|
isCancelled = true;
|
|
314
319
|
};
|
|
315
320
|
}, [currentCase, fileIdsKey, user, selectedFileId, confirmationSaveVersion, files.length, calculateCaseConfirmationStatus]);
|
|
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]);
|
|
316
339
|
|
|
317
340
|
const handleCase = async () => {
|
|
318
341
|
setIsLoading(true);
|
|
@@ -336,6 +359,7 @@ export const CaseSidebar = ({
|
|
|
336
359
|
setFiles(files);
|
|
337
360
|
setCaseNumber('');
|
|
338
361
|
setSuccessAction('loaded');
|
|
362
|
+
setShowCaseManagement(false);
|
|
339
363
|
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
340
364
|
return;
|
|
341
365
|
}
|
|
@@ -362,6 +386,7 @@ export const CaseSidebar = ({
|
|
|
362
386
|
setFiles([]);
|
|
363
387
|
setCaseNumber('');
|
|
364
388
|
setSuccessAction('created');
|
|
389
|
+
setShowCaseManagement(false);
|
|
365
390
|
setTimeout(() => setSuccessAction(null), SUCCESS_MESSAGE_TIMEOUT);
|
|
366
391
|
|
|
367
392
|
// Refresh permissions after successful case creation
|
|
@@ -514,25 +539,40 @@ const handleImageSelect = (file: FileData) => {
|
|
|
514
539
|
? 'Select an image first'
|
|
515
540
|
: undefined;
|
|
516
541
|
|
|
517
|
-
const handleExport = async (exportCaseNumber: string, format: ExportFormat, includeImages?: boolean) => {
|
|
542
|
+
const handleExport = async (exportCaseNumber: string, format: ExportFormat, includeImages?: boolean, onProgress?: (progress: number, label: string) => void) => {
|
|
518
543
|
try {
|
|
519
544
|
const caseExportActions = await loadCaseExportActions();
|
|
520
545
|
|
|
521
546
|
if (includeImages) {
|
|
522
547
|
// ZIP export with images - only available for single case exports
|
|
523
|
-
await caseExportActions.downloadCaseAsZip(user, exportCaseNumber, format)
|
|
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
|
+
});
|
|
524
555
|
} else {
|
|
525
556
|
// Standard data-only export
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
|
|
530
569
|
// Download the exported data in the selected format
|
|
531
570
|
if (format === 'json') {
|
|
532
571
|
await caseExportActions.downloadCaseAsJSON(user, exportData);
|
|
533
572
|
} else {
|
|
534
573
|
await caseExportActions.downloadCaseAsCSV(user, exportData);
|
|
535
574
|
}
|
|
575
|
+
onProgress?.(100, 'Complete');
|
|
536
576
|
}
|
|
537
577
|
|
|
538
578
|
} catch (error) {
|
|
@@ -564,53 +604,73 @@ const handleImageSelect = (file: FileData) => {
|
|
|
564
604
|
};
|
|
565
605
|
|
|
566
606
|
return (
|
|
607
|
+
<>
|
|
567
608
|
<div className={styles.caseSection}>
|
|
568
|
-
|
|
569
|
-
<h4>Case Management</h4>
|
|
570
|
-
{limitsDescription && (
|
|
571
|
-
<p className={styles.limitsInfo}>
|
|
572
|
-
{limitsDescription}
|
|
573
|
-
</p>
|
|
574
|
-
)}
|
|
575
|
-
<div className={`${styles.caseInput} mb-4`}>
|
|
576
|
-
<input
|
|
577
|
-
type="text"
|
|
578
|
-
value={caseNumber}
|
|
579
|
-
onChange={(e) => setCaseNumber(e.target.value)}
|
|
580
|
-
placeholder="Case #"
|
|
581
|
-
/>
|
|
582
|
-
</div>
|
|
609
|
+
{currentCase && !showCaseManagement ? (
|
|
583
610
|
<div className={`${styles.caseLoad} mb-4`}>
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
className={styles.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
+
)}
|
|
614
674
|
<CasesModal
|
|
615
675
|
isOpen={isModalOpen}
|
|
616
676
|
onClose={() => setIsModalOpen(false)}
|
|
@@ -830,6 +890,16 @@ return (
|
|
|
830
890
|
/>
|
|
831
891
|
|
|
832
892
|
</div>
|
|
833
|
-
|
|
893
|
+
<Toast
|
|
894
|
+
message={toastMessage}
|
|
895
|
+
type={toastType}
|
|
896
|
+
isVisible={isToastVisible}
|
|
897
|
+
onClose={() => {
|
|
898
|
+
setIsToastVisible(false);
|
|
899
|
+
setError('');
|
|
900
|
+
setSuccessAction(null);
|
|
901
|
+
}}
|
|
902
|
+
/>
|
|
903
|
+
</>
|
|
834
904
|
);
|
|
835
905
|
};
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
.caseInput input:focus {
|
|
51
51
|
outline: none;
|
|
52
52
|
border-color: #0d6efd;
|
|
53
|
-
box-shadow: 0 0 0 2px rgba(13,110,253
|
|
53
|
+
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/* Buttons */
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
background-color: #0d6efd;
|
|
127
127
|
color: white;
|
|
128
128
|
border: none;
|
|
129
|
-
border-radius: 6px;
|
|
129
|
+
border-radius: 6px;
|
|
130
130
|
font-weight: 500;
|
|
131
131
|
cursor: pointer;
|
|
132
132
|
transition: all 0.2s;
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
padding: 0.5rem;
|
|
175
175
|
border-radius: 4px;
|
|
176
176
|
color: #198754;
|
|
177
|
-
background-color: rgba(25,135,84,0.1);
|
|
177
|
+
background-color: rgba(25, 135, 84, 0.1);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/* Files Section */
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
margin-top: 2rem;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
.filesSection h4 {
|
|
185
|
+
.filesSection h4 {
|
|
186
186
|
margin-bottom: 1rem;
|
|
187
187
|
font-size: 1.3rem;
|
|
188
188
|
font-weight: 900;
|
|
@@ -274,8 +274,6 @@
|
|
|
274
274
|
background-color: #dee2e6;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
|
|
279
277
|
/* Files and Case Management */
|
|
280
278
|
|
|
281
279
|
.fileName {
|
|
@@ -285,7 +283,6 @@
|
|
|
285
283
|
white-space: nowrap;
|
|
286
284
|
}
|
|
287
285
|
|
|
288
|
-
|
|
289
286
|
/* Rename and Delete Cases */
|
|
290
287
|
|
|
291
288
|
.caseRename {
|
|
@@ -307,12 +304,12 @@
|
|
|
307
304
|
.caseRename input:focus {
|
|
308
305
|
outline: none;
|
|
309
306
|
border-color: #0d6efd;
|
|
310
|
-
box-shadow: 0 0 0 2px rgba(13,110,253
|
|
307
|
+
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
|
|
311
308
|
}
|
|
312
309
|
|
|
313
310
|
/* Buttons */
|
|
314
311
|
.caseRename button {
|
|
315
|
-
|
|
312
|
+
width: 100%;
|
|
316
313
|
padding: 0.75rem;
|
|
317
314
|
background-color: #ffc107;
|
|
318
315
|
color: #000;
|
|
@@ -681,33 +678,119 @@
|
|
|
681
678
|
}
|
|
682
679
|
/* Confirmation Status Indicators */
|
|
683
680
|
.fileItemNotConfirmed {
|
|
684
|
-
background-color: color-mix(
|
|
681
|
+
background-color: color-mix(
|
|
682
|
+
in lab,
|
|
683
|
+
var(--warning) 15%,
|
|
684
|
+
var(--backgroundLight)
|
|
685
|
+
);
|
|
685
686
|
}
|
|
686
687
|
|
|
687
688
|
.fileItemNotConfirmed:hover {
|
|
688
|
-
background-color: color-mix(
|
|
689
|
+
background-color: color-mix(
|
|
690
|
+
in lab,
|
|
691
|
+
var(--warning) 20%,
|
|
692
|
+
var(--backgroundLight)
|
|
693
|
+
);
|
|
689
694
|
}
|
|
690
695
|
|
|
691
696
|
.fileItem.active.fileItemNotConfirmed {
|
|
692
|
-
background-color: color-mix(
|
|
697
|
+
background-color: color-mix(
|
|
698
|
+
in lab,
|
|
699
|
+
var(--warning) 15%,
|
|
700
|
+
var(--backgroundLight)
|
|
701
|
+
);
|
|
693
702
|
}
|
|
694
703
|
|
|
695
704
|
.fileItem.active.fileItemNotConfirmed:hover {
|
|
696
|
-
background-color: color-mix(
|
|
705
|
+
background-color: color-mix(
|
|
706
|
+
in lab,
|
|
707
|
+
var(--warning) 20%,
|
|
708
|
+
var(--backgroundLight)
|
|
709
|
+
);
|
|
697
710
|
}
|
|
698
711
|
|
|
699
712
|
.fileItemConfirmed {
|
|
700
|
-
background-color: color-mix(
|
|
713
|
+
background-color: color-mix(
|
|
714
|
+
in lab,
|
|
715
|
+
var(--success) 20%,
|
|
716
|
+
var(--backgroundLight)
|
|
717
|
+
);
|
|
701
718
|
}
|
|
702
719
|
|
|
703
720
|
.fileItemConfirmed:hover {
|
|
704
|
-
background-color: color-mix(
|
|
721
|
+
background-color: color-mix(
|
|
722
|
+
in lab,
|
|
723
|
+
var(--success) 28%,
|
|
724
|
+
var(--backgroundLight)
|
|
725
|
+
);
|
|
705
726
|
}
|
|
706
727
|
|
|
707
728
|
.fileItem.active.fileItemConfirmed {
|
|
708
|
-
background-color: color-mix(
|
|
729
|
+
background-color: color-mix(
|
|
730
|
+
in lab,
|
|
731
|
+
var(--success) 20%,
|
|
732
|
+
var(--backgroundLight)
|
|
733
|
+
);
|
|
709
734
|
}
|
|
710
735
|
|
|
711
736
|
.fileItem.active.fileItemConfirmed:hover {
|
|
712
|
-
background-color: color-mix(
|
|
713
|
-
|
|
737
|
+
background-color: color-mix(
|
|
738
|
+
in lab,
|
|
739
|
+
var(--success) 28%,
|
|
740
|
+
var(--backgroundLight)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/* Switch/Cancel Case buttons */
|
|
745
|
+
.switchCaseButton {
|
|
746
|
+
width: 100%;
|
|
747
|
+
padding: 0.75rem;
|
|
748
|
+
background-color: #198754;
|
|
749
|
+
color: white;
|
|
750
|
+
border: none;
|
|
751
|
+
border-radius: 6px;
|
|
752
|
+
font-weight: 500;
|
|
753
|
+
cursor: pointer;
|
|
754
|
+
transition: all 0.2s;
|
|
755
|
+
margin-top: 0.5rem;
|
|
756
|
+
box-sizing: border-box;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.switchCaseButton:hover:not(:disabled) {
|
|
760
|
+
background-color: #105032;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.switchCaseButton:disabled {
|
|
764
|
+
background-color: #e9ecef;
|
|
765
|
+
color: #6c757d;
|
|
766
|
+
cursor: not-allowed;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.cancelSwitchButton {
|
|
770
|
+
width: 100%;
|
|
771
|
+
padding: 0.75rem;
|
|
772
|
+
margin-top: 0.75rem;
|
|
773
|
+
background-color: #dc3545;
|
|
774
|
+
color: white;
|
|
775
|
+
border: none;
|
|
776
|
+
border-radius: 6px;
|
|
777
|
+
font-weight: 500;
|
|
778
|
+
cursor: pointer;
|
|
779
|
+
transition: all 0.2s;
|
|
780
|
+
box-sizing: border-box;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.cancelSwitchButton:hover:not(:disabled) {
|
|
784
|
+
background-color: #bd2130;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.cancelSwitchButton:disabled {
|
|
788
|
+
background-color: #e9ecef;
|
|
789
|
+
color: #6c757d;
|
|
790
|
+
cursor: not-allowed;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.cancelSwitchButton:disabled {
|
|
794
|
+
opacity: 0.6;
|
|
795
|
+
cursor: not-allowed;
|
|
796
|
+
}
|