PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 1.2.31
Booking for Appointments and Events Calendar – Amelia v1.2.31
2.4.9 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 / assets / views / backend / customers / Customers.vue
ameliabooking / assets / views / backend / customers Last commit date
Customers.vue 1 year ago DialogCustomer.vue 1 year ago
Customers.vue
656 lines
1 <template>
2 <div class="am-wrap">
3 <div id="am-customers" class="am-body">
4
5 <!-- Page Header -->
6 <page-header :customersTotal="options.count" @newCustomerBtnClicked="showDialogNewCustomer()"></page-header>
7
8 <!-- Spinner -->
9 <div class="am-spinner am-section" v-show="!fetched">
10 <img :src="$root.getUrl + 'public/img/spinner.svg'"/>
11 </div>
12
13 <!-- Empty State -->
14 <EmptyState
15 :visible="fetched && customers.length === 0 && !filterApplied && fetchedFiltered"
16 :title="$root.labels.no_customers_yet"
17 :description="$root.labels.click_add_customers"
18 :importing="$root.labels.import_customers"
19 @import="dialogImport = true"
20 >
21 </EmptyState>
22
23 <!-- Customers -->
24 <div v-show="fetched && (customers.length !== 0 || customers.length === 0 && filterApplied || !fetchedFiltered)">
25
26 <!-- Filter -->
27 <div class="am-customers-filter am-section">
28 <el-form class="demo-form-inline" :action="exportAction" method="POST">
29 <el-row :gutter="10">
30
31 <!-- Global Search -->
32 <el-col :lg="$root.settings.roles.enableNoShowTag ? 13 : 15" :md="$root.settings.roles.enableNoShowTag ? 12 : 14">
33 <div class="am-search">
34 <el-input
35 :placeholder="searchPlaceholder"
36 v-model="params.search"
37 name="search"
38 >
39 </el-input>
40 </div>
41 </el-col>
42
43 <!-- Sort -->
44 <transition name="fade">
45 <el-col :lg="7" :md="8" :sm="20">
46 <el-form-item>
47 <el-select
48 v-model="params.sort"
49 :placeholder="$root.labels.sort"
50 class="sort"
51 @change="filterData"
52 name="sort"
53 clearable
54 >
55 <el-option
56 v-for="option in options.sort"
57 :key="option.value"
58 :label="option.label"
59 :value="option.value"
60 >
61 </el-option>
62 </el-select>
63 </el-form-item>
64 </el-col>
65 </transition>
66
67
68 <transition name="fade">
69 <el-col :sm="4" :md="4" :lg="2" v-if="$root.settings.roles.enableNoShowTag">
70 <el-form-item>
71 <el-select
72 v-model="params.noShow"
73 clearable
74 class="am-customers-filter-no-show"
75 @change="filterData"
76 :placeholder="$root.labels['no-show']"
77 >
78 <el-option
79 v-for="item in noShowStatuses"
80 :key="item.count"
81 :value="item.count"
82 :label="' '"
83 style="display: flex; justify-content: center; align-items: center; margin-top: 5px; margin-bottom: 5px"
84 >
85 <img style="width: 24px; height: 24px" class="svg-amelia" :alt="'no-show-'+item.type" :src="$root.getUrl+'public/img/am-user-' + item.type + '-no-show.svg'"/>
86 </el-option>
87
88 <template v-if="params.noShow && noShowStatuses.find(n => n.count === params.noShow)" #prefix class="am-no-show-selected">
89 <img style="width: 24px; height: 24px" class="svg-amelia" :alt="'no-show-'+noShowStatuses.find(n => n.count === params.noShow).type" :src="$root.getUrl+'public/img/am-user-' + noShowStatuses.find(n => n.count === params.noShow).type + '-no-show.svg'"/>
90 </template>
91
92 </el-select>
93 </el-form-item>
94 </el-col>
95
96 </transition>
97
98
99 <el-col :lg="2" :md="3" :sm="4" class="am-buttons">
100 <!-- Export -->
101 <el-tooltip placement="top">
102 <div slot="content" v-html="$root.labels.export_tooltip_customers"></div>
103 <el-button
104 class="button-export am-button-icon"
105 @click="dialogExport = true"
106 >
107 <img class="svg-amelia" alt="Export" :src="$root.getUrl+'public/img/export.svg'"/>
108 </el-button>
109 </el-tooltip>
110 <!-- Import -->
111 <el-tooltip placement="top">
112 <div slot="content" v-html="$root.labels.import_tooltip_customers"></div>
113 <el-button
114 class="button-export am-button-icon"
115 @click="dialogImport = true"
116 >
117 <img class="svg-amelia" alt="Import" :src="$root.getUrl+'public/img/import.svg'"/>
118 </el-button>
119 </el-tooltip>
120 </el-col>
121 </el-row>
122
123 <!-- Dialog Export -->
124 <transition name="slide">
125 <el-dialog
126 class="am-side-dialog am-dialog-export"
127 :visible.sync="dialogExport"
128 :show-close="false"
129 v-if="dialogExport"
130 :close-on-click-modal="false"
131 >
132 <dialog-export
133 :data="Object.assign(params, exportParams)"
134 :action="$root.getAjaxUrl + '/report/customers'"
135 @updateAction="(action) => {this.exportAction = action}"
136 @closeDialogExport="dialogExport = false"
137 >
138 </dialog-export>
139 </el-dialog>
140 </transition>
141
142 </el-form>
143 </div>
144
145 <!-- No Results -->
146 <div class="am-empty-state am-section"
147 v-show="fetched && customers.length === 0 && filterApplied && fetchedFiltered">
148 <img :src="$root.getUrl+'public/img/emptystate.svg'">
149 <h2>{{ $root.labels.no_results }}</h2>
150 </div>
151
152 <!-- Customer List -->
153 <div class="am-customers am-section" v-show="fetchedFiltered && customers.length !== 0">
154
155 <!-- Customer List Header -->
156 <div class="am-customers-list-head">
157 <el-row>
158 <el-col :lg="12">
159 <el-row :gutter="10" class="am-customers-flex-row-middle-align">
160 <el-col
161 v-if="$root.settings.capabilities.canDelete === true"
162 :lg="2"
163 :md="2"
164 >
165 <p>
166 <el-checkbox
167 v-model="checkCustomerData.allChecked"
168 @change="checkCustomerData = handleCheckAll(customers, checkCustomerData)">
169 </el-checkbox>
170 </p>
171 </el-col>
172 <el-col :lg="8" :md="8">
173 <p>{{ $root.labels.customer }}:</p>
174 </el-col>
175 <el-col :lg="8" :md="8">
176 <p>{{ $root.labels.wp_user }}:</p>
177 </el-col>
178 <el-col :lg="6" :md="6">
179 <p>{{ $root.labels.phone }}:</p>
180 </el-col>
181 </el-row>
182 </el-col>
183
184 <el-col :lg="12">
185 <el-row :gutter="10" class="am-customers-flex-row-middle-align">
186 <el-col :lg="0" :md="1"></el-col>
187 <el-col :lg="10" :md="10">
188 <p>{{ $root.labels.note }}:</p>
189 </el-col>
190 <el-col :lg="10" :md="10">
191 <p>{{ $root.labels.last_appointment }}:</p>
192 </el-col>
193 <el-col :lg="4" :md="4"></el-col>
194 </el-row>
195 </el-col>
196
197 </el-row>
198 </div>
199
200 <!-- Customer List Content -->
201 <div class="am-customers-list">
202
203 <!-- Customers -->
204 <el-collapse>
205 <el-collapse-item
206 v-for="(customer, index) in customers"
207 :key="customer.id"
208 :name="customer.id"
209 class="am-customer"
210 :class="customer.status === 'blocked' ? 'am-customer-blocked' : ''"
211 >
212 <template slot="title">
213 <div class="am-customer-data">
214 <el-row :gutter="10">
215
216 <el-col :lg="12">
217 <el-row :gutter="10" class="am-customers-flex-row-middle-align">
218
219 <!-- Checkbox -->
220 <el-col :lg="2" :sm="1" v-if="$root.settings.capabilities.canDelete === true">
221 <span @click.stop>
222 <el-checkbox
223 v-model="customer.checked"
224 @change="checkCustomerData = handleCheckSingle(customers, checkCustomerData)">
225 </el-checkbox>
226 </span>
227 </el-col>
228
229 <!-- Customer Name -->
230 <el-col :lg="8" :sm="8">
231 <p class="am-col-title">{{ $root.labels.customer }}:</p>
232 <h3 :class="getNoShowClass(null, null, customer, customer.status)">
233 {{ customer.firstName + ' ' + customer.lastName }}
234 </h3>
235 <el-tooltip class="item" :content=" customer.email" placement="top">
236 <span>{{ customer.email }}</span>
237 </el-tooltip>
238 </el-col>
239
240 <!-- WordPress User -->
241 <el-col :lg="8" :sm="8">
242 <p class="am-col-title">{{ $root.labels.wp_user }}:</p>
243 <div class="am-assigned">
244 <img :src="customer.wpUserPhotoUrl"/>
245 <el-tooltip class="item" :content="customer.wpName" placement="top">
246 <h4>{{ customer.wpName }}</h4>
247 </el-tooltip>
248 </div>
249 </el-col>
250
251 <!-- Phone -->
252 <el-col :lg="6" :sm="7">
253 <p class="am-col-title">{{ $root.labels.phone }}:</p>
254 <el-tooltip class="item" :content="customer.phone" placement="top">
255 <h4>{{ customer.phone }}</h4>
256 </el-tooltip>
257 </el-col>
258
259 </el-row>
260 </el-col>
261
262 <el-col :lg="12">
263 <el-row :gutter="10" class="am-customers-flex-row-middle-align">
264 <el-col :lg="0" :sm="1" class="hide-on-mobile"></el-col>
265
266 <!-- Note -->
267 <el-col :lg="10" :sm="8" class="hide-on-mobile">
268 <p class="am-col-title">{{ $root.labels.note }}:</p>
269 <p>{{ customer.note ? customer.note.substring(0, 20)+'...' : ''}}</p>
270 </el-col>
271
272 <!-- Last Appointment -->
273 <el-col :lg="10" :sm="8">
274 <p class="am-col-title">{{ $root.labels.last_appointment }}:</p>
275 <h4 v-if="customer.lastAppointment">
276 {{ getFrontedFormattedDateTime(customer.lastAppointment) }}
277 </h4>
278 <h4 v-else>/</h4>
279 </el-col>
280
281 <!-- Edit Button -->
282 <el-col :lg="4" :sm="7" class="align-right">
283 <div @click.stop>
284 <el-button v-if="$root.settings.capabilities.canWrite === true" @click="showDialogEditCustomer(index)">{{ $root.labels.edit }}</el-button>
285 </div>
286 </el-col>
287
288 </el-row>
289 </el-col>
290
291 </el-row>
292 </div>
293 </template>
294
295 <!-- Collapsed Data -->
296 <div class="am-customer-details">
297 <el-row>
298
299 <el-col :md="12">
300
301 <!-- Total Appointments -->
302 <el-row :gutter="10" class="am-customers-flex-row-top-align">
303 <el-col :md="2" :sm="1"></el-col>
304 <el-col :md="10" :sm="10">
305 <p class="am-data">{{ $root.labels.total_appointments }}:</p>
306 </el-col>
307 <el-col :md="10" :sm="13">
308 <p class="am-value">{{ customer.totalAppointments }}</p>
309 </el-col>
310 </el-row>
311
312 <!-- Last Appointment -->
313 <el-row :gutter="10" class="am-customers-flex-row-top-align">
314 <el-col :md="2" :sm="1"></el-col>
315 <el-col :md="10" :sm="10">
316 <p class="am-data">{{ $root.labels.last_appointment }}:</p>
317 </el-col>
318 <el-col :md="10" :sm="13">
319 <p class="am-value" v-if="customer.lastAppointment">
320 {{ getFrontedFormattedDateTime(customer.lastAppointment) }}
321 </p>
322 <h4 v-else>/</h4>
323 </el-col>
324 </el-row>
325
326 </el-col>
327
328 <el-col :md="12">
329
330 <!-- Note -->
331 <el-row :gutter="10" class="am-customers-flex-row-top-align">
332 <el-col :md="0" :sm="1"></el-col>
333 <el-col :md="10" :sm="10">
334 <p class="am-data">{{ $root.labels.note }}:</p>
335 </el-col>
336 <el-col :md="14" :sm="13">
337 <p class="am-value">{{ customer.note }}</p>
338 </el-col>
339 </el-row>
340
341 </el-col>
342
343 </el-row>
344 </div>
345
346 </el-collapse-item>
347 </el-collapse>
348
349 </div>
350 </div>
351
352 <!-- Selected Popover Delete -->
353 <group-delete
354 name="users/customers"
355 :entities="customers"
356 :checkGroupData="checkCustomerData"
357 :confirmDeleteMessage="$root.labels.confirm_delete_customer"
358 :successMessage="{single: $root.labels.customer_deleted, multiple: $root.labels.customers_deleted}"
359 :errorMessage="{single: $root.labels.customer_not_deleted, multiple: $root.labels.customers_not_deleted}"
360 @groupDeleteCallback="groupDeleteCallback"
361 >
362 </group-delete>
363
364 <!-- Pagination -->
365 <pagination-block
366 :params="params"
367 :show="$root.settings.general.itemsPerPageBackEnd"
368 :count="options.filteredCount"
369 :label="$root.labels.customers_lower"
370 :visible="fetched && customers.length !== 0 && fetchedFiltered"
371 @change="filterData"
372 >
373 </pagination-block>
374
375 <!-- Content Spinner -->
376 <div class="am-spinner am-section" v-show="!fetchedFiltered">
377 <img :src="$root.getUrl+'public/img/spinner.svg'"/>
378 </div>
379
380 </div>
381
382 <!-- Dialog Import -->
383 <transition name="slide">
384 <el-dialog
385 class="am-side-dialog am-dialog-import"
386 :visible.sync="dialogImport"
387 :show-close="false"
388 v-if="dialogImport"
389 :close-on-click-modal="false"
390 >
391 <dialog-import
392 :fields="fields"
393 :action="$root.getAjaxUrl + '/import/customers'"
394 :required-fields="requiredFields"
395 :values-overwrite="valuesOverwrite"
396 :values-saved="valuesSaved"
397 @closeDialogImport="dialogImport = false"
398 @getEntities="getCustomers"
399 >
400 </dialog-import>
401 </el-dialog>
402 </transition>
403
404 <!-- Button New -->
405 <div v-if="$root.settings.capabilities.canWrite === true" id="am-button-new" class="am-button-new">
406 <el-button id="am-plus-symbol" type="primary" icon="el-icon-plus" @click="showDialogNewCustomer()"></el-button>
407 </div>
408
409 <!-- Dialog New Customer -->
410 <transition name="slide">
411 <el-dialog
412 :close-on-click-modal="false"
413 class="am-side-dialog"
414 :visible.sync="dialogCustomer"
415 :show-close="false"
416 v-if="dialogCustomer">
417 <dialog-customer
418 :customer="customer"
419 @saveCallback="updateCustomerCallback"
420 @closeDialog="dialogCustomer = false"
421 >
422 </dialog-customer>
423 </el-dialog>
424 </transition>
425
426 <!-- Help Button -->
427 <el-col :md="6" class="">
428 <a class="am-help-button" href="https://wpamelia.com/customers/" target="_blank" rel="nofollow">
429 <i class="el-icon-question"></i> {{ $root.labels.need_help }}?
430 </a>
431 </el-col>
432
433 <!-- <dialog-new-customize></dialog-new-customize>-->
434
435 </div>
436 </div>
437 </template>
438
439 <script>
440 import DialogCustomer from './DialogCustomer.vue'
441 import DialogExport from '../parts/DialogExport.vue'
442 import DialogImport from '../parts/DialogImport.vue'
443 import PageHeader from '../parts/PageHeader.vue'
444 import imageMixin from '../../../js/common/mixins/imageMixin'
445 import dateMixin from '../../../js/common/mixins/dateMixin'
446 import notifyMixin from '../../../js/backend/mixins/notifyMixin'
447 import checkMixin from '../../../js/backend/mixins/checkMixin'
448 import customerMixin from '../../../js/backend/mixins/customerMixin'
449 import GroupDelete from '../parts/GroupDelete.vue'
450 import PaginationBlock from '../parts/PaginationBlock.vue'
451 //import DialogNewCustomize from '../parts/DialogNewCustomize.vue'
452
453 export default {
454
455 mixins: [imageMixin, dateMixin, notifyMixin, checkMixin, customerMixin],
456
457 data () {
458 return {
459 noShowStatuses: [
460 {
461 count: 1,
462 type: 'single'
463 },
464 {
465 count: 2,
466 type: 'double'
467 },
468 {
469 count: 3,
470 type: 'multiple'
471 }
472 ],
473 fields: [
474 {label: this.$root.labels.first_name, value: 'firstName'},
475 {label: this.$root.labels.last_name, value: 'lastName'},
476 {label: this.$root.labels.email, value: 'email'},
477 {label: this.$root.labels.phone, value: 'phone'},
478 {label: this.$root.labels.gender, value: 'gender'},
479 {label: this.$root.labels.birthday, value: 'birthday'},
480 {label: this.$root.labels.note, value: 'note'},
481 {label: this.$root.labels.dont_import, value: 'dontImport'}
482 ],
483 requiredFields: [
484 {label: this.$root.labels.first_name, value: 'firstName'},
485 {label: this.$root.labels.last_name, value: 'lastName'}
486 ],
487 valuesSaved: [this.$root.labels.number_of_appointments, this.$root.labels.date_created, this.$root.labels.last_appointment_date, this.$root.labels.all_customer_appointments],
488 valuesOverwrite: [this.$root.labels.first_name, this.$root.labels.last_name, this.$root.labels.phone, this.$root.labels.gender, this.$root.labels.birthday, this.$root.labels.note],
489 checkCustomerData: {
490 toaster: false,
491 allChecked: false
492 },
493 fetchedFiltered: false,
494 customers: [],
495 dialogExport: false,
496 dialogImport: false,
497 isIndeterminate: true,
498 customer: null,
499 fetched: false,
500 params: {
501 page: 1,
502 sort: '',
503 search: '',
504 noShow: ''
505 },
506
507 exportParams: {
508 fields: [
509 {label: this.$root.labels.first_name, value: 'firstName', checked: true},
510 {label: this.$root.labels.last_name, value: 'lastName', checked: true},
511 {label: this.$root.labels.email, value: 'email', checked: true},
512 {label: this.$root.labels.phone, value: 'phone', checked: true},
513 {label: this.$root.labels.gender, value: 'gender', checked: true},
514 {label: this.$root.labels.date_of_birth, value: 'birthday', checked: true},
515 {label: this.$root.labels.customer_note, value: 'note', checked: true},
516 {label: this.$root.labels.last_appointment, value: 'lastAppointment', checked: true},
517 {label: this.$root.labels.total_appointments, value: 'totalAppointments', checked: true},
518 {label: this.$root.labels.pending_appointments, value: 'pendingAppointments', checked: true}
519 ]
520 },
521 exportAction: '',
522
523 searchPlaceholder: this.$root.labels.customers_search_placeholder,
524 timer: null,
525 options: {
526 filteredCount: 0,
527 fetched: false,
528 count: 0,
529 sort: [
530 {
531 value: 'customer',
532 label: this.$root.labels.name_ascending
533 },
534 {
535 value: '-customer',
536 label: this.$root.labels.name_descending
537 },
538 {
539 value: 'last-appointment',
540 label: this.$root.labels.last_appointment_ascending
541 },
542 {
543 value: '-last-appointment',
544 label: this.$root.labels.last_appointment_descending
545 }
546 ]
547 }
548 }
549 },
550
551 created () {
552 this.fetchData()
553 },
554
555 mounted () {
556 this.inlineSVG()
557 },
558
559 methods: {
560 fetchData () {
561 this.fetched = false
562 this.getCustomers()
563 },
564
565 filterData () {
566 this.fetchedFiltered = false
567 this.getCustomers()
568 },
569
570 getCustomers () {
571 Object.keys(this.params).forEach((key) => (!this.params[key]) && delete this.params[key])
572
573 this.$http.get(`${this.$root.getAjaxUrl}/users/customers`, {
574 params: this.params
575 })
576 .then(response => {
577 let customers = response.data.data.users
578
579 customers.forEach(function (cusItem) {
580 cusItem.checked = false
581
582 if (cusItem.translations) {
583 cusItem.language = JSON.parse(cusItem.translations).defaultLanguage
584 }
585
586 if (cusItem.customFields) {
587 cusItem.customFields = JSON.parse(cusItem.customFields)
588 }
589 })
590
591 this.customers = customers
592 this.options.count = response.data.data.totalCount
593 this.options.filteredCount = response.data.data.filteredCount
594 this.fetched = true
595 this.fetchedFiltered = true
596 })
597 .catch(e => {
598 console.log(e.message)
599 this.fetched = true
600 this.fetchedFiltered = true
601 })
602 },
603
604 showDialogNewCustomer () {
605 this.customer = this.getInitCustomerObject()
606 this.dialogCustomer = true
607 },
608
609 showDialogEditCustomer (id) {
610 this.customer = this.customers[id]
611 this.dialogCustomer = true
612 },
613
614 updateCustomerCallback () {
615 this.dialogCustomer = false
616 this.allChecked = false
617 this.fetchData()
618 },
619
620 groupDeleteCallback () {
621 this.checkCustomerData.allChecked = false
622 this.checkCustomerData.toaster = false
623 this.fetchData()
624 }
625 },
626
627 computed: {
628 filterApplied () {
629 return !!this.params.search || !!this.params.noShow
630 }
631 },
632
633 watch: {
634 'params.search' () {
635 if (typeof this.params.search !== 'undefined') {
636 this.fetchedFiltered = false
637 clearTimeout(this.timer)
638 this.timer = setTimeout(this.filterData, 500)
639 }
640 }
641 },
642
643 components: {
644 PageHeader,
645 DialogCustomer,
646 DialogExport,
647 DialogImport,
648 GroupDelete,
649 PaginationBlock
650 // DialogNewCustomize
651 }
652
653 }
654 </script>
655
656