@isoftdata/svelte-user-configuration 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,439 +1,439 @@
1
- <script lang="ts">
2
- import type { i18n } from 'i18next'
3
- import type { HTMLDivAttributes } from './'
4
- import type { ComponentProps, Snippet } from 'svelte'
5
- import type { UserAccount, ConfirmPasswordSetFn, DeactivateUserFn, IconName, PasswordValidationRules } from './'
6
-
7
- import { getContext } from 'svelte'
8
- import Icon from '@isoftdata/svelte-icon'
9
- import Input from '@isoftdata/svelte-input'
10
- import Button from '@isoftdata/svelte-button'
11
- import TextArea from '@isoftdata/svelte-textarea'
12
- import PasswordSetModal from './PasswordSetModal.svelte'
13
- import DeactivateUserModal from './DeactivateUserModal.svelte'
14
- import PasswordRecoveryModal from './PasswordRecoveryModal.svelte'
15
- import { translate as defaultTranslate } from '@isoftdata/utility-string'
16
-
17
- const { t: translate } = getContext<i18n>('i18next') || { t: defaultTranslate }
18
-
19
- interface Props extends HTMLDivAttributes {
20
- userAccount: UserAccount
21
- canEditAccountInfo?: boolean
22
- canToggleActive?: boolean
23
- hasPermissionToChangePassword?: boolean
24
- generateNewActivationPIN?: (userName: string, hasWorkEmail: boolean) => Promise<void>
25
- confirmPasswordSet?: ConfirmPasswordSetFn
26
- deactivateUser?: DeactivateUserFn
27
- success?: ((info: { heading: string; message: string }) => void | Promise<void>) | undefined
28
- error?: ((info: { heading: string; message: string }) => void | Promise<void>) | undefined
29
- accountInfoChanged?: (() => void | Promise<void>) | undefined
30
- sendPasswordRecoveryToken?: ComponentProps<typeof PasswordRecoveryModal>['sendPasswordRecoveryToken']
31
- doSendPasswordRecoveryToken?: boolean
32
- icon?: IconName
33
- usernameInput?: HTMLInputElement | undefined
34
- cardHeight?: number
35
- myAccountMode?: boolean
36
- passwordValidationRules?: PasswordValidationRules | undefined
37
- cardTitle?: string
38
- recoveryEmailIsValid?: boolean
39
- workEmailIsValid?: boolean
40
- formFields?: Snippet
41
- children?: Snippet
42
- }
43
-
44
- let {
45
- userAccount = $bindable(),
46
- canEditAccountInfo = true,
47
- canToggleActive = true,
48
- hasPermissionToChangePassword = false,
49
- generateNewActivationPIN = () => Promise.resolve(),
50
- confirmPasswordSet = undefined,
51
- deactivateUser = undefined,
52
- success = undefined,
53
- error = undefined,
54
- accountInfoChanged = undefined,
55
- sendPasswordRecoveryToken = undefined,
56
- doSendPasswordRecoveryToken = $bindable(false),
57
- icon = 'user',
58
- usernameInput = $bindable(undefined),
59
- cardHeight = $bindable(),
60
- myAccountMode = false,
61
- passwordValidationRules = undefined,
62
- cardTitle = translate('configuration.user.accountInfoHeader', 'Account'),
63
- recoveryEmailIsValid = $bindable(true),
64
- workEmailIsValid = $bindable(true),
65
- formFields,
66
- children,
67
- ...rest
68
- }: Props = $props()
69
-
70
- let isLoading = $state(false)
71
- let passwordSetModal: PasswordSetModal | undefined = $state()
72
- let passwordRecoveryModal: PasswordRecoveryModal | undefined = $state()
73
- let deactivateUserModal: DeactivateUserModal | undefined = $state()
74
- let activationPINInput: HTMLInputElement | undefined = $state()
75
-
76
- async function getNewActivationPIN(sendEmail: boolean = false) {
77
- let confirmationMessage: string = sendEmail
78
- ? translate(
79
- 'configuration.user.permissions.sendNewActivationPINMessage',
80
- 'Are you sure you want to send a new activation PIN? The user will receive an email with the new activation PIN.',
81
- )
82
- : translate(
83
- 'configuration.user.permissions.generateNewActivationPINMessage',
84
- 'Are you sure you want to generate a new activation PIN?',
85
- )
86
-
87
- if (confirm(confirmationMessage)) {
88
- try {
89
- isLoading = true
90
- await generateNewActivationPIN(userAccount.name, !!userAccount.workEmail)
91
-
92
- let successMessage: string = sendEmail
93
- ? translate(
94
- 'configuration.user.permissions.activationPINSent',
95
- 'Email with new activation PIN has been sent successfully.',
96
- )
97
- : translate('configuration.user.permissions.activationPINGenerated', 'Activation PIN generated successfully.')
98
- let successHeading: string = sendEmail
99
- ? translate('configuration.messageHeading.activationPINSent', 'New Activation PIN Sent!')
100
- : translate('configuration.messageHeading.activationPINGenerated', 'New Activation PIN Generated!')
101
-
102
- await success?.({ heading: successHeading, message: successMessage })
103
-
104
- isLoading = false
105
- await accountInfoChanged?.()
106
- } catch (err) {
107
- console.error(err)
108
- await error?.({
109
- heading: translate('configuration.messageHeading.failedToGenerateNewPIN', 'Failed To Generate New PIN'),
110
- message:
111
- err instanceof Error ? err.message : translate('workOrder.unknownError', 'An unknown error occurred'),
112
- })
113
- }
114
- }
115
- }
116
-
117
- async function copyTextToClipboard() {
118
- if (activationPINInput) {
119
- navigator.clipboard.writeText(activationPINInput.value)
120
- await success?.({
121
- heading: translate('configuration.messageHeading.activationPINCopied', 'Activation PIN Copied!'),
122
- message: translate(
123
- 'configuration.user.permissions.activationPINCopied',
124
- 'Activation PIN copied to clipboard successfully.',
125
- ),
126
- })
127
- }
128
- }
129
-
130
- async function reactivateUserAccount() {
131
- if (
132
- confirm(
133
- translate(
134
- 'configuration.user.permissions.reactivateUserConfirmation',
135
- 'Are you sure you want to reactivate this user?',
136
- ),
137
- )
138
- ) {
139
- userAccount.status = 'ACTIVE'
140
- userAccount.lockNotes = null
141
- await accountInfoChanged?.()
142
- }
143
- }
144
-
145
- const emailAddressRegex =
146
- /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
147
- const emailIsValid = (emailAddress: string): boolean => emailAddressRegex.test(emailAddress)
148
-
149
- let workEmail = $derived(userAccount.workEmail)
150
- let status = $derived(userAccount.status)
151
- let activationPIN = $derived(userAccount.userActivationData?.activationPIN)
152
- let isCreatingNewUser = $derived(userAccount.id === null)
153
- </script>
154
-
155
- <div
156
- class="card"
157
- bind:offsetHeight={cardHeight}
158
- {...rest}
159
- >
160
- <fieldset disabled={!canEditAccountInfo}>
161
- <div class="card-header">
162
- <div class="d-flex justify-content-between align-items-center">
163
- <h5 class="mb-0">
164
- <Icon
165
- {icon}
166
- class="mr-1 me-1"
167
- />
168
- {isCreatingNewUser ? translate('configuration.user.creatingNewAccountInfoHeader', 'New Account') : cardTitle}
169
- </h5>
170
- {#if !myAccountMode && !isCreatingNewUser && (status === 'ACTIVE' || status === 'PENDING_ACTIVATION')}
171
- <Button
172
- size="xs"
173
- outline
174
- color="danger"
175
- iconClass="xmark"
176
- onclick={() => deactivateUserModal?.open(userAccount)}
177
- disabled={!canEditAccountInfo || !canToggleActive}
178
- >
179
- <span>{translate('common:deactivate', 'Deactivate')}</span>
180
- </Button>
181
- {:else if !myAccountMode && ((!isCreatingNewUser && status === 'DEACTIVATED') || status === 'LOCKED')}
182
- <Button
183
- size="xs"
184
- outline
185
- color="success"
186
- iconClass="check"
187
- onclick={() => reactivateUserAccount()}
188
- disabled={!canEditAccountInfo || !canToggleActive}
189
- >
190
- <span>{translate('common:activate', 'Activate')}</span>
191
- </Button>
192
- {/if}
193
- </div>
194
- </div>
195
- <div class="card-body">
196
- <div class="row">
197
- {#if !isCreatingNewUser && status === 'PENDING_ACTIVATION'}
198
- <div class="col-12">
199
- {#if activationPIN && workEmail}
200
- <div class="alert alert-info mb-0">
201
- {translate(
202
- 'configuration.user.accountInfo.activationPINSent',
203
- 'An activation PIN has been sent to {{email}}.',
204
- { email: workEmail },
205
- )}
206
- </div>
207
- {:else}
208
- <label for="activationPIN">{translate('configuration.user.activationPIN', 'Activation PIN')}</label>
209
- <div class="input-group input-group-sm">
210
- <input
211
- bind:this={activationPINInput}
212
- type="text"
213
- class="form-control"
214
- placeholder="###-###"
215
- value={activationPIN}
216
- readonly
217
- />
218
- <div class="input-group-append">
219
- <!-- When this button is hit, the function will call an API endpoint that will write the new PIN into the db -->
220
- <!-- Therefore, this button will ignore the save function, as we need the new PIN to display it here -->
221
- <Button
222
- size="sm"
223
- outline
224
- {isLoading}
225
- iconClass="refresh"
226
- onclick={() => getNewActivationPIN()}
227
- title={translate('configuration.user.generateNewActivationPIN', 'Generate New PIN')}
228
- />
229
- <Button
230
- size="sm"
231
- outline
232
- iconClass="copy"
233
- onclick={() => copyTextToClipboard()}
234
- disabled={!activationPIN}
235
- title={translate('configuration.user.copyActivationPIN', 'Copy Activation PIN')}
236
- />
237
- </div>
238
- </div>
239
- {/if}
240
- {#if activationPIN && userAccount.userActivationData?.activationPINExpiration}
241
- <small class="text-danger"
242
- >{translate('configuration.user.activationPINExpireText', 'Activation PIN expires on {{- date}}', {
243
- date: userAccount.userActivationData.activationPINExpiration.toLocaleString(),
244
- })}</small
245
- >
246
- {/if}
247
- </div>
248
- {/if}
249
- <div class="col-12 col-lg-6">
250
- <Input
251
- label={translate('configuration.user.accountInfo.username', 'Username')}
252
- bind:value={userAccount.name}
253
- maxlength={320}
254
- required={isCreatingNewUser}
255
- validation={{
256
- validator: value => {
257
- if (!value) return 'Username is required.'
258
- else return value.length > 320 ? 'Username must be less than 320 characters.' : true
259
- },
260
- }}
261
- bind:input={usernameInput}
262
- readonly={myAccountMode || undefined}
263
- tabindex={myAccountMode ? -1 : undefined}
264
- />
265
- </div>
266
- <div class="col-12 col-lg-6"></div>
267
- <div class="col-12 col-md-6">
268
- <Input
269
- label={translate('configuration.user.accountInfo.firstName', 'First Name')}
270
- bind:value={userAccount.firstName}
271
- maxlength={100}
272
- />
273
- </div>
274
- <div class="col-12 col-md-6">
275
- <Input
276
- label={translate('configuration.user.accountInfo.lastName', 'Last Name')}
277
- bind:value={userAccount.lastName}
278
- maxlength={100}
279
- />
280
- </div>
281
- <div class="col-12 col-lg-6">
282
- <Input
283
- label={translate('configuration.user.accountInfo.workEmail', 'Work Email')}
284
- bind:value={userAccount.workEmail}
285
- onchange={() => {
286
- workEmailIsValid = !myAccountMode && userAccount.workEmail ? emailIsValid(userAccount.workEmail) : true
287
- }}
288
- autocomplete="email"
289
- type="email"
290
- inputmode="email"
291
- readonly={myAccountMode || undefined}
292
- tabindex={myAccountMode ? -1 : undefined}
293
- hint={!workEmailIsValid ? translate('common:invalidEmailAddress', 'Invalid Email') : undefined}
294
- hintClass="text-danger"
295
- />
296
- {#if workEmail && !isCreatingNewUser && status === 'PENDING_ACTIVATION'}
297
- <Button
298
- size="sm"
299
- color="link"
300
- class="p-0 mb-2"
301
- onclick={() => getNewActivationPIN(true)}
302
- >
303
- {translate('configuration.user.sendNewActivationPIN', 'Send New Activation PIN')}...
304
- </Button>
305
- {/if}
306
- </div>
307
- <div class="col-12 col-lg-6">
308
- <Input
309
- label={translate(
310
- 'configuration.user.accountInfo.passwordRecoveryModal.passwordRecoveryEmail',
311
- 'Password Recovery Email',
312
- )}
313
- bind:value={userAccount.recoveryEmail}
314
- onchange={() => {
315
- recoveryEmailIsValid =
316
- myAccountMode && userAccount.recoveryEmail ? emailIsValid(userAccount.recoveryEmail) : true
317
- }}
318
- autocomplete="email"
319
- type="email"
320
- inputmode="email"
321
- readonly={!myAccountMode || undefined}
322
- tabindex={!myAccountMode ? -1 : undefined}
323
- hint={!recoveryEmailIsValid ? translate('common:invalidEmailAddress', 'Invalid Email') : undefined}
324
- hintClass="text-danger"
325
- />
326
- </div>
327
- {#if !isCreatingNewUser && !myAccountMode}
328
- <div class="col-12">
329
- <TextArea
330
- label={translate('configuration.user.accountInfo.lockNote', 'Lock Note')}
331
- labelClass="py-0 mb-2"
332
- style="min-height:83px;"
333
- bind:value={userAccount.lockNotes}
334
- readonly
335
- />
336
- </div>
337
- {/if}
338
- {@render formFields?.()}
339
- </div>
340
- {@render children?.()}
341
- </div>
342
- <div class="card-footer">
343
- {#if !myAccountMode}
344
- <Button
345
- size="sm"
346
- outline
347
- iconClass="paper-plane"
348
- disabled={!canEditAccountInfo}
349
- onclick={() => {
350
- passwordRecoveryModal?.open(userAccount.workEmail, userAccount.lastPasswordResetDate)
351
- }}
352
- >
353
- {translate('configuration.user.sendResetToken', 'Send Reset Token')}...
354
- </Button>
355
- {/if}
356
- {#if (!myAccountMode && hasPermissionToChangePassword) || (myAccountMode && !userAccount.newPassword)}
357
- <Button
358
- size="sm"
359
- outline
360
- iconClass="key"
361
- disabled={!myAccountMode && !canEditAccountInfo}
362
- onclick={() => passwordSetModal?.open(userAccount)}
363
- >
364
- {#if myAccountMode}
365
- {translate('configuration.user.changePassword', 'Change Password')}...
366
- {:else}
367
- {translate('configuration.user.setPassword', 'Set Password')}...
368
- {/if}
369
- </Button>
370
- {:else if myAccountMode && userAccount.newPassword}
371
- <Button
372
- outline
373
- color="danger"
374
- onclick={() => {
375
- userAccount.currentPassword = ''
376
- userAccount.newPassword = ''
377
- }}
378
- >
379
- {translate('configuration.user.cancelPasswordChange', 'Cancel Password Change')}</Button
380
- >
381
- {/if}
382
- </div>
383
- </fieldset>
384
- </div>
385
-
386
- <PasswordRecoveryModal
387
- bind:this={passwordRecoveryModal}
388
- bind:doSendPasswordRecoveryToken
389
- {sendPasswordRecoveryToken}
390
- />
391
-
392
- <DeactivateUserModal
393
- bind:this={deactivateUserModal}
394
- deactivateUser={async ({ lockNotes }) => {
395
- userAccount.status = 'DEACTIVATED'
396
- userAccount.lockNotes = lockNotes
397
- try {
398
- await deactivateUser?.({ id: userAccount.id, lockNotes })
399
- await accountInfoChanged?.()
400
- } catch (err) {
401
- console.error(err)
402
- await error?.({
403
- heading: translate('configuration.user.messageHeading.failedToDeactivateUser', 'Failed To Deactivate User'),
404
- message: err instanceof Error ? err.message : translate('workOrder.unknownError', 'An unknown error occurred'),
405
- })
406
- }
407
- }}
408
- />
409
-
410
- <PasswordSetModal
411
- bind:this={passwordSetModal}
412
- changePasswordMode={myAccountMode}
413
- confirmPasswordSet={async ({ currentPassword, newPassword }) => {
414
- if (hasPermissionToChangePassword || myAccountMode) {
415
- if (confirmPasswordSet) {
416
- try {
417
- await confirmPasswordSet({ currentPassword, newPassword })
418
- await success?.({
419
- heading: translate('configuration.user.passwordChangeSuccessHeading', 'Password Changed!'),
420
- message: translate('configuration.user.passwordChangeSuccessMessage', 'Password changed successfully'),
421
- })
422
- } catch (err) {
423
- console.error(err)
424
- await error?.({
425
- heading: translate('configuration.user.passwordChangeErrorHeading', 'Failed To Change Password'),
426
- message:
427
- err instanceof Error ? err.message : translate('workOrder.unknownError', 'An unknown error occurred'),
428
- })
429
- throw err
430
- }
431
- } else {
432
- //Guess if they didn't give us a confirmPasswordSet function to call we'll just update the userAccount object and they can handle it later
433
- userAccount.currentPassword = currentPassword
434
- userAccount.newPassword = newPassword
435
- }
436
- }
437
- }}
438
- validationRules={passwordValidationRules}
439
- />
1
+ <script lang="ts">
2
+ import type { i18n } from 'i18next'
3
+ import type { HTMLDivAttributes } from './'
4
+ import type { ComponentProps, Snippet } from 'svelte'
5
+ import type { UserAccount, ConfirmPasswordSetFn, DeactivateUserFn, IconName, PasswordValidationRules } from './'
6
+
7
+ import { getContext } from 'svelte'
8
+ import Icon from '@isoftdata/svelte-icon'
9
+ import Input from '@isoftdata/svelte-input'
10
+ import Button from '@isoftdata/svelte-button'
11
+ import TextArea from '@isoftdata/svelte-textarea'
12
+ import PasswordSetModal from './PasswordSetModal.svelte'
13
+ import DeactivateUserModal from './DeactivateUserModal.svelte'
14
+ import PasswordRecoveryModal from './PasswordRecoveryModal.svelte'
15
+ import { translate as defaultTranslate } from '@isoftdata/utility-string'
16
+
17
+ const { t: translate } = getContext<i18n>('i18next') || { t: defaultTranslate }
18
+
19
+ interface Props extends HTMLDivAttributes {
20
+ userAccount: UserAccount
21
+ canEditAccountInfo?: boolean
22
+ canToggleActive?: boolean
23
+ hasPermissionToChangePassword?: boolean
24
+ generateNewActivationPIN?: (userName: string, hasWorkEmail: boolean) => Promise<void>
25
+ confirmPasswordSet?: ConfirmPasswordSetFn
26
+ deactivateUser?: DeactivateUserFn
27
+ success?: ((info: { heading: string; message: string }) => void | Promise<void>) | undefined
28
+ error?: ((info: { heading: string; message: string }) => void | Promise<void>) | undefined
29
+ accountInfoChanged?: (() => void | Promise<void>) | undefined
30
+ sendPasswordRecoveryToken?: ComponentProps<typeof PasswordRecoveryModal>['sendPasswordRecoveryToken']
31
+ doSendPasswordRecoveryToken?: boolean
32
+ icon?: IconName
33
+ usernameInput?: HTMLInputElement | undefined
34
+ cardHeight?: number
35
+ myAccountMode?: boolean
36
+ passwordValidationRules?: PasswordValidationRules | undefined
37
+ cardTitle?: string
38
+ recoveryEmailIsValid?: boolean
39
+ workEmailIsValid?: boolean
40
+ formFields?: Snippet
41
+ children?: Snippet
42
+ }
43
+
44
+ let {
45
+ userAccount = $bindable(),
46
+ canEditAccountInfo = true,
47
+ canToggleActive = true,
48
+ hasPermissionToChangePassword = false,
49
+ generateNewActivationPIN = () => Promise.resolve(),
50
+ confirmPasswordSet = undefined,
51
+ deactivateUser = undefined,
52
+ success = undefined,
53
+ error = undefined,
54
+ accountInfoChanged = undefined,
55
+ sendPasswordRecoveryToken = undefined,
56
+ doSendPasswordRecoveryToken = $bindable(false),
57
+ icon = 'user',
58
+ usernameInput = $bindable(undefined),
59
+ cardHeight = $bindable(),
60
+ myAccountMode = false,
61
+ passwordValidationRules = undefined,
62
+ cardTitle = translate('configuration.user.accountInfoHeader', 'Account'),
63
+ recoveryEmailIsValid = $bindable(true),
64
+ workEmailIsValid = $bindable(true),
65
+ formFields,
66
+ children,
67
+ ...rest
68
+ }: Props = $props()
69
+
70
+ let isLoading = $state(false)
71
+ let passwordSetModal: PasswordSetModal | undefined = $state()
72
+ let passwordRecoveryModal: PasswordRecoveryModal | undefined = $state()
73
+ let deactivateUserModal: DeactivateUserModal | undefined = $state()
74
+ let activationPINInput: HTMLInputElement | undefined = $state()
75
+
76
+ async function getNewActivationPIN(sendEmail: boolean = false) {
77
+ let confirmationMessage: string = sendEmail
78
+ ? translate(
79
+ 'configuration.user.permissions.sendNewActivationPINMessage',
80
+ 'Are you sure you want to send a new activation PIN? The user will receive an email with the new activation PIN.',
81
+ )
82
+ : translate(
83
+ 'configuration.user.permissions.generateNewActivationPINMessage',
84
+ 'Are you sure you want to generate a new activation PIN?',
85
+ )
86
+
87
+ if (confirm(confirmationMessage)) {
88
+ try {
89
+ isLoading = true
90
+ await generateNewActivationPIN(userAccount.name, !!userAccount.workEmail)
91
+
92
+ let successMessage: string = sendEmail
93
+ ? translate(
94
+ 'configuration.user.permissions.activationPINSent',
95
+ 'Email with new activation PIN has been sent successfully.',
96
+ )
97
+ : translate('configuration.user.permissions.activationPINGenerated', 'Activation PIN generated successfully.')
98
+ let successHeading: string = sendEmail
99
+ ? translate('configuration.messageHeading.activationPINSent', 'New Activation PIN Sent!')
100
+ : translate('configuration.messageHeading.activationPINGenerated', 'New Activation PIN Generated!')
101
+
102
+ await success?.({ heading: successHeading, message: successMessage })
103
+
104
+ isLoading = false
105
+ await accountInfoChanged?.()
106
+ } catch (err) {
107
+ console.error(err)
108
+ await error?.({
109
+ heading: translate('configuration.messageHeading.failedToGenerateNewPIN', 'Failed To Generate New PIN'),
110
+ message:
111
+ err instanceof Error ? err.message : translate('workOrder.unknownError', 'An unknown error occurred'),
112
+ })
113
+ }
114
+ }
115
+ }
116
+
117
+ async function copyTextToClipboard() {
118
+ if (activationPINInput) {
119
+ navigator.clipboard.writeText(activationPINInput.value)
120
+ await success?.({
121
+ heading: translate('configuration.messageHeading.activationPINCopied', 'Activation PIN Copied!'),
122
+ message: translate(
123
+ 'configuration.user.permissions.activationPINCopied',
124
+ 'Activation PIN copied to clipboard successfully.',
125
+ ),
126
+ })
127
+ }
128
+ }
129
+
130
+ async function reactivateUserAccount() {
131
+ if (
132
+ confirm(
133
+ translate(
134
+ 'configuration.user.permissions.reactivateUserConfirmation',
135
+ 'Are you sure you want to reactivate this user?',
136
+ ),
137
+ )
138
+ ) {
139
+ userAccount.status = 'ACTIVE'
140
+ userAccount.lockNotes = null
141
+ await accountInfoChanged?.()
142
+ }
143
+ }
144
+
145
+ const emailAddressRegex =
146
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
147
+ const emailIsValid = (emailAddress: string): boolean => emailAddressRegex.test(emailAddress)
148
+
149
+ let workEmail = $derived(userAccount.workEmail)
150
+ let status = $derived(userAccount.status)
151
+ let activationPIN = $derived(userAccount.userActivationData?.activationPIN)
152
+ let isCreatingNewUser = $derived(userAccount.id === null)
153
+ </script>
154
+
155
+ <div
156
+ class="card"
157
+ bind:offsetHeight={cardHeight}
158
+ {...rest}
159
+ >
160
+ <fieldset disabled={!canEditAccountInfo}>
161
+ <div class="card-header">
162
+ <div class="d-flex justify-content-between align-items-center">
163
+ <h5 class="mb-0">
164
+ <Icon
165
+ {icon}
166
+ class="mr-1 me-1"
167
+ />
168
+ {isCreatingNewUser ? translate('configuration.user.creatingNewAccountInfoHeader', 'New Account') : cardTitle}
169
+ </h5>
170
+ {#if !myAccountMode && !isCreatingNewUser && (status === 'ACTIVE' || status === 'PENDING_ACTIVATION')}
171
+ <Button
172
+ size="xs"
173
+ outline
174
+ color="danger"
175
+ iconClass="xmark"
176
+ onclick={() => deactivateUserModal?.open(userAccount)}
177
+ disabled={!canEditAccountInfo || !canToggleActive}
178
+ >
179
+ <span>{translate('common:deactivate', 'Deactivate')}</span>
180
+ </Button>
181
+ {:else if !myAccountMode && ((!isCreatingNewUser && status === 'DEACTIVATED') || status === 'LOCKED')}
182
+ <Button
183
+ size="xs"
184
+ outline
185
+ color="success"
186
+ iconClass="check"
187
+ onclick={() => reactivateUserAccount()}
188
+ disabled={!canEditAccountInfo || !canToggleActive}
189
+ >
190
+ <span>{translate('common:activate', 'Activate')}</span>
191
+ </Button>
192
+ {/if}
193
+ </div>
194
+ </div>
195
+ <div class="card-body">
196
+ <div class="row">
197
+ {#if !isCreatingNewUser && status === 'PENDING_ACTIVATION'}
198
+ <div class="col-12">
199
+ {#if activationPIN && workEmail}
200
+ <div class="alert alert-info mb-0">
201
+ {translate(
202
+ 'configuration.user.accountInfo.activationPINSent',
203
+ 'An activation PIN has been sent to {{email}}.',
204
+ { email: workEmail },
205
+ )}
206
+ </div>
207
+ {:else}
208
+ <label for="activationPIN">{translate('configuration.user.activationPIN', 'Activation PIN')}</label>
209
+ <div class="input-group input-group-sm">
210
+ <input
211
+ bind:this={activationPINInput}
212
+ type="text"
213
+ class="form-control"
214
+ placeholder="###-###"
215
+ value={activationPIN}
216
+ readonly
217
+ />
218
+ <div class="input-group-append">
219
+ <!-- When this button is hit, the function will call an API endpoint that will write the new PIN into the db -->
220
+ <!-- Therefore, this button will ignore the save function, as we need the new PIN to display it here -->
221
+ <Button
222
+ size="sm"
223
+ outline
224
+ {isLoading}
225
+ iconClass="refresh"
226
+ onclick={() => getNewActivationPIN()}
227
+ title={translate('configuration.user.generateNewActivationPIN', 'Generate New PIN')}
228
+ />
229
+ <Button
230
+ size="sm"
231
+ outline
232
+ iconClass="copy"
233
+ onclick={() => copyTextToClipboard()}
234
+ disabled={!activationPIN}
235
+ title={translate('configuration.user.copyActivationPIN', 'Copy Activation PIN')}
236
+ />
237
+ </div>
238
+ </div>
239
+ {/if}
240
+ {#if activationPIN && userAccount.userActivationData?.activationPINExpiration}
241
+ <small class="text-danger"
242
+ >{translate('configuration.user.activationPINExpireText', 'Activation PIN expires on {{- date}}', {
243
+ date: userAccount.userActivationData.activationPINExpiration.toLocaleString(),
244
+ })}</small
245
+ >
246
+ {/if}
247
+ </div>
248
+ {/if}
249
+ <div class="col-12 col-lg-6">
250
+ <Input
251
+ label={translate('configuration.user.accountInfo.username', 'Username')}
252
+ bind:value={userAccount.name}
253
+ maxlength={320}
254
+ required={isCreatingNewUser}
255
+ validation={{
256
+ validator: value => {
257
+ if (!value) return 'Username is required.'
258
+ else return value.length > 320 ? 'Username must be less than 320 characters.' : true
259
+ },
260
+ }}
261
+ bind:input={usernameInput}
262
+ readonly={myAccountMode || undefined}
263
+ tabindex={myAccountMode ? -1 : undefined}
264
+ />
265
+ </div>
266
+ <div class="col-12 col-lg-6"></div>
267
+ <div class="col-12 col-md-6">
268
+ <Input
269
+ label={translate('configuration.user.accountInfo.firstName', 'First Name')}
270
+ bind:value={userAccount.firstName}
271
+ maxlength={100}
272
+ />
273
+ </div>
274
+ <div class="col-12 col-md-6">
275
+ <Input
276
+ label={translate('configuration.user.accountInfo.lastName', 'Last Name')}
277
+ bind:value={userAccount.lastName}
278
+ maxlength={100}
279
+ />
280
+ </div>
281
+ <div class="col-12 col-lg-6">
282
+ <Input
283
+ label={translate('configuration.user.accountInfo.workEmail', 'Work Email')}
284
+ bind:value={userAccount.workEmail}
285
+ onchange={() => {
286
+ workEmailIsValid = !myAccountMode && userAccount.workEmail ? emailIsValid(userAccount.workEmail) : true
287
+ }}
288
+ autocomplete="email"
289
+ type="email"
290
+ inputmode="email"
291
+ readonly={myAccountMode || undefined}
292
+ tabindex={myAccountMode ? -1 : undefined}
293
+ hint={!workEmailIsValid ? translate('common:invalidEmailAddress', 'Invalid Email') : undefined}
294
+ hintClass="text-danger"
295
+ />
296
+ {#if workEmail && !isCreatingNewUser && status === 'PENDING_ACTIVATION'}
297
+ <Button
298
+ size="sm"
299
+ color="link"
300
+ class="p-0 mb-2"
301
+ onclick={() => getNewActivationPIN(true)}
302
+ >
303
+ {translate('configuration.user.sendNewActivationPIN', 'Send New Activation PIN')}...
304
+ </Button>
305
+ {/if}
306
+ </div>
307
+ <div class="col-12 col-lg-6">
308
+ <Input
309
+ label={translate(
310
+ 'configuration.user.accountInfo.passwordRecoveryModal.passwordRecoveryEmail',
311
+ 'Password Recovery Email',
312
+ )}
313
+ bind:value={userAccount.recoveryEmail}
314
+ onchange={() => {
315
+ recoveryEmailIsValid =
316
+ myAccountMode && userAccount.recoveryEmail ? emailIsValid(userAccount.recoveryEmail) : true
317
+ }}
318
+ autocomplete="email"
319
+ type="email"
320
+ inputmode="email"
321
+ readonly={!myAccountMode || undefined}
322
+ tabindex={!myAccountMode ? -1 : undefined}
323
+ hint={!recoveryEmailIsValid ? translate('common:invalidEmailAddress', 'Invalid Email') : undefined}
324
+ hintClass="text-danger"
325
+ />
326
+ </div>
327
+ {#if !isCreatingNewUser && !myAccountMode}
328
+ <div class="col-12">
329
+ <TextArea
330
+ label={translate('configuration.user.accountInfo.lockNote', 'Lock Note')}
331
+ labelClass="py-0 mb-2"
332
+ style="min-height:83px;"
333
+ bind:value={userAccount.lockNotes}
334
+ readonly
335
+ />
336
+ </div>
337
+ {/if}
338
+ {@render formFields?.()}
339
+ </div>
340
+ {@render children?.()}
341
+ </div>
342
+ <div class="card-footer">
343
+ {#if !myAccountMode}
344
+ <Button
345
+ size="sm"
346
+ outline
347
+ iconClass="paper-plane"
348
+ disabled={!canEditAccountInfo}
349
+ onclick={() => {
350
+ passwordRecoveryModal?.open(userAccount.recoveryEmail, userAccount.lastPasswordResetDate)
351
+ }}
352
+ >
353
+ {translate('configuration.user.sendResetToken', 'Send Reset Token')}...
354
+ </Button>
355
+ {/if}
356
+ {#if (!myAccountMode && hasPermissionToChangePassword) || (myAccountMode && !userAccount.newPassword)}
357
+ <Button
358
+ size="sm"
359
+ outline
360
+ iconClass="key"
361
+ disabled={!myAccountMode && !canEditAccountInfo}
362
+ onclick={() => passwordSetModal?.open(userAccount)}
363
+ >
364
+ {#if myAccountMode}
365
+ {translate('configuration.user.changePassword', 'Change Password')}...
366
+ {:else}
367
+ {translate('configuration.user.setPassword', 'Set Password')}...
368
+ {/if}
369
+ </Button>
370
+ {:else if myAccountMode && userAccount.newPassword}
371
+ <Button
372
+ outline
373
+ color="danger"
374
+ onclick={() => {
375
+ userAccount.currentPassword = ''
376
+ userAccount.newPassword = ''
377
+ }}
378
+ >
379
+ {translate('configuration.user.cancelPasswordChange', 'Cancel Password Change')}</Button
380
+ >
381
+ {/if}
382
+ </div>
383
+ </fieldset>
384
+ </div>
385
+
386
+ <PasswordRecoveryModal
387
+ bind:this={passwordRecoveryModal}
388
+ bind:doSendPasswordRecoveryToken
389
+ {sendPasswordRecoveryToken}
390
+ />
391
+
392
+ <DeactivateUserModal
393
+ bind:this={deactivateUserModal}
394
+ deactivateUser={async ({ lockNotes }) => {
395
+ userAccount.status = 'DEACTIVATED'
396
+ userAccount.lockNotes = lockNotes
397
+ try {
398
+ await deactivateUser?.({ id: userAccount.id, lockNotes })
399
+ await accountInfoChanged?.()
400
+ } catch (err) {
401
+ console.error(err)
402
+ await error?.({
403
+ heading: translate('configuration.user.messageHeading.failedToDeactivateUser', 'Failed To Deactivate User'),
404
+ message: err instanceof Error ? err.message : translate('workOrder.unknownError', 'An unknown error occurred'),
405
+ })
406
+ }
407
+ }}
408
+ />
409
+
410
+ <PasswordSetModal
411
+ bind:this={passwordSetModal}
412
+ changePasswordMode={myAccountMode}
413
+ confirmPasswordSet={async ({ currentPassword, newPassword }) => {
414
+ if (hasPermissionToChangePassword || myAccountMode) {
415
+ if (confirmPasswordSet) {
416
+ try {
417
+ await confirmPasswordSet({ currentPassword, newPassword })
418
+ await success?.({
419
+ heading: translate('configuration.user.passwordChangeSuccessHeading', 'Password Changed!'),
420
+ message: translate('configuration.user.passwordChangeSuccessMessage', 'Password changed successfully'),
421
+ })
422
+ } catch (err) {
423
+ console.error(err)
424
+ await error?.({
425
+ heading: translate('configuration.user.passwordChangeErrorHeading', 'Failed To Change Password'),
426
+ message:
427
+ err instanceof Error ? err.message : translate('workOrder.unknownError', 'An unknown error occurred'),
428
+ })
429
+ throw err
430
+ }
431
+ } else {
432
+ //Guess if they didn't give us a confirmPasswordSet function to call we'll just update the userAccount object and they can handle it later
433
+ userAccount.currentPassword = currentPassword
434
+ userAccount.newPassword = newPassword
435
+ }
436
+ }
437
+ }}
438
+ validationRules={passwordValidationRules}
439
+ />