woocommerce/plugins/woocommerce-admin/client/header/activity-panel/panels/orders.js

451 lines
11 KiB
JavaScript
Raw Normal View History

/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
2018-08-01 19:07:17 +00:00
import { Button } from '@wordpress/components';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import Gridicon from 'gridicons';
import PropTypes from 'prop-types';
import interpolateComponents from 'interpolate-components';
import { keyBy, map, merge } from 'lodash';
import {
EmptyContent,
Flag,
Link,
OrderStatus,
Section,
} from '@woocommerce/components';
import { getNewPath } from '@woocommerce/navigation';
import { getAdminLink, getSetting } from '@woocommerce/wc-admin-settings';
import {
SETTINGS_STORE_NAME,
REPORTS_STORE_NAME,
ITEMS_STORE_NAME,
QUERY_DEFAULTS,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header';
import ActivityOutboundLink from '../activity-outbound-link';
import { DEFAULT_ACTIONABLE_STATUSES } from '../../../analytics/settings/config';
import { CurrencyContext } from '../../../lib/currency-context';
class OrdersPanel extends Component {
recordOrderEvent( eventName ) {
recordEvent( `activity_panel_orders_${ eventName }`, {} );
}
renderEmptyCard() {
const { hasNonActionableOrders } = this.props;
if ( hasNonActionableOrders ) {
return (
<ActivityCard
className="woocommerce-empty-activity-card"
title={ __(
'You have no orders to fulfill',
'woocommerce-admin'
) }
icon={ <Gridicon icon="checkmark" size={ 48 } /> }
>
{ __(
"Good job, you've fulfilled all of your new orders!",
'woocommerce-admin'
) }
</ActivityCard>
);
}
return (
<ActivityCard
className="woocommerce-empty-activity-card"
title={ __(
'You have no orders to fulfill',
'woocommerce-admin'
) }
icon={ <Gridicon icon="time" size={ 48 } /> }
actions={
<Button
href="https://docs.woocommerce.com/document/managing-orders/"
isSecondary
onClick={ () => this.recordOrderEvent( 'learn_more' ) }
target="_blank"
>
{ __( 'Learn more', 'woocommerce-admin' ) }
</Button>
}
>
{ __(
"You're still waiting for your customers to make their first orders. " +
'While you wait why not learn how to manage orders?',
'woocommerce-admin'
) }
</ActivityCard>
);
}
renderOrders() {
const { orders } = this.props;
const Currency = this.context;
if ( orders.length === 0 ) {
return this.renderEmptyCard();
}
const getCustomerString = ( order ) => {
const extendedInfo = order.extended_info || {};
const { first_name: firstName, last_name: lastName } =
extendedInfo.customer || {};
if ( ! firstName && ! lastName ) {
return '';
}
const name = [ firstName, lastName ].join( ' ' );
return sprintf(
__(
/* translators: describes who placed an order, e.g. Order #123 placed by John Doe */
'placed by {{customerLink}}%(customerName)s{{/customerLink}}',
'woocommerce-admin'
),
{
customerName: name,
}
);
};
const orderCardTitle = ( order ) => {
const {
extended_info: extendedInfo,
order_id: orderId,
order_number: orderNumber,
} = order;
const { customer } = extendedInfo || {};
const customerUrl = customer.customer_id
? getNewPath( {}, '/analytics/customers', {
filter: 'single_customer',
customers: customer.customer_id,
} )
: null;
return (
<Fragment>
{ interpolateComponents( {
mixedString: sprintf(
__(
'Order {{orderLink}}#%(orderNumber)s{{/orderLink}} %(customerString)s {{destinationFlag/}}',
'woocommerce-admin'
),
{
orderNumber,
customerString: getCustomerString( order ),
}
),
components: {
orderLink: (
<Link
href={ getAdminLink(
'post.php?action=edit&post=' + orderId
) }
onClick={ () =>
this.recordOrderEvent( 'order_number' )
}
type="wp-admin"
/>
),
destinationFlag: customer.country ? (
<Flag
code={ customer.country }
round={ false }
/>
) : null,
customerLink: customerUrl ? (
<Link
href={ customerUrl }
onClick={ () =>
this.recordOrderEvent( 'customer_name' )
}
type="wc-admin"
/>
) : (
<span />
),
},
} ) }
</Fragment>
);
};
const cards = [];
orders.forEach( ( order ) => {
const extendedInfo = order.extended_info || {};
const productsCount =
extendedInfo && extendedInfo.products
? extendedInfo.products.length
: 0;
Correcting and clarifying analytics terms and calculations (https://github.com/woocommerce/woocommerce-admin/pull/3104) * Relabel Net Revenue to Net Sales, revert previous refund work on Gross revenue and rename to total sales. Update the orer of all the things * Add gross sales calculation to revenue stats endpoint. * Restore coupon_total when updating order stats. * Wire up gross sales to revenue report. * Fix revenue report refunds calculation when there are no refunds. * update net sales labels and cases in order, product and category tables * Subtract refunded shipping and taxes from gross sales. * pluses to minuses to fix the gross revenue and refund totals when refunding * Add gross_sales to revenue stats orderby enum. * Change refund labels to Returns * Remove usage of defunct coupon_total column. * Store refunded amount in stats table. * Rename "gross_total" column to "total_sales". * Net total for refund orders can be used instead of a new column. * Rename gross_revenue to total_sales. * Coalesce coupons total in order stats query. SUM()ing all nulls gives null, not zero. * Use segmentation selections to backfill missing data. Fo when report columns and segmentation columns don't match. * Remove errant gross_sales from expected interval test data. * Fix gross sales tests for revenue/stats. * Move missing segment fills back to their original locations. * Fix remaining tests failing because of gross sales. * Fix db upgrade function rename of gross_total column. * Fix linter errors.
2019-11-22 15:06:14 +00:00
const total = order.total_sales;
cards.push(
<ActivityCard
key={ order.order_id }
className="woocommerce-order-activity-card"
title={ orderCardTitle( order ) }
date={ order.date_created_gmt }
subtitle={
<div>
<span>
{ sprintf(
_n(
'%d product',
'%d products',
productsCount,
'woocommerce-admin'
),
productsCount
) }
</span>
<span>{ Currency.formatAmount( total ) }</span>
</div>
}
actions={
<Button
isSecondary
href={ getAdminLink(
'post.php?action=edit&post=' + order.order_id
) }
onClick={ () =>
this.recordOrderEvent(
'orders_begin_fulfillment'
)
}
>
{ __( 'Begin fulfillment' ) }
</Button>
}
>
<OrderStatus
order={ order }
orderStatusMap={ getSetting( 'orderStatuses', {} ) }
/>
</ActivityCard>
);
} );
return (
<Fragment>
{ cards }
<ActivityOutboundLink
href={ 'edit.php?post_type=shop_order' }
onClick={ () => this.recordOrderEvent( 'orders_manage' ) }
>
{ __( 'Manage all orders', 'woocommerce-admin' ) }
</ActivityOutboundLink>
</Fragment>
);
}
render() {
const { orders, isRequesting, isError, orderStatuses } = this.props;
if ( isError ) {
if ( ! orderStatuses.length ) {
return (
<EmptyContent
title={ __(
"You currently don't have any actionable statuses. " +
'To display orders here, select orders that require further review in settings.',
'woocommerce-admin'
) }
actionLabel={ __( 'Settings', 'woocommerce-admin' ) }
actionURL={ getAdminLink(
'admin.php?page=wc-admin&path=/analytics/settings'
) }
/>
);
}
const title = __(
'There was an error getting your orders. Please try again.',
'woocommerce-admin'
);
const actionLabel = __( 'Reload', 'woocommerce-admin' );
const actionCallback = () => {
// @todo Add tracking for how often an error is displayed, and the reload action is clicked.
window.location.reload();
};
return (
<Fragment>
<EmptyContent
title={ title }
actionLabel={ actionLabel }
actionURL={ null }
actionCallback={ actionCallback }
/>
</Fragment>
);
}
const title =
isRequesting || orders.length
? __( 'Orders', 'woocommerce-admin' )
: __( 'No orders to fulfill', 'woocommerce-admin' );
return (
<Fragment>
<ActivityHeader title={ title } />
<Section>
{ isRequesting ? (
<ActivityCardPlaceholder
className="woocommerce-order-activity-card"
hasAction
hasDate
lines={ 2 }
/>
) : (
this.renderOrders()
) }
</Section>
</Fragment>
);
}
}
OrdersPanel.propTypes = {
orders: PropTypes.array.isRequired,
isError: PropTypes.bool,
isRequesting: PropTypes.bool,
};
2018-08-07 20:11:30 +00:00
OrdersPanel.defaultProps = {
orders: [],
isError: false,
isRequesting: false,
2018-08-07 20:11:30 +00:00
};
OrdersPanel.contextType = CurrencyContext;
export default compose(
withSelect( ( select, props ) => {
const { hasActionableOrders } = props;
const { getItems, getItemsError, getItemsTotalCount } = select(
ITEMS_STORE_NAME
);
const { getReportItems, getReportItemsError, isResolving } = select(
REPORTS_STORE_NAME
);
wp.data Settings refactor add data store for settings using wp.data add use-select-with-refresh example replace fresh-data usage with new settings data store for settings page Add data package move to packages Fix isDirty after save Add isBusy to primary button when saving update Readme remove comment readme to use useSelect Revert "update Readme" This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. Data Layer: Settings page to use Settings store (https://github.com/woocommerce/woocommerce-admin/pull/3430) * Data Layer: Settings store as source of truth for settings page This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. * fixup * save on reset * non mutable constants * add set/getSettings * save using setSettings * separate HOC * cleanup * remove settingsToData * withHydration * remove withSettings HOC * renmove useSettins for now * withSettingsHydration updates * Revert "withSettingsHydration updates" This reverts commit f2adf108fbe19b574978fea5925a1a18e7ed3007. * rename withSettingsHydration * redo withSettingsHydration simplification * restore * useSettings * render using useSettings * handleInputChange working * get setIsDirty working * saving works * reset and cleanup * cleanup * use snake case on hook files * use clearIsDirty * Avoid mutation on setting update * remove @todo * persiting -> isPersisting * better reducer ternaries * add wcSettings as arg to withSettingsHydration reset package-lock Settings: split out mutable wcAdminSettings (https://github.com/woocommerce/woocommerce-admin/pull/3675) Settings: handle async settings groups (https://github.com/woocommerce/woocommerce-admin/pull/3707)
2020-03-25 03:20:17 +00:00
const { getSetting: getMutableSetting } = select( SETTINGS_STORE_NAME );
const {
woocommerce_actionable_order_statuses: orderStatuses = DEFAULT_ACTIONABLE_STATUSES,
wp.data Settings refactor add data store for settings using wp.data add use-select-with-refresh example replace fresh-data usage with new settings data store for settings page Add data package move to packages Fix isDirty after save Add isBusy to primary button when saving update Readme remove comment readme to use useSelect Revert "update Readme" This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. Data Layer: Settings page to use Settings store (https://github.com/woocommerce/woocommerce-admin/pull/3430) * Data Layer: Settings store as source of truth for settings page This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. * fixup * save on reset * non mutable constants * add set/getSettings * save using setSettings * separate HOC * cleanup * remove settingsToData * withHydration * remove withSettings HOC * renmove useSettins for now * withSettingsHydration updates * Revert "withSettingsHydration updates" This reverts commit f2adf108fbe19b574978fea5925a1a18e7ed3007. * rename withSettingsHydration * redo withSettingsHydration simplification * restore * useSettings * render using useSettings * handleInputChange working * get setIsDirty working * saving works * reset and cleanup * cleanup * use snake case on hook files * use clearIsDirty * Avoid mutation on setting update * remove @todo * persiting -> isPersisting * better reducer ternaries * add wcSettings as arg to withSettingsHydration reset package-lock Settings: split out mutable wcAdminSettings (https://github.com/woocommerce/woocommerce-admin/pull/3675) Settings: handle async settings groups (https://github.com/woocommerce/woocommerce-admin/pull/3707)
2020-03-25 03:20:17 +00:00
} = getMutableSetting( 'wc_admin', 'wcAdminSettings', {} );
if ( ! orderStatuses.length ) {
return {
orders: [],
isError: true,
isRequesting: false,
orderStatuses,
};
}
2019-07-17 18:36:32 +00:00
if ( hasActionableOrders ) {
// Query the core Orders endpoint for the most up-to-date statuses.
const actionableOrdersQuery = {
page: 1,
per_page: QUERY_DEFAULTS.pageSize,
status: orderStatuses,
_fields: [ 'id', 'date_created_gmt', 'status' ],
};
const actionableOrders = Array.from(
getItems( 'orders', actionableOrdersQuery ).values()
);
const isRequestingActionable = isResolving( 'getItems', [
'orders',
actionableOrdersQuery,
] );
2019-07-17 18:36:32 +00:00
if ( isRequestingActionable ) {
return {
isError: Boolean(
getItemsError( 'orders', actionableOrdersQuery )
),
2019-07-17 18:36:32 +00:00
isRequesting: isRequestingActionable,
orderStatuses,
};
}
// Retrieve the Order stats data from our reporting table.
const ordersQuery = {
page: 1,
per_page: QUERY_DEFAULTS.pageSize,
extended_info: true,
order_includes: map( actionableOrders, 'id' ),
_fields: [
'order_id',
'order_number',
'status',
'data_created_gmt',
'total_sales',
'extended_info.customer',
'extended_info.products',
],
};
const reportOrders = getReportItems( 'orders', ordersQuery ).data;
const isError = Boolean(
getReportItemsError( 'orders', ordersQuery )
);
const isRequesting = isResolving( 'getReportItems', [
'orders',
ordersQuery,
] );
let orders = [];
if ( reportOrders && reportOrders.length ) {
// Merge the core endpoint data with our reporting table.
const actionableOrdersById = keyBy( actionableOrders, 'id' );
orders = reportOrders.map( ( order ) =>
merge(
{},
order,
actionableOrdersById[ order.order_id ] || {}
)
);
}
return { orders, isError, isRequesting, orderStatuses };
}
2019-07-17 18:36:32 +00:00
// Get a count of all orders for messaging purposes.
// @todo Add a property to settings api for this?
2019-07-17 18:36:32 +00:00
const allOrdersQuery = {
page: 1,
per_page: 1,
_fields: [ 'id' ],
};
getItems( 'orders', allOrdersQuery );
const totalNonActionableOrders = getItemsTotalCount(
'orders',
allOrdersQuery
);
const isError = Boolean( getItemsError( 'orders', allOrdersQuery ) );
const isRequesting = isResolving( 'getItems', [
'orders',
allOrdersQuery,
] );
return {
hasNonActionableOrders: totalNonActionableOrders > 0,
isError,
isRequesting,
orderStatuses,
};
} )
)( OrdersPanel );