@striae-org/striae 3.0.4

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