Plugin Directory

Changeset 3432071


Ignore:
Timestamp:
01/04/2026 11:27:59 AM (3 months ago)
Author:
imbipo
Message:

New Dashboard interface with kanban Board and issue tracker

Location:
bipo-project-manager
Files:
62 added
2 edited

Legend:

Unmodified
Added
Removed
  • bipo-project-manager/trunk/bipo-project-manager.php

    r3407053 r3432071  
    2929        add_action('init', array($this, 'init'));
    3030        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'));
    3135        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
    3244        register_activation_hook(__FILE__, array($this, 'activate'));
    3345        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        }
    3467    }
    3568
     
    4073        $this->load_applications();
    4174        $this->load_submissions();
    42         // Ensure all post types are ````registered
     75        // Ensure all post types are registered
    4376        $this->register_applications_post_type();
    4477        $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'));
    4588    }
    4689
     
    78121            'publicly_queryable' => true,
    79122            'show_ui'            => true,
    80             'show_in_menu'       => true,
     123            'show_in_menu'       => 'pmdash',
    81124            'show_in_admin_bar'  => true,
    82125            'show_in_nav_menus'  => true,
     
    91134            'has_archive'        => true,
    92135            'hierarchical'       => false,
    93             'menu_position'      => 5,
     136            'menu_position'      => null,
    94137            'menu_icon'          => 'dashicons-portfolio',
    95138            'supports'           => array('title', 'editor', 'excerpt', 'comments', 'revisions', 'custom-fields'),
     
    158201            'public' => false,
    159202            'show_ui' => true,
    160             'show_in_menu' => 'edit.php?post_type=project',
     203            'show_in_menu' => 'pmdash',
    161204            'capability_type' => 'post',
    162205            'supports' => array('title', 'editor'),
     
    197240            'public' => false,
    198241            'show_ui' => true,
    199             'show_in_menu' => 'edit.php?post_type=project',
     242            'show_in_menu' => 'pmdash',
    200243            'capability_type' => 'post',
    201244            'supports' => array('title', 'editor'),
     
    210253
    211254    /**
     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    /**
    212894     * Flush rewrite rules when permalink structure is saved
    213895     */
     
    260942    public function activate()
    261943    {
    262 
    263944        $this->ensure_project_page_exists();
    264945
     
    269950        $this->register_submissions_post_type();
    270951
     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        }
    271957
    272958        // Flush rewrite rules on activation (twice to ensure it works)
     
    3161002        flush_rewrite_rules();
    3171003    }
     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    }
    3181408}
    3191409
Note: See TracChangeset for help on using the changeset viewer.