From e21ab60f1334f53a160c41373a386a804cd69a71 Mon Sep 17 00:00:00 2001 From: Timi Wahalahti Date: Mon, 20 May 2024 17:22:51 +0300 Subject: [PATCH] New reimbursement request and vendor payments status and notes functionality (#1051) * add new wcb-needs-followup status for payments * add new wcb-needs-followup status to network payments views * take new wcb-pending-approval status into consideration when doing the overdue vendor payments list * extend the notes field for vendor payments * add new private note metabox to vendor payments and reimbursement requests * limit the new notes for network admins and the request's author --------- Co-authored-by: ren <18050944+renintw@users.noreply.github.com> --- .../includes/payment-requests-dashboard.php | 2 + .../includes/payment-requests-list-table.php | 2 +- .../reimbursement-requests-dashboard.php | 4 + .../includes/payment-request.php | 1 + .../includes/reimbursement-request.php | 114 +--------- .../includes/wordcamp-budgets.php | 209 +++++++++++++++++- .../reimbursement-request/metabox-notes.php | 45 ---- .../metabox-notes-private.php | 36 +++ .../views/wordcamp-budgets/metabox-notes.php | 42 ++++ 9 files changed, 289 insertions(+), 166 deletions(-) delete mode 100644 public_html/wp-content/plugins/wordcamp-payments/views/reimbursement-request/metabox-notes.php create mode 100644 public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes-private.php create mode 100644 public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes.php diff --git a/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-dashboard.php b/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-dashboard.php index 0f33173b0..f51c2aeea 100644 --- a/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-dashboard.php +++ b/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-dashboard.php @@ -340,6 +340,7 @@ public static function get_current_tab() { 'paid', 'cancelled-failed', 'incomplete', + 'needs-followup', ); if ( isset( $_REQUEST['wcp-section'] ) && in_array( $_REQUEST['wcp-section'], $tabs ) ) { @@ -363,6 +364,7 @@ public static function render_dashboard_tabs() { 'paid' => __( 'Paid', 'wordcamporg' ), 'cancelled-failed' => __( 'Cancelled/Failed', 'wordcamporg' ), 'incomplete' => __( 'Incomplete', 'wordcamporg' ), + 'needs-followup' => __( 'Needs Follow-up', 'wordcamporg' ), ); foreach ( $sections as $section_key => $section_caption ) { diff --git a/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-list-table.php b/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-list-table.php index c2ece1589..561bbdfb2 100644 --- a/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-list-table.php +++ b/public_html/wp-content/plugins/wordcamp-payments-network/includes/payment-requests-list-table.php @@ -54,7 +54,7 @@ public function prepare_items() { $order = 'asc'; if ( 'overdue' == $view ) { - $where .= $wpdb->prepare( " AND `status` = 'wcb-pending-approval' AND `due` > 0 AND `due` <= %d ", time() ); + $where .= $wpdb->prepare( " AND `status` IN ( 'wcb-pending-approval', 'wcb-needs-followup' ) AND `due` > 0 AND `due` <= %d ", time() ); } elseif ( 'pending-approval' == $view ) { $where .= " AND `status` = 'wcb-pending-approval' "; } elseif ( 'approved' == $view ) { diff --git a/public_html/wp-content/plugins/wordcamp-payments-network/includes/reimbursement-requests-dashboard.php b/public_html/wp-content/plugins/wordcamp-payments-network/includes/reimbursement-requests-dashboard.php index 50e1529e5..10993c896 100644 --- a/public_html/wp-content/plugins/wordcamp-payments-network/includes/reimbursement-requests-dashboard.php +++ b/public_html/wp-content/plugins/wordcamp-payments-network/includes/reimbursement-requests-dashboard.php @@ -51,6 +51,10 @@ function render_submenu_page() { $section_explanation = 'These requests have been reviewed by a deputy, and sent back to the organizer because they lacked some required information.'; break; + case 'wcb-needs-followup': + $section_explanation = 'These requests have been reviewed by a deputy, and need more information or second opinion.'; + break; + case 'wcb-cancelled': $section_explanation = 'These requests have been reviewed by a deputy and cancelled/rejected.'; break; diff --git a/public_html/wp-content/plugins/wordcamp-payments/includes/payment-request.php b/public_html/wp-content/plugins/wordcamp-payments/includes/payment-request.php index 2a16f472c..02c288cd4 100644 --- a/public_html/wp-content/plugins/wordcamp-payments/includes/payment-request.php +++ b/public_html/wp-content/plugins/wordcamp-payments/includes/payment-request.php @@ -277,6 +277,7 @@ public static function get_post_statuses() { 'draft', 'wcb-incomplete', 'wcb-pending-approval', + 'wcb-needs-followup', 'wcb-approved', 'wcb-pending-payment', 'wcb-paid', diff --git a/public_html/wp-content/plugins/wordcamp-payments/includes/reimbursement-request.php b/public_html/wp-content/plugins/wordcamp-payments/includes/reimbursement-request.php index 5de907861..65eb9644f 100644 --- a/public_html/wp-content/plugins/wordcamp-payments/includes/reimbursement-request.php +++ b/public_html/wp-content/plugins/wordcamp-payments/includes/reimbursement-request.php @@ -79,6 +79,7 @@ function get_post_statuses() { 'draft', 'wcb-incomplete', 'wcb-pending-approval', + 'wcb-needs-followup', 'wcb-approved', 'wcb-pending-payment', 'wcb-paid', @@ -162,15 +163,6 @@ function init_meta_boxes() { 'high' ); - add_meta_box( - 'wcbrr_notes', - esc_html__( 'Notes', 'wordcamporg' ), - __NAMESPACE__ . '\render_notes_metabox', - POST_TYPE, - 'side', - 'high' - ); - add_meta_box( 'wcbrr_general_information', esc_html__( 'General Information', 'wordcamporg' ), @@ -316,19 +308,6 @@ function render_status_metabox( $post ) { require_once dirname( __DIR__ ) . '/views/reimbursement-request/metabox-status.php'; } -/** - * Render the Notes metabox - * - * @param WP_Post $post - */ -function render_notes_metabox( $post ) { - wp_nonce_field( 'notes', 'notes_nonce' ); - - $existing_notes = get_post_meta( $post->ID, '_wcbrr_notes', true ); - - require_once dirname( __DIR__ ) . '/views/reimbursement-request/metabox-notes.php'; -} - /** * Render General Information Metabox * @@ -520,7 +499,6 @@ function save_request( $post_id, $post ) { } verify_metabox_nonces(); - validate_and_save_notes( $post, $_POST['wcbrr_new_note'] ); /* * We need to determine if the user is allowed to modify the request -- in terms of this plugin's post_status @@ -673,7 +651,6 @@ function render_log_metabox( $post ) { function verify_metabox_nonces() { $nonces = array( 'status_nonce', - 'notes_nonce', 'general_information_nonce', 'payment_details_nonce', 'expenses_nonce', @@ -743,95 +720,6 @@ function validate_and_save_expenses( $post_id, $expenses ) { update_post_meta( $post_id, '_wcbrr_expenses', $expenses ); } -/** - * Validate and save expense data - * - * @param WP_Post $post - * @param string $new_note_message - */ -function validate_and_save_notes( $post, $new_note_message ) { - - // Save incomplete message. - if ( isset( $_POST['wcp_mark_incomplete_notes'] ) ) { - $safe_value = ''; - if ( $post->post_status == 'wcb-incomplete' ) { - $safe_value = wp_kses( $_POST['wcp_mark_incomplete_notes'], wp_kses_allowed_html( 'strip' ) ); - } - - update_post_meta( $post->ID, '_wcp_incomplete_notes', $safe_value ); - } - - $new_note_message = sanitize_text_field( wp_unslash( $new_note_message ) ); - - if ( empty( $new_note_message ) ) { - return; - } - - $notes = get_post_meta( $post->ID, '_wcbrr_notes', true ); - if ( ! is_array( $notes ) ) { - $notes = array(); - } - - $new_note = array( - 'timestamp' => time(), - 'author_id' => get_current_user_id(), - 'message' => $new_note_message, - ); - - $notes[] = $new_note; - - update_post_meta( $post->ID, '_wcbrr_notes', $notes ); - notify_parties_of_new_note( $post, $new_note ); - - \WordCamp_Budgets::log( $post->ID, get_current_user_id(), sprintf( 'Note: %s', $new_note_message ), array( - 'action' => 'note-added', - ) ); -} - -/** - * Notify WordCamp Central or the request author when new notes are added - * - * @param WP_Post $request - * @param array $note - */ -function notify_parties_of_new_note( $request, $note ) { - $note_author = get_user_by( 'id', $note['author_id'] ); - - if ( $note_author->has_cap( 'manage_network' ) ) { - $to = \WordCamp_Budgets::get_requester_formatted_email( $request->post_author ); - $subject_prefix = sprintf( '[%s] ', get_wordcamp_name() ); - } else { - $to = 'support@wordcamp.org'; - $subject_prefix = ''; - } - - if ( ! $to ) { - return; - } - - $subject = sprintf( '%sNew note on `%s`', $subject_prefix, sanitize_text_field( $request->post_title ) ); - $note_author_name = \WordCamp_Budgets::get_requester_name( $note['author_id'] ); - $request_url = admin_url( sprintf( 'post.php?post=%s&action=edit', $request->ID ) ); - $headers = array( 'Reply-To: support@wordcamp.org' ); - - $message = sprintf( ' - %s has added the following note on the reimbursement request for %s: - - %s - - You can view the request and respond to their note at: - - %s', - sanitize_text_field( $note_author_name ), - sanitize_text_field( $request->post_title ), - sanitize_text_field( $note['message'] ), - esc_url_raw( $request_url ) - ); - $message = str_replace( "\t", '', $message ); - - wp_mail( $to, $subject, $message, $headers ); -} - /** * Notify the organizer when the status of their reimbursement changes or when notes are added * diff --git a/public_html/wp-content/plugins/wordcamp-payments/includes/wordcamp-budgets.php b/public_html/wp-content/plugins/wordcamp-payments/includes/wordcamp-budgets.php index 29b469df3..56c9a3a77 100644 --- a/public_html/wp-content/plugins/wordcamp-payments/includes/wordcamp-budgets.php +++ b/public_html/wp-content/plugins/wordcamp-payments/includes/wordcamp-budgets.php @@ -19,6 +19,8 @@ public function __construct() { add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_common_assets' ), 11 ); add_filter( 'user_has_cap', array( __CLASS__, 'user_can_view_payment_details' ), 10, 4 ); add_filter( 'default_title', array( $this, 'set_default_payments_title' ), 10, 2 ); + add_action( 'add_meta_boxes', array( $this, 'init_meta_boxes' ) ); + add_action( 'save_post', array( $this, 'save_request' ), 10, 2 ); } /** @@ -27,7 +29,7 @@ public function __construct() { public static function register_post_statuses() { // Uses core's draft status too. - register_post_status( 'wcb-incomplete', array( + register_post_status( 'wcb-incomplete', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments 'label' => esc_html_x( 'Incomplete', 'payment request', 'wordcamporg' ), 'public' => false, 'protected' => true, @@ -38,7 +40,7 @@ public static function register_post_statuses() { ), ) ); - register_post_status( 'wcb-pending-approval', array( + register_post_status( 'wcb-pending-approval', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments 'label' => esc_html_x( 'Pending Approval', 'payment request', 'wordcamporg' ), 'public' => false, 'protected' => true, @@ -49,7 +51,18 @@ public static function register_post_statuses() { ), ) ); - register_post_status( 'wcb-approved', array( + register_post_status( 'wcb-needs-followup', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments + 'label' => esc_html_x( 'Needs Follow-up', 'payment request', 'wordcamporg' ), + 'public' => false, + 'protected' => true, + 'label_count' => _nx_noop( + 'Needs Follow-up (%s)', + 'Needs Follow-up (%s)', + 'wordcamporg' + ), + ) ); + + register_post_status( 'wcb-approved', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments 'label' => esc_html_x( 'Approved', 'payment request', 'wordcamporg' ), 'public' => false, 'protected' => true, @@ -60,7 +73,7 @@ public static function register_post_statuses() { ), ) ); - register_post_status( 'wcb-pending-payment', array( + register_post_status( 'wcb-pending-payment', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments 'label' => esc_html_x( 'Payment Sent', 'payment request', 'wordcamporg' ), 'public' => false, 'protected' => true, @@ -71,7 +84,7 @@ public static function register_post_statuses() { ), ) ); - register_post_status( 'wcb-paid', array( + register_post_status( 'wcb-paid', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments 'label' => esc_html_x( 'Paid', 'payment request', 'wordcamporg' ), 'public' => false, 'protected' => true, @@ -82,7 +95,7 @@ public static function register_post_statuses() { ), ) ); - register_post_status( 'wcb-failed', array( + register_post_status( 'wcb-failed', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments 'label' => esc_html_x( 'Failed', 'payment request', 'wordcamporg' ), 'public' => false, 'protected' => true, @@ -93,7 +106,7 @@ public static function register_post_statuses() { ), ) ); - register_post_status( 'wcb-cancelled', array( + register_post_status( 'wcb-cancelled', array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments 'label' => esc_html_x( 'Cancelled', 'payment request', 'wordcamporg' ), 'public' => false, 'protected' => true, @@ -855,4 +868,186 @@ public static function log( $post_id, $user_id, $message, $data = array() ) { update_post_meta( $post_id, '_wcp_log', wp_slash( $log ) ); } + + /** + * Register meta boxes + */ + public function init_meta_boxes() { + add_meta_box( + 'wcbrr_notes', + esc_html__( 'Notes', 'wordcamporg' ), + array( $this, 'render_notes_metabox' ), + array( 'wcb_reimbursement', 'wcp_payment_request' ), + 'side', + 'default' + ); + + if ( current_user_can( 'manage_network' ) ) { + add_meta_box( + 'wcbrr_notes_private', + esc_html__( 'Private notes', 'wordcamporg' ), + array( $this, 'render_notes_private_metabox' ), + array( 'wcb_reimbursement', 'wcp_payment_request' ), + 'side', + 'default' + ); + } + } + + /** + * Render the Notes metabox + * + * @param WP_Post $post + */ + public function render_notes_metabox( $post ) { + wp_nonce_field( 'notes', 'notes_nonce' ); + + $existing_notes = get_post_meta( $post->ID, '_wcbrr_notes', true ); + + require_once dirname( __DIR__ ) . '/views/wordcamp-budgets/metabox-notes.php'; + } + + /** + * Render the Private notes metabox + * + * @param WP_Post $post + */ + public function render_notes_private_metabox( $post ) { + wp_nonce_field( 'notes_private', 'notes_private_nonce' ); + + $existing_notes = get_post_meta( $post->ID, '_wcbrr_notes_private', true ); + + require_once dirname( __DIR__ ) . '/views/wordcamp-budgets/metabox-notes-private.php'; + } + + /** + * Save the post's data + * + * @param int $post_id + * @param WP_Post $post + */ + public function save_request( $post_id, $post ) { + if ( empty( $_POST ) || ! empty( $_POST['wcpn-request-import'] ) ) { + return; + } + + check_admin_referer( str_replace( '_nonce', '', 'notes_nonce' ), 'notes_nonce' ); + $this::validate_and_save_notes( $post, $_POST['wcbrr_new_note'] ); + + if ( current_user_can( 'manage_network' ) ) { + check_admin_referer( str_replace( '_nonce', '', 'notes_private_nonce' ), 'notes_private_nonce' ); + $this::validate_and_save_notes_private( $post, $_POST['wcbrr_new_note_private'] ); + } + } + + /** + * Validate and save notes. + * + * @param WP_Post $post + * @param string $new_note_message + */ + public function validate_and_save_notes( $post, $new_note_message ) { + $new_note_message = sanitize_text_field( wp_unslash( $new_note_message ) ); + + if ( empty( $new_note_message ) ) { + return; + } + + $notes = get_post_meta( $post->ID, '_wcbrr_notes', true ); + if ( ! is_array( $notes ) ) { + $notes = array(); + } + + $new_note = array( + 'timestamp' => time(), + 'author_id' => get_current_user_id(), + 'message' => $new_note_message, + ); + + $notes[] = $new_note; + + update_post_meta( $post->ID, '_wcbrr_notes', $notes ); + $this::notify_parties_of_new_note( $post, $new_note ); + + $this::log( $post->ID, get_current_user_id(), sprintf( 'Note: %s', $new_note_message ), array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments + 'action' => 'note-added', + ) ); + } + + /** + * Validate and save private notes. + * + * @param WP_Post $post + * @param string $new_note_message + */ + public function validate_and_save_notes_private( $post, $new_note_message ) { + $new_note_message = sanitize_text_field( wp_unslash( $new_note_message ) ); + + if ( empty( $new_note_message ) ) { + return; + } + + $notes = get_post_meta( $post->ID, '_wcbrr_notes_private', true ); + if ( ! is_array( $notes ) ) { + $notes = array(); + } + + $new_note = array( + 'timestamp' => time(), + 'author_id' => get_current_user_id(), + 'message' => $new_note_message, + ); + + $notes[] = $new_note; + + update_post_meta( $post->ID, '_wcbrr_notes_private', $notes ); + + $this::log( $post->ID, get_current_user_id(), __( 'Private note', 'wordcamporg' ), array( // phpcs:ignore PEAR.Functions.FunctionCallSignature.MultipleArguments + 'action' => 'note-added', + ) ); + } + + /** + * Notify WordCamp Central or the request author when new notes are added + * + * @param WP_Post $request + * @param array $note + */ + public function notify_parties_of_new_note( $request, $note ) { + $note_author = get_user_by( 'id', $note['author_id'] ); + + if ( $note_author->has_cap( 'manage_network' ) ) { + $to = $this::get_requester_formatted_email( $request->post_author ); + $subject_prefix = sprintf( '[%s] ', get_wordcamp_name() ); + } else { + $to = 'support@wordcamp.org'; + $subject_prefix = ''; + } + + if ( ! $to ) { + return; + } + + $subject = sprintf( '%sNew note on `%s`', $subject_prefix, sanitize_text_field( $request->post_title ) ); + $note_author_name = $this::get_requester_name( $note['author_id'] ); + $request_url = admin_url( sprintf( 'post.php?post=%s&action=edit', $request->ID ) ); + $headers = array( 'Reply-To: support@wordcamp.org' ); + + $message = sprintf( ' + %s has added the following note on the reimbursement request for %s: + + %s + + You can view the request and respond to their note at: + + %s', + sanitize_text_field( $note_author_name ), + sanitize_text_field( $request->post_title ), + sanitize_text_field( $note['message'] ), + esc_url_raw( $request_url ) + ); + $message = str_replace( "\t", '', $message ); + + wp_mail( $to, $subject, $message, $headers ); + } } diff --git a/public_html/wp-content/plugins/wordcamp-payments/views/reimbursement-request/metabox-notes.php b/public_html/wp-content/plugins/wordcamp-payments/views/reimbursement-request/metabox-notes.php deleted file mode 100644 index c8b71fc64..000000000 --- a/public_html/wp-content/plugins/wordcamp-payments/views/reimbursement-request/metabox-notes.php +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - -
- - - : - - - -
- - - - -
-

- - - -

(visible to organizers)

- -

- - - - -
diff --git a/public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes-private.php b/public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes-private.php new file mode 100644 index 000000000..d68aedd66 --- /dev/null +++ b/public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes-private.php @@ -0,0 +1,36 @@ + +
+ + + : + + + +
+ + +
+

+ +

+ + + + +
diff --git a/public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes.php b/public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes.php new file mode 100644 index 000000000..83294aac0 --- /dev/null +++ b/public_html/wp-content/plugins/wordcamp-payments/views/wordcamp-budgets/metabox-notes.php @@ -0,0 +1,42 @@ + +
+ + + : + + + +
+ ID ) ) : ?> +
+

+ + + +

(visible to organizers)

+ +

+ + + + +
+