@striae-org/striae 4.0.3 → 4.1.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 (66) hide show
  1. package/app/components/actions/confirm-export.ts +4 -2
  2. package/app/components/actions/generate-pdf.ts +10 -2
  3. package/app/components/audit/user-audit-viewer.tsx +121 -940
  4. package/app/components/audit/user-audit.module.css +20 -0
  5. package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
  6. package/app/components/audit/viewer/audit-entries-list.tsx +200 -0
  7. package/app/components/audit/viewer/audit-filters-panel.tsx +306 -0
  8. package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
  9. package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
  10. package/app/components/audit/viewer/audit-viewer-utils.ts +121 -0
  11. package/app/components/audit/viewer/types.ts +1 -0
  12. package/app/components/audit/viewer/use-audit-viewer-data.ts +166 -0
  13. package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
  14. package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
  15. package/app/components/auth/mfa-enrollment.module.css +13 -5
  16. package/app/components/auth/mfa-verification.module.css +13 -5
  17. package/app/components/canvas/canvas.tsx +3 -0
  18. package/app/components/canvas/confirmation/confirmation.tsx +13 -37
  19. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  20. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +8 -37
  21. package/app/components/sidebar/case-export/case-export.tsx +9 -34
  22. package/app/components/sidebar/case-import/case-import.module.css +2 -0
  23. package/app/components/sidebar/case-import/case-import.tsx +10 -34
  24. package/app/components/sidebar/cases/cases-modal.module.css +44 -9
  25. package/app/components/sidebar/cases/cases-modal.tsx +16 -14
  26. package/app/components/sidebar/files/files-modal.module.css +45 -10
  27. package/app/components/sidebar/files/files-modal.tsx +16 -16
  28. package/app/components/sidebar/notes/notes-modal.tsx +17 -15
  29. package/app/components/sidebar/notes/notes.module.css +2 -0
  30. package/app/components/sidebar/sidebar.module.css +2 -2
  31. package/app/components/toast/toast.module.css +2 -1
  32. package/app/components/toast/toast.tsx +16 -11
  33. package/app/components/user/delete-account.tsx +10 -31
  34. package/app/components/user/inactivity-warning.module.css +8 -6
  35. package/app/components/user/manage-profile.module.css +2 -0
  36. package/app/components/user/manage-profile.tsx +85 -30
  37. package/app/hooks/useOverlayDismiss.ts +68 -0
  38. package/app/routes/auth/login.example.tsx +19 -8
  39. package/app/routes/auth/passwordReset.module.css +23 -13
  40. package/app/routes/striae/striae.tsx +8 -1
  41. package/app/routes.ts +7 -0
  42. package/app/services/audit/audit-export-csv.ts +2 -0
  43. package/app/services/audit/audit.service.ts +29 -5
  44. package/app/services/audit/builders/audit-entry-builder.ts +2 -1
  45. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  46. package/app/services/audit/builders/audit-event-builders-workflow.ts +6 -0
  47. package/app/types/audit.ts +2 -1
  48. package/app/types/user.ts +1 -0
  49. package/app/utils/data/permissions.ts +1 -0
  50. package/functions/api/pdf/[[path]].ts +32 -1
  51. package/load-context.ts +9 -0
  52. package/package.json +5 -1
  53. package/primershear.emails.example +6 -0
  54. package/scripts/deploy-pages-secrets.sh +6 -0
  55. package/scripts/deploy-primershear-emails.sh +166 -0
  56. package/worker-configuration.d.ts +7493 -7491
  57. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  58. package/workers/data-worker/wrangler.jsonc.example +1 -1
  59. package/workers/image-worker/wrangler.jsonc.example +1 -1
  60. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  61. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
  62. package/workers/pdf-worker/src/report-types.ts +3 -0
  63. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  64. package/workers/user-worker/src/user-worker.example.ts +6 -1
  65. package/workers/user-worker/wrangler.jsonc.example +1 -1
  66. package/wrangler.toml.example +1 -1
@@ -531,6 +531,26 @@
531
531
  word-break: break-word;
532
532
  }
533
533
 
534
+ .badgeTag {
535
+ display: inline-flex;
536
+ align-items: center;
537
+ padding: 2px 10px;
538
+ border-radius: 999px;
539
+ border: 1px solid color-mix(in lab, var(--primary) 35%, transparent);
540
+ background: color-mix(in lab, var(--primary) 12%, transparent);
541
+ color: color-mix(in lab, var(--primary) 70%, var(--text));
542
+ font-size: 0.78rem;
543
+ font-weight: var(--fontWeightMedium);
544
+ letter-spacing: 0.2px;
545
+ line-height: 1.4;
546
+ }
547
+
548
+ .badgeTagMuted {
549
+ border-color: color-mix(in lab, var(--textLight) 25%, transparent);
550
+ background: color-mix(in lab, var(--backgroundLight) 70%, transparent);
551
+ color: var(--textLight);
552
+ }
553
+
534
554
  .severity {
535
555
  font-weight: var(--fontWeightMedium);
536
556
  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,200 @@
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.result === 'failure' && entry.details.validationErrors.length > 0 && (
48
+ <div className={styles.detailRow}>
49
+ <span className={styles.detailLabel}>Error:</span>
50
+ <span className={styles.detailValue}>{entry.details.validationErrors[0]}</span>
51
+ </div>
52
+ )}
53
+
54
+ {(entry.action === 'user-login' || entry.action === 'user-logout') && entry.details.sessionDetails && (
55
+ <>
56
+ {entry.details.sessionDetails.userAgent && (
57
+ <div className={styles.detailRow}>
58
+ <span className={styles.detailLabel}>User Agent:</span>
59
+ <span className={styles.detailValue}>{entry.details.sessionDetails.userAgent}</span>
60
+ </div>
61
+ )}
62
+ </>
63
+ )}
64
+
65
+ {entry.action === 'security-violation' && entry.details.securityDetails && (
66
+ <>
67
+ <div className={styles.detailRow}>
68
+ <span className={styles.detailLabel}>Severity:</span>
69
+ <span
70
+ className={`${styles.detailValue} ${styles.severity} ${styles[entry.details.securityDetails.severity || 'low']}`}
71
+ >
72
+ {(entry.details.securityDetails.severity || 'low').toUpperCase()}
73
+ </span>
74
+ </div>
75
+ {entry.details.securityDetails.incidentType && (
76
+ <div className={styles.detailRow}>
77
+ <span className={styles.detailLabel}>Type:</span>
78
+ <span className={styles.detailValue}>{entry.details.securityDetails.incidentType}</span>
79
+ </div>
80
+ )}
81
+ </>
82
+ )}
83
+
84
+ {(entry.action === 'file-upload' || entry.action === 'file-delete' || entry.action === 'file-access') && entry.details.fileDetails && (
85
+ <>
86
+ {entry.details.fileDetails.fileId && (
87
+ <div className={styles.detailRow}>
88
+ <span className={styles.detailLabel}>File ID:</span>
89
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
90
+ </div>
91
+ )}
92
+
93
+ {entry.details.fileDetails.originalFileName && (
94
+ <div className={styles.detailRow}>
95
+ <span className={styles.detailLabel}>Original Filename:</span>
96
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
97
+ </div>
98
+ )}
99
+
100
+ {entry.details.fileDetails.fileSize > 0 && (
101
+ <div className={styles.detailRow}>
102
+ <span className={styles.detailLabel}>File Size:</span>
103
+ <span className={styles.detailValue}>
104
+ {(entry.details.fileDetails.fileSize / 1024 / 1024).toFixed(2)} MB
105
+ </span>
106
+ </div>
107
+ )}
108
+
109
+ {entry.details.fileDetails.uploadMethod && (
110
+ <div className={styles.detailRow}>
111
+ <span className={styles.detailLabel}>
112
+ {entry.action === 'file-access' ? 'Access Method' : 'Upload Method'}:
113
+ </span>
114
+ <span className={styles.detailValue}>{entry.details.fileDetails.uploadMethod}</span>
115
+ </div>
116
+ )}
117
+
118
+ {entry.details.fileDetails.deleteReason && (
119
+ <div className={styles.detailRow}>
120
+ <span className={styles.detailLabel}>Reason:</span>
121
+ <span className={styles.detailValue}>{entry.details.fileDetails.deleteReason}</span>
122
+ </div>
123
+ )}
124
+
125
+ {entry.details.fileDetails.sourceLocation && entry.action === 'file-access' && (
126
+ <div className={styles.detailRow}>
127
+ <span className={styles.detailLabel}>Access Source:</span>
128
+ <span className={styles.detailValue}>{entry.details.fileDetails.sourceLocation}</span>
129
+ </div>
130
+ )}
131
+ </>
132
+ )}
133
+
134
+ {(entry.action === 'annotation-create' || entry.action === 'annotation-edit' || entry.action === 'annotation-delete') && entry.details.fileDetails && (
135
+ <>
136
+ {entry.details.fileDetails.fileId && (
137
+ <div className={styles.detailRow}>
138
+ <span className={styles.detailLabel}>File ID:</span>
139
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
140
+ </div>
141
+ )}
142
+
143
+ {entry.details.fileDetails.originalFileName && (
144
+ <div className={styles.detailRow}>
145
+ <span className={styles.detailLabel}>Original Filename:</span>
146
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
147
+ </div>
148
+ )}
149
+
150
+ {entry.details.annotationDetails?.annotationType && (
151
+ <div className={styles.detailRow}>
152
+ <span className={styles.detailLabel}>Annotation Type:</span>
153
+ <span className={styles.detailValue}>{entry.details.annotationDetails.annotationType}</span>
154
+ </div>
155
+ )}
156
+
157
+ {entry.details.annotationDetails?.tool && (
158
+ <div className={styles.detailRow}>
159
+ <span className={styles.detailLabel}>Tool:</span>
160
+ <span className={styles.detailValue}>{entry.details.annotationDetails.tool}</span>
161
+ </div>
162
+ )}
163
+ </>
164
+ )}
165
+
166
+ {(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
167
+ <>
168
+ {entry.details.fileDetails.fileId && (
169
+ <div className={styles.detailRow}>
170
+ <span className={styles.detailLabel}>
171
+ {entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
172
+ </span>
173
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
174
+ </div>
175
+ )}
176
+
177
+ {entry.details.fileDetails.originalFileName && (
178
+ <div className={styles.detailRow}>
179
+ <span className={styles.detailLabel}>
180
+ {entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
181
+ </span>
182
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
183
+ </div>
184
+ )}
185
+
186
+ {entry.action === 'confirm' && entry.details.confirmationId && (
187
+ <div className={styles.detailRow}>
188
+ <span className={styles.detailLabel}>Confirmation ID:</span>
189
+ <span className={styles.detailValue}>{entry.details.confirmationId}</span>
190
+ </div>
191
+ )}
192
+ </>
193
+ )}
194
+ </div>
195
+ </div>
196
+ ))
197
+ )}
198
+ </div>
199
+ );
200
+ };
@@ -0,0 +1,306 @@
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-export">Case Export</option>
263
+ <option value="case-import">Case Import</option>
264
+ </optgroup>
265
+ <optgroup label="File Operations">
266
+ <option value="file-upload">File Upload</option>
267
+ <option value="file-access">File Access</option>
268
+ <option value="file-delete">File Delete</option>
269
+ </optgroup>
270
+ <optgroup label="Annotations">
271
+ <option value="annotation-create">Annotation Create</option>
272
+ <option value="annotation-edit">Annotation Edit</option>
273
+ <option value="annotation-delete">Annotation Delete</option>
274
+ </optgroup>
275
+ <optgroup label="Confirmation Activity">
276
+ <option value="confirmation-create">Confirmation Create</option>
277
+ <option value="confirmation-export">Confirmation Export</option>
278
+ <option value="confirmation-import">Confirmation Import</option>
279
+ </optgroup>
280
+ <optgroup label="Documents">
281
+ <option value="pdf-generate">PDF Generate</option>
282
+ </optgroup>
283
+ <optgroup label="Security">
284
+ <option value="security-violation">Security Violation</option>
285
+ </optgroup>
286
+ </select>
287
+ </div>
288
+
289
+ <div className={styles.filterGroup}>
290
+ <label htmlFor="resultFilter">Result:</label>
291
+ <select
292
+ id="resultFilter"
293
+ value={filterResult}
294
+ onChange={(e) => onFilterResultChange(e.target.value as AuditResult | 'all')}
295
+ className={styles.filterSelect}
296
+ >
297
+ <option value="all">All Results</option>
298
+ <option value="success">Success</option>
299
+ <option value="failure">Failure</option>
300
+ <option value="warning">Warning</option>
301
+ <option value="blocked">Blocked</option>
302
+ </select>
303
+ </div>
304
+ </div>
305
+ );
306
+ };
@@ -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
+ };
@@ -0,0 +1,55 @@
1
+ import styles from '../user-audit.module.css';
2
+
3
+ interface AuditViewerHeaderProps {
4
+ title: string;
5
+ hasEntries: boolean;
6
+ onExportCSV: () => void;
7
+ onExportJSON: () => void;
8
+ onGenerateReport: () => void;
9
+ onClose: () => void;
10
+ }
11
+
12
+ export const AuditViewerHeader = ({
13
+ title,
14
+ hasEntries,
15
+ onExportCSV,
16
+ onExportJSON,
17
+ onGenerateReport,
18
+ onClose,
19
+ }: AuditViewerHeaderProps) => {
20
+ return (
21
+ <div className={styles.header}>
22
+ <h2 className={styles.title}>{title}</h2>
23
+ <div className={styles.headerActions}>
24
+ {hasEntries && (
25
+ <div className={styles.exportButtons}>
26
+ <button
27
+ onClick={onExportCSV}
28
+ className={styles.exportButton}
29
+ title="CSV - Individual entry log with summary data"
30
+ >
31
+ 📊 CSV
32
+ </button>
33
+ <button
34
+ onClick={onExportJSON}
35
+ className={styles.exportButton}
36
+ title="JSON - Complete log data for version capture and auditing"
37
+ >
38
+ 📄 JSON
39
+ </button>
40
+ <button
41
+ onClick={onGenerateReport}
42
+ className={styles.exportButton}
43
+ title="Summary report only"
44
+ >
45
+ 📋 Report
46
+ </button>
47
+ </div>
48
+ )}
49
+ <button className={styles.closeButton} onClick={onClose}>
50
+ ×
51
+ </button>
52
+ </div>
53
+ </div>
54
+ );
55
+ };