Merge branch 'master' into fix/1307

This commit is contained in:
Claudio Sanches 2019-02-05 16:20:08 -02:00
commit d26e82b526
30 changed files with 2221 additions and 209 deletions

View File

@ -6,12 +6,10 @@ import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { format as formatDate } from '@wordpress/date';
import PropTypes from 'prop-types';
import { find, get } from 'lodash';
/**
* WooCommerce dependencies
*/
import { flattenFilters } from '@woocommerce/navigation';
import {
getAllowedIntervalsForQuery,
getCurrentDates,
@ -28,66 +26,37 @@ import { Chart } from '@woocommerce/components';
import { getReportChartData, getTooltipValueFormat } from 'wc-api/reports/utils';
import ReportError from 'analytics/components/report-error';
import withSelect from 'wc-api/with-select';
export const DEFAULT_FILTER = 'all';
import { getChartMode } from './utils';
/**
* Component that renders the chart in reports.
*/
export class ReportChart extends Component {
getSelectedFilter( filters, query ) {
if ( filters.length === 0 ) {
return null;
}
const filterConfig = filters.pop();
if ( filterConfig.showFilters( query ) ) {
const allFilters = flattenFilters( filterConfig.filters );
const value = query[ filterConfig.param ] || DEFAULT_FILTER;
const selectedFilter = find( allFilters, { value } );
const selectedFilterParam = get( selectedFilter, [ 'settings', 'param' ] );
if ( ! selectedFilterParam || Object.keys( query ).includes( selectedFilterParam ) ) {
return selectedFilter;
}
}
return this.getSelectedFilter( filters, query );
getItemChartData() {
const { primaryData, selectedChart } = this.props;
const chartData = primaryData.data.intervals.map( function( interval ) {
const intervalData = {};
interval.subtotals.segments.forEach( function( segment ) {
if ( segment.segment_label ) {
const label = intervalData[ segment.segment_label ]
? segment.segment_label + ' (#' + segment.segment_id + ')'
: segment.segment_label;
intervalData[ label ] = {
value: segment.subtotals[ selectedChart.key ] || 0,
};
}
} );
return {
date: formatDate( 'Y-m-d\\TH:i:s', interval.date_start ),
...intervalData,
};
} );
return chartData;
}
getChartMode() {
const { filters, query } = this.props;
if ( ! filters ) {
return;
}
const clonedFilters = filters.slice( 0 );
const selectedFilter = this.getSelectedFilter( clonedFilters, query );
return get( selectedFilter, [ 'chartMode' ] );
}
render() {
const {
interactiveLegend,
itemsLabel,
legendPosition,
mode,
path,
primaryData,
query,
secondaryData,
selectedChart,
showHeaderControls,
} = this.props;
if ( primaryData.isError || secondaryData.isError ) {
return <ReportError isError />;
}
getTimeChartData() {
const { query, primaryData, secondaryData, selectedChart } = this.props;
const currentInterval = getIntervalForQuery( query );
const allowedIntervals = getAllowedIntervalsForQuery( query );
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
const { primary, secondary } = getCurrentDates( query );
const primaryKey = `${ primary.label } (${ primary.range })`;
const secondaryKey = `${ secondary.label } (${ secondary.range })`;
@ -115,6 +84,23 @@ export class ReportChart extends Component {
};
} );
return chartData;
}
renderChart( mode, isRequesting, chartData ) {
const {
interactiveLegend,
itemsLabel,
legendPosition,
path,
query,
selectedChart,
showHeaderControls,
primaryData,
} = this.props;
const currentInterval = getIntervalForQuery( query );
const allowedIntervals = getAllowedIntervalsForQuery( query );
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
return (
<Chart
allowedIntervals={ allowedIntervals }
@ -122,16 +108,16 @@ export class ReportChart extends Component {
dateParser={ '%Y-%m-%dT%H:%M:%S' }
interactiveLegend={ interactiveLegend }
interval={ currentInterval }
isRequesting={ primaryData.isRequesting || secondaryData.isRequesting }
isRequesting={ isRequesting }
itemsLabel={ itemsLabel }
legendPosition={ legendPosition }
mode={ mode || this.getChartMode() }
mode={ mode }
path={ path }
query={ query }
showHeaderControls={ showHeaderControls }
title={ selectedChart.label }
tooltipLabelFormat={ formats.tooltipLabelFormat }
tooltipTitle={ selectedChart.label }
tooltipTitle={ ( 'time-comparison' === mode && selectedChart.label ) || null }
tooltipValueFormat={ getTooltipValueFormat( selectedChart.type ) }
type={ getChartTypeForQuery( query ) }
valueType={ selectedChart.type }
@ -140,6 +126,40 @@ export class ReportChart extends Component {
/>
);
}
renderItemComparison() {
const { primaryData } = this.props;
if ( primaryData.isError ) {
return <ReportError isError />;
}
const isRequesting = primaryData.isRequesting;
const chartData = this.getItemChartData();
return this.renderChart( 'item-comparison', isRequesting, chartData );
}
renderTimeComparison() {
const { primaryData, secondaryData } = this.props;
if ( ! primaryData || primaryData.isError || secondaryData.isError ) {
return <ReportError isError />;
}
const isRequesting = primaryData.isRequesting || secondaryData.isRequesting;
const chartData = this.getTimeChartData();
return this.renderChart( 'time-comparison', isRequesting, chartData );
}
render() {
const { mode } = this.props;
if ( 'item-comparison' === mode ) {
return this.renderItemComparison();
}
return this.renderTimeComparison();
}
}
ReportChart.propTypes = {
@ -171,7 +191,7 @@ ReportChart.propTypes = {
/**
* Secondary data to display in the chart.
*/
secondaryData: PropTypes.object.isRequired,
secondaryData: PropTypes.object,
/**
* Properties of the selected chart.
*/
@ -180,10 +200,21 @@ ReportChart.propTypes = {
export default compose(
withSelect( ( select, props ) => {
const { query, endpoint } = props;
const { query, endpoint, filters } = props;
const chartMode = props.mode || getChartMode( filters, query ) || 'time-comparison';
if ( 'item-comparison' === chartMode ) {
const primaryData = getReportChartData( endpoint, 'primary', query, select );
return {
mode: chartMode,
primaryData,
};
}
const primaryData = getReportChartData( endpoint, 'primary', query, select );
const secondaryData = getReportChartData( endpoint, 'secondary', query, select );
return {
mode: chartMode,
primaryData,
secondaryData,
};

View File

@ -8,6 +8,7 @@ import { shallow } from 'enzyme';
* Internal dependencies
*/
import { ReportChart } from '../';
import { getChartMode } from '../utils';
jest.mock( '@woocommerce/components', () => ( {
...require.requireActual( '@woocommerce/components' ),
@ -30,7 +31,7 @@ const selectedChart = {
};
describe( 'ReportChart', () => {
test( 'should not set the mode prop by default', () => {
test( 'should set the time-comparison mode prop by default', () => {
const reportChart = shallow(
<ReportChart
path={ path }
@ -42,7 +43,7 @@ describe( 'ReportChart', () => {
);
const chart = reportChart.find( 'Chart' );
expect( chart.props().mode ).toBeUndefined();
expect( chart.props().mode ).toEqual( 'time-comparison' );
} );
test( 'should set the mode prop depending on the active filter', () => {
@ -62,19 +63,7 @@ describe( 'ReportChart', () => {
},
];
const query = { filter: 'lorem-ipsum', filter2: 'ipsum-lorem' };
const reportChart = shallow(
<ReportChart
filters={ filters }
path={ path }
primaryData={ data }
query={ query }
secondaryData={ data }
selectedChart={ selectedChart }
/>
);
const chart = reportChart.find( 'Chart' );
expect( chart.props().mode ).toEqual( 'item-comparison' );
const mode = getChartMode( filters, query );
expect( mode ).toEqual( 'item-comparison' );
} );
} );

View File

@ -0,0 +1,44 @@
/** @format */
/**
* External dependencies
*/
import { find, get } from 'lodash';
/**
* WooCommerce dependencies
*/
import { flattenFilters } from '@woocommerce/navigation';
export const DEFAULT_FILTER = 'all';
export function getSelectedFilter( filters, query, selectedFilterArgs = {} ) {
if ( filters.length === 0 ) {
return null;
}
const filterConfig = filters.pop();
if ( filterConfig.showFilters( query, selectedFilterArgs ) ) {
const allFilters = flattenFilters( filterConfig.filters );
const value = query[ filterConfig.param ] || DEFAULT_FILTER;
const selectedFilter = find( allFilters, { value } );
const selectedFilterParam = get( selectedFilter, [ 'settings', 'param' ] );
if ( ! selectedFilterParam || Object.keys( query ).includes( selectedFilterParam ) ) {
return selectedFilter;
}
}
return getSelectedFilter( filters, query, selectedFilterArgs );
}
export function getChartMode( filters, query, selectedFilterArgs ) {
if ( ! filters ) {
return;
}
const clonedFilters = filters.slice( 0 );
const selectedFilter = getSelectedFilter( clonedFilters, query, selectedFilterArgs );
return get( selectedFilter, [ 'chartMode' ] );
}

View File

@ -29,7 +29,7 @@ export const charts = [
const filterConfig = {
label: __( 'Show', 'wc-admin' ),
staticParams: [ 'chart' ],
staticParams: [],
param: 'filter',
showFilters: () => true,
filters: [
@ -114,19 +114,20 @@ const filterConfig = {
label: __( 'Top Products by Items Sold', 'wc-admin' ),
value: 'top_items',
chartMode: 'item-comparison',
query: { orderby: 'items_sold', order: 'desc' },
query: { orderby: 'items_sold', order: 'desc', chart: 'items_sold' },
},
{
label: __( 'Top Products by Net Revenue', 'wc-admin' ),
value: 'top_sales',
chartMode: 'item-comparison',
query: { orderby: 'net_revenue', order: 'desc' },
query: { orderby: 'net_revenue', order: 'desc', chart: 'net_revenue' },
},
],
};
const variationsConfig = {
showFilters: query => 'single_product' === query.filter && !! query.products,
showFilters: query =>
'single_product' === query.filter && !! query.products && query[ 'is-variable' ],
staticParams: [ 'filter', 'products' ],
param: 'filter-variations',
filters: [
@ -151,13 +152,13 @@ const variationsConfig = {
label: __( 'Top Variations by Items Sold', 'wc-admin' ),
chartMode: 'item-comparison',
value: 'top_items',
query: { orderby: 'items_sold', order: 'desc' },
query: { orderby: 'items_sold', order: 'desc', chart: 'item_sold' },
},
{
label: __( 'Top Variations by Net Revenue', 'wc-admin' ),
chartMode: 'item-comparison',
value: 'top_sales',
query: { orderby: 'net_revenue', order: 'desc' },
query: { orderby: 'net_revenue', order: 'desc', chart: 'net_revenue' },
},
],
};

View File

@ -4,12 +4,13 @@
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
import { ReportFilters } from '@woocommerce/components';
import { ReportFilters, SummaryListPlaceholder, ChartPlaceholder } from '@woocommerce/components';
/**
* Internal dependencies
@ -18,37 +19,99 @@ import { charts, filters } from './config';
import getSelectedChart from 'lib/get-selected-chart';
import ProductsReportTable from './table';
import ReportChart from 'analytics/components/report-chart';
import ReportError from 'analytics/components/report-error';
import ReportSummary from 'analytics/components/report-summary';
import VariationsReportTable from './table-variations';
import withSelect from 'wc-api/with-select';
class ProductsReport extends Component {
getChartMeta() {
const { query, isSingleProductView, isSingleProductVariable } = this.props;
const isProductDetailsView =
'top_items' === query.filter ||
'top_sales' === query.filter ||
'single_product' === query.filter;
const mode =
isProductDetailsView || ( isSingleProductView && isSingleProductVariable )
? 'item-comparison'
: 'time-comparison';
const compareObject =
isSingleProductView && isSingleProductVariable ? 'variations' : 'products';
const label =
isSingleProductView && isSingleProductVariable
? __( '%s variations', 'wc-admin' )
: __( '%s products', 'wc-admin' );
return {
compareObject,
itemsLabel: label,
mode,
};
}
export default class ProductsReport extends Component {
render() {
const { path, query } = this.props;
const isProductDetailsView = query.filter === 'single_product';
const { compareObject, itemsLabel, mode } = this.getChartMeta();
const {
path,
query,
isProductsError,
isProductsRequesting,
isSingleProductVariable,
} = this.props;
const itemsLabel = isProductDetailsView
? __( '%s variations', 'wc-admin' )
: __( '%s products', 'wc-admin' );
if ( isProductsError ) {
return <ReportError isError />;
}
if ( isProductsRequesting ) {
return (
<Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } />
<SummaryListPlaceholder numberOfItems={ charts.length } />
<span className="screen-reader-text">
{ __( 'Your requested data is loading', 'wc-admin' ) }
</span>
<div className="woocommerce-chart">
<div className="woocommerce-chart__body">
<ChartPlaceholder height={ 220 } />
</div>
</div>
<ProductsReportTable query={ query } />
</Fragment>
);
}
const chartQuery = {
...query,
};
if ( 'item-comparison' === mode ) {
chartQuery.segmentby = 'products' === compareObject ? 'product' : 'variation';
}
return (
<Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } />
<ReportSummary
mode={ mode }
charts={ charts }
endpoint="products"
query={ query }
query={ chartQuery }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ReportChart
mode={ mode }
filters={ filters }
charts={ charts }
endpoint="products"
itemsLabel={ itemsLabel }
path={ path }
query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) }
query={ chartQuery }
selectedChart={ getSelectedChart( chartQuery.chart, charts ) }
/>
{ isProductDetailsView ? (
{ isSingleProductVariable ? (
<VariationsReportTable query={ query } />
) : (
<ProductsReportTable query={ query } />
@ -62,3 +125,34 @@ ProductsReport.propTypes = {
path: PropTypes.string.isRequired,
query: PropTypes.object.isRequired,
};
export default compose(
withSelect( ( select, props ) => {
const { query } = props;
const { getProducts, isGetProductsRequesting, getProductsError } = select( 'wc-api' );
const isSingleProductView = query.products && 1 === query.products.split( ',' ).length;
if ( isSingleProductView ) {
const includeArgs = { include: query.products };
// TODO Look at similar usage to populate tags in the Search component.
const products = getProducts( includeArgs );
const isVariable = products[ 0 ] && 'variable' === products[ 0 ].type;
const isProductsRequesting = isGetProductsRequesting( includeArgs );
const isProductsError = getProductsError( includeArgs );
return {
query: {
...query,
'is-variable': isVariable,
},
isSingleProductView,
isSingleProductVariable: isVariable,
isProductsRequesting,
isProductsError,
};
}
return {
query,
isSingleProductView,
};
} )
)( ProductsReport );

View File

@ -139,12 +139,11 @@ export default class VariationsReportTable extends Component {
}
getSummary( totals ) {
const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
const { variations_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
return [
{
// @TODO: When primaryData is segmented, fix this to reflect variations, not products.
label: _n( 'variation sold', 'variations sold', products_count, 'wc-admin' ),
value: numberFormat( products_count ),
label: _n( 'variation sold', 'variations sold', variations_count, 'wc-admin' ),
value: numberFormat( variations_count ),
},
{
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
@ -176,7 +175,7 @@ export default class VariationsReportTable extends Component {
endpoint="variations"
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
itemIdField="product_id"
itemIdField="variation_id"
labels={ labels }
query={ query }
getSummary={ this.getSummary }

View File

@ -0,0 +1,11 @@
/** @format */
/**
* Internal dependencies
*/
import operations from './operations';
import selectors from './selectors';
export default {
operations,
selectors,
};

View File

@ -0,0 +1,51 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* WooCommerce dependencies
*/
import { stringifyQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
function read( resourceNames, fetch = apiFetch ) {
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'product-query' ) );
return filteredNames.map( async resourceName => {
const query = getResourceIdentifier( resourceName );
const url = `/wc/v3/products${ stringifyQuery( query ) }`;
try {
const products = await fetch( {
path: url,
} );
const ids = products.map( product => product.id );
const productResources = products.reduce( ( resources, product ) => {
resources[ getResourceName( 'product', product.id ) ] = { data: product };
return resources;
}, {} );
return {
[ resourceName ]: {
data: ids,
totalCount: ids.length,
},
...productResources,
};
} catch ( error ) {
return { [ resourceName ]: { error } };
}
} );
}
export default {
read,
};

View File

@ -0,0 +1,44 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { DEFAULT_REQUIREMENT } from '../constants';
const getProducts = ( getResource, requireResource ) => (
query = {},
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'product-query', query );
const ids = requireResource( requirement, resourceName ).data || [];
const products = ids.map( id => getResource( getResourceName( 'product', id ) ).data || {} );
return products;
};
const getProductsError = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'product-query', query );
return getResource( resourceName ).error;
};
const isGetProductsRequesting = getResource => ( query = {} ) => {
const resourceName = getResourceName( 'product-query', query );
const { lastRequested, lastReceived } = getResource( resourceName );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getProducts,
getProductsError,
isGetProductsRequesting,
};

View File

@ -168,14 +168,13 @@ function getRequestQuery( endpoint, dataType, query ) {
const interval = getIntervalForQuery( query );
const filterQuery = getFilterQuery( endpoint, query );
const end = datesFromQuery[ dataType ].before;
const endingTimeOfDay = end.isSame( moment(), 'day' ) ? 'now' : 'end';
return {
order: 'asc',
interval,
per_page: MAX_PER_PAGE,
after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
before: appendTimestamp( end, endingTimeOfDay ),
before: appendTimestamp( end, 'end' ),
segmentby: query.segmentby,
...filterQuery,
};
}

View File

@ -5,6 +5,7 @@
*/
import items from './items';
import notes from './notes';
import products from './products';
import reportItems from './reports/items';
import reportStats from './reports/stats';
import reviews from './reviews';
@ -20,6 +21,7 @@ function createWcApiSpec() {
selectors: {
...items.selectors,
...notes.selectors,
...products.selectors,
...reportItems.selectors,
...reportStats.selectors,
...reviews.selectors,
@ -31,6 +33,7 @@ function createWcApiSpec() {
return [
...items.operations.read( resourceNames ),
...notes.operations.read( resourceNames ),
...products.operations.read( resourceNames ),
...reportItems.operations.read( resourceNames ),
...reportStats.operations.read( resourceNames ),
...reviews.operations.read( resourceNames ),

View File

@ -60,6 +60,7 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
'net_revenue',
'orders_count',
'products_count',
'variations_count',
),
);
@ -188,6 +189,13 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'segment_label' => array(
'description' => __( 'Human readable segment label, either product or variation name.', 'wc-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
@ -383,6 +391,15 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
'type' => 'integer',
),
);
$params['variations'] = array(
'description' => __( 'Limit result to items with specified variation ids.', 'wc-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ),
'type' => 'string',

View File

@ -290,7 +290,7 @@ class WC_Admin_REST_Reports_Variations_Controller extends WC_REST_Reports_Contro
'default' => 'date',
'enum' => array(
'date',
'gross_revenue',
'net_revenue',
'orders_count',
'items_sold',
),

View File

@ -180,6 +180,7 @@ class WC_Admin_Api_Init {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-orders-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-categories-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-variations-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-variations-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
@ -217,6 +218,7 @@ class WC_Admin_Api_Init {
'WC_Admin_REST_Orders_Controller',
'WC_Admin_REST_Products_Controller',
'WC_Admin_REST_Product_Categories_Controller',
'WC_Admin_REST_Product_Variations_Controller',
'WC_Admin_REST_Product_Reviews_Controller',
'WC_Admin_REST_Product_Variations_Controller',
'WC_Admin_REST_Reports_Controller',

View File

@ -21,10 +21,11 @@ class WC_Admin_Reports_Products_Stats_Segmenting extends WC_Admin_Reports_Segmen
*/
protected function get_segment_selections_product_level( $products_table ) {
$columns_mapping = array(
'items_sold' => "SUM($products_table.product_qty) as items_sold",
'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue",
'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
'products_count' => "COUNT( DISTINCT $products_table.product_id ) AS products_count",
'items_sold' => "SUM($products_table.product_qty) as items_sold",
'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue",
'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
'products_count' => "COUNT( DISTINCT $products_table.product_id ) AS products_count",
'variations_count' => "COUNT( DISTINCT $products_table.variation_id ) AS variations_count",
);
return $this->prepare_selections( $columns_mapping );

View File

@ -19,6 +19,13 @@ class WC_Admin_Reports_Segmenting {
*/
protected $all_segment_ids = false;
/**
* Array of all segment labels.
*
* @var array
*/
protected $segment_labels = array();
/**
* Query arguments supplied by the user for data store.
*
@ -88,10 +95,12 @@ class WC_Admin_Reports_Segmenting {
foreach ( $segments_db_result as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
$segment_labels = $this->get_segment_labels();
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
'segment_id' => $segment_id,
'segment_label' => $segment_labels[ $segment_id ],
'subtotals' => $segment_data,
);
$segment_result[ $segment_id ] = $segment_datum;
}
@ -133,13 +142,15 @@ class WC_Admin_Reports_Segmenting {
*/
protected function merge_segment_totals_results( $segment_dimension, $result1, $result2 ) {
$result_segments = array();
$segment_labels = $this->get_segment_labels();
foreach ( $result1 as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$result_segments[ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => $segment_data,
);
}
@ -148,8 +159,9 @@ class WC_Admin_Reports_Segmenting {
unset( $segment_data[ $segment_dimension ] );
if ( ! isset( $result_segments[ $segment_id ] ) ) {
$result_segments[ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => array(),
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => array(),
);
}
$result_segments[ $segment_id ]['subtotals'] = array_merge( $result_segments[ $segment_id ]['subtotals'], $segment_data );
@ -196,6 +208,7 @@ class WC_Admin_Reports_Segmenting {
*/
protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
$result_segments = array();
$segment_labels = $this->get_segment_labels();
foreach ( $result1 as $segment_data ) {
$time_interval = $segment_data['time_interval'];
@ -208,8 +221,9 @@ class WC_Admin_Reports_Segmenting {
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => $segment_data,
);
$result_segments[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
}
@ -227,8 +241,9 @@ class WC_Admin_Reports_Segmenting {
if ( ! isset( $result_segments[ $time_interval ]['segments'][ $segment_id ] ) ) {
$result_segments[ $time_interval ]['segments'][ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => array(),
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => array(),
);
}
$result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'] = array_merge( $result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'], $segment_data );
@ -251,6 +266,8 @@ class WC_Admin_Reports_Segmenting {
$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
}
$segment_labels = $this->get_segment_labels();
foreach ( $segments_db_result as $segment_data ) {
$time_interval = $segment_data['time_interval'];
if ( ! isset( $aggregated_segment_result[ $time_interval ] ) ) {
@ -262,8 +279,9 @@ class WC_Admin_Reports_Segmenting {
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => $segment_data,
);
$aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
}
@ -284,13 +302,25 @@ class WC_Admin_Reports_Segmenting {
return;
}
$segments = array();
$segment_labels = array();
if ( 'product' === $this->query_args['segmentby'] ) {
$segments = wc_get_products(
array(
'return' => 'ids',
'limit' => -1,
)
$args = array(
'return' => 'objects',
'limit' => -1,
);
if ( isset( $this->query_args['product_includes'] ) ) {
$args['include'] = $this->query_args['product_includes'];
}
$segment_objects = wc_get_products( $args );
foreach ( $segment_objects as $segment ) {
$id = $segment->get_id();
$segments[] = $id;
$segment_labels[ $id ] = $segment->get_name();
}
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
// @todo: assuming that this will only be used for one product, check assumption.
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
@ -298,14 +328,24 @@ class WC_Admin_Reports_Segmenting {
return;
}
$segments = wc_get_products(
array(
'return' => 'ids',
'limit' => - 1,
'type' => 'variation',
'parent' => $this->query_args['product_includes'][0],
)
$args = array(
'return' => 'objects',
'limit' => -1,
'type' => 'variation',
'parent' => $this->query_args['product_includes'][0],
);
if ( isset( $this->query_args['variations'] ) ) {
$args['include'] = $this->query_args['variations'];
}
$segment_objects = wc_get_products( $args );
foreach ( $segment_objects as $segment ) {
$id = $segment->get_id();
$segments[] = $id;
$segment_labels[ $id ] = $segment->get_name();
}
} elseif ( 'category' === $this->query_args['segmentby'] ) {
$categories = get_categories(
array(
@ -338,6 +378,7 @@ class WC_Admin_Reports_Segmenting {
}
$this->all_segment_ids = $segments;
$this->segment_labels = $segment_labels;
}
/**
@ -353,6 +394,19 @@ class WC_Admin_Reports_Segmenting {
return $this->all_segment_ids;
}
/**
* Return all segment labels for given segmentby query parameter.
*
* @return array
*/
protected function get_segment_labels() {
if ( ! is_array( $this->all_segment_ids ) ) {
$this->set_all_segments();
}
return $this->segment_labels;
}
/**
* Compares two report data objects by pre-defined object property and ASC/DESC ordering.
*
@ -395,11 +449,13 @@ class WC_Admin_Reports_Segmenting {
$segments = array();
}
$all_segment_ids = $this->get_all_segments();
$segment_labels = $this->get_segment_labels();
foreach ( $all_segment_ids as $segment_id ) {
if ( ! isset( $segments[ $segment_id ] ) ) {
$segments[ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => $segment_subtotals,
'segment_id' => $segment_id,
'segment_label' => $segment_labels[ $segment_id ],
'subtotals' => $segment_subtotals,
);
}
}

View File

@ -211,7 +211,7 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons
if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}

View File

@ -76,7 +76,15 @@ class WC_Admin_Reports_Data_Store {
// @todo: should return WP_Error here perhaps?
}
if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
return 0;
// As relative order is undefined in case of equality in usort, second-level sorting by date needs to be enforced
// so that paging is stable.
if ( $a['time_interval'] === $b['time_interval'] ) {
return 0; // This should never happen.
} elseif ( $a['time_interval'] > $b['time_interval'] ) {
return 1;
} elseif ( $a['time_interval'] < $b['time_interval'] ) {
return -1;
}
} elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) {
return strtolower( $this->order ) === 'desc' ? -1 : 1;
} elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) {
@ -174,12 +182,17 @@ class WC_Admin_Reports_Data_Store {
* @param int $db_interval_count Database interval count.
* @param int $expected_interval_count Expected interval count on the output.
* @param string $order_by Order by field.
* @param string $order ASC or DESC.
*/
protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by ) {
protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by, $order ) {
if ( 'date' === strtolower( $order_by ) ) {
$offset = 0;
} else {
$offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count;
if ( 'asc' === strtolower( $order ) ) {
$offset = ( $page_no - 1 ) * $items_per_page;
} else {
$offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count;
}
$offset = $offset < 0 ? 0 : $offset;
}
$count = $expected_interval_count - ( $page_no - 1 ) * $items_per_page;
@ -672,6 +685,24 @@ class WC_Admin_Reports_Data_Store {
return $included_products_str;
}
/**
* Returns comma separated ids of allowed variations, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_variations( $query_args ) {
$included_variations = array();
$operator = $this->get_match_operator( $query_args );
if ( isset( $query_args['variations'] ) && is_array( $query_args['variations'] ) && count( $query_args['variations'] ) > 0 ) {
$included_variations = array_filter( array_map( 'intval', $query_args['variations'] ) );
}
$included_variations_str = implode( ',', $included_variations );
return $included_variations_str;
}
/**
* Returns comma separated ids of excluded products, based on query arguments from the user.
*

View File

@ -167,7 +167,7 @@ class WC_Admin_Reports_Downloads_Stats_Data_Store extends WC_Admin_Reports_Downl
if ( $this->intervals_missing( $expected_interval_count, $db_records_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}

View File

@ -311,7 +311,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}

View File

@ -140,6 +140,11 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
}
$included_variations = $this->get_included_variations( $query_args );
if ( $included_variations ) {
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";

View File

@ -19,13 +19,14 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
* @var array
*/
protected $column_types = array(
'date_start' => 'strval',
'date_end' => 'strval',
'product_id' => 'intval',
'items_sold' => 'intval',
'net_revenue' => 'floatval',
'orders_count' => 'intval',
'products_count' => 'intval',
'date_start' => 'strval',
'date_end' => 'strval',
'product_id' => 'intval',
'items_sold' => 'intval',
'net_revenue' => 'floatval',
'orders_count' => 'intval',
'products_count' => 'intval',
'variations_count' => 'intval',
);
/**
@ -34,10 +35,11 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
* @var array
*/
protected $report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
);
/**
@ -70,6 +72,11 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
$products_where_clause .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
}
$included_variations = $this->get_included_variations( $query_args );
if ( $included_variations ) {
$products_where_clause .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$products_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
@ -207,7 +214,7 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}

View File

@ -229,6 +229,7 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
variation_id
@ -248,6 +249,7 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_time_clause']}
{$sql_query_params['where_clause']}
GROUP BY
variation_id

View File

@ -2,6 +2,7 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { Component, createRef } from '@wordpress/element';
import PropTypes from 'prop-types';
@ -36,7 +37,7 @@ class D3Legend extends Component {
}
updateListScroll() {
if ( ! this.listRef ) {
if ( ! this || ! this.listRef ) {
return;
}
const list = this.listRef.current;
@ -93,7 +94,12 @@ class D3Legend extends Component {
<button
onClick={ handleLegendToggle }
id={ row.key }
disabled={ ( row.visible && numberOfRowsVisible <= 1 ) || ! interactive }
disabled={
( row.visible && numberOfRowsVisible <= 1 ) ||
( ! row.visible && numberOfRowsVisible >= 5 ) ||
! interactive
}
title={ numberOfRowsVisible >= 5 ? __( 'You may select up to 5 items.', 'wc-admin' ) : '' }
>
<div className="woocommerce-legend__item-container" id={ row.key }>
<span

View File

@ -4,9 +4,12 @@
*/
@import './legend.scss';
.woocommerce-chart__body-row .d3-chart__container {
width: calc( 100% - 320px );
}
.d3-chart__container {
position: relative;
width: 100%;
svg {
overflow: visible;

View File

@ -176,6 +176,11 @@ export const compareStrings = ( s1, s2, splitChar = new RegExp( [ ' |,' ], 'g' )
export const getYGrids = ( yMax ) => {
const yGrids = [];
// If all values are 0, yMax can become NaN.
if ( isNaN( yMax ) ) {
return null;
}
for ( let i = 0; i < 4; i++ ) {
const value = yMax > 1 ? Math.round( i / 3 * yMax ) : i / 3 * yMax;
if ( yGrids[ yGrids.length - 1 ] !== value ) {
@ -237,29 +242,31 @@ export const drawAxis = ( node, params, xOffset ) => {
.tickFormat( '' )
);
node
.append( 'g' )
.attr( 'class', 'grid' )
.attr( 'transform', `translate(-${ params.margin.left }, 0)` )
.call(
d3AxisLeft( params.yScale )
.tickValues( yGrids )
.tickSize( -params.width - params.margin.left - params.margin.right )
.tickFormat( '' )
)
.call( g => g.select( '.domain' ).remove() );
if ( yGrids ) {
node
.append( 'g' )
.attr( 'class', 'grid' )
.attr( 'transform', `translate(-${ params.margin.left }, 0)` )
.call(
d3AxisLeft( params.yScale )
.tickValues( yGrids )
.tickSize( -params.width - params.margin.left - params.margin.right )
.tickFormat( '' )
)
.call( g => g.select( '.domain' ).remove() );
node
.append( 'g' )
.attr( 'class', 'axis y-axis' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', 'translate(-50, 0)' )
.attr( 'text-anchor', 'start' )
.call(
d3AxisLeft( params.yTickOffset )
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
);
node
.append( 'g' )
.attr( 'class', 'axis y-axis' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', 'translate(-50, 0)' )
.attr( 'text-anchor', 'start' )
.call(
d3AxisLeft( params.yTickOffset )
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
);
}
node.selectAll( '.domain' ).remove();
node

View File

@ -6,7 +6,7 @@ import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import { Component, createRef, Fragment } from '@wordpress/element';
import { formatDefaultLocale as d3FormatDefaultLocale } from 'd3-format';
import { get, isEqual, partial } from 'lodash';
import { get, isEqual, partial, isEmpty } from 'lodash';
import Gridicon from 'gridicons';
import { IconButton, NavigableMenu, SelectControl } from '@wordpress/components';
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
@ -57,18 +57,29 @@ function getOrderedKeys( props, previousOrderedKeys = [] ) {
return accum;
}, [] )
),
].map( key => {
].map( ( key ) => {
const previousKey = previousOrderedKeys.find( item => key === item.key );
const defaultVisibleStatus = 'item-comparison' === props.mode ? false : true;
return {
key,
total: props.data.reduce( ( a, c ) => a + c[ key ].value, 0 ),
visible: previousKey ? previousKey.visible : true,
visible: previousKey ? previousKey.visible : defaultVisibleStatus,
focus: true,
};
} );
if ( props.mode === 'item-comparison' ) {
if ( 'item-comparison' === props.mode ) {
updatedKeys.sort( ( a, b ) => b.total - a.total );
if ( isEmpty( previousOrderedKeys ) ) {
return updatedKeys.filter( key => key.total > 0 ).map( ( key, index ) => {
return {
...key,
visible: index < 5 || key.visible,
};
} );
}
}
return updatedKeys;
}
@ -94,7 +105,7 @@ class Chart extends Component {
}
componentDidUpdate( prevProps ) {
const { data } = this.props;
const { data, query, mode } = this.props;
if ( ! isEqual( [ ...data ].sort(), [ ...prevProps.data ].sort() ) ) {
/**
* Only update the orderedKeys when data is present so that
@ -110,6 +121,16 @@ class Chart extends Component {
} );
/* eslint-enable react/no-did-update-set-state */
}
if ( 'item-comparison' === mode && ! isEqual( query, prevProps.query ) ) {
const orderedKeys = getOrderedKeys( this.props );
/* eslint-disable react/no-did-update-set-state */
this.setState( {
orderedKeys,
visibleData: this.getVisibleData( data, orderedKeys ),
} );
/* eslint-enable react/no-did-update-set-state */
}
}
componentDidMount() {
@ -267,7 +288,7 @@ class Chart extends Component {
const chartDirection = legendPosition === 'side' ? 'row' : 'column';
const chartHeight = this.getChartHeight();
const legend = (
const legend = isRequesting ? null : (
<D3Legend
colorScheme={ d3InterpolateViridis }
data={ orderedKeys }

View File

@ -45,8 +45,7 @@ import TableSummary from './summary';
class TableCard extends Component {
constructor( props ) {
super( props );
const { compareBy, query } = props;
const { query, compareBy } = this.props;
const showCols = props.headers.map( ( { key, hiddenByDefault } ) => ! hiddenByDefault && key ).filter( Boolean );
const selectedRows = query.filter ? getIdsFromQuery( query[ compareBy ] ) : [];
@ -143,11 +142,10 @@ class TableCard extends Component {
}
onCompare() {
const { compareBy, compareParam, onQueryChange } = this.props;
const { selectedRows } = this.state;
if ( compareBy ) {
onQueryChange( 'compare' )( compareBy, compareParam, selectedRows.join( ',' ) );
}
// Reset selected rows so the user can start a comparison again.
this.setState( {
selectedRows: [],
} );
}
onSearch( values ) {

View File

@ -87,11 +87,12 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
$expected_reports = array(
'totals' => array(
'items_sold' => 4,
'net_revenue' => 100.0,
'orders_count' => 1,
'products_count' => 1,
'segments' => array(),
'items_sold' => 4,
'net_revenue' => 100.0,
'orders_count' => 1,
'products_count' => 1,
'variations_count' => 1,
'segments' => array(),
),
'intervals' => array(
array(
@ -101,11 +102,12 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
'date_end' => date( 'Y-m-d 23:59:59', $time ),
'date_end_gmt' => date( 'Y-m-d 23:59:59', $time ),
'subtotals' => (object) array(
'items_sold' => 4,
'net_revenue' => 100.0,
'orders_count' => 1,
'products_count' => 1,
'segments' => array(),
'items_sold' => 4,
'net_revenue' => 100.0,
'orders_count' => 1,
'products_count' => 1,
'variations_count' => 1,
'segments' => array(),
),
),
),