Profile.vue
1023 lines
| 1 | <template> |
| 2 | <div |
| 3 | ref="pageContainer" |
| 4 | class="am-capi" |
| 5 | :style="cssVars" |
| 6 | role="region" |
| 7 | :aria-busy="loading ? 'true' : 'false'" |
| 8 | :aria-label="currentRegionLabel" |
| 9 | > |
| 10 | <div class="am-capi__inner"> |
| 11 | <AmAlert |
| 12 | v-if="alertVisibility" |
| 13 | type="success" |
| 14 | :show-border="true" |
| 15 | :close-after="5000" |
| 16 | custom-class="am-capi__alert" |
| 17 | role="status" |
| 18 | aria-live="polite" |
| 19 | @close="closeAlert" |
| 20 | @trigger-close="closeAlert" |
| 21 | > |
| 22 | <template #title> |
| 23 | <span class="am-icon-checkmark-circle-full"></span> {{ successMessage }} |
| 24 | </template> |
| 25 | </AmAlert> |
| 26 | |
| 27 | <AmTabs |
| 28 | v-model="activeTab" |
| 29 | class="am-capi__tabs" |
| 30 | :aria-label="tabsAriaLabel" |
| 31 | @tab-click="tabClick" |
| 32 | > |
| 33 | <AmTabsItem class="am-capi__tabs-item" :label="amLabels.personal_info" name="first"> |
| 34 | <template v-if="!loading"> |
| 35 | <div class="am-capi__sr-only">{{ amLabels.personal_info }}</div> |
| 36 | <el-form |
| 37 | v-if="store.getters['auth/getProfile']" |
| 38 | ref="infoFormRef" |
| 39 | :model="infoFormData" |
| 40 | :rules="infoFormRules" |
| 41 | label-position="top" |
| 42 | class="am-capi__form" |
| 43 | :class="responsiveClass" |
| 44 | > |
| 45 | <template v-for="item in amCustomize.profile.order" :key="item.id"> |
| 46 | <component |
| 47 | :is="infoFormConstruction[item.id].template" |
| 48 | v-if="customizedOptions[item.id] ? customizedOptions[item.id].visibility : true" |
| 49 | ref="customerCollectorRef" |
| 50 | v-model="infoFormData[item.id]" |
| 51 | v-model:country-code="infoFormConstruction[item.id].countryCode" |
| 52 | v-bind="infoFormConstruction[item.id].props" |
| 53 | v-on=" |
| 54 | 'handlers' in infoFormConstruction[item.id] |
| 55 | ? infoFormConstruction[item.id].handlers |
| 56 | : {} |
| 57 | " |
| 58 | ></component> |
| 59 | </template> |
| 60 | </el-form> |
| 61 | </template> |
| 62 | <ProfileSkeleton v-else :count="5" :page-width="pageWidth"></ProfileSkeleton> |
| 63 | </AmTabsItem> |
| 64 | |
| 65 | <!-- Customer Custom Fields --> |
| 66 | <AmTabsItem |
| 67 | v-if="Object.keys(customFields).length" |
| 68 | :label="amLabels.custom_fields" |
| 69 | name="third" |
| 70 | > |
| 71 | <template v-if="!loading"> |
| 72 | <div class="am-capi__sr-only">{{ amLabels.custom_fields }}</div> |
| 73 | <el-form |
| 74 | ref="customFieldsFormRef" |
| 75 | :model="customFieldsForm" |
| 76 | :rules="customFieldsFormRules" |
| 77 | label-position="top" |
| 78 | class="am-capei-att-cf__form" |
| 79 | :class="responsiveClass" |
| 80 | > |
| 81 | <template v-for="cf in Object.values(customFields)" :key="cf.id"> |
| 82 | <component |
| 83 | :is="cfFormConstruction[cf.id]?.template" |
| 84 | v-if="cf.id in customFieldsForm" |
| 85 | v-model="customFieldsForm[cf.id]" |
| 86 | v-bind="cfFormConstruction[cf.id]?.props" |
| 87 | @address-selected="(address) => addressSelected(address, cf.id)" |
| 88 | /> |
| 89 | </template> |
| 90 | </el-form> |
| 91 | </template> |
| 92 | <ProfileSkeleton |
| 93 | v-else |
| 94 | :count="Object.keys(customFields).length" |
| 95 | :page-width="pageWidth" |
| 96 | ></ProfileSkeleton> |
| 97 | </AmTabsItem> |
| 98 | <!-- /Customer Custom Fields --> |
| 99 | |
| 100 | <AmTabsItem class="am-capi__tabs-item" :label="amLabels.password_tab" name="second"> |
| 101 | <template v-if="!loading"> |
| 102 | <div class="am-capi__sr-only">{{ amLabels.password_tab }}</div> |
| 103 | <el-form |
| 104 | ref="passFormRef" |
| 105 | :model="passFormData" |
| 106 | :rules="passFormRules" |
| 107 | label-position="top" |
| 108 | class="am-capi__form" |
| 109 | :class="responsiveClass" |
| 110 | > |
| 111 | <template v-for="(item, key) in passFormConstruction" :key="item.props.itemName"> |
| 112 | <component |
| 113 | :is="item.template" |
| 114 | ref="customerPassCollectorRef" |
| 115 | v-model="passFormData[key]" |
| 116 | v-bind="item.props" |
| 117 | ></component> |
| 118 | </template> |
| 119 | </el-form> |
| 120 | </template> |
| 121 | |
| 122 | <ProfileSkeleton |
| 123 | v-else |
| 124 | :item-direction="'column'" |
| 125 | :count="2" |
| 126 | :page-width="pageWidth" |
| 127 | ></ProfileSkeleton> |
| 128 | </AmTabsItem> |
| 129 | </AmTabs> |
| 130 | |
| 131 | <DeleteProfile |
| 132 | :visibility="deleteProfileDialog" |
| 133 | :customized-labels="customizedStepLabels('deleteProfile')" |
| 134 | :customized-options="amCustomize.deleteProfile.options" |
| 135 | @close="deleteProfileDialog = false" |
| 136 | @delete-profile="deleteProfile" |
| 137 | /> |
| 138 | |
| 139 | <MainProfileFooter |
| 140 | :loading="loading" |
| 141 | :display="activeTab" |
| 142 | :parent-width="pageWidth" |
| 143 | :customized-labels="amLabels" |
| 144 | :save-footer-button="customizedOptions.saveFooterButton.buttonType" |
| 145 | :delete-footer-type="customizedOptions.deleteFooterButton.buttonType" |
| 146 | :pass-footer-button="customizedOptions.passFooterButton.buttonType" |
| 147 | /> |
| 148 | </div> |
| 149 | </div> |
| 150 | </template> |
| 151 | |
| 152 | <script setup> |
| 153 | import AmTabs from '../../../../_components/tabs/AmTabs.vue' |
| 154 | import AmTabsItem from '../../../../_components/tabs/AmTabsItem.vue' |
| 155 | // * _components |
| 156 | import AmAlert from '../../../../_components/alert/AmAlert.vue' |
| 157 | |
| 158 | // * Dedicated Components |
| 159 | import DeleteProfile from '../parts/DeleteProfile.vue' |
| 160 | import MainProfileFooter from '../../../../common/SbsFormConstruction/MainContent/parts/MainProfileFooter.vue' |
| 161 | import ProfileSkeleton from './parts/ProfileSkeleton.vue' |
| 162 | |
| 163 | // * Import Libraries |
| 164 | import moment from 'moment' |
| 165 | import { useCookies } from 'vue3-cookies' |
| 166 | import httpClient from '../../../../../plugins/axios' |
| 167 | |
| 168 | // * Import from Vue |
| 169 | import { |
| 170 | ref, |
| 171 | reactive, |
| 172 | computed, |
| 173 | inject, |
| 174 | provide, |
| 175 | onMounted, |
| 176 | onBeforeUnmount, |
| 177 | nextTick, |
| 178 | watch, |
| 179 | } from 'vue' |
| 180 | |
| 181 | // * Composables |
| 182 | import { useResponsiveClass } from '../../../../../assets/js/common/responsive' |
| 183 | import { useColorTransparency } from '../../../../../assets/js/common/colorManipulation' |
| 184 | |
| 185 | // * Form Fields Templates |
| 186 | import { formFieldsTemplates } from '../../../../../assets/js/common/formFieldsTemplates' |
| 187 | |
| 188 | // * Import from Vuex |
| 189 | import { useStore } from 'vuex' |
| 190 | import { mapAddressComponentsForXML } from '../../../../../assets/js/common/helper' |
| 191 | import { getInternationalPhoneValue } from '../../../../../assets/js/common/phone' |
| 192 | |
| 193 | let store = useStore() |
| 194 | |
| 195 | // * Loading state |
| 196 | let loading = computed(() => store.getters['getLoading']) |
| 197 | |
| 198 | // * Page Content width |
| 199 | let pageContainer = ref(null) |
| 200 | let pageWidth = ref(0) |
| 201 | |
| 202 | // * Sidebar collapsed var |
| 203 | let sidebarCollapsed = inject('sidebarCollapsed') |
| 204 | |
| 205 | // * window resize listener |
| 206 | function resize() { |
| 207 | if (pageContainer.value) { |
| 208 | pageWidth.value = pageContainer.value.offsetWidth |
| 209 | } |
| 210 | } |
| 211 | watch(sidebarCollapsed, (current) => { |
| 212 | if (current) { |
| 213 | setTimeout(() => { |
| 214 | collapseTriggered() |
| 215 | }, 1500) |
| 216 | } else { |
| 217 | setTimeout(() => { |
| 218 | collapseTriggered() |
| 219 | }, 500) |
| 220 | } |
| 221 | }) |
| 222 | |
| 223 | function collapseTriggered() { |
| 224 | pageWidth.value = pageContainer.value.offsetWidth |
| 225 | } |
| 226 | |
| 227 | onMounted(() => { |
| 228 | window.addEventListener('resize', resize) |
| 229 | nextTick(() => { |
| 230 | pageWidth.value = pageContainer.value.offsetWidth |
| 231 | }) |
| 232 | setInitCustomerCustomFields() |
| 233 | }) |
| 234 | |
| 235 | onBeforeUnmount(() => { |
| 236 | window.removeEventListener('resize', resize) |
| 237 | }) |
| 238 | |
| 239 | let responsiveClass = computed(() => { |
| 240 | return useResponsiveClass(pageWidth.value) |
| 241 | }) |
| 242 | |
| 243 | // * Root Settings |
| 244 | const amSettings = inject('settings') |
| 245 | |
| 246 | // * Cabinet type |
| 247 | let cabinetType = inject('cabinetType') |
| 248 | |
| 249 | // * Customized form data |
| 250 | let amCustomize = inject('amCustomize') |
| 251 | |
| 252 | // * labels |
| 253 | const labels = inject('labels') |
| 254 | |
| 255 | // * local language short code |
| 256 | const localLanguage = inject('localLanguage') |
| 257 | |
| 258 | // * if local lang is in settings lang |
| 259 | let langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value)) |
| 260 | |
| 261 | function customizedLabelsObject(obj, custObj) { |
| 262 | if (custObj) { |
| 263 | Object.keys(custObj).forEach((labelKey) => { |
| 264 | if (custObj[labelKey][localLanguage.value] && langDetection.value) { |
| 265 | obj[labelKey] = custObj[labelKey][localLanguage.value] |
| 266 | } else if (custObj[labelKey].default) { |
| 267 | obj[labelKey] = custObj[labelKey].default |
| 268 | } |
| 269 | }) |
| 270 | } |
| 271 | return obj |
| 272 | } |
| 273 | |
| 274 | // * Computed labels |
| 275 | let amLabels = computed(() => { |
| 276 | let computedLabels = reactive({ ...labels }) |
| 277 | let customizedLabels = amCustomize.value.profile.translations |
| 278 | |
| 279 | return customizedLabelsObject(computedLabels, customizedLabels) |
| 280 | }) |
| 281 | |
| 282 | // * Dedicated step labels |
| 283 | function customizedStepLabels(step) { |
| 284 | let stepLabels = {} |
| 285 | let customizedLabels = amCustomize.value[step].translations |
| 286 | |
| 287 | return customizedLabelsObject(stepLabels, customizedLabels) |
| 288 | } |
| 289 | |
| 290 | // * Customized options |
| 291 | let customizedOptions = computed(() => { |
| 292 | return amCustomize.value.profile.options |
| 293 | }) |
| 294 | |
| 295 | let activeTab = ref('first') |
| 296 | |
| 297 | let currentRegionLabel = computed(() => { |
| 298 | switch (activeTab.value) { |
| 299 | case 'second': |
| 300 | return amLabels.value.password_tab |
| 301 | case 'third': |
| 302 | return amLabels.value.custom_fields |
| 303 | case 'first': |
| 304 | return amLabels.value.personal_info |
| 305 | default: |
| 306 | return amLabels.value.personal_info |
| 307 | } |
| 308 | }) |
| 309 | |
| 310 | let tabsAriaLabel = computed(() => { |
| 311 | let labels = [amLabels.value.personal_info, amLabels.value.password_tab] |
| 312 | if (Object.keys(customFields).length) { |
| 313 | labels.splice(1, 0, amLabels.value.custom_fields) |
| 314 | } |
| 315 | return labels.join(' / ') |
| 316 | }) |
| 317 | |
| 318 | // * Success message |
| 319 | let successMessage = ref('') |
| 320 | |
| 321 | function closeAlert() { |
| 322 | alertVisibility.value = false |
| 323 | successMessage.value = '' |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * * Customer information form |
| 328 | */ |
| 329 | |
| 330 | // * Form reference |
| 331 | let infoFormRef = ref(null) |
| 332 | |
| 333 | let phoneError = ref(false) |
| 334 | let isPhoneValid = ref(false) |
| 335 | |
| 336 | // * Form data |
| 337 | let infoFormData = ref({ |
| 338 | firstName: computed({ |
| 339 | get: () => store.getters['auth/getProfile'].firstName, |
| 340 | set: (val) => { |
| 341 | store.commit('auth/setProfileFirstName', val ? val : '') |
| 342 | }, |
| 343 | }), |
| 344 | lastName: computed({ |
| 345 | get: () => store.getters['auth/getProfile'].lastName, |
| 346 | set: (val) => { |
| 347 | store.commit('auth/setProfileLastName', val ? val : '') |
| 348 | }, |
| 349 | }), |
| 350 | email: computed({ |
| 351 | get: () => store.getters['auth/getProfile'].email, |
| 352 | set: (val) => { |
| 353 | store.commit('auth/setProfileEmail', val ? val : '') |
| 354 | }, |
| 355 | }), |
| 356 | phone: computed({ |
| 357 | get: () => store.getters['auth/getProfile'].phone, |
| 358 | set: (val) => { |
| 359 | store.commit('auth/setProfilePhone', val ? val : '') |
| 360 | }, |
| 361 | }), |
| 362 | birthday: computed({ |
| 363 | get: () => |
| 364 | store.getters['auth/getProfile'].birthday ? store.getters['auth/getProfile'].birthday : '', |
| 365 | set: (val) => { |
| 366 | store.commit('auth/setProfileBirthday', val ? moment(val).format('YYYY-MM-DD') : '') |
| 367 | }, |
| 368 | }), |
| 369 | }) |
| 370 | |
| 371 | // * Form construction |
| 372 | let infoFormConstruction = ref({ |
| 373 | firstName: { |
| 374 | template: formFieldsTemplates.text, |
| 375 | props: { |
| 376 | itemName: 'firstName', |
| 377 | label: amLabels.value.first_name_colon, |
| 378 | placeholder: amLabels.value.enter_first_name, |
| 379 | class: computed(() => `am-capi__item ${responsiveClass.value}`), |
| 380 | }, |
| 381 | }, |
| 382 | lastName: { |
| 383 | template: formFieldsTemplates.text, |
| 384 | props: { |
| 385 | itemName: 'lastName', |
| 386 | label: amLabels.value.last_name_colon, |
| 387 | placeholder: amLabels.value.enter_last_name, |
| 388 | class: computed(() => `am-capi__item ${responsiveClass.value}`), |
| 389 | }, |
| 390 | }, |
| 391 | email: { |
| 392 | template: formFieldsTemplates.text, |
| 393 | props: { |
| 394 | itemName: 'email', |
| 395 | label: amLabels.value.email_colon, |
| 396 | placeholder: amLabels.value.enter_email, |
| 397 | class: computed(() => `am-capi__item ${responsiveClass.value}`), |
| 398 | }, |
| 399 | }, |
| 400 | phone: { |
| 401 | countryCode: computed({ |
| 402 | get: () => |
| 403 | store.getters['auth/getProfile'].countryPhoneIso |
| 404 | ? store.getters['auth/getProfile'].countryPhoneIso.toUpperCase() |
| 405 | : '', |
| 406 | set: (val) => { |
| 407 | store.commit('auth/setProfileCountryPhoneIso', val ? val.toLowerCase() : '') |
| 408 | }, |
| 409 | }), |
| 410 | template: formFieldsTemplates.phone, |
| 411 | props: { |
| 412 | itemName: 'phone', |
| 413 | label: amLabels.value.phone_colon, |
| 414 | placeholder: amLabels.value.enter_phone, |
| 415 | phoneError: computed(() => phoneError.value && !isPhoneValid.value), |
| 416 | errorMessage: computed(() => |
| 417 | phoneError.value && !isPhoneValid.value && infoFormData.value.phone |
| 418 | ? amLabels.value.enter_valid_phone_warning |
| 419 | : '', |
| 420 | ), |
| 421 | whatsAppLabel: amLabels.value.whatsapp_opt_in_text, |
| 422 | isWhatsApp: computed( |
| 423 | () => |
| 424 | amSettings.notifications.whatsAppEnabled && !(phoneError.value && !isPhoneValid.value), |
| 425 | ), |
| 426 | class: computed(() => `am-capi__item ${responsiveClass.value}`), |
| 427 | }, |
| 428 | handlers: { |
| 429 | handlePhoneData: (phoneData) => { |
| 430 | if ( |
| 431 | phoneData && |
| 432 | !phoneData.countryCode && |
| 433 | !store.getters['auth/getProfile'].countryPhoneIso && |
| 434 | amSettings.general.phoneDefaultCountryCode !== 'auto' |
| 435 | ) { |
| 436 | store.commit( |
| 437 | 'auth/setProfileCountryPhoneIso', |
| 438 | amSettings.general.phoneDefaultCountryCode.toLowerCase(), |
| 439 | ) |
| 440 | } |
| 441 | |
| 442 | store.commit( |
| 443 | 'auth/setProfilePhone', |
| 444 | getInternationalPhoneValue( |
| 445 | phoneData?.phoneNumber ?? phoneData?.e164, |
| 446 | phoneData, |
| 447 | phoneData?.countryCode, |
| 448 | ), |
| 449 | ) |
| 450 | |
| 451 | isPhoneValid.value = !!phoneData.isValid |
| 452 | }, |
| 453 | }, |
| 454 | }, |
| 455 | birthday: { |
| 456 | template: formFieldsTemplates.datepicker, |
| 457 | props: { |
| 458 | itemName: 'birthday', |
| 459 | label: amLabels.value.date_of_birth, |
| 460 | placeholder: amLabels.value.enter_date_of_birth, |
| 461 | clearable: true, |
| 462 | readonly: false, |
| 463 | class: computed(() => `am-capi__item am-capi__item-birthday ${responsiveClass.value}`), |
| 464 | }, |
| 465 | }, |
| 466 | }) |
| 467 | |
| 468 | // * Form validation rules |
| 469 | let infoFormRules = ref({ |
| 470 | firstName: [ |
| 471 | { |
| 472 | required: true, |
| 473 | message: amLabels.value.enter_first_name_warning, |
| 474 | trigger: 'submit', |
| 475 | }, |
| 476 | ], |
| 477 | lastName: [ |
| 478 | { |
| 479 | required: customizedOptions.value.lastName.required, |
| 480 | message: amLabels.value.enter_last_name_warning, |
| 481 | trigger: 'submit', |
| 482 | }, |
| 483 | ], |
| 484 | email: [ |
| 485 | { |
| 486 | required: customizedOptions.value.email.required, |
| 487 | type: 'email', |
| 488 | message: amLabels.value.enter_valid_email_warning, |
| 489 | trigger: 'submit', |
| 490 | }, |
| 491 | ], |
| 492 | phone: [ |
| 493 | { |
| 494 | required: customizedOptions.value.phone.required, |
| 495 | message: amLabels.value.enter_phone_warning, |
| 496 | trigger: ['blur', 'submit'], |
| 497 | }, |
| 498 | ], |
| 499 | birthday: [ |
| 500 | { |
| 501 | required: customizedOptions.value.birthday.required, |
| 502 | message: amLabels.value.enter_date_of_birth_warning, |
| 503 | trigger: 'submit', |
| 504 | }, |
| 505 | ], |
| 506 | }) |
| 507 | |
| 508 | let deleteProfileDialog = ref(false) |
| 509 | provide('deleteProfileDialog', deleteProfileDialog) |
| 510 | |
| 511 | function saveProfileChanges() { |
| 512 | // Trim inputs |
| 513 | store.commit( |
| 514 | 'auth/setProfileFirstName', |
| 515 | infoFormData.value.firstName ? infoFormData.value.firstName.trim() : '', |
| 516 | ) |
| 517 | store.commit( |
| 518 | 'auth/setProfileLastName', |
| 519 | infoFormData.value.lastName ? infoFormData.value.lastName.trim() : '', |
| 520 | ) |
| 521 | store.commit( |
| 522 | 'auth/setProfileEmail', |
| 523 | infoFormData.value.email ? infoFormData.value.email.trim() : '', |
| 524 | ) |
| 525 | |
| 526 | infoFormRef.value.validate((valid) => { |
| 527 | if (valid && (customizedOptions.value.phone.required ? isPhoneValid.value : true)) { |
| 528 | phoneError.value = false |
| 529 | successMessage.value = amLabels.value.profile_data_success |
| 530 | let user = store.getters['auth/getProfile'] |
| 531 | |
| 532 | store.commit('setLoading', true) |
| 533 | |
| 534 | httpClient.post('/users/' + cabinetType.value + 's/' + user.id, user).finally(() => { |
| 535 | store.commit('setLoading', false) |
| 536 | alertVisibility.value = true |
| 537 | }) |
| 538 | } else { |
| 539 | let phoneField = infoFormRef.value.fields.find((el) => el.prop === 'phone') |
| 540 | if (phoneField && !isPhoneValid.value) { |
| 541 | phoneField.validateState = 'error' |
| 542 | } |
| 543 | phoneError.value = !!(phoneField && phoneField.validateState === 'error') |
| 544 | return false |
| 545 | } |
| 546 | }) |
| 547 | } |
| 548 | provide('saveProfileChanges', saveProfileChanges) |
| 549 | |
| 550 | /** |
| 551 | * * Customer change password form |
| 552 | */ |
| 553 | |
| 554 | // * Form reference |
| 555 | let passFormRef = ref(null) |
| 556 | |
| 557 | // * Form data |
| 558 | let passFormData = ref({ |
| 559 | newPass: computed({ |
| 560 | get: () => store.getters['auth/getNewPassword'], |
| 561 | set: (val) => { |
| 562 | store.commit('auth/setNewPassword', val ? val : '') |
| 563 | }, |
| 564 | }), |
| 565 | confirmPass: computed({ |
| 566 | get: () => store.getters['auth/getConfirmPassword'], |
| 567 | set: (val) => { |
| 568 | store.commit('auth/setConfirmPassword', val ? val : '') |
| 569 | }, |
| 570 | }), |
| 571 | }) |
| 572 | |
| 573 | // * Form construction |
| 574 | let passFormConstruction = ref({ |
| 575 | newPass: { |
| 576 | template: formFieldsTemplates.text, |
| 577 | props: { |
| 578 | itemName: 'newPass', |
| 579 | itemType: 'password', |
| 580 | showPassword: true, |
| 581 | label: amLabels.value.new_password_colon, |
| 582 | placeholder: '', |
| 583 | minLength: 3, |
| 584 | class: computed(() => `am-capp__item ${responsiveClass.value}`), |
| 585 | }, |
| 586 | }, |
| 587 | confirmPass: { |
| 588 | template: formFieldsTemplates.text, |
| 589 | props: { |
| 590 | itemName: 'confirmPass', |
| 591 | itemType: 'password', |
| 592 | showPassword: true, |
| 593 | label: amLabels.value.new_password_colon_retype, |
| 594 | placeholder: '', |
| 595 | minLength: 3, |
| 596 | class: computed(() => `am-capp__item ${responsiveClass.value}`), |
| 597 | }, |
| 598 | }, |
| 599 | }) |
| 600 | |
| 601 | // * Form validation rules |
| 602 | let passFormRules = ref({ |
| 603 | newPass: [ |
| 604 | { |
| 605 | required: true, |
| 606 | message: amLabels.value.new_password_required, |
| 607 | trigger: 'submit', |
| 608 | }, |
| 609 | { |
| 610 | min: 4, |
| 611 | message: amLabels.value.new_password_length, |
| 612 | trigger: 'submit', |
| 613 | }, |
| 614 | ], |
| 615 | confirmPass: [ |
| 616 | { |
| 617 | required: true, |
| 618 | message: amLabels.value.new_password_required, |
| 619 | trigger: 'submit', |
| 620 | }, |
| 621 | { |
| 622 | min: 4, |
| 623 | message: amLabels.value.new_password_length, |
| 624 | trigger: 'submit', |
| 625 | }, |
| 626 | { |
| 627 | validator: () => |
| 628 | store.getters['auth/getNewPassword'] === store.getters['auth/getConfirmPassword'], |
| 629 | message: amLabels.value.passwords_not_match, |
| 630 | trigger: 'submit', |
| 631 | }, |
| 632 | ], |
| 633 | }) |
| 634 | |
| 635 | // * Change Password Alert visibility |
| 636 | let alertVisibility = ref(false) |
| 637 | |
| 638 | function tabClick() { |
| 639 | alertVisibility.value = false |
| 640 | } |
| 641 | |
| 642 | // * Submit Form |
| 643 | function changeProfilePassword() { |
| 644 | passFormRef.value.validate((valid) => { |
| 645 | if (valid) { |
| 646 | let user = store.getters['auth/getProfile'] |
| 647 | |
| 648 | store.commit('setLoading', true) |
| 649 | |
| 650 | httpClient |
| 651 | .post('/users/customers/' + user.id, { password: store.getters['auth/getNewPassword'] }) |
| 652 | .then(() => { |
| 653 | successMessage.value = amLabels.value.password_success |
| 654 | |
| 655 | store.commit('auth/setNewPassword', '') |
| 656 | store.commit('auth/setConfirmPassword', '') |
| 657 | }) |
| 658 | .catch(() => {}) |
| 659 | .finally(() => { |
| 660 | store.commit('setLoading', false) |
| 661 | alertVisibility.value = true |
| 662 | }) |
| 663 | } else { |
| 664 | return false |
| 665 | } |
| 666 | }) |
| 667 | } |
| 668 | provide('changeProfilePassword', changeProfilePassword) |
| 669 | |
| 670 | // * Logout |
| 671 | function logout() { |
| 672 | deleteProfileDialog.value = false |
| 673 | store.commit('auth/setProfileDeleted', true) |
| 674 | store.dispatch('auth/logout') |
| 675 | } |
| 676 | |
| 677 | function addressSelected(addressComponents, cfId) { |
| 678 | if (addressComponents) { |
| 679 | for (const key in customFields) { |
| 680 | if (customFields[key].id === cfId) { |
| 681 | customFields[key].components = mapAddressComponentsForXML(addressComponents) |
| 682 | break |
| 683 | } |
| 684 | } |
| 685 | } |
| 686 | } |
| 687 | |
| 688 | function deleteProfile() { |
| 689 | store.commit('setLoading', true) |
| 690 | |
| 691 | let profileId = store.getters['auth/getProfile'] ? store.getters['auth/getProfile'].id : null |
| 692 | if (!profileId) { |
| 693 | return |
| 694 | } |
| 695 | |
| 696 | let data = { |
| 697 | email: '', |
| 698 | firstName: amLabels.value.customer, |
| 699 | lastName: profileId, |
| 700 | phone: '', |
| 701 | birthday: '', |
| 702 | gender: '', |
| 703 | externalId: '', |
| 704 | password: '', |
| 705 | } |
| 706 | |
| 707 | const vueCookies = useCookies()['cookies'] |
| 708 | |
| 709 | httpClient |
| 710 | .post('/users/customers/' + profileId, data) |
| 711 | .then(() => { |
| 712 | store.commit('auth/setProfile', {}) |
| 713 | vueCookies.remove('ameliaUserEmail') |
| 714 | |
| 715 | store.commit('setLoading', false) |
| 716 | |
| 717 | logout() |
| 718 | }) |
| 719 | .catch((error) => { |
| 720 | store.commit('setLoading', false) |
| 721 | console.log(error) |
| 722 | }) |
| 723 | } |
| 724 | |
| 725 | /** |
| 726 | * * Customer custom fields form |
| 727 | */ |
| 728 | |
| 729 | // Refs and reactive objects |
| 730 | const customFieldsFormRef = ref(null) |
| 731 | const customFieldsForm = reactive({}) |
| 732 | const customFieldsFormRules = reactive({}) |
| 733 | const cfFormConstruction = reactive({}) |
| 734 | const customFields = reactive({}) |
| 735 | |
| 736 | function setCustomerCustomFieldsFormData(field, id, savedValue) { |
| 737 | customFieldsForm[id] = savedValue ?? '' |
| 738 | } |
| 739 | |
| 740 | function setCustomerCustomFieldsFormConstruction(field, id) { |
| 741 | cfFormConstruction[id] = { |
| 742 | template: formFieldsTemplates[field.type], |
| 743 | props: { |
| 744 | id, |
| 745 | itemName: id.toString(), |
| 746 | label: field.label, |
| 747 | options: field.options, |
| 748 | class: `am-capei-att-cf__item am-cf-width-${field.width}`, |
| 749 | }, |
| 750 | } |
| 751 | |
| 752 | if (field.type === 'text-area') { |
| 753 | cfFormConstruction[id].props.itemType = 'textarea' |
| 754 | } |
| 755 | |
| 756 | if (field.type === 'datepicker') { |
| 757 | cfFormConstruction[id].props.weekStartsFromDay = amSettings.wordpress.startOfWeek |
| 758 | } |
| 759 | |
| 760 | if (field.type === 'file') { |
| 761 | let profileId = store.getters['auth/getProfile'] ? store.getters['auth/getProfile'].id : null |
| 762 | cfFormConstruction[id].props = { |
| 763 | ...cfFormConstruction[id].props, |
| 764 | ...{ |
| 765 | btnLabel: amLabels.value.upload_file_here, |
| 766 | isUpload: true, |
| 767 | bookingId: profileId, |
| 768 | source: 'cabinet-customer', |
| 769 | }, |
| 770 | } |
| 771 | } |
| 772 | } |
| 773 | |
| 774 | function setCustomerCustomFieldsFormRules(field) { |
| 775 | customFieldsFormRules[field.id] = [ |
| 776 | { |
| 777 | message: amLabels.value.required_field, |
| 778 | required: field.required, |
| 779 | trigger: ['submit', 'change'], |
| 780 | }, |
| 781 | ] |
| 782 | } |
| 783 | |
| 784 | function setInitCustomerCustomFields() { |
| 785 | const allFields = store.getters['entities/getCustomFields'] |
| 786 | const savedCustomFields = JSON.parse(store.getters['auth/getProfile'].customFields || '{}') |
| 787 | |
| 788 | allFields |
| 789 | .filter((field) => field.saveType === 'customer' && field.type !== 'content') |
| 790 | .sort((a, b) => a.position - b.position) |
| 791 | .forEach((field, index) => { |
| 792 | const { id } = field |
| 793 | const savedValue = savedCustomFields[id]?.value |
| 794 | |
| 795 | customFields[index] = { |
| 796 | ...field, |
| 797 | value: savedValue ?? (field.type === 'checkbox' || field.type === 'file' ? [] : ''), |
| 798 | } |
| 799 | |
| 800 | if (field.type === 'address' && savedCustomFields[id]?.components) { |
| 801 | customFields[index].components = savedCustomFields[id]?.components |
| 802 | } |
| 803 | |
| 804 | setCustomerCustomFieldsFormData(field, id, customFields[index].value) |
| 805 | setCustomerCustomFieldsFormConstruction(field, id) |
| 806 | setCustomerCustomFieldsFormRules(field) |
| 807 | }) |
| 808 | } |
| 809 | |
| 810 | function saveCustomerCustomFields() { |
| 811 | customFieldsFormRef.value.validate((valid) => { |
| 812 | if (valid) { |
| 813 | const user = store.getters['auth/getProfile'] |
| 814 | const customFieldData = {} |
| 815 | |
| 816 | // Build full custom field data structure |
| 817 | for (const id in customFields) { |
| 818 | const field = customFields[id] |
| 819 | const value = customFieldsForm[field.id] |
| 820 | customFieldData[field.id] = { |
| 821 | label: field.label, |
| 822 | type: field.type, |
| 823 | value: typeof value === 'string' ? value.trim() : value, |
| 824 | } |
| 825 | if (field.type === 'address' && field.components) { |
| 826 | customFieldData[field.id].components = field.components |
| 827 | } |
| 828 | } |
| 829 | |
| 830 | const updatedUser = { |
| 831 | ...user, |
| 832 | customFields: JSON.stringify(customFieldData), |
| 833 | } |
| 834 | |
| 835 | store.commit('setLoading', true) |
| 836 | |
| 837 | httpClient.post(`/users/${cabinetType.value}s/${user.id}`, updatedUser).finally(() => { |
| 838 | store.commit('setLoading', false) |
| 839 | successMessage.value = amLabels.value.profile_data_success |
| 840 | alertVisibility.value = true |
| 841 | }) |
| 842 | } else { |
| 843 | return false |
| 844 | } |
| 845 | }) |
| 846 | } |
| 847 | provide('saveCustomerCustomFields', saveCustomerCustomFields) |
| 848 | |
| 849 | // * Colors |
| 850 | let amColors = inject('amColors') |
| 851 | |
| 852 | let cssVars = computed(() => { |
| 853 | return { |
| 854 | '--am-c-capi-primary': amColors.value.colorPrimary, |
| 855 | '--am-c-capi-text': amColors.value.colorMainText, |
| 856 | '--am-c-capi-text-op10': useColorTransparency(amColors.value.colorMainText, 0.1), |
| 857 | } |
| 858 | }) |
| 859 | </script> |
| 860 | |
| 861 | <script> |
| 862 | export default { |
| 863 | name: 'CabinetProfile', |
| 864 | key: 'profile', |
| 865 | } |
| 866 | </script> |
| 867 | |
| 868 | <style lang="scss"> |
| 869 | @mixin am-cabinet-profile { |
| 870 | // am - amelia |
| 871 | // capi - cabinet personal information |
| 872 | // capp - cabinet personal password |
| 873 | .am-capi { |
| 874 | &__sr-only { |
| 875 | position: absolute; |
| 876 | width: 1px; |
| 877 | height: 1px; |
| 878 | padding: 0; |
| 879 | margin: -1px; |
| 880 | overflow: hidden; |
| 881 | clip: rect(0, 0, 0, 0); |
| 882 | white-space: nowrap; |
| 883 | border: 0; |
| 884 | } |
| 885 | |
| 886 | &__inner { |
| 887 | display: block; |
| 888 | padding: 16px 32px; |
| 889 | } |
| 890 | |
| 891 | &__tabs { |
| 892 | .el-tabs__item:focus-visible { |
| 893 | box-shadow: 0 0 2px 2px var(--am-c-capi-primary) inset; |
| 894 | border-radius: 3px; |
| 895 | } |
| 896 | } |
| 897 | |
| 898 | &__form { |
| 899 | display: flex; |
| 900 | flex-wrap: wrap; |
| 901 | justify-content: space-between; |
| 902 | |
| 903 | & > * { |
| 904 | $count: 100; |
| 905 | @for $i from 0 through $count { |
| 906 | &:nth-child(#{$i + 1}) { |
| 907 | animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{$i * 100}ms am-animation-slide-up; |
| 908 | animation-fill-mode: both; |
| 909 | } |
| 910 | } |
| 911 | } |
| 912 | |
| 913 | @media (prefers-reduced-motion: reduce) { |
| 914 | & > * { |
| 915 | animation: none !important; |
| 916 | } |
| 917 | } |
| 918 | |
| 919 | // * traba da postanu globalni stilovi za formu |
| 920 | .el-form { |
| 921 | &-item { |
| 922 | display: block; |
| 923 | font-family: var(--am-font-family); |
| 924 | font-size: var(--am-fs-label); |
| 925 | margin-bottom: 24px; |
| 926 | |
| 927 | &.am-capi__item { |
| 928 | width: calc(50% - 12px); |
| 929 | |
| 930 | &-birthday { |
| 931 | .el-input__inner { |
| 932 | color: transparent !important; |
| 933 | text-shadow: 0 0 0 var(--am-c-inp-text); |
| 934 | cursor: pointer; |
| 935 | } |
| 936 | } |
| 937 | |
| 938 | &.am-rw-480 { |
| 939 | width: 100%; |
| 940 | } |
| 941 | } |
| 942 | |
| 943 | &.am-capp__item { |
| 944 | width: 100%; |
| 945 | |
| 946 | .el-form-item__content { |
| 947 | max-width: 360px; |
| 948 | } |
| 949 | } |
| 950 | |
| 951 | &__label { |
| 952 | flex: 0 0 auto; |
| 953 | text-align: left; |
| 954 | font-size: var(--am-fs-label); |
| 955 | line-height: 1.3; |
| 956 | color: var(--am-c-main-text); |
| 957 | box-sizing: border-box; |
| 958 | margin: 0; |
| 959 | |
| 960 | &:before { |
| 961 | color: var(--am-c-error); |
| 962 | } |
| 963 | } |
| 964 | |
| 965 | &__content { |
| 966 | display: flex; |
| 967 | flex-wrap: wrap; |
| 968 | align-items: center; |
| 969 | flex: 1; |
| 970 | position: relative; |
| 971 | font-size: var(--am-fs-input); |
| 972 | min-width: 0; |
| 973 | color: var(--am-c-main-text); |
| 974 | } |
| 975 | |
| 976 | &__error { |
| 977 | font-size: 12px; |
| 978 | color: var(--am-c-error); |
| 979 | padding-top: 4px; |
| 980 | } |
| 981 | } |
| 982 | } |
| 983 | |
| 984 | .am-whatsapp-opt-in-text { |
| 985 | font-size: 14px; |
| 986 | line-height: 1.5; |
| 987 | } |
| 988 | } |
| 989 | |
| 990 | &__alert { |
| 991 | .el-alert { |
| 992 | padding: 4px 12px 4px 0; |
| 993 | box-sizing: border-box; |
| 994 | |
| 995 | &__content { |
| 996 | .el-alert__closebtn { |
| 997 | top: 50%; |
| 998 | transform: translateY(-50%); |
| 999 | } |
| 1000 | } |
| 1001 | |
| 1002 | &__title { |
| 1003 | display: flex; |
| 1004 | align-items: center; |
| 1005 | font-size: 16px; |
| 1006 | line-height: 1.5; |
| 1007 | |
| 1008 | .am-icon-checkmark-circle-full { |
| 1009 | font-size: 28px; |
| 1010 | line-height: 1; |
| 1011 | color: var(--am-c-alerts-bgr); |
| 1012 | } |
| 1013 | } |
| 1014 | } |
| 1015 | } |
| 1016 | } |
| 1017 | } |
| 1018 | |
| 1019 | .amelia-v2-booking #amelia-container { |
| 1020 | @include am-cabinet-profile; |
| 1021 | } |
| 1022 | </style> |
| 1023 |