| 1 |
<?php |
| 2 |
|
| 3 |
|
| 4 |
namespace WPFunnels\Rest\Controllers; |
| 5 |
|
| 6 |
use WPFunnels\Report\ReportGenerator; |
| 7 |
use WPFunnels\Wpfnl_functions; |
| 8 |
|
| 9 |
/** |
| 10 |
* |
| 11 |
* |
| 12 |
* Class DashboardController |
| 13 |
* @package WPFunnels\Rest\Controllers |
| 14 |
* @since 3.2.0 |
| 15 |
*/ |
| 16 |
class DashboardController extends Wpfnl_REST_Controller |
| 17 |
{ |
| 18 |
|
| 19 |
/** |
| 20 |
* Endpoint namespace. |
| 21 |
* |
| 22 |
* @var string |
| 23 |
* @since 3.2.0 |
| 24 |
*/ |
| 25 |
protected $namespace = 'wpfunnels/v1'; |
| 26 |
|
| 27 |
|
| 28 |
/** |
| 29 |
* Route base. |
| 30 |
* |
| 31 |
* @var string |
| 32 |
* @since 3.2.0 |
| 33 |
*/ |
| 34 |
protected $rest_base = 'report'; |
| 35 |
|
| 36 |
|
| 37 |
/** |
| 38 |
* Makes sure the current user has access to READ the settings APIs. |
| 39 |
* |
| 40 |
* |
| 41 |
* @param $request |
| 42 |
* @return \WP_Error|boolean |
| 43 |
* @since 3.2.0 |
| 44 |
*/ |
| 45 |
public function get_items_permissions_check($request) |
| 46 |
{ |
| 47 |
if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('settings')) { |
| 48 |
return new \WP_Error('wpfunnels_rest_cannot_edit', __('Sorry, you cannot list resources.', 'wpfnl'), array('status' => rest_authorization_required_code())); |
| 49 |
} |
| 50 |
return true; |
| 51 |
} |
| 52 |
|
| 53 |
|
| 54 |
/** |
| 55 |
* Register rest routes |
| 56 |
* |
| 57 |
* @since 3.2.0 |
| 58 |
*/ |
| 59 |
public function register_routes() |
| 60 |
{ |
| 61 |
register_rest_route( |
| 62 |
$this->namespace, |
| 63 |
'/' . $this->rest_base . '/overview', |
| 64 |
array( |
| 65 |
array( |
| 66 |
'methods' => \WP_REST_Server::READABLE, |
| 67 |
'args' => $this->get_stats_args(), |
| 68 |
'callback' => array($this, 'get_overview'), |
| 69 |
'permission_callback' => array($this, 'get_items_permissions_check'), |
| 70 |
), |
| 71 |
) |
| 72 |
); |
| 73 |
|
| 74 |
register_rest_route( |
| 75 |
$this->namespace, |
| 76 |
'/' . $this->rest_base . '/stats', |
| 77 |
array( |
| 78 |
array( |
| 79 |
'methods' => \WP_REST_Server::READABLE, |
| 80 |
'args' => $this->get_stats_args(), |
| 81 |
'callback' => array($this, 'get_stats'), |
| 82 |
'permission_callback' => array($this, 'get_items_permissions_check'), |
| 83 |
), |
| 84 |
) |
| 85 |
); |
| 86 |
|
| 87 |
register_rest_route( |
| 88 |
$this->namespace, |
| 89 |
'/' . $this->rest_base . '/top-funnels', |
| 90 |
array( |
| 91 |
array( |
| 92 |
'methods' => \WP_REST_Server::READABLE, |
| 93 |
'args' => $this->get_stats_args(), |
| 94 |
'callback' => array($this, 'get_top_funnels'), |
| 95 |
'permission_callback' => array($this, 'get_items_permissions_check'), |
| 96 |
), |
| 97 |
) |
| 98 |
); |
| 99 |
|
| 100 |
register_rest_route( |
| 101 |
$this->namespace, |
| 102 |
'/' . $this->rest_base . '/onboarding-status', |
| 103 |
array( |
| 104 |
array( |
| 105 |
'methods' => \WP_REST_Server::READABLE, |
| 106 |
'callback' => array($this, 'get_onboarding_status'), |
| 107 |
'permission_callback' => array($this, 'get_items_permissions_check'), |
| 108 |
), |
| 109 |
) |
| 110 |
); |
| 111 |
|
| 112 |
register_rest_route( |
| 113 |
$this->namespace, |
| 114 |
'/' . $this->rest_base . '/has-funnels', |
| 115 |
array( |
| 116 |
array( |
| 117 |
'methods' => \WP_REST_Server::READABLE, |
| 118 |
'callback' => array($this, 'check_has_funnels'), |
| 119 |
'permission_callback' => array( $this, 'get_items_permissions_check' ), |
| 120 |
), |
| 121 |
) |
| 122 |
); |
| 123 |
|
| 124 |
register_rest_route( |
| 125 |
$this->namespace, |
| 126 |
'/' . $this->rest_base . '/dismiss-onboarding', |
| 127 |
array( |
| 128 |
array( |
| 129 |
'methods' => \WP_REST_Server::CREATABLE, |
| 130 |
'callback' => array($this, 'dismiss_onboarding'), |
| 131 |
'permission_callback' => array( $this, 'get_items_permissions_check' ), |
| 132 |
), |
| 133 |
) |
| 134 |
); |
| 135 |
|
| 136 |
register_rest_route( |
| 137 |
$this->namespace, |
| 138 |
'/' . $this->rest_base . '/sync-cache', |
| 139 |
array( |
| 140 |
array( |
| 141 |
'methods' => \WP_REST_Server::CREATABLE, |
| 142 |
'callback' => array( $this, 'sync_cache' ), |
| 143 |
'permission_callback' => array( $this, 'get_items_permissions_check' ), |
| 144 |
), |
| 145 |
) |
| 146 |
); |
| 147 |
} |
| 148 |
|
| 149 |
/** |
| 150 |
* Get stats arguments |
| 151 |
* |
| 152 |
* @return mixed|void |
| 153 |
* @since 3.2.0 |
| 154 |
*/ |
| 155 |
public function get_stats_args() |
| 156 |
{ |
| 157 |
return array( |
| 158 |
'after' => array( |
| 159 |
'type' => 'string', |
| 160 |
'format' => 'date-time', |
| 161 |
'validate_callback' => 'rest_validate_request_arg', |
| 162 |
), |
| 163 |
'before' => array( |
| 164 |
'type' => 'string', |
| 165 |
'format' => 'date-time', |
| 166 |
'validate_callback' => 'rest_validate_request_arg', |
| 167 |
), |
| 168 |
'interval' => array( |
| 169 |
'type' => 'string', |
| 170 |
'default' => 'week', |
| 171 |
'enum' => array( |
| 172 |
'hour', |
| 173 |
'day', |
| 174 |
'week', |
| 175 |
'month', |
| 176 |
'quarter', |
| 177 |
'year', |
| 178 |
), |
| 179 |
), |
| 180 |
); |
| 181 |
} |
| 182 |
|
| 183 |
|
| 184 |
/** |
| 185 |
* Cache payload version. Bump whenever the shape or the meaning of a cached |
| 186 |
* report response changes, so entries written by an older build are ignored |
| 187 |
* instead of served until they expire. |
| 188 |
* |
| 189 |
* @var string |
| 190 |
* @since 3.13.0 |
| 191 |
*/ |
| 192 |
const CACHE_VERSION = 'v2'; |
| 193 |
|
| 194 |
|
| 195 |
/** |
| 196 |
* Build a cache key from a prefix and parameter array. |
| 197 |
* |
| 198 |
* @param string $prefix |
| 199 |
* @param array $params |
| 200 |
* @return string |
| 201 |
* |
| 202 |
* @since 3.9.6 |
| 203 |
*/ |
| 204 |
protected function get_cache_key( $prefix, $params ) { |
| 205 |
return 'wpfnl_dash_' . $prefix . '_' . self::CACHE_VERSION . '_' . md5( serialize( $params ) ); |
| 206 |
} |
| 207 |
|
| 208 |
|
| 209 |
/** |
| 210 |
* Delete all dashboard transients from the database. |
| 211 |
* Called by the sync-cache endpoint or when cache must be invalidated. |
| 212 |
* |
| 213 |
* @since 3.9.6 |
| 214 |
*/ |
| 215 |
public static function delete_dashboard_cache() { |
| 216 |
global $wpdb; |
| 217 |
$wpdb->query( |
| 218 |
"DELETE FROM {$wpdb->options} |
| 219 |
WHERE option_name LIKE '_transient_wpfnl_dash_%' |
| 220 |
OR option_name LIKE '_transient_timeout_wpfnl_dash_%'" |
| 221 |
); |
| 222 |
} |
| 223 |
|
| 224 |
|
| 225 |
/** |
| 226 |
* Get overview data of funnels (with 24-hour server-side cache). |
| 227 |
* |
| 228 |
* @param \WP_REST_Request $request |
| 229 |
* @return \WP_Error|\WP_REST_Response |
| 230 |
* @throws \Exception |
| 231 |
* |
| 232 |
* @since 3.2.0 |
| 233 |
*/ |
| 234 |
public function get_overview(\WP_REST_Request $request) |
| 235 |
{ |
| 236 |
$params = $request->get_params(); |
| 237 |
$start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); |
| 238 |
$end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); |
| 239 |
|
| 240 |
$cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) ); |
| 241 |
$cached = get_transient( $cache_key ); |
| 242 |
if ( false !== $cached ) { |
| 243 |
return rest_ensure_response( $cached ); |
| 244 |
} |
| 245 |
|
| 246 |
$response = ReportGenerator::get_overview( $start_date, $end_date ); |
| 247 |
set_transient( $cache_key, $response, DAY_IN_SECONDS ); |
| 248 |
|
| 249 |
return rest_ensure_response( $response ); |
| 250 |
} |
| 251 |
|
| 252 |
|
| 253 |
/** |
| 254 |
* Get stats of the funnels with intervals period (with 24-hour server-side cache). |
| 255 |
* |
| 256 |
* @param \WP_REST_Request $request |
| 257 |
* @return \WP_Error|\WP_REST_Response |
| 258 |
* @throws \Exception |
| 259 |
* |
| 260 |
* @since 3.2.0 |
| 261 |
*/ |
| 262 |
public function get_stats(\WP_REST_Request $request) |
| 263 |
{ |
| 264 |
$params = $request->get_params(); |
| 265 |
$start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); |
| 266 |
$end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); |
| 267 |
$interval = isset($params['interval']) ? $params['interval'] : 'day'; |
| 268 |
|
| 269 |
$cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) ); |
| 270 |
$cached = get_transient( $cache_key ); |
| 271 |
if ( false !== $cached ) { |
| 272 |
return rest_ensure_response( $cached ); |
| 273 |
} |
| 274 |
|
| 275 |
$response = ReportGenerator::get_stats( $start_date, $end_date, $interval ); |
| 276 |
set_transient( $cache_key, $response, DAY_IN_SECONDS ); |
| 277 |
|
| 278 |
return rest_ensure_response( $response ); |
| 279 |
} |
| 280 |
|
| 281 |
|
| 282 |
/** |
| 283 |
* Get 3 top performing funnels |
| 284 |
* |
| 285 |
* @param \WP_REST_Request $request |
| 286 |
* @return \WP_Error|\WP_REST_Response |
| 287 |
* |
| 288 |
* @since 3.2.0 |
| 289 |
*/ |
| 290 |
public function get_top_funnels(\WP_REST_Request $request) |
| 291 |
{ |
| 292 |
$params = $request->get_params(); |
| 293 |
$start_date = isset( $params['after'] ) ? $params['after'] : $this->default_after()->format( 'Y-m-d H:i:s' ); |
| 294 |
$end_date = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' ); |
| 295 |
|
| 296 |
$response['status'] = true; |
| 297 |
$response['data'] = ReportGenerator::get_top_funnels( $start_date, $end_date ); |
| 298 |
return rest_ensure_response( $response ); |
| 299 |
} |
| 300 |
|
| 301 |
|
| 302 |
/** |
| 303 |
* Get onboarding status. |
| 304 |
* |
| 305 |
* Determines whether the onboarding section should be shown on the dashboard. |
| 306 |
* The onboarding is hidden when ALL three conditions are met: |
| 307 |
* 1. At least one funnel is published (live) |
| 308 |
* 2. At least one funnel contains an order bump (upsell is optional/bonus) |
| 309 |
* 3. At least one real order has been placed through any funnel |
| 310 |
* |
| 311 |
* Also returns individual step completion states for the checklist. |
| 312 |
* |
| 313 |
* @return \WP_REST_Response |
| 314 |
* @since 3.9.5 |
| 315 |
*/ |
| 316 |
public function get_onboarding_status() |
| 317 |
{ |
| 318 |
// ── Check if onboarding has been dismissed ── |
| 319 |
$is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' ); |
| 320 |
if ( $is_dismissed ) { |
| 321 |
return rest_ensure_response( array( |
| 322 |
'success' => true, |
| 323 |
'show_onboarding' => false, |
| 324 |
'steps' => array(), |
| 325 |
'completed_count' => 0, |
| 326 |
'total_steps' => 0, |
| 327 |
'progress_percent' => 0, |
| 328 |
) ); |
| 329 |
} |
| 330 |
|
| 331 |
$is_wc_active = class_exists('WooCommerce'); |
| 332 |
$has_live_funnel = false; |
| 333 |
$has_order_bump = false; |
| 334 |
$has_upsell = false; |
| 335 |
$has_real_order = false; |
| 336 |
$has_funnel_created = false; |
| 337 |
$has_store_checkout_created = false; |
| 338 |
|
| 339 |
// Meta query to exclude store checkout funnels |
| 340 |
$exclude_sc_meta = array( |
| 341 |
'relation' => 'OR', |
| 342 |
array( |
| 343 |
'key' => '_wpfnl_funnel_type', |
| 344 |
'compare' => 'NOT EXISTS', |
| 345 |
), |
| 346 |
array( |
| 347 |
'key' => '_wpfnl_funnel_type', |
| 348 |
'value' => 'store_checkout', |
| 349 |
'compare' => '!=', |
| 350 |
), |
| 351 |
); |
| 352 |
|
| 353 |
// ── Step 1: Check for published funnels (excluding store checkouts) ── |
| 354 |
$funnels = get_posts(array( |
| 355 |
'post_type' => WPFNL_FUNNELS_POST_TYPE, |
| 356 |
'post_status' => 'publish', |
| 357 |
'numberposts' => -1, |
| 358 |
'fields' => 'ids', |
| 359 |
'meta_query' => $exclude_sc_meta, |
| 360 |
)); |
| 361 |
|
| 362 |
if (!empty($funnels)) { |
| 363 |
$has_funnel_created = true; |
| 364 |
$has_live_funnel = true; |
| 365 |
} |
| 366 |
|
| 367 |
// Also check for draft funnels (counts as "created" but not "live") |
| 368 |
if (!$has_funnel_created) { |
| 369 |
$draft_funnels = get_posts(array( |
| 370 |
'post_type' => WPFNL_FUNNELS_POST_TYPE, |
| 371 |
'post_status' => array('publish', 'draft'), |
| 372 |
'numberposts' => 1, |
| 373 |
'fields' => 'ids', |
| 374 |
'meta_query' => $exclude_sc_meta, |
| 375 |
)); |
| 376 |
$has_funnel_created = !empty($draft_funnels); |
| 377 |
} |
| 378 |
|
| 379 |
// ── Check for store checkout funnels separately ── |
| 380 |
$sc_funnels = get_posts(array( |
| 381 |
'post_type' => WPFNL_FUNNELS_POST_TYPE, |
| 382 |
'post_status' => array('publish', 'draft'), |
| 383 |
'numberposts' => 1, |
| 384 |
'fields' => 'ids', |
| 385 |
'meta_query' => array( |
| 386 |
array( |
| 387 |
'key' => '_wpfnl_funnel_type', |
| 388 |
'value' => 'store_checkout', |
| 389 |
), |
| 390 |
), |
| 391 |
)); |
| 392 |
$has_store_checkout_created = !empty($sc_funnels); |
| 393 |
|
| 394 |
// ── Step 2: Check for order bump & upsell across all published funnels ── |
| 395 |
if (!empty($funnels)) { |
| 396 |
foreach ($funnels as $funnel_id) { |
| 397 |
$steps = Wpfnl_functions::get_steps($funnel_id); |
| 398 |
|
| 399 |
if (!is_array($steps) || empty($steps)) { |
| 400 |
continue; |
| 401 |
} |
| 402 |
|
| 403 |
foreach ($steps as $step) { |
| 404 |
$step_id = isset($step['id']) ? $step['id'] : 0; |
| 405 |
$step_type = isset($step['step_type']) ? $step['step_type'] : ''; |
| 406 |
|
| 407 |
// Check for order bump on checkout steps |
| 408 |
if ('checkout' === $step_type && !$has_order_bump) { |
| 409 |
$ob_settings = get_post_meta($step_id, 'order-bump-settings', true); |
| 410 |
if (!empty($ob_settings) && is_array($ob_settings)) { |
| 411 |
$is_multidimensional = Wpfnl_functions::check_array_is_multidimentional($ob_settings); |
| 412 |
if ($is_multidimensional) { |
| 413 |
foreach ($ob_settings as $ob) { |
| 414 |
if (!empty($ob['product']) && !empty($ob['isEnabled'])) { |
| 415 |
$has_order_bump = true; |
| 416 |
break; |
| 417 |
} |
| 418 |
} |
| 419 |
} |
| 420 |
else { |
| 421 |
// Single OB (legacy format) |
| 422 |
if (!empty($ob_settings['product'])) { |
| 423 |
$has_order_bump = true; |
| 424 |
} |
| 425 |
} |
| 426 |
} |
| 427 |
} |
| 428 |
|
| 429 |
// Check for upsell step |
| 430 |
if ('upsell' === $step_type) { |
| 431 |
$has_upsell = true; |
| 432 |
} |
| 433 |
} |
| 434 |
|
| 435 |
// If we already found both, no need to check more funnels |
| 436 |
if ($has_order_bump && $has_upsell) { |
| 437 |
break; |
| 438 |
} |
| 439 |
} |
| 440 |
} |
| 441 |
|
| 442 |
// ── Build the checklist deep links ── |
| 443 |
// The checklist actions should drop the user into the place where the step |
| 444 |
// actually gets done, rather than the funnel list: the order bump tab of the |
| 445 |
// newest funnel's checkout step, and the canvas of that funnel for the upsell. |
| 446 |
// Drafts count too — a funnel does not need to be live to be worked on. |
| 447 |
$order_bump_url = ''; |
| 448 |
$funnel_builder_url = ''; |
| 449 |
$recent_funnels = get_posts(array( |
| 450 |
'post_type' => WPFNL_FUNNELS_POST_TYPE, |
| 451 |
'post_status' => array('publish', 'draft'), |
| 452 |
'numberposts' => 10, |
| 453 |
'orderby' => 'date', |
| 454 |
'order' => 'DESC', |
| 455 |
'fields' => 'ids', |
| 456 |
'meta_query' => $exclude_sc_meta, |
| 457 |
)); |
| 458 |
|
| 459 |
if (!empty($recent_funnels)) { |
| 460 |
// The upsell step is added on the canvas, so that is where its action goes. |
| 461 |
$funnel_builder_url = admin_url( |
| 462 |
sprintf('admin.php?page=edit_funnel&id=%d', $recent_funnels[0]) |
| 463 |
); |
| 464 |
} |
| 465 |
|
| 466 |
foreach ($recent_funnels as $recent_funnel_id) { |
| 467 |
$recent_steps = Wpfnl_functions::get_steps($recent_funnel_id); |
| 468 |
|
| 469 |
if (!is_array($recent_steps) || empty($recent_steps)) { |
| 470 |
continue; |
| 471 |
} |
| 472 |
|
| 473 |
foreach ($recent_steps as $recent_step) { |
| 474 |
$recent_step_type = isset($recent_step['step_type']) ? $recent_step['step_type'] : ''; |
| 475 |
$recent_step_id = isset($recent_step['id']) ? (int)$recent_step['id'] : 0; |
| 476 |
|
| 477 |
if ('checkout' !== $recent_step_type || !$recent_step_id) { |
| 478 |
continue; |
| 479 |
} |
| 480 |
|
| 481 |
// The hash is what tells the canvas which step settings drawer and |
| 482 |
// tab to open once the funnel editor has finished loading. |
| 483 |
$order_bump_url = admin_url( |
| 484 |
sprintf( |
| 485 |
'admin.php?page=edit_funnel&id=%d&step_id=%d#%d/settings/order-bump/', |
| 486 |
$recent_funnel_id, |
| 487 |
$recent_step_id, |
| 488 |
$recent_step_id |
| 489 |
) |
| 490 |
); |
| 491 |
break 2; |
| 492 |
} |
| 493 |
} |
| 494 |
|
| 495 |
// ── Step 3: Check for real orders through funnels ── |
| 496 |
if ($is_wc_active && !empty($funnels)) { |
| 497 |
global $wpdb; |
| 498 |
$stats_table = $wpdb->prefix . 'wpfnl_stats'; |
| 499 |
|
| 500 |
// Check if stats table exists |
| 501 |
$table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table)); |
| 502 |
if ($table_exists) { |
| 503 |
$order_count = $wpdb->get_var( |
| 504 |
"SELECT COUNT(id) FROM {$stats_table} WHERE status IN ('completed', 'processing') LIMIT 1" |
| 505 |
); |
| 506 |
$has_real_order = (int)$order_count > 0; |
| 507 |
} |
| 508 |
} |
| 509 |
|
| 510 |
// ── Determine if onboarding should be shown ── |
| 511 |
// Hide onboarding only when ALL conditions are met: |
| 512 |
// - At least 1 live funnel |
| 513 |
// - Has an order bump (upsell is optional) |
| 514 |
// - Has at least 1 real order |
| 515 |
$should_hide_onboarding = $has_live_funnel && $has_order_bump && $has_real_order; |
| 516 |
|
| 517 |
// ── Build checklist step completion data ── |
| 518 |
$completed_count = 0; |
| 519 |
$steps_status = array( |
| 520 |
'store_checkout_created' => $has_store_checkout_created, |
| 521 |
'funnel_created' => $has_funnel_created, |
| 522 |
'order_bump_added' => $has_order_bump, |
| 523 |
'upsell_added' => $has_upsell, |
| 524 |
'test_order_placed' => $has_real_order, |
| 525 |
'funnel_live' => $has_live_funnel, |
| 526 |
); |
| 527 |
|
| 528 |
foreach ($steps_status as $status) { |
| 529 |
if ($status) { |
| 530 |
$completed_count++; |
| 531 |
} |
| 532 |
} |
| 533 |
|
| 534 |
$total_steps = count($steps_status); |
| 535 |
$progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0; |
| 536 |
|
| 537 |
return rest_ensure_response(array( |
| 538 |
'success' => true, |
| 539 |
'show_onboarding' => !$should_hide_onboarding, |
| 540 |
'steps' => $steps_status, |
| 541 |
'completed_count' => $completed_count, |
| 542 |
'total_steps' => $total_steps, |
| 543 |
'progress_percent' => $progress_percent, |
| 544 |
'order_bump_url' => $order_bump_url, |
| 545 |
'funnel_builder_url' => $funnel_builder_url, |
| 546 |
)); |
| 547 |
} |
| 548 |
|
| 549 |
/** |
| 550 |
* Check if there are any funnels |
| 551 |
* |
| 552 |
* @param \WP_REST_Request $request |
| 553 |
* @return \WP_REST_Response |
| 554 |
* |
| 555 |
* @since 3.9.5 |
| 556 |
*/ |
| 557 |
public function check_has_funnels( \WP_REST_Request $request ) { |
| 558 |
global $wpdb; |
| 559 |
|
| 560 |
// Count only non-store-checkout funnels |
| 561 |
$funnel_count = $wpdb->get_var( |
| 562 |
$wpdb->prepare( |
| 563 |
"SELECT COUNT(*) FROM {$wpdb->posts} p |
| 564 |
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wpfnl_funnel_type' |
| 565 |
WHERE p.post_type = %s |
| 566 |
AND p.post_status IN ('publish', 'draft') |
| 567 |
AND (pm.meta_value IS NULL OR pm.meta_value != 'store_checkout')", |
| 568 |
'wpfunnels' |
| 569 |
) |
| 570 |
); |
| 571 |
|
| 572 |
return rest_ensure_response( array( |
| 573 |
'success' => true, |
| 574 |
'has_funnels' => (int) $funnel_count > 0, |
| 575 |
'funnel_count' => (int) $funnel_count |
| 576 |
) ); |
| 577 |
} |
| 578 |
|
| 579 |
/** |
| 580 |
* Clear all dashboard transient caches so the next request fetches fresh data. |
| 581 |
* |
| 582 |
* @param \WP_REST_Request $request |
| 583 |
* @return \WP_REST_Response |
| 584 |
* |
| 585 |
* @since 3.9.6 |
| 586 |
*/ |
| 587 |
public function sync_cache( \WP_REST_Request $request ) { |
| 588 |
self::delete_dashboard_cache(); |
| 589 |
return rest_ensure_response( array( |
| 590 |
'success' => true, |
| 591 |
'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ), |
| 592 |
) ); |
| 593 |
} |
| 594 |
|
| 595 |
|
| 596 |
/** |
| 597 |
* Dismiss the onboarding checklist |
| 598 |
* |
| 599 |
* @param \WP_REST_Request $request |
| 600 |
* @return \WP_REST_Response |
| 601 |
* |
| 602 |
* @since 3.9.5 |
| 603 |
*/ |
| 604 |
public function dismiss_onboarding( \WP_REST_Request $request ) { |
| 605 |
// Store dismissal in a transient for 30 days |
| 606 |
set_transient( 'wpfnl_onboarding_dismissed', true, 30 * DAY_IN_SECONDS ); |
| 607 |
|
| 608 |
return rest_ensure_response( array( |
| 609 |
'success' => true, |
| 610 |
'message' => 'Onboarding dismissed successfully' |
| 611 |
) ); |
| 612 |
} |
| 613 |
} |
| 614 |
|