Merge branch 'master' into fix/1035
# Conflicts: # includes/wc-admin-order-functions.php
This commit is contained in:
commit
1e39bc0756
|
@ -12,45 +12,82 @@ const { parse, resolver } = require( 'react-docgen' );
|
||||||
*/
|
*/
|
||||||
const { getDescription, getProps, getTitle } = require( './lib/formatting' );
|
const { getDescription, getProps, getTitle } = require( './lib/formatting' );
|
||||||
const {
|
const {
|
||||||
|
ANALYTICS_FOLDER,
|
||||||
PACKAGES_FOLDER,
|
PACKAGES_FOLDER,
|
||||||
|
DOCS_FOLDER,
|
||||||
deleteExistingDocs,
|
deleteExistingDocs,
|
||||||
getExportedFileList,
|
getExportedFileList,
|
||||||
getMdFileName,
|
getMdFileName,
|
||||||
getRealFilePaths,
|
getRealFilePaths,
|
||||||
writeTableOfContents,
|
getTocSection,
|
||||||
} = require( './lib/file-system' );
|
} = require( './lib/file-system' );
|
||||||
|
|
||||||
// Start by wiping the existing docs. **Change this if we end up manually editing docs**
|
const fileCollections = [
|
||||||
deleteExistingDocs();
|
{
|
||||||
|
folder: ANALYTICS_FOLDER,
|
||||||
// Read components file to get a list of exported files, convert that to a list of absolute paths to public components.
|
route: 'analytics',
|
||||||
const files = [
|
title: 'Analytics components',
|
||||||
...getRealFilePaths( getExportedFileList( path.resolve( PACKAGES_FOLDER, 'index.js' ) ), PACKAGES_FOLDER ),
|
},
|
||||||
|
{
|
||||||
|
folder: PACKAGES_FOLDER,
|
||||||
|
route: 'packages',
|
||||||
|
title: 'Package components',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const tocSections = [];
|
||||||
|
|
||||||
|
fileCollections.forEach( fileCollection => {
|
||||||
|
// Start by wiping the existing docs. **Change this if we end up manually editing docs**
|
||||||
|
deleteExistingDocs( fileCollection.route );
|
||||||
|
|
||||||
|
// Read components file to get a list of exported files, convert that to a list of absolute paths to public components.
|
||||||
|
const files = getRealFilePaths( getExportedFileList( path.resolve( fileCollection.folder, 'index.js' ) ), fileCollection.folder );
|
||||||
|
|
||||||
|
// Build documentation
|
||||||
|
buildComponentDocs( files, fileCollection.route );
|
||||||
|
|
||||||
|
// Concatenate TOC contents
|
||||||
|
tocSections.push( ...getTocSection( files, fileCollection.route, fileCollection.title ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Write TOC file
|
||||||
|
const tocFile = path.resolve( DOCS_FOLDER, '_sidebar.md' );
|
||||||
|
const tocHeader = '* [Home](/)\n\n* [Components](components/)\n\n';
|
||||||
|
fs.writeFileSync( tocFile, tocHeader + tocSections.join( '\n' ) );
|
||||||
|
|
||||||
|
// Sum the number of TOC lines and substract the titles
|
||||||
|
const numberOfFiles = tocSections.length - fileCollections.length;
|
||||||
|
console.log( `Wrote docs for ${ numberOfFiles } files.` );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse each file's content & build up a markdown file.
|
||||||
|
*
|
||||||
|
* @param { Array } files The absolute path of this file.
|
||||||
|
* @param { string } route Folder where the docs must be stored.
|
||||||
|
*/
|
||||||
|
function buildComponentDocs( files, route ) {
|
||||||
// Build the documentation by reading each file.
|
// Build the documentation by reading each file.
|
||||||
files.forEach( file => {
|
files.forEach( file => {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync( file );
|
const content = fs.readFileSync( file );
|
||||||
buildDocs( file, content );
|
buildDocs( file, route, content );
|
||||||
} catch ( readErr ) {
|
} catch ( readErr ) {
|
||||||
console.warn( file, readErr );
|
console.warn( file, readErr );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
}
|
||||||
writeTableOfContents( files );
|
|
||||||
|
|
||||||
console.log( `Wrote docs for ${ files.length } files.` );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse each file's content & build up a markdown file.
|
* Parse each file's content & build up a markdown file.
|
||||||
*
|
*
|
||||||
* @param { string } fileName The absolute path of this file.
|
* @param { string } fileName The absolute path of this file.
|
||||||
|
* @param { string } route Folder where the docs must be stored.
|
||||||
* @param { string } content Content of this file.
|
* @param { string } content Content of this file.
|
||||||
* @param { boolean } multiple If there are multiple exports in this file, we need to use a different resolver.
|
* @param { boolean } multiple If there are multiple exports in this file, we need to use a different resolver.
|
||||||
*/
|
*/
|
||||||
function buildDocs( fileName, content, multiple = false ) {
|
function buildDocs( fileName, route, content, multiple = false ) {
|
||||||
const mdFileName = getMdFileName( fileName );
|
const mdFileName = getMdFileName( fileName, route );
|
||||||
let markdown;
|
let markdown;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -65,7 +102,7 @@ function buildDocs( fileName, content, multiple = false ) {
|
||||||
} catch ( parseErr ) {
|
} catch ( parseErr ) {
|
||||||
if ( ! multiple ) {
|
if ( ! multiple ) {
|
||||||
// The most likely error is that there are multiple exported components
|
// The most likely error is that there are multiple exported components
|
||||||
buildDocs( fileName, content, true );
|
buildDocs( fileName, route, content, true );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.warn( fileName, parseErr );
|
console.warn( fileName, parseErr );
|
||||||
|
|
|
@ -14,24 +14,28 @@ const { namedTypes } = types;
|
||||||
*/
|
*/
|
||||||
const { camelCaseDash } = require( './formatting' );
|
const { camelCaseDash } = require( './formatting' );
|
||||||
|
|
||||||
|
const ANALYTICS_FOLDER = path.resolve( __dirname, '../../../client/analytics/components/' );
|
||||||
const PACKAGES_FOLDER = path.resolve( __dirname, '../../../packages/components/src/' );
|
const PACKAGES_FOLDER = path.resolve( __dirname, '../../../packages/components/src/' );
|
||||||
const DOCS_FOLDER = path.resolve( __dirname, '../../../docs/components/' );
|
const DOCS_FOLDER = path.resolve( __dirname, '../../../docs/components/' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all files in existing docs folder.
|
* Remove all files in existing docs folder.
|
||||||
|
* @param { String } route Route of the folder to clean.
|
||||||
*/
|
*/
|
||||||
function deleteExistingDocs() {
|
function deleteExistingDocs( route ) {
|
||||||
if ( ! isDirectory( DOCS_FOLDER ) ) {
|
const folderRoute = path.resolve( DOCS_FOLDER, route );
|
||||||
fs.mkdirSync( DOCS_FOLDER );
|
|
||||||
|
if ( ! isDirectory( folderRoute ) ) {
|
||||||
|
fs.mkdirSync( folderRoute );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync( DOCS_FOLDER );
|
const files = fs.readdirSync( folderRoute );
|
||||||
files.map( file => {
|
files.map( file => {
|
||||||
if ( 'README.md' === file ) {
|
if ( 'README.md' === file ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fs.unlinkSync( path.resolve( DOCS_FOLDER, file ) );
|
fs.unlinkSync( path.resolve( folderRoute, file ) );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,10 +74,11 @@ function getExportedFileList( filePath ) {
|
||||||
* Return the markdown file name for a given component file.
|
* Return the markdown file name for a given component file.
|
||||||
*
|
*
|
||||||
* @param { string } filepath File path for this component.
|
* @param { string } filepath File path for this component.
|
||||||
|
* @param { string } route Folder where the docs must be stored.
|
||||||
* @param { boolean } absolute Whether to return full path (true) or just filename (false).
|
* @param { boolean } absolute Whether to return full path (true) or just filename (false).
|
||||||
* @return { string } Markdown file name.
|
* @return { string } Markdown file name.
|
||||||
*/
|
*/
|
||||||
function getMdFileName( filepath, absolute = true ) {
|
function getMdFileName( filepath, route, absolute = true ) {
|
||||||
const fileParts = filepath.split( '/components/' );
|
const fileParts = filepath.split( '/components/' );
|
||||||
if ( ! fileParts || ! fileParts[ 1 ] ) {
|
if ( ! fileParts || ! fileParts[ 1 ] ) {
|
||||||
return;
|
return;
|
||||||
|
@ -82,7 +87,7 @@ function getMdFileName( filepath, absolute = true ) {
|
||||||
if ( ! absolute ) {
|
if ( ! absolute ) {
|
||||||
return name + '.md';
|
return name + '.md';
|
||||||
}
|
}
|
||||||
return path.resolve( DOCS_FOLDER, name + '.md' );
|
return path.resolve( DOCS_FOLDER + '/' + route + '/', name + '.md' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,25 +150,32 @@ function isFile( file ) {
|
||||||
/**
|
/**
|
||||||
* Create a table of contents given a list of markdown files.
|
* Create a table of contents given a list of markdown files.
|
||||||
*
|
*
|
||||||
* @param { array } files A list of readme files.
|
* @param { array } files A list of files, presumably in the components directory.
|
||||||
|
* @param { string } route Folder where the docs are stored.
|
||||||
|
* @param { string } title Title of the TOC section
|
||||||
|
* @return { string } TOC contents.
|
||||||
*/
|
*/
|
||||||
function writeTableOfContents( files ) {
|
function getTocSection( files, route, title ) {
|
||||||
const mdFiles = files.map( f => getMdFileName( f, false ) ).sort();
|
const mdFiles = files.map( f => getMdFileName( f, route, false ) ).sort();
|
||||||
const TOC = uniq( mdFiles ).map( doc => {
|
|
||||||
const name = camelCaseDash( doc.replace( '.md', '' ) );
|
|
||||||
return ` * [${ name }](components/${ doc })`;
|
|
||||||
} ).join( '\n' );
|
|
||||||
|
|
||||||
const TocFile = path.resolve( DOCS_FOLDER, '_sidebar.md' );
|
const toc = uniq( mdFiles ).map( doc => {
|
||||||
fs.writeFileSync( TocFile, '* [Home](/)\n\n* [Components](components/)\n\n' + TOC + '\n' );
|
const name = camelCaseDash( doc.replace( '.md', '' ) );
|
||||||
|
return ` * [${ name }](components/${ route }/${ doc })`;
|
||||||
|
} );
|
||||||
|
|
||||||
|
return [
|
||||||
|
' * [' + title + '](components/' + route + '/)',
|
||||||
|
...toc,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DOCS_FOLDER,
|
DOCS_FOLDER,
|
||||||
|
ANALYTICS_FOLDER,
|
||||||
PACKAGES_FOLDER,
|
PACKAGES_FOLDER,
|
||||||
deleteExistingDocs,
|
deleteExistingDocs,
|
||||||
getExportedFileList,
|
getExportedFileList,
|
||||||
getMdFileName,
|
getMdFileName,
|
||||||
getRealFilePaths,
|
getRealFilePaths,
|
||||||
writeTableOfContents,
|
getTocSection,
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,6 @@ module.exports = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} ),
|
} ),
|
||||||
require( 'autoprefixer' ),
|
require( 'autoprefixer' )( { grid: true } ),
|
||||||
require( 'postcss-color-function' ),
|
require( 'postcss-color-function' ),
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
export { default as ReportChart } from './report-chart';
|
||||||
|
export { default as ReportError } from './report-error';
|
||||||
|
export { default as ReportSummary } from './report-summary';
|
||||||
|
export { default as ReportTable } from './report-table';
|
|
@ -31,6 +31,9 @@ import withSelect from 'wc-api/with-select';
|
||||||
|
|
||||||
export const DEFAULT_FILTER = 'all';
|
export const DEFAULT_FILTER = 'all';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders the chart in reports.
|
||||||
|
*/
|
||||||
export class ReportChart extends Component {
|
export class ReportChart extends Component {
|
||||||
getSelectedFilter( filters, query ) {
|
getSelectedFilter( filters, query ) {
|
||||||
if ( filters.length === 0 ) {
|
if ( filters.length === 0 ) {
|
||||||
|
@ -65,7 +68,7 @@ export class ReportChart extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { query, itemsLabel, path, primaryData, secondaryData, selectedChart } = this.props;
|
const { query, itemsLabel, mode, path, primaryData, secondaryData, selectedChart } = this.props;
|
||||||
|
|
||||||
if ( primaryData.isError || secondaryData.isError ) {
|
if ( primaryData.isError || secondaryData.isError ) {
|
||||||
return <ReportError isError />;
|
return <ReportError isError />;
|
||||||
|
@ -80,7 +83,7 @@ export class ReportChart extends Component {
|
||||||
|
|
||||||
const chartData = primaryData.data.intervals.map( function( interval, index ) {
|
const chartData = primaryData.data.intervals.map( function( interval, index ) {
|
||||||
const secondaryDate = getPreviousDate(
|
const secondaryDate = getPreviousDate(
|
||||||
formatDate( 'Y-m-d', interval.date_start ),
|
interval.date_start,
|
||||||
primary.after,
|
primary.after,
|
||||||
secondary.after,
|
secondary.after,
|
||||||
query.compare,
|
query.compare,
|
||||||
|
@ -111,7 +114,7 @@ export class ReportChart extends Component {
|
||||||
type={ getChartTypeForQuery( query ) }
|
type={ getChartTypeForQuery( query ) }
|
||||||
allowedIntervals={ allowedIntervals }
|
allowedIntervals={ allowedIntervals }
|
||||||
itemsLabel={ itemsLabel }
|
itemsLabel={ itemsLabel }
|
||||||
mode={ this.getChartMode() }
|
mode={ mode || this.getChartMode() }
|
||||||
tooltipLabelFormat={ formats.tooltipLabelFormat }
|
tooltipLabelFormat={ formats.tooltipLabelFormat }
|
||||||
tooltipValueFormat={ getTooltipValueFormat( selectedChart.type ) }
|
tooltipValueFormat={ getTooltipValueFormat( selectedChart.type ) }
|
||||||
tooltipTitle={ selectedChart.label }
|
tooltipTitle={ selectedChart.label }
|
||||||
|
@ -126,12 +129,38 @@ export class ReportChart extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportChart.propTypes = {
|
ReportChart.propTypes = {
|
||||||
|
/**
|
||||||
|
* Filters available for that report.
|
||||||
|
*/
|
||||||
filters: PropTypes.array,
|
filters: PropTypes.array,
|
||||||
|
/**
|
||||||
|
* Label describing the legend items.
|
||||||
|
*/
|
||||||
itemsLabel: PropTypes.string,
|
itemsLabel: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* `items-comparison` (default) or `time-comparison`, this is used to generate correct
|
||||||
|
* ARIA properties.
|
||||||
|
*/
|
||||||
|
mode: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Current path
|
||||||
|
*/
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Primary data to display in the chart.
|
||||||
|
*/
|
||||||
primaryData: PropTypes.object.isRequired,
|
primaryData: PropTypes.object.isRequired,
|
||||||
|
/**
|
||||||
|
* The query string represented in object form.
|
||||||
|
*/
|
||||||
query: PropTypes.object.isRequired,
|
query: PropTypes.object.isRequired,
|
||||||
|
/**
|
||||||
|
* Secondary data to display in the chart.
|
||||||
|
*/
|
||||||
secondaryData: PropTypes.object.isRequired,
|
secondaryData: PropTypes.object.isRequired,
|
||||||
|
/**
|
||||||
|
* Properties of the selected chart.
|
||||||
|
*/
|
||||||
selectedChart: PropTypes.object.isRequired,
|
selectedChart: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,10 @@ import PropTypes from 'prop-types';
|
||||||
import { EmptyContent } from '@woocommerce/components';
|
import { EmptyContent } from '@woocommerce/components';
|
||||||
import { getAdminLink } from '@woocommerce/navigation';
|
import { getAdminLink } from '@woocommerce/navigation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render when there is an error in a report component due to data
|
||||||
|
* not being loaded or being invalid.
|
||||||
|
*/
|
||||||
class ReportError extends Component {
|
class ReportError extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { className, isError, isEmpty } = this.props;
|
const { className, isError, isEmpty } = this.props;
|
||||||
|
@ -42,8 +46,17 @@ class ReportError extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportError.propTypes = {
|
ReportError.propTypes = {
|
||||||
|
/**
|
||||||
|
* Additional class name to style the component.
|
||||||
|
*/
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Boolean representing whether there was an error.
|
||||||
|
*/
|
||||||
isError: PropTypes.bool,
|
isError: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Boolean representing whether the issue is that there is no data.
|
||||||
|
*/
|
||||||
isEmpty: PropTypes.bool,
|
isEmpty: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,9 @@ import ReportError from 'analytics/components/report-error';
|
||||||
import { calculateDelta, formatValue } from './utils';
|
import { calculateDelta, formatValue } from './utils';
|
||||||
import withSelect from 'wc-api/with-select';
|
import withSelect from 'wc-api/with-select';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render summary numbers in reports.
|
||||||
|
*/
|
||||||
export class ReportSummary extends Component {
|
export class ReportSummary extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { charts, query, selectedChart, summaryData } = this.props;
|
const { charts, query, selectedChart, summaryData } = this.props;
|
||||||
|
@ -70,10 +73,30 @@ export class ReportSummary extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportSummary.propTypes = {
|
ReportSummary.propTypes = {
|
||||||
|
/**
|
||||||
|
* Properties of all the charts available for that report.
|
||||||
|
*/
|
||||||
charts: PropTypes.array.isRequired,
|
charts: PropTypes.array.isRequired,
|
||||||
|
/**
|
||||||
|
* The endpoint to use in API calls to populate the Summary Numbers.
|
||||||
|
* For example, if `taxes` is provided, data will be fetched from the report
|
||||||
|
* `taxes` endpoint (ie: `/wc/v3/reports/taxes/stats`). If the provided endpoint
|
||||||
|
* doesn't exist, an error will be shown to the user with `ReportError`.
|
||||||
|
*/
|
||||||
endpoint: PropTypes.string.isRequired,
|
endpoint: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* The query string represented in object form.
|
||||||
|
*/
|
||||||
query: PropTypes.object.isRequired,
|
query: PropTypes.object.isRequired,
|
||||||
selectedChart: PropTypes.object.isRequired,
|
/**
|
||||||
|
* Properties of the selected chart.
|
||||||
|
*/
|
||||||
|
selectedChart: PropTypes.shape( {
|
||||||
|
/**
|
||||||
|
* Key of the selected chart.
|
||||||
|
*/
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
} ).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
|
|
|
@ -25,28 +25,39 @@ import { extendTableData } from './utils';
|
||||||
|
|
||||||
const TABLE_FILTER = 'woocommerce_admin_report_table';
|
const TABLE_FILTER = 'woocommerce_admin_report_table';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that extends `TableCard` to facilitate its usage in reports.
|
||||||
|
*/
|
||||||
class ReportTable extends Component {
|
class ReportTable extends Component {
|
||||||
onColumnsChange = columns => {
|
constructor( props ) {
|
||||||
const { columnPrefsKey } = this.props;
|
super( props );
|
||||||
|
|
||||||
|
this.onColumnsChange = this.onColumnsChange.bind( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
onColumnsChange( shownColumns ) {
|
||||||
|
const { columnPrefsKey, getHeadersContent } = this.props;
|
||||||
|
const columns = getHeadersContent().map( header => header.key );
|
||||||
|
const hiddenColumns = columns.filter( column => ! shownColumns.includes( column ) );
|
||||||
|
|
||||||
if ( columnPrefsKey ) {
|
if ( columnPrefsKey ) {
|
||||||
const userDataFields = {
|
const userDataFields = {
|
||||||
[ columnPrefsKey ]: columns,
|
[ columnPrefsKey ]: hiddenColumns,
|
||||||
};
|
};
|
||||||
this.props.updateCurrentUserData( userDataFields );
|
this.props.updateCurrentUserData( userDataFields );
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
filterShownHeaders = ( headers, shownKeys ) => {
|
filterShownHeaders( headers, hiddenKeys ) {
|
||||||
if ( ! shownKeys ) {
|
if ( ! hiddenKeys ) {
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers.map( header => {
|
return headers.map( header => {
|
||||||
const hidden = ! shownKeys.includes( header.key );
|
const hidden = hiddenKeys.includes( header.key ) && ! header.required;
|
||||||
return { ...header, hiddenByDefault: hidden };
|
return { ...header, hiddenByDefault: hidden };
|
||||||
} );
|
} );
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
@ -56,7 +67,7 @@ class ReportTable extends Component {
|
||||||
itemIdField,
|
itemIdField,
|
||||||
primaryData,
|
primaryData,
|
||||||
tableData,
|
tableData,
|
||||||
// These two props are not used in the render function, but are destructured
|
// These props are not used in the render function, but are destructured
|
||||||
// so they are not included in the `tableProps` variable.
|
// so they are not included in the `tableProps` variable.
|
||||||
endpoint,
|
endpoint,
|
||||||
tableQuery,
|
tableQuery,
|
||||||
|
@ -113,7 +124,11 @@ ReportTable.propTypes = {
|
||||||
*/
|
*/
|
||||||
columnPrefsKey: PropTypes.string,
|
columnPrefsKey: PropTypes.string,
|
||||||
/**
|
/**
|
||||||
* The endpoint to use in API calls.
|
* The endpoint to use in API calls to populate the table rows and summary.
|
||||||
|
* For example, if `taxes` is provided, data will be fetched from the report
|
||||||
|
* `taxes` endpoint (ie: `/wc/v3/reports/taxes` and `/wc/v3/reports/taxes/stats`).
|
||||||
|
* If the provided endpoint doesn't exist, an error will be shown to the user
|
||||||
|
* with `ReportError`.
|
||||||
*/
|
*/
|
||||||
endpoint: PropTypes.string,
|
endpoint: PropTypes.string,
|
||||||
/**
|
/**
|
||||||
|
@ -143,11 +158,13 @@ ReportTable.propTypes = {
|
||||||
*/
|
*/
|
||||||
itemIdField: PropTypes.string,
|
itemIdField: PropTypes.string,
|
||||||
/**
|
/**
|
||||||
* Primary data of that report.
|
* Primary data of that report. If it's not provided, it will be automatically
|
||||||
|
* loaded via the provided `endpoint`.
|
||||||
*/
|
*/
|
||||||
primaryData: PropTypes.object.isRequired,
|
primaryData: PropTypes.object.isRequired,
|
||||||
/**
|
/**
|
||||||
* Table data of that report.
|
* Table data of that report. If it's not provided, it will be automatically
|
||||||
|
* loaded via the provided `endpoint`.
|
||||||
*/
|
*/
|
||||||
tableData: PropTypes.object.isRequired,
|
tableData: PropTypes.object.isRequired,
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import { first, last } from 'lodash';
|
||||||
|
import { Spinner } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce dependencies
|
||||||
|
*/
|
||||||
|
import { Link } from '@woocommerce/components';
|
||||||
|
|
||||||
|
export default class CategoryBreadcrumbs extends Component {
|
||||||
|
getCategoryAncestorIds( category, categories ) {
|
||||||
|
const ancestors = [];
|
||||||
|
let parent = category.parent;
|
||||||
|
while ( parent ) {
|
||||||
|
ancestors.unshift( parent );
|
||||||
|
parent = categories[ parent ].parent;
|
||||||
|
}
|
||||||
|
return ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategoryAncestors( category, categories ) {
|
||||||
|
const ancestorIds = this.getCategoryAncestorIds( category, categories );
|
||||||
|
|
||||||
|
if ( ! ancestorIds.length ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( ancestorIds.length === 1 ) {
|
||||||
|
return categories[ first( ancestorIds ) ].name + ' › ';
|
||||||
|
}
|
||||||
|
if ( ancestorIds.length === 2 ) {
|
||||||
|
return (
|
||||||
|
categories[ first( ancestorIds ) ].name +
|
||||||
|
' › ' +
|
||||||
|
categories[ last( ancestorIds ) ].name +
|
||||||
|
' › '
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
categories[ first( ancestorIds ) ].name +
|
||||||
|
' … ' +
|
||||||
|
categories[ last( ancestorIds ) ].name +
|
||||||
|
' › '
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { categories, category } = this.props;
|
||||||
|
|
||||||
|
return category ? (
|
||||||
|
<div className="woocommerce-table__breadcrumbs">
|
||||||
|
{ this.getCategoryAncestors( category, categories ) }
|
||||||
|
<Link
|
||||||
|
href={ 'term.php?taxonomy=product_cat&post_type=product&tag_ID=' + category.id }
|
||||||
|
type="wp-admin"
|
||||||
|
>
|
||||||
|
{ category.name }
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,12 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { getRequestByIdString } from 'lib/async-requests';
|
||||||
|
import { NAMESPACE } from 'store/constants';
|
||||||
|
|
||||||
export const charts = [
|
export const charts = [
|
||||||
{
|
{
|
||||||
key: 'items_sold',
|
key: 'items_sold',
|
||||||
|
@ -30,7 +36,37 @@ export const filters = [
|
||||||
showFilters: () => true,
|
showFilters: () => true,
|
||||||
filters: [
|
filters: [
|
||||||
{ label: __( 'All Categories', 'wc-admin' ), value: 'all' },
|
{ label: __( 'All Categories', 'wc-admin' ), value: 'all' },
|
||||||
{ label: __( 'Advanced Filters', 'wc-admin' ), value: 'advanced' },
|
{
|
||||||
|
label: __( 'Comparison', 'wc-admin' ),
|
||||||
|
value: 'compare-categories',
|
||||||
|
chartMode: 'item-comparison',
|
||||||
|
settings: {
|
||||||
|
type: 'categories',
|
||||||
|
param: 'categories',
|
||||||
|
getLabels: getRequestByIdString( NAMESPACE + 'products/categories', cat => ( {
|
||||||
|
id: cat.id,
|
||||||
|
label: cat.name,
|
||||||
|
} ) ),
|
||||||
|
labels: {
|
||||||
|
helpText: __( 'Select at least two categories to compare', 'wc-admin' ),
|
||||||
|
placeholder: __( 'Search for categories to compare', 'wc-admin' ),
|
||||||
|
title: __( 'Compare Categories', 'wc-admin' ),
|
||||||
|
update: __( 'Compare', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Top Categories by Items Sold', 'wc-admin' ),
|
||||||
|
value: 'top_items',
|
||||||
|
chartMode: 'item-comparison',
|
||||||
|
query: { orderby: 'items_sold', order: 'desc' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Top Categories by Net Revenue', 'wc-admin' ),
|
||||||
|
value: 'top_revenue',
|
||||||
|
chartMode: 'item-comparison',
|
||||||
|
query: { orderby: 'net_revenue', order: 'desc' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,21 +4,23 @@
|
||||||
*/
|
*/
|
||||||
import { __, _n } from '@wordpress/i18n';
|
import { __, _n } from '@wordpress/i18n';
|
||||||
import { Component } from '@wordpress/element';
|
import { Component } from '@wordpress/element';
|
||||||
|
import { compose } from '@wordpress/compose';
|
||||||
import { map } from 'lodash';
|
import { map } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WooCommerce dependencies
|
* WooCommerce dependencies
|
||||||
*/
|
*/
|
||||||
import { Link } from '@woocommerce/components';
|
|
||||||
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import ReportTable from 'analytics/components/report-table';
|
import CategoryBreacrumbs from './breadcrumbs';
|
||||||
import { numberFormat } from 'lib/number';
|
import { numberFormat } from 'lib/number';
|
||||||
|
import ReportTable from 'analytics/components/report-table';
|
||||||
|
import withSelect from 'wc-api/with-select';
|
||||||
|
|
||||||
export default class CategoriesReportTable extends Component {
|
class CategoriesReportTable extends Component {
|
||||||
constructor( props ) {
|
constructor( props ) {
|
||||||
super( props );
|
super( props );
|
||||||
|
|
||||||
|
@ -32,7 +34,6 @@ export default class CategoriesReportTable extends Component {
|
||||||
key: 'category',
|
key: 'category',
|
||||||
required: true,
|
required: true,
|
||||||
isLeftAligned: true,
|
isLeftAligned: true,
|
||||||
isSortable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Items sold', 'wc-admin' ),
|
label: __( 'Items sold', 'wc-admin' ),
|
||||||
|
@ -43,8 +44,7 @@ export default class CategoriesReportTable extends Component {
|
||||||
isNumeric: true,
|
isNumeric: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'G. Revenue', 'wc-admin' ),
|
label: __( 'Net Revenue', 'wc-admin' ),
|
||||||
screenReaderLabel: __( 'Gross Revenue', 'wc-admin' ),
|
|
||||||
key: 'net_revenue',
|
key: 'net_revenue',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isNumeric: true,
|
isNumeric: true,
|
||||||
|
@ -64,29 +64,16 @@ export default class CategoriesReportTable extends Component {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowsContent( categories ) {
|
getRowsContent( categoryStats ) {
|
||||||
return map( categories, category => {
|
return map( categoryStats, categoryStat => {
|
||||||
const {
|
const { category_id, items_sold, net_revenue, products_count, orders_count } = categoryStat;
|
||||||
category_id,
|
const categories = this.props.categories;
|
||||||
items_sold,
|
const category = categories[ category_id ];
|
||||||
net_revenue,
|
|
||||||
products_count,
|
|
||||||
orders_count,
|
|
||||||
extended_info,
|
|
||||||
} = category;
|
|
||||||
const { name } = extended_info;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
display: (
|
display: <CategoryBreacrumbs category={ category } categories={ categories } />,
|
||||||
<Link
|
value: category && category.name,
|
||||||
href={ 'term.php?taxonomy=product_cat&post_type=product&tag_ID=' + category_id }
|
|
||||||
type="wp-admin"
|
|
||||||
>
|
|
||||||
{ name }
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
value: name,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: numberFormat( items_sold ),
|
display: numberFormat( items_sold ),
|
||||||
|
@ -136,15 +123,21 @@ export default class CategoriesReportTable extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { query } = this.props;
|
const { query } = this.props;
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
helpText: __( 'Select at least two categories to compare', 'wc-admin' ),
|
||||||
|
placeholder: __( 'Search by category name', 'wc-admin' ),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReportTable
|
<ReportTable
|
||||||
compareBy="product_cats"
|
compareBy="categories"
|
||||||
endpoint="categories"
|
endpoint="categories"
|
||||||
getHeadersContent={ this.getHeadersContent }
|
getHeadersContent={ this.getHeadersContent }
|
||||||
getRowsContent={ this.getRowsContent }
|
getRowsContent={ this.getRowsContent }
|
||||||
getSummary={ this.getSummary }
|
getSummary={ this.getSummary }
|
||||||
itemIdField="category_id"
|
itemIdField="category_id"
|
||||||
query={ query }
|
query={ query }
|
||||||
|
labels={ labels }
|
||||||
tableQuery={ {
|
tableQuery={ {
|
||||||
orderby: query.orderby || 'items_sold',
|
orderby: query.orderby || 'items_sold',
|
||||||
order: query.order || 'desc',
|
order: query.order || 'desc',
|
||||||
|
@ -156,3 +149,18 @@ export default class CategoriesReportTable extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withSelect( select => {
|
||||||
|
const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
|
||||||
|
const tableQuery = {
|
||||||
|
per_page: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const categories = getCategories( tableQuery );
|
||||||
|
const isError = Boolean( getCategoriesError( tableQuery ) );
|
||||||
|
const isRequesting = isGetCategoriesRequesting( tableQuery );
|
||||||
|
|
||||||
|
return { categories, isError, isRequesting };
|
||||||
|
} )
|
||||||
|
)( CategoriesReportTable );
|
||||||
|
|
|
@ -12,13 +12,13 @@ import { NAMESPACE } from 'store/constants';
|
||||||
|
|
||||||
export const charts = [
|
export const charts = [
|
||||||
{
|
{
|
||||||
key: 'discounted_orders',
|
key: 'orders_count',
|
||||||
label: __( 'Discounted Orders', 'wc-admin' ),
|
label: __( 'Discounted Orders', 'wc-admin' ),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'coupons',
|
key: 'amount',
|
||||||
label: __( 'Gross Discounted', 'wc-admin' ),
|
label: __( 'Amount', 'wc-admin' ),
|
||||||
type: 'currency',
|
type: 'currency',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -48,7 +48,7 @@ export const filters = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ label: __( 'Top Coupons by Discounted Orders', 'wc-admin' ), value: 'top_orders' },
|
{ label: __( 'Top Coupons by Discounted Orders', 'wc-admin' ), value: 'top_orders' },
|
||||||
{ label: __( 'Top Coupons by Gross Discounted', 'wc-admin' ), value: 'top_discount' },
|
{ label: __( 'Top Coupons by Amount Discounted', 'wc-admin' ), value: 'top_discount' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -28,13 +28,13 @@ export default class CouponsReport extends Component {
|
||||||
<ReportFilters query={ query } path={ path } filters={ filters } />
|
<ReportFilters query={ query } path={ path } filters={ filters } />
|
||||||
<ReportSummary
|
<ReportSummary
|
||||||
charts={ charts }
|
charts={ charts }
|
||||||
endpoint="orders"
|
endpoint="coupons"
|
||||||
query={ query }
|
query={ query }
|
||||||
selectedChart={ getSelectedChart( query.chart, charts ) }
|
selectedChart={ getSelectedChart( query.chart, charts ) }
|
||||||
/>
|
/>
|
||||||
<ReportChart
|
<ReportChart
|
||||||
charts={ charts }
|
charts={ charts }
|
||||||
endpoint="orders"
|
endpoint="coupons"
|
||||||
path={ path }
|
path={ path }
|
||||||
query={ query }
|
query={ query }
|
||||||
selectedChart={ getSelectedChart( query.chart, charts ) }
|
selectedChart={ getSelectedChart( query.chart, charts ) }
|
||||||
|
|
|
@ -33,11 +33,9 @@ export default class CouponsReportTable extends Component {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: __( 'Coupon Code', 'wc-admin' ),
|
label: __( 'Coupon Code', 'wc-admin' ),
|
||||||
// @TODO it should be the coupon code, not the coupon ID
|
|
||||||
key: 'coupon_id',
|
key: 'coupon_id',
|
||||||
required: true,
|
required: true,
|
||||||
isLeftAligned: true,
|
isLeftAligned: true,
|
||||||
isSortable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Orders', 'wc-admin' ),
|
label: __( 'Orders', 'wc-admin' ),
|
||||||
|
@ -48,26 +46,22 @@ export default class CouponsReportTable extends Component {
|
||||||
isNumeric: true,
|
isNumeric: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'G. Discounted', 'wc-admin' ),
|
label: __( 'Amount Discounted', 'wc-admin' ),
|
||||||
screenReaderLabel: __( 'Gross Discounted', 'wc-admin' ),
|
key: 'amount',
|
||||||
key: 'gross_discount',
|
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isNumeric: true,
|
isNumeric: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Created', 'wc-admin' ),
|
label: __( 'Created', 'wc-admin' ),
|
||||||
key: 'created',
|
key: 'created',
|
||||||
isSortable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Expires', 'wc-admin' ),
|
label: __( 'Expires', 'wc-admin' ),
|
||||||
key: 'expires',
|
key: 'expires',
|
||||||
isSortable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Type', 'wc-admin' ),
|
label: __( 'Type', 'wc-admin' ),
|
||||||
key: 'type',
|
key: 'type',
|
||||||
isSortable: false,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -78,12 +72,13 @@ export default class CouponsReportTable extends Component {
|
||||||
const { tableFormat } = getDateFormatsForInterval( currentInterval );
|
const { tableFormat } = getDateFormatsForInterval( currentInterval );
|
||||||
|
|
||||||
return map( coupons, coupon => {
|
return map( coupons, coupon => {
|
||||||
const { coupon_id, gross_discount, orders_count } = coupon;
|
const { amount, coupon_id, extended_info, orders_count } = coupon;
|
||||||
|
const { code, date_created, date_expires, discount_type } = extended_info;
|
||||||
|
|
||||||
// @TODO must link to the coupon detail report
|
// @TODO must link to the coupon detail report
|
||||||
const couponLink = (
|
const couponLink = (
|
||||||
<Link href="" type="wc-admin">
|
<Link href="" type="wc-admin">
|
||||||
{ coupon_id }
|
{ code }
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -97,33 +92,29 @@ export default class CouponsReportTable extends Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// @TODO it should be the coupon code, not the coupon ID
|
|
||||||
{
|
{
|
||||||
display: couponLink,
|
display: couponLink,
|
||||||
value: coupon_id,
|
value: code,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: ordersLink,
|
display: ordersLink,
|
||||||
value: orders_count,
|
value: orders_count,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: formatCurrency( gross_discount ),
|
display: formatCurrency( amount ),
|
||||||
value: getCurrencyFormatDecimal( gross_discount ),
|
value: getCurrencyFormatDecimal( amount ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// @TODO
|
display: formatDate( tableFormat, date_created ),
|
||||||
display: formatDate( tableFormat, '' ),
|
value: date_created,
|
||||||
value: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// @TODO
|
display: date_expires ? formatDate( tableFormat, date_expires ) : __( 'N/A', 'wc-admin' ),
|
||||||
display: formatDate( tableFormat, '' ),
|
value: date_expires,
|
||||||
value: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// @TODO
|
display: this.getCouponType( discount_type ),
|
||||||
display: '',
|
value: discount_type,
|
||||||
value: '',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} );
|
} );
|
||||||
|
@ -143,12 +134,21 @@ export default class CouponsReportTable extends Component {
|
||||||
value: numberFormat( totals.orders_count ),
|
value: numberFormat( totals.orders_count ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'gross discounted', 'wc-admin' ),
|
label: __( 'amount discounted', 'wc-admin' ),
|
||||||
value: formatCurrency( totals.gross_discount ),
|
value: formatCurrency( totals.amount ),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCouponType( discount_type ) {
|
||||||
|
const couponTypes = {
|
||||||
|
percent: __( 'Percentage', 'wc-admin' ),
|
||||||
|
fixed_cart: __( 'Fixed cart', 'wc-admin' ),
|
||||||
|
fixed_product: __( 'Fixed product', 'wc-admin' ),
|
||||||
|
};
|
||||||
|
return couponTypes[ discount_type ];
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { query } = this.props;
|
const { query } = this.props;
|
||||||
|
|
||||||
|
@ -161,6 +161,11 @@ export default class CouponsReportTable extends Component {
|
||||||
getSummary={ this.getSummary }
|
getSummary={ this.getSummary }
|
||||||
itemIdField="coupon_id"
|
itemIdField="coupon_id"
|
||||||
query={ query }
|
query={ query }
|
||||||
|
tableQuery={ {
|
||||||
|
orderby: query.orderby || 'coupon_id',
|
||||||
|
order: query.order || 'asc',
|
||||||
|
extended_info: true,
|
||||||
|
} }
|
||||||
title={ __( 'Coupons', 'wc-admin' ) }
|
title={ __( 'Coupons', 'wc-admin' ) }
|
||||||
columnPrefsKey="coupons_report_columns"
|
columnPrefsKey="coupons_report_columns"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -166,6 +166,91 @@ export const advancedFilters = {
|
||||||
} ) ),
|
} ) ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
order_count: {
|
||||||
|
labels: {
|
||||||
|
add: __( 'No. of Orders', 'wc-admin' ),
|
||||||
|
remove: __( 'Remove order filter', 'wc-admin' ),
|
||||||
|
rule: __( 'Select an order count filter match', 'wc-admin' ),
|
||||||
|
title: __( 'No. of Orders {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
value: 'max',
|
||||||
|
/* translators: Sentence fragment, logical, "Less Than" refers to number of orders a customer has placed, less than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'Less Than', 'number of orders', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'min',
|
||||||
|
/* translators: Sentence fragment, logical, "More Than" refers to number of orders a customer has placed, more than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'More Than', 'number of orders', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'between',
|
||||||
|
/* translators: Sentence fragment, logical, "Between" refers to number of orders a customer has placed, between two given integers. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'Between', 'number of orders', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: {
|
||||||
|
component: 'Number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
total_spend: {
|
||||||
|
labels: {
|
||||||
|
add: __( 'Total Spend', 'wc-admin' ),
|
||||||
|
remove: __( 'Remove total spend filter', 'wc-admin' ),
|
||||||
|
rule: __( 'Select a total spend filter match', 'wc-admin' ),
|
||||||
|
title: __( 'Total Spend {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
value: 'max',
|
||||||
|
/* translators: Sentence fragment, logical, "Less Than" refers to total spending by a customer, less than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'Less Than', 'total spend by customer', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'min',
|
||||||
|
/* translators: Sentence fragment, logical, "Less Than" refers to total spending by a customer, more than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'More Than', 'total spend by customer', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'between',
|
||||||
|
/* translators: Sentence fragment, logical, "Between" refers to total spending by a customer, between two given amounts. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'Between', 'total spend by customer', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: {
|
||||||
|
component: 'Currency',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
avg_order_value: {
|
||||||
|
labels: {
|
||||||
|
add: __( 'AOV', 'wc-admin' ),
|
||||||
|
remove: __( 'Remove average older value filter', 'wc-admin' ),
|
||||||
|
rule: __( 'Select an average order value filter match', 'wc-admin' ),
|
||||||
|
title: __( 'AOV {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
value: 'max',
|
||||||
|
/* translators: Sentence fragment, logical, "Less Than" refers to average order value of a customer, more than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'Less Than', 'average order value of customer', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'min',
|
||||||
|
/* translators: Sentence fragment, logical, "Less Than" refers to average order value of a customer, less than a given amount. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
|
||||||
|
label: _x( 'More Than', 'average order value of customer', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'between',
|
||||||
|
/* translators: Sentence fragment, logical, "Between" refers to average order value of a customer, between two given amounts. Screenshot for context: https://cloudup.com/cCsm3GeXJbE */
|
||||||
|
label: _x( 'Between', 'average order value of customer', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: {
|
||||||
|
component: 'Currency',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/*eslint-enable max-len*/
|
/*eslint-enable max-len*/
|
||||||
|
|
|
@ -70,6 +70,104 @@ export const advancedFilters = {
|
||||||
} ) ),
|
} ) ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
username: {
|
||||||
|
labels: {
|
||||||
|
add: __( 'Username', 'wc-admin' ),
|
||||||
|
placeholder: __( 'Search customer username', 'wc-admin' ),
|
||||||
|
remove: __( 'Remove customer username filter', 'wc-admin' ),
|
||||||
|
rule: __( 'Select a customer username filter match', 'wc-admin' ),
|
||||||
|
/* translators: A sentence describing a customer username filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
title: __( 'Username {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
|
filter: __( 'Select customer username', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
value: 'includes',
|
||||||
|
/* translators: Sentence fragment, logical, "Includes" refers to customer usernames including a given username(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
label: _x( 'Includes', 'customer usernames', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'excludes',
|
||||||
|
/* translators: Sentence fragment, logical, "Excludes" refers to customer usernames excluding a given username(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
label: _x( 'Excludes', 'customer usernames', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: {
|
||||||
|
component: 'Search',
|
||||||
|
type: 'usernames',
|
||||||
|
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( {
|
||||||
|
id: customer.id,
|
||||||
|
label: customer.username,
|
||||||
|
} ) ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
labels: {
|
||||||
|
add: __( 'Order number', 'wc-admin' ),
|
||||||
|
placeholder: __( 'Search order number', 'wc-admin' ),
|
||||||
|
remove: __( 'Remove order number filter', 'wc-admin' ),
|
||||||
|
rule: __( 'Select a order number filter match', 'wc-admin' ),
|
||||||
|
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
title: __( 'Order number {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
|
filter: __( 'Select order number', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
value: 'includes',
|
||||||
|
/* translators: Sentence fragment, logical, "Includes" refers to order numbers including a given order(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
label: _x( 'Includes', 'order numbers', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'excludes',
|
||||||
|
/* translators: Sentence fragment, logical, "Excludes" refers to order numbers excluding a given order(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
label: _x( 'Excludes', 'order numbers', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: {
|
||||||
|
component: 'Search',
|
||||||
|
type: 'orders',
|
||||||
|
getLabels: getRequestByIdString( NAMESPACE + 'orders', order => ( {
|
||||||
|
id: order.id,
|
||||||
|
label: '#' + order.id,
|
||||||
|
} ) ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
downloadIp: {
|
||||||
|
labels: {
|
||||||
|
add: __( 'IP Address', 'wc-admin' ),
|
||||||
|
placeholder: __( 'Search IP address', 'wc-admin' ),
|
||||||
|
remove: __( 'Remove IP address filter', 'wc-admin' ),
|
||||||
|
rule: __( 'Select an IP address filter match', 'wc-admin' ),
|
||||||
|
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
title: __( 'IP Address {{rule /}} {{filter /}}', 'wc-admin' ),
|
||||||
|
filter: __( 'Select IP address', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
value: 'includes',
|
||||||
|
/* translators: Sentence fragment, logical, "Includes" refers to IP addresses including a given address(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
label: _x( 'Includes', 'IP addresses', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'excludes',
|
||||||
|
/* translators: Sentence fragment, logical, "Excludes" refers to IP addresses excluding a given address(s). Screenshot for context: https://cloudup.com/ccxhyH2mEDg */
|
||||||
|
label: _x( 'Excludes', 'IP addresses', 'wc-admin' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: {
|
||||||
|
component: 'Search',
|
||||||
|
type: 'downloadIps',
|
||||||
|
getLabels: async value => {
|
||||||
|
const ips = value.split( ',' );
|
||||||
|
return await ips.map( ip => {
|
||||||
|
return {
|
||||||
|
id: ip,
|
||||||
|
label: ip,
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/*eslint-enable max-len*/
|
/*eslint-enable max-len*/
|
||||||
|
|
|
@ -81,10 +81,10 @@ const filterConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __( 'Product Category Comparison', 'wc-admin' ),
|
label: __( 'Product Category Comparison', 'wc-admin' ),
|
||||||
value: 'compare-product_cats',
|
value: 'compare-categories',
|
||||||
chartMode: 'item-comparison',
|
chartMode: 'item-comparison',
|
||||||
settings: {
|
settings: {
|
||||||
type: 'product_cats',
|
type: 'categories',
|
||||||
param: 'categories',
|
param: 'categories',
|
||||||
getLabels: getRequestByIdString( NAMESPACE + 'products/categories', category => ( {
|
getLabels: getRequestByIdString( NAMESPACE + 'products/categories', category => ( {
|
||||||
id: category.id,
|
id: category.id,
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
.woocommerce-table__product-categories {
|
||||||
|
> .woocommerce-table__breadcrumbs {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $gap-small;
|
||||||
|
}
|
||||||
|
.components-popover__content {
|
||||||
|
padding: 0 $gap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.components-popover__content .woocommerce-table__breadcrumbs {
|
||||||
|
margin-top: $gap-small;
|
||||||
|
margin-bottom: $gap-small;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,25 +2,29 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, _n, _x } from '@wordpress/i18n';
|
import { __, _n, _x, sprintf } from '@wordpress/i18n';
|
||||||
import { Component } from '@wordpress/element';
|
import { Component } from '@wordpress/element';
|
||||||
|
import { compose } from '@wordpress/compose';
|
||||||
import { map } from 'lodash';
|
import { map } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WooCommerce dependencies
|
* WooCommerce dependencies
|
||||||
*/
|
*/
|
||||||
import { Link } from '@woocommerce/components';
|
|
||||||
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
||||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||||
|
import { Link, Tag } from '@woocommerce/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import ReportTable from 'analytics/components/report-table';
|
import CategoryBreacrumbs from '../categories/breadcrumbs';
|
||||||
import { numberFormat } from 'lib/number';
|
|
||||||
import { isLowStock } from './utils';
|
import { isLowStock } from './utils';
|
||||||
|
import { numberFormat } from 'lib/number';
|
||||||
|
import ReportTable from 'analytics/components/report-table';
|
||||||
|
import withSelect from 'wc-api/with-select';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
export default class ProductsReportTable extends Component {
|
class ProductsReportTable extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -91,15 +95,20 @@ export default class ProductsReportTable extends Component {
|
||||||
return map( data, row => {
|
return map( data, row => {
|
||||||
const {
|
const {
|
||||||
product_id,
|
product_id,
|
||||||
sku = '', // @TODO
|
|
||||||
extended_info,
|
extended_info,
|
||||||
items_sold,
|
items_sold,
|
||||||
net_revenue,
|
net_revenue,
|
||||||
orders_count,
|
orders_count,
|
||||||
categories = [], // @TODO
|
|
||||||
variations = [], // @TODO
|
variations = [], // @TODO
|
||||||
} = row;
|
} = row;
|
||||||
const { name, stock_status, stock_quantity, low_stock_amount } = extended_info;
|
const {
|
||||||
|
category_ids,
|
||||||
|
low_stock_amount,
|
||||||
|
name,
|
||||||
|
sku,
|
||||||
|
stock_status,
|
||||||
|
stock_quantity,
|
||||||
|
} = extended_info;
|
||||||
const ordersLink = getNewPath( persistedQuery, 'orders', {
|
const ordersLink = getNewPath( persistedQuery, 'orders', {
|
||||||
filter: 'advanced',
|
filter: 'advanced',
|
||||||
product_includes: product_id,
|
product_includes: product_id,
|
||||||
|
@ -108,6 +117,10 @@ export default class ProductsReportTable extends Component {
|
||||||
filter: 'single_product',
|
filter: 'single_product',
|
||||||
products: product_id,
|
products: product_id,
|
||||||
} );
|
} );
|
||||||
|
const categories = this.props.categories;
|
||||||
|
const productCategories = category_ids
|
||||||
|
.map( category_id => categories[ category_id ] )
|
||||||
|
.filter( Boolean );
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -139,10 +152,29 @@ export default class ProductsReportTable extends Component {
|
||||||
value: orders_count,
|
value: orders_count,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: Array.isArray( categories )
|
display: (
|
||||||
? categories.map( cat => cat.name ).join( ', ' )
|
<div className="woocommerce-table__product-categories">
|
||||||
: '',
|
{ productCategories[ 0 ] && (
|
||||||
value: Array.isArray( categories ) ? categories.map( cat => cat.name ).join( ', ' ) : '',
|
<CategoryBreacrumbs category={ productCategories[ 0 ] } categories={ categories } />
|
||||||
|
) }
|
||||||
|
{ productCategories.length > 1 && (
|
||||||
|
<Tag
|
||||||
|
label={ sprintf(
|
||||||
|
_x( '+%d more', 'categories', 'wc-admin' ),
|
||||||
|
productCategories.length - 1
|
||||||
|
) }
|
||||||
|
popoverContents={ productCategories.map( category => (
|
||||||
|
<CategoryBreacrumbs
|
||||||
|
category={ category }
|
||||||
|
categories={ categories }
|
||||||
|
key={ category.id }
|
||||||
|
/>
|
||||||
|
) ) }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: productCategories.map( category => category.name ).join( ', ' ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: numberFormat( variations.length ),
|
display: numberFormat( variations.length ),
|
||||||
|
@ -219,3 +251,18 @@ export default class ProductsReportTable extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withSelect( select => {
|
||||||
|
const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
|
||||||
|
const tableQuery = {
|
||||||
|
per_page: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const categories = getCategories( tableQuery );
|
||||||
|
const isError = Boolean( getCategoriesError( tableQuery ) );
|
||||||
|
const isRequesting = isGetCategoriesRequesting( tableQuery );
|
||||||
|
|
||||||
|
return { categories, isError, isRequesting };
|
||||||
|
} )
|
||||||
|
)( ProductsReportTable );
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Component, Fragment } from '@wordpress/element';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce dependencies
|
||||||
|
*/
|
||||||
|
import { Card } from '@woocommerce/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import ReportChart from 'analytics/components/report-chart';
|
||||||
|
import './block.scss';
|
||||||
|
|
||||||
|
class ChartBlock extends Component {
|
||||||
|
render() {
|
||||||
|
const { charts, endpoint, path, query } = this.props;
|
||||||
|
|
||||||
|
if ( ! charts || ! charts.length ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Card className="woocommerce-dashboard__chart-block" title={ charts[ 0 ].label }>
|
||||||
|
<ReportChart
|
||||||
|
charts={ charts }
|
||||||
|
endpoint={ endpoint }
|
||||||
|
mode="block"
|
||||||
|
path={ path }
|
||||||
|
query={ query }
|
||||||
|
selectedChart={ charts[ 0 ] }
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartBlock.propTypes = {
|
||||||
|
charts: PropTypes.array,
|
||||||
|
endpoint: PropTypes.string.isRequired,
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
query: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChartBlock;
|
|
@ -0,0 +1,37 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
.woocommerce-dashboard__chart-block {
|
||||||
|
.woocommerce-card__body {
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.woocommerce-chart {
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.woocommerce-legend__item > button {
|
||||||
|
cursor: default;
|
||||||
|
&:hover {
|
||||||
|
background: $core-grey-light-100;
|
||||||
|
}
|
||||||
|
.woocommerce-legend__item-container {
|
||||||
|
cursor: default;
|
||||||
|
.woocommerce-legend__item-checkmark.woocommerce-legend__item-checkmark-checked::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $core-grey-light-100;
|
||||||
|
.woocommerce-legend__item > button {
|
||||||
|
background: $core-grey-light-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $core-grey-light-100;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { charts as ordersCharts } from 'analytics/report/orders/config';
|
||||||
|
import { charts as productsCharts } from 'analytics/report/products/config';
|
||||||
|
import { charts as revenueCharts } from 'analytics/report/revenue/config';
|
||||||
|
import { charts as categoriesCharts } from 'analytics/report/categories/config';
|
||||||
|
import { charts as couponsCharts } from 'analytics/report/coupons/config';
|
||||||
|
import { charts as taxesCharts } from 'analytics/report/taxes/config';
|
||||||
|
|
||||||
|
const allCharts = ordersCharts
|
||||||
|
.map( d => ( { ...d, endpoint: 'orders' } ) )
|
||||||
|
.concat(
|
||||||
|
productsCharts.map( d => ( { ...d, endpoint: 'products' } ) ),
|
||||||
|
revenueCharts.map( d => ( { ...d, endpoint: 'revenue' } ) ),
|
||||||
|
categoriesCharts.map( d => ( { ...d, endpoint: 'categories' } ) ),
|
||||||
|
couponsCharts.map( d => ( { ...d, endpoint: 'orders' } ) ),
|
||||||
|
taxesCharts.map( d => ( { ...d, endpoint: 'taxes' } ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Need to remove duplicate charts, by key, from the configs
|
||||||
|
const uniqCharts = allCharts.reduce( ( a, b ) => {
|
||||||
|
if ( a.findIndex( d => d.key === b.key ) < 0 ) {
|
||||||
|
a.push( b );
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
// Default charts.
|
||||||
|
// TODO: Implement user-based toggling/persistence.
|
||||||
|
const defaultCharts = [ 'items_sold', 'gross_revenue' ];
|
||||||
|
|
||||||
|
export const showCharts = uniqCharts.map( d => ( {
|
||||||
|
...d,
|
||||||
|
show: defaultCharts.indexOf( d.key ) >= 0,
|
||||||
|
} ) );
|
||||||
|
export const getChartFromKey = key => allCharts.filter( d => d.key === key );
|
|
@ -0,0 +1,130 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Gridicon from 'gridicons';
|
||||||
|
import { ToggleControl, IconButton, NavigableMenu } from '@wordpress/components';
|
||||||
|
import { Component, Fragment } from '@wordpress/element';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce dependencies
|
||||||
|
*/
|
||||||
|
import { EllipsisMenu, MenuItem, SectionHeader } from '@woocommerce/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import ChartBlock from './block';
|
||||||
|
import { getChartFromKey, showCharts } from './config';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
class DashboardCharts extends Component {
|
||||||
|
constructor( props ) {
|
||||||
|
super( ...arguments );
|
||||||
|
this.state = {
|
||||||
|
showCharts,
|
||||||
|
query: props.query,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toggle = this.toggle.bind( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle( key ) {
|
||||||
|
return () => {
|
||||||
|
this.setState( state => {
|
||||||
|
const foundIndex = state.showCharts.findIndex( x => x.key === key );
|
||||||
|
state.showCharts[ foundIndex ].show = ! state.showCharts[ foundIndex ].show;
|
||||||
|
return state;
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTypeToggle( type ) {
|
||||||
|
return () => {
|
||||||
|
this.setState( state => ( { query: { ...state.query, type } } ) );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMenu() {
|
||||||
|
return (
|
||||||
|
<EllipsisMenu label={ __( 'Choose which charts to display', 'wc-admin' ) }>
|
||||||
|
{ this.state.showCharts.map( chart => {
|
||||||
|
return (
|
||||||
|
<MenuItem onInvoke={ this.toggle( chart.key ) } key={ chart.key }>
|
||||||
|
<ToggleControl
|
||||||
|
label={ __( `${ chart.label }`, 'wc-admin' ) }
|
||||||
|
checked={ chart.show }
|
||||||
|
onChange={ this.toggle( chart.key ) }
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
} ) }
|
||||||
|
</EllipsisMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { path } = this.props;
|
||||||
|
const { query } = this.state;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="woocommerce-dashboard__dashboard-charts">
|
||||||
|
<SectionHeader title={ __( 'Charts', 'wc-admin' ) } menu={ this.renderMenu() }>
|
||||||
|
<NavigableMenu
|
||||||
|
className="woocommerce-chart__types"
|
||||||
|
orientation="horizontal"
|
||||||
|
role="menubar"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
className={ classNames( 'woocommerce-chart__type-button', {
|
||||||
|
'woocommerce-chart__type-button-selected': ! query.type || query.type === 'line',
|
||||||
|
} ) }
|
||||||
|
icon={ <Gridicon icon="line-graph" /> }
|
||||||
|
title={ __( 'Line chart', 'wc-admin' ) }
|
||||||
|
aria-checked={ query.type === 'line' }
|
||||||
|
role="menuitemradio"
|
||||||
|
tabIndex={ query.type === 'line' ? 0 : -1 }
|
||||||
|
onClick={ this.handleTypeToggle( 'line' ) }
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
className={ classNames( 'woocommerce-chart__type-button', {
|
||||||
|
'woocommerce-chart__type-button-selected': query.type === 'bar',
|
||||||
|
} ) }
|
||||||
|
icon={ <Gridicon icon="stats-alt" /> }
|
||||||
|
title={ __( 'Bar chart', 'wc-admin' ) }
|
||||||
|
aria-checked={ query.type === 'bar' }
|
||||||
|
role="menuitemradio"
|
||||||
|
tabIndex={ query.type === 'bar' ? 0 : -1 }
|
||||||
|
onClick={ this.handleTypeToggle( 'bar' ) }
|
||||||
|
/>
|
||||||
|
</NavigableMenu>
|
||||||
|
</SectionHeader>
|
||||||
|
<div className="woocommerce-dashboard__columns">
|
||||||
|
{ this.state.showCharts.map( chart => {
|
||||||
|
return ! chart.show ? null : (
|
||||||
|
<div key={ chart.key }>
|
||||||
|
<ChartBlock
|
||||||
|
charts={ getChartFromKey( chart.key ) }
|
||||||
|
endpoint={ chart.endpoint }
|
||||||
|
path={ path }
|
||||||
|
query={ query }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} ) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DashboardCharts.propTypes = {
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
query: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardCharts;
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
.woocommerce-dashboard__dashboard-charts {
|
||||||
|
border-bottom: 0;
|
||||||
|
border-right: 0;
|
||||||
|
|
||||||
|
.woocommerce-section-header__actions {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-card__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-summary {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import './style.scss';
|
||||||
import Header from 'header';
|
import Header from 'header';
|
||||||
import StorePerformance from './store-performance';
|
import StorePerformance from './store-performance';
|
||||||
import TopSellingProducts from './top-selling-products';
|
import TopSellingProducts from './top-selling-products';
|
||||||
|
import DashboardCharts from './dashboard-charts';
|
||||||
import { ReportFilters } from '@woocommerce/components';
|
import { ReportFilters } from '@woocommerce/components';
|
||||||
|
|
||||||
export default class Dashboard extends Component {
|
export default class Dashboard extends Component {
|
||||||
|
@ -24,9 +25,10 @@ export default class Dashboard extends Component {
|
||||||
<StorePerformance />
|
<StorePerformance />
|
||||||
<div className="woocommerce-dashboard__columns">
|
<div className="woocommerce-dashboard__columns">
|
||||||
<div>
|
<div>
|
||||||
<TopSellingProducts />
|
<TopSellingProducts query={ query } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DashboardCharts query={ query } path={ path } />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import { compose } from '@wordpress/compose';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce dependencies
|
||||||
|
*/
|
||||||
|
import { Card, EmptyTable, TableCard } from '@woocommerce/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import ReportError from 'analytics/components/report-error';
|
||||||
|
import { getReportTableData } from 'store/reports/utils';
|
||||||
|
import withSelect from 'wc-api/with-select';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
export class Leaderboard extends Component {
|
||||||
|
render() {
|
||||||
|
const { getHeadersContent, getRowsContent, isRequesting, isError, items, title } = this.props;
|
||||||
|
const data = get( items, [ 'data' ], [] );
|
||||||
|
const rows = getRowsContent( data );
|
||||||
|
|
||||||
|
if ( isError ) {
|
||||||
|
return <ReportError className="woocommerce-leaderboard" isError />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isRequesting && rows.length === 0 ) {
|
||||||
|
return (
|
||||||
|
<Card title={ title } className="woocommerce-leaderboard">
|
||||||
|
<EmptyTable>
|
||||||
|
{ __( 'No data recorded for the selected time period.', 'wc-admin' ) }
|
||||||
|
</EmptyTable>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCard
|
||||||
|
className="woocommerce-leaderboard"
|
||||||
|
headers={ getHeadersContent() }
|
||||||
|
isLoading={ isRequesting }
|
||||||
|
rows={ rows }
|
||||||
|
rowsPerPage={ 5 }
|
||||||
|
title={ title }
|
||||||
|
totalRows={ 5 }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Leaderboard.propTypes = {
|
||||||
|
/**
|
||||||
|
* The endpoint to use in API calls to populate the table rows and summary.
|
||||||
|
* For example, if `taxes` is provided, data will be fetched from the report
|
||||||
|
* `taxes` endpoint (ie: `/wc/v3/reports/taxes` and `/wc/v3/reports/taxes/stats`).
|
||||||
|
* If the provided endpoint doesn't exist, an error will be shown to the user
|
||||||
|
* with `ReportError`.
|
||||||
|
*/
|
||||||
|
endpoint: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* A function that returns the headers object to build the table.
|
||||||
|
*/
|
||||||
|
getHeadersContent: PropTypes.func.isRequired,
|
||||||
|
/**
|
||||||
|
* A function that returns the rows array to build the table.
|
||||||
|
*/
|
||||||
|
getRowsContent: PropTypes.func.isRequired,
|
||||||
|
/**
|
||||||
|
* Query args added to the report table endpoint request.
|
||||||
|
*/
|
||||||
|
query: PropTypes.object,
|
||||||
|
/**
|
||||||
|
* Properties to be added to the query sent to the report table endpoint.
|
||||||
|
*/
|
||||||
|
tableQuery: PropTypes.object,
|
||||||
|
/**
|
||||||
|
* String to display as the title of the table.
|
||||||
|
*/
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withSelect( ( select, props ) => {
|
||||||
|
const { endpoint, tableQuery, query } = props;
|
||||||
|
const tableData = getReportTableData( endpoint, query, select, tableQuery );
|
||||||
|
|
||||||
|
return { ...tableData };
|
||||||
|
} )
|
||||||
|
)( Leaderboard );
|
|
@ -1,5 +1,5 @@
|
||||||
/** @format */
|
/** @format */
|
||||||
.woocommerce-top-selling-products {
|
.woocommerce-leaderboard {
|
||||||
&.woocommerce-empty-content {
|
&.woocommerce-empty-content {
|
||||||
margin-bottom: $gap-large;
|
margin-bottom: $gap-large;
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
import TestRenderer from 'react-test-renderer';
|
import TestRenderer from 'react-test-renderer';
|
||||||
|
import { map, noop } from 'lodash';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency'
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import TopSellingProductsWithSelect, { TopSellingProducts } from '../';
|
import LeaderboardWithSelect, { Leaderboard } from '../';
|
||||||
import { numberFormat } from 'lib/number';
|
import { numberFormat } from 'lib/number';
|
||||||
import mockData from '../__mocks__/top-selling-products-mock-data';
|
import mockData from '../__mocks__/top-selling-products-mock-data';
|
||||||
|
|
||||||
|
@ -26,16 +27,49 @@ jest.mock( '@woocommerce/components', () => ( {
|
||||||
TableCard: () => null,
|
TableCard: () => null,
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
describe( 'TopSellingProducts', () => {
|
const getRowsContent = data => {
|
||||||
test( 'should render empty message when there are no rows', () => {
|
return map( data, row => {
|
||||||
const topSellingProducts = shallow( <TopSellingProducts data={ {} } /> );
|
const { name, items_sold, net_revenue, orders_count } = row;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
display: name,
|
||||||
|
value: name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: numberFormat( items_sold ),
|
||||||
|
value: items_sold,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: numberFormat( orders_count ),
|
||||||
|
value: orders_count,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: formatCurrency( net_revenue ),
|
||||||
|
value: getCurrencyFormatDecimal( net_revenue ),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
expect( topSellingProducts.find( 'EmptyTable' ).length ).toBe( 1 );
|
describe( 'Leaderboard', () => {
|
||||||
|
test( 'should render empty message when there are no rows', () => {
|
||||||
|
const leaderboard = shallow(
|
||||||
|
<Leaderboard title={ '' } getHeadersContent={ noop } getRowsContent={ getRowsContent } />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect( leaderboard.find( 'EmptyTable' ).length ).toBe( 1 );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'should render correct data in the table', () => {
|
test( 'should render correct data in the table', () => {
|
||||||
const topSellingProducts = shallow( <TopSellingProducts data={ mockData } /> );
|
const leaderboard = shallow(
|
||||||
const table = topSellingProducts.find( 'TableCard' );
|
<Leaderboard
|
||||||
|
title={ '' }
|
||||||
|
getHeadersContent={ noop }
|
||||||
|
getRowsContent={ getRowsContent }
|
||||||
|
items={ { data: { ...mockData } } }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const table = leaderboard.find( 'TableCard' );
|
||||||
const firstRow = table.props().rows[ 0 ];
|
const firstRow = table.props().rows[ 0 ];
|
||||||
|
|
||||||
expect( firstRow[ 0 ].value ).toBe( mockData[ 0 ].name );
|
expect( firstRow[ 0 ].value ).toBe( mockData[ 0 ].name );
|
||||||
|
@ -47,12 +81,13 @@ describe( 'TopSellingProducts', () => {
|
||||||
expect( firstRow[ 3 ].value ).toBe( getCurrencyFormatDecimal( mockData[ 0 ].net_revenue ) );
|
expect( firstRow[ 3 ].value ).toBe( getCurrencyFormatDecimal( mockData[ 0 ].net_revenue ) );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'should load report stats from API', () => {
|
// TODO: Since this now uses fresh-data / wc-api, the API testing needs to be revisted.
|
||||||
|
xtest( 'should load report stats from API', () => {
|
||||||
const getReportStatsMock = jest.fn().mockReturnValue( { data: mockData } );
|
const getReportStatsMock = jest.fn().mockReturnValue( { data: mockData } );
|
||||||
const isReportStatsRequestingMock = jest.fn().mockReturnValue( false );
|
const isReportStatsRequestingMock = jest.fn().mockReturnValue( false );
|
||||||
const isReportStatsErrorMock = jest.fn().mockReturnValue( false );
|
const isReportStatsErrorMock = jest.fn().mockReturnValue( false );
|
||||||
const registry = createRegistry();
|
const registry = createRegistry();
|
||||||
registry.registerStore( 'wc-admin', {
|
registry.registerStore( 'wc-api', {
|
||||||
reducer: () => {},
|
reducer: () => {},
|
||||||
selectors: {
|
selectors: {
|
||||||
getReportStats: getReportStatsMock,
|
getReportStats: getReportStatsMock,
|
||||||
|
@ -60,12 +95,12 @@ describe( 'TopSellingProducts', () => {
|
||||||
isReportStatsError: isReportStatsErrorMock,
|
isReportStatsError: isReportStatsErrorMock,
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
const topSellingProductsWrapper = TestRenderer.create(
|
const leaderboardWrapper = TestRenderer.create(
|
||||||
<RegistryProvider value={ registry }>
|
<RegistryProvider value={ registry }>
|
||||||
<TopSellingProductsWithSelect />
|
<LeaderboardWithSelect />
|
||||||
</RegistryProvider>
|
</RegistryProvider>
|
||||||
);
|
);
|
||||||
const topSellingProducts = topSellingProductsWrapper.root.findByType( TopSellingProducts );
|
const leaderboard = leaderboardWrapper.root.findByType( Leaderboard );
|
||||||
|
|
||||||
const endpoint = '/wc/v3/reports/products';
|
const endpoint = '/wc/v3/reports/products';
|
||||||
const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 };
|
const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 };
|
||||||
|
@ -76,6 +111,6 @@ describe( 'TopSellingProducts', () => {
|
||||||
expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
|
expect( isReportStatsRequestingMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
|
||||||
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
|
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
|
||||||
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
|
expect( isReportStatsErrorMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );
|
||||||
expect( topSellingProducts.props.data ).toBe( mockData );
|
expect( leaderboard.props.data ).toBe( mockData );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
|
@ -5,13 +5,10 @@
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Component } from '@wordpress/element';
|
import { Component } from '@wordpress/element';
|
||||||
import { get, map } from 'lodash';
|
import { get, map } from 'lodash';
|
||||||
import { compose } from '@wordpress/compose';
|
|
||||||
import { withSelect } from '@wordpress/data';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WooCommerce dependencies
|
* WooCommerce dependencies
|
||||||
*/
|
*/
|
||||||
import { Card, EmptyTable, TableCard } from '@woocommerce/components';
|
|
||||||
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
|
||||||
import { getAdminLink } from '@woocommerce/navigation';
|
import { getAdminLink } from '@woocommerce/navigation';
|
||||||
|
|
||||||
|
@ -19,11 +16,16 @@ import { getAdminLink } from '@woocommerce/navigation';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { numberFormat } from 'lib/number';
|
import { numberFormat } from 'lib/number';
|
||||||
import ReportError from 'analytics/components/report-error';
|
import Leaderboard from 'dashboard/leaderboard';
|
||||||
import { NAMESPACE } from 'store/constants';
|
|
||||||
import './style.scss';
|
|
||||||
|
|
||||||
export class TopSellingProducts extends Component {
|
export class TopSellingProducts extends Component {
|
||||||
|
constructor( props ) {
|
||||||
|
super( props );
|
||||||
|
|
||||||
|
this.getRowsContent = this.getRowsContent.bind( this );
|
||||||
|
this.getHeadersContent = this.getHeadersContent.bind( this );
|
||||||
|
}
|
||||||
|
|
||||||
getHeadersContent() {
|
getHeadersContent() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -59,8 +61,8 @@ export class TopSellingProducts extends Component {
|
||||||
|
|
||||||
getRowsContent( data ) {
|
getRowsContent( data ) {
|
||||||
return map( data, row => {
|
return map( data, row => {
|
||||||
const { product_id, items_sold, net_revenue, orders_count, name } = row;
|
const { product_id, items_sold, net_revenue, orders_count, extended_info } = row;
|
||||||
|
const name = get( extended_info, [ 'name' ] );
|
||||||
const productLink = (
|
const productLink = (
|
||||||
<a href={ getAdminLink( `/post.php?post=${ product_id }&action=edit` ) }>{ name }</a>
|
<a href={ getAdminLink( `/post.php?post=${ product_id }&action=edit` ) }>{ name }</a>
|
||||||
);
|
);
|
||||||
|
@ -86,53 +88,24 @@ export class TopSellingProducts extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, isRequesting, isError } = this.props;
|
const tableQuery = {
|
||||||
|
orderby: 'items_sold',
|
||||||
const rows = isRequesting || isError ? [] : this.getRowsContent( data );
|
order: 'desc',
|
||||||
const title = __( 'Top Selling Products', 'wc-admin' );
|
per_page: 5, //TODO replace with user configured leaderboard per page value.
|
||||||
|
extended_info: true,
|
||||||
if ( isError ) {
|
};
|
||||||
return <ReportError className="woocommerce-top-selling-products" isError />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! isRequesting && rows.length === 0 ) {
|
|
||||||
return (
|
|
||||||
<Card title={ title } className="woocommerce-top-selling-products">
|
|
||||||
<EmptyTable>
|
|
||||||
{ __( 'When new orders arrive, popular products will be listed here.', 'wc-admin' ) }
|
|
||||||
</EmptyTable>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = this.getHeadersContent();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCard
|
<Leaderboard
|
||||||
className="woocommerce-top-selling-products"
|
endpoint="products"
|
||||||
headers={ headers }
|
getHeadersContent={ this.getHeadersContent }
|
||||||
isLoading={ isRequesting }
|
getRowsContent={ this.getRowsContent }
|
||||||
rows={ rows }
|
query={ this.props.query }
|
||||||
rowsPerPage={ 5 }
|
tableQuery={ tableQuery }
|
||||||
title={ title }
|
title={ __( 'Top Selling Products', 'wc-admin' ) }
|
||||||
totalRows={ 5 }
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
export default TopSellingProducts;
|
||||||
withSelect( select => {
|
|
||||||
const { getReportStats, isReportStatsRequesting, isReportStatsError } = select( 'wc-admin' );
|
|
||||||
const endpoint = NAMESPACE + 'reports/products';
|
|
||||||
// @TODO We will need to add the date parameters from the Date Picker
|
|
||||||
// { after: '2018-04-22', before: '2018-05-06' }
|
|
||||||
const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 };
|
|
||||||
|
|
||||||
const stats = getReportStats( endpoint, query );
|
|
||||||
const isRequesting = isReportStatsRequesting( endpoint, query );
|
|
||||||
const isError = isReportStatsError( endpoint, query );
|
|
||||||
|
|
||||||
return { data: get( stats, 'data', [] ), isRequesting, isError };
|
|
||||||
} )
|
|
||||||
)( TopSellingProducts );
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
height: 80px;
|
height: 80px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 32px;
|
top: $adminbar-height;
|
||||||
z-index: 99991; /* higher than component-popover (99990), lower than notification dropdown at 99999 */
|
z-index: 99991; /* higher than component-popover (99990), lower than notification dropdown at 99999 */
|
||||||
|
|
||||||
&.is-scrolled {
|
&.is-scrolled {
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
top: 46px;
|
top: $adminbar-height-mobile;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,28 +14,12 @@ import { stringifyQuery } from '@woocommerce/navigation';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { NAMESPACE, SWAGGERNAMESPACE } from 'store/constants';
|
import { NAMESPACE } from 'store/constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async getReportItems( ...args ) {
|
async getReportItems( ...args ) {
|
||||||
const [ endpoint, query ] = args.slice( -2 );
|
const [ endpoint, query ] = args.slice( -2 );
|
||||||
|
|
||||||
const swaggerEndpoints = [ 'coupons' ];
|
|
||||||
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
SWAGGERNAMESPACE + 'reports/' + endpoint + stringifyQuery( query )
|
|
||||||
);
|
|
||||||
const itemsData = await response.json();
|
|
||||||
|
|
||||||
dispatch( 'wc-admin' ).setReportItems( endpoint, query, itemsData );
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch( 'wc-admin' ).setReportItemsError( endpoint, query );
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch( {
|
const response = await apiFetch( {
|
||||||
parse: false,
|
parse: false,
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default {
|
||||||
let apiPath = endpoint + stringifyQuery( query );
|
let apiPath = endpoint + stringifyQuery( query );
|
||||||
|
|
||||||
// TODO: Remove once swagger endpoints are phased out.
|
// TODO: Remove once swagger endpoints are phased out.
|
||||||
const swaggerEndpoints = [ 'categories', 'coupons' ];
|
const swaggerEndpoints = [ 'categories' ];
|
||||||
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
|
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
|
||||||
apiPath = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
|
apiPath = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -327,7 +327,7 @@ describe( 'getSummaryNumbers()', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
setGetReportStats( ( endpoint, _query ) => {
|
setGetReportStats( ( endpoint, _query ) => {
|
||||||
if ( '2018-10-10T00:00:00+00:00' === _query.after ) {
|
if ( '2018-10-10T00:00:00' === _query.after ) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
totals: totals.primary,
|
totals: totals.primary,
|
||||||
|
|
|
@ -119,15 +119,15 @@ export function isReportDataEmpty( report ) {
|
||||||
* @returns {Object} data request query parameters.
|
* @returns {Object} data request query parameters.
|
||||||
*/
|
*/
|
||||||
function getRequestQuery( endpoint, dataType, query ) {
|
function getRequestQuery( endpoint, dataType, query ) {
|
||||||
const datesFromQuery = getCurrentDates( query );
|
const datesFromQuery = getCurrentDates( query, 'YYYY-MM-DDTHH:00:00' );
|
||||||
const interval = getIntervalForQuery( query );
|
const interval = getIntervalForQuery( query );
|
||||||
const filterQuery = getFilterQuery( endpoint, query );
|
const filterQuery = getFilterQuery( endpoint, query );
|
||||||
return {
|
return {
|
||||||
order: 'asc',
|
order: 'asc',
|
||||||
interval,
|
interval,
|
||||||
per_page: MAX_PER_PAGE,
|
per_page: MAX_PER_PAGE,
|
||||||
after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
|
after: datesFromQuery[ dataType ].after,
|
||||||
before: appendTimestamp( datesFromQuery[ dataType ].before, 'end' ),
|
before: datesFromQuery[ dataType ].before,
|
||||||
...filterQuery,
|
...filterQuery,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,3 +23,7 @@ $light-gray-500: $core-grey-light-500;
|
||||||
$dark-gray-300: $core-grey-dark-300;
|
$dark-gray-300: $core-grey-dark-300;
|
||||||
$dark-gray-900: $core-grey-dark-900;
|
$dark-gray-900: $core-grey-dark-900;
|
||||||
$alert-red: $error-red;
|
$alert-red: $error-red;
|
||||||
|
|
||||||
|
// WordPress defaults
|
||||||
|
$adminbar-height: 32px;
|
||||||
|
$adminbar-height-mobile: 46px;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#wpbody-content {
|
#wpbody-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
|
min-height: calc(100vh - #{$adminbar-height});
|
||||||
}
|
}
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
|
@ -16,6 +17,10 @@
|
||||||
.wp-responsive-open #wpbody {
|
.wp-responsive-open #wpbody {
|
||||||
right: -14.5em;
|
right: -14.5em;
|
||||||
}
|
}
|
||||||
|
#wpcontent,
|
||||||
|
#wpbody-content {
|
||||||
|
min-height: calc(100vh - #{$adminbar-height-mobile});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include breakpoint( '>960px' ) {
|
@include breakpoint( '>960px' ) {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
/** @format */
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import operations from './operations';
|
||||||
|
import selectors from './selectors';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
operations,
|
||||||
|
selectors,
|
||||||
|
};
|
|
@ -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, 'category-query' ) );
|
||||||
|
|
||||||
|
return filteredNames.map( async resourceName => {
|
||||||
|
const query = getResourceIdentifier( resourceName );
|
||||||
|
const url = `/wc/v3/products/categories${ stringifyQuery( query ) }`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const categories = await fetch( {
|
||||||
|
path: url,
|
||||||
|
} );
|
||||||
|
|
||||||
|
const ids = categories.map( category => category.id );
|
||||||
|
const categoryResources = categories.reduce( ( resources, category ) => {
|
||||||
|
resources[ getResourceName( 'category', category.id ) ] = { data: category };
|
||||||
|
return resources;
|
||||||
|
}, {} );
|
||||||
|
|
||||||
|
return {
|
||||||
|
[ resourceName ]: {
|
||||||
|
data: ids,
|
||||||
|
totalCount: ids.length,
|
||||||
|
},
|
||||||
|
...categoryResources,
|
||||||
|
};
|
||||||
|
} catch ( error ) {
|
||||||
|
return { [ resourceName ]: { error } };
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
read,
|
||||||
|
};
|
|
@ -0,0 +1,56 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { getResourceName } from '../utils';
|
||||||
|
import { DEFAULT_REQUIREMENT } from '../constants';
|
||||||
|
|
||||||
|
const getCategories = ( getResource, requireResource ) => (
|
||||||
|
query = {},
|
||||||
|
requirement = DEFAULT_REQUIREMENT
|
||||||
|
) => {
|
||||||
|
const resourceName = getResourceName( 'category-query', query );
|
||||||
|
const ids = requireResource( requirement, resourceName ).data || [];
|
||||||
|
const categories = ids.reduce(
|
||||||
|
( acc, id ) => ( {
|
||||||
|
...acc,
|
||||||
|
[ id ]: getResource( getResourceName( 'category', id ) ).data || {},
|
||||||
|
} ),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoriesTotalCount = getResource => ( query = {} ) => {
|
||||||
|
const resourceName = getResourceName( 'category-query', query );
|
||||||
|
return getResource( resourceName ).totalCount || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoriesError = getResource => ( query = {} ) => {
|
||||||
|
const resourceName = getResourceName( 'category-query', query );
|
||||||
|
return getResource( resourceName ).error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGetCategoriesRequesting = getResource => ( query = {} ) => {
|
||||||
|
const resourceName = getResourceName( 'category-query', query );
|
||||||
|
const { lastRequested, lastReceived } = getResource( resourceName );
|
||||||
|
|
||||||
|
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastRequested > lastReceived;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getCategories,
|
||||||
|
getCategoriesError,
|
||||||
|
getCategoriesTotalCount,
|
||||||
|
isGetCategoriesRequesting,
|
||||||
|
};
|
|
@ -17,7 +17,7 @@ import { NAMESPACE } from '../../constants';
|
||||||
import { SWAGGERNAMESPACE } from 'store/constants';
|
import { SWAGGERNAMESPACE } from 'store/constants';
|
||||||
|
|
||||||
// TODO: Remove once swagger endpoints are phased out.
|
// TODO: Remove once swagger endpoints are phased out.
|
||||||
const swaggerEndpoints = [ 'coupons', 'customers', 'downloads' ];
|
const swaggerEndpoints = [ 'customers', 'downloads' ];
|
||||||
|
|
||||||
const typeEndpointMap = {
|
const typeEndpointMap = {
|
||||||
'report-items-query-orders': 'orders',
|
'report-items-query-orders': 'orders',
|
||||||
|
|
|
@ -16,9 +16,9 @@ import { getResourceIdentifier, getResourcePrefix } from '../../utils';
|
||||||
import { NAMESPACE } from '../../constants';
|
import { NAMESPACE } from '../../constants';
|
||||||
import { SWAGGERNAMESPACE } from 'store/constants';
|
import { SWAGGERNAMESPACE } from 'store/constants';
|
||||||
|
|
||||||
const statEndpoints = [ 'orders', 'revenue', 'products', 'taxes' ];
|
const statEndpoints = [ 'orders', 'revenue', 'products', 'taxes', 'coupons' ];
|
||||||
// TODO: Remove once swagger endpoints are phased out.
|
// TODO: Remove once swagger endpoints are phased out.
|
||||||
const swaggerEndpoints = [ 'categories', 'coupons', 'downloads' ];
|
const swaggerEndpoints = [ 'categories', 'downloads' ];
|
||||||
|
|
||||||
const typeEndpointMap = {
|
const typeEndpointMap = {
|
||||||
'report-stats-query-orders': 'orders',
|
'report-stats-query-orders': 'orders',
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import categories from './categories';
|
||||||
import customers from './customers';
|
import customers from './customers';
|
||||||
import notes from './notes';
|
import notes from './notes';
|
||||||
import orders from './orders';
|
import orders from './orders';
|
||||||
|
@ -17,6 +18,7 @@ function createWcApiSpec() {
|
||||||
...user.mutations,
|
...user.mutations,
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
|
...categories.selectors,
|
||||||
...customers.selectors,
|
...customers.selectors,
|
||||||
...notes.selectors,
|
...notes.selectors,
|
||||||
...orders.selectors,
|
...orders.selectors,
|
||||||
|
@ -28,6 +30,7 @@ function createWcApiSpec() {
|
||||||
operations: {
|
operations: {
|
||||||
read( resourceNames ) {
|
read( resourceNames ) {
|
||||||
return [
|
return [
|
||||||
|
...categories.operations.read( resourceNames ),
|
||||||
...customers.operations.read( resourceNames ),
|
...customers.operations.read( resourceNames ),
|
||||||
...notes.operations.read( resourceNames ),
|
...notes.operations.read( resourceNames ),
|
||||||
...orders.operations.read( resourceNames ),
|
...orders.operations.read( resourceNames ),
|
||||||
|
|
|
@ -1,38 +1,4 @@
|
||||||
Components
|
Components
|
||||||
==========
|
==========
|
||||||
|
|
||||||
This folder contains the WooCommerce-created components. These are exported onto a global, `wc.components`, for general use.
|
This folder contains components used in `wc-admin`. They are divided in [analytics components](components/analytics), only available internally, and [package components](components/packages), which are available for plugins and third-party developers.
|
||||||
|
|
||||||
## How to use:
|
|
||||||
|
|
||||||
For any files not imported into `components/index.js` (`analytics/*`, `layout/*`, `dashboard/*`, etc), we can use `import { Card, etc … } from @woocommerce/components`.
|
|
||||||
|
|
||||||
For any `component/*` files, we should import from component-specific paths, not from `component/index.js`, to prevent circular dependencies. See `components/card/index.js` for an example.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Card, Link } from '@woocommerce/components';
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<Card title="Card demo">
|
|
||||||
Card content with an <Link href="/">example link.</Link>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## For external development
|
|
||||||
|
|
||||||
External developers will need to enqueue the components library, `wc-components`, and then can access them from the global.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const { Card, Link } = wc.components;
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<Card title="Card demo">
|
|
||||||
Card content with an <Link href="/">example link.</Link>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
@ -2,30 +2,36 @@
|
||||||
|
|
||||||
* [Components](components/)
|
* [Components](components/)
|
||||||
|
|
||||||
* [AnimationSlider](components/animation-slider.md)
|
* [Analytics components](components/analytics/)
|
||||||
* [Calendar](components/calendar.md)
|
* [ReportChart](components/analytics/report-chart.md)
|
||||||
* [Card](components/card.md)
|
* [ReportError](components/analytics/report-error.md)
|
||||||
* [Chart](components/chart.md)
|
* [ReportSummary](components/analytics/report-summary.md)
|
||||||
* [Count](components/count.md)
|
* [ReportTable](components/analytics/report-table.md)
|
||||||
* [DropdownButton](components/dropdown-button.md)
|
* [Package components](components/packages/)
|
||||||
* [EllipsisMenu](components/ellipsis-menu.md)
|
* [AnimationSlider](components/packages/animation-slider.md)
|
||||||
* [EmptyContent](components/empty-content.md)
|
* [Calendar](components/packages/calendar.md)
|
||||||
* [Filters](components/filters.md)
|
* [Card](components/packages/card.md)
|
||||||
* [Flag](components/flag.md)
|
* [Chart](components/packages/chart.md)
|
||||||
* [Gravatar](components/gravatar.md)
|
* [Count](components/packages/count.md)
|
||||||
* [ImageAsset](components/image-asset.md)
|
* [DropdownButton](components/packages/dropdown-button.md)
|
||||||
* [Link](components/link.md)
|
* [EllipsisMenu](components/packages/ellipsis-menu.md)
|
||||||
* [OrderStatus](components/order-status.md)
|
* [EmptyContent](components/packages/empty-content.md)
|
||||||
* [Pagination](components/pagination.md)
|
* [Filters](components/packages/filters.md)
|
||||||
* [ProductImage](components/product-image.md)
|
* [Flag](components/packages/flag.md)
|
||||||
* [Rating](components/rating.md)
|
* [Gravatar](components/packages/gravatar.md)
|
||||||
* [Search](components/search.md)
|
* [ImageAsset](components/packages/image-asset.md)
|
||||||
* [SectionHeader](components/section-header.md)
|
* [Link](components/packages/link.md)
|
||||||
* [Section](components/section.md)
|
* [OrderStatus](components/packages/order-status.md)
|
||||||
* [SegmentedSelection](components/segmented-selection.md)
|
* [Pagination](components/packages/pagination.md)
|
||||||
* [SplitButton](components/split-button.md)
|
* [ProductImage](components/packages/product-image.md)
|
||||||
* [Summary](components/summary.md)
|
* [Rating](components/packages/rating.md)
|
||||||
* [Table](components/table.md)
|
* [Search](components/packages/search.md)
|
||||||
* [Tag](components/tag.md)
|
* [SectionHeader](components/packages/section-header.md)
|
||||||
* [TextControlWithAffixes](components/text-control-with-affixes.md)
|
* [Section](components/packages/section.md)
|
||||||
* [ViewMoreList](components/view-more-list.md)
|
* [SegmentedSelection](components/packages/segmented-selection.md)
|
||||||
|
* [SplitButton](components/packages/split-button.md)
|
||||||
|
* [Summary](components/packages/summary.md)
|
||||||
|
* [Table](components/packages/table.md)
|
||||||
|
* [Tag](components/packages/tag.md)
|
||||||
|
* [TextControlWithAffixes](components/packages/text-control-with-affixes.md)
|
||||||
|
* [ViewMoreList](components/packages/view-more-list.md)
|
|
@ -0,0 +1,24 @@
|
||||||
|
Analytics Components
|
||||||
|
====================
|
||||||
|
|
||||||
|
This folder contains components internally used by `wc-admin`.
|
||||||
|
|
||||||
|
## How to use:
|
||||||
|
|
||||||
|
Analytics components can be imported by their relative or absolute path.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import ReportChart from 'analytics/components/report-chart';
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<ReportChart
|
||||||
|
charts={ charts }
|
||||||
|
endpoint={ endpoint }
|
||||||
|
path={ path }
|
||||||
|
query={ query }
|
||||||
|
selectedChart={ selectedChart }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,62 @@
|
||||||
|
`ReportChart` (component)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Component that renders the chart in reports.
|
||||||
|
|
||||||
|
Props
|
||||||
|
-----
|
||||||
|
|
||||||
|
### `filters`
|
||||||
|
|
||||||
|
- Type: Array
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Filters available for that report.
|
||||||
|
|
||||||
|
### `itemsLabel`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Label describing the legend items.
|
||||||
|
|
||||||
|
### `path`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Current path
|
||||||
|
|
||||||
|
### `primaryData`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Object
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Primary data to display in the chart.
|
||||||
|
|
||||||
|
### `query`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Object
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The query string represented in object form.
|
||||||
|
|
||||||
|
### `secondaryData`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Object
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Secondary data to display in the chart.
|
||||||
|
|
||||||
|
### `selectedChart`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Object
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Properties of the selected chart.
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
`ReportError` (component)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Component to render when there is an error in a report component due to data
|
||||||
|
not being loaded or being invalid.
|
||||||
|
|
||||||
|
Props
|
||||||
|
-----
|
||||||
|
|
||||||
|
### `className`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: `''`
|
||||||
|
|
||||||
|
Additional class name to style the component.
|
||||||
|
|
||||||
|
### `isError`
|
||||||
|
|
||||||
|
- Type: Boolean
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Boolean representing whether there was an error.
|
||||||
|
|
||||||
|
### `isEmpty`
|
||||||
|
|
||||||
|
- Type: Boolean
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Boolean representing whether the issue is that there is no data.
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
`ReportSummary` (component)
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Component to render summary numbers in reports.
|
||||||
|
|
||||||
|
Props
|
||||||
|
-----
|
||||||
|
|
||||||
|
### `charts`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Array
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Properties of all the charts available for that report.
|
||||||
|
|
||||||
|
### `endpoint`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The endpoint to use in API calls.
|
||||||
|
|
||||||
|
### `query`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Object
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The query string represented in object form.
|
||||||
|
|
||||||
|
### `selectedChart`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Object
|
||||||
|
- key: String - Key of the selected chart.
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Properties of the selected chart.
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
`ReportTable` (component)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Component that extends `TableCard` to facilitate its usage in reports.
|
||||||
|
|
||||||
|
Props
|
||||||
|
-----
|
||||||
|
|
||||||
|
### `columnPrefsKey`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The key for user preferences settings for column visibility.
|
||||||
|
|
||||||
|
### `endpoint`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The endpoint to use in API calls.
|
||||||
|
|
||||||
|
### `extendItemsMethodNames`
|
||||||
|
|
||||||
|
- Type: Object
|
||||||
|
- getError: String
|
||||||
|
- isRequesting: String
|
||||||
|
- load: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Name of the methods available via `select( 'wc-api' )` that will be used to
|
||||||
|
load more data for table items. If omitted, no call will be made and only
|
||||||
|
the data returned by the reports endpoint will be used.
|
||||||
|
|
||||||
|
### `getHeadersContent`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Function
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
A function that returns the headers object to build the table.
|
||||||
|
|
||||||
|
### `getRowsContent`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Function
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
A function that returns the rows array to build the table.
|
||||||
|
|
||||||
|
### `getSummary`
|
||||||
|
|
||||||
|
- Type: Function
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
A function that returns the summary object to build the table.
|
||||||
|
|
||||||
|
### `itemIdField`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The name of the property in the item object which contains the id.
|
||||||
|
|
||||||
|
### `primaryData`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Object
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Primary data of that report. If it's not provided, it will be automatically
|
||||||
|
loaded via the provided `endpoint`.
|
||||||
|
|
||||||
|
### `tableData`
|
||||||
|
|
||||||
|
- Type: Object
|
||||||
|
- Default: `{}`
|
||||||
|
|
||||||
|
Table data of that report. If it's not provided, it will be automatically
|
||||||
|
loaded via the provided `endpoint`.
|
||||||
|
|
||||||
|
### `tableQuery`
|
||||||
|
|
||||||
|
- Type: Object
|
||||||
|
- Default: `{}`
|
||||||
|
|
||||||
|
Properties to be added to the query sent to the report table endpoint.
|
||||||
|
|
||||||
|
### `title`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
String to display as the title of the table.
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
Package Components
|
||||||
|
==================
|
||||||
|
|
||||||
|
This folder contains the WooCommerce-created components. These are exported onto a global, `wc.components`, for general use.
|
||||||
|
|
||||||
|
## How to use:
|
||||||
|
|
||||||
|
For any files not imported into `components/index.js` (`analytics/*`, `layout/*`, `dashboard/*`, etc), we can use `import { Card, etc … } from @woocommerce/components`.
|
||||||
|
|
||||||
|
For any `component/*` files, we should import from component-specific paths, not from `component/index.js`, to prevent circular dependencies. See `components/card/index.js` for an example.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Card, Link } from '@woocommerce/components';
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<Card title="Card demo">
|
||||||
|
Card content with an <Link href="/">example link.</Link>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## For external development
|
||||||
|
|
||||||
|
External developers will need to enqueue the components library, `wc-components`, and then can access them from the global.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const { Card, Link } = wc.components;
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<Card title="Card demo">
|
||||||
|
Card content with an <Link href="/">example link.</Link>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
|
@ -1,3 +1,56 @@
|
||||||
|
`DatePicker` (component)
|
||||||
|
========================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Props
|
||||||
|
-----
|
||||||
|
|
||||||
|
### `date`
|
||||||
|
|
||||||
|
- Type: Object
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
A moment date object representing the selected date. `null` for no selection.
|
||||||
|
|
||||||
|
### `text`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The date in human-readable format. Displayed in the text input.
|
||||||
|
|
||||||
|
### `error`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
A string error message, shown to the user.
|
||||||
|
|
||||||
|
### `invalidDays`
|
||||||
|
|
||||||
|
- Type: One of type: enum, func
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
(Coming Soon) Optionally invalidate certain days. `past`, `future`, `none`, or function are accepted.
|
||||||
|
A function will be passed to react-dates' `isOutsideRange` prop
|
||||||
|
|
||||||
|
### `onUpdate`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: Function
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
A function called upon selection of a date or input change.
|
||||||
|
|
||||||
|
### `dateFormat`
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
The date format in moment.js-style tokens.
|
||||||
|
|
||||||
`DateRange` (component)
|
`DateRange` (component)
|
||||||
=======================
|
=======================
|
||||||
|
|
|
@ -20,6 +20,13 @@ An array of data.
|
||||||
|
|
||||||
Format to parse dates into d3 time format
|
Format to parse dates into d3 time format
|
||||||
|
|
||||||
|
### `itemsLabel`
|
||||||
|
|
||||||
|
- Type: String
|
||||||
|
- Default: null
|
||||||
|
|
||||||
|
Label describing the legend items.
|
||||||
|
|
||||||
### `path`
|
### `path`
|
||||||
|
|
||||||
- Type: String
|
- Type: String
|
|
@ -144,8 +144,8 @@ The query string represented in object form
|
||||||
|
|
||||||
Which type of autocompleter should be used in the Search
|
Which type of autocompleter should be used in the Search
|
||||||
|
|
||||||
`DatePicker` (component)
|
`DateRangeFilterPicker` (component)
|
||||||
========================
|
===================================
|
||||||
|
|
||||||
Select a range of dates or single dates.
|
Select a range of dates or single dates.
|
||||||
|
|
|
@ -24,7 +24,7 @@ Function called when selected results change, passed result list.
|
||||||
### `type`
|
### `type`
|
||||||
|
|
||||||
- **Required**
|
- **Required**
|
||||||
- Type: One of: 'products', 'product_cats', 'orders', 'customers', 'coupons', 'taxes', 'variations'
|
- Type: One of: 'countries', 'coupons', 'customers', 'emails', 'orders', 'products', 'product_cats', 'taxes', 'usernames', 'variations'
|
||||||
- Default: null
|
- Default: null
|
||||||
|
|
||||||
The object type to be used in searching.
|
The object type to be used in searching.
|
||||||
|
@ -39,7 +39,7 @@ A placeholder for the search input.
|
||||||
### `selected`
|
### `selected`
|
||||||
|
|
||||||
- Type: Array
|
- Type: Array
|
||||||
- id: Number
|
- id: One of type: number, string
|
||||||
- label: String
|
- label: String
|
||||||
- Default: `[]`
|
- Default: `[]`
|
||||||
|
|
|
@ -11,7 +11,7 @@ Props
|
||||||
|
|
||||||
### `id`
|
### `id`
|
||||||
|
|
||||||
- Type: Number
|
- Type: One of type: number, string
|
||||||
- Default: null
|
- Default: null
|
||||||
|
|
||||||
The ID for this item, used in the remove function.
|
The ID for this item, used in the remove function.
|
|
@ -76,6 +76,15 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller {
|
||||||
*/
|
*/
|
||||||
public function get_item( $request ) {
|
public function get_item( $request ) {
|
||||||
$note = WC_Admin_Notes::get_note( $request->get_param( 'id' ) );
|
$note = WC_Admin_Notes::get_note( $request->get_param( 'id' ) );
|
||||||
|
|
||||||
|
if ( ! $note ) {
|
||||||
|
return new WP_Error(
|
||||||
|
'woocommerce_admin_notes_invalid_id',
|
||||||
|
__( 'Sorry, there is no resouce with that ID.', 'wc-admin' ),
|
||||||
|
array( 'status' => 404 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( is_wp_error( $note ) ) {
|
if ( is_wp_error( $note ) ) {
|
||||||
return $note;
|
return $note;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Data Controller
|
||||||
|
*
|
||||||
|
* Handles requests to /data
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/API
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data controller.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/API
|
||||||
|
* @extends WC_REST_Data_Controller
|
||||||
|
*/
|
||||||
|
class WC_Admin_REST_Data_Controller extends WC_REST_Data_Controller {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of data resources.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request data.
|
||||||
|
* @return WP_Error|WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
$response = parent::get_items( $request );
|
||||||
|
$response->data[] = $this->prepare_response_for_collection(
|
||||||
|
$this->prepare_item_for_response(
|
||||||
|
(object) array(
|
||||||
|
'slug' => 'download-ips',
|
||||||
|
'description' => __( 'An endpoint used for searching download logs for a specific IP address.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
$request
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Data Download IP Controller
|
||||||
|
*
|
||||||
|
* Handles requests to /data/download-ips
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/API
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Download IP controller.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/API
|
||||||
|
* @extends WC_REST_Data_Controller
|
||||||
|
*/
|
||||||
|
class WC_Admin_REST_Data_Download_Ips_Controller extends WC_REST_Data_Controller {
|
||||||
|
/**
|
||||||
|
* Endpoint namespace.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $namespace = 'wc/v3';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'data/download-ips';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @since 3.5.0
|
||||||
|
*/
|
||||||
|
public function register_routes() {
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_items' ),
|
||||||
|
'permission_callback' => array( $this, 'get_items_permissions_check' ),
|
||||||
|
),
|
||||||
|
'schema' => array( $this, 'get_public_item_schema' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the download IPs matching the passed parameters.
|
||||||
|
*
|
||||||
|
* @since 3.5.0
|
||||||
|
* @param WP_REST_Request $request Request data.
|
||||||
|
* @return WP_Error|WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ( isset( $request['match'] ) ) {
|
||||||
|
$downloads = $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT DISTINCT( user_ip_address ) FROM {$wpdb->prefix}wc_download_log
|
||||||
|
WHERE user_ip_address LIKE %s
|
||||||
|
LIMIT 10",
|
||||||
|
$request['match'] . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new WP_Error( 'woocommerce_rest_data_download_ips_invalid_request', __( 'Invalid request. Please pass the match parameter.', 'wc-admin' ), array( 'status' => 400 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
if ( ! empty( $downloads ) ) {
|
||||||
|
foreach ( $downloads as $download ) {
|
||||||
|
$response = $this->prepare_item_for_response( $download, $request );
|
||||||
|
$data[] = $this->prepare_response_for_collection( $response );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest_ensure_response( $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the data object for response.
|
||||||
|
*
|
||||||
|
* @since 3.5.0
|
||||||
|
* @param object $item Data object.
|
||||||
|
* @param WP_REST_Request $request Request object.
|
||||||
|
* @return WP_REST_Response $response Response data.
|
||||||
|
*/
|
||||||
|
public function prepare_item_for_response( $item, $request ) {
|
||||||
|
$data = $this->add_additional_fields_to_object( $item, $request );
|
||||||
|
$data = $this->filter_response_by_context( $data, 'view' );
|
||||||
|
$response = rest_ensure_response( $data );
|
||||||
|
|
||||||
|
$response->add_links( $this->prepare_links( $item ) );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the ist returned from the API.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Response $response The response object.
|
||||||
|
* @param array $item The original item.
|
||||||
|
* @param WP_REST_Request $request Request used to generate the response.
|
||||||
|
*/
|
||||||
|
return apply_filters( 'woocommerce_rest_prepare_data_download_ip', $response, $item, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare links for the request.
|
||||||
|
*
|
||||||
|
* @param object $item Data object.
|
||||||
|
* @return array Links for the given object.
|
||||||
|
*/
|
||||||
|
protected function prepare_links( $item ) {
|
||||||
|
$links = array(
|
||||||
|
'collection' => array(
|
||||||
|
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return $links;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query params for collections.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_collection_params() {
|
||||||
|
$params = array();
|
||||||
|
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||||
|
$params['match'] = array(
|
||||||
|
'description' => __( 'A partial IP address can be passed and matching results will be returned.', 'wc-admin' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the schema, conforming to JSON Schema.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_item_schema() {
|
||||||
|
$schema = array(
|
||||||
|
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||||
|
'title' => 'data_download_ips',
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'user_ip_address' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'IP address.', 'wc-admin' ),
|
||||||
|
'context' => array( 'view' ),
|
||||||
|
'readonly' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_additional_fields_schema( $schema );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Orders Controller
|
||||||
|
*
|
||||||
|
* Handles requests to /orders/*
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/API
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orders controller.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/API
|
||||||
|
* @extends WC_REST_Orders_Controller
|
||||||
|
*/
|
||||||
|
class WC_Admin_REST_Orders_Controller extends WC_REST_Orders_Controller {
|
||||||
|
/**
|
||||||
|
* Get the query params for collections.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_collection_params() {
|
||||||
|
$params = parent::get_collection_params();
|
||||||
|
$params['number'] = array(
|
||||||
|
'description' => __( 'Limit result set to orders matching part of an order number.', 'wc-admin' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare objects query.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Full details about the request.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function prepare_objects_query( $request ) {
|
||||||
|
global $wpdb;
|
||||||
|
$args = parent::prepare_objects_query( $request );
|
||||||
|
|
||||||
|
// Search by partial order number.
|
||||||
|
if ( ! empty( $request['number'] ) ) {
|
||||||
|
$order_ids = $wpdb->get_col(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT ID FROM {$wpdb->prefix}posts WHERE post_type = 'shop_order' AND ID LIKE %s",
|
||||||
|
intval( $request['number'] ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force WP_Query return empty if don't found any order.
|
||||||
|
$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );
|
||||||
|
$args['post__in'] = $order_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
}
|
|
@ -252,9 +252,9 @@ class WC_Admin_REST_Reports_Categories_Controller extends WC_REST_Reports_Contro
|
||||||
$params['orderby'] = array(
|
$params['orderby'] = array(
|
||||||
'description' => __( 'Sort collection by object attribute.', 'wc-admin' ),
|
'description' => __( 'Sort collection by object attribute.', 'wc-admin' ),
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'default' => 'date',
|
'default' => 'category_id',
|
||||||
'enum' => array(
|
'enum' => array(
|
||||||
'date',
|
'category_id',
|
||||||
'items_sold',
|
'items_sold',
|
||||||
'net_revenue',
|
'net_revenue',
|
||||||
'orders_count',
|
'orders_count',
|
||||||
|
|
|
@ -51,10 +51,13 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
$args['last_active_after'] = $request['last_active_after'];
|
$args['last_active_after'] = $request['last_active_after'];
|
||||||
$args['order_count_min'] = $request['order_count_min'];
|
$args['order_count_min'] = $request['order_count_min'];
|
||||||
$args['order_count_max'] = $request['order_count_max'];
|
$args['order_count_max'] = $request['order_count_max'];
|
||||||
|
$args['order_count_between'] = $request['order_count_between'];
|
||||||
$args['total_spend_min'] = $request['total_spend_min'];
|
$args['total_spend_min'] = $request['total_spend_min'];
|
||||||
$args['total_spend_max'] = $request['total_spend_max'];
|
$args['total_spend_max'] = $request['total_spend_max'];
|
||||||
|
$args['total_spend_between'] = $request['total_spend_between'];
|
||||||
$args['avg_order_value_min'] = $request['avg_order_value_min'];
|
$args['avg_order_value_min'] = $request['avg_order_value_min'];
|
||||||
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
||||||
|
$args['avg_order_value_between'] = $request['avg_order_value_between'];
|
||||||
return $args;
|
return $args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +282,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
'sanitize_callback' => 'absint',
|
'sanitize_callback' => 'absint',
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
|
$params['order_count_between'] = array(
|
||||||
|
'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
$params['total_spend_min'] = array(
|
$params['total_spend_min'] = array(
|
||||||
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ),
|
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ),
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
|
@ -289,6 +297,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
|
$params['total_spend_between'] = array(
|
||||||
|
'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
$params['avg_order_value_min'] = array(
|
$params['avg_order_value_min'] = array(
|
||||||
'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'wc-admin' ),
|
'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'wc-admin' ),
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
|
@ -299,6 +312,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
);
|
);
|
||||||
|
$params['avg_order_value_between'] = array(
|
||||||
|
'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,4 +30,334 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $rest_base = 'reports/downloads';
|
protected $rest_base = 'reports/downloads';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get items.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request data.
|
||||||
|
*
|
||||||
|
* @return array|WP_Error
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
$args = array();
|
||||||
|
$registered = array_keys( $this->get_collection_params() );
|
||||||
|
foreach ( $registered as $param_name ) {
|
||||||
|
if ( isset( $request[ $param_name ] ) ) {
|
||||||
|
$args[ $param_name ] = $request[ $param_name ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reports = new WC_Admin_Reports_Downloads_Query( $args );
|
||||||
|
$downloads_data = $reports->get_data();
|
||||||
|
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
foreach ( $downloads_data->data as $download_data ) {
|
||||||
|
$item = $this->prepare_item_for_response( $download_data, $request );
|
||||||
|
$data[] = $this->prepare_response_for_collection( $item );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = rest_ensure_response( $data );
|
||||||
|
|
||||||
|
$response->header( 'X-WP-Total', (int) $downloads_data->total );
|
||||||
|
$response->header( 'X-WP-TotalPages', (int) $downloads_data->pages );
|
||||||
|
|
||||||
|
$page = $downloads_data->page_no;
|
||||||
|
$max_pages = $downloads_data->pages;
|
||||||
|
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
|
||||||
|
if ( $page > 1 ) {
|
||||||
|
$prev_page = $page - 1;
|
||||||
|
if ( $prev_page > $max_pages ) {
|
||||||
|
$prev_page = $max_pages;
|
||||||
|
}
|
||||||
|
$prev_link = add_query_arg( 'page', $prev_page, $base );
|
||||||
|
$response->link_header( 'prev', $prev_link );
|
||||||
|
}
|
||||||
|
if ( $max_pages > $page ) {
|
||||||
|
$next_page = $page + 1;
|
||||||
|
$next_link = add_query_arg( 'page', $next_page, $base );
|
||||||
|
$response->link_header( 'next', $next_link );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare a report object for serialization.
|
||||||
|
*
|
||||||
|
* @param Array $report Report data.
|
||||||
|
* @param WP_REST_Request $request Request object.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function prepare_item_for_response( $report, $request ) {
|
||||||
|
$data = $report;
|
||||||
|
|
||||||
|
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||||
|
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||||
|
$data = $this->filter_response_by_context( $data, $context );
|
||||||
|
|
||||||
|
// Wrap the data in a response object.
|
||||||
|
$response = rest_ensure_response( $data );
|
||||||
|
$response->add_links( $this->prepare_links( $report ) );
|
||||||
|
|
||||||
|
$response->data['date'] = get_date_from_gmt( $data['date_gmt'], 'Y-m-d H:i:s' );
|
||||||
|
|
||||||
|
// Figure out file name.
|
||||||
|
// Matches https://github.com/woocommerce/woocommerce/blob/4be0018c092e617c5d2b8c46b800eb71ece9ddef/includes/class-wc-download-handler.php#L197.
|
||||||
|
$product_id = intval( $data['product_id'] );
|
||||||
|
$_product = wc_get_product( $product_id );
|
||||||
|
$file_path = $_product->get_file_download_path( $data['download_id'] );
|
||||||
|
$filename = basename( $file_path );
|
||||||
|
$response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a report returned from the API.
|
||||||
|
*
|
||||||
|
* Allows modification of the report data right before it is returned.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Response $response The response object.
|
||||||
|
* @param object $report The original report object.
|
||||||
|
* @param WP_REST_Request $request Request used to generate the response.
|
||||||
|
*/
|
||||||
|
return apply_filters( 'woocommerce_rest_prepare_report_downloads', $response, $report, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare links for the request.
|
||||||
|
*
|
||||||
|
* @param Array $object Object data.
|
||||||
|
* @return array Links for the given post.
|
||||||
|
*/
|
||||||
|
protected function prepare_links( $object ) {
|
||||||
|
$links = array(
|
||||||
|
'product' => array(
|
||||||
|
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
|
||||||
|
'embeddable' => true,
|
||||||
|
),
|
||||||
|
'user' => array(
|
||||||
|
'href' => rest_url( 'wp/v2/users/' . $object['user_id'] ),
|
||||||
|
'embeddable' => true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $links;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Report's schema, conforming to JSON Schema.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_item_schema() {
|
||||||
|
$schema = array(
|
||||||
|
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||||
|
'title' => 'report_downloads',
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'id' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'ID.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
'product_id' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'Product ID.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
'date' => array(
|
||||||
|
'description' => __( "The date of the download, in the site's timezone.", 'wc-admin' ),
|
||||||
|
'type' => 'date-time',
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'readonly' => true,
|
||||||
|
),
|
||||||
|
'date_gmt' => array(
|
||||||
|
'description' => __( 'The date of the download, as GMT.', 'wc-admin' ),
|
||||||
|
'type' => 'date-time',
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'readonly' => true,
|
||||||
|
),
|
||||||
|
'download_id' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'Download ID.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
'file_name' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'File name.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
'product_id' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'Product ID.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
'order_id' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'Order ID.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
'user_id' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'User ID for the downloader.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
'ip_address' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'IP address for the downloader.', 'wc-admin' ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_additional_fields_schema( $schema );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query params for collections.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_collection_params() {
|
||||||
|
$params = array();
|
||||||
|
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||||
|
$params['page'] = array(
|
||||||
|
'description' => __( 'Current page of the collection.', 'wc-admin' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'minimum' => 1,
|
||||||
|
);
|
||||||
|
$params['per_page'] = array(
|
||||||
|
'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 10,
|
||||||
|
'minimum' => 1,
|
||||||
|
'maximum' => 100,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
$params['after'] = array(
|
||||||
|
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'wc-admin' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'date-time',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
$params['before'] = array(
|
||||||
|
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'wc-admin' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'date-time',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
$params['order'] = array(
|
||||||
|
'description' => __( 'Order sort attribute ascending or descending.', 'wc-admin' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'desc',
|
||||||
|
'enum' => array( 'asc', 'desc' ),
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
$params['orderby'] = array(
|
||||||
|
'description' => __( 'Sort collection by object attribute.', 'wc-admin' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'date',
|
||||||
|
'enum' => array(
|
||||||
|
'date',
|
||||||
|
),
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
$params['match'] = array(
|
||||||
|
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: products, orders, username, ip_address.', 'wc-admin' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'all',
|
||||||
|
'enum' => array(
|
||||||
|
'all',
|
||||||
|
'any',
|
||||||
|
),
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
);
|
||||||
|
$params['product_includes'] = array(
|
||||||
|
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
'default' => array(),
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
|
||||||
|
);
|
||||||
|
$params['product_excludes'] = array(
|
||||||
|
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
'default' => array(),
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
);
|
||||||
|
$params['order_includes'] = array(
|
||||||
|
'description' => __( 'Limit result set to items that have the specified order ids.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$params['order_excludes'] = array(
|
||||||
|
'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$params['user_includes'] = array(
|
||||||
|
'description' => __( 'Limit response to objects that have the specified user ids.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$params['user_excludes'] = array(
|
||||||
|
'description' => __( 'Limit response to objects that don\'t have the specified user ids.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'sanitize_callback' => 'wp_parse_id_list',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$params['ip_address_includes'] = array(
|
||||||
|
'description' => __( 'Limit response to objects that have a specified ip address.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['ip_address_excludes'] = array(
|
||||||
|
'description' => __( 'Limit response to objects that don\'t have a specified ip address.', 'wc-admin' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,6 +224,12 @@ class WC_Admin_REST_Reports_Products_Controller extends WC_REST_Reports_Controll
|
||||||
'context' => array( 'view', 'edit' ),
|
'context' => array( 'view', 'edit' ),
|
||||||
'description' => __( 'Product inventory threshold for low stock.', 'wc-admin' ),
|
'description' => __( 'Product inventory threshold for low stock.', 'wc-admin' ),
|
||||||
),
|
),
|
||||||
|
'sku' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'readonly' => true,
|
||||||
|
'context' => array( 'view', 'edit' ),
|
||||||
|
'description' => __( 'Product SKU.', 'wc-admin' ),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -53,6 +53,7 @@ class WC_Admin_Api_Init {
|
||||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-query.php';
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-query.php';
|
||||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-query.php';
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-query.php';
|
||||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-query.php';
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-query.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-query.php';
|
||||||
|
|
||||||
// Data stores.
|
// Data stores.
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
|
||||||
|
@ -65,9 +66,9 @@ class WC_Admin_Api_Init {
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-taxes-stats-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-taxes-stats-data-store.php';
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-data-store.php';
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-stats-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-stats-data-store.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-data-store.php';
|
||||||
|
|
||||||
// Data triggers.
|
// Data triggers.
|
||||||
require_once dirname( __FILE__ ) . '/wc-admin-order-functions.php';
|
|
||||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php';
|
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php';
|
||||||
|
|
||||||
// CRUD classes.
|
// CRUD classes.
|
||||||
|
@ -81,6 +82,9 @@ class WC_Admin_Api_Init {
|
||||||
public function rest_api_init() {
|
public function rest_api_init() {
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-admin-notes-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-admin-notes-controller.php';
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-controller.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-download-ips-controller.php';
|
||||||
|
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-products-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-reviews-controller.php';
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
|
||||||
|
@ -100,10 +104,14 @@ class WC_Admin_Api_Init {
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
|
||||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
|
||||||
|
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-controller.php';
|
||||||
|
|
||||||
$controllers = array(
|
$controllers = array(
|
||||||
'WC_Admin_REST_Admin_Notes_Controller',
|
'WC_Admin_REST_Admin_Notes_Controller',
|
||||||
'WC_Admin_REST_Customers_Controller',
|
'WC_Admin_REST_Customers_Controller',
|
||||||
|
'WC_Admin_REST_Data_Controller',
|
||||||
|
'WC_Admin_REST_Data_Download_Ips_Controller',
|
||||||
|
'WC_Admin_REST_Orders_Controller',
|
||||||
'WC_Admin_REST_Products_Controller',
|
'WC_Admin_REST_Products_Controller',
|
||||||
'WC_Admin_REST_Product_Reviews_Controller',
|
'WC_Admin_REST_Product_Reviews_Controller',
|
||||||
'WC_Admin_REST_Reports_Controller',
|
'WC_Admin_REST_Reports_Controller',
|
||||||
|
@ -119,6 +127,7 @@ class WC_Admin_Api_Init {
|
||||||
'WC_Admin_REST_Reports_Coupons_Controller',
|
'WC_Admin_REST_Reports_Coupons_Controller',
|
||||||
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
|
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
|
||||||
'WC_Admin_REST_Reports_Stock_Controller',
|
'WC_Admin_REST_Reports_Stock_Controller',
|
||||||
|
'WC_Admin_REST_Reports_Downloads_Controller',
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $controllers as $controller ) {
|
foreach ( $controllers as $controller ) {
|
||||||
|
@ -162,17 +171,6 @@ class WC_Admin_Api_Init {
|
||||||
$endpoints['/wc/v3/reports'][0] = $endpoints['/wc/v3/reports'][1];
|
$endpoints['/wc/v3/reports'][0] = $endpoints['/wc/v3/reports'][1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override /wc/v3/products.
|
|
||||||
if ( isset( $endpoints['/wc/v3/products'] )
|
|
||||||
&& isset( $endpoints['/wc/v3/products'][3] )
|
|
||||||
&& isset( $endpoints['/wc/v3/products'][2] )
|
|
||||||
&& $endpoints['/wc/v3/products'][2]['callback'][0] instanceof WC_Admin_REST_Products_Controller
|
|
||||||
&& $endpoints['/wc/v3/products'][3]['callback'][0] instanceof WC_Admin_REST_Products_Controller
|
|
||||||
) {
|
|
||||||
$endpoints['/wc/v3/products'][0] = $endpoints['/wc/v3/products'][2];
|
|
||||||
$endpoints['/wc/v3/products'][1] = $endpoints['/wc/v3/products'][3];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override /wc/v3/customers.
|
// Override /wc/v3/customers.
|
||||||
if ( isset( $endpoints['/wc/v3/customers'] )
|
if ( isset( $endpoints['/wc/v3/customers'] )
|
||||||
&& isset( $endpoints['/wc/v3/customers'][3] )
|
&& isset( $endpoints['/wc/v3/customers'][3] )
|
||||||
|
@ -184,6 +182,50 @@ class WC_Admin_Api_Init {
|
||||||
$endpoints['/wc/v3/customers'][1] = $endpoints['/wc/v3/customers'][3];
|
$endpoints['/wc/v3/customers'][1] = $endpoints['/wc/v3/customers'][3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override /wc/v3/orders/$id.
|
||||||
|
if ( isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'] )
|
||||||
|
&& isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][5] )
|
||||||
|
&& isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][4] )
|
||||||
|
&& isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][3] )
|
||||||
|
&& $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
|
||||||
|
&& $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][4]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
|
||||||
|
&& $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][5]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
|
||||||
|
) {
|
||||||
|
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][0] = $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][3];
|
||||||
|
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][1] = $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][4];
|
||||||
|
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][2] = $endpoints['/wc/v3/orders/(?P<id>[\d]+)'][5];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override /wc/v3orders.
|
||||||
|
if ( isset( $endpoints['/wc/v3/orders'] )
|
||||||
|
&& isset( $endpoints['/wc/v3/orders'][3] )
|
||||||
|
&& isset( $endpoints['/wc/v3/orders'][2] )
|
||||||
|
&& $endpoints['/wc/v3/orders'][2]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
|
||||||
|
&& $endpoints['/wc/v3/orders'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Controller
|
||||||
|
) {
|
||||||
|
$endpoints['/wc/v3/orders'][0] = $endpoints['/wc/v3/orders'][2];
|
||||||
|
$endpoints['/wc/v3/orders'][1] = $endpoints['/wc/v3/orders'][3];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override /wc/v3/data.
|
||||||
|
if ( isset( $endpoints['/wc/v3/data'] )
|
||||||
|
&& isset( $endpoints['/wc/v3/data'][1] )
|
||||||
|
&& $endpoints['/wc/v3/data'][1]['callback'][0] instanceof WC_Admin_REST_Data_Controller
|
||||||
|
) {
|
||||||
|
$endpoints['/wc/v3/data'][0] = $endpoints['/wc/v3/data'][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override /wc/v3/products.
|
||||||
|
if ( isset( $endpoints['/wc/v3/products'] )
|
||||||
|
&& isset( $endpoints['/wc/v3/products'][3] )
|
||||||
|
&& isset( $endpoints['/wc/v3/products'][2] )
|
||||||
|
&& $endpoints['/wc/v3/products'][2]['callback'][0] instanceof WC_Admin_REST_Products_Controller
|
||||||
|
&& $endpoints['/wc/v3/products'][3]['callback'][0] instanceof WC_Admin_REST_Products_Controller
|
||||||
|
) {
|
||||||
|
$endpoints['/wc/v3/products'][0] = $endpoints['/wc/v3/products'][2];
|
||||||
|
$endpoints['/wc/v3/products'][1] = $endpoints['/wc/v3/products'][3];
|
||||||
|
}
|
||||||
|
|
||||||
// Override /wc/v3/products/$id.
|
// Override /wc/v3/products/$id.
|
||||||
if ( isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'] )
|
if ( isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'] )
|
||||||
&& isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'][5] )
|
&& isset( $endpoints['/wc/v3/products/(?P<id>[\d]+)'][5] )
|
||||||
|
@ -215,7 +257,7 @@ class WC_Admin_Api_Init {
|
||||||
/**
|
/**
|
||||||
* Regenerate data for reports.
|
* Regenerate data for reports.
|
||||||
*/
|
*/
|
||||||
public static function regenrate_report_data() {
|
public static function regenerate_report_data() {
|
||||||
WC_Admin_Reports_Orders_Data_Store::queue_order_stats_repopulate_database();
|
WC_Admin_Reports_Orders_Data_Store::queue_order_stats_repopulate_database();
|
||||||
self::order_product_lookup_store_init();
|
self::order_product_lookup_store_init();
|
||||||
}
|
}
|
||||||
|
@ -234,7 +276,7 @@ class WC_Admin_Api_Init {
|
||||||
'name' => __( 'Rebuild reports data', 'wc-admin' ),
|
'name' => __( 'Rebuild reports data', 'wc-admin' ),
|
||||||
'button' => __( 'Rebuild reports', 'wc-admin' ),
|
'button' => __( 'Rebuild reports', 'wc-admin' ),
|
||||||
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'wc-admin' ),
|
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'wc-admin' ),
|
||||||
'callback' => array( 'WC_Admin_Api_Init', 'regenrate_report_data' ),
|
'callback' => array( 'WC_Admin_Api_Init', 'regenerate_report_data' ),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -245,6 +287,9 @@ class WC_Admin_Api_Init {
|
||||||
*/
|
*/
|
||||||
public static function orders_data_store_init() {
|
public static function orders_data_store_init() {
|
||||||
WC_Admin_Reports_Orders_Data_Store::init();
|
WC_Admin_Reports_Orders_Data_Store::init();
|
||||||
|
WC_Admin_Reports_Products_Data_Store::init();
|
||||||
|
WC_Admin_Reports_Taxes_Data_Store::init();
|
||||||
|
WC_Admin_Reports_Coupons_Data_Store::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -270,35 +315,7 @@ class WC_Admin_Api_Init {
|
||||||
|
|
||||||
// Process orders until close to running out of memory timeouts on large sites then requeue.
|
// Process orders until close to running out of memory timeouts on large sites then requeue.
|
||||||
foreach ( $orders as $order_id ) {
|
foreach ( $orders as $order_id ) {
|
||||||
$order = wc_get_order( $order_id );
|
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id );
|
||||||
if ( ! $order ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach ( $order->get_items() as $order_item ) {
|
|
||||||
$wpdb->replace(
|
|
||||||
$wpdb->prefix . 'wc_order_product_lookup',
|
|
||||||
array(
|
|
||||||
'order_item_id' => $order_item->get_id(),
|
|
||||||
'order_id' => $order->get_id(),
|
|
||||||
'product_id' => $order_item->get_product_id( 'edit' ),
|
|
||||||
'variation_id' => $order_item->get_variation_id( 'edit' ),
|
|
||||||
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
|
|
||||||
'product_qty' => $order_item->get_quantity( 'edit' ),
|
|
||||||
'product_net_revenue' => $order_item->get_subtotal( 'edit' ),
|
|
||||||
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%f',
|
|
||||||
'%s',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Pop the order ID from the array for updating the transient later should we near memory exhaustion.
|
// Pop the order ID from the array for updating the transient later should we near memory exhaustion.
|
||||||
unset( $orders[ $order_id ] );
|
unset( $orders[ $order_id ] );
|
||||||
if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) {
|
if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) {
|
||||||
|
@ -331,6 +348,7 @@ class WC_Admin_Api_Init {
|
||||||
'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
|
'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
|
||||||
'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
|
'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
|
||||||
'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
|
'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
|
||||||
|
'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store',
|
||||||
'admin-note' => 'WC_Admin_Notes_Data_Store',
|
'admin-note' => 'WC_Admin_Notes_Data_Store',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -477,6 +495,17 @@ class WC_Admin_Api_Init {
|
||||||
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'order_product_lookup_store_init' ), 20 );
|
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'order_product_lookup_store_init' ), 20 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the WP REST API for product categories
|
||||||
|
*
|
||||||
|
* @param array $args Default arguments for product_cat taxonomy.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function show_product_categories_in_rest( $args ) {
|
||||||
|
$args['show_in_rest'] = true;
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new WC_Admin_Api_Init();
|
new WC_Admin_Api_Init();
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Class for parameter-based downloads report querying.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* $args = array(
|
||||||
|
* 'before' => '2018-07-19 00:00:00',
|
||||||
|
* 'after' => '2018-07-05 00:00:00',
|
||||||
|
* 'page' => 2,
|
||||||
|
* 'products' => array(1,2,3)
|
||||||
|
* );
|
||||||
|
* $report = new WC_Admin_Reports_Downloads_Query( $args );
|
||||||
|
* $mydata = $report->get_data();
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WC_Admin_Reports_Downloads_Query
|
||||||
|
*/
|
||||||
|
class WC_Admin_Reports_Downloads_Query extends WC_Admin_Reports_Query {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid fields for downloads report.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_default_query_vars() {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get downloads data based on the current query vars.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_data() {
|
||||||
|
$args = apply_filters( 'woocommerce_reports_downloads_query_args', $this->get_query_vars() );
|
||||||
|
|
||||||
|
$data_store = WC_Data_Store::load( 'report-downloads' );
|
||||||
|
$results = $data_store->get_data( $args );
|
||||||
|
return apply_filters( 'woocommerce_reports_downloads_select_query', $results, $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
'items_sold' => 'SUM(product_qty) as items_sold',
|
'items_sold' => 'SUM(product_qty) as items_sold',
|
||||||
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
|
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
|
||||||
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
|
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
|
||||||
// 'products_count' is not a SQL column at the moment, see below.
|
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,6 +73,16 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
// Limit is left out here so that the grouping in code by PHP can be applied correctly.
|
// Limit is left out here so that the grouping in code by PHP can be applied correctly.
|
||||||
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
|
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
|
||||||
|
|
||||||
|
// join wp_order_product_lookup_table with relationships and taxonomies
|
||||||
|
// @TODO: How to handle custom product tables?
|
||||||
|
$sql_query_params['from_clause'] .= " LEFT JOIN {$wpdb->prefix}term_relationships ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}term_relationships.object_id";
|
||||||
|
$sql_query_params['from_clause'] .= " LEFT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id";
|
||||||
|
|
||||||
|
$included_categories = $this->get_included_categories( $query_args );
|
||||||
|
if ( $included_categories ) {
|
||||||
|
$sql_query_params['where_clause'] .= " AND {$wpdb->prefix}term_taxonomy.term_id IN ({$included_categories})";
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: only products in the category C or orders with products from category C (and, possibly others?).
|
// TODO: only products in the category C or orders with products from category C (and, possibly others?).
|
||||||
$included_products = $this->get_included_products( $query_args );
|
$included_products = $this->get_included_products( $query_args );
|
||||||
if ( $included_products ) {
|
if ( $included_products ) {
|
||||||
|
@ -85,21 +95,24 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
|
$sql_query_params['where_clause'] .= " AND ( {$order_status_filter} )";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sql_query_params['where_clause'] .= " AND taxonomy = 'product_cat' ";
|
||||||
|
|
||||||
return $sql_query_params;
|
return $sql_query_params;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps ordering specified by the user to columns in the database/fields in the data.
|
* Returns comma separated ids of included categories, based on query arguments from the user.
|
||||||
*
|
*
|
||||||
* @param string $order_by Sorting criterion.
|
* @param array $query_args Parameters supplied by the user.
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function normalize_order_by( $order_by ) {
|
protected function get_included_categories( $query_args ) {
|
||||||
if ( 'date' === $order_by ) {
|
$included_categories_str = '';
|
||||||
return 'date_created';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $order_by;
|
if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) {
|
||||||
|
$included_categories_str = implode( ',', $query_args['categories'] );
|
||||||
|
}
|
||||||
|
return $included_categories_str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -208,10 +221,9 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
$selections = $this->selected_columns( $query_args );
|
$selections = $this->selected_columns( $query_args );
|
||||||
$sql_query_params = $this->get_sql_query_params( $query_args );
|
$sql_query_params = $this->get_sql_query_params( $query_args );
|
||||||
|
|
||||||
$products_data = $wpdb->get_results(
|
$categories_data = $wpdb->get_results(
|
||||||
"SELECT
|
"SELECT
|
||||||
product_id,
|
term_id as category_id,
|
||||||
date_created,
|
|
||||||
{$selections}
|
{$selections}
|
||||||
FROM
|
FROM
|
||||||
{$table_name}
|
{$table_name}
|
||||||
|
@ -221,39 +233,15 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store
|
||||||
{$sql_query_params['where_time_clause']}
|
{$sql_query_params['where_time_clause']}
|
||||||
{$sql_query_params['where_clause']}
|
{$sql_query_params['where_clause']}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
product_id
|
category_id
|
||||||
",
|
",
|
||||||
ARRAY_A
|
ARRAY_A
|
||||||
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||||
|
|
||||||
if ( null === $products_data ) {
|
if ( null === $categories_data ) {
|
||||||
return new WP_Error( 'woocommerce_reports_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ), array( 'status' => 500 ) );
|
return new WP_Error( 'woocommerce_reports_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ), array( 'status' => 500 ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group by category without a helper table, worst case we add it and change the SQL afterwards.
|
|
||||||
// Other option would be a join with wp_post and taxonomies, but a) performance might be bad, b) how to handle custom product tables?
|
|
||||||
$categories_data = array();
|
|
||||||
foreach ( $products_data as $product_data ) {
|
|
||||||
$categories = get_the_terms( $product_data['product_id'], 'product_cat' );
|
|
||||||
foreach ( $categories as $category ) {
|
|
||||||
$cat_id = $category->term_id;
|
|
||||||
if ( ! key_exists( $cat_id, $categories_data ) ) {
|
|
||||||
$categories_data[ $cat_id ] = array(
|
|
||||||
'category_id' => 0,
|
|
||||||
'items_sold' => 0,
|
|
||||||
'net_revenue' => 0.0,
|
|
||||||
'orders_count' => 0,
|
|
||||||
'products_count' => 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$categories_data[ $cat_id ]['category_id'] = $cat_id;
|
|
||||||
$categories_data[ $cat_id ]['items_sold'] += $product_data['items_sold'];
|
|
||||||
$categories_data[ $cat_id ]['net_revenue'] += $product_data['net_revenue'];
|
|
||||||
$categories_data[ $cat_id ]['orders_count'] += $product_data['orders_count'];
|
|
||||||
$categories_data[ $cat_id ]['products_count'] ++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$record_count = count( $categories_data );
|
$record_count = count( $categories_data );
|
||||||
$total_pages = (int) ceil( $record_count / $query_args['per_page'] );
|
$total_pages = (int) ceil( $record_count / $query_args['per_page'] );
|
||||||
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
|
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
|
||||||
|
|
|
@ -41,6 +41,15 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
|
||||||
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
|
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up all the hooks for maintaining and populating table data.
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
add_action( 'save_post', array( __CLASS__, 'sync_order_coupons' ) );
|
||||||
|
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_coupons' ) );
|
||||||
|
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_coupons' ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns comma separated ids of included coupons, based on query arguments from the user.
|
* Returns comma separated ids of included coupons, based on query arguments from the user.
|
||||||
*
|
*
|
||||||
|
@ -249,4 +258,48 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
|
||||||
return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) );
|
return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update an an entry in the wc_order_coupon_lookup table for an order.
|
||||||
|
*
|
||||||
|
* @since 3.5.0
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function sync_order_coupons( $order_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
if ( ! $order ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array( $order->get_status(), parent::get_report_order_statuses(), true ) ) {
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . self::TABLE_NAME,
|
||||||
|
array( 'order_id' => $order->get_id() ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$coupon_items = $order->get_items( 'coupon' );
|
||||||
|
foreach ( $coupon_items as $coupon_item ) {
|
||||||
|
$wpdb->replace(
|
||||||
|
$wpdb->prefix . self::TABLE_NAME,
|
||||||
|
array(
|
||||||
|
'order_id' => $order_id,
|
||||||
|
'coupon_id' => wc_get_coupon_id_by_code( $coupon_item->get_code() ),
|
||||||
|
'discount_amount' => $coupon_item->get_discount(),
|
||||||
|
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'%d',
|
||||||
|
'%d',
|
||||||
|
'%f',
|
||||||
|
'%s',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -635,6 +635,65 @@ class WC_Admin_Reports_Data_Store {
|
||||||
return $excluded_coupons_str;
|
return $excluded_coupons_str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns comma separated ids of included orders, based on query arguments from the user.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_included_orders( $query_args ) {
|
||||||
|
$included_orders_str = '';
|
||||||
|
|
||||||
|
if ( isset( $query_args['order_includes'] ) && is_array( $query_args['order_includes'] ) && count( $query_args['order_includes'] ) > 0 ) {
|
||||||
|
$included_orders_str = implode( ',', $query_args['order_includes'] );
|
||||||
|
}
|
||||||
|
return $included_orders_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns comma separated ids of excluded orders, based on query arguments from the user.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_excluded_orders( $query_args ) {
|
||||||
|
$excluded_orders_str = '';
|
||||||
|
|
||||||
|
if ( isset( $query_args['order_excludes'] ) && is_array( $query_args['order_excludes'] ) && count( $query_args['order_excludes'] ) > 0 ) {
|
||||||
|
$excluded_orders_str = implode( ',', $query_args['order_excludes'] );
|
||||||
|
}
|
||||||
|
return $excluded_orders_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns comma separated ids of included users, based on query arguments from the user.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_included_users( $query_args ) {
|
||||||
|
$included_users_str = '';
|
||||||
|
|
||||||
|
if ( isset( $query_args['user_includes'] ) && is_array( $query_args['user_includes'] ) && count( $query_args['user_includes'] ) > 0 ) {
|
||||||
|
$included_users_str = implode( ',', $query_args['user_includes'] );
|
||||||
|
}
|
||||||
|
return $included_users_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns comma separated ids of excluded users, based on query arguments from the user.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_excluded_users( $query_args ) {
|
||||||
|
$excluded_users_str = '';
|
||||||
|
|
||||||
|
if ( isset( $query_args['user_excludes'] ) && is_array( $query_args['user_excludes'] ) && count( $query_args['user_excludes'] ) > 0 ) {
|
||||||
|
$excluded_users_str = implode( ',', $query_args['user_excludes'] );
|
||||||
|
}
|
||||||
|
return $excluded_users_str;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns order status subquery to be used in WHERE SQL query, based on query arguments from the user.
|
* Returns order status subquery to be used in WHERE SQL query, based on query arguments from the user.
|
||||||
|
|
|
@ -0,0 +1,383 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WC_Admin_Reports_Downloads_Data_Store class file.
|
||||||
|
*
|
||||||
|
* @package WooCommerce Admin/Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WC_Admin_Reports_Downloads_Data_Store.
|
||||||
|
*/
|
||||||
|
class WC_Admin_Reports_Downloads_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table used to get the data.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const TABLE_NAME = 'wc_download_log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping columns to data type to return correct response types.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $column_types = array(
|
||||||
|
'id' => 'intval',
|
||||||
|
'date' => 'strval',
|
||||||
|
'date_gmt' => 'strval',
|
||||||
|
'download_id' => 'strval', // String because this can sometimes be a hash.
|
||||||
|
'file_name' => 'strval',
|
||||||
|
'product_id' => 'intval',
|
||||||
|
'order_id' => 'intval',
|
||||||
|
'user_id' => 'intval',
|
||||||
|
'ip_address' => 'strval',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL columns to select in the db query and their mapping to SQL code.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $report_columns = array(
|
||||||
|
'id' => 'download_log_id as id',
|
||||||
|
'date' => 'timestamp as date_gmt',
|
||||||
|
'download_id' => 'product_permissions.download_id',
|
||||||
|
'product_id' => 'product_permissions.product_id',
|
||||||
|
'order_id' => 'product_permissions.order_id',
|
||||||
|
'user_id' => 'product_permissions.user_id',
|
||||||
|
'ip_address' => 'user_ip_address as ip_address',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
global $wpdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the database query with parameters used for downloads report.
|
||||||
|
*
|
||||||
|
* @param array $query_args Query arguments supplied by the user.
|
||||||
|
* @return array Array of parameters used for SQL query.
|
||||||
|
*/
|
||||||
|
protected function get_sql_query_params( $query_args ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$lookup_table = $wpdb->prefix . self::TABLE_NAME;
|
||||||
|
$operator = $this->get_match_operator( $query_args );
|
||||||
|
$where_filters = array();
|
||||||
|
|
||||||
|
$sql_query_params = $this->get_time_period_sql_params( $query_args, $lookup_table );
|
||||||
|
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
|
||||||
|
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
|
||||||
|
|
||||||
|
$included_products = $this->get_included_products( $query_args );
|
||||||
|
$excluded_products = $this->get_excluded_products( $query_args );
|
||||||
|
if ( $included_products ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.permission_id IN (
|
||||||
|
SELECT
|
||||||
|
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
|
||||||
|
FROM
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||||
|
WHERE
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$included_products})
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $excluded_products ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.permission_id NOT IN (
|
||||||
|
SELECT
|
||||||
|
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
|
||||||
|
FROM
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||||
|
WHERE
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions.product_id IN ({$excluded_products})
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
$included_orders = $this->get_included_orders( $query_args );
|
||||||
|
$excluded_orders = $this->get_excluded_orders( $query_args );
|
||||||
|
if ( $included_orders ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.permission_id IN (
|
||||||
|
SELECT
|
||||||
|
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
|
||||||
|
FROM
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||||
|
WHERE
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$included_orders})
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $excluded_orders ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.permission_id NOT IN (
|
||||||
|
SELECT
|
||||||
|
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
|
||||||
|
FROM
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||||
|
WHERE
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions.order_id IN ({$excluded_orders})
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
$included_users = $this->get_included_users( $query_args );
|
||||||
|
$excluded_users = $this->get_excluded_users( $query_args );
|
||||||
|
if ( $included_users ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.permission_id IN (
|
||||||
|
SELECT
|
||||||
|
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
|
||||||
|
FROM
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||||
|
WHERE
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$included_users})
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $excluded_users ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.permission_id NOT IN (
|
||||||
|
SELECT
|
||||||
|
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
|
||||||
|
FROM
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||||
|
WHERE
|
||||||
|
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$excluded_users})
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
$included_ip_addresses = $this->get_included_ip_addresses( $query_args );
|
||||||
|
$excluded_ip_addresses = $this->get_excluded_ip_addresses( $query_args );
|
||||||
|
if ( $included_ip_addresses ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.user_ip_address IN ('{$included_ip_addresses}')";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $excluded_ip_addresses ) {
|
||||||
|
$where_filters[] = " {$lookup_table}.user_ip_address NOT IN ('{$excluded_ip_addresses}')";
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_subclause = implode( " $operator ", $where_filters );
|
||||||
|
if ( $where_subclause ) {
|
||||||
|
$sql_query_params['where_clause'] .= " AND ( $where_subclause )";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}woocommerce_downloadable_product_permissions as product_permissions ON {$lookup_table}.permission_id = product_permissions.permission_id";
|
||||||
|
|
||||||
|
return $sql_query_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns comma separated ids of included ip address, based on query arguments from the user.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_included_ip_addresses( $query_args ) {
|
||||||
|
$included_ips_str = '';
|
||||||
|
|
||||||
|
if ( isset( $query_args['ip_address_includes'] ) && is_array( $query_args['ip_address_includes'] ) && count( $query_args['ip_address_includes'] ) > 0 ) {
|
||||||
|
$ip_includes = array();
|
||||||
|
foreach ( $query_args['ip_address_includes'] as $ip ) {
|
||||||
|
$ip_includes[] = esc_sql( $ip );
|
||||||
|
}
|
||||||
|
$included_ips_str = implode( "','", $ip_includes );
|
||||||
|
}
|
||||||
|
return $included_ips_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns comma separated ids of excluded ip address, based on query arguments from the user.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_excluded_ip_addresses( $query_args ) {
|
||||||
|
$excluded_ips_str = '';
|
||||||
|
|
||||||
|
if ( isset( $query_args['ip_address_excludes'] ) && is_array( $query_args['ip_address_excludes'] ) && count( $query_args['ip_address_excludes'] ) > 0 ) {
|
||||||
|
$ip_excludes = array();
|
||||||
|
foreach ( $query_args['ip_address_excludes'] as $ip ) {
|
||||||
|
$ip_excludes[] = esc_sql( $ip );
|
||||||
|
}
|
||||||
|
$excluded_ips_str = implode( ',', $ip_excludes );
|
||||||
|
}
|
||||||
|
return $excluded_ips_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills WHERE clause of SQL request with date-related constraints.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @param string $table_name Name of the db table relevant for the date constraint.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_time_period_sql_params( $query_args, $table_name ) {
|
||||||
|
$sql_query = array(
|
||||||
|
'from_clause' => '',
|
||||||
|
'where_time_clause' => '',
|
||||||
|
'where_clause' => '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) {
|
||||||
|
$datetime = new DateTime( $query_args['before'] );
|
||||||
|
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
|
||||||
|
$sql_query['where_time_clause'] .= " AND {$table_name}.timestamp <= '$datetime_str'";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) {
|
||||||
|
$datetime = new DateTime( $query_args['after'] );
|
||||||
|
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
|
||||||
|
$sql_query['where_time_clause'] .= " AND {$table_name}.timestamp >= '$datetime_str'";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sql_query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills ORDER BY clause of SQL request based on user supplied parameters.
|
||||||
|
*
|
||||||
|
* @param array $query_args Parameters supplied by the user.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_order_by_sql_params( $query_args ) {
|
||||||
|
$sql_query['order_by_clause'] = '';
|
||||||
|
if ( isset( $query_args['orderby'] ) ) {
|
||||||
|
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $query_args['order'] ) ) {
|
||||||
|
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
|
||||||
|
} else {
|
||||||
|
$sql_query['order_by_clause'] .= ' DESC';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sql_query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the report data based on parameters supplied by the user.
|
||||||
|
*
|
||||||
|
* @param array $query_args Query parameters.
|
||||||
|
* @return stdClass|WP_Error Data.
|
||||||
|
*/
|
||||||
|
public function get_data( $query_args ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table_name = $wpdb->prefix . self::TABLE_NAME;
|
||||||
|
$now = time();
|
||||||
|
$week_back = $now - WEEK_IN_SECONDS;
|
||||||
|
|
||||||
|
// These defaults are only partially applied when used via REST API, as that has its own defaults.
|
||||||
|
$defaults = array(
|
||||||
|
'per_page' => get_option( 'posts_per_page' ),
|
||||||
|
'page' => 1,
|
||||||
|
'order' => 'DESC',
|
||||||
|
'orderby' => 'timestamp',
|
||||||
|
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
|
||||||
|
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
|
||||||
|
'fields' => '*',
|
||||||
|
);
|
||||||
|
$query_args = wp_parse_args( $query_args, $defaults );
|
||||||
|
|
||||||
|
$cache_key = $this->get_cache_key( $query_args );
|
||||||
|
$data = wp_cache_get( $cache_key, $this->cache_group );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
$data = (object) array(
|
||||||
|
'data' => array(),
|
||||||
|
'total' => 0,
|
||||||
|
'pages' => 0,
|
||||||
|
'page_no' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
$selections = $this->selected_columns( $query_args );
|
||||||
|
$sql_query_params = $this->get_sql_query_params( $query_args );
|
||||||
|
|
||||||
|
$db_records_count = (int) $wpdb->get_var(
|
||||||
|
"SELECT COUNT(*) FROM (
|
||||||
|
SELECT
|
||||||
|
{$table_name}.download_log_id
|
||||||
|
FROM
|
||||||
|
{$table_name}
|
||||||
|
{$sql_query_params['from_clause']}
|
||||||
|
WHERE
|
||||||
|
1=1
|
||||||
|
{$sql_query_params['where_time_clause']}
|
||||||
|
{$sql_query_params['where_clause']}
|
||||||
|
GROUP BY
|
||||||
|
{$table_name}.download_log_id
|
||||||
|
) AS tt"
|
||||||
|
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||||
|
|
||||||
|
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
|
||||||
|
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$download_data = $wpdb->get_results(
|
||||||
|
"SELECT
|
||||||
|
{$selections}
|
||||||
|
FROM
|
||||||
|
{$table_name}
|
||||||
|
{$sql_query_params['from_clause']}
|
||||||
|
WHERE
|
||||||
|
1=1
|
||||||
|
{$sql_query_params['where_time_clause']}
|
||||||
|
{$sql_query_params['where_clause']}
|
||||||
|
GROUP BY
|
||||||
|
{$table_name}.download_log_id
|
||||||
|
ORDER BY
|
||||||
|
{$sql_query_params['order_by_clause']}
|
||||||
|
{$sql_query_params['limit']}
|
||||||
|
",
|
||||||
|
ARRAY_A
|
||||||
|
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||||
|
|
||||||
|
if ( null === $download_data ) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$download_data = array_map( array( $this, 'cast_numbers' ), $download_data );
|
||||||
|
$data = (object) array(
|
||||||
|
'data' => $download_data,
|
||||||
|
'total' => $db_records_count,
|
||||||
|
'pages' => $total_pages,
|
||||||
|
'page_no' => (int) $query_args['page'],
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_cache_set( $cache_key, $data, $this->cache_group );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns string to be used as cache key for the data.
|
||||||
|
*
|
||||||
|
* @param array $params Query parameters.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_cache_key( $params ) {
|
||||||
|
return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps ordering specified by the user to columns in the database/fields in the data.
|
||||||
|
*
|
||||||
|
* @param string $order_by Sorting criterion.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function normalize_order_by( $order_by ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ( 'date' === $order_by ) {
|
||||||
|
return $wpdb->prefix . 'wc_download_log.timestamp';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $order_by;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -66,7 +66,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
|
||||||
'shipping' => 'SUM(shipping_total) AS shipping',
|
'shipping' => 'SUM(shipping_total) AS shipping',
|
||||||
'net_revenue' => '( SUM(net_total) - SUM(refund_total) ) AS net_revenue',
|
'net_revenue' => '( SUM(net_total) - SUM(refund_total) ) AS net_revenue',
|
||||||
'avg_items_per_order' => 'AVG(num_items_sold) AS avg_items_per_order',
|
'avg_items_per_order' => 'AVG(num_items_sold) AS avg_items_per_order',
|
||||||
'avg_order_value' => 'AVG(gross_total) AS avg_order_value',
|
'avg_order_value' => '( SUM(net_total) - SUM(refund_total) ) / COUNT(*) AS avg_order_value',
|
||||||
'num_returning_customers' => 'SUM(returning_customer = 1) AS num_returning_customers',
|
'num_returning_customers' => 'SUM(returning_customer = 1) AS num_returning_customers',
|
||||||
'num_new_customers' => 'SUM(returning_customer = 0) AS num_new_customers',
|
'num_new_customers' => 'SUM(returning_customer = 0) AS num_new_customers',
|
||||||
);
|
);
|
||||||
|
@ -95,6 +95,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
|
||||||
// TODO: this is required as order update skips save_post.
|
// TODO: this is required as order update skips save_post.
|
||||||
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order' ) );
|
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order' ) );
|
||||||
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) );
|
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) );
|
||||||
|
add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 );
|
||||||
|
|
||||||
if ( ! self::$background_process ) {
|
if ( ! self::$background_process ) {
|
||||||
self::$background_process = new WC_Admin_Order_Stats_Background_Process();
|
self::$background_process = new WC_Admin_Order_Stats_Background_Process();
|
||||||
|
@ -711,6 +712,16 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
|
||||||
self::update( $order );
|
self::update( $order );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs order information when a refund is deleted.
|
||||||
|
*
|
||||||
|
* @param int $refund_id Refund ID.
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
*/
|
||||||
|
public static function sync_on_refund_delete( $refund_id, $order_id ) {
|
||||||
|
self::sync_order( $order_id );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the database with stats data.
|
* Update the database with stats data.
|
||||||
*
|
*
|
||||||
|
|
|
@ -36,6 +36,11 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
|
||||||
'price' => 'floatval',
|
'price' => 'floatval',
|
||||||
'image' => 'strval',
|
'image' => 'strval',
|
||||||
'permalink' => 'strval',
|
'permalink' => 'strval',
|
||||||
|
'stock_status' => 'strval',
|
||||||
|
'stock_quantity' => 'intval',
|
||||||
|
'low_stock_amount' => 'intval',
|
||||||
|
'category_ids' => 'array_values',
|
||||||
|
'sku' => 'strval',
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,8 +68,19 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
|
||||||
'stock_status',
|
'stock_status',
|
||||||
'stock_quantity',
|
'stock_quantity',
|
||||||
'low_stock_amount',
|
'low_stock_amount',
|
||||||
|
'category_ids',
|
||||||
|
'sku',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up all the hooks for maintaining and populating table data.
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
add_action( 'save_post', array( __CLASS__, 'sync_order_products' ) );
|
||||||
|
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_products' ) );
|
||||||
|
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_products' ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills ORDER BY clause of SQL request based on user supplied parameters.
|
* Fills ORDER BY clause of SQL request based on user supplied parameters.
|
||||||
*
|
*
|
||||||
|
@ -282,4 +298,93 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
|
||||||
return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) );
|
return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update an entry in the wc_admin_order_product_lookup table for an order.
|
||||||
|
*
|
||||||
|
* @since 3.5.0
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function sync_order_products( $order_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
// This hook gets called on refunds as well, so return early to avoid errors.
|
||||||
|
if ( ! $order || 'shop_order_refund' === $order->get_type() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array( $order->get_status(), parent::get_report_order_statuses(), true ) ) {
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . self::TABLE_NAME,
|
||||||
|
array( 'order_id' => $order->get_id() ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refunds = self::get_order_refund_items( $order );
|
||||||
|
|
||||||
|
foreach ( $order->get_items() as $order_item ) {
|
||||||
|
$order_item_id = $order_item->get_id();
|
||||||
|
$quantity_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['quantity'] : 0;
|
||||||
|
$amount_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['subtotal'] : 0;
|
||||||
|
if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) {
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . self::TABLE_NAME,
|
||||||
|
array( 'order_item_id' => $order_item_id ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$wpdb->replace(
|
||||||
|
$wpdb->prefix . self::TABLE_NAME,
|
||||||
|
array(
|
||||||
|
'order_item_id' => $order_item_id,
|
||||||
|
'order_id' => $order->get_id(),
|
||||||
|
'product_id' => $order_item->get_product_id( 'edit' ),
|
||||||
|
'variation_id' => $order_item->get_variation_id( 'edit' ),
|
||||||
|
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
|
||||||
|
'product_qty' => $order_item->get_quantity( 'edit' ) - $quantity_refunded,
|
||||||
|
'product_net_revenue' => $order_item->get_subtotal( 'edit' ) - $amount_refunded,
|
||||||
|
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'%d',
|
||||||
|
'%d',
|
||||||
|
'%d',
|
||||||
|
'%d',
|
||||||
|
'%d',
|
||||||
|
'%d',
|
||||||
|
'%f',
|
||||||
|
'%s',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get order refund items quantity and subtotal
|
||||||
|
*
|
||||||
|
* @param object $order WC Order object.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function get_order_refund_items( $order ) {
|
||||||
|
$refunds = $order->get_refunds();
|
||||||
|
$refunded_line_items = array();
|
||||||
|
foreach ( $refunds as $refund ) {
|
||||||
|
foreach ( $refund->get_items() as $refunded_item ) {
|
||||||
|
$line_item_id = wc_get_order_item_meta( $refunded_item->get_id(), '_refunded_item_id', true );
|
||||||
|
if ( ! isset( $refunded_line_items[ $line_item_id ] ) ) {
|
||||||
|
$refunded_line_items[ $line_item_id ]['quantity'] = 0;
|
||||||
|
$refunded_line_items[ $line_item_id ]['subtotal'] = 0;
|
||||||
|
}
|
||||||
|
$refunded_line_items[ $line_item_id ]['quantity'] += absint( $refunded_item['quantity'] );
|
||||||
|
$refunded_line_items[ $line_item_id ]['subtotal'] += abs( $refunded_item['subtotal'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $refunded_line_items;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,9 +115,8 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
||||||
$intervals_query = array();
|
$intervals_query = array();
|
||||||
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
|
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
|
||||||
|
|
||||||
$db_records_count = (int) $wpdb->get_var(
|
$db_intervals = $wpdb->get_col(
|
||||||
"SELECT COUNT(*) FROM (
|
"SELECT
|
||||||
SELECT
|
|
||||||
{$intervals_query['select_clause']} AS time_interval
|
{$intervals_query['select_clause']} AS time_interval
|
||||||
FROM
|
FROM
|
||||||
{$table_name}
|
{$table_name}
|
||||||
|
@ -127,15 +126,18 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
||||||
{$intervals_query['where_time_clause']}
|
{$intervals_query['where_time_clause']}
|
||||||
{$intervals_query['where_clause']}
|
{$intervals_query['where_clause']}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
time_interval
|
time_interval"
|
||||||
) AS t"
|
); // WPCS: cache ok, DB call ok, , unprepared SQL ok.
|
||||||
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
|
||||||
|
|
||||||
$total_pages = (int) ceil( $db_records_count / $intervals_query['per_page'] );
|
$db_interval_count = count( $db_intervals );
|
||||||
|
$expected_interval_count = WC_Admin_Reports_Interval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
|
||||||
|
$total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] );
|
||||||
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
|
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
|
||||||
return array();
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count );
|
||||||
|
|
||||||
$totals = $wpdb->get_results(
|
$totals = $wpdb->get_results(
|
||||||
"SELECT
|
"SELECT
|
||||||
{$selections}
|
{$selections}
|
||||||
|
@ -183,21 +185,41 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
||||||
|
|
||||||
$totals = (object) $this->cast_numbers( $totals[0] );
|
$totals = (object) $this->cast_numbers( $totals[0] );
|
||||||
|
|
||||||
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $intervals );
|
|
||||||
$this->create_interval_subtotals( $intervals );
|
|
||||||
|
|
||||||
$data = (object) array(
|
$data = (object) array(
|
||||||
'totals' => $totals,
|
'totals' => $totals,
|
||||||
'intervals' => $intervals,
|
'intervals' => $intervals,
|
||||||
'total' => $db_records_count,
|
'total' => $expected_interval_count,
|
||||||
'pages' => $total_pages,
|
'pages' => $total_pages,
|
||||||
'page_no' => (int) $query_args['page'],
|
'page_no' => (int) $query_args['page'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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'] );
|
||||||
|
} else {
|
||||||
|
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
|
||||||
|
}
|
||||||
|
$this->create_interval_subtotals( $data->intervals );
|
||||||
|
|
||||||
wp_cache_set( $cache_key, $data, $this->cache_group );
|
wp_cache_set( $cache_key, $data, $this->cache_group );
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes order_by clause to match to SQL query.
|
||||||
|
*
|
||||||
|
* @param string $order_by Order by option requeste by user.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function normalize_order_by( $order_by ) {
|
||||||
|
if ( 'date' === $order_by ) {
|
||||||
|
return 'time_interval';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $order_by;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,15 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
|
||||||
$this->report_columns['tax_rate_id'] = $table_name . '.' . $this->report_columns['tax_rate_id'];
|
$this->report_columns['tax_rate_id'] = $table_name . '.' . $this->report_columns['tax_rate_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up all the hooks for maintaining and populating table data.
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
add_action( 'save_post', array( __CLASS__, 'sync_order_taxes' ) );
|
||||||
|
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_taxes' ) );
|
||||||
|
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_taxes' ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the database query with parameters used for Taxes report: categories and order status.
|
* Updates the database query with parameters used for Taxes report: categories and order status.
|
||||||
*
|
*
|
||||||
|
@ -235,4 +244,49 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
|
||||||
return $order_by;
|
return $order_by;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update an entry in the wc_order_tax_lookup table for an order.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function sync_order_taxes( $order_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
if ( ! $order ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array( $order->get_status(), parent::get_report_order_statuses(), true ) ) {
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . self::TABLE_NAME,
|
||||||
|
array( 'order_id' => $order->get_id() ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $order->get_items( 'tax' ) as $tax_item ) {
|
||||||
|
$wpdb->replace(
|
||||||
|
$wpdb->prefix . self::TABLE_NAME,
|
||||||
|
array(
|
||||||
|
'order_id' => $order->get_id(),
|
||||||
|
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
||||||
|
'tax_rate_id' => $tax_item->get_rate_id(),
|
||||||
|
'shipping_tax' => $tax_item->get_shipping_tax_total(),
|
||||||
|
'order_tax' => $tax_item->get_tax_total(),
|
||||||
|
'total_tax' => $tax_item->get_tax_total() + $tax_item->get_shipping_tax_total(),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'%d',
|
||||||
|
'%s',
|
||||||
|
'%d',
|
||||||
|
'%f',
|
||||||
|
'%f',
|
||||||
|
'%f',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,192 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Order Functions
|
|
||||||
*
|
|
||||||
* @package WooCommerce Admin
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an entry in the wc_admin_order_product_lookup table for an order.
|
|
||||||
*
|
|
||||||
* @since 3.5.0
|
|
||||||
* @param int $order_id Order ID.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function wc_admin_order_product_lookup_entry( $order_id ) {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$order = wc_get_order( $order_id );
|
|
||||||
if ( ! $order ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $order->get_items() as $order_item ) {
|
|
||||||
// Shipping amount based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php
|
|
||||||
// distributed simply based on number of line items.
|
|
||||||
$order_items = $order->get_item_count();
|
|
||||||
$refunded = $order->get_total_shipping_refunded();
|
|
||||||
if ( $refunded > 0 ) {
|
|
||||||
$total_shipping_amount = $order->get_shipping_total() - $refunded;
|
|
||||||
} else {
|
|
||||||
$total_shipping_amount = $order->get_shipping_total();
|
|
||||||
}
|
|
||||||
$shipping_amount = $total_shipping_amount / $order_items;
|
|
||||||
|
|
||||||
// Shipping amount tax based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php
|
|
||||||
// distribute simply based on number of line items.
|
|
||||||
$shipping_tax_amount = 0;
|
|
||||||
// TODO: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly?
|
|
||||||
if ( wc_tax_enabled() ) {
|
|
||||||
$order_taxes = $order->get_taxes();
|
|
||||||
$line_items_shipping = $order->get_items( 'shipping' );
|
|
||||||
$total_shipping_tax_amount = 0;
|
|
||||||
foreach ( $line_items_shipping as $item_id => $item ) {
|
|
||||||
$tax_data = $item->get_taxes();
|
|
||||||
if ( $tax_data ) {
|
|
||||||
foreach ( $order_taxes as $tax_item ) {
|
|
||||||
$tax_item_id = $tax_item->get_rate_id();
|
|
||||||
$tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : '';
|
|
||||||
$refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' );
|
|
||||||
if ( $refunded ) {
|
|
||||||
$total_shipping_tax_amount += $tax_item_total - $refunded;
|
|
||||||
} else {
|
|
||||||
$total_shipping_tax_amount += $tax_item_total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$shipping_tax_amount = $total_shipping_tax_amount / $order_items;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tax amount.
|
|
||||||
// TODO: check if this calculates tax correctly with refunds.
|
|
||||||
$tax_amount = 0;
|
|
||||||
if ( wc_tax_enabled() ) {
|
|
||||||
$order_taxes = $order->get_taxes();
|
|
||||||
$tax_data = $order_item->get_taxes();
|
|
||||||
foreach ( $order_taxes as $tax_item ) {
|
|
||||||
$tax_item_id = $tax_item->get_rate_id();
|
|
||||||
$tax_amount += isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$net_revenue = $order_item->get_subtotal( 'edit' );
|
|
||||||
|
|
||||||
// Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php.
|
|
||||||
$coupon_amount = $order_item->get_subtotal( 'edit' ) - $order_item->get_total( 'edit' );
|
|
||||||
|
|
||||||
$wpdb->replace(
|
|
||||||
$wpdb->prefix . 'wc_order_product_lookup',
|
|
||||||
array(
|
|
||||||
'order_item_id' => $order_item->get_id(),
|
|
||||||
'order_id' => $order->get_id(),
|
|
||||||
'product_id' => $order_item->get_product_id( 'edit' ),
|
|
||||||
'variation_id' => $order_item->get_variation_id( 'edit' ),
|
|
||||||
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
|
|
||||||
'product_qty' => $order_item->get_quantity( 'edit' ),
|
|
||||||
'product_net_revenue' => $net_revenue,
|
|
||||||
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
|
||||||
'price' => $order_item->get_subtotal( 'edit' ) / $order_item->get_quantity( 'edit' ),
|
|
||||||
'coupon_amount' => $coupon_amount,
|
|
||||||
'tax_amount' => $tax_amount,
|
|
||||||
'shipping_amount' => $shipping_amount,
|
|
||||||
'shipping_tax_amount' => $shipping_tax_amount,
|
|
||||||
'product_gross_revenue' => $net_revenue + $tax_amount + $shipping_amount + $shipping_tax_amount,
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%f',
|
|
||||||
'%s',
|
|
||||||
'%f',
|
|
||||||
'%f',
|
|
||||||
'%f',
|
|
||||||
'%f',
|
|
||||||
'%f',
|
|
||||||
'%f',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: maybe replace these with woocommerce_create_order, woocommerce_update_order, woocommerce_trash_order, woocommerce_delete_order, as clean_post_cache might be called in other circumstances and trigger too many updates?
|
|
||||||
add_action( 'save_post', 'wc_admin_order_product_lookup_entry', 10, 1 );
|
|
||||||
add_action( 'clean_post_cache', 'wc_admin_order_product_lookup_entry', 10, 1 );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an entry in the wc_order_tax_lookup table for an order.
|
|
||||||
*
|
|
||||||
* @since 3.5.0
|
|
||||||
* @param int $order_id Order ID.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function wc_order_tax_lookup_entry( $order_id ) {
|
|
||||||
global $wpdb;
|
|
||||||
$order = wc_get_order( $order_id );
|
|
||||||
if ( ! $order ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach ( $order->get_items( 'tax' ) as $tax_item ) {
|
|
||||||
$wpdb->replace(
|
|
||||||
$wpdb->prefix . 'wc_order_tax_lookup',
|
|
||||||
array(
|
|
||||||
'order_id' => $order->get_id(),
|
|
||||||
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
|
||||||
'tax_rate_id' => $tax_item->get_rate_id(),
|
|
||||||
'shipping_tax' => $tax_item->get_shipping_tax_total(),
|
|
||||||
'order_tax' => $tax_item->get_tax_total(),
|
|
||||||
'total_tax' => $tax_item->get_tax_total() + $tax_item->get_shipping_tax_total(),
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'%d',
|
|
||||||
'%s',
|
|
||||||
'%d',
|
|
||||||
'%f',
|
|
||||||
'%f',
|
|
||||||
'%f',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_action( 'save_post', 'wc_order_tax_lookup_entry', 10, 1 );
|
|
||||||
add_action( 'clean_post_cache', 'wc_order_tax_lookup_entry', 10, 1 );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an entry in the wc_order_coupon_lookup table for an order.
|
|
||||||
*
|
|
||||||
* @since 3.5.0
|
|
||||||
* @param int $order_id Order ID.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function wc_order_coupon_lookup_entry( $order_id ) {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$order = wc_get_order( $order_id );
|
|
||||||
if ( ! $order ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$coupon_items = $order->get_items( 'coupon' );
|
|
||||||
foreach ( $coupon_items as $coupon_item ) {
|
|
||||||
$wpdb->replace(
|
|
||||||
$wpdb->prefix . 'wc_order_coupon_lookup',
|
|
||||||
array(
|
|
||||||
'order_id' => $order_id,
|
|
||||||
'coupon_id' => wc_get_coupon_id_by_code( $coupon_item->get_code() ),
|
|
||||||
'discount_amount' => $coupon_item->get_discount(),
|
|
||||||
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'%d',
|
|
||||||
'%d',
|
|
||||||
'%f',
|
|
||||||
'%s',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_action( 'save_post', 'wc_order_coupon_lookup_entry', 10, 1 );
|
|
||||||
add_action( 'clean_post_cache', 'wc_order_coupon_lookup_entry', 10, 1 );
|
|
|
@ -214,6 +214,16 @@ function wc_admin_enqueue_script() {
|
||||||
|
|
||||||
wp_enqueue_script( WC_ADMIN_APP );
|
wp_enqueue_script( WC_ADMIN_APP );
|
||||||
wp_enqueue_style( WC_ADMIN_APP );
|
wp_enqueue_style( WC_ADMIN_APP );
|
||||||
|
|
||||||
|
// Use server-side detection to prevent unneccessary stylesheet loading in other browsers.
|
||||||
|
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''; // WPCS: sanitization ok.
|
||||||
|
preg_match( '/MSIE (.*?);/', $user_agent, $matches );
|
||||||
|
if ( count( $matches ) < 2 ) {
|
||||||
|
preg_match( '/Trident\/\d{1,2}.\d{1,2}; rv:([0-9]*)/', $user_agent, $matches );
|
||||||
|
}
|
||||||
|
if ( count( $matches ) > 1 ) {
|
||||||
|
wp_enqueue_style( 'wc-components-ie' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
add_action( 'admin_enqueue_scripts', 'wc_admin_enqueue_script' );
|
add_action( 'admin_enqueue_scripts', 'wc_admin_enqueue_script' );
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,13 @@ function wc_admin_register_script() {
|
||||||
filemtime( wc_admin_dir_path( 'dist/components/style.css' ) )
|
filemtime( wc_admin_dir_path( 'dist/components/style.css' ) )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
wp_register_style(
|
||||||
|
'wc-components-ie',
|
||||||
|
wc_admin_url( 'dist/components/ie.css' ),
|
||||||
|
array( 'wp-edit-blocks' ),
|
||||||
|
filemtime( wc_admin_dir_path( 'dist/components/ie.css' ) )
|
||||||
|
);
|
||||||
|
|
||||||
wp_register_style(
|
wp_register_style(
|
||||||
WC_ADMIN_APP,
|
WC_ADMIN_APP,
|
||||||
wc_admin_url( "dist/{$entry}/style.css" ),
|
wc_admin_url( "dist/{$entry}/style.css" ),
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue