@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,17 +1,17 @@
1
- import { useState, useEffect, useContext, useCallback } from 'react';
1
+ import { useContext, useMemo } from 'react';
2
2
  import { AuthContext } from '~/contexts/auth.context';
3
- import { auditService, auditExportService } from '~/services/audit';
4
- import { type ValidationAuditEntry, type AuditAction, type AuditResult, type AuditTrail, type UserData, type WorkflowPhase } from '~/types';
5
- import { getUserData } from '~/utils/data';
3
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
4
+ import { AuditViewerHeader } from './viewer/audit-viewer-header';
5
+ import { AuditUserInfoCard } from './viewer/audit-user-info-card';
6
+ import { AuditActivitySummary } from './viewer/audit-activity-summary';
7
+ import { AuditFiltersPanel } from './viewer/audit-filters-panel';
8
+ import { AuditEntriesList } from './viewer/audit-entries-list';
9
+ import { summarizeAuditEntries } from './viewer/audit-viewer-utils';
10
+ import { useAuditViewerData } from './viewer/use-audit-viewer-data';
11
+ import { useAuditViewerFilters } from './viewer/use-audit-viewer-filters';
12
+ import { useAuditViewerExport } from './viewer/use-audit-viewer-export';
6
13
  import styles from './user-audit.module.css';
7
14
 
8
- const isWorkflowPhase = (phase: unknown): phase is WorkflowPhase =>
9
- phase === 'casework' ||
10
- phase === 'case-export' ||
11
- phase === 'case-import' ||
12
- phase === 'confirmation' ||
13
- phase === 'user-management';
14
-
15
15
  interface UserAuditViewerProps {
16
16
  isOpen: boolean;
17
17
  onClose: () => void;
@@ -21,509 +21,97 @@ interface UserAuditViewerProps {
21
21
 
22
22
  export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAuditViewerProps) => {
23
23
  const { user } = useContext(AuthContext);
24
- const [auditEntries, setAuditEntries] = useState<ValidationAuditEntry[]>([]);
25
- const [userData, setUserData] = useState<UserData | null>(null);
26
- const [loading, setLoading] = useState(false);
27
- const [error, setError] = useState<string>('');
28
- const [filterAction, setFilterAction] = useState<AuditAction | 'all'>('all');
29
- const [filterResult, setFilterResult] = useState<AuditResult | 'all'>('all');
30
- const [filterCaseNumber, setFilterCaseNumber] = useState<string>('');
31
- const [caseNumberInput, setCaseNumberInput] = useState<string>('');
32
- const [dateRange, setDateRange] = useState<'1d' | '7d' | '30d' | '90d' | 'custom'>('1d');
33
- const [customStartDate, setCustomStartDate] = useState<string>('');
34
- const [customEndDate, setCustomEndDate] = useState<string>('');
35
- const [customStartDateInput, setCustomStartDateInput] = useState<string>('');
36
- const [customEndDateInput, setCustomEndDateInput] = useState<string>('');
37
- const [auditTrail, setAuditTrail] = useState<AuditTrail | null>(null);
38
-
39
- useEffect(() => {
40
- const handleEscape = (event: KeyboardEvent) => {
41
- if (event.key === 'Escape' && isOpen) {
42
- onClose();
43
- }
44
- };
45
-
46
- if (isOpen) {
47
- document.addEventListener('keydown', handleEscape);
48
- return () => {
49
- document.removeEventListener('keydown', handleEscape);
50
- };
51
- }
52
- }, [isOpen, onClose]);
53
-
54
- const loadUserData = useCallback(async () => {
55
- if (!user) return;
56
-
57
- try {
58
- const data = await getUserData(user);
59
- setUserData(data);
60
- } catch (error) {
61
- console.error('Failed to load user data:', error);
62
- // Don't set error state for user data failure, just log it
63
- }
64
- }, [user]);
65
-
66
- const loadAuditData = useCallback(async () => {
67
- if (!user?.uid) return;
68
-
69
- setLoading(true);
70
- setError('');
71
-
72
- try {
73
- // Calculate date range
74
- let startDate: string | undefined;
75
- let endDate: string | undefined;
76
-
77
- if (dateRange === 'custom') {
78
- if (customStartDate) {
79
- startDate = new Date(customStartDate + 'T00:00:00').toISOString();
80
- }
81
- if (customEndDate) {
82
- endDate = new Date(customEndDate + 'T23:59:59').toISOString();
83
- }
84
- // If only one custom date is provided, handle it appropriately
85
- if (customStartDate && !customEndDate) {
86
- // If only start date, set end date to now
87
- const endDateObj = new Date();
88
- endDate = endDateObj.toISOString();
89
- } else if (!customStartDate && customEndDate) {
90
- // If only end date, set start date to 30 days before end date
91
- const startDateObj = new Date(customEndDate + 'T23:59:59');
92
- startDateObj.setDate(startDateObj.getDate() - 30);
93
- startDate = startDateObj.toISOString();
94
- }
95
- } else if (dateRange === '90d') {
96
- // For '90d' entries, get last 90 days to avoid loading too much data
97
- const startDateObj = new Date();
98
- startDateObj.setDate(startDateObj.getDate() - 90);
99
- startDate = startDateObj.toISOString();
100
-
101
- const endDateObj = new Date();
102
- endDate = endDateObj.toISOString();
103
- } else {
104
- // Handle predefined ranges like '1d', '7d', '30d'
105
- const days = parseInt(dateRange.replace('d', ''));
106
- const startDateObj = new Date();
107
- startDateObj.setDate(startDateObj.getDate() - days);
108
- startDate = startDateObj.toISOString();
109
-
110
- // Always set end date to now for proper range querying
111
- const endDateObj = new Date();
112
- endDate = endDateObj.toISOString();
113
- }
114
-
115
- // Get audit entries (filtered by case if specified)
116
- const effectiveCaseNumber = caseNumber || (filterCaseNumber.trim() || undefined);
117
- const entries = await auditService.getAuditEntriesForUser(user.uid, {
118
- caseNumber: effectiveCaseNumber,
119
- startDate,
120
- endDate,
121
- limit: effectiveCaseNumber ? 1000 : 500 // More entries for case-specific view
122
- });
123
-
124
- setAuditEntries(entries);
125
-
126
- // If case-specific, create audit trail for enhanced export functionality
127
- if (effectiveCaseNumber && entries.length > 0) {
128
- const trail: AuditTrail = {
129
- caseNumber: effectiveCaseNumber,
130
- workflowId: `workflow-${effectiveCaseNumber}-${user.uid}`,
131
- entries,
132
- summary: {
133
- totalEvents: entries.length,
134
- successfulEvents: entries.filter(e => e.result === 'success').length,
135
- failedEvents: entries.filter(e => e.result === 'failure').length,
136
- warningEvents: entries.filter(e => e.result === 'warning').length,
137
- workflowPhases: [...new Set(entries
138
- .map(e => e.details.workflowPhase)
139
- .filter(isWorkflowPhase))],
140
- participatingUsers: [...new Set(entries.map(e => e.userId))],
141
- startTimestamp: entries[entries.length - 1]?.timestamp || new Date().toISOString(),
142
- endTimestamp: entries[0]?.timestamp || new Date().toISOString(),
143
- complianceStatus: entries.some(e => e.result === 'failure') ? 'non-compliant' : 'compliant',
144
- securityIncidents: entries.filter(e => e.action === 'security-violation').length
145
- }
146
- };
147
- setAuditTrail(trail);
148
- } else {
149
- setAuditTrail(null);
150
- }
151
- } catch (err) {
152
- setError(err instanceof Error ? err.message : 'Failed to load audit data');
153
- } finally {
154
- setLoading(false);
155
- }
156
- }, [
157
- user,
24
+ const {
25
+ filterAction,
26
+ setFilterAction,
27
+ filterResult,
28
+ setFilterResult,
29
+ filterCaseNumber,
30
+ caseNumberInput,
31
+ setCaseNumberInput,
32
+ filterBadgeId,
33
+ badgeIdInput,
34
+ setBadgeIdInput,
158
35
  dateRange,
159
36
  customStartDate,
160
37
  customEndDate,
161
- caseNumber,
162
- filterCaseNumber
163
- ]);
164
-
165
- useEffect(() => {
166
- if (isOpen && user) {
167
- loadAuditData();
168
- loadUserData();
169
- }
170
- }, [isOpen, user, loadAuditData, loadUserData]);
171
-
172
- const handleApplyCaseFilter = () => {
173
- setFilterCaseNumber(caseNumberInput.trim());
174
- };
175
-
176
- const handleClearCaseFilter = () => {
177
- setCaseNumberInput('');
178
- setFilterCaseNumber('');
179
- };
180
-
181
- const handleApplyCustomDateRange = () => {
182
- setCustomStartDate(customStartDateInput);
183
- setCustomEndDate(customEndDateInput);
184
- };
185
-
186
- const handleClearCustomDateRange = () => {
187
- setCustomStartDateInput('');
188
- setCustomEndDateInput('');
189
- setCustomStartDate('');
190
- setCustomEndDate('');
191
- };
192
-
193
- const getFilteredEntries = (): ValidationAuditEntry[] => {
194
- return auditEntries.filter(entry => {
195
- // Handle consolidation and mapping of actions
196
- let actionMatch: boolean;
197
- if (filterAction === 'all') {
198
- actionMatch = true;
199
- } else if (filterAction === 'confirmation-create') {
200
- // Accept both 'confirm' and 'confirmation-create' for this filter
201
- actionMatch = entry.action === 'confirm' || entry.action === 'confirmation-create';
202
- } else if (filterAction === 'case-export') {
203
- // Case exports use legacy 'export' action with 'case-export' workflowPhase
204
- actionMatch = entry.action === 'export' && entry.details.workflowPhase === 'case-export';
205
- } else if (filterAction === 'case-import') {
206
- // Case imports use legacy 'import' action with 'case-import' workflowPhase
207
- actionMatch = entry.action === 'import' && entry.details.workflowPhase === 'case-import';
208
- } else if (filterAction === 'confirmation-export') {
209
- // Confirmation exports use legacy 'export' action with 'confirmation' workflowPhase
210
- actionMatch = entry.action === 'export' && entry.details.workflowPhase === 'confirmation';
211
- } else if (filterAction === 'confirmation-import') {
212
- // Confirmation imports use legacy 'import' action with 'confirmation' workflowPhase
213
- actionMatch = entry.action === 'import' && entry.details.workflowPhase === 'confirmation';
214
- } else {
215
- // Direct action match for all other cases
216
- actionMatch = entry.action === filterAction;
217
- }
218
-
219
- const resultMatch = filterResult === 'all' || entry.result === filterResult;
220
- return actionMatch && resultMatch;
221
- });
222
- };
223
-
224
- // Export functions
225
- const handleExportCSV = async () => {
226
- if (!user) return;
227
-
228
- const filteredEntries = getFilteredEntries();
229
- const effectiveCaseNumber = caseNumber || filterCaseNumber.trim();
230
- const identifier = effectiveCaseNumber || user.uid;
231
- const type = effectiveCaseNumber ? 'case' : 'user';
232
- const filename = auditExportService.generateFilename(type, identifier, 'csv');
233
- const exportContext = {
234
- user,
235
- scopeType: type,
236
- scopeIdentifier: identifier,
237
- caseNumber: effectiveCaseNumber || undefined
238
- } as const;
239
-
240
- try {
241
- if (auditTrail && effectiveCaseNumber) {
242
- // Use full audit trail export for case-specific data
243
- await auditExportService.exportAuditTrailToCSV(auditTrail, filename, exportContext);
244
- } else {
245
- // Use regular entry export for user data
246
- await auditExportService.exportToCSV(filteredEntries, filename, exportContext);
247
- }
248
- } catch (error) {
249
- console.error('Export failed:', error);
250
- setError('Failed to export audit trail to CSV');
251
- }
252
- };
253
-
254
- const handleExportJSON = async () => {
255
- if (!user) return;
256
-
257
- const filteredEntries = getFilteredEntries();
258
- const effectiveCaseNumber = caseNumber || filterCaseNumber.trim();
259
- const identifier = effectiveCaseNumber || user.uid;
260
- const type = effectiveCaseNumber ? 'case' : 'user';
261
- const filename = auditExportService.generateFilename(type, identifier, 'csv'); // Will be converted to .json
262
- const exportContext = {
263
- user,
264
- scopeType: type,
265
- scopeIdentifier: identifier,
266
- caseNumber: effectiveCaseNumber || undefined
267
- } as const;
268
-
269
- try {
270
- if (auditTrail && effectiveCaseNumber) {
271
- // Use full audit trail export for case-specific data
272
- await auditExportService.exportAuditTrailToJSON(auditTrail, filename, exportContext);
273
- } else {
274
- // Use regular entry export for user data
275
- await auditExportService.exportToJSON(filteredEntries, filename, exportContext);
276
- }
277
- } catch (error) {
278
- console.error('Export failed:', error);
279
- setError('Failed to export audit trail to JSON');
280
- }
281
- };
282
-
283
- const handleGenerateReport = async () => {
284
- if (!user) return;
285
-
286
- const filteredEntries = getFilteredEntries();
287
- const effectiveCaseNumber = caseNumber || filterCaseNumber.trim();
288
- const identifier = effectiveCaseNumber || user.uid;
289
- const type = effectiveCaseNumber ? 'case' : 'user';
290
- const filename = `${type}-audit-report-${identifier}-${new Date().toISOString().split('T')[0]}.txt`;
291
- const exportContext = {
292
- user,
293
- scopeType: type,
294
- scopeIdentifier: identifier,
295
- caseNumber: effectiveCaseNumber || undefined
296
- } as const;
297
-
298
- try {
299
- let reportContent: string;
300
-
301
- if (auditTrail && effectiveCaseNumber) {
302
- // Use audit trail report for case-specific data
303
- reportContent = await auditExportService.generateReportSummary(auditTrail, exportContext);
304
- } else {
305
- // Generate user-specific report
306
- const totalEntries = filteredEntries.length;
307
- const successfulActions = filteredEntries.filter(e => e.result === 'success').length;
308
- const failedActions = filteredEntries.filter(e => e.result === 'failure').length;
309
-
310
- const actionCounts = filteredEntries.reduce((acc, entry) => {
311
- acc[entry.action] = (acc[entry.action] || 0) + 1;
312
- return acc;
313
- }, {} as Record<string, number>);
314
-
315
- const dateRange = filteredEntries.length > 0 ? {
316
- earliest: new Date(Math.min(...filteredEntries.map(e => new Date(e.timestamp).getTime()))),
317
- latest: new Date(Math.max(...filteredEntries.map(e => new Date(e.timestamp).getTime())))
318
- } : null;
319
-
320
- reportContent = `${caseNumber ? 'CASE' : 'USER'} AUDIT REPORT
321
- Generated: ${new Date().toISOString()}
322
- ${caseNumber ? `Case: ${caseNumber}` : `User: ${user.email}`}
323
- ${caseNumber ? '' : `User ID: ${user.uid}`}
324
-
325
- === SUMMARY ===
326
- Total Actions: ${totalEntries}
327
- Successful: ${successfulActions}
328
- Failed: ${failedActions}
329
- Success Rate: ${totalEntries > 0 ? ((successfulActions / totalEntries) * 100).toFixed(1) : 0}%
330
-
331
- ${dateRange ? `Date Range: ${dateRange.earliest.toLocaleDateString()} - ${dateRange.latest.toLocaleDateString()}` : 'No entries found'}
332
-
333
- === ACTION BREAKDOWN ===
334
- ${Object.entries(actionCounts)
335
- .sort(([,a], [,b]) => b - a)
336
- .map(([action, count]) => `${action}: ${count}`)
337
- .join('\n')}
338
-
339
- === RECENT ACTIVITIES ===
340
- ${filteredEntries.slice(0, 10).map(entry =>
341
- `${new Date(entry.timestamp).toLocaleString()} | ${entry.action} | ${entry.result}${entry.details.caseNumber ? ` | Case: ${entry.details.caseNumber}` : ''}`
342
- ).join('\n')}
343
-
344
- Generated by Striae
345
- `;
346
-
347
- reportContent = await auditExportService.appendSignedReportIntegrity(
348
- reportContent,
349
- exportContext,
350
- totalEntries
351
- );
352
- }
353
-
354
- // Create and download the report file
355
- const blob = new Blob([reportContent], { type: 'text/plain' });
356
- const url = URL.createObjectURL(blob);
357
- const a = document.createElement('a');
358
- a.href = url;
359
- a.download = filename;
360
- document.body.appendChild(a);
361
- a.click();
362
- document.body.removeChild(a);
363
- URL.revokeObjectURL(url);
364
- } catch (error) {
365
- console.error('Report generation failed:', error);
366
- setError('Failed to generate audit report');
367
- }
368
- };
369
-
370
- const getActionIcon = (action: AuditAction): string => {
371
- switch (action) {
372
- // User & Session Management
373
- case 'user-login': return '🔑';
374
- case 'user-logout': return '🚪';
375
- case 'user-profile-update': return '👤';
376
- case 'user-password-reset': return '🔒';
377
- // NEW: User Registration & Authentication
378
- case 'user-registration': return '📝';
379
- case 'email-verification': return '📧';
380
- case 'mfa-enrollment': return '🔐';
381
- case 'mfa-authentication': return '📱';
382
-
383
- // Case Management
384
- case 'case-create': return '📂';
385
- case 'case-rename': return '✏️';
386
- case 'case-delete': return '🗑️';
387
-
388
- // Confirmation Workflow
389
- case 'case-export': return '📤';
390
- case 'case-import': return '📥';
391
- case 'confirmation-create': return '✅';
392
- case 'confirmation-export': return '📤';
393
- case 'confirmation-import': return '📥';
394
-
395
- // File Operations
396
- case 'file-upload': return '⬆️';
397
- case 'file-delete': return '🗑️';
398
- case 'file-access': return '👁️';
399
-
400
- // Annotation Operations
401
- case 'annotation-create': return '✨';
402
- case 'annotation-edit': return '✏️';
403
- case 'annotation-delete': return '❌';
404
-
405
- // Document Generation
406
- case 'pdf-generate': return '📄';
407
-
408
- // Security & Monitoring
409
- case 'security-violation': return '🚨';
410
-
411
- // Legacy Actions
412
- case 'export': return '📤';
413
- case 'import': return '📥';
414
- case 'confirm': return '✓';
415
-
416
- default: return '📄';
417
- }
418
- };
419
-
420
- const getStatusIcon = (result: AuditResult): string => {
421
- switch (result) {
422
- case 'success': return '✅';
423
- case 'failure': return '❌';
424
- case 'warning': return '⚠️';
425
- case 'blocked': return '🛑';
426
- case 'pending': return '⏳';
427
- default: return '❓';
428
- }
429
- };
430
-
431
- const formatTimestamp = (timestamp: string): string => {
432
- return new Date(timestamp).toLocaleString();
433
- };
434
-
435
- const getDateRangeDisplay = (): string => {
436
- switch (dateRange) {
437
- case '90d':
438
- return 'Last 90 Days';
439
- case 'custom':
440
- if (customStartDate && customEndDate) {
441
- const startFormatted = new Date(customStartDate).toLocaleDateString();
442
- const endFormatted = new Date(customEndDate).toLocaleDateString();
443
- return `${startFormatted} - ${endFormatted}`;
444
- } else if (customStartDate) {
445
- return `From ${new Date(customStartDate).toLocaleDateString()}`;
446
- } else if (customEndDate) {
447
- return `Until ${new Date(customEndDate).toLocaleDateString()}`;
448
- } else {
449
- return 'Custom Range';
450
- }
451
- default:
452
- return `Last ${dateRange}`;
453
- }
454
- };
455
-
456
- // Get summary statistics
457
- const totalEntries = auditEntries.length;
458
- const successfulEntries = auditEntries.filter(e => e.result === 'success').length;
459
- const failedEntries = auditEntries.filter(e => e.result === 'failure').length;
460
- const securityIncidents = auditEntries.filter(e =>
461
- e.action === 'security-violation'
462
- ).length;
463
- const loginSessions = auditEntries.filter(e => e.action === 'user-login').length;
464
-
465
- const handleOverlayMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
466
- if (event.target === event.currentTarget) {
467
- onClose();
468
- }
469
- };
38
+ customStartDateInput,
39
+ customEndDateInput,
40
+ setCustomStartDateInput,
41
+ setCustomEndDateInput,
42
+ handleApplyCaseFilter,
43
+ handleClearCaseFilter,
44
+ handleApplyBadgeFilter,
45
+ handleClearBadgeFilter,
46
+ handleApplyCustomDateRange,
47
+ handleClearCustomDateRange,
48
+ handleDateRangeChange,
49
+ getFilteredEntries,
50
+ dateRangeDisplay,
51
+ effectiveCaseNumber
52
+ } = useAuditViewerFilters(caseNumber);
53
+
54
+ const {
55
+ auditEntries,
56
+ userData,
57
+ loading,
58
+ error,
59
+ setError,
60
+ auditTrail,
61
+ isArchivedReadOnlyCase,
62
+ bundledAuditWarning,
63
+ loadAuditData
64
+ } = useAuditViewerData({
65
+ isOpen,
66
+ user,
67
+ effectiveCaseNumber,
68
+ dateRange,
69
+ customStartDate,
70
+ customEndDate
71
+ });
470
72
 
471
- const handleOverlayKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
472
- if (event.target !== event.currentTarget) {
473
- return;
474
- }
73
+ const filteredEntries = useMemo(() => getFilteredEntries(auditEntries), [auditEntries, getFilteredEntries]);
74
+ const auditSummary = useMemo(() => summarizeAuditEntries(auditEntries), [auditEntries]);
475
75
 
476
- if (event.key === 'Enter' || event.key === ' ') {
477
- event.preventDefault();
478
- onClose();
479
- }
480
- };
76
+ const {
77
+ handleExportCSV,
78
+ handleExportJSON,
79
+ handleGenerateReport
80
+ } = useAuditViewerExport({
81
+ user,
82
+ effectiveCaseNumber,
83
+ filteredEntries,
84
+ auditTrail,
85
+ setError
86
+ });
87
+
88
+ const {
89
+ requestClose,
90
+ overlayProps
91
+ } = useOverlayDismiss({
92
+ isOpen,
93
+ onClose
94
+ });
95
+
96
+ const userBadgeId = userData?.badgeId?.trim() || '';
481
97
 
482
98
  if (!isOpen) return null;
483
99
 
484
100
  return (
485
101
  <div
486
102
  className={styles.overlay}
487
- onMouseDown={handleOverlayMouseDown}
488
- onKeyDown={handleOverlayKeyDown}
489
- role="button"
490
- tabIndex={0}
491
103
  aria-label="Close audit trail dialog"
104
+ {...overlayProps}
492
105
  >
493
106
  <div className={styles.modal}>
494
- <div className={styles.header}>
495
- <h2 className={styles.title}>
496
- {title || (caseNumber ? `Audit Trail - Case ${caseNumber}` : 'My Audit Trail')}
497
- </h2>
498
- <div className={styles.headerActions}>
499
- {auditEntries.length > 0 && (
500
- <div className={styles.exportButtons}>
501
- <button
502
- onClick={handleExportCSV}
503
- className={styles.exportButton}
504
- title="CSV - Individual entry log with summary data"
505
- >
506
- 📊 CSV
507
- </button>
508
- <button
509
- onClick={handleExportJSON}
510
- className={styles.exportButton}
511
- title="JSON - Complete log data for version capture and auditing"
512
- >
513
- 📄 JSON
514
- </button>
515
- <button
516
- onClick={handleGenerateReport}
517
- className={styles.exportButton}
518
- title="Summary report only"
519
- >
520
- 📋 Report
521
- </button>
522
- </div>
523
- )}
524
- <button className={styles.closeButton} onClick={onClose}>×</button>
525
- </div>
526
- </div>
107
+ <AuditViewerHeader
108
+ title={title || (effectiveCaseNumber ? `Audit Trail - Case ${effectiveCaseNumber}` : 'My Audit Trail')}
109
+ hasEntries={auditEntries.length > 0}
110
+ onExportCSV={handleExportCSV}
111
+ onExportJSON={handleExportJSON}
112
+ onGenerateReport={handleGenerateReport}
113
+ onClose={requestClose}
114
+ />
527
115
 
528
116
  <div className={styles.content}>
529
117
  {loading && (
@@ -544,469 +132,73 @@ Generated by Striae
544
132
 
545
133
  {!loading && !error && (
546
134
  <>
135
+ {isArchivedReadOnlyCase && (
136
+ <div className={bundledAuditWarning ? styles.archivedWarning : styles.archivedNotice}>
137
+ <p>
138
+ {bundledAuditWarning || 'Viewing bundled audit trail data from this imported archived case package.'}
139
+ </p>
140
+ </div>
141
+ )}
142
+
547
143
  {/* User Information Section */}
548
144
  {user && (
549
- <div className={styles.summary}>
550
- <h3>User Information</h3>
551
- <div className={styles.userInfoContent}>
552
- <div className={styles.userInfoItem}>
553
- Name: <strong>
554
- {userData ? `${userData.firstName} ${userData.lastName}` : user.displayName || 'Not provided'}
555
- </strong>
556
- </div>
557
- <div className={styles.userInfoItem}>
558
- Email: <strong>{user.email || 'Not provided'}</strong>
559
- </div>
560
- <div className={styles.userInfoItem}>
561
- Lab/Company: <strong>{userData?.company || 'Not provided'}</strong>
562
- </div>
563
- <div className={styles.userInfoItem}>
564
- User ID: <strong>{user.uid}</strong>
565
- </div>
566
- </div>
567
- </div>
145
+ <AuditUserInfoCard user={user} userData={userData} userBadgeId={userBadgeId} />
568
146
  )}
569
147
 
570
148
  {/* Summary Section */}
571
- <div className={styles.summary}>
572
- <h3>
573
- {(caseNumber || filterCaseNumber.trim())
574
- ? `Case Activity Summary - ${caseNumber || filterCaseNumber.trim()} (${getDateRangeDisplay()})`
575
- : `Activity Summary (${getDateRangeDisplay()})`
576
- }
577
- </h3>
578
- <div className={styles.summaryGrid}>
579
- <div className={styles.summaryItem}>
580
- <span className={styles.label}>Total Activities:</span>
581
- <span className={styles.value}>{totalEntries}</span>
582
- </div>
583
- <div className={styles.summaryItem}>
584
- <span className={styles.label}>Successful:</span>
585
- <span className={styles.value}>{successfulEntries}</span>
586
- </div>
587
- <div className={styles.summaryItem}>
588
- <span className={styles.label}>Failed:</span>
589
- <span className={styles.value}>{failedEntries}</span>
590
- </div>
591
- <div className={styles.summaryItem}>
592
- <span className={styles.label}>Login Sessions:</span>
593
- <span className={styles.value}>{loginSessions}</span>
594
- </div>
595
- <div className={styles.summaryItem}>
596
- <span className={styles.label}>Security Incidents:</span>
597
- <span className={`${styles.value} ${securityIncidents > 0 ? styles.warning : ''}`}>
598
- {securityIncidents}
599
- </span>
600
- </div>
601
- </div>
602
- </div>
149
+ <AuditActivitySummary
150
+ caseNumber={caseNumber}
151
+ filterCaseNumber={filterCaseNumber}
152
+ dateRangeDisplay={dateRangeDisplay}
153
+ summary={auditSummary}
154
+ />
603
155
 
604
156
  {/* Filters */}
605
- <div className={styles.filters}>
606
- <div className={styles.filterGroup}>
607
- <label htmlFor="dateRange">Time Period:</label>
608
- <select
609
- id="dateRange"
610
- value={dateRange}
611
- onChange={(e) => {
612
- const newRange = e.target.value as '1d' | '7d' | '30d' | '90d' | 'custom';
613
- setDateRange(newRange);
614
- // When switching to custom, populate inputs with current applied values
615
- if (newRange === 'custom') {
616
- setCustomStartDateInput(customStartDate);
617
- setCustomEndDateInput(customEndDate);
618
- }
619
- }}
620
- className={styles.filterSelect}
621
- >
622
- <option value="1d">Last 24 Hours</option>
623
- <option value="7d">Last 7 Days</option>
624
- <option value="30d">Last 30 Days</option>
625
- <option value="90d">Last 90 Days</option>
626
- <option value="custom">Custom Range</option>
627
- </select>
628
- </div>
629
-
630
- {/* Custom Date Range Inputs */}
631
- {dateRange === 'custom' && (
632
- <div className={styles.customDateRange}>
633
- <div className={styles.customDateInputs}>
634
- <div className={styles.filterGroup}>
635
- <label htmlFor="startDate">Start Date:</label>
636
- <input
637
- type="date"
638
- id="startDate"
639
- value={customStartDateInput}
640
- onChange={(e) => setCustomStartDateInput(e.target.value)}
641
- className={styles.filterInput}
642
- max={customEndDateInput || new Date().toISOString().split('T')[0]}
643
- />
644
- </div>
645
- <div className={styles.filterGroup}>
646
- <label htmlFor="endDate">End Date:</label>
647
- <input
648
- type="date"
649
- id="endDate"
650
- value={customEndDateInput}
651
- onChange={(e) => setCustomEndDateInput(e.target.value)}
652
- className={styles.filterInput}
653
- min={customStartDateInput}
654
- max={new Date().toISOString().split('T')[0]}
655
- />
656
- </div>
657
- <div className={styles.dateRangeButtons}>
658
- {(customStartDateInput || customEndDateInput) && (
659
- <button
660
- type="button"
661
- onClick={handleApplyCustomDateRange}
662
- className={styles.filterButton}
663
- title="Apply custom date range"
664
- >
665
- Apply Dates
666
- </button>
667
- )}
668
- {(customStartDate || customEndDate) && (
669
- <button
670
- type="button"
671
- onClick={handleClearCustomDateRange}
672
- className={styles.clearButton}
673
- title="Clear custom date range"
674
- >
675
- Clear Dates
676
- </button>
677
- )}
678
- </div>
679
- </div>
680
- {(customStartDate || customEndDate) && (
681
- <div className={styles.activeFilter}>
682
- <small>
683
- Custom range:
684
- {customStartDate && <strong> from {new Date(customStartDate).toLocaleDateString()}</strong>}
685
- {customEndDate && <strong> to {new Date(customEndDate).toLocaleDateString()}</strong>}
686
- </small>
687
- </div>
688
- )}
689
- </div>
690
- )}
691
-
692
- <div className={styles.filterGroup}>
693
- <label htmlFor="caseFilter">Case Number:</label>
694
- <div className={styles.inputWithButton}>
695
- <input
696
- type="text"
697
- id="caseFilter"
698
- value={caseNumberInput}
699
- onChange={(e) => setCaseNumberInput(e.target.value)}
700
- className={styles.filterInput}
701
- placeholder="Enter case number..."
702
- disabled={!!caseNumber} // Disable if already viewing a specific case
703
- title={caseNumber ? "Case filter disabled - viewing specific case" : "Enter complete case number and click Filter"}
704
- onKeyDown={(e) => {
705
- if (e.key === 'Enter' && caseNumberInput.trim() && !caseNumber) {
706
- handleApplyCaseFilter();
707
- }
708
- }}
709
- />
710
- {!caseNumber && (
711
- <div className={styles.caseFilterButtons}>
712
- {caseNumberInput.trim() && (
713
- <button
714
- type="button"
715
- onClick={handleApplyCaseFilter}
716
- className={styles.filterButton}
717
- title="Apply case filter"
718
- >
719
- Filter
720
- </button>
721
- )}
722
- {filterCaseNumber && (
723
- <button
724
- type="button"
725
- onClick={handleClearCaseFilter}
726
- className={styles.clearButton}
727
- title="Clear case filter"
728
- >
729
- Clear
730
- </button>
731
- )}
732
- </div>
733
- )}
734
- </div>
735
- {filterCaseNumber && !caseNumber && (
736
- <div className={styles.activeFilter}>
737
- <small>Filtering by case: <strong>{filterCaseNumber}</strong></small>
738
- </div>
739
- )}
740
- </div>
741
-
742
- <div className={styles.filterGroup}>
743
- <label htmlFor="actionFilter">Activity Type:</label>
744
- <select
745
- id="actionFilter"
746
- value={filterAction}
747
- onChange={(e) => setFilterAction(e.target.value as AuditAction | 'all')}
748
- className={styles.filterSelect}
749
- >
750
- <option value="all">All Activities</option>
751
- <optgroup label="User Sessions">
752
- <option value="user-login">Login</option>
753
- <option value="user-logout">Logout</option>
754
- </optgroup>
755
- <optgroup label="Case Management">
756
- <option value="case-create">Case Create</option>
757
- <option value="case-rename">Case Rename</option>
758
- <option value="case-delete">Case Delete</option>
759
- <option value="case-export">Case Export</option>
760
- <option value="case-import">Case Import</option>
761
- </optgroup>
762
- <optgroup label="File Operations">
763
- <option value="file-upload">File Upload</option>
764
- <option value="file-access">File Access</option>
765
- <option value="file-delete">File Delete</option>
766
- </optgroup>
767
- <optgroup label="Annotations">
768
- <option value="annotation-create">Annotation Create</option>
769
- <option value="annotation-edit">Annotation Edit</option>
770
- <option value="annotation-delete">Annotation Delete</option>
771
- </optgroup>
772
- <optgroup label="Confirmation Activity">
773
- <option value="confirmation-create">Confirmation Create</option>
774
- <option value="confirmation-export">Confirmation Export</option>
775
- <option value="confirmation-import">Confirmation Import</option>
776
- </optgroup>
777
- <optgroup label="Documents">
778
- <option value="pdf-generate">PDF Generate</option>
779
- </optgroup>
780
- <optgroup label="Security">
781
- <option value="security-violation">Security Violation</option>
782
- </optgroup>
783
- </select>
784
- </div>
785
-
786
- <div className={styles.filterGroup}>
787
- <label htmlFor="resultFilter">Result:</label>
788
- <select
789
- id="resultFilter"
790
- value={filterResult}
791
- onChange={(e) => setFilterResult(e.target.value as AuditResult | 'all')}
792
- className={styles.filterSelect}
793
- >
794
- <option value="all">All Results</option>
795
- <option value="success">Success</option>
796
- <option value="failure">Failure</option>
797
- <option value="warning">Warning</option>
798
- <option value="blocked">Blocked</option>
799
- </select>
800
- </div>
801
- </div>
157
+ <AuditFiltersPanel
158
+ dateRange={dateRange}
159
+ customStartDate={customStartDate}
160
+ customEndDate={customEndDate}
161
+ customStartDateInput={customStartDateInput}
162
+ customEndDateInput={customEndDateInput}
163
+ caseNumber={caseNumber}
164
+ filterCaseNumber={filterCaseNumber}
165
+ caseNumberInput={caseNumberInput}
166
+ filterBadgeId={filterBadgeId}
167
+ badgeIdInput={badgeIdInput}
168
+ filterAction={filterAction}
169
+ filterResult={filterResult}
170
+ onDateRangeChange={handleDateRangeChange}
171
+ onCustomStartDateInputChange={setCustomStartDateInput}
172
+ onCustomEndDateInputChange={setCustomEndDateInput}
173
+ onApplyCustomDateRange={handleApplyCustomDateRange}
174
+ onClearCustomDateRange={handleClearCustomDateRange}
175
+ onCaseNumberInputChange={setCaseNumberInput}
176
+ onApplyCaseFilter={handleApplyCaseFilter}
177
+ onClearCaseFilter={handleClearCaseFilter}
178
+ onBadgeIdInputChange={setBadgeIdInput}
179
+ onApplyBadgeFilter={handleApplyBadgeFilter}
180
+ onClearBadgeFilter={handleClearBadgeFilter}
181
+ onFilterActionChange={setFilterAction}
182
+ onFilterResultChange={setFilterResult}
183
+ />
802
184
 
803
185
  {/* Entries List */}
804
- <div className={styles.entriesList}>
805
- <h3>Activity Log ({getFilteredEntries().length} entries)</h3>
806
- {getFilteredEntries().length === 0 ? (
807
- <div className={styles.noEntries}>
808
- <p>No activities match the current filters.</p>
809
- </div>
810
- ) : (
811
- getFilteredEntries().map((entry, index) => (
812
- <div key={index} className={`${styles.entry} ${styles[entry.result]}`}>
813
- <div className={styles.entryHeader}>
814
- <div className={styles.entryIcons}>
815
- <span className={styles.actionIcon}>{getActionIcon(entry.action)}</span>
816
- <span className={styles.statusIcon}>{getStatusIcon(entry.result)}</span>
817
- </div>
818
- <div className={styles.entryTitle}>
819
- <span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
820
- <span className={styles.fileName}>{entry.details.fileName}</span>
821
- </div>
822
- <div className={styles.entryTimestamp}>
823
- {formatTimestamp(entry.timestamp)}
824
- </div>
825
- </div>
826
-
827
- {/* Basic Details */}
828
- <div className={styles.entryDetails}>
829
- {entry.details.caseNumber && (
830
- <div className={styles.detailRow}>
831
- <span className={styles.detailLabel}>Case:</span>
832
- <span className={styles.detailValue}>{entry.details.caseNumber}</span>
833
- </div>
834
- )}
835
-
836
- {entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
837
- <div className={styles.detailRow}>
838
- <span className={styles.detailLabel}>Error:</span>
839
- <span className={styles.detailValue}>{entry.details.validationErrors[0]}</span>
840
- </div>
841
- )}
842
-
843
- {/* Session Details for Login/Logout */}
844
- {(entry.action === 'user-login' || entry.action === 'user-logout') && entry.details.sessionDetails && (
845
- <>
846
- {entry.details.sessionDetails.userAgent && (
847
- <div className={styles.detailRow}>
848
- <span className={styles.detailLabel}>User Agent:</span>
849
- <span className={styles.detailValue}>{entry.details.sessionDetails.userAgent}</span>
850
- </div>
851
- )}
852
- </>
853
- )}
854
-
855
- {/* Security Details */}
856
- {entry.action === 'security-violation' && entry.details.securityDetails && (
857
- <>
858
- <div className={styles.detailRow}>
859
- <span className={styles.detailLabel}>Severity:</span>
860
- <span className={`${styles.detailValue} ${styles.severity} ${styles[entry.details.securityDetails.severity || 'low']}`}>
861
- {(entry.details.securityDetails.severity || 'low').toUpperCase()}
862
- </span>
863
- </div>
864
- {entry.details.securityDetails.incidentType && (
865
- <div className={styles.detailRow}>
866
- <span className={styles.detailLabel}>Type:</span>
867
- <span className={styles.detailValue}>{entry.details.securityDetails.incidentType}</span>
868
- </div>
869
- )}
870
- </>
871
- )}
872
-
873
- {/* File Operation Details */}
874
- {(entry.action === 'file-upload' || entry.action === 'file-delete' || entry.action === 'file-access') && entry.details.fileDetails && (
875
- <>
876
- {/* File ID */}
877
- {entry.details.fileDetails.fileId && (
878
- <div className={styles.detailRow}>
879
- <span className={styles.detailLabel}>File ID:</span>
880
- <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
881
- </div>
882
- )}
883
-
884
- {/* Original Filename */}
885
- {entry.details.fileDetails.originalFileName && (
886
- <div className={styles.detailRow}>
887
- <span className={styles.detailLabel}>Original Filename:</span>
888
- <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
889
- </div>
890
- )}
891
-
892
- {/* File Size */}
893
- {entry.details.fileDetails.fileSize > 0 && (
894
- <div className={styles.detailRow}>
895
- <span className={styles.detailLabel}>File Size:</span>
896
- <span className={styles.detailValue}>
897
- {(entry.details.fileDetails.fileSize / 1024 / 1024).toFixed(2)} MB
898
- </span>
899
- </div>
900
- )}
901
-
902
- {/* Access Method/Upload Method */}
903
- {entry.details.fileDetails.uploadMethod && (
904
- <div className={styles.detailRow}>
905
- <span className={styles.detailLabel}>
906
- {entry.action === 'file-access' ? 'Access Method' : 'Upload Method'}:
907
- </span>
908
- <span className={styles.detailValue}>{entry.details.fileDetails.uploadMethod}</span>
909
- </div>
910
- )}
911
-
912
- {/* Delete Reason */}
913
- {entry.details.fileDetails.deleteReason && (
914
- <div className={styles.detailRow}>
915
- <span className={styles.detailLabel}>Reason:</span>
916
- <span className={styles.detailValue}>{entry.details.fileDetails.deleteReason}</span>
917
- </div>
918
- )}
919
-
920
- {/* Access Source */}
921
- {entry.details.fileDetails.sourceLocation && entry.action === 'file-access' && (
922
- <div className={styles.detailRow}>
923
- <span className={styles.detailLabel}>Access Source:</span>
924
- <span className={styles.detailValue}>{entry.details.fileDetails.sourceLocation}</span>
925
- </div>
926
- )}
927
- </>
928
- )}
929
-
930
- {/* Annotation Details */}
931
- {(entry.action === 'annotation-create' || entry.action === 'annotation-edit' || entry.action === 'annotation-delete') && entry.details.fileDetails && (
932
- <>
933
- {/* File ID */}
934
- {entry.details.fileDetails.fileId && (
935
- <div className={styles.detailRow}>
936
- <span className={styles.detailLabel}>File ID:</span>
937
- <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
938
- </div>
939
- )}
940
-
941
- {/* Original Filename */}
942
- {entry.details.fileDetails.originalFileName && (
943
- <div className={styles.detailRow}>
944
- <span className={styles.detailLabel}>Original Filename:</span>
945
- <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
946
- </div>
947
- )}
948
-
949
- {/* Annotation Type */}
950
- {entry.details.annotationDetails?.annotationType && (
951
- <div className={styles.detailRow}>
952
- <span className={styles.detailLabel}>Annotation Type:</span>
953
- <span className={styles.detailValue}>{entry.details.annotationDetails.annotationType}</span>
954
- </div>
955
- )}
956
-
957
- {/* Tool Used */}
958
- {entry.details.annotationDetails?.tool && (
959
- <div className={styles.detailRow}>
960
- <span className={styles.detailLabel}>Tool:</span>
961
- <span className={styles.detailValue}>{entry.details.annotationDetails.tool}</span>
962
- </div>
963
- )}
964
- </>
965
- )}
966
-
967
- {/* PDF Generation and Confirmation Details */}
968
- {(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
969
- <>
970
- {/* Source File ID */}
971
- {entry.details.fileDetails.fileId && (
972
- <div className={styles.detailRow}>
973
- <span className={styles.detailLabel}>{entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}</span>
974
- <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
975
- </div>
976
- )}
977
-
978
- {/* Source Original Filename */}
979
- {entry.details.fileDetails.originalFileName && (
980
- <div className={styles.detailRow}>
981
- <span className={styles.detailLabel}>{entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}</span>
982
- <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
983
- </div>
984
- )}
186
+ <AuditEntriesList entries={filteredEntries} />
985
187
 
986
- {/* Confirmation ID (for confirm actions) */}
987
- {entry.action === 'confirm' && entry.details.confirmationId && (
988
- <div className={styles.detailRow}>
989
- <span className={styles.detailLabel}>Confirmation ID:</span>
990
- <span className={styles.detailValue}>{entry.details.confirmationId}</span>
991
- </div>
992
- )}
993
- </>
994
- )}
995
- </div>
996
- </div>
997
- ))
998
- )}
999
- </div>
1000
188
  </>
1001
189
  )}
1002
190
 
1003
191
  {auditEntries.length === 0 && !loading && !error && (
1004
192
  <div className={styles.noData}>
1005
- <p>No audit trail available. Your activities will appear here as you use Striae.</p>
193
+ <p>
194
+ {isArchivedReadOnlyCase
195
+ ? 'No bundled audit trail entries are available for this imported archived case.'
196
+ : 'No audit trail available. Your activities will appear here as you use Striae.'}
197
+ </p>
1006
198
  </div>
1007
199
  )}
1008
200
  </div>
1009
201
  </div>
1010
202
  </div>
1011
203
  );
1012
- };
204
+ };