class-wc-admin-dashboard.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <?php
  2. /**
  3. * Admin Dashboard
  4. *
  5. * @author WooThemes
  6. * @category Admin
  7. * @package WooCommerce/Admin
  8. * @version 2.1.0
  9. */
  10. if ( ! defined( 'ABSPATH' ) ) {
  11. exit; // Exit if accessed directly
  12. }
  13. if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
  14. /**
  15. * WC_Admin_Dashboard Class.
  16. */
  17. class WC_Admin_Dashboard {
  18. /**
  19. * Hook in tabs.
  20. */
  21. public function __construct() {
  22. // Only hook in admin parts if the user has admin access
  23. if ( current_user_can( 'view_woocommerce_reports' ) || current_user_can( 'manage_woocommerce' ) || current_user_can( 'publish_shop_orders' ) ) {
  24. // If on network admin, only load the widget that works in that context and skip the rest.
  25. if ( is_multisite() && is_network_admin() ) {
  26. add_action( 'wp_network_dashboard_setup', array( $this, 'register_network_order_widget' ) );
  27. } else {
  28. add_action( 'wp_dashboard_setup', array( $this, 'init' ) );
  29. }
  30. }
  31. }
  32. /**
  33. * Init dashboard widgets.
  34. */
  35. public function init() {
  36. if ( current_user_can( 'publish_shop_orders' ) && post_type_supports( 'product', 'comments' ) ) {
  37. wp_add_dashboard_widget( 'woocommerce_dashboard_recent_reviews', __( 'WooCommerce recent reviews', 'woocommerce' ), array( $this, 'recent_reviews' ) );
  38. }
  39. wp_add_dashboard_widget( 'woocommerce_dashboard_status', __( 'WooCommerce status', 'woocommerce' ), array( $this, 'status_widget' ) );
  40. // Network Order Widget.
  41. if ( is_multisite() ) {
  42. $this->register_network_order_widget();
  43. }
  44. }
  45. /**
  46. * Register the network order dashboard widget.
  47. */
  48. public function register_network_order_widget() {
  49. wp_add_dashboard_widget( 'woocommerce_network_orders', __( 'WooCommerce network orders', 'woocommerce' ), array( $this, 'network_orders' ) );
  50. }
  51. /**
  52. * Get top seller from DB.
  53. *
  54. * @return object
  55. */
  56. private function get_top_seller() {
  57. global $wpdb;
  58. $query = array();
  59. $query['fields'] = "SELECT SUM( order_item_meta.meta_value ) as qty, order_item_meta_2.meta_value as product_id
  60. FROM {$wpdb->posts} as posts";
  61. $query['join'] = "INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_id ";
  62. $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id ";
  63. $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_2 ON order_items.order_item_id = order_item_meta_2.order_item_id ";
  64. $query['where'] = "WHERE posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) ";
  65. $query['where'] .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) ";
  66. $query['where'] .= "AND order_item_meta.meta_key = '_qty' ";
  67. $query['where'] .= "AND order_item_meta_2.meta_key = '_product_id' ";
  68. $query['where'] .= "AND posts.post_date >= '" . date( 'Y-m-01', current_time( 'timestamp' ) ) . "' ";
  69. $query['where'] .= "AND posts.post_date <= '" . date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) . "' ";
  70. $query['groupby'] = 'GROUP BY product_id';
  71. $query['orderby'] = 'ORDER BY qty DESC';
  72. $query['limits'] = 'LIMIT 1';
  73. return $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) );
  74. }
  75. /**
  76. * Get sales report data.
  77. *
  78. * @return object
  79. */
  80. private function get_sales_report_data() {
  81. include_once dirname( __FILE__ ) . '/reports/class-wc-report-sales-by-date.php';
  82. $sales_by_date = new WC_Report_Sales_By_Date();
  83. $sales_by_date->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) );
  84. $sales_by_date->end_date = current_time( 'timestamp' );
  85. $sales_by_date->chart_groupby = 'day';
  86. $sales_by_date->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)';
  87. return $sales_by_date->get_report_data();
  88. }
  89. /**
  90. * Show status widget.
  91. */
  92. public function status_widget() {
  93. include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php';
  94. $reports = new WC_Admin_Report();
  95. echo '<ul class="wc_status_list">';
  96. if ( current_user_can( 'view_woocommerce_reports' ) && ( $report_data = $this->get_sales_report_data() ) ) {
  97. ?>
  98. <li class="sales-this-month">
  99. <a href="<?php echo admin_url( 'admin.php?page=wc-reports&tab=orders&range=month' ); ?>">
  100. <?php echo $reports->sales_sparkline( '', max( 7, date( 'd', current_time( 'timestamp' ) ) ) ); ?>
  101. <?php
  102. /* translators: %s: net sales */
  103. printf(
  104. __( '%s net sales this month', 'woocommerce' ),
  105. '<strong>' . wc_price( $report_data->net_sales ) . '</strong>'
  106. );
  107. ?>
  108. </a>
  109. </li>
  110. <?php
  111. }
  112. if ( current_user_can( 'view_woocommerce_reports' ) && ( $top_seller = $this->get_top_seller() ) && $top_seller->qty ) {
  113. ?>
  114. <li class="best-seller-this-month">
  115. <a href="<?php echo admin_url( 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids=' . $top_seller->product_id ); ?>">
  116. <?php echo $reports->sales_sparkline( $top_seller->product_id, max( 7, date( 'd', current_time( 'timestamp' ) ) ), 'count' ); ?>
  117. <?php
  118. /* translators: 1: top seller product title 2: top seller quantity */
  119. printf(
  120. __( '%1$s top seller this month (sold %2$d)', 'woocommerce' ),
  121. '<strong>' . get_the_title( $top_seller->product_id ) . '</strong>',
  122. $top_seller->qty
  123. );
  124. ?>
  125. </a>
  126. </li>
  127. <?php
  128. }
  129. $this->status_widget_order_rows();
  130. $this->status_widget_stock_rows();
  131. do_action( 'woocommerce_after_dashboard_status_widget', $reports );
  132. echo '</ul>';
  133. }
  134. /**
  135. * Show order data is status widget.
  136. */
  137. private function status_widget_order_rows() {
  138. if ( ! current_user_can( 'edit_shop_orders' ) ) {
  139. return;
  140. }
  141. $on_hold_count = 0;
  142. $processing_count = 0;
  143. foreach ( wc_get_order_types( 'order-count' ) as $type ) {
  144. $counts = (array) wp_count_posts( $type );
  145. $on_hold_count += isset( $counts['wc-on-hold'] ) ? $counts['wc-on-hold'] : 0;
  146. $processing_count += isset( $counts['wc-processing'] ) ? $counts['wc-processing'] : 0;
  147. }
  148. ?>
  149. <li class="processing-orders">
  150. <a href="<?php echo admin_url( 'edit.php?post_status=wc-processing&post_type=shop_order' ); ?>">
  151. <?php
  152. /* translators: %s: order count */
  153. printf(
  154. _n( '<strong>%s order</strong> awaiting processing', '<strong>%s orders</strong> awaiting processing', $processing_count, 'woocommerce' ),
  155. $processing_count
  156. );
  157. ?>
  158. </a>
  159. </li>
  160. <li class="on-hold-orders">
  161. <a href="<?php echo admin_url( 'edit.php?post_status=wc-on-hold&post_type=shop_order' ); ?>">
  162. <?php
  163. /* translators: %s: order count */
  164. printf(
  165. _n( '<strong>%s order</strong> on-hold', '<strong>%s orders</strong> on-hold', $on_hold_count, 'woocommerce' ),
  166. $on_hold_count
  167. );
  168. ?>
  169. </a>
  170. </li>
  171. <?php
  172. }
  173. /**
  174. * Show stock data is status widget.
  175. */
  176. private function status_widget_stock_rows() {
  177. global $wpdb;
  178. // Get products using a query - this is too advanced for get_posts :(
  179. $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
  180. $nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
  181. $transient_name = 'wc_low_stock_count';
  182. if ( false === ( $lowinstock_count = get_transient( $transient_name ) ) ) {
  183. $query_from = apply_filters(
  184. 'woocommerce_report_low_in_stock_query_from',
  185. "FROM {$wpdb->posts} as posts
  186. INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id
  187. INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id
  188. WHERE 1=1
  189. AND posts.post_type IN ( 'product', 'product_variation' )
  190. AND posts.post_status = 'publish'
  191. AND postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes'
  192. AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$stock}'
  193. AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) > '{$nostock}'"
  194. );
  195. $lowinstock_count = absint( $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ) );
  196. set_transient( $transient_name, $lowinstock_count, DAY_IN_SECONDS * 30 );
  197. }
  198. $transient_name = 'wc_outofstock_count';
  199. if ( false === ( $outofstock_count = get_transient( $transient_name ) ) ) {
  200. $query_from = apply_filters(
  201. 'woocommerce_report_out_of_stock_query_from',
  202. "FROM {$wpdb->posts} as posts
  203. INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id
  204. INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id
  205. WHERE 1=1
  206. AND posts.post_type IN ( 'product', 'product_variation' )
  207. AND posts.post_status = 'publish'
  208. AND postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes'
  209. AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$nostock}'"
  210. );
  211. $outofstock_count = absint( $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ) );
  212. set_transient( $transient_name, $outofstock_count, DAY_IN_SECONDS * 30 );
  213. }
  214. ?>
  215. <li class="low-in-stock">
  216. <a href="<?php echo admin_url( 'admin.php?page=wc-reports&tab=stock&report=low_in_stock' ); ?>">
  217. <?php
  218. /* translators: %s: order count */
  219. printf(
  220. _n( '<strong>%s product</strong> low in stock', '<strong>%s products</strong> low in stock', $lowinstock_count, 'woocommerce' ),
  221. $lowinstock_count
  222. );
  223. ?>
  224. </a>
  225. </li>
  226. <li class="out-of-stock">
  227. <a href="<?php echo admin_url( 'admin.php?page=wc-reports&tab=stock&report=out_of_stock' ); ?>">
  228. <?php
  229. /* translators: %s: order count */
  230. printf(
  231. _n( '<strong>%s product</strong> out of stock', '<strong>%s products</strong> out of stock', $outofstock_count, 'woocommerce' ),
  232. $outofstock_count
  233. );
  234. ?>
  235. </a>
  236. </li>
  237. <?php
  238. }
  239. /**
  240. * Recent reviews widget.
  241. */
  242. public function recent_reviews() {
  243. global $wpdb;
  244. $query_from = apply_filters(
  245. 'woocommerce_report_recent_reviews_query_from',
  246. "FROM {$wpdb->comments} comments
  247. LEFT JOIN {$wpdb->posts} posts ON (comments.comment_post_ID = posts.ID)
  248. WHERE comments.comment_approved = '1'
  249. AND comments.comment_type = ''
  250. AND posts.post_password = ''
  251. AND posts.post_type = 'product'
  252. AND comments.comment_parent = 0
  253. ORDER BY comments.comment_date_gmt DESC
  254. LIMIT 5"
  255. );
  256. $comments = $wpdb->get_results(
  257. "SELECT posts.ID, posts.post_title, comments.comment_author, comments.comment_ID, comments.comment_content {$query_from};"
  258. );
  259. if ( $comments ) {
  260. echo '<ul>';
  261. foreach ( $comments as $comment ) {
  262. echo '<li>';
  263. echo get_avatar( $comment->comment_author, '32' );
  264. $rating = intval( get_comment_meta( $comment->comment_ID, 'rating', true ) );
  265. /* translators: %s: rating */
  266. echo '<div class="star-rating"><span style="width:' . ( $rating * 20 ) . '%">' . sprintf( __( '%s out of 5', 'woocommerce' ), $rating ) . '</span></div>';
  267. /* translators: %s: review author */
  268. echo '<h4 class="meta"><a href="' . get_permalink( $comment->ID ) . '#comment-' . absint( $comment->comment_ID ) . '">' . esc_html( apply_filters( 'woocommerce_admin_dashboard_recent_reviews', $comment->post_title, $comment ) ) . '</a> ' . sprintf( __( 'reviewed by %s', 'woocommerce' ), esc_html( $comment->comment_author ) ) . '</h4>';
  269. echo '<blockquote>' . wp_kses_data( $comment->comment_content ) . '</blockquote></li>';
  270. }
  271. echo '</ul>';
  272. } else {
  273. echo '<p>' . __( 'There are no product reviews yet.', 'woocommerce' ) . '</p>';
  274. }
  275. }
  276. /**
  277. * Network orders widget.
  278. */
  279. public function network_orders() {
  280. $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
  281. wp_enqueue_style( 'wc-network-orders', WC()->plugin_url() . '/assets/css/network-order-widget.css', array(), WC_VERSION );
  282. wp_enqueue_script( 'wc-network-orders', WC()->plugin_url() . '/assets/js/admin/network-orders' . $suffix . '.js', array( 'jquery', 'underscore' ), WC_VERSION, true );
  283. $user = wp_get_current_user();
  284. $blogs = get_blogs_of_user( $user->ID );
  285. $blog_ids = wp_list_pluck( $blogs, 'userblog_id' );
  286. wp_localize_script(
  287. 'wc-network-orders', 'woocommerce_network_orders', array(
  288. 'nonce' => wp_create_nonce( 'wp_rest' ),
  289. 'sites' => array_values( $blog_ids ),
  290. 'order_endpoint' => get_rest_url( null, 'wc/v2/orders/network' ),
  291. )
  292. );
  293. ?>
  294. <div class="post-type-shop_order">
  295. <div id="woocommerce-network-order-table-loading" class="woocommerce-network-order-table-loading is-active">
  296. <p>
  297. <span class="spinner is-active"></span> <?php esc_html_e( 'Loading network orders', 'woocommerce' ); ?>
  298. </p>
  299. </div>
  300. <table id="woocommerce-network-order-table" class="woocommerce-network-order-table">
  301. <thead>
  302. <tr>
  303. <td><?php esc_html_e( 'Order', 'woocommerce' ); ?></td>
  304. <td><?php esc_html_e( 'Status', 'woocommerce' ); ?></td>
  305. <td><?php esc_html_e( 'Total', 'woocommerce' ); ?></td>
  306. </tr>
  307. </thead>
  308. <tbody id="network-orders-tbody">
  309. </tbody>
  310. </table>
  311. <div id="woocommerce-network-orders-no-orders" class="woocommerce-network-orders-no-orders">
  312. <p>
  313. <?php esc_html_e( 'No orders found', 'woocommerce' ); ?>
  314. </p>
  315. </div>
  316. <script type="text/template" id="network-orders-row-template">
  317. <tr>
  318. <td>
  319. <a href="<%- edit_url %>" class="order-view"><strong>#<%- number %> <%- customer %></strong></a>
  320. <br>
  321. <em>
  322. <%- blog.blogname %>
  323. </em>
  324. </td>
  325. <td>
  326. <mark class="order-status status-<%- status %>"><span><%- status_name %></span></mark>
  327. </td>
  328. <td>
  329. <%= formatted_total %>
  330. </td>
  331. </tr>
  332. </script>
  333. </div>
  334. <?php
  335. }
  336. }
  337. endif;
  338. return new WC_Admin_Dashboard();