@striae-org/striae 4.3.2 → 4.3.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/app/components/actions/case-import/orchestrator.ts +1 -1
- package/app/components/actions/case-manage.ts +50 -14
- package/app/components/audit/user-audit.module.css +49 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
- package/app/components/navbar/navbar.tsx +25 -12
- package/app/components/sidebar/case-import/case-import.tsx +56 -14
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +7 -6
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +9 -5
- package/app/components/sidebar/cases/cases-modal.module.css +19 -0
- package/app/components/sidebar/cases/cases-modal.tsx +23 -8
- package/app/routes/striae/hooks/use-striae-reset-helpers.ts +102 -0
- package/app/routes/striae/striae.tsx +72 -74
- package/app/routes/striae/utils/case-export.ts +37 -0
- package/app/routes/striae/utils/open-case-helper.ts +18 -0
- package/app/services/audit/audit-console-logger.ts +1 -1
- package/app/services/audit/audit-export-csv.ts +1 -1
- package/app/services/audit/audit-export-signing.ts +2 -2
- package/app/services/audit/audit-export.service.ts +1 -1
- package/app/services/audit/audit-worker-client.ts +1 -1
- package/app/services/audit/audit.service.ts +5 -75
- package/app/services/audit/builders/audit-event-builders-case-file.ts +3 -0
- package/app/services/audit/index.ts +2 -2
- package/app/types/audit.ts +8 -7
- package/app/utils/data/case-filters.ts +1 -1
- package/app/utils/ui/case-messages.ts +69 -0
- package/app/utils/ui/index.ts +1 -0
- package/package.json +5 -5
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
|
@@ -287,7 +287,7 @@ export async function importCaseForReview(
|
|
|
287
287
|
throw new Error(`Case "${result.caseNumber}" already exists in your case list. You cannot import a case for review if you were the original analyst.`);
|
|
288
288
|
}
|
|
289
289
|
if (existingRegularCase && isArchivedExport) {
|
|
290
|
-
throw new Error(`Cannot import this
|
|
290
|
+
throw new Error(`Cannot import this archived case because "${result.caseNumber}" already exists in your regular case list. Delete the regular case before importing this archive.`);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
// Step 2b: Check if read-only case already exists
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
signForensicManifest,
|
|
15
15
|
removeCaseConfirmationSummary
|
|
16
16
|
} from '~/utils/data';
|
|
17
|
-
import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData } from '~/types';
|
|
17
|
+
import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData, type ValidationAuditEntry } from '~/types';
|
|
18
18
|
import { auditService } from '~/services/audit';
|
|
19
19
|
import { fetchImageApi } from '~/utils/api';
|
|
20
20
|
import { exportCaseData, formatDateForFilename } from '~/components/actions/case-export';
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
getVerificationPublicKey,
|
|
28
28
|
} from '~/utils/forensics';
|
|
29
29
|
import { signAuditExport } from '~/services/audit/audit-export-signing';
|
|
30
|
-
import { generateAuditSummary } from '~/services/audit/audit-query-helpers';
|
|
30
|
+
import { generateAuditSummary, sortAuditEntriesNewestFirst } from '~/services/audit/audit-query-helpers';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Delete a file without individual audit logging (for bulk operations)
|
|
@@ -182,8 +182,9 @@ export const checkExistingCase = async (user: User, caseNumber: string): Promise
|
|
|
182
182
|
return null;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
//
|
|
186
|
-
if
|
|
185
|
+
// Imported review cases are read-only and should not be treated as regular cases.
|
|
186
|
+
// Archived cases remain regular case records even if legacy data includes isReadOnly.
|
|
187
|
+
if ('isReadOnly' in caseData && caseData.isReadOnly && !caseData.archived) {
|
|
187
188
|
return null;
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -212,11 +213,6 @@ export const checkCaseIsReadOnly = async (user: User, caseNumber: string): Promi
|
|
|
212
213
|
return false;
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
// Archived cases are always treated as read-only.
|
|
216
|
-
if (caseData.archived) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
216
|
// Use type guard to check for isReadOnly property safely
|
|
221
217
|
return isReadOnlyCaseData(caseData) ? !!caseData.isReadOnly : false;
|
|
222
218
|
|
|
@@ -397,15 +393,23 @@ export const renameCase = async (
|
|
|
397
393
|
// 5) Delete old case number in user's KV entry
|
|
398
394
|
await removeUserCase(user, oldCaseNumber);
|
|
399
395
|
|
|
400
|
-
// Log successful case rename
|
|
396
|
+
// Log successful case rename under the original case number context
|
|
401
397
|
const endTime = Date.now();
|
|
402
398
|
await auditService.logCaseRename(
|
|
403
399
|
user,
|
|
404
|
-
|
|
400
|
+
oldCaseNumber,
|
|
405
401
|
oldCaseNumber,
|
|
406
402
|
newCaseNumber
|
|
407
403
|
);
|
|
408
404
|
|
|
405
|
+
// Log creation of the new case number as a rename-derived case
|
|
406
|
+
await auditService.logCaseCreation(
|
|
407
|
+
user,
|
|
408
|
+
newCaseNumber,
|
|
409
|
+
newCaseNumber,
|
|
410
|
+
oldCaseNumber
|
|
411
|
+
);
|
|
412
|
+
|
|
409
413
|
console.log(`✅ Case renamed: ${oldCaseNumber} → ${newCaseNumber} (${endTime - startTime}ms)`);
|
|
410
414
|
|
|
411
415
|
} catch (error) {
|
|
@@ -748,7 +752,7 @@ export const archiveCase = async (
|
|
|
748
752
|
archivedBy: user.uid,
|
|
749
753
|
archivedByDisplay,
|
|
750
754
|
archiveReason: archiveReason?.trim() || undefined,
|
|
751
|
-
isReadOnly:
|
|
755
|
+
isReadOnly: false,
|
|
752
756
|
} as CaseData;
|
|
753
757
|
|
|
754
758
|
const exportData = await exportCaseData(user, caseNumber, { includeMetadata: true });
|
|
@@ -812,11 +816,43 @@ export const archiveCase = async (
|
|
|
812
816
|
startDate: caseData.createdAt,
|
|
813
817
|
endDate: archivedAt,
|
|
814
818
|
});
|
|
819
|
+
|
|
820
|
+
// Ensure the bundled archive trail includes the archival event itself.
|
|
821
|
+
const archiveAuditEntry: ValidationAuditEntry = {
|
|
822
|
+
timestamp: archivedAt,
|
|
823
|
+
userId: user.uid,
|
|
824
|
+
userEmail: user.email || '',
|
|
825
|
+
action: 'case-archive',
|
|
826
|
+
result: 'success',
|
|
827
|
+
details: {
|
|
828
|
+
fileName: `${caseNumber}.case`,
|
|
829
|
+
fileType: 'case-package',
|
|
830
|
+
validationErrors: [],
|
|
831
|
+
caseNumber,
|
|
832
|
+
workflowPhase: 'casework',
|
|
833
|
+
caseDetails: {
|
|
834
|
+
newCaseName: caseNumber,
|
|
835
|
+
archiveReason: archiveReason?.trim() || 'No reason provided',
|
|
836
|
+
totalFiles: archiveData.files?.length || 0,
|
|
837
|
+
lastModified: archivedAt,
|
|
838
|
+
},
|
|
839
|
+
performanceMetrics: {
|
|
840
|
+
processingTimeMs: Date.now() - startTime,
|
|
841
|
+
fileSizeBytes: 0,
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const auditEntriesWithArchive = sortAuditEntriesNewestFirst([
|
|
847
|
+
...auditEntries,
|
|
848
|
+
archiveAuditEntry,
|
|
849
|
+
]);
|
|
850
|
+
|
|
815
851
|
const auditTrail: AuditTrail = {
|
|
816
852
|
caseNumber,
|
|
817
853
|
workflowId: `${caseNumber}-archive-${Date.now()}`,
|
|
818
|
-
entries:
|
|
819
|
-
summary: generateAuditSummary(
|
|
854
|
+
entries: auditEntriesWithArchive,
|
|
855
|
+
summary: generateAuditSummary(auditEntriesWithArchive),
|
|
820
856
|
};
|
|
821
857
|
|
|
822
858
|
const auditTrailPayload = {
|
|
@@ -522,11 +522,60 @@
|
|
|
522
522
|
white-space: nowrap;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
.entryHeaderActions {
|
|
526
|
+
display: flex;
|
|
527
|
+
align-items: center;
|
|
528
|
+
gap: 8px;
|
|
529
|
+
margin-left: auto;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.entryDetailsToggle {
|
|
533
|
+
background: color-mix(in lab, var(--primary) 10%, transparent);
|
|
534
|
+
color: color-mix(in lab, var(--primary) 65%, var(--text));
|
|
535
|
+
border: 1px solid color-mix(in lab, var(--primary) 30%, transparent);
|
|
536
|
+
padding: 4px 8px;
|
|
537
|
+
border-radius: 999px;
|
|
538
|
+
font-size: 0.75rem;
|
|
539
|
+
font-weight: var(--fontWeightMedium);
|
|
540
|
+
cursor: pointer;
|
|
541
|
+
transition: background-color var(--durationS) var(--bezierFastoutSlowin);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.entryDetailsToggle:hover {
|
|
545
|
+
background: color-mix(in lab, var(--primary) 16%, transparent);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.entryDetailsToggle:focus-visible {
|
|
549
|
+
outline: 2px solid color-mix(in lab, var(--primary) 45%, transparent);
|
|
550
|
+
outline-offset: 2px;
|
|
551
|
+
}
|
|
552
|
+
|
|
525
553
|
/* Entry Details */
|
|
526
554
|
.entryDetails {
|
|
527
555
|
padding: 12px 14px;
|
|
528
556
|
}
|
|
529
557
|
|
|
558
|
+
.expandedDetails {
|
|
559
|
+
margin-top: 10px;
|
|
560
|
+
padding-top: 10px;
|
|
561
|
+
border-top: 1px dashed color-mix(in lab, var(--textLight) 25%, transparent);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.expandedDetailsCode {
|
|
565
|
+
margin: 4px 0 0;
|
|
566
|
+
padding: 10px;
|
|
567
|
+
border-radius: 6px;
|
|
568
|
+
border: 1px solid color-mix(in lab, var(--textLight) 20%, transparent);
|
|
569
|
+
background: color-mix(in lab, var(--backgroundLight) 75%, transparent);
|
|
570
|
+
color: var(--text);
|
|
571
|
+
font-size: 0.78rem;
|
|
572
|
+
line-height: 1.4;
|
|
573
|
+
white-space: pre-wrap;
|
|
574
|
+
word-break: break-word;
|
|
575
|
+
max-height: 280px;
|
|
576
|
+
overflow: auto;
|
|
577
|
+
}
|
|
578
|
+
|
|
530
579
|
.detailRow {
|
|
531
580
|
display: flex;
|
|
532
581
|
align-items: center;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMemo, useState, type MouseEvent } from 'react';
|
|
1
2
|
import { type ValidationAuditEntry } from '~/types';
|
|
2
3
|
import { formatAuditTimestamp, getAuditActionIcon, getAuditStatusIcon } from './audit-viewer-utils';
|
|
3
4
|
import styles from '../user-audit.module.css';
|
|
@@ -13,7 +14,57 @@ const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
|
|
|
13
14
|
);
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const isConfirmationEvent = (entry: ValidationAuditEntry): boolean => {
|
|
18
|
+
return (
|
|
19
|
+
entry.action === 'confirmation-create' ||
|
|
20
|
+
entry.action === 'confirmation-export' ||
|
|
21
|
+
entry.action === 'confirmation-import' ||
|
|
22
|
+
entry.action === 'confirm' ||
|
|
23
|
+
(entry.action === 'import' && entry.details.workflowPhase === 'confirmation') ||
|
|
24
|
+
(entry.action === 'export' && entry.details.workflowPhase === 'confirmation')
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const supportsFullDetailsToggle = (entry: ValidationAuditEntry): boolean => {
|
|
29
|
+
return (
|
|
30
|
+
entry.action === 'annotation-create' ||
|
|
31
|
+
entry.action === 'annotation-edit' ||
|
|
32
|
+
entry.action === 'annotation-delete' ||
|
|
33
|
+
isConfirmationEvent(entry)
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getEntryKey = (entry: ValidationAuditEntry): string => {
|
|
38
|
+
return `${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}-${entry.details.confirmationId || ''}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
16
41
|
export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
42
|
+
const [expandedEntryKeys, setExpandedEntryKeys] = useState<Set<string>>(new Set());
|
|
43
|
+
|
|
44
|
+
const expandableEntries = useMemo(() => {
|
|
45
|
+
return new Set(entries.filter(supportsFullDetailsToggle).map(getEntryKey));
|
|
46
|
+
}, [entries]);
|
|
47
|
+
|
|
48
|
+
const toggleExpanded = (entryKey: string) => {
|
|
49
|
+
setExpandedEntryKeys((current) => {
|
|
50
|
+
const next = new Set(current);
|
|
51
|
+
|
|
52
|
+
if (next.has(entryKey)) {
|
|
53
|
+
next.delete(entryKey);
|
|
54
|
+
} else {
|
|
55
|
+
next.add(entryKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return next;
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleToggleClick = (event: MouseEvent<HTMLButtonElement>, entryKey: string) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
toggleExpanded(entryKey);
|
|
66
|
+
};
|
|
67
|
+
|
|
17
68
|
return (
|
|
18
69
|
<div className={styles.entriesList}>
|
|
19
70
|
<h3>Activity Log ({entries.length} entries)</h3>
|
|
@@ -22,30 +73,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
|
22
73
|
<p>No activities match the current filters.</p>
|
|
23
74
|
</div>
|
|
24
75
|
) : (
|
|
25
|
-
entries.map((entry) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
>
|
|
30
|
-
<div className={styles.entryHeader}>
|
|
31
|
-
<div className={styles.entryIcons}>
|
|
32
|
-
<span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
|
|
33
|
-
<span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
|
|
34
|
-
</div>
|
|
35
|
-
<div className={styles.entryTitle}>
|
|
36
|
-
<span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
|
|
37
|
-
<span className={styles.fileName}>{entry.details.fileName}</span>
|
|
38
|
-
</div>
|
|
39
|
-
<div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
|
|
40
|
-
</div>
|
|
76
|
+
entries.map((entry) => {
|
|
77
|
+
const entryKey = getEntryKey(entry);
|
|
78
|
+
const isExpandable = expandableEntries.has(entryKey);
|
|
79
|
+
const isExpanded = expandedEntryKeys.has(entryKey);
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
key={entryKey}
|
|
84
|
+
className={`${styles.entry} ${styles[entry.result]}`}
|
|
85
|
+
>
|
|
86
|
+
<div className={styles.entryHeader}>
|
|
87
|
+
<div className={styles.entryIcons}>
|
|
88
|
+
<span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
|
|
89
|
+
<span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
|
|
47
90
|
</div>
|
|
48
|
-
|
|
91
|
+
<div className={styles.entryTitle}>
|
|
92
|
+
<span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
|
|
93
|
+
<span className={styles.fileName}>{entry.details.fileName}</span>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className={styles.entryHeaderActions}>
|
|
97
|
+
<div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
|
|
98
|
+
{isExpandable && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className={styles.entryDetailsToggle}
|
|
102
|
+
aria-expanded={isExpanded}
|
|
103
|
+
aria-label={isExpanded ? 'Hide full entry details' : 'Show full entry details'}
|
|
104
|
+
onClick={(event) => handleToggleClick(event, entryKey)}
|
|
105
|
+
>
|
|
106
|
+
{isExpanded ? 'Hide details' : 'Show details'}
|
|
107
|
+
</button>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className={styles.entryDetails}>
|
|
113
|
+
{entry.details.caseNumber && (
|
|
114
|
+
<div className={styles.detailRow}>
|
|
115
|
+
<span className={styles.detailLabel}>Case:</span>
|
|
116
|
+
<span className={styles.detailValue}>{entry.details.caseNumber}</span>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
49
119
|
|
|
50
120
|
{entry.details.userProfileDetails?.badgeId && (
|
|
51
121
|
<div className={styles.detailRow}>
|
|
@@ -191,37 +261,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
|
191
261
|
</>
|
|
192
262
|
)}
|
|
193
263
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
264
|
+
{(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
|
|
265
|
+
<>
|
|
266
|
+
{entry.details.fileDetails.fileId && (
|
|
267
|
+
<div className={styles.detailRow}>
|
|
268
|
+
<span className={styles.detailLabel}>
|
|
269
|
+
{entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
|
|
270
|
+
</span>
|
|
271
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
204
274
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
275
|
+
{entry.details.fileDetails.originalFileName && (
|
|
276
|
+
<div className={styles.detailRow}>
|
|
277
|
+
<span className={styles.detailLabel}>
|
|
278
|
+
{entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
|
|
279
|
+
</span>
|
|
280
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{entry.action === 'confirm' && entry.details.confirmationId && (
|
|
285
|
+
<div className={styles.detailRow}>
|
|
286
|
+
<span className={styles.detailLabel}>Confirmation ID:</span>
|
|
287
|
+
<span className={styles.detailValue}>{entry.details.confirmationId}</span>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</>
|
|
291
|
+
)}
|
|
213
292
|
|
|
214
|
-
|
|
293
|
+
{isExpandable && isExpanded && (
|
|
294
|
+
<div className={styles.expandedDetails}>
|
|
215
295
|
<div className={styles.detailRow}>
|
|
216
|
-
<span className={styles.detailLabel}>
|
|
217
|
-
<span className={styles.detailValue}>{entry.details.confirmationId}</span>
|
|
296
|
+
<span className={styles.detailLabel}>Full Entry Details:</span>
|
|
218
297
|
</div>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
298
|
+
<pre className={styles.expandedDetailsCode}>
|
|
299
|
+
{JSON.stringify(entry, null, 2)}
|
|
300
|
+
</pre>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
222
304
|
</div>
|
|
223
|
-
|
|
224
|
-
)
|
|
305
|
+
);
|
|
306
|
+
})
|
|
225
307
|
)}
|
|
226
308
|
</div>
|
|
227
309
|
);
|
|
@@ -13,6 +13,7 @@ interface NavbarProps {
|
|
|
13
13
|
isUploading?: boolean;
|
|
14
14
|
company?: string;
|
|
15
15
|
isReadOnly?: boolean;
|
|
16
|
+
isReviewOnlyCase?: boolean;
|
|
16
17
|
currentCase?: string;
|
|
17
18
|
currentFileName?: string;
|
|
18
19
|
isCurrentImageConfirmed?: boolean;
|
|
@@ -43,6 +44,7 @@ export const Navbar = ({
|
|
|
43
44
|
isUploading = false,
|
|
44
45
|
company,
|
|
45
46
|
isReadOnly = false,
|
|
47
|
+
isReviewOnlyCase = false,
|
|
46
48
|
currentCase,
|
|
47
49
|
currentFileName,
|
|
48
50
|
isCurrentImageConfirmed = false,
|
|
@@ -119,16 +121,17 @@ export const Navbar = ({
|
|
|
119
121
|
const disableLongRunningCaseActions = isUploading;
|
|
120
122
|
const isCaseManagementActive = true;
|
|
121
123
|
const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
|
|
122
|
-
const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed;
|
|
124
|
+
const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed && !isReadOnly;
|
|
123
125
|
const isImageNotesActive = canOpenImageNotes;
|
|
124
126
|
const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
|
|
127
|
+
const isArchivedRegularReadOnly = Boolean(isReadOnly && archiveDetails?.archived && !isReviewOnlyCase);
|
|
125
128
|
|
|
126
129
|
return (
|
|
127
130
|
<>
|
|
128
131
|
<header className={styles.navbar} aria-label="Canvas top navigation">
|
|
129
132
|
<div className={styles.companyLabelContainer}>
|
|
130
133
|
<div className={styles.companyLabel}>
|
|
131
|
-
{
|
|
134
|
+
{isReviewOnlyCase ? 'CASE REVIEW ONLY' : `${company}${user?.displayName ? ` | ${user.displayName}` : ''}${userBadgeId ? `, ${userBadgeId}` : ''}`}
|
|
132
135
|
</div>
|
|
133
136
|
</div>
|
|
134
137
|
<div className={styles.navCenterTrack}>
|
|
@@ -153,8 +156,8 @@ export const Navbar = ({
|
|
|
153
156
|
type="button"
|
|
154
157
|
role="menuitem"
|
|
155
158
|
className={`${styles.caseMenuItem} ${styles.caseMenuItemOpen}`}
|
|
156
|
-
disabled={
|
|
157
|
-
title={
|
|
159
|
+
disabled={isReviewOnlyCase}
|
|
160
|
+
title={isReviewOnlyCase ? 'Clear the read-only case first to open or switch cases' : undefined}
|
|
158
161
|
onClick={() => {
|
|
159
162
|
onOpenCase?.();
|
|
160
163
|
setIsCaseMenuOpen(false);
|
|
@@ -166,8 +169,8 @@ export const Navbar = ({
|
|
|
166
169
|
type="button"
|
|
167
170
|
role="menuitem"
|
|
168
171
|
className={`${styles.caseMenuItem} ${styles.caseMenuItemList}`}
|
|
169
|
-
disabled={
|
|
170
|
-
title={
|
|
172
|
+
disabled={isReviewOnlyCase}
|
|
173
|
+
title={isReviewOnlyCase ? 'Clear the read-only case first to list all cases' : undefined}
|
|
171
174
|
onClick={() => {
|
|
172
175
|
onOpenListAllCases?.();
|
|
173
176
|
setIsCaseMenuOpen(false);
|
|
@@ -180,9 +183,11 @@ export const Navbar = ({
|
|
|
180
183
|
type="button"
|
|
181
184
|
role="menuitem"
|
|
182
185
|
className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
|
|
183
|
-
disabled={!hasLoadedCase || disableLongRunningCaseActions}
|
|
186
|
+
disabled={!hasLoadedCase || disableLongRunningCaseActions || isArchivedRegularReadOnly}
|
|
184
187
|
title={
|
|
185
|
-
|
|
188
|
+
isArchivedRegularReadOnly
|
|
189
|
+
? 'Export is unavailable for archived cases loaded from your regular case list'
|
|
190
|
+
: !hasLoadedCase
|
|
186
191
|
? 'Load a case to export case data'
|
|
187
192
|
: disableLongRunningCaseActions
|
|
188
193
|
? 'Export is unavailable while files are uploading'
|
|
@@ -209,7 +214,7 @@ export const Navbar = ({
|
|
|
209
214
|
Case Audit Trail
|
|
210
215
|
</button>
|
|
211
216
|
<div className={styles.caseMenuSectionLabel}>Maintenance</div>
|
|
212
|
-
{
|
|
217
|
+
{isReviewOnlyCase && (
|
|
213
218
|
<button
|
|
214
219
|
type="button"
|
|
215
220
|
role="menuitem"
|
|
@@ -249,9 +254,9 @@ export const Navbar = ({
|
|
|
249
254
|
type="button"
|
|
250
255
|
role="menuitem"
|
|
251
256
|
className={`${styles.caseMenuItem} ${styles.caseMenuItemDelete}`}
|
|
252
|
-
disabled={!hasLoadedCase || disableLongRunningCaseActions ||
|
|
257
|
+
disabled={!hasLoadedCase || disableLongRunningCaseActions || isReviewOnlyCase}
|
|
253
258
|
title={
|
|
254
|
-
|
|
259
|
+
isReviewOnlyCase
|
|
255
260
|
? 'Clear the read-only case first before deleting'
|
|
256
261
|
: !hasLoadedCase
|
|
257
262
|
? 'Load a case to delete it'
|
|
@@ -370,7 +375,15 @@ export const Navbar = ({
|
|
|
370
375
|
className={`${styles.navSectionButton} ${isImageNotesActive ? styles.navSectionButtonActive : ''}`}
|
|
371
376
|
disabled={!canOpenImageNotes}
|
|
372
377
|
aria-pressed={isImageNotesActive}
|
|
373
|
-
title={
|
|
378
|
+
title={
|
|
379
|
+
!hasLoadedImage
|
|
380
|
+
? 'Load an image to enable image notes'
|
|
381
|
+
: isCurrentImageConfirmed
|
|
382
|
+
? 'Confirmed images are read-only and viewable via toolbar only'
|
|
383
|
+
: isReadOnly
|
|
384
|
+
? 'Image notes are disabled for read-only cases'
|
|
385
|
+
: undefined
|
|
386
|
+
}
|
|
374
387
|
onClick={() => {
|
|
375
388
|
onOpenImageNotes?.();
|
|
376
389
|
}}
|