PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.8
Booking for Appointments and Events Calendar – Amelia v2.4.8
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 / admin / customize / steps / Cabinet / common / Profile / Profile.vue
ameliabooking / v3 / src / views / admin / customize / steps / Cabinet / common / Profile Last commit date
Profile.vue 1 month ago
Profile.vue
617 lines
1 <template>
2 <div ref="pageContainer" class="am-capi" :style="cssVars">
3 <div class="am-capi__inner">
4 <AmAlert
5 v-if="alertVisibility"
6 type="success"
7 :show-border="true"
8 :close-after="5000"
9 custom-class="am-capi__alert"
10 @trigger-close="closeAlert"
11 >
12 <template #title>
13 <span class="am-icon-checkmark-circle-full"></span>
14 {{ successMessage }}
15 </template>
16 </AmAlert>
17
18 <AmTabs v-model="activeTab" class="am-capi__tabs" @tab-click="tabClick">
19 <AmTabsItem class="am-capi__tabs-item" :label="labelsDisplay('personal_info')" name="first">
20 <el-form
21 ref="infoFormRef"
22 :model="infoFormData"
23 :rules="infoFormRules"
24 label-position="top"
25 class="am-capi__form"
26 :class="responsiveClass"
27 >
28 <template v-for="item in amCustomize.capc.profile.order" :key="item.id">
29 <component
30 :is="infoFormConstruction[item.id].template"
31 v-if="
32 amCustomize[pageRenderKey].profile.options[item.id]
33 ? amCustomize[pageRenderKey].profile.options[item.id].visibility
34 : true
35 "
36 ref="customerCollectorRef"
37 v-model="infoFormData[item.id]"
38 v-model:country-code="infoFormConstruction[item.id].countryPhoneIso"
39 v-bind="infoFormConstruction[item.id].props"
40 v-on="
41 'handlers' in infoFormConstruction[item.id]
42 ? infoFormConstruction[item.id].handlers
43 : {}
44 "
45 ></component>
46 </template>
47 </el-form>
48 </AmTabsItem>
49 <AmTabsItem class="am-capi__tabs-item" :label="labelsDisplay('password_tab')" name="second">
50 <el-form
51 ref="passFormRef"
52 :model="passFormData"
53 :rules="passFormRules"
54 label-position="top"
55 class="am-capi__form"
56 :class="responsiveClass"
57 >
58 <template v-for="(item, key) in passFormConstruction" :key="item.props.itemName">
59 <component
60 :is="item.template"
61 ref="customerPassCollectorRef"
62 v-model="passFormData[key]"
63 v-bind="item.props"
64 ></component>
65 </template>
66 </el-form>
67 </AmTabsItem>
68 </AmTabs>
69
70 <DeleteProfile
71 :visibility="deleteProfileVisibility"
72 :customized-labels="globalStepLabels('deleteProfile')"
73 :customized-options="amCustomize.capc.deleteProfile.options"
74 />
75
76 <MainProfileFooter
77 :display="activeTab"
78 :customized-labels="globalStepLabels('profile')"
79 :delete-footer-type="amCustomize.capc.profile.options.deleteFooterButton.buttonType"
80 :save-footer-button="amCustomize.capc.profile.options.saveFooterButton.buttonType"
81 :pass-footer-button="amCustomize.capc.profile.options.passFooterButton.buttonType"
82 />
83 </div>
84 </div>
85 </template>
86
87 <script setup>
88 import AmTabs from '../../../../../../_components/tabs/AmTabs.vue'
89 import AmTabsItem from '../../../../../../_components/tabs/AmTabsItem.vue'
90 // * _components
91 import AmAlert from '../../../../../../_components/alert/AmAlert.vue'
92
93 // * Dedicated Components
94 import DeleteProfile from '../parts/DeleteProfile.vue'
95 import MainProfileFooter from '../../../../../../common/SbsFormConstruction/MainContent/parts/MainProfileFooter.vue'
96
97 // * Import from Vue
98 import { ref, computed, inject, provide, onMounted, nextTick, watch } from 'vue'
99
100 // * Composables
101 import { useResponsiveClass } from '../../../../../../../assets/js/common/responsive'
102 import { useReactiveCustomize } from '../../../../../../../assets/js/admin/useReactiveCustomize.js'
103
104 // * Form Fields Templates
105 import { formFieldsTemplates } from '../../../../../../../assets/js/common/formFieldsTemplates'
106 import { useColorTransparency } from '../../../../../../../assets/js/common/colorManipulation'
107 import { getInternationalPhoneValue } from '../../../../../../../assets/js/common/phone'
108
109 // * Customize
110 const { amCustomize } = useReactiveCustomize()
111
112 // * Page Content width
113 let pageContainer = ref(null)
114 let pageWidth = ref(0)
115
116 // * Sidebar collapsed var
117 let sidebarCollapsed = inject('sidebarCollapsed')
118
119 // * window resize listener
120 window.addEventListener('resize', resize)
121 // * resize function
122 function resize() {
123 if (pageContainer.value) {
124 pageWidth.value = pageContainer.value.offsetWidth
125 }
126 }
127 watch(sidebarCollapsed, (current) => {
128 if (current) {
129 setTimeout(() => {
130 collapseTriggered()
131 }, 1500)
132 } else {
133 setTimeout(() => {
134 collapseTriggered()
135 }, 500)
136 }
137 })
138
139 function collapseTriggered() {
140 pageWidth.value = pageContainer.value.offsetWidth
141 }
142
143 onMounted(() => {
144 nextTick(() => {
145 pageWidth.value = pageContainer.value.offsetWidth
146 })
147 })
148
149 let responsiveClass = computed(() => {
150 return useResponsiveClass(pageWidth.value)
151 })
152
153 // * Root Settings
154 const amSettings = inject('settings')
155
156 // * Labels
157 let langKey = inject('langKey')
158 let amLabels = inject('labels')
159
160 let stepName = inject('stepName')
161 let subStepName = inject('subStepName')
162 let pageRenderKey = inject('pageRenderKey')
163
164 // * Label computed function
165 function labelsDisplay(label) {
166 let computedLabel = computed(() => {
167 return amCustomize.value[pageRenderKey.value][stepName.value].translations &&
168 amCustomize.value[pageRenderKey.value][stepName.value].translations[label] &&
169 amCustomize.value[pageRenderKey.value][stepName.value].translations[label][langKey.value]
170 ? amCustomize.value[pageRenderKey.value][stepName.value].translations[label][langKey.value]
171 : amLabels[label]
172 })
173
174 return computedLabel.value
175 }
176
177 function globalStepLabels(step) {
178 let stepLabels = {}
179 let customizedLabels = step ? amCustomize.value[pageRenderKey.value][step].translations : null
180 if (customizedLabels && Object.keys(customizedLabels)) {
181 Object.keys(amCustomize.value[pageRenderKey.value][step].translations).forEach((label) => {
182 stepLabels[label] =
183 amCustomize.value[pageRenderKey.value][step].translations[label][langKey.value]
184 })
185 } else {
186 stepLabels = {}
187 }
188
189 return stepLabels
190 }
191
192 let activeTab = ref('first')
193
194 // * Success message
195 let successMessage = ref('')
196
197 // * Change Password Alert visibility
198 let alertVisibility = ref(false)
199
200 function closeAlert() {
201 alertVisibility.value = false
202 successMessage.value = ''
203 }
204
205 /**
206 * * Customer information form
207 */
208
209 // * Form reference
210 let infoFormRef = ref(null)
211
212 // * Form data
213 let infoFormData = ref({
214 firstName: '',
215 lastName: '',
216 email: '',
217 phone: '',
218 birthday: '',
219 })
220
221 // * Form construction
222 let infoFormConstruction = computed(() => {
223 return {
224 firstName: {
225 template: formFieldsTemplates.text,
226 props: {
227 itemName: 'firstName',
228 label: labelsDisplay('first_name_colon'),
229 placeholder: labelsDisplay('enter_first_name'),
230 class: `am-capi__item ${responsiveClass.value}`,
231 },
232 },
233 lastName: {
234 template: formFieldsTemplates.text,
235 props: {
236 itemName: 'lastName',
237 label: labelsDisplay('last_name_colon'),
238 placeholder: labelsDisplay('enter_last_name'),
239 class: `am-capi__item ${responsiveClass.value}`,
240 },
241 },
242 email: {
243 template: formFieldsTemplates.text,
244 props: {
245 itemName: 'email',
246 label: labelsDisplay('email_colon'),
247 placeholder: labelsDisplay('enter_email'),
248 class: `am-capi__item ${responsiveClass.value}`,
249 },
250 },
251 phone: {
252 countryPhoneIso: 'US',
253 template: formFieldsTemplates.phone,
254 props: {
255 itemName: 'phone',
256 label: labelsDisplay('phone_colon'),
257 placeholder: labelsDisplay('enter_phone'),
258 defaultCode:
259 amSettings.general.phoneDefaultCountryCode === 'auto'
260 ? 'us'
261 : amSettings.general.phoneDefaultCountryCode.toLowerCase(),
262 phoneError: false,
263 whatsAppLabel: labelsDisplay('whatsapp_opt_in_text'),
264 isWhatsApp: amSettings.notifications.whatsAppEnabled,
265 class: `am-capi__item ${responsiveClass.value}`,
266 },
267 handlers: {
268 handlePhoneData: (phoneData) => {
269 infoFormData.value.phone = getInternationalPhoneValue(
270 phoneData?.phoneNumber ?? phoneData?.e164,
271 phoneData,
272 phoneData?.countryCode,
273 )
274 },
275 },
276 },
277 birthday: {
278 template: formFieldsTemplates.datepicker,
279 props: {
280 itemName: 'birthday',
281 label: labelsDisplay('date_of_birth'),
282 placeholder: labelsDisplay('enter_date_of_birth'),
283 clearable: true,
284 readonly: false,
285 class: `am-capi__item ${responsiveClass.value}`,
286 },
287 },
288 }
289 })
290
291 // * Form validation rules
292 let infoFormRules = computed(() => {
293 return {
294 firstName: [
295 {
296 required: true,
297 message: labelsDisplay('enter_first_name_warning'),
298 trigger: 'submit',
299 },
300 ],
301 lastName: [
302 {
303 required: amCustomize.value[pageRenderKey.value][stepName.value].options.lastName.required,
304 message: labelsDisplay('enter_last_name_warning'),
305 trigger: 'submit',
306 },
307 ],
308 email: [
309 {
310 required: amCustomize.value[pageRenderKey.value][stepName.value].options.email.required,
311 type: 'email',
312 message: labelsDisplay('enter_valid_email_warning'),
313 trigger: 'submit',
314 },
315 ],
316 phone: [
317 {
318 required: amCustomize.value[pageRenderKey.value][stepName.value].options.phone.required,
319 message: labelsDisplay('enter_phone_warning'),
320 trigger: 'submit',
321 },
322 ],
323 birthday: [
324 {
325 required: amCustomize.value[pageRenderKey.value][stepName.value].options.birthday.required,
326 message: labelsDisplay('enter_date_of_birth_warning'),
327 trigger: 'submit',
328 },
329 ],
330 }
331 })
332
333 let deleteProfileDialog = ref(false)
334 provide('deleteProfileDialog', deleteProfileDialog)
335
336 function saveProfileChanges() {
337 infoFormRef.value.validate((valid) => {
338 if (valid) {
339 successMessage.value = labelsDisplay('profile_data_success')
340 alertVisibility.value = true
341 } else {
342 return false
343 }
344 })
345 }
346 provide('saveProfileChanges', saveProfileChanges)
347
348 // * Delete Profile
349 let deleteProfileVisibility = computed(() => {
350 return subStepName.value === 'deleteProfile'
351 })
352
353 /**
354 * * Customer change password form
355 */
356
357 // * Form reference
358 let passFormRef = ref(null)
359
360 // * Form data
361 let passFormData = ref({
362 newPass: '',
363 confirmPass: '',
364 })
365
366 // * Form construction
367 let passFormConstruction = computed(() => {
368 return {
369 newPass: {
370 template: formFieldsTemplates.text,
371 props: {
372 itemName: 'newPass',
373 itemType: 'password',
374 showPassword: true,
375 label: labelsDisplay('new_password_colon'),
376 placeholder: '',
377 minLength: 3,
378 class: `am-capp__item ${responsiveClass.value}`,
379 },
380 },
381 confirmPass: {
382 template: formFieldsTemplates.text,
383 props: {
384 itemName: 'confirmPass',
385 itemType: 'password',
386 showPassword: true,
387 label: labelsDisplay('new_password_colon_retype'),
388 placeholder: '',
389 minLength: 3,
390 class: `am-capp__item ${responsiveClass.value}`,
391 },
392 },
393 }
394 })
395
396 // * Form validation rules
397 let passFormRules = computed(() => {
398 return {
399 newPass: [
400 {
401 required: true,
402 message: labelsDisplay('new_password_required'),
403 trigger: 'submit',
404 },
405 {
406 min: 4,
407 message: labelsDisplay('new_password_length'),
408 trigger: 'submit',
409 },
410 ],
411 confirmPass: [
412 {
413 required: true,
414 message: labelsDisplay('new_password_required'),
415 trigger: 'submit',
416 },
417 {
418 min: 4,
419 message: labelsDisplay('new_password_length'),
420 trigger: 'submit',
421 },
422 {
423 validator: () => passFormData.value.newPass === passFormData.value.confirmPass,
424 message: labelsDisplay('passwords_not_match'),
425 trigger: 'submit',
426 },
427 ],
428 }
429 })
430
431 function tabClick() {
432 alertVisibility.value = false
433 }
434
435 // * Submit Form
436 function changeProfilePassword() {
437 passFormRef.value.validate((valid) => {
438 if (valid) {
439 successMessage.value = labelsDisplay('password_success')
440 alertVisibility.value = true
441 } else {
442 return false
443 }
444 })
445 }
446 provide('changeProfilePassword', changeProfilePassword)
447
448 // * Colors
449 let amColors = inject('amColors')
450 let cssVars = computed(() => {
451 return {
452 '--am-c-capi-primary': amColors.value.colorPrimary,
453 '--am-c-capi-text': amColors.value.colorMainText,
454 '--am-c-capi-text-op10': useColorTransparency(amColors.value.colorMainText, 0.1),
455 }
456 })
457 </script>
458
459 <script>
460 export default {
461 name: 'CabinetProfile',
462 key: 'profile',
463 }
464 </script>
465
466 <style lang="scss">
467 @mixin am-cabinet-profile {
468 // am - amelia
469 // capi - cabinet personal information
470 // capp - cabinet personal password
471 .am-capi {
472 &__inner {
473 display: block;
474 padding: 16px 32px;
475 }
476
477 // Tabs
478 .el-tabs {
479 &__nav {
480 &-wrap {
481 &:after {
482 background-color: var(--am-c-capi-text-op10);
483 }
484 }
485 }
486
487 &__active-bar {
488 background-color: var(--am-c-capi-primary);
489 }
490
491 &__item {
492 &.is-focus {
493 color: var(--am-c-capi-text);
494
495 &.is-active {
496 color: var(--am-c-capi-primary);
497
498 &:focus {
499 &:not(:active) {
500 box-shadow: none;
501 }
502 }
503 }
504 }
505 }
506
507 &__content {
508 overflow: unset;
509 }
510 }
511
512 &__form {
513 display: flex;
514 flex-wrap: wrap;
515 justify-content: space-between;
516
517 & > * {
518 $count: 100;
519 @for $i from 0 through $count {
520 &:nth-child(#{$i + 1}) {
521 animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{$i * 100}ms am-animation-slide-up;
522 animation-fill-mode: both;
523 }
524 }
525 }
526
527 // * traba da postanu globalni stilovi za formu
528 .el-form {
529 &-item {
530 display: block;
531 font-family: var(--am-font-family), sans-serif;
532 font-size: var(--am-fs-label);
533 margin-bottom: 24px;
534
535 &.am-capi__item {
536 width: calc(50% - 12px);
537
538 &.am-rw-480 {
539 width: 100%;
540 }
541 }
542
543 &.am-capp__item {
544 width: 100%;
545
546 .el-form-item__content {
547 max-width: 360px;
548 }
549 }
550
551 &__label {
552 flex: 0 0 auto;
553 text-align: left;
554 font-size: var(--am-fs-label);
555 line-height: 1.3;
556 color: var(--am-c-main-text);
557 box-sizing: border-box;
558 margin: 0;
559
560 &:before {
561 color: var(--am-c-error);
562 }
563 }
564
565 &__content {
566 display: flex;
567 flex-wrap: wrap;
568 align-items: center;
569 flex: 1;
570 position: relative;
571 font-size: var(--am-fs-input);
572 min-width: 0;
573 color: var(--am-c-main-text);
574 }
575
576 &__error {
577 font-size: 12px;
578 color: var(--am-c-error);
579 padding-top: 4px;
580 }
581 }
582 }
583 }
584
585 &__alert {
586 .el-alert {
587 padding: 4px 12px 4px 0;
588 box-sizing: border-box;
589
590 &__content {
591 .el-alert__closebtn {
592 top: 50%;
593 transform: translateY(-50%);
594 }
595 }
596
597 &__title {
598 display: flex;
599 align-items: center;
600 font-size: 16px;
601 line-height: 1.5;
602
603 .am-icon-checkmark-circle-full {
604 font-size: 28px;
605 line-height: 1;
606 color: var(--am-c-alerts-bgr);
607 }
608 }
609 }
610 }
611 }
612 }
613 #amelia-app-backend-new #amelia-container {
614 @include am-cabinet-profile;
615 }
616 </style>
617