@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
@@ -0,0 +1,121 @@
1
+ import { type AuditAction, type AuditResult, type ValidationAuditEntry } from '~/types';
2
+
3
+ export interface AuditViewerSummaryStats {
4
+ totalEntries: number;
5
+ successfulEntries: number;
6
+ failedEntries: number;
7
+ loginSessions: number;
8
+ securityIncidents: number;
9
+ }
10
+
11
+ export const getAuditActionIcon = (action: AuditAction): string => {
12
+ switch (action) {
13
+ case 'user-login':
14
+ return '🔑';
15
+ case 'user-logout':
16
+ return '🚪';
17
+ case 'user-profile-update':
18
+ return '👤';
19
+ case 'user-password-reset':
20
+ return '🔒';
21
+ case 'user-registration':
22
+ return '📝';
23
+ case 'email-verification':
24
+ return '📧';
25
+ case 'mfa-enrollment':
26
+ return '🔐';
27
+ case 'mfa-authentication':
28
+ return '📱';
29
+ case 'case-create':
30
+ return '📂';
31
+ case 'case-rename':
32
+ return '✏️';
33
+ case 'case-delete':
34
+ return '🗑️';
35
+ case 'case-export':
36
+ return '📤';
37
+ case 'case-import':
38
+ return '📥';
39
+ case 'confirmation-create':
40
+ return '✅';
41
+ case 'confirmation-export':
42
+ return '📤';
43
+ case 'confirmation-import':
44
+ return '📥';
45
+ case 'file-upload':
46
+ return '⬆️';
47
+ case 'file-delete':
48
+ return '🗑️';
49
+ case 'file-access':
50
+ return '👁️';
51
+ case 'annotation-create':
52
+ return '✨';
53
+ case 'annotation-edit':
54
+ return '✏️';
55
+ case 'annotation-delete':
56
+ return '❌';
57
+ case 'pdf-generate':
58
+ return '📄';
59
+ case 'security-violation':
60
+ return '🚨';
61
+ case 'export':
62
+ return '📤';
63
+ case 'import':
64
+ return '📥';
65
+ case 'confirm':
66
+ return '✓';
67
+ default:
68
+ return '📄';
69
+ }
70
+ };
71
+
72
+ export const getAuditStatusIcon = (result: AuditResult): string => {
73
+ switch (result) {
74
+ case 'success':
75
+ return '✅';
76
+ case 'failure':
77
+ return '❌';
78
+ case 'warning':
79
+ return '⚠️';
80
+ case 'blocked':
81
+ return '🛑';
82
+ case 'pending':
83
+ return '⏳';
84
+ default:
85
+ return '❓';
86
+ }
87
+ };
88
+
89
+ export const formatAuditTimestamp = (timestamp: string): string => {
90
+ return new Date(timestamp).toLocaleString();
91
+ };
92
+
93
+ export const summarizeAuditEntries = (entries: ValidationAuditEntry[]): AuditViewerSummaryStats => {
94
+ return entries.reduce<AuditViewerSummaryStats>((summary, entry) => {
95
+ summary.totalEntries += 1;
96
+
97
+ if (entry.result === 'success') {
98
+ summary.successfulEntries += 1;
99
+ }
100
+
101
+ if (entry.result === 'failure') {
102
+ summary.failedEntries += 1;
103
+ }
104
+
105
+ if (entry.action === 'user-login') {
106
+ summary.loginSessions += 1;
107
+ }
108
+
109
+ if (entry.action === 'security-violation') {
110
+ summary.securityIncidents += 1;
111
+ }
112
+
113
+ return summary;
114
+ }, {
115
+ totalEntries: 0,
116
+ successfulEntries: 0,
117
+ failedEntries: 0,
118
+ loginSessions: 0,
119
+ securityIncidents: 0
120
+ });
121
+ };
@@ -0,0 +1 @@
1
+ export type DateRangeFilter = '1d' | '7d' | '30d' | '90d' | 'custom';
@@ -0,0 +1,166 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import type { User } from 'firebase/auth';
3
+ import { auditService } from '~/services/audit';
4
+ import { type AuditTrail, type UserData, type ValidationAuditEntry, type WorkflowPhase } from '~/types';
5
+ import { getUserData } from '~/utils/data';
6
+ import type { DateRangeFilter } from './types';
7
+
8
+ const isWorkflowPhase = (phase: unknown): phase is WorkflowPhase =>
9
+ phase === 'casework' ||
10
+ phase === 'case-export' ||
11
+ phase === 'case-import' ||
12
+ phase === 'confirmation' ||
13
+ phase === 'user-management';
14
+
15
+ interface UseAuditViewerDataParams {
16
+ isOpen: boolean;
17
+ user: User | null;
18
+ effectiveCaseNumber?: string;
19
+ dateRange: DateRangeFilter;
20
+ customStartDate: string;
21
+ customEndDate: string;
22
+ }
23
+
24
+ interface AuditDateQuery {
25
+ startDate?: string;
26
+ endDate?: string;
27
+ }
28
+
29
+ const buildAuditDateQuery = (
30
+ dateRange: DateRangeFilter,
31
+ customStartDate: string,
32
+ customEndDate: string
33
+ ): AuditDateQuery => {
34
+ let startDate: string | undefined;
35
+ let endDate: string | undefined;
36
+
37
+ if (dateRange === 'custom') {
38
+ if (customStartDate) {
39
+ startDate = new Date(customStartDate + 'T00:00:00').toISOString();
40
+ }
41
+
42
+ if (customEndDate) {
43
+ endDate = new Date(customEndDate + 'T23:59:59').toISOString();
44
+ }
45
+
46
+ if (customStartDate && !customEndDate) {
47
+ endDate = new Date().toISOString();
48
+ } else if (!customStartDate && customEndDate) {
49
+ const startDateObj = new Date(customEndDate + 'T23:59:59');
50
+ startDateObj.setDate(startDateObj.getDate() - 30);
51
+ startDate = startDateObj.toISOString();
52
+ }
53
+ } else if (dateRange === '90d') {
54
+ const startDateObj = new Date();
55
+ startDateObj.setDate(startDateObj.getDate() - 90);
56
+ startDate = startDateObj.toISOString();
57
+ endDate = new Date().toISOString();
58
+ } else {
59
+ const days = parseInt(dateRange.replace('d', ''), 10);
60
+ const startDateObj = new Date();
61
+ startDateObj.setDate(startDateObj.getDate() - days);
62
+ startDate = startDateObj.toISOString();
63
+ endDate = new Date().toISOString();
64
+ }
65
+
66
+ return {
67
+ startDate,
68
+ endDate
69
+ };
70
+ };
71
+
72
+ export const useAuditViewerData = ({
73
+ isOpen,
74
+ user,
75
+ effectiveCaseNumber,
76
+ dateRange,
77
+ customStartDate,
78
+ customEndDate
79
+ }: UseAuditViewerDataParams) => {
80
+ const [auditEntries, setAuditEntries] = useState<ValidationAuditEntry[]>([]);
81
+ const [userData, setUserData] = useState<UserData | null>(null);
82
+ const [loading, setLoading] = useState(false);
83
+ const [error, setError] = useState<string>('');
84
+ const [auditTrail, setAuditTrail] = useState<AuditTrail | null>(null);
85
+
86
+ const loadUserData = useCallback(async () => {
87
+ if (!user) {
88
+ return;
89
+ }
90
+
91
+ try {
92
+ const data = await getUserData(user);
93
+ setUserData(data);
94
+ } catch (loadError) {
95
+ console.error('Failed to load user data:', loadError);
96
+ }
97
+ }, [user]);
98
+
99
+ const loadAuditData = useCallback(async () => {
100
+ if (!user?.uid) {
101
+ return;
102
+ }
103
+
104
+ setLoading(true);
105
+ setError('');
106
+
107
+ try {
108
+ const { startDate, endDate } = buildAuditDateQuery(dateRange, customStartDate, customEndDate);
109
+
110
+ const entries = await auditService.getAuditEntriesForUser(user.uid, {
111
+ caseNumber: effectiveCaseNumber,
112
+ startDate,
113
+ endDate,
114
+ limit: effectiveCaseNumber ? 1000 : 500
115
+ });
116
+
117
+ setAuditEntries(entries);
118
+
119
+ if (effectiveCaseNumber && entries.length > 0) {
120
+ const trail: AuditTrail = {
121
+ caseNumber: effectiveCaseNumber,
122
+ workflowId: `workflow-${effectiveCaseNumber}-${user.uid}`,
123
+ entries,
124
+ summary: {
125
+ totalEvents: entries.length,
126
+ successfulEvents: entries.filter(entry => entry.result === 'success').length,
127
+ failedEvents: entries.filter(entry => entry.result === 'failure').length,
128
+ warningEvents: entries.filter(entry => entry.result === 'warning').length,
129
+ workflowPhases: [...new Set(entries
130
+ .map(entry => entry.details.workflowPhase)
131
+ .filter(isWorkflowPhase))],
132
+ participatingUsers: [...new Set(entries.map(entry => entry.userId))],
133
+ startTimestamp: entries[entries.length - 1]?.timestamp || new Date().toISOString(),
134
+ endTimestamp: entries[0]?.timestamp || new Date().toISOString(),
135
+ complianceStatus: entries.some(entry => entry.result === 'failure') ? 'non-compliant' : 'compliant',
136
+ securityIncidents: entries.filter(entry => entry.action === 'security-violation').length
137
+ }
138
+ };
139
+ setAuditTrail(trail);
140
+ } else {
141
+ setAuditTrail(null);
142
+ }
143
+ } catch (loadError) {
144
+ setError(loadError instanceof Error ? loadError.message : 'Failed to load audit data');
145
+ } finally {
146
+ setLoading(false);
147
+ }
148
+ }, [user, dateRange, customStartDate, customEndDate, effectiveCaseNumber]);
149
+
150
+ useEffect(() => {
151
+ if (isOpen && user) {
152
+ void loadAuditData();
153
+ void loadUserData();
154
+ }
155
+ }, [isOpen, user, loadAuditData, loadUserData]);
156
+
157
+ return {
158
+ auditEntries,
159
+ userData,
160
+ loading,
161
+ error,
162
+ setError,
163
+ auditTrail,
164
+ loadAuditData
165
+ };
166
+ };
@@ -0,0 +1,176 @@
1
+ import { useCallback } from 'react';
2
+ import type { Dispatch, SetStateAction } from 'react';
3
+ import type { User } from 'firebase/auth';
4
+ import { auditExportService } from '~/services/audit';
5
+ import type { AuditTrail, ValidationAuditEntry } from '~/types';
6
+
7
+ interface UseAuditViewerExportParams {
8
+ user: User | null;
9
+ effectiveCaseNumber?: string;
10
+ filteredEntries: ValidationAuditEntry[];
11
+ auditTrail: AuditTrail | null;
12
+ setError: Dispatch<SetStateAction<string>>;
13
+ }
14
+
15
+ export const useAuditViewerExport = ({
16
+ user,
17
+ effectiveCaseNumber,
18
+ filteredEntries,
19
+ auditTrail,
20
+ setError
21
+ }: UseAuditViewerExportParams) => {
22
+ const resolveExportContext = useCallback(() => {
23
+ if (!user) {
24
+ return null;
25
+ }
26
+
27
+ const identifier = effectiveCaseNumber || user.uid;
28
+ const scopeType: 'case' | 'user' = effectiveCaseNumber ? 'case' : 'user';
29
+
30
+ return {
31
+ identifier,
32
+ scopeType,
33
+ context: {
34
+ user,
35
+ scopeType,
36
+ scopeIdentifier: identifier,
37
+ caseNumber: effectiveCaseNumber || undefined
38
+ } as const
39
+ };
40
+ }, [user, effectiveCaseNumber]);
41
+
42
+ const handleExportCSV = useCallback(async () => {
43
+ const exportContextData = resolveExportContext();
44
+ if (!exportContextData) {
45
+ return;
46
+ }
47
+
48
+ const filename = auditExportService.generateFilename(
49
+ exportContextData.scopeType,
50
+ exportContextData.identifier,
51
+ 'csv'
52
+ );
53
+
54
+ try {
55
+ if (auditTrail && effectiveCaseNumber) {
56
+ await auditExportService.exportAuditTrailToCSV(auditTrail, filename, exportContextData.context);
57
+ } else {
58
+ await auditExportService.exportToCSV(filteredEntries, filename, exportContextData.context);
59
+ }
60
+ } catch (exportError) {
61
+ console.error('Export failed:', exportError);
62
+ setError('Failed to export audit trail to CSV');
63
+ }
64
+ }, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
65
+
66
+ const handleExportJSON = useCallback(async () => {
67
+ const exportContextData = resolveExportContext();
68
+ if (!exportContextData) {
69
+ return;
70
+ }
71
+
72
+ const filename = auditExportService.generateFilename(
73
+ exportContextData.scopeType,
74
+ exportContextData.identifier,
75
+ 'csv'
76
+ );
77
+
78
+ try {
79
+ if (auditTrail && effectiveCaseNumber) {
80
+ await auditExportService.exportAuditTrailToJSON(auditTrail, filename, exportContextData.context);
81
+ } else {
82
+ await auditExportService.exportToJSON(filteredEntries, filename, exportContextData.context);
83
+ }
84
+ } catch (exportError) {
85
+ console.error('Export failed:', exportError);
86
+ setError('Failed to export audit trail to JSON');
87
+ }
88
+ }, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
89
+
90
+ const handleGenerateReport = useCallback(async () => {
91
+ const exportContextData = resolveExportContext();
92
+ if (!exportContextData) {
93
+ return;
94
+ }
95
+
96
+ const resolvedUser = exportContextData.context.user;
97
+
98
+ const filename = `${exportContextData.scopeType}-audit-report-${exportContextData.identifier}-${new Date().toISOString().split('T')[0]}.txt`;
99
+
100
+ try {
101
+ let reportContent: string;
102
+
103
+ if (auditTrail && effectiveCaseNumber) {
104
+ reportContent = await auditExportService.generateReportSummary(auditTrail, exportContextData.context);
105
+ } else {
106
+ const totalEntries = filteredEntries.length;
107
+ const successfulActions = filteredEntries.filter(entry => entry.result === 'success').length;
108
+ const failedActions = filteredEntries.filter(entry => entry.result === 'failure').length;
109
+
110
+ const actionCounts = filteredEntries.reduce((accumulator, entry) => {
111
+ accumulator[entry.action] = (accumulator[entry.action] || 0) + 1;
112
+ return accumulator;
113
+ }, {} as Record<string, number>);
114
+
115
+ const detectedDateRange = filteredEntries.length > 0
116
+ ? {
117
+ earliest: new Date(Math.min(...filteredEntries.map(entry => new Date(entry.timestamp).getTime()))),
118
+ latest: new Date(Math.max(...filteredEntries.map(entry => new Date(entry.timestamp).getTime())))
119
+ }
120
+ : null;
121
+
122
+ reportContent = `${effectiveCaseNumber ? 'CASE' : 'USER'} AUDIT REPORT
123
+ Generated: ${new Date().toISOString()}
124
+ ${effectiveCaseNumber ? `Case: ${effectiveCaseNumber}` : `User: ${resolvedUser.email}`}
125
+ ${effectiveCaseNumber ? '' : `User ID: ${resolvedUser.uid}`}
126
+
127
+ === SUMMARY ===
128
+ Total Actions: ${totalEntries}
129
+ Successful: ${successfulActions}
130
+ Failed: ${failedActions}
131
+ Success Rate: ${totalEntries > 0 ? ((successfulActions / totalEntries) * 100).toFixed(1) : 0}%
132
+
133
+ ${detectedDateRange ? `Date Range: ${detectedDateRange.earliest.toLocaleDateString()} - ${detectedDateRange.latest.toLocaleDateString()}` : 'No entries found'}
134
+
135
+ === ACTION BREAKDOWN ===
136
+ ${Object.entries(actionCounts)
137
+ .sort(([, actionCountA], [, actionCountB]) => actionCountB - actionCountA)
138
+ .map(([action, count]) => `${action}: ${count}`)
139
+ .join('\n')}
140
+
141
+ === RECENT ACTIVITIES ===
142
+ ${filteredEntries.slice(0, 10).map(entry =>
143
+ `${new Date(entry.timestamp).toLocaleString()} | ${entry.action} | ${entry.result}${entry.details.caseNumber ? ` | Case: ${entry.details.caseNumber}` : ''}`
144
+ ).join('\n')}
145
+
146
+ Generated by Striae
147
+ `;
148
+
149
+ reportContent = await auditExportService.appendSignedReportIntegrity(
150
+ reportContent,
151
+ exportContextData.context,
152
+ totalEntries
153
+ );
154
+ }
155
+
156
+ const blob = new Blob([reportContent], { type: 'text/plain' });
157
+ const url = URL.createObjectURL(blob);
158
+ const anchor = document.createElement('a');
159
+ anchor.href = url;
160
+ anchor.download = filename;
161
+ document.body.appendChild(anchor);
162
+ anchor.click();
163
+ document.body.removeChild(anchor);
164
+ URL.revokeObjectURL(url);
165
+ } catch (reportError) {
166
+ console.error('Report generation failed:', reportError);
167
+ setError('Failed to generate audit report');
168
+ }
169
+ }, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
170
+
171
+ return {
172
+ handleExportCSV,
173
+ handleExportJSON,
174
+ handleGenerateReport
175
+ };
176
+ };
@@ -0,0 +1,141 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import type { AuditAction, AuditResult, ValidationAuditEntry } from '~/types';
3
+ import type { DateRangeFilter } from './types';
4
+
5
+ export const useAuditViewerFilters = (caseNumber?: string) => {
6
+ const [filterAction, setFilterAction] = useState<AuditAction | 'all'>('all');
7
+ const [filterResult, setFilterResult] = useState<AuditResult | 'all'>('all');
8
+ const [filterCaseNumber, setFilterCaseNumber] = useState<string>('');
9
+ const [caseNumberInput, setCaseNumberInput] = useState<string>('');
10
+ const [filterBadgeId, setFilterBadgeId] = useState<string>('');
11
+ const [badgeIdInput, setBadgeIdInput] = useState<string>('');
12
+ const [dateRange, setDateRange] = useState<DateRangeFilter>('1d');
13
+ const [customStartDate, setCustomStartDate] = useState<string>('');
14
+ const [customEndDate, setCustomEndDate] = useState<string>('');
15
+ const [customStartDateInput, setCustomStartDateInput] = useState<string>('');
16
+ const [customEndDateInput, setCustomEndDateInput] = useState<string>('');
17
+
18
+ const handleApplyCaseFilter = useCallback(() => {
19
+ setFilterCaseNumber(caseNumberInput.trim());
20
+ }, [caseNumberInput]);
21
+
22
+ const handleClearCaseFilter = useCallback(() => {
23
+ setCaseNumberInput('');
24
+ setFilterCaseNumber('');
25
+ }, []);
26
+
27
+ const handleApplyBadgeFilter = useCallback(() => {
28
+ setFilterBadgeId(badgeIdInput.trim());
29
+ }, [badgeIdInput]);
30
+
31
+ const handleClearBadgeFilter = useCallback(() => {
32
+ setBadgeIdInput('');
33
+ setFilterBadgeId('');
34
+ }, []);
35
+
36
+ const handleApplyCustomDateRange = useCallback(() => {
37
+ setCustomStartDate(customStartDateInput);
38
+ setCustomEndDate(customEndDateInput);
39
+ }, [customStartDateInput, customEndDateInput]);
40
+
41
+ const handleClearCustomDateRange = useCallback(() => {
42
+ setCustomStartDateInput('');
43
+ setCustomEndDateInput('');
44
+ setCustomStartDate('');
45
+ setCustomEndDate('');
46
+ }, []);
47
+
48
+ const handleDateRangeChange = useCallback((value: DateRangeFilter) => {
49
+ setDateRange(value);
50
+ if (value === 'custom') {
51
+ setCustomStartDateInput(customStartDate);
52
+ setCustomEndDateInput(customEndDate);
53
+ }
54
+ }, [customStartDate, customEndDate]);
55
+
56
+ const getFilteredEntries = useCallback((entries: ValidationAuditEntry[]): ValidationAuditEntry[] => {
57
+ const normalizedBadgeFilter = filterBadgeId.trim().toLowerCase();
58
+
59
+ return entries.filter(entry => {
60
+ let actionMatch: boolean;
61
+ if (filterAction === 'all') {
62
+ actionMatch = true;
63
+ } else if (filterAction === 'confirmation-create') {
64
+ actionMatch = entry.action === 'confirm' || entry.action === 'confirmation-create';
65
+ } else if (filterAction === 'case-export') {
66
+ actionMatch = entry.action === 'export' && entry.details.workflowPhase === 'case-export';
67
+ } else if (filterAction === 'case-import') {
68
+ actionMatch = entry.action === 'import' && entry.details.workflowPhase === 'case-import';
69
+ } else if (filterAction === 'confirmation-export') {
70
+ actionMatch = entry.action === 'export' && entry.details.workflowPhase === 'confirmation';
71
+ } else if (filterAction === 'confirmation-import') {
72
+ actionMatch = entry.action === 'import' && entry.details.workflowPhase === 'confirmation';
73
+ } else {
74
+ actionMatch = entry.action === filterAction;
75
+ }
76
+
77
+ const resultMatch = filterResult === 'all' || entry.result === filterResult;
78
+ const entryBadgeId = entry.details.userProfileDetails?.badgeId?.trim().toLowerCase() || '';
79
+ const badgeMatch = normalizedBadgeFilter === '' || entryBadgeId.includes(normalizedBadgeFilter);
80
+
81
+ return actionMatch && resultMatch && badgeMatch;
82
+ });
83
+ }, [filterAction, filterResult, filterBadgeId]);
84
+
85
+ const dateRangeDisplay = useMemo(() => {
86
+ switch (dateRange) {
87
+ case '90d':
88
+ return 'Last 90 Days';
89
+ case 'custom':
90
+ if (customStartDate && customEndDate) {
91
+ const startFormatted = new Date(customStartDate).toLocaleDateString();
92
+ const endFormatted = new Date(customEndDate).toLocaleDateString();
93
+ return `${startFormatted} - ${endFormatted}`;
94
+ }
95
+ if (customStartDate) {
96
+ return `From ${new Date(customStartDate).toLocaleDateString()}`;
97
+ }
98
+ if (customEndDate) {
99
+ return `Until ${new Date(customEndDate).toLocaleDateString()}`;
100
+ }
101
+ return 'Custom Range';
102
+ default:
103
+ return `Last ${dateRange}`;
104
+ }
105
+ }, [dateRange, customStartDate, customEndDate]);
106
+
107
+ const effectiveCaseNumber = useMemo(() => {
108
+ const trimmedCaseNumber = filterCaseNumber.trim();
109
+ return caseNumber || trimmedCaseNumber || undefined;
110
+ }, [caseNumber, filterCaseNumber]);
111
+
112
+ return {
113
+ filterAction,
114
+ setFilterAction,
115
+ filterResult,
116
+ setFilterResult,
117
+ filterCaseNumber,
118
+ caseNumberInput,
119
+ setCaseNumberInput,
120
+ filterBadgeId,
121
+ badgeIdInput,
122
+ setBadgeIdInput,
123
+ dateRange,
124
+ customStartDate,
125
+ customEndDate,
126
+ customStartDateInput,
127
+ customEndDateInput,
128
+ setCustomStartDateInput,
129
+ setCustomEndDateInput,
130
+ handleApplyCaseFilter,
131
+ handleClearCaseFilter,
132
+ handleApplyBadgeFilter,
133
+ handleClearBadgeFilter,
134
+ handleApplyCustomDateRange,
135
+ handleClearCustomDateRange,
136
+ handleDateRangeChange,
137
+ getFilteredEntries,
138
+ dateRangeDisplay,
139
+ effectiveCaseNumber
140
+ };
141
+ };
@@ -10,6 +10,7 @@
10
10
  justify-content: center;
11
11
  z-index: 1000;
12
12
  padding: 1rem;
13
+ cursor: default;
13
14
  }
14
15
 
15
16
  .modal {
@@ -21,6 +22,7 @@
21
22
  max-height: 90vh;
22
23
  overflow-y: auto;
23
24
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
25
+ cursor: default;
24
26
  }
25
27
 
26
28
  .header {
@@ -149,7 +151,8 @@
149
151
  }
150
152
 
151
153
  .errorMessage {
152
- background: linear-gradient(135deg,
154
+ background: linear-gradient(
155
+ 135deg,
153
156
  color-mix(in lab, var(--error) 12%, transparent),
154
157
  color-mix(in lab, var(--error) 8%, transparent)
155
158
  );
@@ -169,13 +172,17 @@
169
172
  }
170
173
 
171
174
  .errorMessage::before {
172
- content: '';
175
+ content: "";
173
176
  position: absolute;
174
177
  top: 0;
175
178
  left: 0;
176
179
  right: 0;
177
180
  height: 2px;
178
- background: linear-gradient(90deg, var(--error), color-mix(in lab, var(--error) 60%, transparent));
181
+ background: linear-gradient(
182
+ 90deg,
183
+ var(--error),
184
+ color-mix(in lab, var(--error) 60%, transparent)
185
+ );
179
186
  animation: shimmer 2s ease-in-out infinite;
180
187
  }
181
188
 
@@ -246,7 +253,8 @@
246
253
  }
247
254
 
248
255
  @keyframes shimmer {
249
- 0%, 100% {
256
+ 0%,
257
+ 100% {
250
258
  opacity: 0.6;
251
259
  transform: translateX(-100%);
252
260
  }
@@ -261,7 +269,7 @@
261
269
  .errorMessage {
262
270
  animation: none;
263
271
  }
264
-
272
+
265
273
  .errorMessage::before {
266
274
  animation: none;
267
275
  }