Fulfillment creation refactor (#1896)

* Create change warehouse dialog (#1850)

* Add allocations & variant stocks to order details query

* Add asc sorting to warehouse search query

* Add OrderChangeWarehouseDialog component

* Add key to warehouse list in dialog

* Update snapshots

* Remove debug directive

* Remove IDs from messages

* Fix typo in method name & extract messages

* Add quantity to allocations in order details query

* Add types to functions

* Move functions to local utils file

* Add utility type WithOptional

* Fix warehouse types

* Change multiple items unavailable message name

* Fix fetching onScroll

* Fix types in utils

* Add backdrop click support

* Add id to stocks and allocations

* Change unavailableLines from .map to .filter

Co-authored-by: Wojciech Mista <wojciech.mista@hotmail.com>

* Fix linter issue

Co-authored-by: Wojciech Mista <wojciech.mista@hotmail.com>

* Refactor order cards headers (#1875)

* Add keys to TableLines

* Bump macaw

* Move & rename CardTitle used in Cards with order lines

* Improve OrderCardTitle typography

* Replace StatusLabels with CircleIndicators

* Fix card title divs placement

* Add warehouse selection button to OrderUnfulfilledCard

* Fix fulfill button placement

* Update snapshots

* Make warehouse in order details view optional so that it works with uncofirmed orders

* Fix undefined lines in warehouse dialog

* Fix spacing in warehouse change button

* Fix macaw dependency

* Bump macaw-ui

* Extract messages

* Implement default warehouse selection logic

* Move CircleIndicator render condition to wrapper

* Fix failing reduce on orders with no lines

* Improve warehousesAvailable early return

* Drop counter in favor of filter().length

* Fix tests post-rebase

* Refactor fulfillment details page (#1915)

* Add shipment information card

* Refactor multiple warehouse fulfilling to one warehouse

* Fix fulfill button navigation

* Remove redundant code from OrderFulfill view

* Fix OrderFulfill story

* Move styling to seperate file & remove unused classes

* Replace colQuantityTotal class with colStock

* Add warehouse label under page header

* Fix preorder cases

* Change default values to maximum

* Simplify logic

* Add badge for preorder & deleted variant cases

* Remove unused data

* Add yellow outline for exceeding stock

* Fix failing tests

* Extract messages

* Fix tests post-rebase

* Add support for tracking number

* Block fulfilling no items

* Fix deleted variant order details bug

* Fix preorder & deleted variant cases

* Update snapshots

* Remove redundant import

* Fix linter issue

* Extract fulfillment lines as separate component

* Fix types

* Export styles & messages to seperate files

* Simplify formset changes

* Fix warning input styling

* Fix shouldEnableSave for overfulfillment cases

* Simplify preorder rendering

* Move empty line rendering

* Change Warehouse prop to just id of it

* Add endAdornment for deleted variant cases

* Update snapshots

* Fix linter issue

* Extract messages

* Fix incorrect operator precedence resulting in NaN values

* Extract fulfillment lines to fragment

* Replace nested types with fragment type

* Match fragment names

* Update snapshots

* Create exceeding stock confirmation dialog (#1970)

* Cherry-pick OrderFulfillStockExceededDialog

* Fix types to graphql-codegen

* Unify names in OrderFulfillStockExceededDialogLines

* Fix types in OrderFulfillStockExceededDialogLines

* Fix types in util

* Change utils for usage with single warehouse context

* Refactor OrderFulfillStockExceededDialogLine for usage with single warehouse context

* Fix deleted variant cases in OrderFulfillStockExceededDialogLine

* Include only exceeded lines

* Display stock exceeded dialog on error

* Add confirm button state

* Change fixed height in OrderFulfillStockExceededDialog to responsive

* Extract messages

* Move initial form data after interfaces

* Change nested type to fragment

* Reuse logic

* Remove unused import

* Remove redundant isStockError

* Remove unused imports

* Fix minor bugs in fulfillment creation refactor (#1972)

* Fix unfulfilled card header quantity calculation

* Fix formset default value for deleted variants

* Update snapshots

* Fix default warehouse selection in order draft (#1971)

* Fix default warehouse selection

* Replace Skeleton with circular progress

* Reuse logic

* Reuse logic

* Apply CR fixes

* Remove unused imports

* Fix canceled order header status

* Update snapshots

* Revert CircularProgress & change to Skeleton

* Change complex types to fragments

* Extract default warehouse logic to hook

* Fix linter issue

* Remove type assertion from OrderFulfillPage story

* Handle exceeding stock fulfillment approvals (#1988)

* wip Add OrderFulfillStockExceeded modal for fulfillment approvals

* wip Fix types & imports

* wip Fix union typing in stock exceeded dialog for both views

* Add allowStockToBeExceeded flag on submit

* Fix lines keys in FulfilledCard

* Remove redundant import

* Extract attributes caption function

* Use getById util

* Fix deleted warehouse cases in approvals

* Fix typo

* Fix styling for long warehouse names (#2019)

* Fix styling in change warehouse dialog

* Fix styling in warehouse selection button

* Add extra margin in button

* Update snapshots

Co-authored-by: Wojciech Mista <wojciech.mista@hotmail.com>
This commit is contained in:
Michał Droń 2022-04-29 11:16:58 +02:00 committed by GitHub
parent e0cc89478f
commit 6cf148c4c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 5536 additions and 2501 deletions

View file

@ -4313,6 +4313,70 @@
"src_dot_orders_dot_components_dot_OrderCannotCancelOrderDialog_dot_775268031": {
"string": "There are still fulfillments created for this order. Cancel the fulfillments first before you cancel the order."
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_canceled": {
"context": "canceled fulfillment, section header",
"string": "Canceled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_fulfilled": {
"context": "section header",
"string": "Fulfilled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_fulfilledFrom": {
"context": "fulfilled fulfillment, section header",
"string": "Fulfilled from {warehouseName}"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_refunded": {
"context": "refunded fulfillment, section header",
"string": "Refunded ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_refundedAndReturned": {
"context": "cancelled fulfillment, section header",
"string": "Refunded and Returned ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_replaced": {
"context": "refunded fulfillment, section header",
"string": "Replaced ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_returned": {
"context": "refunded fulfillment, section header",
"string": "Returned ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_unfulfilled": {
"context": "section header",
"string": "Unfulfilled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderCardTitle_dot_waitingForApproval": {
"context": "unapproved fulfillment, section header",
"string": "Waiting for approval ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderChangeWarehouseDialog_dot_currentSelection": {
"context": "label for currently selected warehouse",
"string": "currently selected"
},
"src_dot_orders_dot_components_dot_OrderChangeWarehouseDialog_dot_dialogDescription": {
"context": "change warehouse dialog description",
"string": "Choose warehouse you want to fulfill this order from"
},
"src_dot_orders_dot_components_dot_OrderChangeWarehouseDialog_dot_dialogTitle": {
"context": "change warehouse dialog title",
"string": "Change warehouse"
},
"src_dot_orders_dot_components_dot_OrderChangeWarehouseDialog_dot_multipleProductsUnavailable": {
"context": "warehouse label when multiple products are unavailable",
"string": "{productCount} products are unavailable at this location"
},
"src_dot_orders_dot_components_dot_OrderChangeWarehouseDialog_dot_productUnavailable": {
"context": "warehouse label when one product is unavailable",
"string": "{productName} is unavailable at this location"
},
"src_dot_orders_dot_components_dot_OrderChangeWarehouseDialog_dot_searchFieldPlaceholder": {
"context": "change warehouse dialog search placeholder",
"string": "Search warehouses"
},
"src_dot_orders_dot_components_dot_OrderChangeWarehouseDialog_dot_warehouseListLabel": {
"context": "change warehouse dialog warehouse list label",
"string": "Warehouses A to Z"
},
"src_dot_orders_dot_components_dot_OrderChannelSectionCard_dot_1243773440": {
"context": "section header",
"string": "Sales channel"
@ -4600,6 +4664,18 @@
"context": "button",
"string": "Finalize"
},
"src_dot_orders_dot_components_dot_OrderFulfillLine_dot_deletedVariantWarning": {
"context": "tooltip content when line's variant has been deleted",
"string": "This variant no longer exists. You can still fulfill it."
},
"src_dot_orders_dot_components_dot_OrderFulfillLine_dot_preorderWarning": {
"context": "tooltip content when product is in preorder",
"string": "This product is still in preorder. You will be able to fulfill it after it reaches its release date"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_fulfillingFrom": {
"context": "Support text under page header",
"string": "Fulfilling from {warehouseName}"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_headerOrder": {
"context": "page header",
"string": "Order"
@ -4624,6 +4700,10 @@
"context": "name",
"string": "Product name"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_quantity": {
"context": "Header row quantity label",
"string": "Quantity"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_quantityToFulfill": {
"context": "quantity of fulfilled products",
"string": "Quantity to fulfill"
@ -4632,10 +4712,18 @@
"context": "checkbox label",
"string": "Send shipment details to customer"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_shipmentInformation": {
"context": "Shipment information card header",
"string": "Shipment information"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_sku": {
"context": "product's sku",
"string": "SKU"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_stock": {
"context": "Header row stock label",
"string": "Stock"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_submitFulfillment": {
"context": "fulfill order, button",
"string": "Fulfill"
@ -4644,6 +4732,46 @@
"context": "prepare order fulfillment, button",
"string": "Prepare fulfillment"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_trackingNumber": {
"context": "Tracking number input label",
"string": "Tracking number"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_availableStockLabel": {
"context": "table header available stock label",
"string": "Available"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_cancelButton": {
"context": "cancel button label",
"string": "Cancel"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_fulfillButton": {
"context": "fulfill button label",
"string": "Fulfill anyway"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_infoLabel": {
"context": "stock exceeded dialog description",
"string": "Stock for items shown below are not enough to prepare fulfillment:"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_productLabel": {
"context": "table header product label",
"string": "Product"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_questionLabel": {
"context": "stock exceeded action question label",
"string": "Are you sure you want to fulfill those products anyway?"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_requiredStockLabel": {
"context": "table header required stock label",
"string": "Required"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_title": {
"context": "stock exceeded dialog title",
"string": "Not enough stock"
},
"src_dot_orders_dot_components_dot_OrderFulfillStockExceededDialog_dot_warehouseStockLabel": {
"context": "table header warehouse stock label",
"string": "Warehouse stock"
},
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_addTracking": {
"context": "add tracking button",
"string": "Add tracking"
@ -5345,42 +5473,14 @@
"context": "button",
"string": "Set maximal quantities"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_cancelled": {
"context": "cancelled fulfillment, section header",
"string": "Cancelled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_description": {
"context": "product no longer exists error description",
"string": "This product is no longer in database so it cant be replaced, nor returned"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_fulfilled": {
"context": "section header",
"string": "Fulfilled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_fulfilledFrom": {
"context": "fulfilled fulfillment, section header",
"string": "Fulfilled from {warehouseName}"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_improperValue": {
"context": "error message",
"string": "Improper value"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refunded": {
"context": "refunded fulfillment, section header",
"string": "Refunded ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refundedAndReturned": {
"context": "cancelled fulfillment, section header",
"string": "Refunded and Returned ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_replaced": {
"context": "refunded fulfillment, section header",
"string": "Replaced ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_returned": {
"context": "refunded fulfillment, section header",
"string": "Returned ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_title": {
"context": "product no longer exists error title",
"string": "Product no longer exists"
@ -5393,14 +5493,6 @@
"context": "section header",
"string": "Unfulfilled Items"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_unfulfilled": {
"context": "section header",
"string": "Unfulfilled"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_waitingForApproval": {
"context": "unapproved fulfillment, section header",
"string": "Waiting for approval ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_appTitle": {
"context": "page header with order number",
"string": "Order #{orderNumber}"

View file

@ -74,12 +74,22 @@ export const fragmentOrderLine = gql`
fragment OrderLine on OrderLine {
id
isShippingRequired
allocations {
id
quantity
warehouse {
id
}
}
variant {
id
quantityAvailable
preorder {
endDate
}
stocks {
...Stock
}
}
productName
productSku
@ -324,3 +334,60 @@ export const fragmentShopOrderSettings = gql`
fulfillmentAllowUnpaid
}
`;
export const fragmentOrderFulfillLine = gql`
fragment OrderFulfillLine on OrderLine {
id
isShippingRequired
productName
quantity
allocations {
quantity
warehouse {
id
}
}
quantityFulfilled
quantityToFulfill
variant {
id
name
sku
preorder {
endDate
}
attributes {
values {
id
name
}
}
stocks {
...Stock
}
trackInventory
}
thumbnail(size: 64) {
url
}
}
`;
export const fragmentOrderLineStockData = gql`
fragment OrderLineStockData on OrderLine {
id
allocations {
quantity
warehouse {
id
}
}
quantity
quantityToFulfill
variant {
stocks {
...Stock
}
}
}
`;

View file

@ -6,8 +6,7 @@ export const stockFragment = gql`
quantity
quantityAllocated
warehouse {
id
name
...Warehouse
}
}
`;

View file

@ -1182,16 +1182,42 @@ export const OrderEventFragmentDoc = gql`
}
}
`;
export const WarehouseFragmentDoc = gql`
fragment Warehouse on Warehouse {
id
name
}
`;
export const StockFragmentDoc = gql`
fragment Stock on Stock {
id
quantity
quantityAllocated
warehouse {
...Warehouse
}
}
${WarehouseFragmentDoc}`;
export const OrderLineFragmentDoc = gql`
fragment OrderLine on OrderLine {
id
isShippingRequired
allocations {
id
quantity
warehouse {
id
}
}
variant {
id
quantityAvailable
preorder {
endDate
}
stocks {
...Stock
}
}
productName
productSku
@ -1230,7 +1256,7 @@ export const OrderLineFragmentDoc = gql`
url
}
}
`;
${StockFragmentDoc}`;
export const FulfillmentFragmentDoc = gql`
fragment Fulfillment on Fulfillment {
id
@ -1421,6 +1447,61 @@ export const ShopOrderSettingsFragmentDoc = gql`
fulfillmentAllowUnpaid
}
`;
export const OrderFulfillLineFragmentDoc = gql`
fragment OrderFulfillLine on OrderLine {
id
isShippingRequired
productName
quantity
allocations {
quantity
warehouse {
id
}
}
quantityFulfilled
quantityToFulfill
variant {
id
name
sku
preorder {
endDate
}
attributes {
values {
id
name
}
}
stocks {
...Stock
}
trackInventory
}
thumbnail(size: 64) {
url
}
}
${StockFragmentDoc}`;
export const OrderLineStockDataFragmentDoc = gql`
fragment OrderLineStockData on OrderLine {
id
allocations {
quantity
warehouse {
id
}
}
quantity
quantityToFulfill
variant {
stocks {
...Stock
}
}
}
${StockFragmentDoc}`;
export const PageTypeFragmentDoc = gql`
fragment PageType on PageType {
id
@ -1803,17 +1884,6 @@ export const ProductMediaFragmentDoc = gql`
oembedData
}
`;
export const StockFragmentDoc = gql`
fragment Stock on Stock {
id
quantity
quantityAllocated
warehouse {
id
name
}
}
`;
export const PreorderFragmentDoc = gql`
fragment Preorder on PreorderData {
globalThreshold
@ -2566,12 +2636,6 @@ export const AttributeValueTranslatableContentFragmentDoc = gql`
}
}
${AttributeChoicesTranslationFragmentDoc}`;
export const WarehouseFragmentDoc = gql`
fragment Warehouse on Warehouse {
id
name
}
`;
export const WarehouseWithShippingFragmentDoc = gql`
fragment WarehouseWithShipping on Warehouse {
...Warehouse
@ -8334,8 +8398,12 @@ export type OrderFulfillmentUpdateTrackingMutationHookResult = ReturnType<typeof
export type OrderFulfillmentUpdateTrackingMutationResult = Apollo.MutationResult<Types.OrderFulfillmentUpdateTrackingMutation>;
export type OrderFulfillmentUpdateTrackingMutationOptions = Apollo.BaseMutationOptions<Types.OrderFulfillmentUpdateTrackingMutation, Types.OrderFulfillmentUpdateTrackingMutationVariables>;
export const OrderFulfillmentApproveDocument = gql`
mutation OrderFulfillmentApprove($id: ID!, $notifyCustomer: Boolean!) {
orderFulfillmentApprove(id: $id, notifyCustomer: $notifyCustomer) {
mutation OrderFulfillmentApprove($id: ID!, $notifyCustomer: Boolean!, $allowStockToBeExceeded: Boolean) {
orderFulfillmentApprove(
id: $id
notifyCustomer: $notifyCustomer
allowStockToBeExceeded: $allowStockToBeExceeded
) {
errors {
...OrderError
}
@ -8363,6 +8431,7 @@ export type OrderFulfillmentApproveMutationFn = Apollo.MutationFunction<Types.Or
* variables: {
* id: // value for 'id'
* notifyCustomer: // value for 'notifyCustomer'
* allowStockToBeExceeded: // value for 'allowStockToBeExceeded'
* },
* });
*/
@ -9147,49 +9216,12 @@ export const OrderFulfillDataDocument = gql`
}
}
lines {
id
isShippingRequired
productName
quantity
allocations {
quantity
warehouse {
id
}
}
quantityFulfilled
quantityToFulfill
variant {
id
name
sku
preorder {
endDate
}
attributes {
values {
id
name
}
}
stocks {
id
warehouse {
...Warehouse
}
quantity
quantityAllocated
}
trackInventory
}
thumbnail(size: 64) {
url
}
...OrderFulfillLine
}
number
}
}
${WarehouseFragmentDoc}`;
${OrderFulfillLineFragmentDoc}`;
/**
* __useOrderFulfillDataQuery__
@ -13441,7 +13473,12 @@ export type SearchStaffMembersLazyQueryHookResult = ReturnType<typeof useSearchS
export type SearchStaffMembersQueryResult = Apollo.QueryResult<Types.SearchStaffMembersQuery, Types.SearchStaffMembersQueryVariables>;
export const SearchWarehousesDocument = gql`
query SearchWarehouses($after: String, $first: Int!, $query: String!) {
search: warehouses(after: $after, first: $first, filter: {search: $query}) {
search: warehouses(
after: $after
first: $first
sortBy: {direction: ASC, field: NAME}
filter: {search: $query}
) {
edges {
node {
id

File diff suppressed because one or more lines are too long

View file

@ -23,7 +23,7 @@ export { useLazyQuery, LazyQueryHookOptions } from "@apollo/client";
const getPermissionKey = (permission: string) =>
`PERMISSION_${permission}` as PrefixedPermissions;
const allPermissions: Record<PrefixedPermissions, boolean> = Object.keys(
export const allPermissions: Record<PrefixedPermissions, boolean> = Object.keys(
PermissionEnum
).reduce(
(prev, code) => ({

View file

@ -514,3 +514,6 @@ export const combinedMultiAutocompleteChoices = (
export const isInDevelopment =
!process.env.NODE_ENV || process.env.NODE_ENV === "development";
export type WithOptional<T, K extends keyof T> = Omit<T, K> &
Partial<Pick<T, K>>;

View file

@ -1,7 +1,8 @@
import { Typography } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import DefaultCardTitle from "@saleor/components/CardTitle";
import { FulfillmentStatus } from "@saleor/graphql";
import { makeStyles, Pill } from "@saleor/macaw-ui";
import { CircleIndicator, makeStyles } from "@saleor/macaw-ui";
import { StatusType } from "@saleor/types";
import camelCase from "lodash/camelCase";
import React from "react";
@ -12,7 +13,7 @@ const useStyles = makeStyles(
title: {
width: "100%",
display: "flex",
justifyContent: "space-between"
justifyContent: "flex-start"
},
orderNumber: {
display: "inline",
@ -23,15 +24,26 @@ const useStyles = makeStyles(
alignSelf: "center",
color: theme.palette.text.secondary,
margin: `auto ${theme.spacing(1)} auto auto`
},
cardHeader: {
fontSize: "24px",
fontWeight: 500,
lineHeight: "29px",
letterSpacing: "0.02em",
textAlign: "left"
},
indicator: {
display: "flex",
alignItems: "center"
}
}),
{ name: "CardTitle" }
{ name: "OrderCardTitle" }
);
const messages = defineMessages({
cancelled: {
defaultMessage: "Cancelled ({quantity})",
description: "cancelled fulfillment, section header"
canceled: {
defaultMessage: "Canceled ({quantity})",
description: "canceled fulfillment, section header"
},
fulfilled: {
defaultMessage: "Fulfilled ({quantity})",
@ -58,7 +70,7 @@ const messages = defineMessages({
description: "unapproved fulfillment, section header"
},
unfulfilled: {
defaultMessage: "Unfulfilled",
defaultMessage: "Unfulfilled ({quantity})",
description: "section header"
},
fulfilledFrom: {
@ -71,9 +83,10 @@ type CardTitleStatus = FulfillmentStatus | "unfulfilled";
type CardTitleLines = Array<{
quantity: number;
quantityToFulfill?: number;
}>;
interface CardTitleProps {
interface OrderCardTitleProps {
lines?: CardTitleLines;
fulfillmentOrder?: number;
status: CardTitleStatus;
@ -81,6 +94,7 @@ interface CardTitleProps {
orderNumber?: string;
warehouseName?: string;
withStatus?: boolean;
className?: string;
}
const selectStatus = (status: CardTitleStatus) => {
@ -100,18 +114,19 @@ const selectStatus = (status: CardTitleStatus) => {
case FulfillmentStatus.CANCELED:
return StatusType.ERROR;
default:
return StatusType.WARNING;
return StatusType.ERROR;
}
};
const CardTitle: React.FC<CardTitleProps> = ({
const OrderCardTitle: React.FC<OrderCardTitleProps> = ({
lines = [],
fulfillmentOrder,
status,
orderNumber = "",
warehouseName,
withStatus = false,
toolbar
toolbar,
className
}) => {
const intl = useIntl();
const classes = useStyles({});
@ -123,35 +138,36 @@ const CardTitle: React.FC<CardTitleProps> = ({
const messageForStatus = messages[camelCase(status)] || messages.unfulfilled;
const totalQuantity = lines.reduce(
(resultQuantity, { quantity }) => resultQuantity + quantity,
0
);
const title = (
<>
{intl.formatMessage(messageForStatus, {
fulfillmentName,
quantity: totalQuantity
})}
{fulfillmentName && (
<Typography className={classes.orderNumber} variant="body1">
{fulfillmentName}
</Typography>
)}
</>
);
const totalQuantity =
status === "unfulfilled"
? lines.reduce(
(resultQuantity, line) =>
resultQuantity + (line.quantityToFulfill ?? line.quantity),
0
)
: lines.reduce(
(resultQuantity, { quantity }) => resultQuantity + quantity,
0
);
return (
<DefaultCardTitle
toolbar={toolbar}
className={className}
title={
<div className={classes.title}>
{withStatus ? (
<Pill label={title} color={selectStatus(status)} />
) : (
title
{withStatus && (
<div className={classes.indicator}>
<CircleIndicator color={selectStatus(status)} />
</div>
)}
<HorizontalSpacer spacing={2} />
<Typography className={classes.cardHeader}>
{intl.formatMessage(messageForStatus, {
fulfillmentName,
quantity: totalQuantity
})}
</Typography>
{!!warehouseName && (
<Typography className={classes.warehouseName} variant="caption">
<FormattedMessage
@ -168,4 +184,5 @@ const CardTitle: React.FC<CardTitleProps> = ({
);
};
export default CardTitle;
OrderCardTitle.displayName = "OrderCardTitle";
export default OrderCardTitle;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderCardTitle";
export * from "./OrderCardTitle";

View file

@ -0,0 +1,45 @@
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
import { allPermissions } from "@saleor/hooks/makeQuery";
import { order, warehouseSearch } from "@saleor/orders/fixtures";
import { searchWarehouses } from "@saleor/searches/useWarehouseSearch";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import OrderChangeWarehouseDialog, { OrderChangeWarehouseDialogProps } from ".";
const props: OrderChangeWarehouseDialogProps = {
open: true,
lines: order("abc").lines,
currentWarehouse: null,
onConfirm: () => null,
onClose: () => null
};
const mocks: MockedResponse[] = [
{
request: {
query: searchWarehouses,
variables: {
first: 20,
after: null,
query: "",
...allPermissions
}
},
result: {
data: { search: warehouseSearch }
}
}
];
storiesOf(
"Orders / Order details fulfillment warehouse selection modal",
module
)
.addDecorator(Decorator)
.add("default", () => (
<MockedProvider mocks={mocks}>
<OrderChangeWarehouseDialog {...props} />
</MockedProvider>
));

View file

@ -0,0 +1,213 @@
import {
Dialog,
DialogActions,
DialogContent,
FormControlLabel,
InputAdornment,
Radio,
RadioGroup,
TableCell,
TableRow,
TextField,
Typography
} from "@material-ui/core";
import Debounce from "@saleor/components/Debounce";
import Skeleton from "@saleor/components/Skeleton";
import { OrderLineFragment, WarehouseFragment } from "@saleor/graphql";
import { buttonMessages } from "@saleor/intl";
import {
Button,
DialogHeader,
DialogTable,
isScrolledToBottom,
isScrolledToTop,
ScrollShadow,
SearchIcon,
useElementScroll
} from "@saleor/macaw-ui";
import { isLineAvailableInWarehouse } from "@saleor/orders/utils/data";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { getById } from "../OrderReturnPage/utils";
import { changeWarehouseDialogMessages as messages } from "./messages";
import { useStyles } from "./styles";
export interface OrderChangeWarehouseDialogProps {
open: boolean;
lines: OrderLineFragment[];
currentWarehouse: WarehouseFragment;
onConfirm: (warehouse: WarehouseFragment) => void;
onClose();
}
export const OrderChangeWarehouseDialog: React.FC<OrderChangeWarehouseDialogProps> = ({
open,
lines,
currentWarehouse,
onConfirm,
onClose
}) => {
const classes = useStyles();
const intl = useIntl();
const { anchor, position, setAnchor } = useElementScroll();
const topShadow = isScrolledToTop(anchor, position, 20) === false;
const bottomShadow = isScrolledToBottom(anchor, position, 20) === false;
const [query, setQuery] = React.useState<string>("");
const [selectedWarehouseId, setSelectedWarehouseId] = React.useState<
string | null
>(null);
React.useEffect(() => {
if (currentWarehouse?.id) {
setSelectedWarehouseId(currentWarehouse.id);
}
}, [currentWarehouse]);
const { result: warehousesOpts, loadMore, search } = useWarehouseSearch({
variables: {
after: null,
first: 20,
query: ""
}
});
const filteredWarehouses = mapEdgesToItems(warehousesOpts?.data?.search);
const selectedWarehouse = filteredWarehouses?.find(
getById(selectedWarehouseId ?? "")
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedWarehouseId(e.target.value);
};
const handleSubmit = () => {
onConfirm(selectedWarehouse);
onClose();
};
React.useEffect(() => {
if (!bottomShadow) {
loadMore();
}
}, [bottomShadow]);
return (
<Dialog fullWidth open={open} onClose={onClose}>
<ScrollShadow variant="top" show={topShadow}>
<DialogHeader onClose={onClose}>
<FormattedMessage {...messages.dialogTitle} />
</DialogHeader>
<DialogContent className={classes.container}>
<FormattedMessage {...messages.dialogDescription} />
<Debounce debounceFn={search}>
{debounceSearchChange => {
const handleSearchChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const value = event.target.value;
setQuery(value);
debounceSearchChange(value);
};
return (
<TextField
className={classes.searchBox}
value={query}
variant="outlined"
onChange={handleSearchChange}
placeholder={intl.formatMessage(
messages.searchFieldPlaceholder
)}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
inputProps={{ className: classes.searchInput }}
/>
);
}}
</Debounce>
<Typography className={classes.supportHeader}>
<FormattedMessage {...messages.warehouseListLabel} />
</Typography>
</DialogContent>
</ScrollShadow>
<DialogTable ref={setAnchor}>
{filteredWarehouses ? (
<RadioGroup value={selectedWarehouseId} onChange={handleChange}>
{filteredWarehouses.map(warehouse => {
const unavailableLines = lines?.filter(
line => !isLineAvailableInWarehouse(line, warehouse)
);
const someLinesUnavailable = unavailableLines?.length > 0;
return (
<TableRow key={warehouse.id}>
<TableCell>
<FormControlLabel
value={warehouse.id}
control={<Radio color="primary" />}
label={
<div className={classes.radioLabelContainer}>
<span className={classes.warehouseName}>
{warehouse.name}
</span>
{someLinesUnavailable && (
<Typography className={classes.supportText}>
{unavailableLines.length === 1
? intl.formatMessage(
messages.productUnavailable,
{
productName:
unavailableLines[0].productName
}
)
: intl.formatMessage(
messages.multipleProductsUnavailable,
{ productCount: unavailableLines.length }
)}
</Typography>
)}
</div>
}
/>
{currentWarehouse?.id === warehouse?.id && (
<Typography className={classes.helpText}>
{intl.formatMessage(messages.currentSelection)}
</Typography>
)}
</TableCell>
</TableRow>
);
})}
</RadioGroup>
) : (
<Skeleton />
)}
</DialogTable>
<ScrollShadow variant="bottom" show={bottomShadow}>
<DialogActions>
<Button
onClick={handleSubmit}
color="primary"
variant="primary"
disabled={!selectedWarehouse}
>
{intl.formatMessage(buttonMessages.select)}
</Button>
</DialogActions>
</ScrollShadow>
</Dialog>
);
};
OrderChangeWarehouseDialog.displayName = "OrderChangeWarehouseDialog";
export default OrderChangeWarehouseDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderChangeWarehouseDialog";
export * from "./OrderChangeWarehouseDialog";

View file

@ -0,0 +1,32 @@
import { defineMessages } from "react-intl";
export const changeWarehouseDialogMessages = defineMessages({
dialogTitle: {
defaultMessage: "Change warehouse",
description: "change warehouse dialog title"
},
dialogDescription: {
defaultMessage: "Choose warehouse you want to fulfill this order from",
description: "change warehouse dialog description"
},
searchFieldPlaceholder: {
defaultMessage: "Search warehouses",
description: "change warehouse dialog search placeholder"
},
warehouseListLabel: {
defaultMessage: "Warehouses A to Z",
description: "change warehouse dialog warehouse list label"
},
productUnavailable: {
defaultMessage: "{productName} is unavailable at this location",
description: "warehouse label when one product is unavailable"
},
multipleProductsUnavailable: {
defaultMessage: "{productCount} products are unavailable at this location",
description: "warehouse label when multiple products are unavailable"
},
currentSelection: {
defaultMessage: "currently selected",
description: "label for currently selected warehouse"
}
});

View file

@ -0,0 +1,46 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
container: {
paddingTop: 0
},
searchBox: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2)
},
searchInput: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
supportHeader: {
textTransform: "uppercase",
color: theme.palette.saleor.main[3],
fontWeight: 500,
letterSpacing: "0.1em",
fontSize: "12px",
lineHeight: "160%"
},
helpText: {
display: "inline",
fontSize: "12px",
lineHeight: "160%",
color: theme.palette.saleor.main[3]
},
supportText: {
fontSize: "14px",
lineHeight: "160%",
color: theme.palette.saleor.main[3]
},
radioLabelContainer: {
display: "flex",
flexDirection: "column"
},
warehouseName: {
maxWidth: "350px",
overflow: "hidden",
textOverflow: "ellipsis"
}
}),
{ name: "OrderChangeWarehouseDialog" }
);

View file

@ -12,7 +12,8 @@ import Skeleton from "@saleor/components/Skeleton";
import {
OrderDetailsFragment,
OrderDetailsQuery,
OrderStatus
OrderStatus,
WarehouseFragment
} from "@saleor/graphql";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
@ -65,6 +66,7 @@ export interface OrderDetailsPageProps {
}>;
disabled: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
selectedWarehouse?: WarehouseFragment;
onOrderLineAdd?: () => void;
onOrderLineChange?: (
id: string,
@ -91,6 +93,7 @@ export interface OrderDetailsPageProps {
onInvoiceClick(invoiceId: string);
onInvoiceGenerate();
onInvoiceSend(invoiceId: string);
onWarehouseChange?();
onSubmit(data: MetadataFormData): SubmitPromise;
}
@ -115,6 +118,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
order,
shop,
saveButtonBarState,
selectedWarehouse,
onBack,
onBillingAddressEdit,
onFulfillmentApprove,
@ -137,6 +141,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
onOrderLineChange,
onOrderLineRemove,
onShippingMethodEdit,
onWarehouseChange,
onSubmit
} = props;
const classes = useStyles(props);
@ -243,6 +248,8 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid}
lines={unfulfilled}
onFulfill={onOrderFulfill}
onWarehouseChange={onWarehouseChange}
selectedWarehouse={selectedWarehouse}
/>
) : (
<>

View file

@ -0,0 +1,148 @@
import { TableCell, TableRow, TextField, Typography } from "@material-ui/core";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import {
OrderFulfillLineFragment,
OrderFulfillStockInput
} from "@saleor/graphql";
import { FormsetChange, FormsetData } from "@saleor/hooks/useFormset";
import { Tooltip, WarningIcon } from "@saleor/macaw-ui";
import {
getAttributesCaption,
getOrderLineAvailableQuantity,
getWarehouseStock
} from "@saleor/orders/utils/data";
import classNames from "classnames";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "./messages";
import { useStyles } from "./styles";
interface OrderFulfillLineProps {
line: OrderFulfillLineFragment;
lineIndex: number;
warehouseId: string;
formsetData: FormsetData<null, OrderFulfillStockInput[]>;
formsetChange: FormsetChange<OrderFulfillStockInput[]>;
}
export const OrderFulfillLine: React.FC<OrderFulfillLineProps> = props => {
const { line, lineIndex, warehouseId, formsetData, formsetChange } = props;
const classes = useStyles();
const intl = useIntl();
const isDeletedVariant = !line?.variant;
const isPreorder = !!line.variant?.preorder;
const lineFormQuantity = isPreorder
? 0
: formsetData[lineIndex].value?.[0]?.quantity;
const overfulfill = lineFormQuantity > line.quantityToFulfill;
const warehouseStock = getWarehouseStock(line?.variant?.stocks, warehouseId);
const availableQuantity = getOrderLineAvailableQuantity(line, warehouseStock);
const isStockExceeded = lineFormQuantity > availableQuantity;
if (!line) {
return (
<TableRow key={lineIndex}>
<TableCellAvatar className={classes.colName}>
<Skeleton />
</TableCellAvatar>
<TableCell className={classes.colSku}>
<Skeleton />
</TableCell>
<TableCell className={classes.colQuantity}>
<Skeleton />
</TableCell>
<TableCell className={classes.colStock}>
<Skeleton />
</TableCell>
</TableRow>
);
}
return (
<TableRow key={line.id}>
<TableCellAvatar
className={classes.colName}
thumbnail={line?.thumbnail?.url}
badge={
isPreorder || !line?.variant ? (
<Tooltip
variant="warning"
title={intl.formatMessage(
isPreorder
? messages.preorderWarning
: messages.deletedVariantWarning
)}
>
<div className={classes.warningIcon}>
<WarningIcon />
</div>
</Tooltip>
) : (
undefined
)
}
>
{line.productName}
<Typography color="textSecondary" variant="caption">
{getAttributesCaption(line.variant?.attributes)}
</Typography>
</TableCellAvatar>
<TableCell className={classes.colSku}>{line.variant?.sku}</TableCell>
{isPreorder ? (
<TableCell className={classes.colQuantity} />
) : (
<TableCell
className={classes.colQuantity}
key={warehouseStock?.id ?? "deletedVariant" + lineIndex}
>
<TextField
type="number"
inputProps={{
className: classNames(classes.quantityInnerInput, {
[classes.quantityInnerInputNoRemaining]: !line.variant
?.trackInventory
}),
min: 0,
style: { textAlign: "right" }
}}
fullWidth
value={lineFormQuantity}
onChange={event =>
formsetChange(line.id, [
{
quantity: parseInt(event.target.value, 10),
warehouse: warehouseId
}
])
}
error={overfulfill}
variant="outlined"
InputProps={{
classes: {
...(isStockExceeded &&
!overfulfill && {
notchedOutline: classes.warning
})
},
endAdornment: (
<div className={classes.remainingQuantity}>
/ {line.quantityToFulfill}
</div>
)
}}
/>
</TableCell>
)}
<TableCell className={classes.colStock} key="total">
{isPreorder || isDeletedVariant ? undefined : availableQuantity}
</TableCell>
</TableRow>
);
};
OrderFulfillLine.displayName = "OrderFulfillLine";
export default OrderFulfillLine;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderFulfillLine";
export * from "./OrderFulfillLine";

View file

@ -0,0 +1,13 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
preorderWarning: {
defaultMessage:
"This product is still in preorder. You will be able to fulfill it after it reaches its release date",
description: "tooltip content when product is in preorder"
},
deletedVariantWarning: {
defaultMessage: "This variant no longer exists. You can still fulfill it.",
description: "tooltip content when line's variant has been deleted"
}
});

View file

@ -0,0 +1,47 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
colStock: {
textAlign: "right",
width: 180
},
colName: {
width: 250
},
colQuantity: {
textAlign: "right",
width: 210
},
colSku: {
textAlign: "right",
textOverflow: "ellipsis",
width: 150
},
warningIcon: {
color: theme.palette.saleor.warning.mid,
marginRight: theme.spacing(2)
},
error: {
color: theme.palette.error.main
},
warning: {
borderColor: theme.palette.saleor.warning.dark + " !important",
boxShadow: `0 0 0 3px ${theme.palette.saleor.warning.light}`
},
quantityInnerInput: {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2)
},
quantityInnerInputNoRemaining: {
paddingRight: 0
},
remainingQuantity: {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2),
color: theme.palette.text.secondary,
whiteSpace: "nowrap"
}
}),
{ name: "OrderFulfillLine" }
);

View file

@ -14,7 +14,7 @@ const props: OrderFulfillPageProps = {
onSubmit: () => undefined,
order: orderToFulfill,
saveButtonBar: "default",
warehouses: warehouseList
warehouse: warehouseList[0]
};
storiesOf("Views / Orders / Fulfill order", module)
@ -25,7 +25,7 @@ storiesOf("Views / Orders / Fulfill order", module)
{...props}
loading={true}
order={undefined}
warehouses={undefined}
warehouse={undefined}
/>
))
.add("error", () => (
@ -44,6 +44,4 @@ storiesOf("Views / Orders / Fulfill order", module)
]}
/>
))
.add("one warehouse", () => (
<OrderFulfillPage {...props} warehouses={warehouseList.slice(0, 1)} />
));
.add("one warehouse", () => <OrderFulfillPage {...props} />);

View file

@ -1,6 +1,5 @@
import {
Card,
CardActions,
TableBody,
TableCell,
TableHead,
@ -8,19 +7,20 @@ import {
TextField,
Typography
} from "@material-ui/core";
import { CSSProperties } from "@material-ui/styles";
import CardTitle from "@saleor/components/CardTitle";
import Container from "@saleor/components/Container";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form";
import { Grid } from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Savebar from "@saleor/components/Savebar";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import {
FulfillOrderMutation,
OrderErrorCode,
OrderFulfillDataQuery,
OrderFulfillLineFragment,
OrderFulfillStockInput,
ShopOrderSettingsFragment,
WarehouseFragment
@ -28,91 +28,25 @@ import {
import { SubmitPromise } from "@saleor/hooks/useForm";
import useFormset, { FormsetData } from "@saleor/hooks/useFormset";
import { commonMessages } from "@saleor/intl";
import {
Backlink,
ConfirmButtonTransitionState,
makeStyles
} from "@saleor/macaw-ui";
import { Backlink, ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { renderCollection } from "@saleor/misc";
import {
getToFulfillOrderLines,
isStockError
getAttributesCaption,
getToFulfillOrderLines
} from "@saleor/orders/utils/data";
import { update } from "@saleor/utils/lists";
import classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import OrderFulfillLine from "../OrderFulfillLine/OrderFulfillLine";
import OrderFulfillStockExceededDialog from "../OrderFulfillStockExceededDialog";
import { messages } from "./messages";
const useStyles = makeStyles(
theme => {
const inputPadding: CSSProperties = {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2)
};
return {
actionBar: {
flexDirection: "row",
padding: theme.spacing(1, 4)
},
colName: {
width: 250,
[theme.breakpoints.up("lg")]: {
width: ({ warehouses }: OrderFulfillPageProps) =>
warehouses?.length > 3 ? 250 : "auto"
},
[theme.breakpoints.only("md")]: {
width: ({ warehouses }: OrderFulfillPageProps) =>
warehouses?.length > 2 ? 250 : "auto"
}
},
colQuantity: {
textAlign: "right",
width: 210
},
colQuantityHeader: {
textAlign: "right"
},
colQuantityTotal: {
textAlign: "right",
width: 180
},
colSku: {
textAlign: "right",
textOverflow: "ellipsis",
width: 150
},
error: {
color: theme.palette.error.main
},
full: {
fontWeight: 600
},
quantityInnerInput: {
...inputPadding
},
quantityInnerInputNoRemaining: {
paddingRight: 0
},
remainingQuantity: {
...inputPadding,
color: theme.palette.text.secondary,
whiteSpace: "nowrap"
},
table: {
"&&": {
tableLayout: "fixed"
}
}
};
},
{ name: "OrderFulfillPage" }
);
import { useStyles } from "./styles";
interface OrderFulfillFormData {
sendInfo: boolean;
trackingNumber: string;
allowStockToBeExceeded: boolean;
}
export interface OrderFulfillSubmitData extends OrderFulfillFormData {
items: FormsetData<null, OrderFulfillStockInput[]>;
@ -122,14 +56,16 @@ export interface OrderFulfillPageProps {
errors: FulfillOrderMutation["orderFulfill"]["errors"];
order: OrderFulfillDataQuery["order"];
saveButtonBar: ConfirmButtonTransitionState;
warehouses: WarehouseFragment[];
warehouse: WarehouseFragment;
shopSettings?: ShopOrderSettingsFragment;
onBack: () => void;
onSubmit: (data: OrderFulfillSubmitData) => SubmitPromise;
}
const initialFormData: OrderFulfillFormData = {
sendInfo: true
sendInfo: true,
trackingNumber: "",
allowStockToBeExceeded: false
};
const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
@ -138,7 +74,7 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
errors,
order,
saveButtonBar,
warehouses,
warehouse,
shopSettings,
onBack,
onSubmit
@ -151,28 +87,50 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
null,
OrderFulfillStockInput[]
>(
getToFulfillOrderLines(order?.lines).map(line => ({
data: null,
id: line.id,
label: line.variant?.attributes
.map(attribute =>
attribute.values
.map(attributeValue => attributeValue.name)
.join(" , ")
)
.join(" / "),
value: line.variant?.stocks?.map(stock => ({
quantity: 0,
warehouse: stock.warehouse.id
}))
}))
(getToFulfillOrderLines(order?.lines) as OrderFulfillLineFragment[]).map(
line => ({
data: null,
id: line.id,
label: getAttributesCaption(line?.variant?.attributes),
value: line?.variant?.preorder
? null
: [
{
quantity: line.quantityToFulfill,
warehouse: warehouse?.id
}
]
})
)
);
const handleSubmit = (formData: OrderFulfillFormData) =>
onSubmit({
const [
displayStockExceededDialog,
setDisplayStockExceededDialog
] = React.useState(false);
const handleSubmit = ({
formData,
allowStockToBeExceeded
}: {
formData: OrderFulfillFormData;
allowStockToBeExceeded: boolean;
}) => {
setDisplayStockExceededDialog(false);
return onSubmit({
...formData,
items: formsetData
allowStockToBeExceeded,
items: formsetData.filter(item => !!item.value)
});
};
React.useEffect(() => {
if (
errors &&
errors.every(err => err.code === OrderErrorCode.INSUFFICIENT_STOCK)
) {
setDisplayStockExceededDialog(true);
}
}, [errors]);
const notAllowedToFulfillUnpaid =
shopSettings?.fulfillmentAutoApprove &&
@ -188,26 +146,21 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
return false;
}
const isAtLeastOneFulfilled = formsetData?.some(({ value }) =>
value?.some(({ quantity }) => quantity > 0)
const isAtLeastOneFulfilled = formsetData?.some(
el => el.value?.[0]?.quantity > 0
);
const areProperlyFulfilled = formsetData?.every(({ id, value }) => {
const { lines } = order;
const overfulfill = formsetData
.filter(item => !!item?.value) // this can be removed after preorder is dropped
.some(item => {
const formQuantityFulfilled = item?.value?.[0]?.quantity;
const quantityToFulfill = order?.lines?.find(
line => line.id === item.id
).quantityToFulfill;
return formQuantityFulfilled > quantityToFulfill;
});
const { quantityToFulfill } = lines.find(
({ id: lineId }) => lineId === id
);
const formQuantityFulfilled = value?.reduce(
(result, { quantity }) => result + quantity,
0
);
return formQuantityFulfilled <= quantityToFulfill;
});
return isAtLeastOneFulfilled && areProperlyFulfilled;
return !overfulfill && isAtLeastOneFulfilled;
};
return (
@ -224,247 +177,94 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
orderNumber: order?.number
})}
/>
<Form confirmLeave initial={initialFormData} onSubmit={handleSubmit}>
<Typography className={classes.warehouseLabel}>
<FormattedMessage
{...messages.fulfillingFrom}
values={{ warehouseName: warehouse?.name }}
/>
</Typography>
<Form
confirmLeave
initial={initialFormData}
onSubmit={formData =>
handleSubmit({
formData,
allowStockToBeExceeded: displayStockExceededDialog
})
}
>
{({ change, data, submit }) => (
<>
<Card>
<CardTitle
title={intl.formatMessage(messages.itemsReadyToShip)}
/>
<ResponsiveTable className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage {...messages.productName} />
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage {...messages.sku} />
</TableCell>
{warehouses?.map(warehouse => (
<TableCell
key={warehouse.id}
className={classNames(
classes.colQuantity,
classes.colQuantityHeader
)}
>
{warehouse.name}
</TableCell>
))}
<TableCell className={classes.colQuantityTotal}>
<FormattedMessage {...messages.quantityToFulfill} />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(
getToFulfillOrderLines(order?.lines),
(
line: OrderFulfillDataQuery["order"]["lines"][0],
lineIndex
) => {
if (!line) {
return (
<TableRow key={lineIndex}>
<TableCellAvatar className={classes.colName}>
<Skeleton />
</TableCellAvatar>
<TableCell className={classes.colSku}>
<Skeleton />
</TableCell>
{warehouses?.map(warehouse => (
<TableCell
className={classes.colQuantity}
key={warehouse.id}
>
<Skeleton />
</TableCell>
))}
<TableCell className={classes.colQuantityTotal}>
{" "}
<Skeleton />
</TableCell>
</TableRow>
);
}
<Grid>
<Card>
<CardTitle
title={intl.formatMessage(messages.itemsReadyToShip)}
/>
{warehouse ? (
<ResponsiveTable className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage {...messages.productName} />
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage {...messages.sku} />
</TableCell>
<TableCell
className={classNames(
classes.colQuantity,
classes.colQuantityHeader
)}
>
<FormattedMessage {...messages.quantity} />
</TableCell>
<TableCell className={classes.colStock}>
<FormattedMessage {...messages.stock} />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(
getToFulfillOrderLines(order?.lines),
(line: OrderFulfillLineFragment, lineIndex) => (
<OrderFulfillLine
line={line}
lineIndex={lineIndex}
warehouseId={warehouse?.id}
formsetData={formsetData}
formsetChange={formsetChange}
/>
)
)}
</TableBody>
</ResponsiveTable>
) : (
<Skeleton />
)}
</Card>
const remainingQuantity = line.quantityToFulfill;
const quantityToFulfill = formsetData[
lineIndex
].value?.reduce(
(quantityToFulfill, lineInput) =>
quantityToFulfill + (lineInput.quantity || 0),
0
);
const overfulfill = remainingQuantity < quantityToFulfill;
const isPreorder = !!line.variant?.preorder;
return (
<TableRow key={line.id}>
<TableCellAvatar
className={classes.colName}
thumbnail={line?.thumbnail?.url}
>
{line.productName}
<Typography color="textSecondary" variant="caption">
{line.variant?.attributes
?.map(attribute =>
attribute.values
.map(attributeValue => attributeValue.name)
.join(", ")
)
?.join(" / ")}
</Typography>
</TableCellAvatar>
<TableCell className={classes.colSku}>
{line.variant?.sku}
</TableCell>
{warehouses?.map(warehouse => {
if (isPreorder) {
return (
<TableCell
key="skeleton"
className={classNames(
classes.colQuantity,
classes.error
)}
/>
);
}
const warehouseStock = line.variant?.stocks?.find(
stock => stock.warehouse.id === warehouse.id
);
const formsetStock = formsetData[
lineIndex
].value.find(
line => line.warehouse === warehouse.id
);
if (!warehouseStock) {
return (
<TableCell
key="skeleton"
className={classNames(
classes.colQuantity,
classes.error
)}
>
<FormattedMessage {...messages.noStock} />
</TableCell>
);
}
const warehouseAllocation = line.allocations.find(
allocation =>
allocation.warehouse.id === warehouse.id
);
const allocatedQuantityForLine =
warehouseAllocation?.quantity || 0;
const availableQuantity =
warehouseStock.quantity -
warehouseStock.quantityAllocated +
allocatedQuantityForLine;
return (
<TableCell
className={classes.colQuantity}
key={warehouseStock.id}
>
<TextField
type="number"
inputProps={{
className: classNames(
classes.quantityInnerInput,
{
[classes.quantityInnerInputNoRemaining]: !line
.variant.trackInventory
}
),
max: (
line.variant.trackInventory &&
availableQuantity
).toString(),
min: 0,
style: { textAlign: "right" }
}}
fullWidth
value={formsetStock.quantity}
onChange={event =>
formsetChange(
line.id,
update(
{
quantity: parseInt(
event.target.value,
10
),
warehouse: warehouse.id
},
formsetData[lineIndex].value,
(a, b) => a.warehouse === b.warehouse
)
)
}
error={isStockError(
overfulfill,
formsetStock,
availableQuantity,
warehouse,
line,
errors
)}
InputProps={{
endAdornment: line.variant
.trackInventory && (
<div
className={classes.remainingQuantity}
>
/ {availableQuantity}
</div>
)
}}
/>
</TableCell>
);
})}
<TableCell
className={classes.colQuantityTotal}
key="total"
>
{!isPreorder && (
<>
<span
className={classNames({
[classes.error]: overfulfill,
[classes.full]:
remainingQuantity <= quantityToFulfill
})}
>
{quantityToFulfill}
</span>{" "}
/ {remainingQuantity}
</>
)}
</TableCell>
</TableRow>
);
}
)}
</TableBody>
</ResponsiveTable>
{shopSettings?.fulfillmentAutoApprove && (
<CardActions className={classes.actionBar}>
<Card className={classes.shipmentInformationCard}>
<Typography className={classes.supportHeader}>
<FormattedMessage {...messages.shipmentInformation} />
</Typography>
<TextField
value={data.trackingNumber}
name="trackingNumber"
label={intl.formatMessage(messages.trackingNumber)}
fullWidth
onChange={change}
/>
{shopSettings?.fulfillmentAutoApprove && (
<ControlledCheckbox
checked={data.sendInfo}
label={intl.formatMessage(messages.sentShipmentDetails)}
name="sendInfo"
onChange={change}
/>
</CardActions>
)}
</Card>
)}
</Card>
</Grid>
<Savebar
disabled={!shouldEnableSave()}
labels={{
@ -481,6 +281,15 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
onSubmit={submit}
onCancel={onBack}
/>
<OrderFulfillStockExceededDialog
open={displayStockExceededDialog}
lines={order?.lines}
formsetData={formsetData}
warehouseId={warehouse?.id}
confirmButtonState={saveButtonBar}
onSubmit={submit}
onClose={() => setDisplayStockExceededDialog(false)}
/>
</>
)}
</Form>

View file

@ -37,6 +37,14 @@ export const messages = defineMessages({
defaultMessage: "Quantity to fulfill",
description: "quantity of fulfilled products"
},
quantity: {
defaultMessage: "Quantity",
description: "Header row quantity label"
},
stock: {
defaultMessage: "Stock",
description: "Header row stock label"
},
noStock: {
defaultMessage: "No Stock",
description: "no variant stock in warehouse"
@ -44,5 +52,17 @@ export const messages = defineMessages({
sentShipmentDetails: {
defaultMessage: "Send shipment details to customer",
description: "checkbox label"
},
shipmentInformation: {
defaultMessage: "Shipment information",
description: "Shipment information card header"
},
trackingNumber: {
defaultMessage: "Tracking number",
description: "Tracking number input label"
},
fulfillingFrom: {
defaultMessage: "Fulfilling from {warehouseName}",
description: "Support text under page header"
}
});

View file

@ -0,0 +1,49 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
colQuantityHeader: {
textAlign: "right"
},
colStock: {
textAlign: "right",
width: 180
},
colName: {
width: 250
},
colQuantity: {
textAlign: "right",
width: 210
},
colSku: {
textAlign: "right",
textOverflow: "ellipsis",
width: 150
},
table: {
"&&": {
tableLayout: "fixed"
}
},
shipmentInformationCard: {
padding: theme.spacing(3),
alignSelf: "start",
display: "grid",
gridRowGap: theme.spacing(1)
},
supportHeader: {
textTransform: "uppercase",
color: theme.palette.saleor.main[3],
fontWeight: 500,
letterSpacing: "0.1em",
fontSize: "12px",
lineHeight: "160%",
marginBottom: theme.spacing(2)
},
warehouseLabel: {
marginBottom: theme.spacing(4)
}
}),
{ name: "OrderFulfillPage" }
);

View file

@ -0,0 +1,113 @@
import {
TableBody,
TableCell,
TableHead,
TableRow,
Typography
} from "@material-ui/core";
import ActionDialog from "@saleor/components/ActionDialog";
import { CardSpacer } from "@saleor/components/CardSpacer";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { FulfillmentFragment, OrderFulfillLineFragment } from "@saleor/graphql";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { renderCollection } from "@saleor/misc";
import {
getFulfillmentFormsetQuantity,
getOrderLineAvailableQuantity,
OrderFulfillStockInputFormsetData
} from "@saleor/orders/utils/data";
import React from "react";
import { useIntl } from "react-intl";
import OrderFulfillStockExceededDialogLine from "../OrderFulfillStockExceededDialogLine";
import { stockExceededDialogMessages as messages } from "./messages";
import { useStyles } from "./styles";
export interface OrderFulfillStockExceededDialogProps {
lines: Array<FulfillmentFragment["lines"][0] | OrderFulfillLineFragment>;
open: boolean;
formsetData: OrderFulfillStockInputFormsetData;
warehouseId: string;
confirmButtonState: ConfirmButtonTransitionState;
onSubmit();
onClose();
}
const OrderFulfillStockExceededDialog: React.FC<OrderFulfillStockExceededDialogProps> = props => {
const {
lines,
open,
formsetData,
warehouseId,
confirmButtonState,
onClose,
onSubmit
} = props;
const intl = useIntl();
const classes = useStyles(props);
const exceededLines = lines?.filter(el => {
const line = "orderLine" in el ? el.orderLine : el;
const stock = line.variant?.stocks.find(
stock => stock.warehouse.id === warehouseId
);
return (
getFulfillmentFormsetQuantity(formsetData, line) >
getOrderLineAvailableQuantity(line, stock)
);
});
return (
<>
<ActionDialog
open={open}
title={intl.formatMessage(messages.title)}
onConfirm={onSubmit}
onClose={onClose}
confirmButtonState={confirmButtonState}
maxWidth="sm"
confirmButtonLabel={intl.formatMessage(messages.fulfillButton)}
>
<Typography>{intl.formatMessage(messages.infoLabel)}</Typography>
<CardSpacer />
<div className={classes.scrollable}>
<ResponsiveTable className={classes.table}>
{!!lines?.length && (
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
{intl.formatMessage(messages.productLabel)}
</TableCell>
<TableCell className={classes.colQuantity}>
{intl.formatMessage(messages.requiredStockLabel)}
</TableCell>
<TableCell className={classes.colWarehouseStock}>
{intl.formatMessage(messages.warehouseStockLabel)}
</TableCell>
</TableRow>
</TableHead>
)}
<TableBody>
{renderCollection(exceededLines, line => (
<OrderFulfillStockExceededDialogLine
key={line?.id}
line={line}
formsetData={formsetData}
warehouseId={warehouseId}
/>
))}
</TableBody>
</ResponsiveTable>
</div>
<CardSpacer />
<Typography>{intl.formatMessage(messages.questionLabel)}</Typography>
</ActionDialog>
</>
);
};
OrderFulfillStockExceededDialog.displayName = "OrderFulfillStockExceededDialog";
export default OrderFulfillStockExceededDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderFulfillStockExceededDialog";
export * from "./OrderFulfillStockExceededDialog";

View file

@ -0,0 +1,41 @@
import { defineMessages } from "react-intl";
export const stockExceededDialogMessages = defineMessages({
title: {
defaultMessage: "Not enough stock",
description: "stock exceeded dialog title"
},
infoLabel: {
defaultMessage:
"Stock for items shown below are not enough to prepare fulfillment:",
description: "stock exceeded dialog description"
},
questionLabel: {
defaultMessage: "Are you sure you want to fulfill those products anyway?",
description: "stock exceeded action question label"
},
cancelButton: {
defaultMessage: "Cancel",
description: "cancel button label"
},
fulfillButton: {
defaultMessage: "Fulfill anyway",
description: "fulfill button label"
},
productLabel: {
defaultMessage: "Product",
description: "table header product label"
},
requiredStockLabel: {
defaultMessage: "Required",
description: "table header required stock label"
},
availableStockLabel: {
defaultMessage: "Available",
description: "table header available stock label"
},
warehouseStockLabel: {
defaultMessage: "Warehouse stock",
description: "table header warehouse stock label"
}
});

View file

@ -0,0 +1,31 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
colName: {
width: "auto",
margin: "0px"
},
colQuantity: {
textAlign: "right",
width: 100,
padding: "4px 4px"
},
colWarehouseStock: {
textAlign: "right",
width: 150,
padding: "4px 24px"
},
table: {
tableLayout: "fixed"
},
label: {
margin: theme.spacing(2)
},
scrollable: {
maxHeight: 450,
overflow: "scroll"
}
}),
{ name: "OrderFulfillStockExceededDialog" }
);

View file

@ -0,0 +1,59 @@
import { TableCell, TableRow, Typography } from "@material-ui/core";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { FulfillmentFragment, OrderFulfillLineFragment } from "@saleor/graphql";
import {
getAttributesCaption,
getFulfillmentFormsetQuantity,
getOrderLineAvailableQuantity,
OrderFulfillStockInputFormsetData
} from "@saleor/orders/utils/data";
import React from "react";
import { useStyles } from "../OrderFulfillStockExceededDialog/styles";
export interface OrderFulfillStockExceededDialogLineProps {
line: OrderFulfillLineFragment | FulfillmentFragment["lines"][0];
warehouseId: string;
formsetData: OrderFulfillStockInputFormsetData;
}
const OrderFulfillStockExceededDialogLine: React.FC<OrderFulfillStockExceededDialogLineProps> = props => {
const { line: genericLine, warehouseId, formsetData } = props;
if (!genericLine) {
return null;
}
const line = "orderLine" in genericLine ? genericLine.orderLine : genericLine;
const classes = useStyles(props);
const stock = line?.variant?.stocks.find(
stock => stock.warehouse.id === warehouseId
);
return (
<TableRow key={line?.id}>
<TableCellAvatar
className={classes.colName}
thumbnail={line?.thumbnail?.url}
>
{line?.productName}
{"attributes" in line.variant && (
<Typography color="textSecondary" variant="caption">
{getAttributesCaption(line.variant?.attributes)}
</Typography>
)}
</TableCellAvatar>
<TableCell className={classes.colQuantity}>
{getFulfillmentFormsetQuantity(formsetData, line)}
</TableCell>
<TableCell className={classes.colWarehouseStock}>
{getOrderLineAvailableQuantity(line, stock)}
</TableCell>
</TableRow>
);
};
OrderFulfillStockExceededDialogLine.displayName =
"OrderFulfillStockExceededDialogLine";
export default OrderFulfillStockExceededDialogLine;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderFulfillStockExceededDialogLine";
export * from "./OrderFulfillStockExceededDialogLine";

View file

@ -8,9 +8,9 @@ import { mergeRepeatedOrderLines } from "@saleor/orders/utils/data";
import React from "react";
import { renderCollection } from "../../../misc";
import OrderCardTitle from "../OrderCardTitle";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import CardTitle from "../OrderReturnPage/OrderReturnRefundItemsCard/CardTitle";
import ActionButtons from "./ActionButtons";
import ExtraInfoLines from "./ExtraInfoLines";
import useStyles from "./styles";
@ -63,7 +63,7 @@ const OrderFulfilledProductsCard: React.FC<OrderFulfilledProductsCardProps> = pr
return (
<>
<Card>
<CardTitle
<OrderCardTitle
withStatus
lines={fulfillment?.lines}
fulfillmentOrder={fulfillment?.fulfillmentOrder}
@ -87,7 +87,7 @@ const OrderFulfilledProductsCard: React.FC<OrderFulfilledProductsCardProps> = pr
<TableHeader />
<TableBody>
{renderCollection(getLines(), line => (
<TableLine line={line} />
<TableLine key={line.id} line={line} />
))}
</TableBody>
<ExtraInfoLines fulfillment={fulfillment} />

View file

@ -22,9 +22,9 @@ import { renderCollection } from "@saleor/misc";
import React, { CSSProperties } from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import OrderCardTitle from "../../OrderCardTitle";
import { FormsetQuantityData, FormsetReplacementData } from "../form";
import { getById } from "../utils";
import CardTitle from "./CardTitle";
import MaximalButton from "./MaximalButton";
import ProductErrorCell from "./ProductErrorCell";
@ -120,7 +120,7 @@ const ItemsCard: React.FC<OrderReturnRefundLinesCardProps> = ({
return (
<Card>
<CardTitle
<OrderCardTitle
orderNumber={order?.number}
lines={lines}
fulfillmentOrder={fulfillment?.fulfillmentOrder}

View file

@ -1,16 +1,17 @@
import { Card, CardActions, TableBody, Typography } from "@material-ui/core";
import CardSpacer from "@saleor/components/CardSpacer";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { OrderLineFragment } from "@saleor/graphql";
import Skeleton from "@saleor/components/Skeleton";
import { OrderLineFragment, WarehouseFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl";
import { Button, makeStyles } from "@saleor/macaw-ui";
import { Button, ChevronIcon, makeStyles } from "@saleor/macaw-ui";
import { renderCollection } from "@saleor/misc";
import React from "react";
import { FormattedMessage } from "react-intl";
import OrderCardTitle from "../OrderCardTitle";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import CardTitle from "../OrderReturnPage/OrderReturnRefundItemsCard/CardTitle";
const useStyles = makeStyles(
theme => ({
@ -26,6 +27,41 @@ const useStyles = makeStyles(
}
},
tableLayout: "fixed"
},
toolbar: {
display: "flex",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
borderRadius: "4px",
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
paddingRight: theme.spacing(0.5),
paddingLeft: theme.spacing(1.5),
"&:hover": {
backgroundColor: theme.palette.saleor.active[5],
color: theme.palette.saleor.active[1]
},
"& > div": {
minWidth: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}
},
cardTitle: {
justifyContent: "space-between",
"& > div": {
"&:first-child": {
flex: 0,
whiteSpace: "nowrap"
},
"&:last-child": {
flex: "0 1 auto",
minWidth: 0,
marginLeft: theme.spacing(1)
}
}
}
}),
{ name: "OrderUnfulfilledItems" }
@ -36,6 +72,8 @@ interface OrderUnfulfilledProductsCardProps {
notAllowedToFulfillUnpaid: boolean;
lines: OrderLineFragment[];
onFulfill: () => void;
selectedWarehouse: WarehouseFragment;
onWarehouseChange: () => null;
}
const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps> = props => {
@ -43,7 +81,9 @@ const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps>
showFulfillmentAction,
notAllowedToFulfillUnpaid,
lines,
onFulfill
onFulfill,
selectedWarehouse,
onWarehouseChange
} = props;
const classes = useStyles({});
@ -54,19 +94,30 @@ const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps>
return (
<>
<Card>
<CardTitle withStatus status="unfulfilled" />
<OrderCardTitle
lines={lines}
withStatus
status="unfulfilled"
className={classes.cardTitle}
toolbar={
<div className={classes.toolbar} onClick={onWarehouseChange}>
<div>{selectedWarehouse?.name ?? <Skeleton />}</div>
<ChevronIcon />
</div>
}
/>
<ResponsiveTable className={classes.table}>
<TableHeader />
<TableBody>
{renderCollection(lines, line => (
<TableLine isOrderLine line={line} />
<TableLine key={line.id} isOrderLine line={line} />
))}
</TableBody>
</ResponsiveTable>
{showFulfillmentAction && (
<CardActions>
<CardActions className={classes.actions}>
<Button
variant="tertiary"
variant="primary"
onClick={onFulfill}
disabled={notAllowedToFulfillUnpaid}
>

View file

@ -14,6 +14,7 @@ import {
PaymentChargeStatusEnum,
SearchCustomersQuery,
SearchOrderVariantQuery,
SearchWarehousesQuery,
ShopOrderSettingsFragment,
WeightUnitsEnum
} from "@saleor/graphql";
@ -1073,6 +1074,18 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
quantity: 2,
quantityFulfilled: 2,
quantityToFulfill: 0,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: {
__typename: "Image" as "Image",
url: placeholder
@ -1116,7 +1129,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant",
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "stock_warehouse1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "stock_warehouse2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
]
}
},
quantity: 1
@ -1143,6 +1182,18 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
quantity: 2,
quantityFulfilled: 2,
quantityToFulfill: 0,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: {
__typename: "Image" as "Image",
url: placeholder
@ -1186,7 +1237,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant",
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "stock_warehouse1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "stock_warehouse2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
]
}
},
quantity: 1
@ -1221,6 +1298,18 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
quantity: 3,
quantityFulfilled: 0,
quantityToFulfill: 3,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: {
__typename: "Image" as "Image",
url: placeholder
@ -1264,7 +1353,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant",
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "stock_warehouse1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "stock_warehouse2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
]
}
},
{
@ -1276,6 +1391,18 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
quantity: 2,
quantityFulfilled: 2,
quantityToFulfill: 0,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: {
__typename: "Image" as "Image",
url: placeholder
@ -1320,7 +1447,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant",
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "stock_warehouse1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "stock_warehouse2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
]
}
}
],
@ -1471,6 +1624,18 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
quantity: 2,
quantityFulfilled: 0,
quantityToFulfill: 2,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: {
__typename: "Image" as "Image",
url: placeholder
@ -1514,7 +1679,33 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant",
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "stock_warehouse1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "stock_warehouse2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
]
}
},
{
@ -1526,6 +1717,18 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
quantity: 2,
quantityFulfilled: 0,
quantityToFulfill: 2,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: {
__typename: "Image" as "Image",
url: placeholder
@ -1569,7 +1772,33 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant",
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "stock_warehouse1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "stock_warehouse2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
]
}
}
],
@ -2088,3 +2317,105 @@ export const shopOrderSettings: ShopOrderSettingsFragment = {
fulfillmentAutoApprove: true,
fulfillmentAllowUnpaid: true
};
export const warehouseSearch: SearchWarehousesQuery["search"] = {
edges: [
{
node: {
id: "V2FyZWhvdXNlOmJiZTEwZjk1LTQyYjAtNDRlMS04Yjc5LWU5MjllMmViYTRjMQ==",
name: "CyVou-97803",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjdhOGViNThhLTYwN2QtNGMxNC04ODVmLTBiMWU3ZDcyMTIyNQ==",
name: "CyWarehouse72715",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjY2NWIxZWFmLTU5MDYtNGE0Mi1iYWVkLTc1ODQ3YWNhMWI1NQ==",
name: "CyWarehouseCheckout70441",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjdkNmVmNmFkLWY4NTMtNGVmNS1iMzQ5LTUyY2I2N2U3NmIwZQ==",
name: "CyWeightRates-78849",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjcwZjMyYTUyLWVlODQtNGExYi1iMjgzLTgwYjllMzgyNDlkNg==",
name: "EditShipping-82885",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
name: "Europe for click and collect",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
name: "Oceania",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjNiZDM0YjEyLTllNDktNDMwZC1iM2QyLTRkYmRhMjM1MGUyOQ==",
name: "ProductsWithoutSkuInOrder",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOmU4M2U2NjQ2LTFhYjctNGNmNC05N2M4LTFiZjI2NGE2NjQ4Yw==",
name: "StocksThreshold",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOmJkMmQ1NDFjLWQwMjMtNDAwNi05YmRjLWZhZTA4OWZlNzZiYg==",
name: "UpdateProductsSku59844",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
},
{
node: {
id: "V2FyZWhvdXNlOjgzNDMwMzI4LTI2YWItNDNkZS1hNzdhLTVmNGNhMTljMDJhNg==",
name: "WithoutShipmentCheckout-4505",
__typename: "Warehouse"
},
__typename: "WarehouseCountableEdge"
}
],
pageInfo: {
endCursor:
"WyJXaXRob3V0U2hpcG1lbnRDaGVja291dC00NTA1IiwgIldpdGhvdXRTaGlwbWVudENoZWNrb3V0LTQ1MDUiXQ==",
hasNextPage: false,
hasPreviousPage: true,
startCursor: "WyJDeVZvdS05NzgwMyIsICJDeVZvdS05NzgwMyJd",
__typename: "PageInfo"
},
__typename: "WarehouseCountableConnection"
};

View file

@ -11,6 +11,7 @@ import {
OrderDraftListUrlQueryParams,
OrderDraftListUrlSortField,
orderFulfillPath,
OrderFulfillUrlQueryParams,
orderListPath,
OrderListUrlQueryParams,
OrderListUrlSortField,
@ -61,9 +62,19 @@ const OrderDetails: React.FC<RouteComponentProps<any>> = ({
return <OrderDetailsComponent id={decodeURIComponent(id)} params={params} />;
};
const OrderFulfill: React.FC<RouteComponentProps<any>> = ({ match }) => (
<OrderFulfillComponent orderId={decodeURIComponent(match.params.id)} />
);
const OrderFulfill: React.FC<RouteComponentProps<any>> = ({
location,
match
}) => {
const qs = parseQs(location.search.substr(1));
const params: OrderFulfillUrlQueryParams = qs;
return (
<OrderFulfillComponent
orderId={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
const OrderRefund: React.FC<RouteComponentProps<any>> = ({ match }) => (
<OrderRefundComponent orderId={decodeURIComponent(match.params.id)} />

View file

@ -243,8 +243,16 @@ export const orderFulfillmentUpdateTrackingMutation = gql`
`;
export const orderFulfillmentApproveMutation = gql`
mutation OrderFulfillmentApprove($id: ID!, $notifyCustomer: Boolean!) {
orderFulfillmentApprove(id: $id, notifyCustomer: $notifyCustomer) {
mutation OrderFulfillmentApprove(
$id: ID!
$notifyCustomer: Boolean!
$allowStockToBeExceeded: Boolean
) {
orderFulfillmentApprove(
id: $id
notifyCustomer: $notifyCustomer
allowStockToBeExceeded: $allowStockToBeExceeded
) {
errors {
...OrderError
}

View file

@ -130,44 +130,7 @@ export const orderFulfillData = gql`
}
}
lines {
id
isShippingRequired
productName
quantity
allocations {
quantity
warehouse {
id
}
}
quantityFulfilled
quantityToFulfill
variant {
id
name
sku
preorder {
endDate
}
attributes {
values {
id
name
}
}
stocks {
id
warehouse {
...Warehouse
}
quantity
quantityAllocated
}
trackInventory
}
thumbnail(size: 64) {
url
}
...OrderFulfillLine
}
number
}

View file

@ -111,6 +111,7 @@ export type OrderUrlDialog =
| "cancel"
| "cancel-fulfillment"
| "capture"
| "change-warehouse"
| "customer-change"
| "edit-customer-addresses"
| "edit-billing-address"
@ -124,6 +125,8 @@ export type OrderUrlDialog =
export type OrderUrlQueryParams = Dialog<OrderUrlDialog> & SingleAction;
export type OrderFulfillUrlQueryParams = Partial<{ warehouse: string }>;
export const orderUrl = (id: string, params?: OrderUrlQueryParams) =>
orderPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
@ -132,8 +135,10 @@ export const orderFulfillPath = (id: string) =>
export const orderReturnPath = (id: string) => urlJoin(orderPath(id), "return");
export const orderFulfillUrl = (id: string) =>
orderFulfillPath(encodeURIComponent(id));
export const orderFulfillUrl = (
id: string,
params?: OrderFulfillUrlQueryParams
) => orderFulfillPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const orderSettingsPath = urlJoin(orderSectionUrl, "settings");

View file

@ -521,11 +521,49 @@ describe("Get the total value of all replaced products", () => {
{
id: "1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
__typename: "ProductVariant"
__typename: "ProductVariant",
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
]
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -577,10 +615,48 @@ describe("Get the total value of all replaced products", () => {
{
id: "2",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -633,10 +709,48 @@ describe("Get the total value of all replaced products", () => {
{
id: "3",
isShippingRequired: true,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "T-shirt",
@ -695,10 +809,48 @@ describe("Get the total value of all replaced products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -756,10 +908,48 @@ describe("Get the total value of all replaced products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -817,10 +1007,48 @@ describe("Get the total value of all replaced products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ3",
isShippingRequired: true,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "T-shirt",
@ -878,10 +1106,48 @@ describe("Get the total value of all replaced products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -939,10 +1205,48 @@ describe("Get the total value of all replaced products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -1134,10 +1438,48 @@ describe("Get the total value of all selected products", () => {
{
id: "1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -1190,10 +1532,48 @@ describe("Get the total value of all selected products", () => {
{
id: "2",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -1246,10 +1626,48 @@ describe("Get the total value of all selected products", () => {
{
id: "3",
isShippingRequired: true,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "T-shirt",
@ -1308,10 +1726,48 @@ describe("Get the total value of all selected products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -1369,10 +1825,48 @@ describe("Get the total value of all selected products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -1430,10 +1924,48 @@ describe("Get the total value of all selected products", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ3",
isShippingRequired: true,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "T-shirt",
@ -1619,10 +2151,48 @@ describe("Merge repeated order lines of fulfillment lines", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -1680,10 +2250,48 @@ describe("Merge repeated order lines of fulfillment lines", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ1",
isShippingRequired: false,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE3",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "Lake Tunes",
@ -1741,10 +2349,48 @@ describe("Merge repeated order lines of fulfillment lines", () => {
orderLine: {
id: "T3JkZXJMaW5lOjQ3",
isShippingRequired: true,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjg2",
quantityAvailable: 50,
preorder: null,
stocks: [
{
id: "stock_test_id1",
warehouse: {
name: "warehouse_stock1",
id:
"V2FyZWhvdXNlOjc4OGUyMGRlLTlmYTAtNDI5My1iZDk2LWUwM2RjY2RhMzc0ZQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
},
{
id: "stock_test_id2",
warehouse: {
name: "warehouse_stock2",
id:
"V2FyZWhvdXNlOjczYzI0OGNmLTliNzAtNDlmMi1hMDRlLTM4ZTYxMmQ5MDYwMQ==",
__typename: "Warehouse"
},
quantity: 166,
quantityAllocated: 0,
__typename: "Stock"
}
],
__typename: "ProductVariant"
},
productName: "T-shirt",

View file

@ -3,13 +3,15 @@ import {
AddressFragment,
AddressInput,
CountryCode,
FulfillmentFragment,
FulfillmentStatus,
FulfillOrderMutation,
OrderDetailsFragment,
OrderErrorCode,
OrderFulfillDataQuery,
OrderFulfillLineFragment,
OrderFulfillStockInput,
OrderLineFragment,
OrderLineStockDataFragment,
OrderRefundDataQuery,
StockFragment,
WarehouseFragment
} from "@saleor/graphql";
import { FormsetData } from "@saleor/hooks/useFormset";
@ -36,9 +38,7 @@ export interface OrderLineWithStockWarehouses {
};
}
export function getToFulfillOrderLines(
lines?: OrderFulfillDataQuery["order"]["lines"]
) {
export function getToFulfillOrderLines(lines?: OrderLineStockDataFragment[]) {
return lines?.filter(line => line.quantityToFulfill > 0) || [];
}
@ -276,31 +276,6 @@ export function mergeRepeatedOrderLines(
}, Array<OrderDetailsFragment["fulfillments"][0]["lines"][0]>());
}
export const isStockError = (
overfulfill: boolean,
formsetStock: { quantity: number },
availableQuantity: number,
warehouse: WarehouseFragment,
line: OrderFulfillDataQuery["order"]["lines"][0],
errors: FulfillOrderMutation["orderFulfill"]["errors"]
) => {
if (overfulfill) {
return true;
}
const isQuantityLargerThanAvailable =
line.variant.trackInventory && formsetStock.quantity > availableQuantity;
const isError = !!errors?.find(
err =>
err.warehouse === warehouse.id &&
err.orderLines.find((id: string) => id === line.id) &&
err.code === OrderErrorCode.INSUFFICIENT_STOCK
);
return isQuantityLargerThanAvailable || isError;
};
export function addressToAddressInput<T>(
address: T & AddressFragment
): AddressInput {
@ -324,3 +299,83 @@ export const getVariantSearchAddress = (
return { country: order.channel.defaultCountry.code as CountryCode };
};
export const getAllocatedQuantityForLine = (
line: OrderLineStockDataFragment,
warehouseId: string
) => {
const warehouseAllocation = line.allocations.find(
allocation => allocation.warehouse.id === warehouseId
);
return warehouseAllocation?.quantity || 0;
};
export const getOrderLineAvailableQuantity = (
line: OrderLineStockDataFragment,
stock: StockFragment
) => {
if (!stock) {
return 0;
}
const allocatedQuantityForLine = getAllocatedQuantityForLine(
line,
stock.warehouse.id
);
const availableQuantity =
stock.quantity - stock.quantityAllocated + allocatedQuantityForLine;
return availableQuantity;
};
export type OrderFulfillStockInputFormsetData = Array<
Pick<FormsetData<null, OrderFulfillStockInput[]>[0], "id" | "value">
>;
export const getFulfillmentFormsetQuantity = (
formsetData: OrderFulfillStockInputFormsetData,
line: OrderLineStockDataFragment
) => formsetData?.find(getById(line.id))?.value?.[0]?.quantity;
export const getWarehouseStock = (
stocks: StockFragment[],
warehouseId: string
) => stocks?.find(stock => stock.warehouse.id === warehouseId);
export const isLineAvailableInWarehouse = (
line: OrderLineStockDataFragment,
warehouse: WarehouseFragment
) => {
if (!line?.variant?.stocks) {
return false;
}
const stock = getWarehouseStock(line.variant.stocks, warehouse.id);
if (stock) {
return line.quantityToFulfill <= getOrderLineAvailableQuantity(line, stock);
}
return false;
};
export const transformFuflillmentLinesToStockInputFormsetData = (
lines: FulfillmentFragment["lines"],
warehouseId: string
): OrderFulfillStockInputFormsetData =>
lines?.map(line => ({
data: null,
id: line.orderLine.id,
value: [
{
quantity: line.quantity,
warehouse: warehouseId
}
]
}));
export const getAttributesCaption = (
attributes: OrderFulfillLineFragment["variant"]["attributes"]
): string =>
attributes
.map(attribute =>
attribute.values.map(attributeValue => attributeValue.name).join(", ")
)
.join(" / ");

View file

@ -1,18 +1,25 @@
import { WindowTitle } from "@saleor/components/WindowTitle";
import {
FulfillmentFragment,
FulfillmentStatus,
OrderDetailsQueryResult,
OrderFulfillmentApproveMutation,
OrderFulfillmentApproveMutationVariables,
OrderUpdateMutation,
OrderUpdateMutationVariables,
useCustomerAddressesQuery,
useWarehouseListQuery
useWarehouseListQuery,
WarehouseFragment
} from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator";
import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog";
import OrderChangeWarehouseDialog from "@saleor/orders/components/OrderChangeWarehouseDialog";
import { OrderCustomerAddressesEditDialogOutput } from "@saleor/orders/components/OrderCustomerAddressesEditDialog/types";
import OrderFulfillmentApproveDialog from "@saleor/orders/components/OrderFulfillmentApproveDialog";
import OrderFulfillStockExceededDialog from "@saleor/orders/components/OrderFulfillStockExceededDialog";
import OrderInvoiceEmailSendDialog from "@saleor/orders/components/OrderInvoiceEmailSendDialog";
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
import { transformFuflillmentLinesToStockInputFormsetData } from "@saleor/orders/utils/data";
import { PartialMutationProviderOutput } from "@saleor/types";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react";
@ -42,11 +49,12 @@ import {
OrderUrlQueryParams
} from "../../../urls";
import { isAnyAddressEditModalOpen } from "../OrderDraftDetails";
import { useDefaultWarehouse } from "./useDefaultWarehouse";
interface OrderNormalDetailsProps {
id: string;
params: OrderUrlQueryParams;
data: any;
data: OrderDetailsQueryResult["data"];
orderAddNote: any;
orderInvoiceRequest: any;
handleSubmit: any;
@ -70,6 +78,10 @@ interface OrderNormalDetailsProps {
openModal: any;
closeModal: any;
}
interface ApprovalState {
fulfillment: FulfillmentFragment;
notifyCustomer: boolean;
}
export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
id,
@ -96,13 +108,27 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
const shop = data?.shop;
const navigate = useNavigator();
const warehouses = useWarehouseListQuery({
const {
data: warehousesData,
loading: warehousesLoading
} = useWarehouseListQuery({
displayLoader: true,
variables: {
first: 30
}
});
const warehouses = mapEdgesToItems(warehousesData?.warehouses);
const [fulfillmentWarehouse, setFulfillmentWarehouse] = React.useState<
WarehouseFragment
>(null);
useDefaultWarehouse({ warehouses, order, setter: setFulfillmentWarehouse }, [
warehousesData,
warehousesLoading
]);
const {
data: customerAddresses,
loading: customerAddressesLoading
@ -125,6 +151,22 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
const handleBack = () => navigate(orderListUrl());
const [
currentApproval,
setCurrentApproval
] = React.useState<ApprovalState | null>(null);
const [stockExceeded, setStockExceeded] = React.useState(false);
const approvalErrors =
orderFulfillmentApprove.opts.data?.orderFulfillmentApprove.errors || [];
React.useEffect(() => {
if (
approvalErrors.length &&
approvalErrors.every(err => err.code === "INSUFFICIENT_STOCK")
) {
setStockExceeded(true);
}
}, [approvalErrors]);
return (
<>
<WindowTitle
@ -167,8 +209,11 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
]
)}
shippingMethods={data?.order?.shippingMethods || []}
selectedWarehouse={fulfillmentWarehouse}
onOrderCancel={() => openModal("cancel")}
onOrderFulfill={() => navigate(orderFulfillUrl(id))}
onOrderFulfill={() =>
navigate(orderFulfillUrl(id, { warehouse: fulfillmentWarehouse?.id }))
}
onFulfillmentApprove={fulfillmentId =>
navigate(
orderUrl(id, {
@ -213,6 +258,7 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
})
}
onInvoiceSend={id => openModal("invoice-send", { id })}
onWarehouseChange={() => openModal("change-warehouse")}
onSubmit={handleSubmit}
/>
<OrderCannotCancelOrderDialog
@ -279,21 +325,44 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
[]
}
open={params.action === "approve-fulfillment"}
onConfirm={({ notifyCustomer }) =>
orderFulfillmentApprove.mutate({
onConfirm={({ notifyCustomer }) => {
setCurrentApproval({
fulfillment: order?.fulfillments.find(getById(params.id)),
notifyCustomer
});
return orderFulfillmentApprove.mutate({
id: params.id,
notifyCustomer
})
}
});
}}
onClose={closeModal}
/>
<OrderFulfillStockExceededDialog
lines={currentApproval?.fulfillment.lines}
formsetData={transformFuflillmentLinesToStockInputFormsetData(
currentApproval?.fulfillment.lines,
currentApproval?.fulfillment.warehouse?.id
)}
open={stockExceeded}
warehouseId={currentApproval?.fulfillment.warehouse?.id}
onClose={() => setStockExceeded(false)}
confirmButtonState="default"
onSubmit={() => {
setStockExceeded(false);
return orderFulfillmentApprove.mutate({
id: params.id,
notifyCustomer: currentApproval?.notifyCustomer,
allowStockToBeExceeded: true
});
}}
/>
<OrderFulfillmentCancelDialog
confirmButtonState={orderFulfillmentCancel.opts.status}
errors={
orderFulfillmentCancel.opts.data?.orderFulfillmentCancel.errors || []
}
open={params.action === "cancel-fulfillment"}
warehouses={mapEdgesToItems(warehouses?.data?.warehouses) || []}
warehouses={warehouses || []}
onConfirm={variables =>
orderFulfillmentCancel.mutate({
id: params.id,
@ -325,6 +394,13 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
}
onClose={closeModal}
/>
<OrderChangeWarehouseDialog
open={params.action === "change-warehouse"}
lines={order?.lines}
currentWarehouse={fulfillmentWarehouse}
onConfirm={warehouse => setFulfillmentWarehouse(warehouse)}
onClose={closeModal}
/>
<OrderInvoiceEmailSendDialog
confirmButtonState={orderInvoiceSend.opts.status}
errors={orderInvoiceSend.opts.data?.invoiceSendEmail.errors || []}

View file

@ -0,0 +1,48 @@
import { OrderDetailsFragment, WarehouseFragment } from "@saleor/graphql";
import {
getToFulfillOrderLines,
isLineAvailableInWarehouse
} from "@saleor/orders/utils/data";
import React from "react";
export interface UseDefaultWarehouseOpts {
warehouses: WarehouseFragment[];
order: OrderDetailsFragment;
setter: React.Dispatch<React.SetStateAction<WarehouseFragment>>;
}
interface WarehousesAvailibility {
warehouse: WarehouseFragment;
linesAvailable: number;
}
export function useDefaultWarehouse(
{ warehouses, order, setter }: UseDefaultWarehouseOpts,
deps: unknown[]
) {
React.useEffect(() => {
const warehousesAvailability: WarehousesAvailibility[] = warehouses?.map(
warehouse => {
if (!order?.lines) {
return undefined;
}
const linesToFulfill = getToFulfillOrderLines(order.lines);
const linesAvailable = linesToFulfill.filter(line =>
isLineAvailableInWarehouse(line, warehouse)
).length;
return {
warehouse,
linesAvailable
};
}
);
const defaultWarehouse = order?.lines
? warehousesAvailability?.reduce((prev, curr) =>
curr.linesAvailable > prev.linesAvailable ? curr : prev
).warehouse
: undefined;
setter(defaultWarehouse);
}, [order, ...deps]);
}

View file

@ -1,43 +1,27 @@
import { WindowTitle } from "@saleor/components/WindowTitle";
import {
OrderFulfillDataQuery,
useFulfillOrderMutation,
useOrderFulfillDataQuery,
useOrderFulfillmentUpdateTrackingMutation,
useOrderFulfillSettingsQuery,
WarehouseClickAndCollectOptionEnum,
WarehouseFragment
useWarehouseDetailsQuery
} from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { extractMutationErrors } from "@saleor/misc";
import OrderFulfillPage from "@saleor/orders/components/OrderFulfillPage";
import { orderUrl } from "@saleor/orders/urls";
import { getWarehousesFromOrderLines } from "@saleor/orders/utils/data";
import { getMutationErrors } from "@saleor/misc";
import OrderFulfillPage, {
OrderFulfillSubmitData
} from "@saleor/orders/components/OrderFulfillPage";
import { OrderFulfillUrlQueryParams, orderUrl } from "@saleor/orders/urls";
import React from "react";
import { useIntl } from "react-intl";
export interface OrderFulfillProps {
orderId: string;
params: OrderFulfillUrlQueryParams;
}
const resolveLocalFulfillment = (
order: OrderFulfillDataQuery["order"],
orderLineWarehouses: WarehouseFragment[]
) => {
const deliveryMethod = order?.deliveryMethod;
if (
deliveryMethod?.__typename === "Warehouse" &&
deliveryMethod?.clickAndCollectOption ===
WarehouseClickAndCollectOptionEnum.LOCAL
) {
return orderLineWarehouses?.filter(
warehouse => warehouse?.id === deliveryMethod?.id
);
}
return orderLineWarehouses;
};
const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId, params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
@ -54,7 +38,7 @@ const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
}
});
const orderLinesWarehouses = getWarehousesFromOrderLines(data?.order?.lines);
const [updateTracking] = useOrderFulfillmentUpdateTrackingMutation();
const [fulfillOrder, fulfillOrderOpts] = useFulfillOrderMutation({
onCompleted: data => {
@ -71,10 +55,11 @@ const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
}
});
const resolvedOrderLinesWarehouses = resolveLocalFulfillment(
data?.order,
orderLinesWarehouses
);
const { data: warehouseData } = useWarehouseDetailsQuery({
variables: {
id: params?.warehouse
}
});
return (
<>
@ -100,26 +85,44 @@ const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
loading={loading || settingsLoading || fulfillOrderOpts.loading}
errors={fulfillOrderOpts.data?.orderFulfill.errors}
onBack={() => navigate(orderUrl(orderId))}
onSubmit={formData =>
extractMutationErrors(
fulfillOrder({
variables: {
input: {
lines: formData.items.map(line => ({
onSubmit={async (formData: OrderFulfillSubmitData) => {
const res = await fulfillOrder({
variables: {
input: {
lines: formData.items
.filter(line => !!line?.value)
.map(line => ({
orderLineId: line.id,
stocks: line.value
})),
notifyCustomer:
settings?.shop?.fulfillmentAutoApprove && formData.sendInfo,
allowStockToBeExceeded: formData.allowStockToBeExceeded
},
orderId
}
});
const fulfillments = res?.data?.orderFulfill?.order?.fulfillments;
if (fulfillments && formData.trackingNumber) {
updateTracking({
variables: {
id: fulfillments[fulfillments.length - 1].id,
input: {
...(formData?.trackingNumber && {
trackingNumber: formData.trackingNumber
}),
notifyCustomer:
settings?.shop?.fulfillmentAutoApprove && formData.sendInfo
},
orderId
}
}
})
)
}
});
}
return getMutationErrors(res);
}}
order={data?.order}
saveButtonBar="default"
warehouses={resolvedOrderLinesWarehouses}
saveButtonBar={fulfillOrderOpts.status}
warehouse={warehouseData?.warehouse}
shopSettings={settings?.shop}
/>
</>

View file

@ -11,6 +11,7 @@ export const searchWarehouses = gql`
search: warehouses(
after: $after
first: $first
sortBy: { direction: ASC, field: NAME }
filter: { search: $query }
) {
edges {

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@ const order = orderFixture(placeholderImage);
const props: Omit<OrderDetailsPageProps, "classes"> = {
disabled: false,
selectedWarehouse: undefined,
onBack: () => undefined,
onBillingAddressEdit: undefined,
onFulfillmentApprove: () => undefined,
@ -39,6 +40,7 @@ const props: Omit<OrderDetailsPageProps, "classes"> = {
onProductClick: undefined,
onProfileView: () => undefined,
onShippingAddressEdit: undefined,
onWarehouseChange: undefined,
onSubmit: () => undefined,
order,
shop: shopFixture,