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