← All changes
|
includes/core/rest-api/Controllers/DashboardController.php
+201
-17
3.10.9
→
3.13.3
View file →
| @@ -16,8 +16,19 @@ | ||
| 16 | 16 | class DashboardController extends Wpfnl_REST_Controller |
| 17 | 17 | { |
| 18 | 18 | |
| 19 | 19 | /** |
| 20 | + * Transient holding the last known answer to "is the onboarding checklist | |
| 21 | + * on show?", read by the admin page so it can decide whether to hold space | |
| 22 | + * for it while the real status loads. | |
| 23 | + * | |
| 24 | + * @var string | |
| 25 | + * @since 3.13.3 | |
| 26 | + */ | |
| 27 | + const ONBOARDING_VISIBILITY_KEY = 'wpfnl_onboarding_should_show'; | |
| 28 | + | |
| 29 | + | |
| 30 | + /** | |
| 20 | 31 | * Endpoint namespace. |
| 21 | 32 | * |
| 22 | 33 | * @var string |
| 23 | 34 | * @since 3.2.0 |
| @@ -131,8 +142,20 @@ | ||
| 131 | 142 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), |
| 132 | 143 | ), |
| 133 | 144 | ) |
| 134 | 145 | ); |
| 146 | + | |
| 147 | + register_rest_route( | |
| 148 | + $this->namespace, | |
| 149 | + '/' . $this->rest_base . '/sync-cache', | |
| 150 | + array( | |
| 151 | + array( | |
| 152 | + 'methods' => \WP_REST_Server::CREATABLE, | |
| 153 | + 'callback' => array( $this, 'sync_cache' ), | |
| 154 | + 'permission_callback' => array( $this, 'get_items_permissions_check' ), | |
| 155 | + ), | |
| 156 | + ) | |
| 157 | + ); | |
| 135 | 158 | } |
| 136 | 159 | |
| 137 | 160 | /** |
| 138 | 161 | * Get stats arguments |
| @@ -169,10 +192,51 @@ | ||
| 169 | 192 | } |
| 170 | 193 | |
| 171 | 194 | |
| 172 | 195 | /** |
| 173 | - * Get overview data of funnels | |
| 196 | + * Cache payload version. Bump whenever the shape or the meaning of a cached | |
| 197 | + * report response changes, so entries written by an older build are ignored | |
| 198 | + * instead of served until they expire. | |
| 174 | 199 | * |
| 200 | + * @var string | |
| 201 | + * @since 3.13.0 | |
| 202 | + */ | |
| 203 | + const CACHE_VERSION = 'v2'; | |
| 204 | + | |
| 205 | + | |
| 206 | + /** | |
| 207 | + * Build a cache key from a prefix and parameter array. | |
| 208 | + * | |
| 209 | + * @param string $prefix | |
| 210 | + * @param array $params | |
| 211 | + * @return string | |
| 212 | + * | |
| 213 | + * @since 3.9.6 | |
| 214 | + */ | |
| 215 | + protected function get_cache_key( $prefix, $params ) { | |
| 216 | + return 'wpfnl_dash_' . $prefix . '_' . self::CACHE_VERSION . '_' . md5( serialize( $params ) ); | |
| 217 | + } | |
| 218 | + | |
| 219 | + | |
| 220 | + /** | |
| 221 | + * Delete all dashboard transients from the database. | |
| 222 | + * Called by the sync-cache endpoint or when cache must be invalidated. | |
| 223 | + * | |
| 224 | + * @since 3.9.6 | |
| 225 | + */ | |
| 226 | + public static function delete_dashboard_cache() { | |
| 227 | + global $wpdb; | |
| 228 | + $wpdb->query( | |
| 229 | + "DELETE FROM {$wpdb->options} | |
| 230 | + WHERE option_name LIKE '_transient_wpfnl_dash_%' | |
| 231 | + OR option_name LIKE '_transient_timeout_wpfnl_dash_%'" | |
| 232 | + ); | |
| 233 | + } | |
| 234 | + | |
| 235 | + | |
| 236 | + /** | |
| 237 | + * Get overview data of funnels (with 24-hour server-side cache). | |
| 238 | + * | |
| 175 | 239 | * @param \WP_REST_Request $request |
| 176 | 240 | * @return \WP_Error|\WP_REST_Response |
| 177 | 241 | * @throws \Exception |
| 178 | 242 | * |
| @@ -179,18 +243,27 @@ | ||
| 179 | 243 | * @since 3.2.0 |
| 180 | 244 | */ |
| 181 | 245 | public function get_overview(\WP_REST_Request $request) |
| 182 | 246 | { |
| 183 | - $params = $request->get_params(); | |
| 184 | - $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); | |
| 185 | - $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); | |
| 186 | - $response = ReportGenerator::get_overview($start_date, $end_date); | |
| 187 | - return rest_ensure_response($response); | |
| 247 | + $params = $request->get_params(); | |
| 248 | + $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); | |
| 249 | + $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); | |
| 250 | + | |
| 251 | + $cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) ); | |
| 252 | + $cached = get_transient( $cache_key ); | |
| 253 | + if ( false !== $cached ) { | |
| 254 | + return rest_ensure_response( $cached ); | |
| 255 | + } | |
| 256 | + | |
| 257 | + $response = ReportGenerator::get_overview( $start_date, $end_date ); | |
| 258 | + set_transient( $cache_key, $response, DAY_IN_SECONDS ); | |
| 259 | + | |
| 260 | + return rest_ensure_response( $response ); | |
| 188 | 261 | } |
| 189 | 262 | |
| 190 | 263 | |
| 191 | 264 | /** |
| 192 | - * Get stats of the funnels with intervals period | |
| 265 | + * Get stats of the funnels with intervals period (with 24-hour server-side cache). | |
| 193 | 266 | * |
| 194 | 267 | * @param \WP_REST_Request $request |
| 195 | 268 | * @return \WP_Error|\WP_REST_Response |
| 196 | 269 | * @throws \Exception |
| @@ -198,14 +271,23 @@ | ||
| 198 | 271 | * @since 3.2.0 |
| 199 | 272 | */ |
| 200 | 273 | public function get_stats(\WP_REST_Request $request) |
| 201 | 274 | { |
| 202 | - $params = $request->get_params(); | |
| 203 | - $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); | |
| 204 | - $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); | |
| 205 | - $interval = isset($params['interval']) ? $params['interval'] : 'day'; | |
| 206 | - $response = ReportGenerator::get_stats($start_date, $end_date, $interval); | |
| 207 | - return rest_ensure_response($response); | |
| 275 | + $params = $request->get_params(); | |
| 276 | + $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); | |
| 277 | + $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); | |
| 278 | + $interval = isset($params['interval']) ? $params['interval'] : 'day'; | |
| 279 | + | |
| 280 | + $cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) ); | |
| 281 | + $cached = get_transient( $cache_key ); | |
| 282 | + if ( false !== $cached ) { | |
| 283 | + return rest_ensure_response( $cached ); | |
| 284 | + } | |
| 285 | + | |
| 286 | + $response = ReportGenerator::get_stats( $start_date, $end_date, $interval ); | |
| 287 | + set_transient( $cache_key, $response, DAY_IN_SECONDS ); | |
| 288 | + | |
| 289 | + return rest_ensure_response( $response ); | |
| 208 | 290 | } |
| 209 | 291 | |
| 210 | 292 | |
| 211 | 293 | /** |
| @@ -217,15 +299,42 @@ | ||
| 217 | 299 | * @since 3.2.0 |
| 218 | 300 | */ |
| 219 | 301 | public function get_top_funnels(\WP_REST_Request $request) |
| 220 | 302 | { |
| 303 | + $params = $request->get_params(); | |
| 304 | + $start_date = isset( $params['after'] ) ? $params['after'] : $this->default_after()->format( 'Y-m-d H:i:s' ); | |
| 305 | + $end_date = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' ); | |
| 306 | + | |
| 221 | 307 | $response['status'] = true; |
| 222 | - $response['data'] = ReportGenerator::get_top_funnels(); | |
| 223 | - return rest_ensure_response($response); | |
| 308 | + $response['data'] = ReportGenerator::get_top_funnels( $start_date, $end_date ); | |
| 309 | + return rest_ensure_response( $response ); | |
| 224 | 310 | } |
| 225 | 311 | |
| 226 | 312 | |
| 227 | 313 | /** |
| 314 | + * Remember whether the checklist was on show, for the next page load. | |
| 315 | + * | |
| 316 | + * Working this out costs several queries, so the admin page cannot ask | |
| 317 | + * before it prints — but it can read the last answer. The React dashboard | |
| 318 | + * uses it for one thing: whether to hold space for the checklist while the | |
| 319 | + * real status loads. Someone who is done with onboarding then sees no | |
| 320 | + * placeholder at all, instead of a skeleton that resolves to nothing. | |
| 321 | + * | |
| 322 | + * Short lived on purpose: it only has to outlive the gap between one page | |
| 323 | + * load and the next, and a stale answer costs at most one wrong placeholder. | |
| 324 | + * | |
| 325 | + * @param bool $is_visible Whether the checklist is on show. | |
| 326 | + * | |
| 327 | + * @return void | |
| 328 | + * @since 3.13.3 | |
| 329 | + */ | |
| 330 | + private function remember_onboarding_visibility( $is_visible ) | |
| 331 | + { | |
| 332 | + set_transient( self::ONBOARDING_VISIBILITY_KEY, $is_visible ? 'yes' : 'no', 15 * MINUTE_IN_SECONDS ); | |
| 333 | + } | |
| 334 | + | |
| 335 | + | |
| 336 | + /** | |
| 228 | 337 | * Get onboarding status. |
| 229 | 338 | * |
| 230 | 339 | * Determines whether the onboarding section should be shown on the dashboard. |
| 231 | 340 | * The onboarding is hidden when ALL three conditions are met: |
| @@ -242,8 +351,10 @@ | ||
| 242 | 351 | { |
| 243 | 352 | // ── Check if onboarding has been dismissed ── |
| 244 | 353 | $is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' ); |
| 245 | 354 | if ( $is_dismissed ) { |
| 355 | + $this->remember_onboarding_visibility( false ); | |
| 356 | + | |
| 246 | 357 | return rest_ensure_response( array( |
| 247 | 358 | 'success' => true, |
| 248 | 359 | 'show_onboarding' => false, |
| 249 | 360 | 'steps' => array(), |
| @@ -363,8 +474,61 @@ | ||
| 363 | 474 | } |
| 364 | 475 | } |
| 365 | 476 | } |
| 366 | 477 | |
| 478 | + // ── Build the checklist deep links ── | |
| 479 | + // The checklist actions should drop the user into the place where the step | |
| 480 | + // actually gets done, rather than the funnel list: the order bump tab of the | |
| 481 | + // newest funnel's checkout step, and the canvas of that funnel for the upsell. | |
| 482 | + // Drafts count too — a funnel does not need to be live to be worked on. | |
| 483 | + $order_bump_url = ''; | |
| 484 | + $funnel_builder_url = ''; | |
| 485 | + $recent_funnels = get_posts(array( | |
| 486 | + 'post_type' => WPFNL_FUNNELS_POST_TYPE, | |
| 487 | + 'post_status' => array('publish', 'draft'), | |
| 488 | + 'numberposts' => 10, | |
| 489 | + 'orderby' => 'date', | |
| 490 | + 'order' => 'DESC', | |
| 491 | + 'fields' => 'ids', | |
| 492 | + 'meta_query' => $exclude_sc_meta, | |
| 493 | + )); | |
| 494 | + | |
| 495 | + if (!empty($recent_funnels)) { | |
| 496 | + // The upsell step is added on the canvas, so that is where its action goes. | |
| 497 | + $funnel_builder_url = admin_url( | |
| 498 | + sprintf('admin.php?page=edit_funnel&id=%d', $recent_funnels[0]) | |
| 499 | + ); | |
| 500 | + } | |
| 501 | + | |
| 502 | + foreach ($recent_funnels as $recent_funnel_id) { | |
| 503 | + $recent_steps = Wpfnl_functions::get_steps($recent_funnel_id); | |
| 504 | + | |
| 505 | + if (!is_array($recent_steps) || empty($recent_steps)) { | |
| 506 | + continue; | |
| 507 | + } | |
| 508 | + | |
| 509 | + foreach ($recent_steps as $recent_step) { | |
| 510 | + $recent_step_type = isset($recent_step['step_type']) ? $recent_step['step_type'] : ''; | |
| 511 | + $recent_step_id = isset($recent_step['id']) ? (int)$recent_step['id'] : 0; | |
| 512 | + | |
| 513 | + if ('checkout' !== $recent_step_type || !$recent_step_id) { | |
| 514 | + continue; | |
| 515 | + } | |
| 516 | + | |
| 517 | + // The hash is what tells the canvas which step settings drawer and | |
| 518 | + // tab to open once the funnel editor has finished loading. | |
| 519 | + $order_bump_url = admin_url( | |
| 520 | + sprintf( | |
| 521 | + 'admin.php?page=edit_funnel&id=%d&step_id=%d#%d/settings/order-bump/', | |
| 522 | + $recent_funnel_id, | |
| 523 | + $recent_step_id, | |
| 524 | + $recent_step_id | |
| 525 | + ) | |
| 526 | + ); | |
| 527 | + break 2; | |
| 528 | + } | |
| 529 | + } | |
| 530 | + | |
| 367 | 531 | // ── Step 3: Check for real orders through funnels ── |
| 368 | 532 | if ($is_wc_active && !empty($funnels)) { |
| 369 | 533 | global $wpdb; |
| 370 | 534 | $stats_table = $wpdb->prefix . 'wpfnl_stats'; |
| @@ -388,11 +552,10 @@ | ||
| 388 | 552 | |
| 389 | 553 | // ── Build checklist step completion data ── |
| 390 | 554 | $completed_count = 0; |
| 391 | 555 | $steps_status = array( |
| 392 | - 'woocommerce_connected' => $is_wc_active, | |
| 556 | + 'store_checkout_created' => $has_store_checkout_created, | |
| 393 | 557 | 'funnel_created' => $has_funnel_created, |
| 394 | - 'store_checkout_created' => $has_store_checkout_created, | |
| 395 | 558 | 'order_bump_added' => $has_order_bump, |
| 396 | 559 | 'upsell_added' => $has_upsell, |
| 397 | 560 | 'test_order_placed' => $has_real_order, |
| 398 | 561 | 'funnel_live' => $has_live_funnel, |
| @@ -406,8 +569,10 @@ | ||
| 406 | 569 | |
| 407 | 570 | $total_steps = count($steps_status); |
| 408 | 571 | $progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0; |
| 409 | 572 | |
| 573 | + $this->remember_onboarding_visibility( !$should_hide_onboarding ); | |
| 574 | + | |
| 410 | 575 | return rest_ensure_response(array( |
| 411 | 576 | 'success' => true, |
| 412 | 577 | 'show_onboarding' => !$should_hide_onboarding, |
| 413 | 578 | 'steps' => $steps_status, |
| @@ -413,8 +578,10 @@ | ||
| 413 | 578 | 'steps' => $steps_status, |
| 414 | 579 | 'completed_count' => $completed_count, |
| 415 | 580 | 'total_steps' => $total_steps, |
| 416 | 581 | 'progress_percent' => $progress_percent, |
| 582 | + 'order_bump_url' => $order_bump_url, | |
| 583 | + 'funnel_builder_url' => $funnel_builder_url, | |
| 417 | 584 | )); |
| 418 | 585 | } |
| 419 | 586 | |
| 420 | 587 | /** |
| @@ -445,8 +612,25 @@ | ||
| 445 | 612 | 'has_funnels' => (int) $funnel_count > 0, |
| 446 | 613 | 'funnel_count' => (int) $funnel_count |
| 447 | 614 | ) ); |
| 448 | 615 | } |
| 616 | + | |
| 617 | + /** | |
| 618 | + * Clear all dashboard transient caches so the next request fetches fresh data. | |
| 619 | + * | |
| 620 | + * @param \WP_REST_Request $request | |
| 621 | + * @return \WP_REST_Response | |
| 622 | + * | |
| 623 | + * @since 3.9.6 | |
| 624 | + */ | |
| 625 | + public function sync_cache( \WP_REST_Request $request ) { | |
| 626 | + self::delete_dashboard_cache(); | |
| 627 | + return rest_ensure_response( array( | |
| 628 | + 'success' => true, | |
| 629 | + 'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ), | |
| 630 | + ) ); | |
| 631 | + } | |
| 632 | + | |
| 449 | 633 | |
| 450 | 634 | /** |
| 451 | 635 | * Dismiss the onboarding checklist |
| 452 | 636 | * |