@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
@@ -101,6 +101,27 @@
101
101
  padding: 20px;
102
102
  }
103
103
 
104
+ .archivedNotice,
105
+ .archivedWarning {
106
+ margin-bottom: 16px;
107
+ padding: 12px 14px;
108
+ border-radius: 6px;
109
+ border: 1px solid;
110
+ font-size: 0.9rem;
111
+ }
112
+
113
+ .archivedNotice {
114
+ background: color-mix(in lab, var(--primary) 8%, transparent);
115
+ border-color: color-mix(in lab, var(--primary) 35%, transparent);
116
+ color: var(--textBody);
117
+ }
118
+
119
+ .archivedWarning {
120
+ background: color-mix(in lab, var(--warning) 12%, transparent);
121
+ border-color: color-mix(in lab, var(--warning) 40%, transparent);
122
+ color: var(--text);
123
+ }
124
+
104
125
  /* Loading & Error States */
105
126
  .loading,
106
127
  .error {
@@ -531,6 +552,26 @@
531
552
  word-break: break-word;
532
553
  }
533
554
 
555
+ .badgeTag {
556
+ display: inline-flex;
557
+ align-items: center;
558
+ padding: 2px 10px;
559
+ border-radius: 999px;
560
+ border: 1px solid color-mix(in lab, var(--primary) 35%, transparent);
561
+ background: color-mix(in lab, var(--primary) 12%, transparent);
562
+ color: color-mix(in lab, var(--primary) 70%, var(--text));
563
+ font-size: 0.78rem;
564
+ font-weight: var(--fontWeightMedium);
565
+ letter-spacing: 0.2px;
566
+ line-height: 1.4;
567
+ }
568
+
569
+ .badgeTagMuted {
570
+ border-color: color-mix(in lab, var(--textLight) 25%, transparent);
571
+ background: color-mix(in lab, var(--backgroundLight) 70%, transparent);
572
+ color: var(--textLight);
573
+ }
574
+
534
575
  .severity {
535
576
  font-weight: var(--fontWeightMedium);
536
577
  padding: 2px 6px;
@@ -0,0 +1,52 @@
1
+ import type { AuditViewerSummaryStats } from './audit-viewer-utils';
2
+ import styles from '../user-audit.module.css';
3
+
4
+ interface AuditActivitySummaryProps {
5
+ caseNumber?: string;
6
+ filterCaseNumber: string;
7
+ dateRangeDisplay: string;
8
+ summary: AuditViewerSummaryStats;
9
+ }
10
+
11
+ export const AuditActivitySummary = ({
12
+ caseNumber,
13
+ filterCaseNumber,
14
+ dateRangeDisplay,
15
+ summary,
16
+ }: AuditActivitySummaryProps) => {
17
+ const activeCaseNumber = caseNumber || filterCaseNumber.trim();
18
+
19
+ return (
20
+ <div className={styles.summary}>
21
+ <h3>
22
+ {activeCaseNumber
23
+ ? `Case Activity Summary - ${activeCaseNumber} (${dateRangeDisplay})`
24
+ : `Activity Summary (${dateRangeDisplay})`}
25
+ </h3>
26
+ <div className={styles.summaryGrid}>
27
+ <div className={styles.summaryItem}>
28
+ <span className={styles.label}>Total Activities:</span>
29
+ <span className={styles.value}>{summary.totalEntries}</span>
30
+ </div>
31
+ <div className={styles.summaryItem}>
32
+ <span className={styles.label}>Successful:</span>
33
+ <span className={styles.value}>{summary.successfulEntries}</span>
34
+ </div>
35
+ <div className={styles.summaryItem}>
36
+ <span className={styles.label}>Failed:</span>
37
+ <span className={styles.value}>{summary.failedEntries}</span>
38
+ </div>
39
+ <div className={styles.summaryItem}>
40
+ <span className={styles.label}>Login Sessions:</span>
41
+ <span className={styles.value}>{summary.loginSessions}</span>
42
+ </div>
43
+ <div className={styles.summaryItem}>
44
+ <span className={styles.label}>Security Incidents:</span>
45
+ <span className={`${styles.value} ${summary.securityIncidents > 0 ? styles.warning : ''}`}>
46
+ {summary.securityIncidents}
47
+ </span>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ );
52
+ };
@@ -0,0 +1,207 @@
1
+ import { type ValidationAuditEntry } from '~/types';
2
+ import { formatAuditTimestamp, getAuditActionIcon, getAuditStatusIcon } from './audit-viewer-utils';
3
+ import styles from '../user-audit.module.css';
4
+
5
+ interface AuditEntriesListProps {
6
+ entries: ValidationAuditEntry[];
7
+ }
8
+
9
+ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
10
+ return (
11
+ <div className={styles.entriesList}>
12
+ <h3>Activity Log ({entries.length} entries)</h3>
13
+ {entries.length === 0 ? (
14
+ <div className={styles.noEntries}>
15
+ <p>No activities match the current filters.</p>
16
+ </div>
17
+ ) : (
18
+ entries.map((entry, index) => (
19
+ <div key={index} className={`${styles.entry} ${styles[entry.result]}`}>
20
+ <div className={styles.entryHeader}>
21
+ <div className={styles.entryIcons}>
22
+ <span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
23
+ <span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
24
+ </div>
25
+ <div className={styles.entryTitle}>
26
+ <span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
27
+ <span className={styles.fileName}>{entry.details.fileName}</span>
28
+ </div>
29
+ <div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
30
+ </div>
31
+
32
+ <div className={styles.entryDetails}>
33
+ {entry.details.caseNumber && (
34
+ <div className={styles.detailRow}>
35
+ <span className={styles.detailLabel}>Case:</span>
36
+ <span className={styles.detailValue}>{entry.details.caseNumber}</span>
37
+ </div>
38
+ )}
39
+
40
+ {entry.details.userProfileDetails?.badgeId && (
41
+ <div className={styles.detailRow}>
42
+ <span className={styles.detailLabel}>Badge/ID:</span>
43
+ <span className={styles.badgeTag}>{entry.details.userProfileDetails.badgeId}</span>
44
+ </div>
45
+ )}
46
+
47
+ {entry.action === 'confirmation-import' && entry.details.reviewerBadgeId && (
48
+ <div className={styles.detailRow}>
49
+ <span className={styles.detailLabel}>Reviewer Badge/ID:</span>
50
+ <span className={styles.badgeTag}>{entry.details.reviewerBadgeId}</span>
51
+ </div>
52
+ )}
53
+
54
+ {entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
55
+ <div className={styles.detailRow}>
56
+ <span className={styles.detailLabel}>Error:</span>
57
+ <span className={styles.detailValue}>{entry.details.validationErrors[0]}</span>
58
+ </div>
59
+ )}
60
+
61
+ {(entry.action === 'user-login' || entry.action === 'user-logout') && entry.details.sessionDetails && (
62
+ <>
63
+ {entry.details.sessionDetails.userAgent && (
64
+ <div className={styles.detailRow}>
65
+ <span className={styles.detailLabel}>User Agent:</span>
66
+ <span className={styles.detailValue}>{entry.details.sessionDetails.userAgent}</span>
67
+ </div>
68
+ )}
69
+ </>
70
+ )}
71
+
72
+ {entry.action === 'security-violation' && entry.details.securityDetails && (
73
+ <>
74
+ <div className={styles.detailRow}>
75
+ <span className={styles.detailLabel}>Severity:</span>
76
+ <span
77
+ className={`${styles.detailValue} ${styles.severity} ${styles[entry.details.securityDetails.severity || 'low']}`}
78
+ >
79
+ {(entry.details.securityDetails.severity || 'low').toUpperCase()}
80
+ </span>
81
+ </div>
82
+ {entry.details.securityDetails.incidentType && (
83
+ <div className={styles.detailRow}>
84
+ <span className={styles.detailLabel}>Type:</span>
85
+ <span className={styles.detailValue}>{entry.details.securityDetails.incidentType}</span>
86
+ </div>
87
+ )}
88
+ </>
89
+ )}
90
+
91
+ {(entry.action === 'file-upload' || entry.action === 'file-delete' || entry.action === 'file-access') && entry.details.fileDetails && (
92
+ <>
93
+ {entry.details.fileDetails.fileId && (
94
+ <div className={styles.detailRow}>
95
+ <span className={styles.detailLabel}>File ID:</span>
96
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
97
+ </div>
98
+ )}
99
+
100
+ {entry.details.fileDetails.originalFileName && (
101
+ <div className={styles.detailRow}>
102
+ <span className={styles.detailLabel}>Original Filename:</span>
103
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
104
+ </div>
105
+ )}
106
+
107
+ {entry.details.fileDetails.fileSize > 0 && (
108
+ <div className={styles.detailRow}>
109
+ <span className={styles.detailLabel}>File Size:</span>
110
+ <span className={styles.detailValue}>
111
+ {(entry.details.fileDetails.fileSize / 1024 / 1024).toFixed(2)} MB
112
+ </span>
113
+ </div>
114
+ )}
115
+
116
+ {entry.details.fileDetails.uploadMethod && (
117
+ <div className={styles.detailRow}>
118
+ <span className={styles.detailLabel}>
119
+ {entry.action === 'file-access' ? 'Access Method' : 'Upload Method'}:
120
+ </span>
121
+ <span className={styles.detailValue}>{entry.details.fileDetails.uploadMethod}</span>
122
+ </div>
123
+ )}
124
+
125
+ {entry.details.fileDetails.deleteReason && (
126
+ <div className={styles.detailRow}>
127
+ <span className={styles.detailLabel}>Reason:</span>
128
+ <span className={styles.detailValue}>{entry.details.fileDetails.deleteReason}</span>
129
+ </div>
130
+ )}
131
+
132
+ {entry.details.fileDetails.sourceLocation && entry.action === 'file-access' && (
133
+ <div className={styles.detailRow}>
134
+ <span className={styles.detailLabel}>Access Source:</span>
135
+ <span className={styles.detailValue}>{entry.details.fileDetails.sourceLocation}</span>
136
+ </div>
137
+ )}
138
+ </>
139
+ )}
140
+
141
+ {(entry.action === 'annotation-create' || entry.action === 'annotation-edit' || entry.action === 'annotation-delete') && entry.details.fileDetails && (
142
+ <>
143
+ {entry.details.fileDetails.fileId && (
144
+ <div className={styles.detailRow}>
145
+ <span className={styles.detailLabel}>File ID:</span>
146
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
147
+ </div>
148
+ )}
149
+
150
+ {entry.details.fileDetails.originalFileName && (
151
+ <div className={styles.detailRow}>
152
+ <span className={styles.detailLabel}>Original Filename:</span>
153
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
154
+ </div>
155
+ )}
156
+
157
+ {entry.details.annotationDetails?.annotationType && (
158
+ <div className={styles.detailRow}>
159
+ <span className={styles.detailLabel}>Annotation Type:</span>
160
+ <span className={styles.detailValue}>{entry.details.annotationDetails.annotationType}</span>
161
+ </div>
162
+ )}
163
+
164
+ {entry.details.annotationDetails?.tool && (
165
+ <div className={styles.detailRow}>
166
+ <span className={styles.detailLabel}>Tool:</span>
167
+ <span className={styles.detailValue}>{entry.details.annotationDetails.tool}</span>
168
+ </div>
169
+ )}
170
+ </>
171
+ )}
172
+
173
+ {(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
174
+ <>
175
+ {entry.details.fileDetails.fileId && (
176
+ <div className={styles.detailRow}>
177
+ <span className={styles.detailLabel}>
178
+ {entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
179
+ </span>
180
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
181
+ </div>
182
+ )}
183
+
184
+ {entry.details.fileDetails.originalFileName && (
185
+ <div className={styles.detailRow}>
186
+ <span className={styles.detailLabel}>
187
+ {entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
188
+ </span>
189
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
190
+ </div>
191
+ )}
192
+
193
+ {entry.action === 'confirm' && entry.details.confirmationId && (
194
+ <div className={styles.detailRow}>
195
+ <span className={styles.detailLabel}>Confirmation ID:</span>
196
+ <span className={styles.detailValue}>{entry.details.confirmationId}</span>
197
+ </div>
198
+ )}
199
+ </>
200
+ )}
201
+ </div>
202
+ </div>
203
+ ))
204
+ )}
205
+ </div>
206
+ );
207
+ };
@@ -0,0 +1,307 @@
1
+ import type { AuditAction, AuditResult } from '~/types';
2
+ import type { DateRangeFilter } from './types';
3
+ import styles from '../user-audit.module.css';
4
+
5
+ interface AuditFiltersPanelProps {
6
+ dateRange: DateRangeFilter;
7
+ customStartDate: string;
8
+ customEndDate: string;
9
+ customStartDateInput: string;
10
+ customEndDateInput: string;
11
+ caseNumber?: string;
12
+ filterCaseNumber: string;
13
+ caseNumberInput: string;
14
+ filterBadgeId: string;
15
+ badgeIdInput: string;
16
+ filterAction: AuditAction | 'all';
17
+ filterResult: AuditResult | 'all';
18
+ onDateRangeChange: (value: DateRangeFilter) => void;
19
+ onCustomStartDateInputChange: (value: string) => void;
20
+ onCustomEndDateInputChange: (value: string) => void;
21
+ onApplyCustomDateRange: () => void;
22
+ onClearCustomDateRange: () => void;
23
+ onCaseNumberInputChange: (value: string) => void;
24
+ onApplyCaseFilter: () => void;
25
+ onClearCaseFilter: () => void;
26
+ onBadgeIdInputChange: (value: string) => void;
27
+ onApplyBadgeFilter: () => void;
28
+ onClearBadgeFilter: () => void;
29
+ onFilterActionChange: (value: AuditAction | 'all') => void;
30
+ onFilterResultChange: (value: AuditResult | 'all') => void;
31
+ }
32
+
33
+ export const AuditFiltersPanel = ({
34
+ dateRange,
35
+ customStartDate,
36
+ customEndDate,
37
+ customStartDateInput,
38
+ customEndDateInput,
39
+ caseNumber,
40
+ filterCaseNumber,
41
+ caseNumberInput,
42
+ filterBadgeId,
43
+ badgeIdInput,
44
+ filterAction,
45
+ filterResult,
46
+ onDateRangeChange,
47
+ onCustomStartDateInputChange,
48
+ onCustomEndDateInputChange,
49
+ onApplyCustomDateRange,
50
+ onClearCustomDateRange,
51
+ onCaseNumberInputChange,
52
+ onApplyCaseFilter,
53
+ onClearCaseFilter,
54
+ onBadgeIdInputChange,
55
+ onApplyBadgeFilter,
56
+ onClearBadgeFilter,
57
+ onFilterActionChange,
58
+ onFilterResultChange,
59
+ }: AuditFiltersPanelProps) => {
60
+ return (
61
+ <div className={styles.filters}>
62
+ <div className={styles.filterGroup}>
63
+ <label htmlFor="dateRange">Time Period:</label>
64
+ <select
65
+ id="dateRange"
66
+ value={dateRange}
67
+ onChange={(e) => {
68
+ onDateRangeChange(e.target.value as DateRangeFilter);
69
+ }}
70
+ className={styles.filterSelect}
71
+ >
72
+ <option value="1d">Last 24 Hours</option>
73
+ <option value="7d">Last 7 Days</option>
74
+ <option value="30d">Last 30 Days</option>
75
+ <option value="90d">Last 90 Days</option>
76
+ <option value="custom">Custom Range</option>
77
+ </select>
78
+ </div>
79
+
80
+ {dateRange === 'custom' && (
81
+ <div className={styles.customDateRange}>
82
+ <div className={styles.customDateInputs}>
83
+ <div className={styles.filterGroup}>
84
+ <label htmlFor="startDate">Start Date:</label>
85
+ <input
86
+ type="date"
87
+ id="startDate"
88
+ value={customStartDateInput}
89
+ onChange={(e) => onCustomStartDateInputChange(e.target.value)}
90
+ className={styles.filterInput}
91
+ max={customEndDateInput || new Date().toISOString().split('T')[0]}
92
+ />
93
+ </div>
94
+ <div className={styles.filterGroup}>
95
+ <label htmlFor="endDate">End Date:</label>
96
+ <input
97
+ type="date"
98
+ id="endDate"
99
+ value={customEndDateInput}
100
+ onChange={(e) => onCustomEndDateInputChange(e.target.value)}
101
+ className={styles.filterInput}
102
+ min={customStartDateInput}
103
+ max={new Date().toISOString().split('T')[0]}
104
+ />
105
+ </div>
106
+ <div className={styles.dateRangeButtons}>
107
+ {(customStartDateInput || customEndDateInput) && (
108
+ <button
109
+ type="button"
110
+ onClick={onApplyCustomDateRange}
111
+ className={styles.filterButton}
112
+ title="Apply custom date range"
113
+ >
114
+ Apply Dates
115
+ </button>
116
+ )}
117
+ {(customStartDate || customEndDate) && (
118
+ <button
119
+ type="button"
120
+ onClick={onClearCustomDateRange}
121
+ className={styles.clearButton}
122
+ title="Clear custom date range"
123
+ >
124
+ Clear Dates
125
+ </button>
126
+ )}
127
+ </div>
128
+ </div>
129
+ {(customStartDate || customEndDate) && (
130
+ <div className={styles.activeFilter}>
131
+ <small>
132
+ Custom range:
133
+ {customStartDate && <strong> from {new Date(customStartDate).toLocaleDateString()}</strong>}
134
+ {customEndDate && <strong> to {new Date(customEndDate).toLocaleDateString()}</strong>}
135
+ </small>
136
+ </div>
137
+ )}
138
+ </div>
139
+ )}
140
+
141
+ <div className={styles.filterGroup}>
142
+ <label htmlFor="caseFilter">Case Number:</label>
143
+ <div className={styles.inputWithButton}>
144
+ <input
145
+ type="text"
146
+ id="caseFilter"
147
+ value={caseNumberInput}
148
+ onChange={(e) => onCaseNumberInputChange(e.target.value)}
149
+ className={styles.filterInput}
150
+ placeholder="Enter case number..."
151
+ disabled={!!caseNumber}
152
+ title={
153
+ caseNumber
154
+ ? 'Case filter disabled - viewing specific case'
155
+ : 'Enter complete case number and click Filter'
156
+ }
157
+ onKeyDown={(e) => {
158
+ if (e.key === 'Enter' && caseNumberInput.trim() && !caseNumber) {
159
+ onApplyCaseFilter();
160
+ }
161
+ }}
162
+ />
163
+ {!caseNumber && (
164
+ <div className={styles.caseFilterButtons}>
165
+ {caseNumberInput.trim() && (
166
+ <button
167
+ type="button"
168
+ onClick={onApplyCaseFilter}
169
+ className={styles.filterButton}
170
+ title="Apply case filter"
171
+ >
172
+ Filter
173
+ </button>
174
+ )}
175
+ {filterCaseNumber && (
176
+ <button
177
+ type="button"
178
+ onClick={onClearCaseFilter}
179
+ className={styles.clearButton}
180
+ title="Clear case filter"
181
+ >
182
+ Clear
183
+ </button>
184
+ )}
185
+ </div>
186
+ )}
187
+ </div>
188
+ {filterCaseNumber && !caseNumber && (
189
+ <div className={styles.activeFilter}>
190
+ <small>
191
+ Filtering by case: <strong>{filterCaseNumber}</strong>
192
+ </small>
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ <div className={styles.filterGroup}>
198
+ <label htmlFor="badgeFilter">Badge/ID #:</label>
199
+ <div className={styles.inputWithButton}>
200
+ <input
201
+ type="text"
202
+ id="badgeFilter"
203
+ value={badgeIdInput}
204
+ onChange={(e) => onBadgeIdInputChange(e.target.value)}
205
+ className={styles.filterInput}
206
+ placeholder="Enter badge/id #..."
207
+ onKeyDown={(e) => {
208
+ if (e.key === 'Enter' && badgeIdInput.trim()) {
209
+ onApplyBadgeFilter();
210
+ }
211
+ }}
212
+ />
213
+ <div className={styles.caseFilterButtons}>
214
+ {badgeIdInput.trim() && (
215
+ <button
216
+ type="button"
217
+ onClick={onApplyBadgeFilter}
218
+ className={styles.filterButton}
219
+ title="Apply badge filter"
220
+ >
221
+ Filter
222
+ </button>
223
+ )}
224
+ {filterBadgeId && (
225
+ <button
226
+ type="button"
227
+ onClick={onClearBadgeFilter}
228
+ className={styles.clearButton}
229
+ title="Clear badge filter"
230
+ >
231
+ Clear
232
+ </button>
233
+ )}
234
+ </div>
235
+ </div>
236
+ {filterBadgeId && (
237
+ <div className={styles.activeFilter}>
238
+ <small>
239
+ Filtering by Badge/ID: <strong>{filterBadgeId}</strong>
240
+ </small>
241
+ </div>
242
+ )}
243
+ </div>
244
+
245
+ <div className={styles.filterGroup}>
246
+ <label htmlFor="actionFilter">Activity Type:</label>
247
+ <select
248
+ id="actionFilter"
249
+ value={filterAction}
250
+ onChange={(e) => onFilterActionChange(e.target.value as AuditAction | 'all')}
251
+ className={styles.filterSelect}
252
+ >
253
+ <option value="all">All Activities</option>
254
+ <optgroup label="User Sessions">
255
+ <option value="user-login">Login</option>
256
+ <option value="user-logout">Logout</option>
257
+ </optgroup>
258
+ <optgroup label="Case Management">
259
+ <option value="case-create">Case Create</option>
260
+ <option value="case-rename">Case Rename</option>
261
+ <option value="case-delete">Case Delete</option>
262
+ <option value="case-archive">Case Archive</option>
263
+ <option value="case-export">Case Export</option>
264
+ <option value="case-import">Case Import</option>
265
+ </optgroup>
266
+ <optgroup label="File Operations">
267
+ <option value="file-upload">File Upload</option>
268
+ <option value="file-access">File Access</option>
269
+ <option value="file-delete">File Delete</option>
270
+ </optgroup>
271
+ <optgroup label="Annotations">
272
+ <option value="annotation-create">Annotation Create</option>
273
+ <option value="annotation-edit">Annotation Edit</option>
274
+ <option value="annotation-delete">Annotation Delete</option>
275
+ </optgroup>
276
+ <optgroup label="Confirmation Activity">
277
+ <option value="confirmation-create">Confirmation Create</option>
278
+ <option value="confirmation-export">Confirmation Export</option>
279
+ <option value="confirmation-import">Confirmation Import</option>
280
+ </optgroup>
281
+ <optgroup label="Documents">
282
+ <option value="pdf-generate">PDF Generate</option>
283
+ </optgroup>
284
+ <optgroup label="Security">
285
+ <option value="security-violation">Security Violation</option>
286
+ </optgroup>
287
+ </select>
288
+ </div>
289
+
290
+ <div className={styles.filterGroup}>
291
+ <label htmlFor="resultFilter">Result:</label>
292
+ <select
293
+ id="resultFilter"
294
+ value={filterResult}
295
+ onChange={(e) => onFilterResultChange(e.target.value as AuditResult | 'all')}
296
+ className={styles.filterSelect}
297
+ >
298
+ <option value="all">All Results</option>
299
+ <option value="success">Success</option>
300
+ <option value="failure">Failure</option>
301
+ <option value="warning">Warning</option>
302
+ <option value="blocked">Blocked</option>
303
+ </select>
304
+ </div>
305
+ </div>
306
+ );
307
+ };
@@ -0,0 +1,44 @@
1
+ import type { User } from 'firebase/auth';
2
+ import type { UserData } from '~/types';
3
+ import styles from '../user-audit.module.css';
4
+
5
+ interface AuditUserInfoCardProps {
6
+ user: User;
7
+ userData: UserData | null;
8
+ userBadgeId: string;
9
+ }
10
+
11
+ export const AuditUserInfoCard = ({
12
+ user,
13
+ userData,
14
+ userBadgeId,
15
+ }: AuditUserInfoCardProps) => {
16
+ return (
17
+ <div className={styles.summary}>
18
+ <h3>User Information</h3>
19
+ <div className={styles.userInfoContent}>
20
+ <div className={styles.userInfoItem}>
21
+ Name:{' '}
22
+ <strong>
23
+ {userData ? `${userData.firstName} ${userData.lastName}` : user.displayName || 'Not provided'}
24
+ </strong>
25
+ </div>
26
+ <div className={styles.userInfoItem}>
27
+ Email: <strong>{user.email || 'Not provided'}</strong>
28
+ </div>
29
+ <div className={styles.userInfoItem}>
30
+ Lab/Company: <strong>{userData?.company || 'Not provided'}</strong>
31
+ </div>
32
+ <div className={styles.userInfoItem}>
33
+ Badge/ID #:{' '}
34
+ <span className={`${styles.badgeTag} ${!userBadgeId ? styles.badgeTagMuted : ''}`}>
35
+ {userBadgeId || 'Not provided'}
36
+ </span>
37
+ </div>
38
+ <div className={styles.userInfoItem}>
39
+ User ID: <strong>{user.uid}</strong>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ );
44
+ };