2019-03-27 16:43:31 +00:00
|
|
|
/**
|
|
|
|
* External dependencies
|
|
|
|
*/
|
|
|
|
import { __, sprintf } from '@wordpress/i18n';
|
2019-04-04 00:05:19 +00:00
|
|
|
import { BaseControl, Button } from '@wordpress/components';
|
2024-05-31 03:49:36 +00:00
|
|
|
import clsx from 'clsx';
|
2019-03-27 17:59:29 +00:00
|
|
|
import { Component, Fragment } from '@wordpress/element';
|
2019-06-18 15:58:10 +00:00
|
|
|
import { ESCAPE } from '@wordpress/keycodes';
|
2019-06-12 21:51:08 +00:00
|
|
|
import { get } from 'lodash';
|
2019-03-27 16:43:31 +00:00
|
|
|
import { Link, ProductImage } from '@woocommerce/components';
|
2020-08-20 04:59:52 +00:00
|
|
|
import { recordEvent } from '@woocommerce/tracks';
|
2020-11-25 18:51:15 +00:00
|
|
|
import moment from 'moment';
|
2019-03-27 16:43:31 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal dependencies
|
|
|
|
*/
|
2021-12-14 16:56:42 +00:00
|
|
|
import { ActivityCard } from '~/activity-panel/activity-card';
|
2022-01-06 12:53:30 +00:00
|
|
|
import { getAdminSetting } from '~/utils/admin-settings';
|
2019-03-27 16:43:31 +00:00
|
|
|
|
2020-11-25 18:51:15 +00:00
|
|
|
export class ProductStockCard extends Component {
|
2019-03-27 17:59:29 +00:00
|
|
|
constructor( props ) {
|
|
|
|
super( props );
|
|
|
|
this.state = {
|
|
|
|
quantity: props.product.stock_quantity,
|
|
|
|
editing: false,
|
2019-06-27 09:21:43 +00:00
|
|
|
edited: false,
|
2019-03-27 17:59:29 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this.beginEdit = this.beginEdit.bind( this );
|
|
|
|
this.cancelEdit = this.cancelEdit.bind( this );
|
|
|
|
this.onQuantityChange = this.onQuantityChange.bind( this );
|
2019-06-18 15:58:10 +00:00
|
|
|
this.handleKeyDown = this.handleKeyDown.bind( this );
|
2019-06-27 09:21:43 +00:00
|
|
|
this.onSubmit = this.onSubmit.bind( this );
|
2019-04-04 00:05:19 +00:00
|
|
|
}
|
|
|
|
|
2020-08-03 16:38:57 +00:00
|
|
|
recordStockEvent( eventName, eventProps = {} ) {
|
|
|
|
recordEvent( `activity_panel_stock_${ eventName }`, eventProps );
|
|
|
|
}
|
|
|
|
|
2019-03-27 17:59:29 +00:00
|
|
|
beginEdit() {
|
2019-06-27 09:21:43 +00:00
|
|
|
const { product } = this.props;
|
|
|
|
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
editing: true,
|
|
|
|
quantity: product.stock_quantity,
|
|
|
|
},
|
|
|
|
() => {
|
2020-02-14 02:23:21 +00:00
|
|
|
if ( this.quantityInput ) {
|
|
|
|
this.quantityInput.focus();
|
|
|
|
}
|
2019-06-27 09:21:43 +00:00
|
|
|
}
|
|
|
|
);
|
2020-08-03 16:38:57 +00:00
|
|
|
this.recordStockEvent( 'update_stock' );
|
2019-03-27 17:59:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cancelEdit() {
|
2019-06-27 09:21:43 +00:00
|
|
|
const { product } = this.props;
|
|
|
|
|
2019-03-27 17:59:29 +00:00
|
|
|
this.setState( {
|
|
|
|
editing: false,
|
2019-06-27 09:21:43 +00:00
|
|
|
quantity: product.stock_quantity,
|
2019-03-27 17:59:29 +00:00
|
|
|
} );
|
2020-08-03 16:38:57 +00:00
|
|
|
this.recordStockEvent( 'cancel' );
|
2019-03-27 17:59:29 +00:00
|
|
|
}
|
|
|
|
|
2019-06-18 15:58:10 +00:00
|
|
|
handleKeyDown( event ) {
|
|
|
|
if ( event.keyCode === ESCAPE ) {
|
|
|
|
this.cancelEdit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-05 20:31:20 +00:00
|
|
|
onQuantityChange( event ) {
|
|
|
|
this.setState( { quantity: event.target.value } );
|
2019-03-27 17:59:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-20 23:37:41 +00:00
|
|
|
async onSubmit() {
|
|
|
|
const { product, updateProductStock, createNotice } = this.props;
|
2020-11-25 18:51:15 +00:00
|
|
|
const quantity = parseInt( this.state.quantity, 10 );
|
|
|
|
|
|
|
|
// Bail on an actual update if the quantity is unchanged.
|
|
|
|
if ( product.stock_quantity === quantity ) {
|
|
|
|
this.setState( { editing: false } );
|
|
|
|
return;
|
|
|
|
}
|
2019-03-28 17:39:55 +00:00
|
|
|
|
2019-06-27 09:21:43 +00:00
|
|
|
this.setState( { editing: false, edited: true } );
|
2019-03-28 19:10:01 +00:00
|
|
|
|
2020-11-30 15:12:08 +00:00
|
|
|
const success = await updateProductStock( product, quantity );
|
2020-08-20 23:37:41 +00:00
|
|
|
|
2020-11-30 15:12:08 +00:00
|
|
|
if ( success ) {
|
2020-08-20 23:37:41 +00:00
|
|
|
createNotice(
|
|
|
|
'success',
|
|
|
|
sprintf(
|
2020-11-25 18:51:15 +00:00
|
|
|
/* translators: %s = name of the product having stock updated */
|
2022-03-30 09:00:04 +00:00
|
|
|
__( '%s stock updated', 'woocommerce' ),
|
2020-08-20 23:37:41 +00:00
|
|
|
product.name
|
2020-11-25 18:51:15 +00:00
|
|
|
),
|
|
|
|
{
|
|
|
|
actions: [
|
|
|
|
{
|
2022-03-30 09:00:04 +00:00
|
|
|
label: __( 'Undo', 'woocommerce' ),
|
2020-11-25 18:51:15 +00:00
|
|
|
onClick: () => {
|
|
|
|
updateProductStock(
|
|
|
|
product,
|
|
|
|
product.stock_quantity
|
|
|
|
);
|
|
|
|
|
|
|
|
this.recordStockEvent( 'undo' );
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
2020-08-20 23:37:41 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
createNotice(
|
|
|
|
'error',
|
|
|
|
sprintf(
|
2020-11-25 18:51:15 +00:00
|
|
|
/* translators: %s = name of the product having stock updated */
|
2022-03-30 09:00:04 +00:00
|
|
|
__( '%s stock could not be updated', 'woocommerce' ),
|
2020-08-20 23:37:41 +00:00
|
|
|
product.name
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-03 16:38:57 +00:00
|
|
|
this.recordStockEvent( 'save', {
|
|
|
|
quantity,
|
|
|
|
} );
|
2019-03-28 17:39:55 +00:00
|
|
|
}
|
|
|
|
|
2019-03-27 17:59:29 +00:00
|
|
|
getActions() {
|
|
|
|
const { editing } = this.state;
|
|
|
|
|
|
|
|
if ( editing ) {
|
|
|
|
return [
|
2020-02-14 02:23:21 +00:00
|
|
|
<Button key="save" type="submit" isPrimary>
|
2022-03-30 09:00:04 +00:00
|
|
|
{ __( 'Save', 'woocommerce' ) }
|
2019-03-28 17:39:55 +00:00
|
|
|
</Button>,
|
2020-02-14 02:23:21 +00:00
|
|
|
<Button key="cancel" type="reset">
|
2022-03-30 09:00:04 +00:00
|
|
|
{ __( 'Cancel', 'woocommerce' ) }
|
2020-02-14 02:23:21 +00:00
|
|
|
</Button>,
|
2019-03-27 17:59:29 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
return [
|
2020-06-11 19:22:20 +00:00
|
|
|
<Button key="update" isSecondary onClick={ this.beginEdit }>
|
2022-03-30 09:00:04 +00:00
|
|
|
{ __( 'Update stock', 'woocommerce' ) }
|
2019-03-27 17:59:29 +00:00
|
|
|
</Button>,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
getBody() {
|
2019-06-27 09:21:43 +00:00
|
|
|
const { product } = this.props;
|
2019-03-27 17:59:29 +00:00
|
|
|
const { editing, quantity } = this.state;
|
|
|
|
|
|
|
|
if ( editing ) {
|
|
|
|
return (
|
|
|
|
<Fragment>
|
2019-04-04 00:05:19 +00:00
|
|
|
<BaseControl className="woocommerce-stock-activity-card__edit-quantity">
|
|
|
|
<input
|
|
|
|
className="components-text-control__input"
|
|
|
|
type="number"
|
|
|
|
value={ quantity }
|
2019-06-18 15:58:10 +00:00
|
|
|
onKeyDown={ this.handleKeyDown }
|
2019-04-04 00:05:19 +00:00
|
|
|
onChange={ this.onQuantityChange }
|
2020-02-14 02:23:21 +00:00
|
|
|
ref={ ( input ) => {
|
2019-04-04 00:05:19 +00:00
|
|
|
this.quantityInput = input;
|
|
|
|
} }
|
|
|
|
/>
|
|
|
|
</BaseControl>
|
2022-03-30 09:00:04 +00:00
|
|
|
<span>{ __( 'in stock', 'woocommerce' ) }</span>
|
2019-03-27 17:59:29 +00:00
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2020-11-25 18:51:15 +00:00
|
|
|
<span
|
2024-05-31 03:49:36 +00:00
|
|
|
className={ clsx(
|
2020-11-25 18:51:15 +00:00
|
|
|
'woocommerce-stock-activity-card__stock-quantity',
|
|
|
|
{
|
|
|
|
'out-of-stock': product.stock_quantity < 1,
|
|
|
|
}
|
|
|
|
) }
|
|
|
|
>
|
2020-02-14 02:23:21 +00:00
|
|
|
{ sprintf(
|
2020-11-25 18:51:15 +00:00
|
|
|
/* translators: %d = stock quantity of the product being updated */
|
2022-03-30 09:00:04 +00:00
|
|
|
__( '%d in stock', 'woocommerce' ),
|
2020-02-14 02:23:21 +00:00
|
|
|
product.stock_quantity
|
|
|
|
) }
|
2019-03-27 17:59:29 +00:00
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-03-27 16:43:31 +00:00
|
|
|
render() {
|
|
|
|
const { product } = this.props;
|
2019-06-27 09:21:43 +00:00
|
|
|
const { edited, editing } = this.state;
|
2022-01-06 12:53:30 +00:00
|
|
|
const notifyLowStockAmount = getAdminSetting(
|
|
|
|
'notifyLowStockAmount',
|
|
|
|
0
|
|
|
|
);
|
2019-06-27 09:21:43 +00:00
|
|
|
const lowStockAmount = Number.isFinite( product.low_stock_amount )
|
|
|
|
? product.low_stock_amount
|
|
|
|
: notifyLowStockAmount;
|
|
|
|
const isLowStock = product.stock_quantity <= lowStockAmount;
|
2020-11-25 18:51:15 +00:00
|
|
|
const lastOrderDate = product.last_order_date
|
|
|
|
? sprintf(
|
|
|
|
/* translators: %s = time since last product order. e.g.: "10 minutes ago" - translated. */
|
2022-03-30 09:00:04 +00:00
|
|
|
__( 'Last ordered %s', 'woocommerce' ),
|
2020-11-25 18:51:15 +00:00
|
|
|
moment.utc( product.last_order_date ).fromNow()
|
|
|
|
)
|
|
|
|
: null;
|
2019-06-27 09:21:43 +00:00
|
|
|
|
|
|
|
// Hide cards that are not in low stock and have not been edited.
|
|
|
|
// This allows clearing cards which are no longer in low stock after
|
|
|
|
// closing & opening the panel without having to make another request.
|
|
|
|
if ( ! isLowStock && ! edited ) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-03-27 16:43:31 +00:00
|
|
|
const title = (
|
2019-04-30 09:43:55 +00:00
|
|
|
<Link
|
2020-02-14 02:23:21 +00:00
|
|
|
href={
|
|
|
|
'post.php?action=edit&post=' +
|
|
|
|
( product.parent_id || product.id )
|
|
|
|
}
|
2020-08-03 16:38:57 +00:00
|
|
|
onClick={ () => this.recordStockEvent( 'product_name' ) }
|
2019-04-30 09:43:55 +00:00
|
|
|
type="wp-admin"
|
|
|
|
>
|
2019-03-27 16:43:31 +00:00
|
|
|
{ product.name }
|
|
|
|
</Link>
|
|
|
|
);
|
2019-04-02 17:37:50 +00:00
|
|
|
let subtitle = null;
|
|
|
|
|
2020-02-14 02:23:21 +00:00
|
|
|
if ( product.type === 'variation' ) {
|
2019-04-02 17:37:50 +00:00
|
|
|
subtitle = Object.values( product.attributes )
|
2020-02-14 02:23:21 +00:00
|
|
|
.map( ( attr ) => attr.option )
|
2019-04-02 17:37:50 +00:00
|
|
|
.join( ', ' );
|
|
|
|
}
|
2019-03-27 16:43:31 +00:00
|
|
|
|
2020-02-14 02:23:21 +00:00
|
|
|
const productImage =
|
|
|
|
get( product, [ 'images', 0 ] ) || get( product, [ 'image' ] );
|
2024-05-31 03:49:36 +00:00
|
|
|
const productImageClasses = clsx(
|
2019-06-12 21:51:08 +00:00
|
|
|
'woocommerce-stock-activity-card__image-overlay__product',
|
|
|
|
{
|
|
|
|
'is-placeholder': ! productImage || ! productImage.src,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
const icon = (
|
|
|
|
<div className="woocommerce-stock-activity-card__image-overlay">
|
|
|
|
<div className={ productImageClasses }>
|
|
|
|
<ProductImage product={ product } />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2024-05-31 03:49:36 +00:00
|
|
|
const activityCardClasses = clsx( 'woocommerce-stock-activity-card', {
|
|
|
|
'is-dimmed': ! editing && ! isLowStock,
|
|
|
|
} );
|
2019-06-12 21:51:08 +00:00
|
|
|
|
2019-06-18 15:58:10 +00:00
|
|
|
const activityCard = (
|
2019-03-27 16:43:31 +00:00
|
|
|
<ActivityCard
|
2019-06-27 09:21:43 +00:00
|
|
|
className={ activityCardClasses }
|
2019-03-27 16:43:31 +00:00
|
|
|
title={ title }
|
2019-04-02 17:37:50 +00:00
|
|
|
subtitle={ subtitle }
|
2019-06-12 21:51:08 +00:00
|
|
|
icon={ icon }
|
2020-11-25 18:51:15 +00:00
|
|
|
date={ lastOrderDate }
|
2019-03-27 17:59:29 +00:00
|
|
|
actions={ this.getActions() }
|
2019-03-27 16:43:31 +00:00
|
|
|
>
|
2019-03-27 17:59:29 +00:00
|
|
|
{ this.getBody() }
|
2019-03-27 16:43:31 +00:00
|
|
|
</ActivityCard>
|
|
|
|
);
|
2019-06-18 15:58:10 +00:00
|
|
|
|
|
|
|
if ( editing ) {
|
|
|
|
return (
|
2019-06-27 09:21:43 +00:00
|
|
|
<form onReset={ this.cancelEdit } onSubmit={ this.onSubmit }>
|
2019-06-18 15:58:10 +00:00
|
|
|
{ activityCard }
|
|
|
|
</form>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return activityCard;
|
2019-03-27 16:43:31 +00:00
|
|
|
}
|
|
|
|
}
|