add_woocommerce_endpoints(); add_filter('query_vars', array($this, 'add_query_vars')); add_filter('woocommerce_get_query_vars', array($this, 'add_woocommerce_query_vars')); add_filter('woocommerce_account_menu_items', array($this, 'add_account_menu_items')); add_action('woocommerce_account_crewcommerce-dashboard_endpoint', array($this, 'dashboard_content')); add_action('woocommerce_account_crewcommerce-team_endpoint', array($this, 'team_management_content')); add_action('woocommerce_account_crewcommerce-approvals_endpoint', array($this, 'approvals_content')); add_action('woocommerce_account_crewcommerce-my-requests_endpoint', array($this, 'my_requests_content')); add_action('wp_ajax_crewcommerce_frontend_remove_member', array($this, 'ajax_remove_member')); add_action('wp_ajax_crewcommerce_frontend_update_role', array($this, 'ajax_update_role')); add_action('wp_ajax_crewcommerce_frontend_approve_order', array($this, 'ajax_approve_order')); add_action('wp_ajax_crewcommerce_frontend_claim_approval', array($this, 'ajax_claim_approval')); add_action('wp_ajax_crewcommerce_frontend_reject_order', array($this, 'ajax_reject_order')); add_action('wp_ajax_crewcommerce_frontend_cancel_approval', array($this, 'ajax_cancel_approval')); add_shortcode('crewcommerce_dashboard', array($this, 'dashboard_shortcode')); add_filter('woocommerce_checkout_fields', array($this, 'add_approval_notice_to_checkout')); // Tell the requester, on the order confirmation page, that the order // they just placed is held for approval rather than on its way. add_filter('woocommerce_thankyou_order_received_text', array($this, 'approval_order_received_text'), 10, 2); } public function add_woocommerce_endpoints() { foreach (self::ENDPOINTS as $endpoint) { add_rewrite_endpoint($endpoint, EP_ROOT | EP_PAGES); } // Flush once per endpoint set, so a site that was active before an // endpoint was added picks it up. if (get_option('crewcommerce_endpoints_version') !== CREWCOMMERCE_ENDPOINTS_VERSION) { flush_rewrite_rules(); update_option('crewcommerce_endpoints_version', CREWCOMMERCE_ENDPOINTS_VERSION); } add_action('template_redirect', array($this, 'handle_fallback_requests')); } public function add_query_vars($vars) { $vars[] = 'crewcommerce_action'; return $vars; } public function add_woocommerce_query_vars($vars) { foreach (self::ENDPOINTS as $endpoint) { $vars[$endpoint] = $endpoint; } return $vars; } /** * ?crewcommerce_action=… fallback for sites whose rewrite rules can't * serve the endpoints (some local development setups). */ public function handle_fallback_requests() { if (!is_account_page() || !is_user_logged_in()) { return; } $action = get_query_var('crewcommerce_action', ''); $map = array( 'dashboard' => 'dashboard_content', 'team' => 'team_management_content', 'approvals' => 'approvals_content', 'my-requests' => 'my_requests_content', ); if (isset($map[$action])) { $method = $map[$action]; add_filter('woocommerce_account_content', function () use ($method) { ob_start(); $this->$method(); return ob_get_clean(); }); } } public function add_account_menu_items($items) { $user_id = get_current_user_id(); $team_accounts = CrewCommerce_Team_Accounts::get_instance(); if (!$team_accounts->get_user_company($user_id)) { return $items; } $user_role = $team_accounts->get_user_role($user_id); $new_items = array(); foreach ($items as $key => $item) { $new_items[$key] = $item; if ('orders' !== $key) { continue; } $new_items['crewcommerce-dashboard'] = __('Team Dashboard', 'crewcommerce'); if (in_array($user_role, array('admin', 'manager'), true)) { $new_items['crewcommerce-team'] = __('Manage Team', 'crewcommerce'); $pending = count(CrewCommerce_Approvals::get_instance()->get_pending_approvals($user_id)); $new_items['crewcommerce-approvals'] = $pending > 0 /* translators: %d: number of orders waiting for approval */ ? sprintf(__('Pending Approvals (%d)', 'crewcommerce'), $pending) : __('Pending Approvals', 'crewcommerce'); } $new_items['crewcommerce-my-requests'] = __('My Requests', 'crewcommerce'); } return $new_items; } /** * URL of a CrewCommerce My Account screen. 'team' maps to the * crewcommerce-team endpoint, and so on. */ public function get_dashboard_url($action) { if (strpos(home_url(), 'localhost') !== false || strpos(home_url(), '127.0.0.1') !== false) { return add_query_arg('crewcommerce_action', $action, wc_get_page_permalink('myaccount')); } return wc_get_account_endpoint_url('crewcommerce-' . $action); } public function dashboard_shortcode($atts) { if (!is_user_logged_in()) { return '

' . esc_html__('Please log in to see your team dashboard.', 'crewcommerce') . '

'; } $atts = shortcode_atts(array('view' => 'dashboard'), $atts); ob_start(); switch ($atts['view']) { case 'team': $this->team_management_content(); break; case 'approvals': $this->approvals_content(); break; case 'my-requests': $this->my_requests_content(); break; default: $this->dashboard_content(); } return ob_get_clean(); } private function role_label($role) { $labels = array( 'admin' => __('Admin', 'crewcommerce'), 'manager' => __('Manager', 'crewcommerce'), 'requester' => __('Requester', 'crewcommerce'), ); return isset($labels[$role]) ? $labels[$role] : ucfirst((string) $role); } public function dashboard_content() { $user_id = get_current_user_id(); $team_accounts = CrewCommerce_Team_Accounts::get_instance(); $company_id = $team_accounts->get_user_company($user_id); if (!$company_id) { echo '

' . esc_html__('You are not part of a company yet.', 'crewcommerce') . '

'; return; } /** * Fires at the top of the Team Dashboard. * * @param int $user_id The viewer. * @param int $company_id Their company. */ do_action('crewcommerce_team_dashboard_before', $user_id, (int) $company_id); $company = get_post($company_id); $company_name = $company ? $company->post_title : __('Your company', 'crewcommerce'); $user_role = $team_accounts->get_user_role($user_id); $team_members = $team_accounts->get_team_members($company_id); $can_manage = in_array($user_role, array('admin', 'manager'), true); $pending = $can_manage ? CrewCommerce_Approvals::get_instance()->get_pending_approvals($user_id) : array(); $recent_orders = wc_get_orders(array( 'customer_id' => $user_id, 'limit' => 5, 'orderby' => 'date', 'order' => 'DESC', )); ?>

–

' . esc_html($this->role_label($user_role)) . '' ); ?>

#get_order_number()); ?> get_date_created())); ?>
get_status())); ?>
get_formatted_order_total()); ?>

display_name); ?> role_label($member->role)); ?>
5) : ?>

get_user_company($user_id); $user_role = $team_accounts->get_user_role($user_id); if (!$company_id || !in_array($user_role, array('admin', 'manager'), true)) { echo '

' . esc_html__('You do not have permission to manage this team.', 'crewcommerce') . '

'; return; } $company = get_post($company_id); $company_name = $company ? $company->post_title : __('your company', 'crewcommerce'); $team_members = $team_accounts->get_team_members($company_id); $admin_id = get_post_meta($company_id, '_company_admin', true); /** * Extra columns for the Manage Team table, as key => heading. Each * row's cell is rendered by the crewcommerce_manage_team_column_{key} * action. * * @param array $columns Extra columns. * @param int $company_id The company. */ $extra_columns = (array) apply_filters('crewcommerce_manage_team_columns', array(), $company_id); ?>

user_id); $is_current_user = ($member->user_id == $user_id); ?>
display_name); ?> user_email); ?> role_label($member->role)); ?> created_at))); ?>
get_user_role($user_id); if (!in_array($user_role, array('admin', 'manager'), true)) { echo '

' . esc_html__('You do not have permission to approve orders.', 'crewcommerce') . '

'; return; } $approvals = CrewCommerce_Approvals::get_instance(); $pending = $approvals->get_pending_approvals($user_id); $stale_hours = $approvals->get_stale_hours(); $now = current_time('timestamp'); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested -- compared with created_at, which is stored in site time. ?>

order_id); if (!$order) { continue; } $requester = get_user_by('ID', $workflow->requester_id); $created = strtotime($workflow->created_at); $is_stale = ($now - $created) >= ($stale_hours * HOUR_IN_SECONDS); $is_unassigned = ('unassigned' === $workflow->status); $classes = 'approval-card' . ($is_stale ? ' approval-card-stale' : '') . ($is_unassigned ? ' approval-card-unassigned' : ''); ?>

get_order_number())); ?>

display_name : __('Unknown user', 'crewcommerce')); ?>
get_formatted_order_total()); ?>
get_item_count()); ?>

    get_items() as $item) : ?>
  • get_name()); ?> × get_quantity()); ?>

user_can_decide($workflow, get_current_user_id())) : ?>

approver_id); echo esc_html($decider /* translators: %s: name of the person who decides this order */ ? sprintf(__('Waiting for %s to approve or reject this order.', 'crewcommerce'), $decider->display_name) : __('Waiting for another approver.', 'crewcommerce')); ?>

get_user_role($user_id); if ($role && in_array($role, CrewCommerce_Approvals::get_instance()->get_roles_checked(), true)) { // This filter runs more than once per page; only add the notice once. $notice = __('Some orders need your manager’s approval before they are processed.', 'crewcommerce'); if (!wc_has_notice($notice, 'notice')) { wc_add_notice($notice, 'notice'); } } return $fields; } /** * Order confirmation text for an order that was placed but is held for * approval. Runs on both the classic and block confirmation pages. */ public function approval_order_received_text($text, $order) { if (!($order instanceof WC_Order) || !$order->has_status('pending-approval')) { return $text; } $settings = get_option('crewcommerce_settings', array()); if (!empty($settings['approval_required_message'])) { return esc_html($settings['approval_required_message']); } $approvals = CrewCommerce_Approvals::get_instance(); $workflow = $approvals->get_order_workflow($order->get_id()); $approver = ($workflow && $workflow->approver_id) ? get_userdata($workflow->approver_id) : false; $message = $approver ? sprintf( /* translators: %s: approver's name */ __('Thank you — your order has been received. It needs approval from %s before it is processed, and they have been emailed about it.', 'crewcommerce'), $approver->display_name ) : __('Thank you — your order has been received. It needs approval before it is processed.', 'crewcommerce'); // Nothing is charged at checkout for a held order. if ('pending' === $approvals->status_after_approval($order)) { $message .= ' ' . __('You have not been charged yet — once it is approved, we will email you a link to pay.', 'crewcommerce'); } return esc_html($message) . ' ' . sprintf( '%s', esc_url(wc_get_account_endpoint_url('crewcommerce-my-requests')), esc_html__('Follow it under My Requests.', 'crewcommerce') ); } /** * Read a value from the current AJAX request. */ private function post_value($key, $type = 'int') { // phpcs:disable WordPress.Security.NonceVerification.Missing -- every caller checks the nonce first. if (!isset($_POST[$key])) { return 'int' === $type ? 0 : ''; } $value = wp_unslash($_POST[$key]); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below by type. // phpcs:enable switch ($type) { case 'key': return sanitize_key($value); case 'textarea': return sanitize_textarea_field($value); default: return absint($value); } } /** * The viewer's company, when they are its Manager or Admin. Ends the * request with an error otherwise. */ private function require_team_manager() { $user_id = get_current_user_id(); $team_accounts = CrewCommerce_Team_Accounts::get_instance(); $company_id = (int) $team_accounts->get_user_company($user_id); if (!$company_id || !in_array($team_accounts->get_user_role($user_id), array('admin', 'manager'), true)) { wp_send_json_error(__('Insufficient permissions.', 'crewcommerce')); } return $company_id; } public function ajax_remove_member() { check_ajax_referer('crewcommerce_nonce', 'nonce'); $company_id = $this->require_team_manager(); $user_id = $this->post_value('user_id'); if ($user_id === get_current_user_id()) { wp_send_json_error(__('You cannot remove yourself.', 'crewcommerce')); } $team_accounts = CrewCommerce_Team_Accounts::get_instance(); if ((int) $team_accounts->get_user_company($user_id) !== $company_id) { wp_send_json_error(__('That person is not on your team.', 'crewcommerce')); } // Only an Admin can remove or change another Admin. if ('admin' === $team_accounts->get_user_role($user_id) && 'admin' !== $team_accounts->get_user_role(get_current_user_id())) { wp_send_json_error(__('Only the company Admin can remove or change another Admin.', 'crewcommerce')); } $result = $team_accounts->remove_team_member($user_id, $company_id); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } elseif ($result) { wp_send_json_success(__('Team member removed.', 'crewcommerce')); } wp_send_json_error(__('Could not remove the team member.', 'crewcommerce')); } public function ajax_update_role() { check_ajax_referer('crewcommerce_nonce', 'nonce'); $company_id = $this->require_team_manager(); $team_accounts = CrewCommerce_Team_Accounts::get_instance(); $user_id = $this->post_value('user_id'); $new_role = $this->post_value('role', 'key'); if ((int) $team_accounts->get_user_company($user_id) !== $company_id) { wp_send_json_error(__('That person is not on your team.', 'crewcommerce')); } // Only an Admin can remove or change another Admin. if ('admin' === $team_accounts->get_user_role($user_id) && 'admin' !== $team_accounts->get_user_role(get_current_user_id())) { wp_send_json_error(__('Only the company Admin can remove or change another Admin.', 'crewcommerce')); } // The Admin seat only changes hands by demoting the current Admin, // never by assigning "admin" to someone else directly. if ('admin' === $new_role && get_post_meta($company_id, '_company_admin', true) != $user_id) { wp_send_json_error(__('The Admin role changes hands by changing the current Admin’s role, not by assigning it to someone else.', 'crewcommerce')); } $result = $team_accounts->update_member_role($user_id, $company_id, $new_role); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } elseif ($result) { wp_send_json_success(__('Role updated.', 'crewcommerce')); } wp_send_json_error(__('Could not update the role.', 'crewcommerce')); } private function require_approver() { $role = CrewCommerce_Team_Accounts::get_instance()->get_user_role(get_current_user_id()); if (!in_array($role, array('admin', 'manager'), true)) { wp_send_json_error(__('Insufficient permissions.', 'crewcommerce')); } } public function ajax_approve_order() { check_ajax_referer('crewcommerce_nonce', 'nonce'); $this->require_approver(); $workflow_id = $this->post_value('workflow_id'); $approvals = CrewCommerce_Approvals::get_instance(); if ($approvals->approve_order($workflow_id, get_current_user_id(), $this->post_value('notes', 'textarea'))) { wp_send_json_success(array('message' => $approvals->get_decision_message($workflow_id, 'approved'))); } wp_send_json_error($approvals->get_last_error() ?: __('Could not approve the order.', 'crewcommerce')); } public function ajax_claim_approval() { check_ajax_referer('crewcommerce_nonce', 'nonce'); $this->require_approver(); $result = CrewCommerce_Approvals::get_instance()->claim_approval($this->post_value('workflow_id'), get_current_user_id()); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } wp_send_json_success(__('Order claimed. You can now approve or reject it.', 'crewcommerce')); } public function ajax_reject_order() { check_ajax_referer('crewcommerce_nonce', 'nonce'); $this->require_approver(); $workflow_id = $this->post_value('workflow_id'); $notes = $this->post_value('notes', 'textarea'); if ('' === $notes) { wp_send_json_error(__('A rejection reason is required.', 'crewcommerce')); } $approvals = CrewCommerce_Approvals::get_instance(); if ($approvals->reject_order($workflow_id, get_current_user_id(), $notes)) { wp_send_json_success(array('message' => $approvals->get_decision_message($workflow_id, 'rejected'))); } wp_send_json_error($approvals->get_last_error() ?: __('Could not reject the order.', 'crewcommerce')); } public function ajax_cancel_approval() { check_ajax_referer('crewcommerce_nonce', 'nonce'); $result = CrewCommerce_Approvals::get_instance()->cancel_pending_approval($this->post_value('workflow_id'), get_current_user_id()); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } wp_send_json_success(__('Approval request cancelled. Your order is on hold.', 'crewcommerce')); } /** * My Requests: the requester's orders and where each one stands. */ public function my_requests_content() { $user_id = get_current_user_id(); $team_accounts = CrewCommerce_Team_Accounts::get_instance(); $company_id = (int) $team_accounts->get_user_company($user_id); if (!$company_id) { echo '

' . esc_html__('You are not part of a company yet.', 'crewcommerce') . '

'; return; } $approvals = CrewCommerce_Approvals::get_instance(); $workflows = $approvals->get_requester_workflows($user_id, 50); // Also list orders that didn't need approval. $workflow_order_ids = array_map(function ($wf) { return (int) $wf->order_id; }, $workflows); foreach (wc_get_orders(array('customer_id' => $user_id, 'limit' => 50, 'orderby' => 'date', 'order' => 'DESC')) as $bare_order) { if (in_array((int) $bare_order->get_id(), $workflow_order_ids, true)) { continue; } $stub = new stdClass(); $stub->id = 0; $stub->order_id = $bare_order->get_id(); $stub->requester_id = $user_id; $stub->approver_id = null; $stub->approver_name = null; $stub->status = 'no_workflow'; $stub->notes = ''; $stub->created_at = $bare_order->get_date_created() ? $bare_order->get_date_created()->date('Y-m-d H:i:s') : ''; $workflows[] = $stub; } $status_labels = array( 'pending' => array(__('Pending Approval', 'crewcommerce'), 'status-pending'), 'unassigned' => array(__('Unassigned', 'crewcommerce'), 'status-pending'), 'approved' => array(__('Approved', 'crewcommerce'), 'status-approved'), 'rejected' => array(__('Rejected', 'crewcommerce'), 'status-rejected'), 'cancelled' => array(__('Cancelled', 'crewcommerce'), 'status-cancelled'), 'no_workflow' => array(__('Placed', 'crewcommerce'), 'status-placed'), ); $now = current_time('timestamp'); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested -- compared with created_at, stored in site time. ?>

order_id); if (!$order) { continue; } $status_info = isset($status_labels[$wf->status]) ? $status_labels[$wf->status] : array(ucfirst($wf->status), ''); if ('approved' === $wf->status && $order->needs_payment()) { $status_info = array(__('Awaiting Payment', 'crewcommerce'), 'status-pending'); } $is_waiting = in_array($wf->status, array('pending', 'unassigned'), true); ?>
get_order_number())); ?> get_formatted_order_total()); ?>
created_at)))); ?> approver_name) : ?> approver_name)); ?> status) : ?> created_at), $now))); ?>
notes && 'pending' !== $wf->status) : ?>
notes); ?>
    get_items() as $item) : ?>
  • get_name()); ?> × get_quantity()); ?>
status) : ?> status && $order->needs_payment()) : ?>
id) { /** This action is documented in approvals_content(). */ do_action('crewcommerce_approval_card_end', $wf, 'requester'); } ?>