@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.
- package/app/components/actions/confirm-export.ts +4 -2
- package/app/components/actions/generate-pdf.ts +10 -2
- package/app/components/audit/user-audit-viewer.tsx +121 -940
- package/app/components/audit/user-audit.module.css +20 -0
- package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +200 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +306 -0
- package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
- package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +121 -0
- package/app/components/audit/viewer/types.ts +1 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +166 -0
- package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
- package/app/components/auth/mfa-enrollment.module.css +13 -5
- package/app/components/auth/mfa-verification.module.css +13 -5
- package/app/components/canvas/canvas.tsx +3 -0
- package/app/components/canvas/confirmation/confirmation.tsx +13 -37
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +8 -37
- package/app/components/sidebar/case-export/case-export.tsx +9 -34
- package/app/components/sidebar/case-import/case-import.module.css +2 -0
- package/app/components/sidebar/case-import/case-import.tsx +10 -34
- package/app/components/sidebar/cases/cases-modal.module.css +44 -9
- package/app/components/sidebar/cases/cases-modal.tsx +16 -14
- package/app/components/sidebar/files/files-modal.module.css +45 -10
- package/app/components/sidebar/files/files-modal.tsx +16 -16
- package/app/components/sidebar/notes/notes-modal.tsx +17 -15
- package/app/components/sidebar/notes/notes.module.css +2 -0
- package/app/components/sidebar/sidebar.module.css +2 -2
- package/app/components/toast/toast.module.css +2 -1
- package/app/components/toast/toast.tsx +16 -11
- package/app/components/user/delete-account.tsx +10 -31
- package/app/components/user/inactivity-warning.module.css +8 -6
- package/app/components/user/manage-profile.module.css +2 -0
- package/app/components/user/manage-profile.tsx +85 -30
- package/app/hooks/useOverlayDismiss.ts +68 -0
- package/app/routes/auth/login.example.tsx +19 -8
- package/app/routes/auth/passwordReset.module.css +23 -13
- package/app/routes/striae/striae.tsx +8 -1
- package/app/routes.ts +7 -0
- package/app/services/audit/audit-export-csv.ts +2 -0
- package/app/services/audit/audit.service.ts +29 -5
- package/app/services/audit/builders/audit-entry-builder.ts +2 -1
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/audit/builders/audit-event-builders-workflow.ts +6 -0
- package/app/types/audit.ts +2 -1
- package/app/types/user.ts +1 -0
- package/app/utils/data/permissions.ts +1 -0
- package/functions/api/pdf/[[path]].ts +32 -1
- package/load-context.ts +9 -0
- package/package.json +5 -1
- package/primershear.emails.example +6 -0
- package/scripts/deploy-pages-secrets.sh +6 -0
- package/scripts/deploy-primershear-emails.sh +166 -0
- package/worker-configuration.d.ts +7493 -7491
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
- package/workers/pdf-worker/src/report-types.ts +3 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/user-worker.example.ts +6 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import { useState, useContext, useEffect } from 'react';
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
4
5
|
import { deleteFile } from '~/components/actions/image-manage';
|
|
5
6
|
import { getFileAnnotations } from '~/utils/data';
|
|
6
7
|
import { type FileData } from '~/types';
|
|
@@ -33,6 +34,13 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
33
34
|
const [currentPage, setCurrentPage] = useState(0);
|
|
34
35
|
const [deletingFileId, setDeletingFileId] = useState<string | null>(null);
|
|
35
36
|
const [fileConfirmationStatus, setFileConfirmationStatus] = useState<FileConfirmationStatus>({});
|
|
37
|
+
const {
|
|
38
|
+
handleOverlayMouseDown,
|
|
39
|
+
handleOverlayKeyDown
|
|
40
|
+
} = useOverlayDismiss({
|
|
41
|
+
isOpen,
|
|
42
|
+
onClose
|
|
43
|
+
});
|
|
36
44
|
|
|
37
45
|
const totalPages = Math.ceil(files.length / FILES_PER_PAGE);
|
|
38
46
|
const startIndex = currentPage * FILES_PER_PAGE;
|
|
@@ -88,21 +96,6 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
88
96
|
fetchConfirmationStatuses();
|
|
89
97
|
}, [isOpen, currentCase, currentPage, files, user]);
|
|
90
98
|
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
const handleEscape = (event: KeyboardEvent) => {
|
|
93
|
-
if (event.key === 'Escape' && isOpen) {
|
|
94
|
-
onClose();
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (isOpen) {
|
|
99
|
-
document.addEventListener('keydown', handleEscape);
|
|
100
|
-
return () => {
|
|
101
|
-
document.removeEventListener('keydown', handleEscape);
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}, [isOpen, onClose]);
|
|
105
|
-
|
|
106
99
|
const handleFileSelect = (file: FileData) => {
|
|
107
100
|
onFileSelect?.(file);
|
|
108
101
|
onClose();
|
|
@@ -166,7 +159,14 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
166
159
|
if (!isOpen) return null;
|
|
167
160
|
|
|
168
161
|
return (
|
|
169
|
-
<div
|
|
162
|
+
<div
|
|
163
|
+
className={styles.modalOverlay}
|
|
164
|
+
onMouseDown={handleOverlayMouseDown}
|
|
165
|
+
onKeyDown={handleOverlayKeyDown}
|
|
166
|
+
role="button"
|
|
167
|
+
tabIndex={0}
|
|
168
|
+
aria-label="Close files dialog"
|
|
169
|
+
>
|
|
170
170
|
<div className={styles.modal}>
|
|
171
171
|
<div className={styles.modalHeader}>
|
|
172
172
|
<h2>Files in Case {currentCase}</h2>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useState
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
2
3
|
import styles from './notes.module.css';
|
|
3
4
|
|
|
4
5
|
interface NotesModalProps {
|
|
@@ -10,19 +11,13 @@ interface NotesModalProps {
|
|
|
10
11
|
|
|
11
12
|
export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps) => {
|
|
12
13
|
const [tempNotes, setTempNotes] = useState(notes);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (isOpen) {
|
|
22
|
-
document.addEventListener('keydown', handleEscape);
|
|
23
|
-
return () => document.removeEventListener('keydown', handleEscape);
|
|
24
|
-
}
|
|
25
|
-
}, [isOpen, onClose]);
|
|
14
|
+
const {
|
|
15
|
+
handleOverlayMouseDown,
|
|
16
|
+
handleOverlayKeyDown
|
|
17
|
+
} = useOverlayDismiss({
|
|
18
|
+
isOpen,
|
|
19
|
+
onClose
|
|
20
|
+
});
|
|
26
21
|
|
|
27
22
|
if (!isOpen) return null;
|
|
28
23
|
|
|
@@ -32,7 +27,14 @@ export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps)
|
|
|
32
27
|
};
|
|
33
28
|
|
|
34
29
|
return (
|
|
35
|
-
<div
|
|
30
|
+
<div
|
|
31
|
+
className={styles.modalOverlay}
|
|
32
|
+
onMouseDown={handleOverlayMouseDown}
|
|
33
|
+
onKeyDown={handleOverlayKeyDown}
|
|
34
|
+
role="button"
|
|
35
|
+
tabIndex={0}
|
|
36
|
+
aria-label="Close notes dialog"
|
|
37
|
+
>
|
|
36
38
|
<div className={styles.modal}>
|
|
37
39
|
<h5 className={styles.modalTitle}>Additional Notes</h5>
|
|
38
40
|
<textarea
|
|
@@ -262,6 +262,7 @@ textarea:focus {
|
|
|
262
262
|
justify-content: center;
|
|
263
263
|
align-items: center;
|
|
264
264
|
z-index: 1000;
|
|
265
|
+
cursor: default;
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
.modal {
|
|
@@ -271,6 +272,7 @@ textarea:focus {
|
|
|
271
272
|
width: 90%;
|
|
272
273
|
max-width: 500px;
|
|
273
274
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
275
|
+
cursor: default;
|
|
274
276
|
}
|
|
275
277
|
|
|
276
278
|
.modalTitle {
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
justify-content: center;
|
|
121
121
|
align-items: center;
|
|
122
122
|
z-index: 1000;
|
|
123
|
+
cursor: default;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
.footerModal {
|
|
@@ -131,6 +132,7 @@
|
|
|
131
132
|
max-height: 80vh;
|
|
132
133
|
overflow-y: auto;
|
|
133
134
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
135
|
+
cursor: default;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
.footerModalHeader {
|
|
@@ -237,8 +239,6 @@
|
|
|
237
239
|
text-underline-offset: 3px;
|
|
238
240
|
}
|
|
239
241
|
|
|
240
|
-
|
|
241
|
-
|
|
242
242
|
/* Import Section */
|
|
243
243
|
.importSection {
|
|
244
244
|
margin-top: auto;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
bottom: 0;
|
|
7
7
|
background: color-mix(in lab, var(--black) 40%, transparent);
|
|
8
8
|
z-index: 999;
|
|
9
|
-
cursor:
|
|
9
|
+
cursor: default;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.toast {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
max-width: 400px;
|
|
29
29
|
min-width: 300px;
|
|
30
30
|
backdrop-filter: blur(10px);
|
|
31
|
+
cursor: default;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
.toast.show {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, type ReactNode } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
2
3
|
import styles from './toast.module.css';
|
|
3
4
|
|
|
4
5
|
interface ToastProps {
|
|
@@ -10,30 +11,34 @@ interface ToastProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export const Toast = ({ message, type, isVisible, onClose, duration = 4000 }: ToastProps) => {
|
|
14
|
+
const {
|
|
15
|
+
requestClose,
|
|
16
|
+
handleOverlayMouseDown,
|
|
17
|
+
handleOverlayKeyDown
|
|
18
|
+
} = useOverlayDismiss({
|
|
19
|
+
isOpen: isVisible,
|
|
20
|
+
onClose,
|
|
21
|
+
closeOnEscape: false
|
|
22
|
+
});
|
|
23
|
+
|
|
13
24
|
useEffect(() => {
|
|
14
25
|
if (isVisible && duration > 0) {
|
|
15
26
|
const timer = setTimeout(() => {
|
|
16
|
-
|
|
27
|
+
requestClose();
|
|
17
28
|
}, duration);
|
|
18
29
|
|
|
19
30
|
return () => clearTimeout(timer);
|
|
20
31
|
}
|
|
21
|
-
}, [isVisible,
|
|
32
|
+
}, [isVisible, requestClose, duration]);
|
|
22
33
|
|
|
23
34
|
if (!isVisible) return null;
|
|
24
35
|
|
|
25
|
-
const handleBackdropKeyDown = (e: React.KeyboardEvent) => {
|
|
26
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
27
|
-
onClose();
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
36
|
return (
|
|
32
37
|
<>
|
|
33
38
|
<div
|
|
34
39
|
className={styles.backdrop}
|
|
35
|
-
|
|
36
|
-
onKeyDown={
|
|
40
|
+
onMouseDown={handleOverlayMouseDown}
|
|
41
|
+
onKeyDown={handleOverlayKeyDown}
|
|
37
42
|
role="button"
|
|
38
43
|
tabIndex={0}
|
|
39
44
|
aria-label="Close notification"
|
|
@@ -45,7 +50,7 @@ export const Toast = ({ message, type, isVisible, onClose, duration = 4000 }: To
|
|
|
45
50
|
<span className={styles.message}>{message}</span>
|
|
46
51
|
<button
|
|
47
52
|
className={styles.closeButton}
|
|
48
|
-
onClick={
|
|
53
|
+
onClick={requestClose}
|
|
49
54
|
aria-label="Close notification"
|
|
50
55
|
>
|
|
51
56
|
×
|
|
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|
|
2
2
|
import { signOut } from 'firebase/auth';
|
|
3
3
|
import { auth } from '~/services/firebase';
|
|
4
4
|
import { fetchUserApi } from '~/utils/api';
|
|
5
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
5
6
|
import { auditService } from '~/services/audit';
|
|
6
7
|
import styles from './delete-account.module.css';
|
|
7
8
|
|
|
@@ -37,6 +38,13 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
|
|
|
37
38
|
const [error, setError] = useState('');
|
|
38
39
|
const [success, setSuccess] = useState(false);
|
|
39
40
|
const [deletionProgress, setDeletionProgress] = useState<DeletionProgress>(initialDeletionProgress);
|
|
41
|
+
const {
|
|
42
|
+
handleOverlayMouseDown,
|
|
43
|
+
handleOverlayKeyDown
|
|
44
|
+
} = useOverlayDismiss({
|
|
45
|
+
isOpen,
|
|
46
|
+
onClose
|
|
47
|
+
});
|
|
40
48
|
|
|
41
49
|
// Extract first and last name from display name
|
|
42
50
|
const [firstName, lastName] = (user.displayName || '').split(' ');
|
|
@@ -170,26 +178,14 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
|
|
|
170
178
|
};
|
|
171
179
|
|
|
172
180
|
useEffect(() => {
|
|
173
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
174
|
-
if (e.key === 'Escape' && isOpen) {
|
|
175
|
-
onClose();
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
|
|
179
181
|
if (isOpen) {
|
|
180
|
-
document.addEventListener('keydown', handleEscape);
|
|
181
|
-
// Reset form when modal opens
|
|
182
182
|
setUidConfirmation('');
|
|
183
183
|
setEmailConfirmation('');
|
|
184
184
|
setError('');
|
|
185
185
|
setSuccess(false);
|
|
186
186
|
setDeletionProgress(initialDeletionProgress);
|
|
187
187
|
}
|
|
188
|
-
|
|
189
|
-
return () => {
|
|
190
|
-
document.removeEventListener('keydown', handleEscape);
|
|
191
|
-
};
|
|
192
|
-
}, [isOpen, onClose]);
|
|
188
|
+
}, [isOpen]);
|
|
193
189
|
|
|
194
190
|
const handleDeleteAccount = async () => {
|
|
195
191
|
if (!isConfirmationValid) return;
|
|
@@ -317,27 +313,10 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
|
|
|
317
313
|
? `Deleting case ${deletionProgress.currentCaseNumber}...`
|
|
318
314
|
: 'Preparing account deletion...');
|
|
319
315
|
|
|
320
|
-
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
321
|
-
if (event.target === event.currentTarget) {
|
|
322
|
-
onClose();
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const handleOverlayKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
327
|
-
if (event.target !== event.currentTarget) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
332
|
-
event.preventDefault();
|
|
333
|
-
onClose();
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
316
|
return (
|
|
338
317
|
<div
|
|
339
318
|
className={styles.modalOverlay}
|
|
340
|
-
|
|
319
|
+
onMouseDown={handleOverlayMouseDown}
|
|
341
320
|
onKeyDown={handleOverlayKeyDown}
|
|
342
321
|
role="button"
|
|
343
322
|
tabIndex={0}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
align-items: center;
|
|
11
11
|
z-index: 9999;
|
|
12
12
|
backdrop-filter: blur(2px);
|
|
13
|
+
cursor: default;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
.modal {
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
|
22
23
|
border: 1px solid #e0e0e0;
|
|
23
24
|
animation: slideIn 0.3s ease-out;
|
|
25
|
+
cursor: default;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
@keyframes slideIn {
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
font-size: 2rem;
|
|
63
65
|
font-weight: bold;
|
|
64
66
|
color: #dc3545;
|
|
65
|
-
font-family:
|
|
67
|
+
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
|
66
68
|
background: rgba(220, 53, 69, 0.1);
|
|
67
69
|
padding: 1rem;
|
|
68
70
|
border-radius: 8px;
|
|
@@ -118,25 +120,25 @@
|
|
|
118
120
|
.overlay {
|
|
119
121
|
background-color: rgba(0, 0, 0, 0.9);
|
|
120
122
|
}
|
|
121
|
-
|
|
123
|
+
|
|
122
124
|
.modal {
|
|
123
125
|
background: #2d2d2d;
|
|
124
126
|
border-color: #404040;
|
|
125
127
|
}
|
|
126
|
-
|
|
128
|
+
|
|
127
129
|
.header h3 {
|
|
128
130
|
color: #ffffff;
|
|
129
131
|
}
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
.content p {
|
|
132
134
|
color: #cccccc;
|
|
133
135
|
}
|
|
134
|
-
|
|
136
|
+
|
|
135
137
|
.signOutButton {
|
|
136
138
|
color: #adb5bd;
|
|
137
139
|
border-color: #6c757d;
|
|
138
140
|
}
|
|
139
|
-
|
|
141
|
+
|
|
140
142
|
.signOutButton:hover {
|
|
141
143
|
background: #404040;
|
|
142
144
|
color: #ffffff;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
justify-content: center;
|
|
8
8
|
align-items: center;
|
|
9
9
|
z-index: var(--zIndex5);
|
|
10
|
+
cursor: default;
|
|
10
11
|
transition: background-color var(--durationM) var(--bezierFastoutSlowin);
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
flex-direction: column;
|
|
23
24
|
transition: background-color var(--durationM) var(--bezierFastoutSlowin);
|
|
24
25
|
overflow: hidden;
|
|
26
|
+
cursor: default;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/* Modal Header */
|
|
@@ -4,6 +4,7 @@ import { PasswordReset } from '~/routes/auth/passwordReset';
|
|
|
4
4
|
import { DeleteAccount } from './delete-account';
|
|
5
5
|
import { UserAuditViewer } from '../audit/user-audit-viewer';
|
|
6
6
|
import { AuthContext } from '~/contexts/auth.context';
|
|
7
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
7
8
|
import { getUserData, updateUserData } from '~/utils/data';
|
|
8
9
|
import { auditService } from '~/services/audit';
|
|
9
10
|
import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase/errors';
|
|
@@ -19,6 +20,8 @@ interface ManageProfileProps {
|
|
|
19
20
|
export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
20
21
|
const { user } = useContext(AuthContext);
|
|
21
22
|
const [displayName, setDisplayName] = useState(user?.displayName || '');
|
|
23
|
+
const [badgeId, setBadgeId] = useState('');
|
|
24
|
+
const [initialBadgeId, setInitialBadgeId] = useState('');
|
|
22
25
|
const [company, setCompany] = useState('');
|
|
23
26
|
const [email, setEmail] = useState('');
|
|
24
27
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -29,19 +32,20 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
29
32
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
30
33
|
const [showAuditViewer, setShowAuditViewer] = useState(false);
|
|
31
34
|
const isCloseBlocked = isMfaBusy || isLoading;
|
|
35
|
+
const {
|
|
36
|
+
requestClose,
|
|
37
|
+
handleOverlayMouseDown,
|
|
38
|
+
handleOverlayKeyDown
|
|
39
|
+
} = useOverlayDismiss({
|
|
40
|
+
isOpen,
|
|
41
|
+
onClose,
|
|
42
|
+
canDismiss: !isCloseBlocked
|
|
43
|
+
});
|
|
32
44
|
|
|
33
45
|
const handleMfaBusyChange = useCallback((isBusy: boolean) => {
|
|
34
46
|
setIsMfaBusy(isBusy);
|
|
35
47
|
}, []);
|
|
36
48
|
|
|
37
|
-
const handleCloseRequest = () => {
|
|
38
|
-
if (isCloseBlocked) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
onClose();
|
|
43
|
-
};
|
|
44
|
-
|
|
45
49
|
useEffect(() => {
|
|
46
50
|
if (isOpen && user) {
|
|
47
51
|
const loadUserData = async () => {
|
|
@@ -51,6 +55,18 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
51
55
|
if (userData) {
|
|
52
56
|
setCompany(userData.company || '');
|
|
53
57
|
setEmail(userData.email || '');
|
|
58
|
+
const storedBadgeId = userData.badgeId || '';
|
|
59
|
+
setBadgeId(storedBadgeId);
|
|
60
|
+
setInitialBadgeId(storedBadgeId);
|
|
61
|
+
|
|
62
|
+
if (userData.badgeId === undefined) {
|
|
63
|
+
try {
|
|
64
|
+
await updateUserData(user, { badgeId: '' });
|
|
65
|
+
setInitialBadgeId('');
|
|
66
|
+
} catch (badgeInitError) {
|
|
67
|
+
console.error('Failed to initialize badge ID field:', badgeInitError);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
}
|
|
55
71
|
} catch (err) {
|
|
56
72
|
console.error('Failed to load user data:', err);
|
|
@@ -61,22 +77,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
61
77
|
}
|
|
62
78
|
}, [isOpen, user]);
|
|
63
79
|
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
66
|
-
if (e.key === 'Escape' && isOpen && !isCloseBlocked) {
|
|
67
|
-
onClose();
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (isOpen) {
|
|
72
|
-
document.addEventListener('keydown', handleEscape);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return () => {
|
|
76
|
-
document.removeEventListener('keydown', handleEscape);
|
|
77
|
-
};
|
|
78
|
-
}, [isOpen, isCloseBlocked, onClose]);
|
|
79
|
-
|
|
80
80
|
const handleUpdateProfile = async (e: React.FormEvent) => {
|
|
81
81
|
e.preventDefault();
|
|
82
82
|
setIsLoading(true);
|
|
@@ -84,6 +84,8 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
84
84
|
setSuccess('');
|
|
85
85
|
|
|
86
86
|
const oldDisplayName = user?.displayName || '';
|
|
87
|
+
const oldBadgeId = initialBadgeId;
|
|
88
|
+
const normalizedBadgeId = badgeId.trim();
|
|
87
89
|
|
|
88
90
|
try {
|
|
89
91
|
if (!user) throw new Error(ERROR_MESSAGES.NO_USER);
|
|
@@ -98,6 +100,7 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
98
100
|
email: user.email,
|
|
99
101
|
firstName: firstName || '',
|
|
100
102
|
lastName: lastName || '',
|
|
103
|
+
badgeId: normalizedBadgeId,
|
|
101
104
|
});
|
|
102
105
|
|
|
103
106
|
await auditService.logUserProfileUpdate(
|
|
@@ -105,9 +108,27 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
105
108
|
'displayName',
|
|
106
109
|
oldDisplayName,
|
|
107
110
|
displayName,
|
|
108
|
-
'success'
|
|
111
|
+
'success',
|
|
112
|
+
undefined,
|
|
113
|
+
[],
|
|
114
|
+
normalizedBadgeId
|
|
109
115
|
);
|
|
110
116
|
|
|
117
|
+
if (oldBadgeId !== normalizedBadgeId) {
|
|
118
|
+
await auditService.logUserProfileUpdate(
|
|
119
|
+
user,
|
|
120
|
+
'badgeId',
|
|
121
|
+
oldBadgeId,
|
|
122
|
+
normalizedBadgeId,
|
|
123
|
+
'success',
|
|
124
|
+
undefined,
|
|
125
|
+
[],
|
|
126
|
+
normalizedBadgeId
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setInitialBadgeId(normalizedBadgeId);
|
|
131
|
+
|
|
111
132
|
setSuccess(ERROR_MESSAGES.PROFILE_UPDATED);
|
|
112
133
|
} catch (err) {
|
|
113
134
|
const { message } = handleAuthError(err);
|
|
@@ -119,9 +140,23 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
119
140
|
displayName,
|
|
120
141
|
'failure',
|
|
121
142
|
undefined,
|
|
122
|
-
[message]
|
|
143
|
+
[message],
|
|
144
|
+
normalizedBadgeId
|
|
123
145
|
);
|
|
124
146
|
|
|
147
|
+
if (oldBadgeId !== normalizedBadgeId) {
|
|
148
|
+
await auditService.logUserProfileUpdate(
|
|
149
|
+
user!,
|
|
150
|
+
'badgeId',
|
|
151
|
+
oldBadgeId,
|
|
152
|
+
normalizedBadgeId,
|
|
153
|
+
'failure',
|
|
154
|
+
undefined,
|
|
155
|
+
[message],
|
|
156
|
+
normalizedBadgeId
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
125
160
|
setError(message);
|
|
126
161
|
} finally {
|
|
127
162
|
setIsLoading(false);
|
|
@@ -158,19 +193,24 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
158
193
|
}
|
|
159
194
|
|
|
160
195
|
return (
|
|
161
|
-
<div
|
|
162
|
-
{
|
|
196
|
+
<div
|
|
197
|
+
className={styles.modalOverlay}
|
|
198
|
+
onMouseDown={handleOverlayMouseDown}
|
|
199
|
+
onKeyDown={handleOverlayKeyDown}
|
|
200
|
+
role="button"
|
|
201
|
+
tabIndex={0}
|
|
202
|
+
aria-label="Close manage profile dialog"
|
|
203
|
+
>
|
|
163
204
|
<div
|
|
164
205
|
className={styles.modal}
|
|
165
206
|
role="dialog"
|
|
166
207
|
aria-modal="true"
|
|
167
208
|
aria-labelledby="modal-title"
|
|
168
|
-
onClick={(e) => e.stopPropagation()}
|
|
169
209
|
>
|
|
170
210
|
<header className={styles.modalHeader}>
|
|
171
211
|
<h1 id="modal-title">Manage Profile</h1>
|
|
172
212
|
<button
|
|
173
|
-
onClick={
|
|
213
|
+
onClick={requestClose}
|
|
174
214
|
className={styles.closeButton}
|
|
175
215
|
aria-label="Close modal"
|
|
176
216
|
disabled={isCloseBlocked}
|
|
@@ -192,6 +232,21 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
192
232
|
required
|
|
193
233
|
/>
|
|
194
234
|
|
|
235
|
+
<div className={styles.formGroup}>
|
|
236
|
+
<label htmlFor="badgeId">Badge/ID #</label>
|
|
237
|
+
<input
|
|
238
|
+
id="badgeId"
|
|
239
|
+
type="text"
|
|
240
|
+
value={badgeId}
|
|
241
|
+
onChange={(e) => setBadgeId(e.target.value)}
|
|
242
|
+
className={styles.input}
|
|
243
|
+
autoComplete="off"
|
|
244
|
+
/>
|
|
245
|
+
<p className={styles.helpText}>
|
|
246
|
+
Enter your Badge/ID number for confirmations and reports. This can be updated as needed.
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
195
250
|
<div className={styles.formGroup}>
|
|
196
251
|
<label htmlFor="company">Lab/Company Name</label>
|
|
197
252
|
<input
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useCallback, useEffect, type KeyboardEventHandler, type MouseEventHandler } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseOverlayDismissOptions {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
canDismiss?: boolean;
|
|
7
|
+
closeOnEscape?: boolean;
|
|
8
|
+
closeOnBackdrop?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useOverlayDismiss = ({
|
|
12
|
+
isOpen,
|
|
13
|
+
onClose,
|
|
14
|
+
canDismiss = true,
|
|
15
|
+
closeOnEscape = true,
|
|
16
|
+
closeOnBackdrop = true
|
|
17
|
+
}: UseOverlayDismissOptions) => {
|
|
18
|
+
const requestClose = useCallback(() => {
|
|
19
|
+
if (!canDismiss) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onClose();
|
|
24
|
+
}, [canDismiss, onClose]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!isOpen || !closeOnEscape || !canDismiss) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
32
|
+
if (event.key === 'Escape') {
|
|
33
|
+
onClose();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
document.addEventListener('keydown', handleEscape);
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
document.removeEventListener('keydown', handleEscape);
|
|
41
|
+
};
|
|
42
|
+
}, [isOpen, closeOnEscape, canDismiss, onClose]);
|
|
43
|
+
|
|
44
|
+
const handleOverlayMouseDown = useCallback<MouseEventHandler<HTMLDivElement>>((event) => {
|
|
45
|
+
if (!closeOnBackdrop || event.target !== event.currentTarget) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
requestClose();
|
|
50
|
+
}, [closeOnBackdrop, requestClose]);
|
|
51
|
+
|
|
52
|
+
const handleOverlayKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>((event) => {
|
|
53
|
+
if (!closeOnBackdrop || event.target !== event.currentTarget) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
requestClose();
|
|
60
|
+
}
|
|
61
|
+
}, [closeOnBackdrop, requestClose]);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
requestClose,
|
|
65
|
+
handleOverlayMouseDown,
|
|
66
|
+
handleOverlayKeyDown
|
|
67
|
+
};
|
|
68
|
+
};
|