PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / v3 / src / views / public / Cabinet / common / Profile / Profile.vue
ameliabooking / v3 / src / views / public / Cabinet / common / Profile Last commit date
parts 1 month ago EmployeeProfile.vue 1 month ago Profile.vue 1 month ago
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