PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.4.2
LatePoint – Calendar Booking Plugin for Appointments and Events v5.4.2
5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / controllers / customers_controller.php
latepoint / lib / controllers Last commit date
activities_controller.php 3 months ago auth_controller.php 3 months ago booking_form_settings_controller.php 3 months ago bookings_controller.php 3 months ago calendars_controller.php 3 months ago carts_controller.php 3 months ago controller.php 3 months ago customer_cabinet_controller.php 2 months ago customers_controller.php 3 months ago dashboard_controller.php 3 months ago default_agent_controller.php 3 months ago events_controller.php 3 months ago form_fields_controller.php 3 months ago integrations_controller.php 3 months ago invoices_controller.php 2 months ago manage_booking_by_key_controller.php 3 months ago manage_order_by_key_controller.php 3 months ago notifications_controller.php 3 months ago orders_controller.php 3 months ago pro_controller.php 2 months ago process_jobs_controller.php 3 months ago processes_controller.php 3 months ago search_controller.php 3 months ago services_controller.php 3 months ago settings_controller.php 3 months ago steps_controller.php 3 months ago stripe_connect_controller.php 2 months ago support_topics_controller.php 3 months ago todos_controller.php 3 months ago transactions_controller.php 3 months ago wizard_controller.php 2 months ago
customers_controller.php
588 lines
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit; // Exit if accessed directly.
4 }
5
6
7 if ( ! class_exists( 'OsCustomersController' ) ) :
8
9
10 class OsCustomersController extends OsController {
11
12 function __construct() {
13 parent::__construct();
14
15
16 $this->views_folder = LATEPOINT_VIEWS_ABSPATH . 'customers/';
17 $this->vars['page_header'] = OsMenuHelper::get_menu_items_by_id( 'customers' );
18 $this->vars['breadcrumbs'][] = array(
19 'label' => __( 'Customers', 'latepoint' ),
20 'link' => OsRouterHelper::build_link( OsRouterHelper::build_route_name( 'customers', 'index' ) ),
21 );
22 }
23
24 public function destroy() {
25 if ( filter_var( $this->params['id'], FILTER_VALIDATE_INT ) ) {
26 $this->check_nonce( 'destroy_customer_' . $this->params['id'] );
27 $customer = new OsCustomerModel( $this->params['id'] );
28 if ( $customer->delete() ) {
29 $status = LATEPOINT_STATUS_SUCCESS;
30 $response_html = __( 'Customer Removed', 'latepoint' );
31 } else {
32 $status = LATEPOINT_STATUS_ERROR;
33 $response_html = __( 'Error Removing Customer', 'latepoint' );
34 }
35 } else {
36 $status = LATEPOINT_STATUS_ERROR;
37 $response_html = __( 'Error Removing Customer', 'latepoint' );
38 }
39
40 if ( $this->get_return_format() == 'json' ) {
41 $this->send_json(
42 array(
43 'status' => $status,
44 'message' => $response_html,
45 )
46 );
47 }
48 }
49
50
51 public function view_customer_log() {
52
53 $activities = new OsActivityModel();
54 $activities = $activities->where( [ 'customer_id' => absint( $this->params['customer_id'] ) ] )->order_by( 'id desc' )->get_results_as_models();
55
56 $customer = new OsCustomerModel( $this->params['customer_id'] );
57
58 $this->vars['customer'] = $customer;
59 $this->vars['activities'] = $activities;
60
61 $this->format_render( __FUNCTION__ );
62 }
63
64
65 public function quick_new() {
66 $customer = new OsCustomerModel();
67
68 $this->vars['customer'] = $customer;
69
70 $this->format_render( 'quick_edit' );
71 }
72
73 public function quick_edit() {
74 if ( ! filter_var( $this->params['customer_id'], FILTER_VALIDATE_INT ) ) {
75 $this->access_not_allowed();
76 }
77 $customer = new OsCustomerModel( $this->params['customer_id'] );
78
79 $this->vars['customer'] = $customer;
80
81 $this->format_render( __FUNCTION__ );
82 }
83
84
85 public function inline_edit_form() {
86 $selected_customer = new OsCustomerModel();
87 if ( isset( $this->params['customer_id'] ) ) {
88 $selected_customer->load_by_id( $this->params['customer_id'] );
89 }
90 $this->vars['default_fields_for_customer'] = OsSettingsHelper::get_default_fields_for_customer();
91 $this->vars['selected_customer'] = $selected_customer;
92 $this->format_render( __FUNCTION__ );
93 }
94
95 public function set_as_guest() {
96 // CSRF protection
97 $this->check_nonce( 'set_customer_as_guest_' . $this->params['id'] );
98
99 if ( filter_var( $this->params['id'], FILTER_VALIDATE_INT ) ) {
100 $customer = new OsCustomerModel( $this->params['id'] );
101 if ( $customer->update_attributes( [ 'is_guest' => true ] ) ) {
102 $status = LATEPOINT_STATUS_SUCCESS;
103 $response_html = __( 'Customer is now allowed to book without password', 'latepoint' );
104 } else {
105 $status = LATEPOINT_STATUS_ERROR;
106 $response_html = $customer->get_error_messages();
107 }
108 } else {
109 $status = LATEPOINT_STATUS_ERROR;
110 $response_html = __( 'Error setting customer as guest', 'latepoint' );
111 }
112
113 if ( $this->get_return_format() == 'json' ) {
114 $this->send_json(
115 array(
116 'status' => $status,
117 'message' => $response_html,
118 )
119 );
120 }
121 }
122
123 /*
124 Edit customer
125 */
126
127 public function edit_form() {
128 $this->vars['page_header'] = __( 'Edit Customer', 'latepoint' );
129 $this->vars['breadcrumbs'][] = array(
130 'label' => __( 'Edit Customer', 'latepoint' ),
131 'link' => false,
132 );
133
134 if ( filter_var( $this->params['id'], FILTER_VALIDATE_INT ) ) {
135 // check if allowed to access
136 $customer = new OsCustomerModel();
137 $customer = $customer->where( [ LATEPOINT_TABLE_CUSTOMERS . '.id' => absint( $this->params['id'] ) ] )->filter_allowed_records()->set_limit( 1 )->get_results_as_models();
138 $this->vars['customer'] = $customer;
139 $this->vars['wp_users_for_select'] = OsWpUserHelper::get_wp_users_for_select();
140 }
141
142 $this->format_render( __FUNCTION__ );
143 }
144
145
146 public function query_for_booking_form() {
147 $query = trim( $this->params['query'] );
148 $sql_query = '%' . $query . '%';
149 $query = $this->params['query'];
150 $customers = new OsCustomerModel();
151 $this->vars['query'] = $query;
152 $this->vars['customers'] = $customers->where(
153 array(
154 'OR' => array(
155 'CONCAT (first_name, " ", last_name) LIKE ' => $sql_query,
156 'email LIKE' => $sql_query,
157 'phone LIKE' => $sql_query,
158 ),
159 )
160 )->set_limit( 20 )->order_by( 'first_name asc, last_name asc' )->get_results_as_models();
161
162 $this->format_render( __FUNCTION__ );
163 }
164
165
166 /*
167 Create customer
168 */
169
170 public function create() {
171 $this->check_nonce( 'new_customer' );
172 $customer = new OsCustomerModel();
173 // Security fix: Prevent mass assignment of wordpress_user_id by non-admin users.
174 // Use admin scope if user is authenticated as admin, otherwise restrict to public fields.
175 $customer->set_data( $this->params['customer'], OsAuthHelper::is_admin_logged_in() ? LATEPOINT_PARAMS_SCOPE_ADMIN : LATEPOINT_PARAMS_SCOPE_PUBLIC );
176 if ( $customer->save() ) {
177 // translators: %s is the html of a customer edit link
178 $response_html = sprintf( __( 'Customer Created ID: %s', 'latepoint' ), '<span class="os-notification-link" ' . OsCustomerHelper::quick_customer_btn_html( $customer->id ) . '>' . $customer->id . '</span>' );
179 $status = LATEPOINT_STATUS_SUCCESS;
180 do_action( 'latepoint_customer_created', $customer );
181 } else {
182 $response_html = $customer->get_error_messages();
183 $status = LATEPOINT_STATUS_ERROR;
184 }
185 if ( $this->get_return_format() == 'json' ) {
186 $this->send_json(
187 array(
188 'status' => $status,
189 'message' => $response_html,
190 )
191 );
192 }
193 }
194
195
196 /*
197 Update customer
198 */
199
200 public function update() {
201 if ( isset( $this->params['customer']['id'] ) && filter_var( $this->params['customer']['id'], FILTER_VALIDATE_INT ) ) {
202 $this->check_nonce( 'edit_customer_' . $this->params['customer']['id'] );
203 $customer = new OsCustomerModel( $this->params['customer']['id'] );
204 if ( ! $customer || ! OsRolesHelper::can_user_make_action_on_model_record( $customer, 'edit' ) ) {
205 $response_html = __( 'Access Restricted', 'latepoint' );
206 $status = LATEPOINT_STATUS_ERROR;
207 } else {
208 $old_customer_data = $customer->get_data_vars();
209 // Security fix: Prevent mass assignment of wordpress_user_id by non-admin users.
210 // Use admin scope if user is authenticated as admin, otherwise restrict to public fields.
211 $customer->set_data( $this->params['customer'], OsAuthHelper::is_admin_logged_in() ? LATEPOINT_PARAMS_SCOPE_ADMIN : LATEPOINT_PARAMS_SCOPE_PUBLIC );
212 if ( $customer->save() ) {
213 // translators: %s is the html of a customer edit link
214 $response_html = sprintf( __( 'Customer Updated ID: %s', 'latepoint' ), '<span class="os-notification-link" ' . OsCustomerHelper::quick_customer_btn_html( $customer->id ) . '>' . $customer->id . '</span>' );
215 $status = LATEPOINT_STATUS_SUCCESS;
216 do_action( 'latepoint_customer_updated', $customer, $old_customer_data );
217 } else {
218 $response_html = $customer->get_error_messages();
219 $status = LATEPOINT_STATUS_ERROR;
220 }
221 }
222 } else {
223 $response_html = __( 'Invalid customer ID', 'latepoint' );
224 $status = LATEPOINT_STATUS_ERROR;
225 }
226 if ( $this->get_return_format() == 'json' ) {
227 $this->send_json(
228 array(
229 'status' => $status,
230 'message' => $response_html,
231 )
232 );
233 }
234 }
235
236 public function mini_profile() {
237 if ( filter_var( $this->params['customer_id'], FILTER_VALIDATE_INT ) ) {
238 $customer = new OsCustomerModel( $this->params['customer_id'] );
239 $this->vars['upcoming_booking'] = $customer->get_future_bookings( 1, true );
240 $this->vars['customer'] = $customer;
241
242
243 $pie_labels = [];
244 $pie_colors = [];
245 $pie_values = [];
246 $pie_chart_data = OsBookingHelper::get_stat(
247 'bookings',
248 [
249 'group_by' => 'status',
250 'customer_id' => $customer->id,
251 ]
252 );
253 $colors = [ '#2752E4', '#C066F1', '#26B7DD', '#E8C634', '#19CED6', '#2FEAA3', '#252a3e', '#8d87a5', '#b9b784' ];
254 $status_colors = [
255 LATEPOINT_BOOKING_STATUS_APPROVED => '#35d893',
256 LATEPOINT_BOOKING_STATUS_PENDING => '#e6b935',
257 LATEPOINT_BOOKING_STATUS_PAYMENT_PENDING => '#4ca4ef',
258 LATEPOINT_BOOKING_STATUS_CANCELLED => '#f1585d',
259 ];
260 $i = 0;
261 foreach ( $pie_chart_data as $pie_data ) {
262 $pie_labels[] = $pie_data['status'];
263 $pie_colors[] = isset( $status_colors[ $pie_data['status'] ] ) ? $status_colors[ $pie_data['status'] ] : $colors[ $i ];
264 $pie_values[] = $pie_data['stat'];
265 $i++;
266 }
267
268 $this->vars['pie_chart_data'] = [
269 'labels' => $pie_labels,
270 'colors' => $pie_colors,
271 'values' => $pie_values,
272 ];
273
274
275 $this->set_layout( 'none' );
276 $response_html = $this->format_render_return( __FUNCTION__ );
277 } else {
278 $status = LATEPOINT_STATUS_ERROR;
279 $response_html = __( 'Error Accessing Customer', 'latepoint' );
280 }
281
282 if ( $this->get_return_format() == 'json' ) {
283 $this->send_json(
284 array(
285 'status' => $status,
286 'message' => $response_html,
287 )
288 );
289 }
290 }
291
292 public function connect_all_to_wp_users() {
293 // CSRF protection
294 $this->check_nonce( 'connect_all_customers_to_wp_users' );
295
296 $customers = new OsCustomerModel();
297 $customers = $customers->where( [ 'wordpress_user_id' => [ 'OR' => [ 0, 'IS NULL' ] ] ] )->get_results_as_models();
298 if ( $customers ) {
299 foreach ( $customers as $customer ) {
300 $wp_user_id = OsCustomerHelper::create_wp_user_for_customer( $customer );
301 if ( $wp_user_id ) {
302 //check if wp user already connected to another customer
303 $connected_customer = new OsCustomerModel();
304 $connected_customer = $connected_customer->where( [ 'wordpress_user_id' => $wp_user_id ] )->set_limit( 1 )->get_results_as_models();
305 if ( ! $connected_customer ) {
306 $customer->update_attributes( [ 'wordpress_user_id' => $wp_user_id ] );
307 }
308 }
309 }
310 }
311
312 if ( $this->get_return_format() == 'json' ) {
313 $this->send_json(
314 array(
315 'status' => LATEPOINT_STATUS_SUCCESS,
316 'message' => __( 'Customers Connected', 'latepoint' ),
317 )
318 );
319 }
320 }
321
322 public function disconnect_from_wp_user() {
323 // CSRF protection
324 $this->check_nonce( 'disconnect_customer_from_wp_user_' . $this->params['customer_id'] );
325
326 $customer_id = $this->params['customer_id'];
327 $customer = new OsCustomerModel();
328 $customer = $customer->where( [ 'id' => $customer_id ] )->set_limit( 1 )->get_results_as_models();
329 if ( $customer ) {
330 $customer->update_attributes( [ 'wordpress_user_id' => null ] );
331 }
332 if ( $this->get_return_format() == 'json' ) {
333 $this->send_json(
334 array(
335 'status' => LATEPOINT_STATUS_SUCCESS,
336 'message' => __( 'Customer Disconnected', 'latepoint' ),
337 )
338 );
339 }
340 }
341
342 public function connect_to_wp_user() {
343 // CSRF protection
344 $this->check_nonce( 'connect_customer_to_wp_user_' . $this->params['customer_id'] );
345
346 $customer_id = $this->params['customer_id'];
347 $customer = new OsCustomerModel();
348 $customer = $customer->where( [ 'id' => $customer_id ] )->set_limit( 1 )->get_results_as_models();
349 if ( $customer && ! $customer->wordpress_user_id ) {
350 $wp_user = OsCustomerHelper::create_wp_user_for_customer( $customer );
351 }
352 if ( $this->get_return_format() == 'json' ) {
353 $this->send_json(
354 array(
355 'status' => LATEPOINT_STATUS_SUCCESS,
356 'message' => __( 'Customer Connected', 'latepoint' ),
357 )
358 );
359 }
360 }
361
362
363 public function index() {
364 $this->vars['page_header'] = false;
365 $page_number = isset( $this->params['page_number'] ) ? $this->params['page_number'] : 1;
366 $per_page = OsSettingsHelper::get_number_of_records_per_page();
367 $offset = ( $page_number > 1 ) ? ( ( $page_number - 1 ) * $per_page ) : 0;
368
369
370 $customers = new OsCustomerModel();
371 $query_args = [];
372
373 $filter = isset( $this->params['filter'] ) ? $this->params['filter'] : false;
374
375 // TABLE SEARCH FILTERS
376 if ( $filter ) {
377 if ( $filter['id'] ) {
378 $query_args['id'] = $filter['id'];
379 }
380 if ( $filter['registration_date_from'] && $filter['registration_date_to'] ) {
381 $query_args[ LATEPOINT_TABLE_CUSTOMERS . '.created_at >=' ] = $filter['registration_date_from'];
382 $query_args[ LATEPOINT_TABLE_CUSTOMERS . '.created_at <=' ] = $filter['registration_date_to'];
383 }
384 if ( $filter['customer'] ) {
385 $query_args[ 'concat_ws(" ", ' . LATEPOINT_TABLE_CUSTOMERS . '.first_name,' . LATEPOINT_TABLE_CUSTOMERS . '.last_name) LIKE' ] = '%' . $filter['customer'] . '%';
386 $this->vars['customer_name_query'] = $filter['customer'];
387 }
388 if ( $filter['phone'] ) {
389 $query_args['phone LIKE'] = '%' . $filter['phone'] . '%';
390 $this->vars['phone_query'] = $filter['phone'];
391 }
392 if ( $filter['email'] ) {
393 $query_args['email LIKE'] = '%' . $filter['email'] . '%';
394 $this->vars['email_query'] = $filter['email'];
395 }
396 }
397
398
399 // OUTPUT CSV IF REQUESTED
400 if ( isset( $this->params['download'] ) && $this->params['download'] == 'csv' ) {
401 // CSRF protection
402 $this->check_nonce( 'customers_csv_export' );
403
404 $csv_filename = 'customers_' . OsUtilHelper::random_text();
405
406 header( 'Content-Type: text/csv' );
407 header( "Content-Disposition: attachment; filename={$csv_filename}.csv" );
408
409 $labels_row = [
410 __( 'ID', 'latepoint' ),
411 __( 'Name', 'latepoint' ),
412 __( 'Phone', 'latepoint' ),
413 __( 'Email', 'latepoint' ),
414 __( 'Total Appointments', 'latepoint' ),
415 __( 'Next Appointment', 'latepoint' ),
416 __( 'Registered On', 'latepoint' ),
417 ];
418
419
420 $customers_data = [];
421 $customers_data[] = $labels_row;
422
423
424 $customers_arr = $customers->where( $query_args )->filter_allowed_records()->order_by( 'id desc' )->get_results_as_models();
425 if ( $customers_arr ) {
426 foreach ( $customers_arr as $customer ) {
427 $next_booking = $customer->get_future_bookings( 1, true );
428 $values_row = [
429 $customer->id,
430 $customer->full_name,
431 $customer->phone,
432 $customer->email,
433 $customer->total_bookings_count,
434 $next_booking ? $next_booking->nice_start_datetime : 'n/a',
435 $customer->formatted_created_date(),
436 ];
437 $values_row = apply_filters( 'latepoint_customer_row_for_csv_export', $values_row, $customer, $this->params );
438 $customers_data[] = $values_row;
439 }
440 }
441 $customers_data = apply_filters( 'latepoint_customers_data_for_csv_export', $customers_data, $this->params );
442 OsCSVHelper::array_to_csv( $customers_data );
443
444 return;
445 }
446
447 $customers->where( $query_args )->filter_allowed_records();
448 $count_total_customers = clone $customers;
449
450 $total_customers = $count_total_customers->count();
451 $total_pages = ceil( $total_customers / $per_page );
452
453
454 $this->vars['customers_violating_auth_rules'] = OsAuthHelper::count_total_customers_violating_auth_rules();
455
456 $this->vars['customers'] = $customers->set_limit( $per_page )->set_offset( $offset )->order_by( 'id desc' )->get_results_as_models();
457 $this->vars['total_customers'] = $total_customers;
458
459 $this->vars['total_pages'] = ceil( $total_customers / $per_page );
460 $this->vars['per_page'] = $per_page;
461 $this->vars['current_page_number'] = $page_number;
462
463 $this->vars['showing_from'] = ( ( $page_number - 1 ) * $per_page ) ? ( ( $page_number - 1 ) * $per_page ) : 1;
464 $this->vars['showing_to'] = min( $page_number * $per_page, $this->vars['total_customers'] );
465
466 $this->format_render(
467 [
468 'json_view_name' => '_table_body',
469 'html_view_name' => __FUNCTION__,
470 ],
471 [],
472 [
473 'total_pages' => $total_pages,
474 'showing_from' => $this->vars['showing_from'],
475 'showing_to' => $this->vars['showing_to'],
476 'total_records' => $total_customers,
477 ]
478 );
479 }
480
481
482 public function import_csv_modal() {
483 $current_step = $this->params['step'] ?? 'upload_csv';
484 $steps = [
485 'upload_csv' => [ 'next_step' => 'mapping' ],
486 'mapping' => [ 'next_step' => 'importing' ],
487 ];
488 $this->vars['current_step'] = $current_step;
489 $this->vars['next_step'] = array_key_exists( $current_step, $steps ) ? $steps[ $current_step ]['next_step'] : 'upload_csv';
490 $this->format_render( __FUNCTION__ );
491 }
492
493 public function import_load_step() {
494 $this->check_nonce( 'import_customers_csv' );
495 $current_step = $this->params['step'] ?? 'upload_csv';
496 $status = LATEPOINT_STATUS_SUCCESS;
497
498 try {
499 switch ( $current_step ) {
500 case 'upload_csv':
501 $response_html = $this->_handle_upload_step();
502 break;
503
504 case 'mapping':
505 $response_html = $this->_handle_mapping_step();
506 break;
507
508 case 'confirmation':
509 $response_html = $this->_handleConfirmationStep();
510 break;
511
512 default:
513 throw new Exception( 'Invalid step provided' );
514 }
515 } catch ( Exception $e ) {
516 $response_html = $e->getMessage();
517 $status = LATEPOINT_STATUS_ERROR;
518 }
519
520 $this->send_json(
521 array(
522 'status' => $status,
523 'message' => $response_html,
524 )
525 );
526 }
527
528 private function _handle_upload_step(): string {
529 $file_path = OsCSVHelper::upload_csv_file( $this->files, 'latepoint_customers_csv' );
530 $csv_data = OsCSVHelper::get_csv_data( $file_path, 1 );
531
532 return $this->render(
533 $this->views_folder . 'import_steps/step_mapping.php',
534 'none',
535 [
536 'csv_data' => $csv_data,
537 ]
538 );
539 }
540
541 private function _handle_mapping_step(): string {
542 $columnMapping = $this->params['latepoint_column_mapping'] ?? [];
543
544 if ( ! OsCustomerImportHelper::validate_import_mapping( $columnMapping ) ) {
545 throw new Exception( esc_html__( 'Email field is required', 'latepoint' ) );
546 }
547
548 $csv_data = OsCSVHelper::get_csv_data( OsCustomerImportHelper::get_import_tmp_filepath() );
549 $validation_result = OsCustomerImportHelper::validate_csv_data( $csv_data, $columnMapping );
550
551 return $this->render(
552 $this->views_folder . 'import_steps/step_confirmation.php',
553 'none',
554 [
555 'conflicts' => $validation_result['conflicts'],
556 'can_be_imported' => $validation_result['importable_count'],
557 'latepoint_column_mapping' => $columnMapping,
558 ]
559 );
560 }
561
562
563 private function _handleConfirmationStep(): string {
564 $update_existing_customers = ! empty( $this->params['latepoint_update_customers_acknowledgement'] ) && OsUtilHelper::is_on( $this->params['latepoint_update_customers_acknowledgement'] );
565 $column_mapping = ! empty( $this->params['latepoint_column_mapping'] ) ? json_decode( $this->params['latepoint_column_mapping'], true ) : [];
566
567 $file_path = OsCustomerImportHelper::get_import_tmp_filepath();
568 $csv_data = OsCSVHelper::get_csv_data( $file_path );
569
570 $importResult = OsCustomerImportHelper::import_customers( $csv_data, $column_mapping, $update_existing_customers );
571
572 // Cleanup after successful import
573 OsCustomerImportHelper::cleanup_stored_file();
574
575 return $this->render(
576 $this->views_folder . 'import_steps/step_done.php',
577 'none',
578 [
579 'skipped_records' => $importResult['skipped_count'],
580 'updated_records' => $importResult['updated_count'],
581 ]
582 );
583 }
584 }
585
586
587 endif;
588