Changeset 3432071
- Timestamp:
- 01/04/2026 11:27:59 AM (3 months ago)
- Location:
- bipo-project-manager
- Files:
-
- 62 added
- 2 edited
-
assets/banner-1544x500.png (added)
-
assets/blueprints (added)
-
assets/blueprints/blueprint.json (added)
-
assets/icon-128x128.png (added)
-
assets/icon-256x256.png (added)
-
tags/1.0.15 (added)
-
tags/1.0.15/admin (added)
-
tags/1.0.15/admin-rtl.css (added)
-
tags/1.0.15/admin.asset.php (added)
-
tags/1.0.15/admin.css (added)
-
tags/1.0.15/admin.js (added)
-
tags/1.0.15/admin/assets (added)
-
tags/1.0.15/admin/assets/css (added)
-
tags/1.0.15/admin/assets/css/admin.css (added)
-
tags/1.0.15/admin/assets/css/deactivation.css (added)
-
tags/1.0.15/admin/assets/js (added)
-
tags/1.0.15/admin/assets/js/admin.js (added)
-
tags/1.0.15/admin/assets/js/deactivation.js (added)
-
tags/1.0.15/admin/class-admin-enhancements.php (added)
-
tags/1.0.15/admin/class-admin-menu.php (added)
-
tags/1.0.15/admin/class-admin-pages.php (added)
-
tags/1.0.15/assets (added)
-
tags/1.0.15/assets/admin-rtl.css (added)
-
tags/1.0.15/assets/admin.asset.php (added)
-
tags/1.0.15/assets/admin.css (added)
-
tags/1.0.15/assets/admin.js (added)
-
tags/1.0.15/bipo-project-manager.php (added)
-
tags/1.0.15/frontend (added)
-
tags/1.0.15/frontend/assets (added)
-
tags/1.0.15/frontend/assets/css (added)
-
tags/1.0.15/frontend/assets/css/style.css (added)
-
tags/1.0.15/frontend/assets/css/tailwind.css (added)
-
tags/1.0.15/frontend/assets/css/tailwind.min.css (added)
-
tags/1.0.15/frontend/assets/image (added)
-
tags/1.0.15/frontend/assets/image/wordpress.png (added)
-
tags/1.0.15/frontend/assets/js (added)
-
tags/1.0.15/frontend/assets/js/frontend.js (added)
-
tags/1.0.15/frontend/class-frontend-display.php (added)
-
tags/1.0.15/frontend/templates (added)
-
tags/1.0.15/frontend/templates/archive-projects.php (added)
-
tags/1.0.15/frontend/templates/components (added)
-
tags/1.0.15/frontend/templates/components/project-card.php (added)
-
tags/1.0.15/frontend/templates/single-project.php (added)
-
tags/1.0.15/includes (added)
-
tags/1.0.15/includes/class-applications.php (added)
-
tags/1.0.15/includes/class-enqueue-scripts.php (added)
-
tags/1.0.15/includes/class-submissions.php (added)
-
tags/1.0.15/includes/class-user-status-validator.php (added)
-
tags/1.0.15/readme.txt (added)
-
tags/1.0.15/templates (added)
-
trunk/admin-rtl.css (added)
-
trunk/admin.asset.php (added)
-
trunk/admin.css (added)
-
trunk/admin.js (added)
-
trunk/admin/assets/css/deactivation.css (added)
-
trunk/admin/assets/js/deactivation.js (added)
-
trunk/assets (added)
-
trunk/assets/admin-rtl.css (added)
-
trunk/assets/admin.asset.php (added)
-
trunk/assets/admin.css (added)
-
trunk/assets/admin.js (added)
-
trunk/bipo-project-manager.php (modified) (10 diffs)
-
trunk/frontend/assets/image/wordpress.png (modified) (previous)
-
trunk/templates (added)
Legend:
- Unmodified
- Added
- Removed
-
bipo-project-manager/trunk/bipo-project-manager.php
r3407053 r3432071 29 29 add_action('init', array($this, 'init')); 30 30 add_action('admin_init', array($this, 'flush_rules_on_permalink_save')); 31 add_action('admin_init', array($this, 'dearprma_activation_redirect')); 32 add_action('admin_menu', array($this, 'add_react_admin_page')); 33 add_action('admin_enqueue_scripts', array($this, 'enqueue_react_admin')); 34 add_action('admin_enqueue_scripts', array($this, 'enqueue_deactivation_scripts')); 31 35 add_filter('body_class', array($this, 'add_page_class_to_projects')); 36 37 // Setup wizard AJAX handlers 38 add_action('wp_ajax_dearprma_save_setup', array($this, 'ajax_save_setup')); 39 add_action('wp_ajax_dearprma_deactivate', array($this, 'ajax_deactivate_plugin')); 40 41 // Add deactivation modal footer 42 add_action('admin_footer-plugins.php', array($this, 'render_deactivation_modal')); 43 32 44 register_activation_hook(__FILE__, array($this, 'activate')); 33 45 register_deactivation_hook(__FILE__, array($this, 'deactivate')); 46 } 47 48 /** 49 * Redirect to setup wizard on first activation 50 */ 51 public function dearprma_activation_redirect() 52 { 53 // Check if we should redirect 54 if (get_transient('dearprma_activation_redirect')) { 55 // Delete the transient 56 delete_transient('dearprma_activation_redirect'); 57 58 // Don't redirect on multisite bulk activation 59 if (is_network_admin() || isset($_GET['activate-multi'])) { 60 return; 61 } 62 63 // Redirect to setup wizard 64 wp_safe_redirect(admin_url('admin.php?page=pmdash#/setup')); 65 exit; 66 } 34 67 } 35 68 … … 40 73 $this->load_applications(); 41 74 $this->load_submissions(); 42 // Ensure all post types are ````registered75 // Ensure all post types are registered 43 76 $this->register_applications_post_type(); 44 77 $this->register_submissions_post_type(); 78 $this->register_tasks_post_type(); 79 $this->register_issues_post_type(); 80 81 // Register REST API endpoints 82 add_action('rest_api_init', array($this, 'register_rest_routes')); 83 84 // Public-facing dashboard route 85 add_rewrite_rule('^dashboard/?$', 'index.php?dashboard=1', 'top'); 86 add_filter('query_vars', array($this, 'add_query_vars')); 87 add_action('template_redirect', array($this, 'render_public_dashboard')); 45 88 } 46 89 … … 78 121 'publicly_queryable' => true, 79 122 'show_ui' => true, 80 'show_in_menu' => true,123 'show_in_menu' => 'pmdash', 81 124 'show_in_admin_bar' => true, 82 125 'show_in_nav_menus' => true, … … 91 134 'has_archive' => true, 92 135 'hierarchical' => false, 93 'menu_position' => 5,136 'menu_position' => null, 94 137 'menu_icon' => 'dashicons-portfolio', 95 138 'supports' => array('title', 'editor', 'excerpt', 'comments', 'revisions', 'custom-fields'), … … 158 201 'public' => false, 159 202 'show_ui' => true, 160 'show_in_menu' => ' edit.php?post_type=project',203 'show_in_menu' => 'pmdash', 161 204 'capability_type' => 'post', 162 205 'supports' => array('title', 'editor'), … … 197 240 'public' => false, 198 241 'show_ui' => true, 199 'show_in_menu' => ' edit.php?post_type=project',242 'show_in_menu' => 'pmdash', 200 243 'capability_type' => 'post', 201 244 'supports' => array('title', 'editor'), … … 210 253 211 254 /** 255 * Register the tasks post type 256 */ 257 public function register_tasks_post_type() 258 { 259 $labels = array( 260 'name' => _x('Tasks', 'post type general name', 'bipo-project-manager'), 261 'singular_name' => _x('Task', 'post type singular name', 'bipo-project-manager'), 262 'menu_name' => _x('Tasks', 'admin menu', 'bipo-project-manager'), 263 'name_admin_bar' => _x('Task', 'add new on admin bar', 'bipo-project-manager'), 264 'add_new' => __('Add New', 'bipo-project-manager'), 265 'add_new_item' => __('Add New Task', 'bipo-project-manager'), 266 'new_item' => __('New Task', 'bipo-project-manager'), 267 'edit_item' => __('Edit Task', 'bipo-project-manager'), 268 'view_item' => __('View Task', 'bipo-project-manager'), 269 'all_items' => __('All Tasks', 'bipo-project-manager'), 270 'search_items' => __('Search Tasks', 'bipo-project-manager'), 271 'not_found' => __('No tasks found.', 'bipo-project-manager'), 272 'not_found_in_trash' => __('No tasks found in Trash.', 'bipo-project-manager'), 273 ); 274 275 $args = array( 276 'labels' => $labels, 277 'public' => false, 278 'show_ui' => true, 279 'show_in_menu' => 'pmdash', 280 'capability_type' => 'post', 281 'supports' => array('title', 'editor', 'comments'), 282 'has_archive' => false, 283 'show_in_rest' => true, 284 'exclude_from_search' => true, 285 'publicly_queryable' => false, 286 ); 287 288 register_post_type('dearprma_task', $args); 289 } 290 291 /** 292 * Register the issues post type 293 */ 294 public function register_issues_post_type() 295 { 296 $labels = array( 297 'name' => _x('Issues', 'post type general name', 'bipo-project-manager'), 298 'singular_name' => _x('Issue', 'post type singular name', 'bipo-project-manager'), 299 'menu_name' => _x('Issues', 'admin menu', 'bipo-project-manager'), 300 'name_admin_bar' => _x('Issue', 'add new on admin bar', 'bipo-project-manager'), 301 'add_new' => __('Add New', 'bipo-project-manager'), 302 'add_new_item' => __('Add New Issue', 'bipo-project-manager'), 303 'new_item' => __('New Issue', 'bipo-project-manager'), 304 'edit_item' => __('Edit Issue', 'bipo-project-manager'), 305 'view_item' => __('View Issue', 'bipo-project-manager'), 306 'all_items' => __('All Issues', 'bipo-project-manager'), 307 'search_items' => __('Search Issues', 'bipo-project-manager'), 308 'not_found' => __('No issues found.', 'bipo-project-manager'), 309 'not_found_in_trash' => __('No issues found in Trash.', 'bipo-project-manager'), 310 ); 311 312 $args = array( 313 'labels' => $labels, 314 'public' => false, 315 'show_ui' => true, 316 'show_in_menu' => 'pmdash', 317 'capability_type' => 'post', 318 'supports' => array('title', 'editor', 'comments'), 319 'has_archive' => false, 320 'show_in_rest' => true, 321 'exclude_from_search' => true, 322 'publicly_queryable' => false, 323 ); 324 325 register_post_type('dearprma_issue', $args); 326 } 327 328 /** 329 * Register REST API routes for tasks and issues 330 */ 331 public function register_rest_routes() 332 { 333 // Tasks endpoints 334 register_rest_route('dearprma/v1', '/tasks', array( 335 array( 336 'methods' => 'GET', 337 'callback' => array($this, 'rest_get_tasks'), 338 'permission_callback' => array($this, 'rest_permission_check'), 339 ), 340 array( 341 'methods' => 'POST', 342 'callback' => array($this, 'rest_create_task'), 343 'permission_callback' => array($this, 'rest_permission_check'), 344 ), 345 )); 346 347 register_rest_route('dearprma/v1', '/tasks/(?P<id>\d+)', array( 348 array( 349 'methods' => 'GET', 350 'callback' => array($this, 'rest_get_task'), 351 'permission_callback' => array($this, 'rest_permission_check'), 352 ), 353 array( 354 'methods' => 'PUT,PATCH', 355 'callback' => array($this, 'rest_update_task'), 356 'permission_callback' => array($this, 'rest_permission_check'), 357 ), 358 array( 359 'methods' => 'DELETE', 360 'callback' => array($this, 'rest_delete_task'), 361 'permission_callback' => array($this, 'rest_permission_check'), 362 ), 363 )); 364 365 // Issues endpoints 366 register_rest_route('dearprma/v1', '/issues', array( 367 array( 368 'methods' => 'GET', 369 'callback' => array($this, 'rest_get_issues'), 370 'permission_callback' => array($this, 'rest_permission_check'), 371 ), 372 array( 373 'methods' => 'POST', 374 'callback' => array($this, 'rest_create_issue'), 375 'permission_callback' => array($this, 'rest_permission_check'), 376 ), 377 )); 378 379 register_rest_route('dearprma/v1', '/issues/(?P<id>\d+)', array( 380 array( 381 'methods' => 'GET', 382 'callback' => array($this, 'rest_get_issue'), 383 'permission_callback' => array($this, 'rest_permission_check'), 384 ), 385 array( 386 'methods' => 'PUT,PATCH', 387 'callback' => array($this, 'rest_update_issue'), 388 'permission_callback' => array($this, 'rest_permission_check'), 389 ), 390 array( 391 'methods' => 'DELETE', 392 'callback' => array($this, 'rest_delete_issue'), 393 'permission_callback' => array($this, 'rest_permission_check'), 394 ), 395 )); 396 397 // Issue comments endpoint 398 register_rest_route('dearprma/v1', '/issues/(?P<id>\d+)/comments', array( 399 array( 400 'methods' => 'GET', 401 'callback' => array($this, 'rest_get_issue_comments'), 402 'permission_callback' => array($this, 'rest_permission_check'), 403 ), 404 array( 405 'methods' => 'POST', 406 'callback' => array($this, 'rest_add_issue_comment'), 407 'permission_callback' => array($this, 'rest_permission_check'), 408 ), 409 )); 410 } 411 412 /** 413 * REST API permission check 414 */ 415 public function rest_permission_check() 416 { 417 return current_user_can('edit_posts'); 418 } 419 420 /** 421 * Get all tasks 422 */ 423 public function rest_get_tasks($request) 424 { 425 $args = array( 426 'post_type' => 'dearprma_task', 427 'posts_per_page' => -1, 428 'post_status' => array('publish', 'draft', 'pending'), 429 'orderby' => 'menu_order date', 430 'order' => 'ASC', 431 ); 432 433 // Filter by project 434 if ($request->get_param('project_id')) { 435 $args['meta_query'] = array( 436 array( 437 'key' => '_dearprma_project_id', 438 'value' => intval($request->get_param('project_id')), 439 ), 440 ); 441 } 442 443 // Filter by status 444 if ($request->get_param('status')) { 445 $args['meta_query'][] = array( 446 'key' => '_dearprma_task_status', 447 'value' => sanitize_text_field($request->get_param('status')), 448 ); 449 } 450 451 $posts = get_posts($args); 452 $tasks = array(); 453 454 foreach ($posts as $post) { 455 $tasks[] = $this->format_task_response($post); 456 } 457 458 return rest_ensure_response($tasks); 459 } 460 461 /** 462 * Get single task 463 */ 464 public function rest_get_task($request) 465 { 466 $post = get_post(intval($request['id'])); 467 468 if (!$post || $post->post_type !== 'dearprma_task') { 469 return new WP_Error('not_found', 'Task not found', array('status' => 404)); 470 } 471 472 return rest_ensure_response($this->format_task_response($post)); 473 } 474 475 /** 476 * Create task 477 */ 478 public function rest_create_task($request) 479 { 480 $params = $request->get_json_params(); 481 482 $post_data = array( 483 'post_type' => 'dearprma_task', 484 'post_title' => sanitize_text_field($params['title'] ?? ''), 485 'post_content' => wp_kses_post($params['description'] ?? ''), 486 'post_status' => 'publish', 487 ); 488 489 $post_id = wp_insert_post($post_data); 490 491 if (is_wp_error($post_id)) { 492 return $post_id; 493 } 494 495 // Save meta fields 496 $this->save_task_meta($post_id, $params); 497 498 return rest_ensure_response($this->format_task_response(get_post($post_id))); 499 } 500 501 /** 502 * Update task 503 */ 504 public function rest_update_task($request) 505 { 506 $post_id = intval($request['id']); 507 $post = get_post($post_id); 508 509 if (!$post || $post->post_type !== 'dearprma_task') { 510 return new WP_Error('not_found', 'Task not found', array('status' => 404)); 511 } 512 513 $params = $request->get_json_params(); 514 515 $post_data = array('ID' => $post_id); 516 517 if (isset($params['title'])) { 518 $post_data['post_title'] = sanitize_text_field($params['title']); 519 } 520 521 if (isset($params['description'])) { 522 $post_data['post_content'] = wp_kses_post($params['description']); 523 } 524 525 wp_update_post($post_data); 526 527 // Update meta fields 528 $this->save_task_meta($post_id, $params); 529 530 return rest_ensure_response($this->format_task_response(get_post($post_id))); 531 } 532 533 /** 534 * Delete task 535 */ 536 public function rest_delete_task($request) 537 { 538 $post_id = intval($request['id']); 539 $post = get_post($post_id); 540 541 if (!$post || $post->post_type !== 'dearprma_task') { 542 return new WP_Error('not_found', 'Task not found', array('status' => 404)); 543 } 544 545 wp_delete_post($post_id, true); 546 547 return rest_ensure_response(array('deleted' => true, 'id' => $post_id)); 548 } 549 550 /** 551 * Save task meta fields 552 */ 553 private function save_task_meta($post_id, $params) 554 { 555 $meta_fields = array( 556 'status' => '_dearprma_task_status', 557 'priority' => '_dearprma_task_priority', 558 'assignee' => '_dearprma_task_assignee', 559 'due_date' => '_dearprma_task_due_date', 560 'project_id' => '_dearprma_project_id', 561 'order' => '_dearprma_task_order', 562 ); 563 564 foreach ($meta_fields as $param_key => $meta_key) { 565 if (isset($params[$param_key])) { 566 update_post_meta($post_id, $meta_key, sanitize_text_field($params[$param_key])); 567 } 568 } 569 } 570 571 /** 572 * Format task for response 573 */ 574 private function format_task_response($post) 575 { 576 $assignee_id = get_post_meta($post->ID, '_dearprma_task_assignee', true); 577 $assignee = null; 578 579 if ($assignee_id) { 580 $user = get_user_by('ID', $assignee_id); 581 if ($user) { 582 $assignee = array( 583 'id' => $user->ID, 584 'name' => $user->display_name, 585 'avatar' => get_avatar_url($user->ID, array('size' => 32)), 586 ); 587 } 588 } 589 590 return array( 591 'id' => $post->ID, 592 'title' => $post->post_title, 593 'description' => $post->post_content, 594 'status' => get_post_meta($post->ID, '_dearprma_task_status', true) ?: 'to_do', 595 'priority' => get_post_meta($post->ID, '_dearprma_task_priority', true) ?: 'medium', 596 'assignee' => $assignee, 597 'assignee_id' => $assignee_id ? intval($assignee_id) : null, 598 'due_date' => get_post_meta($post->ID, '_dearprma_task_due_date', true) ?: null, 599 'project_id' => get_post_meta($post->ID, '_dearprma_project_id', true) ?: null, 600 'order' => intval(get_post_meta($post->ID, '_dearprma_task_order', true)) ?: 0, 601 'created_at' => $post->post_date, 602 'updated_at' => $post->post_modified, 603 ); 604 } 605 606 /** 607 * Get all issues 608 */ 609 public function rest_get_issues($request) 610 { 611 $args = array( 612 'post_type' => 'dearprma_issue', 613 'posts_per_page' => -1, 614 'post_status' => array('publish', 'draft', 'pending'), 615 'orderby' => 'date', 616 'order' => 'DESC', 617 ); 618 619 // Filter by project 620 if ($request->get_param('project_id')) { 621 $args['meta_query'] = array( 622 array( 623 'key' => '_dearprma_project_id', 624 'value' => intval($request->get_param('project_id')), 625 ), 626 ); 627 } 628 629 // Filter by status (open/closed) 630 if ($request->get_param('status')) { 631 $args['meta_query'][] = array( 632 'key' => '_dearprma_issue_status', 633 'value' => sanitize_text_field($request->get_param('status')), 634 ); 635 } 636 637 $posts = get_posts($args); 638 $issues = array(); 639 640 foreach ($posts as $post) { 641 $issues[] = $this->format_issue_response($post); 642 } 643 644 return rest_ensure_response($issues); 645 } 646 647 /** 648 * Get single issue 649 */ 650 public function rest_get_issue($request) 651 { 652 $post = get_post(intval($request['id'])); 653 654 if (!$post || $post->post_type !== 'dearprma_issue') { 655 return new WP_Error('not_found', 'Issue not found', array('status' => 404)); 656 } 657 658 return rest_ensure_response($this->format_issue_response($post)); 659 } 660 661 /** 662 * Create issue 663 */ 664 public function rest_create_issue($request) 665 { 666 $params = $request->get_json_params(); 667 668 $post_data = array( 669 'post_type' => 'dearprma_issue', 670 'post_title' => sanitize_text_field($params['title'] ?? ''), 671 'post_content' => wp_kses_post($params['description'] ?? ''), 672 'post_status' => 'publish', 673 ); 674 675 $post_id = wp_insert_post($post_data); 676 677 if (is_wp_error($post_id)) { 678 return $post_id; 679 } 680 681 // Save meta fields 682 $this->save_issue_meta($post_id, $params); 683 684 // Set default status to open 685 if (!isset($params['status'])) { 686 update_post_meta($post_id, '_dearprma_issue_status', 'open'); 687 } 688 689 return rest_ensure_response($this->format_issue_response(get_post($post_id))); 690 } 691 692 /** 693 * Update issue 694 */ 695 public function rest_update_issue($request) 696 { 697 $post_id = intval($request['id']); 698 $post = get_post($post_id); 699 700 if (!$post || $post->post_type !== 'dearprma_issue') { 701 return new WP_Error('not_found', 'Issue not found', array('status' => 404)); 702 } 703 704 $params = $request->get_json_params(); 705 706 $post_data = array('ID' => $post_id); 707 708 if (isset($params['title'])) { 709 $post_data['post_title'] = sanitize_text_field($params['title']); 710 } 711 712 if (isset($params['description'])) { 713 $post_data['post_content'] = wp_kses_post($params['description']); 714 } 715 716 wp_update_post($post_data); 717 718 // Update meta fields 719 $this->save_issue_meta($post_id, $params); 720 721 return rest_ensure_response($this->format_issue_response(get_post($post_id))); 722 } 723 724 /** 725 * Delete issue 726 */ 727 public function rest_delete_issue($request) 728 { 729 $post_id = intval($request['id']); 730 $post = get_post($post_id); 731 732 if (!$post || $post->post_type !== 'dearprma_issue') { 733 return new WP_Error('not_found', 'Issue not found', array('status' => 404)); 734 } 735 736 wp_delete_post($post_id, true); 737 738 return rest_ensure_response(array('deleted' => true, 'id' => $post_id)); 739 } 740 741 /** 742 * Save issue meta fields 743 */ 744 private function save_issue_meta($post_id, $params) 745 { 746 $meta_fields = array( 747 'status' => '_dearprma_issue_status', 748 'priority' => '_dearprma_issue_priority', 749 'assignee' => '_dearprma_issue_assignee', 750 'labels' => '_dearprma_issue_labels', 751 'project_id' => '_dearprma_project_id', 752 ); 753 754 foreach ($meta_fields as $param_key => $meta_key) { 755 if (isset($params[$param_key])) { 756 $value = $params[$param_key]; 757 if ($param_key === 'labels' && is_array($value)) { 758 $value = implode(',', array_map('sanitize_text_field', $value)); 759 } else { 760 $value = sanitize_text_field($value); 761 } 762 update_post_meta($post_id, $meta_key, $value); 763 } 764 } 765 } 766 767 /** 768 * Format issue for response 769 */ 770 private function format_issue_response($post) 771 { 772 $assignee_id = get_post_meta($post->ID, '_dearprma_issue_assignee', true); 773 $assignee = null; 774 775 if ($assignee_id) { 776 $user = get_user_by('ID', $assignee_id); 777 if ($user) { 778 $assignee = array( 779 'id' => $user->ID, 780 'name' => $user->display_name, 781 'avatar' => get_avatar_url($user->ID, array('size' => 32)), 782 ); 783 } 784 } 785 786 $author = get_user_by('ID', $post->post_author); 787 $labels_raw = get_post_meta($post->ID, '_dearprma_issue_labels', true); 788 $labels = $labels_raw ? explode(',', $labels_raw) : array(); 789 790 return array( 791 'id' => $post->ID, 792 'number' => $post->ID, 793 'title' => $post->post_title, 794 'description' => $post->post_content, 795 'status' => get_post_meta($post->ID, '_dearprma_issue_status', true) ?: 'open', 796 'priority' => get_post_meta($post->ID, '_dearprma_issue_priority', true) ?: 'medium', 797 'assignee' => $assignee, 798 'assignee_id' => $assignee_id ? intval($assignee_id) : null, 799 'labels' => $labels, 800 'project_id' => get_post_meta($post->ID, '_dearprma_project_id', true) ?: null, 801 'author' => array( 802 'id' => $author ? $author->ID : 0, 803 'name' => $author ? $author->display_name : 'Unknown', 804 'avatar' => $author ? get_avatar_url($author->ID, array('size' => 32)) : '', 805 ), 806 'comment_count' => intval(get_comments_number($post->ID)), 807 'created_at' => $post->post_date, 808 'updated_at' => $post->post_modified, 809 ); 810 } 811 812 /** 813 * Get issue comments 814 */ 815 public function rest_get_issue_comments($request) 816 { 817 $post_id = intval($request['id']); 818 $post = get_post($post_id); 819 820 if (!$post || $post->post_type !== 'dearprma_issue') { 821 return new WP_Error('not_found', 'Issue not found', array('status' => 404)); 822 } 823 824 $comments = get_comments(array( 825 'post_id' => $post_id, 826 'status' => 'approve', 827 'orderby' => 'comment_date', 828 'order' => 'ASC', 829 )); 830 831 $formatted_comments = array(); 832 833 foreach ($comments as $comment) { 834 $formatted_comments[] = array( 835 'id' => $comment->comment_ID, 836 'content' => $comment->comment_content, 837 'author' => array( 838 'id' => intval($comment->user_id), 839 'name' => $comment->comment_author, 840 'avatar' => get_avatar_url($comment->user_id ?: $comment->comment_author_email, array('size' => 32)), 841 ), 842 'created_at' => $comment->comment_date, 843 ); 844 } 845 846 return rest_ensure_response($formatted_comments); 847 } 848 849 /** 850 * Add comment to issue 851 */ 852 public function rest_add_issue_comment($request) 853 { 854 $post_id = intval($request['id']); 855 $post = get_post($post_id); 856 857 if (!$post || $post->post_type !== 'dearprma_issue') { 858 return new WP_Error('not_found', 'Issue not found', array('status' => 404)); 859 } 860 861 $params = $request->get_json_params(); 862 $current_user = wp_get_current_user(); 863 864 $comment_data = array( 865 'comment_post_ID' => $post_id, 866 'comment_content' => wp_kses_post($params['content'] ?? ''), 867 'user_id' => $current_user->ID, 868 'comment_author' => $current_user->display_name, 869 'comment_author_email' => $current_user->user_email, 870 'comment_approved' => 1, 871 ); 872 873 $comment_id = wp_insert_comment($comment_data); 874 875 if (!$comment_id) { 876 return new WP_Error('comment_failed', 'Failed to add comment', array('status' => 500)); 877 } 878 879 $comment = get_comment($comment_id); 880 881 return rest_ensure_response(array( 882 'id' => $comment->comment_ID, 883 'content' => $comment->comment_content, 884 'author' => array( 885 'id' => intval($comment->user_id), 886 'name' => $comment->comment_author, 887 'avatar' => get_avatar_url($comment->user_id, array('size' => 32)), 888 ), 889 'created_at' => $comment->comment_date, 890 )); 891 } 892 893 /** 212 894 * Flush rewrite rules when permalink structure is saved 213 895 */ … … 260 942 public function activate() 261 943 { 262 263 944 $this->ensure_project_page_exists(); 264 945 … … 269 950 $this->register_submissions_post_type(); 270 951 952 // Check if this is a fresh install (no setup complete option exists) 953 if (get_option('dearprma_setup_complete') === false) { 954 // Set transient for redirect to setup wizard 955 set_transient('dearprma_activation_redirect', true, 60); 956 } 271 957 272 958 // Flush rewrite rules on activation (twice to ensure it works) … … 316 1002 flush_rewrite_rules(); 317 1003 } 1004 1005 /** 1006 * Add React Admin Dashboard Page 1007 */ 1008 public function add_react_admin_page() 1009 { 1010 // Add main menu page 1011 add_menu_page( 1012 __('Dear PM Dashboard', 'bipo-project-manager'), 1013 __('Dear PM', 'bipo-project-manager'), 1014 'manage_options', 1015 'pmdash', 1016 array($this, 'render_react_admin_page'), 1017 'dashicons-portfolio', 1018 3 1019 ); 1020 1021 // Add Dashboard as first submenu (replaces default) 1022 add_submenu_page( 1023 'pmdash', 1024 __('Dashboard', 'bipo-project-manager'), 1025 __('Dashboard', 'bipo-project-manager'), 1026 'manage_options', 1027 'pmdash', 1028 array($this, 'render_react_admin_page') 1029 ); 1030 } 1031 1032 /** 1033 * Render React Admin Page 1034 */ 1035 public function render_react_admin_page() 1036 { 1037 echo '<div id="dear-pm-root"></div>'; 1038 echo '<style> 1039 #dear-pm-root { margin-left: -20px; } 1040 #wpcontent { padding-left: 0; } 1041 #wpfooter { display: none; } 1042 </style>'; 1043 } 1044 1045 /** 1046 * Enqueue React Admin Scripts 1047 */ 1048 public function enqueue_react_admin($hook) 1049 { 1050 // Only load on our plugin page 1051 if ($hook !== 'toplevel_page_pmdash') { 1052 return; 1053 } 1054 1055 $asset_file = DEARPRMA_PLUGIN_PATH . 'assets/admin.asset.php'; 1056 1057 if (!file_exists($asset_file)) { 1058 echo '<div class="notice notice-error"><p>Please run <code>npm run build:js</code></p></div>'; 1059 return; 1060 } 1061 1062 $asset = include $asset_file; 1063 1064 // Enqueue compiled CSS 1065 wp_enqueue_style( 1066 'dear-pm-admin', 1067 DEARPRMA_PLUGIN_URL . 'assets/admin.css', 1068 array(), 1069 $asset['version'] 1070 ); 1071 1072 // Enqueue React Admin App 1073 wp_enqueue_script( 1074 'dear-pm-admin', 1075 DEARPRMA_PLUGIN_URL . 'assets/admin.js', 1076 $asset['dependencies'], 1077 $asset['version'], 1078 true 1079 ); 1080 1081 // Check if setup is needed 1082 $dearprma_setup_complete = get_option('dearprma_setup_complete', false); 1083 1084 // Localize script data 1085 wp_localize_script('dear-pm-admin', 'dearPM', array( 1086 'ajaxUrl' => admin_url('admin-ajax.php'), 1087 'adminUrl' => admin_url(), 1088 'restUrl' => rest_url('wp/v2'), 1089 'nonce' => wp_create_nonce('dearprma_setup_nonce'), 1090 'needsSetup' => !$dearprma_setup_complete, 1091 'currentUser' => array( 1092 'name' => wp_get_current_user()->display_name, 1093 'email' => wp_get_current_user()->user_email, 1094 ), 1095 'features' => get_option('dearprma_features', array()), 1096 )); 1097 } 1098 1099 /** 1100 * Allow custom query vars 1101 */ 1102 public function add_query_vars($vars) 1103 { 1104 $vars[] = 'dashboard'; 1105 return $vars; 1106 } 1107 1108 /** 1109 * Render public-facing dashboard at /dashboard 1110 */ 1111 public function render_public_dashboard() 1112 { 1113 if (!get_query_var('dashboard')) { 1114 return; 1115 } 1116 1117 // Prevent theme from continuing 1118 status_header(200); 1119 nocache_headers(); 1120 1121 // Enqueue built assets (reuse admin build) 1122 $asset_file = DEARPRMA_PLUGIN_PATH . 'assets/admin.asset.php'; 1123 if (file_exists($asset_file)) { 1124 $asset = include $asset_file; 1125 1126 wp_enqueue_style( 1127 'dear-pm-frontend', 1128 DEARPRMA_PLUGIN_URL . 'assets/admin.css', 1129 array(), 1130 $asset['version'] 1131 ); 1132 1133 wp_enqueue_script( 1134 'dear-pm-frontend', 1135 DEARPRMA_PLUGIN_URL . 'assets/admin.js', 1136 $asset['dependencies'], 1137 $asset['version'], 1138 true 1139 ); 1140 1141 // Localize same data for frontend 1142 wp_localize_script('dear-pm-frontend', 'dearPM', array( 1143 'ajaxUrl' => admin_url('admin-ajax.php'), 1144 'adminUrl' => admin_url(), 1145 'restUrl' => rest_url('wp/v2'), 1146 'nonce' => wp_create_nonce('dearprma_setup_nonce'), 1147 'needsSetup' => !get_option('dearprma_setup_complete', false), 1148 'currentUser' => array( 1149 'name' => wp_get_current_user()->display_name, 1150 'email' => wp_get_current_user()->user_email, 1151 ), 1152 )); 1153 } 1154 1155 // Output a minimal, standalone HTML page so the public can access the SPA 1156 echo '<!DOCTYPE html><html><head>'; 1157 wp_head(); 1158 echo '</head><body style="margin:0">'; 1159 echo '<div id="dear-pm-root"></div>'; 1160 wp_footer(); 1161 echo '</body></html>'; 1162 exit; 1163 } 1164 1165 /** 1166 * AJAX handler for saving setup wizard settings 1167 */ 1168 public function ajax_save_setup() 1169 { 1170 // Verify nonce 1171 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'dearprma_setup_nonce')) { 1172 wp_send_json_error(array('message' => 'Invalid nonce')); 1173 return; 1174 } 1175 1176 // Check user capability 1177 if (!current_user_can('manage_options')) { 1178 wp_send_json_error(array('message' => 'Permission denied')); 1179 return; 1180 } 1181 1182 // Get setup type 1183 $setup_type = isset($_POST['setup_type']) ? sanitize_text_field(wp_unslash($_POST['setup_type'])) : 'easy'; 1184 1185 // Get features 1186 $features_json = isset($_POST['features']) ? sanitize_text_field(wp_unslash($_POST['features'])) : '{}'; 1187 $features = json_decode($features_json, true); 1188 1189 if (!is_array($features)) { 1190 $features = array(); 1191 } 1192 1193 // Sanitize features 1194 $sanitized_features = array(); 1195 $allowed_features = array('kanban_board', 'charts', 'public_listing', 'issue_tracker', 'gantt_chart', 'team_management'); 1196 1197 foreach ($allowed_features as $feature_key) { 1198 $sanitized_features[$feature_key] = isset($features[$feature_key]) ? (bool) $features[$feature_key] : false; 1199 } 1200 1201 // Save options 1202 update_option('dearprma_setup_type', $setup_type); 1203 update_option('dearprma_features', $sanitized_features); 1204 update_option('dearprma_setup_complete', true); 1205 1206 wp_send_json_success(array( 1207 'message' => 'Setup saved successfully', 1208 'setup_type' => $setup_type, 1209 'features' => $sanitized_features, 1210 )); 1211 } 1212 1213 /** 1214 * Check if a feature is enabled 1215 * 1216 * @param string $feature_key The feature key to check 1217 * @return bool Whether the feature is enabled 1218 */ 1219 public static function is_feature_enabled($feature_key) 1220 { 1221 $features = get_option('dearprma_features', array()); 1222 return isset($features[$feature_key]) && $features[$feature_key]; 1223 } 1224 1225 /** 1226 * Enqueue deactivation scripts on plugins page 1227 */ 1228 public function enqueue_deactivation_scripts($hook) 1229 { 1230 if ($hook !== 'plugins.php') { 1231 return; 1232 } 1233 1234 wp_enqueue_style( 1235 'dearprma-deactivation', 1236 DEARPRMA_PLUGIN_URL . 'admin/assets/css/deactivation.css', 1237 array(), 1238 DEARPRMA_VERSION 1239 ); 1240 1241 wp_enqueue_script( 1242 'dearprma-deactivation', 1243 DEARPRMA_PLUGIN_URL . 'admin/assets/js/deactivation.js', 1244 array('jquery'), 1245 DEARPRMA_VERSION, 1246 true 1247 ); 1248 1249 wp_localize_script('dearprma-deactivation', 'dearprmaDeactivate', array( 1250 'ajaxUrl' => admin_url('admin-ajax.php'), 1251 'nonce' => wp_create_nonce('dearprma_deactivate_nonce'), 1252 'pluginSlug' => 'bipo-project-manager/bipo-project-manager.php', 1253 )); 1254 } 1255 1256 /** 1257 * Render deactivation modal HTML 1258 */ 1259 public function render_deactivation_modal() 1260 { 1261 ?> 1262 <div id="dearprma-deactivation-modal" class="dearprma-modal-overlay" style="display:none;"> 1263 <div class="dearprma-modal"> 1264 <div class="dearprma-modal-header"> 1265 <div class="dearprma-modal-icon">📁</div> 1266 <h2><?php esc_html_e('Deactivate Dear Project Manager', 'bipo-project-manager'); ?></h2> 1267 <p><?php esc_html_e('What would you like to do with your data?', 'bipo-project-manager'); ?></p> 1268 </div> 1269 1270 <div class="dearprma-modal-options"> 1271 <button type="button" class="dearprma-option" data-action="just_deactivate"> 1272 <span class="dearprma-option-icon">⏸️</span> 1273 <div class="dearprma-option-content"> 1274 <strong><?php esc_html_e('Just Deactivate', 'bipo-project-manager'); ?></strong> 1275 <span><?php esc_html_e('Keep all settings and data for later', 'bipo-project-manager'); ?></span> 1276 </div> 1277 </button> 1278 1279 <button type="button" class="dearprma-option dearprma-option-warning" data-action="delete_settings"> 1280 <span class="dearprma-option-icon">🗑️</span> 1281 <div class="dearprma-option-content"> 1282 <strong><?php esc_html_e('Delete Settings', 'bipo-project-manager'); ?></strong> 1283 <span><?php esc_html_e('Remove setup data and preferences only', 'bipo-project-manager'); ?></span> 1284 </div> 1285 </button> 1286 1287 <button type="button" class="dearprma-option dearprma-option-danger" data-action="delete_all"> 1288 <span class="dearprma-option-icon">⚠️</span> 1289 <div class="dearprma-option-content"> 1290 <strong><?php esc_html_e('Delete All Data', 'bipo-project-manager'); ?></strong> 1291 <span><?php esc_html_e('Remove everything including projects, applications & submissions', 'bipo-project-manager'); ?></span> 1292 </div> 1293 </button> 1294 </div> 1295 1296 <div class="dearprma-modal-footer"> 1297 <button type="button" class="dearprma-btn-cancel" id="dearprma-cancel-deactivate"> 1298 <?php esc_html_e('Cancel', 'bipo-project-manager'); ?> 1299 </button> 1300 </div> 1301 </div> 1302 </div> 1303 <?php 1304 } 1305 1306 /** 1307 * AJAX handler for plugin deactivation with data options 1308 */ 1309 public function ajax_deactivate_plugin() 1310 { 1311 // Verify nonce 1312 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'dearprma_deactivate_nonce')) { 1313 wp_send_json_error(array('message' => 'Invalid nonce')); 1314 return; 1315 } 1316 1317 // Check user capability 1318 if (!current_user_can('activate_plugins')) { 1319 wp_send_json_error(array('message' => 'Permission denied')); 1320 return; 1321 } 1322 1323 $action = isset($_POST['deactivate_action']) ? sanitize_text_field(wp_unslash($_POST['deactivate_action'])) : 'just_deactivate'; 1324 1325 // Handle different deactivation actions 1326 switch ($action) { 1327 case 'delete_settings': 1328 $this->dearprma_delete_settings(); 1329 break; 1330 1331 case 'delete_all': 1332 $this->dearprma_delete_all_data(); 1333 break; 1334 1335 case 'just_deactivate': 1336 default: 1337 // Do nothing, just let the plugin deactivate 1338 break; 1339 } 1340 1341 wp_send_json_success(array( 1342 'message' => 'Plugin ready for deactivation', 1343 'action' => $action, 1344 )); 1345 } 1346 1347 /** 1348 * Delete plugin settings only 1349 */ 1350 private function dearprma_delete_settings() 1351 { 1352 delete_option('dearprma_setup_complete'); 1353 delete_option('dearprma_setup_type'); 1354 delete_option('dearprma_features'); 1355 delete_option('dearprma_project_page_id'); 1356 delete_option('dearprma_needs_setup'); 1357 } 1358 1359 /** 1360 * Delete all plugin data including posts 1361 */ 1362 private function dearprma_delete_all_data() 1363 { 1364 global $wpdb; 1365 1366 // First delete settings 1367 $this->dearprma_delete_settings(); 1368 1369 // Delete all projects 1370 $projects = get_posts(array( 1371 'post_type' => 'project', 1372 'numberposts' => -1, 1373 'post_status' => 'any', 1374 'fields' => 'ids', 1375 )); 1376 1377 foreach ($projects as $project_id) { 1378 wp_delete_post($project_id, true); 1379 } 1380 1381 // Delete all applications 1382 $applications = get_posts(array( 1383 'post_type' => 'project_application', 1384 'numberposts' => -1, 1385 'post_status' => 'any', 1386 'fields' => 'ids', 1387 )); 1388 1389 foreach ($applications as $application_id) { 1390 wp_delete_post($application_id, true); 1391 } 1392 1393 // Delete all submissions 1394 $submissions = get_posts(array( 1395 'post_type' => 'project_submission', 1396 'numberposts' => -1, 1397 'post_status' => 'any', 1398 'fields' => 'ids', 1399 )); 1400 1401 foreach ($submissions as $submission_id) { 1402 wp_delete_post($submission_id, true); 1403 } 1404 1405 // Clean up any orphaned post meta 1406 $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE post_id NOT IN (SELECT ID FROM {$wpdb->posts})"); 1407 } 318 1408 } 319 1409
Note: See TracChangeset
for help on using the changeset viewer.