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": { "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." "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": { "src_dot_orders_dot_components_dot_OrderChannelSectionCard_dot_1243773440": {
"context": "section header", "context": "section header",
"string": "Sales channel" "string": "Sales channel"
@ -4600,6 +4664,18 @@
"context": "button", "context": "button",
"string": "Finalize" "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": { "src_dot_orders_dot_components_dot_OrderFulfillPage_dot_headerOrder": {
"context": "page header", "context": "page header",
"string": "Order" "string": "Order"
@ -4624,6 +4700,10 @@
"context": "name", "context": "name",
"string": "Product 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": { "src_dot_orders_dot_components_dot_OrderFulfillPage_dot_quantityToFulfill": {
"context": "quantity of fulfilled products", "context": "quantity of fulfilled products",
"string": "Quantity to fulfill" "string": "Quantity to fulfill"
@ -4632,10 +4712,18 @@
"context": "checkbox label", "context": "checkbox label",
"string": "Send shipment details to customer" "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": { "src_dot_orders_dot_components_dot_OrderFulfillPage_dot_sku": {
"context": "product's sku", "context": "product's sku",
"string": "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": { "src_dot_orders_dot_components_dot_OrderFulfillPage_dot_submitFulfillment": {
"context": "fulfill order, button", "context": "fulfill order, button",
"string": "Fulfill" "string": "Fulfill"
@ -4644,6 +4732,46 @@
"context": "prepare order fulfillment, button", "context": "prepare order fulfillment, button",
"string": "Prepare fulfillment" "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": { "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_addTracking": {
"context": "add tracking button", "context": "add tracking button",
"string": "Add tracking" "string": "Add tracking"
@ -5345,42 +5473,14 @@
"context": "button", "context": "button",
"string": "Set maximal quantities" "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": { "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_description": {
"context": "product no longer exists error description", "context": "product no longer exists error description",
"string": "This product is no longer in database so it cant be replaced, nor returned" "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": { "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_improperValue": {
"context": "error message", "context": "error message",
"string": "Improper value" "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": { "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_title": {
"context": "product no longer exists error title", "context": "product no longer exists error title",
"string": "Product no longer exists" "string": "Product no longer exists"
@ -5393,14 +5493,6 @@
"context": "section header", "context": "section header",
"string": "Unfulfilled Items" "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": { "src_dot_orders_dot_components_dot_OrderReturnPage_dot_appTitle": {
"context": "page header with order number", "context": "page header with order number",
"string": "Order #{orderNumber}" "string": "Order #{orderNumber}"

View file

@ -74,12 +74,22 @@ export const fragmentOrderLine = gql`
fragment OrderLine on OrderLine { fragment OrderLine on OrderLine {
id id
isShippingRequired isShippingRequired
allocations {
id
quantity
warehouse {
id
}
}
variant { variant {
id id
quantityAvailable quantityAvailable
preorder { preorder {
endDate endDate
} }
stocks {
...Stock
}
} }
productName productName
productSku productSku
@ -324,3 +334,60 @@ export const fragmentShopOrderSettings = gql`
fulfillmentAllowUnpaid 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 quantity
quantityAllocated quantityAllocated
warehouse { warehouse {
id ...Warehouse
name
} }
} }
`; `;

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` export const OrderLineFragmentDoc = gql`
fragment OrderLine on OrderLine { fragment OrderLine on OrderLine {
id id
isShippingRequired isShippingRequired
allocations {
id
quantity
warehouse {
id
}
}
variant { variant {
id id
quantityAvailable quantityAvailable
preorder { preorder {
endDate endDate
} }
stocks {
...Stock
}
} }
productName productName
productSku productSku
@ -1230,7 +1256,7 @@ export const OrderLineFragmentDoc = gql`
url url
} }
} }
`; ${StockFragmentDoc}`;
export const FulfillmentFragmentDoc = gql` export const FulfillmentFragmentDoc = gql`
fragment Fulfillment on Fulfillment { fragment Fulfillment on Fulfillment {
id id
@ -1421,6 +1447,61 @@ export const ShopOrderSettingsFragmentDoc = gql`
fulfillmentAllowUnpaid 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` export const PageTypeFragmentDoc = gql`
fragment PageType on PageType { fragment PageType on PageType {
id id
@ -1803,17 +1884,6 @@ export const ProductMediaFragmentDoc = gql`
oembedData oembedData
} }
`; `;
export const StockFragmentDoc = gql`
fragment Stock on Stock {
id
quantity
quantityAllocated
warehouse {
id
name
}
}
`;
export const PreorderFragmentDoc = gql` export const PreorderFragmentDoc = gql`
fragment Preorder on PreorderData { fragment Preorder on PreorderData {
globalThreshold globalThreshold
@ -2566,12 +2636,6 @@ export const AttributeValueTranslatableContentFragmentDoc = gql`
} }
} }
${AttributeChoicesTranslationFragmentDoc}`; ${AttributeChoicesTranslationFragmentDoc}`;
export const WarehouseFragmentDoc = gql`
fragment Warehouse on Warehouse {
id
name
}
`;
export const WarehouseWithShippingFragmentDoc = gql` export const WarehouseWithShippingFragmentDoc = gql`
fragment WarehouseWithShipping on Warehouse { fragment WarehouseWithShipping on Warehouse {
...Warehouse ...Warehouse
@ -8334,8 +8398,12 @@ export type OrderFulfillmentUpdateTrackingMutationHookResult = ReturnType<typeof
export type OrderFulfillmentUpdateTrackingMutationResult = Apollo.MutationResult<Types.OrderFulfillmentUpdateTrackingMutation>; export type OrderFulfillmentUpdateTrackingMutationResult = Apollo.MutationResult<Types.OrderFulfillmentUpdateTrackingMutation>;
export type OrderFulfillmentUpdateTrackingMutationOptions = Apollo.BaseMutationOptions<Types.OrderFulfillmentUpdateTrackingMutation, Types.OrderFulfillmentUpdateTrackingMutationVariables>; export type OrderFulfillmentUpdateTrackingMutationOptions = Apollo.BaseMutationOptions<Types.OrderFulfillmentUpdateTrackingMutation, Types.OrderFulfillmentUpdateTrackingMutationVariables>;
export const OrderFulfillmentApproveDocument = gql` export const OrderFulfillmentApproveDocument = gql`
mutation OrderFulfillmentApprove($id: ID!, $notifyCustomer: Boolean!) { mutation OrderFulfillmentApprove($id: ID!, $notifyCustomer: Boolean!, $allowStockToBeExceeded: Boolean) {
orderFulfillmentApprove(id: $id, notifyCustomer: $notifyCustomer) { orderFulfillmentApprove(
id: $id
notifyCustomer: $notifyCustomer
allowStockToBeExceeded: $allowStockToBeExceeded
) {
errors { errors {
...OrderError ...OrderError
} }
@ -8363,6 +8431,7 @@ export type OrderFulfillmentApproveMutationFn = Apollo.MutationFunction<Types.Or
* variables: { * variables: {
* id: // value for 'id' * id: // value for 'id'
* notifyCustomer: // value for 'notifyCustomer' * notifyCustomer: // value for 'notifyCustomer'
* allowStockToBeExceeded: // value for 'allowStockToBeExceeded'
* }, * },
* }); * });
*/ */
@ -9147,49 +9216,12 @@ export const OrderFulfillDataDocument = gql`
} }
} }
lines { lines {
id ...OrderFulfillLine
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
}
} }
number number
} }
} }
${WarehouseFragmentDoc}`; ${OrderFulfillLineFragmentDoc}`;
/** /**
* __useOrderFulfillDataQuery__ * __useOrderFulfillDataQuery__
@ -13441,7 +13473,12 @@ export type SearchStaffMembersLazyQueryHookResult = ReturnType<typeof useSearchS
export type SearchStaffMembersQueryResult = Apollo.QueryResult<Types.SearchStaffMembersQuery, Types.SearchStaffMembersQueryVariables>; export type SearchStaffMembersQueryResult = Apollo.QueryResult<Types.SearchStaffMembersQuery, Types.SearchStaffMembersQueryVariables>;
export const SearchWarehousesDocument = gql` export const SearchWarehousesDocument = gql`
query SearchWarehouses($after: String, $first: Int!, $query: String!) { 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 { edges {
node { node {
id 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) => const getPermissionKey = (permission: string) =>
`PERMISSION_${permission}` as PrefixedPermissions; `PERMISSION_${permission}` as PrefixedPermissions;
const allPermissions: Record<PrefixedPermissions, boolean> = Object.keys( export const allPermissions: Record<PrefixedPermissions, boolean> = Object.keys(
PermissionEnum PermissionEnum
).reduce( ).reduce(
(prev, code) => ({ (prev, code) => ({

View file

@ -514,3 +514,6 @@ export const combinedMultiAutocompleteChoices = (
export const isInDevelopment = export const isInDevelopment =
!process.env.NODE_ENV || process.env.NODE_ENV === "development"; !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 { Typography } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import DefaultCardTitle from "@saleor/components/CardTitle"; import DefaultCardTitle from "@saleor/components/CardTitle";
import { FulfillmentStatus } from "@saleor/graphql"; 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 { StatusType } from "@saleor/types";
import camelCase from "lodash/camelCase"; import camelCase from "lodash/camelCase";
import React from "react"; import React from "react";
@ -12,7 +13,7 @@ const useStyles = makeStyles(
title: { title: {
width: "100%", width: "100%",
display: "flex", display: "flex",
justifyContent: "space-between" justifyContent: "flex-start"
}, },
orderNumber: { orderNumber: {
display: "inline", display: "inline",
@ -23,15 +24,26 @@ const useStyles = makeStyles(
alignSelf: "center", alignSelf: "center",
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
margin: `auto ${theme.spacing(1)} auto auto` 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({ const messages = defineMessages({
cancelled: { canceled: {
defaultMessage: "Cancelled ({quantity})", defaultMessage: "Canceled ({quantity})",
description: "cancelled fulfillment, section header" description: "canceled fulfillment, section header"
}, },
fulfilled: { fulfilled: {
defaultMessage: "Fulfilled ({quantity})", defaultMessage: "Fulfilled ({quantity})",
@ -58,7 +70,7 @@ const messages = defineMessages({
description: "unapproved fulfillment, section header" description: "unapproved fulfillment, section header"
}, },
unfulfilled: { unfulfilled: {
defaultMessage: "Unfulfilled", defaultMessage: "Unfulfilled ({quantity})",
description: "section header" description: "section header"
}, },
fulfilledFrom: { fulfilledFrom: {
@ -71,9 +83,10 @@ type CardTitleStatus = FulfillmentStatus | "unfulfilled";
type CardTitleLines = Array<{ type CardTitleLines = Array<{
quantity: number; quantity: number;
quantityToFulfill?: number;
}>; }>;
interface CardTitleProps { interface OrderCardTitleProps {
lines?: CardTitleLines; lines?: CardTitleLines;
fulfillmentOrder?: number; fulfillmentOrder?: number;
status: CardTitleStatus; status: CardTitleStatus;
@ -81,6 +94,7 @@ interface CardTitleProps {
orderNumber?: string; orderNumber?: string;
warehouseName?: string; warehouseName?: string;
withStatus?: boolean; withStatus?: boolean;
className?: string;
} }
const selectStatus = (status: CardTitleStatus) => { const selectStatus = (status: CardTitleStatus) => {
@ -100,18 +114,19 @@ const selectStatus = (status: CardTitleStatus) => {
case FulfillmentStatus.CANCELED: case FulfillmentStatus.CANCELED:
return StatusType.ERROR; return StatusType.ERROR;
default: default:
return StatusType.WARNING; return StatusType.ERROR;
} }
}; };
const CardTitle: React.FC<CardTitleProps> = ({ const OrderCardTitle: React.FC<OrderCardTitleProps> = ({
lines = [], lines = [],
fulfillmentOrder, fulfillmentOrder,
status, status,
orderNumber = "", orderNumber = "",
warehouseName, warehouseName,
withStatus = false, withStatus = false,
toolbar toolbar,
className
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles({}); const classes = useStyles({});
@ -123,35 +138,36 @@ const CardTitle: React.FC<CardTitleProps> = ({
const messageForStatus = messages[camelCase(status)] || messages.unfulfilled; const messageForStatus = messages[camelCase(status)] || messages.unfulfilled;
const totalQuantity = lines.reduce( const totalQuantity =
(resultQuantity, { quantity }) => resultQuantity + quantity, status === "unfulfilled"
0 ? lines.reduce(
); (resultQuantity, line) =>
resultQuantity + (line.quantityToFulfill ?? line.quantity),
const title = ( 0
<> )
{intl.formatMessage(messageForStatus, { : lines.reduce(
fulfillmentName, (resultQuantity, { quantity }) => resultQuantity + quantity,
quantity: totalQuantity 0
})} );
{fulfillmentName && (
<Typography className={classes.orderNumber} variant="body1">
{fulfillmentName}
</Typography>
)}
</>
);
return ( return (
<DefaultCardTitle <DefaultCardTitle
toolbar={toolbar} toolbar={toolbar}
className={className}
title={ title={
<div className={classes.title}> <div className={classes.title}>
{withStatus ? ( {withStatus && (
<Pill label={title} color={selectStatus(status)} /> <div className={classes.indicator}>
) : ( <CircleIndicator color={selectStatus(status)} />
title </div>
)} )}
<HorizontalSpacer spacing={2} />
<Typography className={classes.cardHeader}>
{intl.formatMessage(messageForStatus, {
fulfillmentName,
quantity: totalQuantity
})}
</Typography>
{!!warehouseName && ( {!!warehouseName && (
<Typography className={classes.warehouseName} variant="caption"> <Typography className={classes.warehouseName} variant="caption">
<FormattedMessage <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 { import {
OrderDetailsFragment, OrderDetailsFragment,
OrderDetailsQuery, OrderDetailsQuery,
OrderStatus OrderStatus,
WarehouseFragment
} from "@saleor/graphql"; } from "@saleor/graphql";
import { SubmitPromise } from "@saleor/hooks/useForm"; import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
@ -65,6 +66,7 @@ export interface OrderDetailsPageProps {
}>; }>;
disabled: boolean; disabled: boolean;
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
selectedWarehouse?: WarehouseFragment;
onOrderLineAdd?: () => void; onOrderLineAdd?: () => void;
onOrderLineChange?: ( onOrderLineChange?: (
id: string, id: string,
@ -91,6 +93,7 @@ export interface OrderDetailsPageProps {
onInvoiceClick(invoiceId: string); onInvoiceClick(invoiceId: string);
onInvoiceGenerate(); onInvoiceGenerate();
onInvoiceSend(invoiceId: string); onInvoiceSend(invoiceId: string);
onWarehouseChange?();
onSubmit(data: MetadataFormData): SubmitPromise; onSubmit(data: MetadataFormData): SubmitPromise;
} }
@ -115,6 +118,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
order, order,
shop, shop,
saveButtonBarState, saveButtonBarState,
selectedWarehouse,
onBack, onBack,
onBillingAddressEdit, onBillingAddressEdit,
onFulfillmentApprove, onFulfillmentApprove,
@ -137,6 +141,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
onOrderLineChange, onOrderLineChange,
onOrderLineRemove, onOrderLineRemove,
onShippingMethodEdit, onShippingMethodEdit,
onWarehouseChange,
onSubmit onSubmit
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
@ -243,6 +248,8 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid} notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid}
lines={unfulfilled} lines={unfulfilled}
onFulfill={onOrderFulfill} 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, onSubmit: () => undefined,
order: orderToFulfill, order: orderToFulfill,
saveButtonBar: "default", saveButtonBar: "default",
warehouses: warehouseList warehouse: warehouseList[0]
}; };
storiesOf("Views / Orders / Fulfill order", module) storiesOf("Views / Orders / Fulfill order", module)
@ -25,7 +25,7 @@ storiesOf("Views / Orders / Fulfill order", module)
{...props} {...props}
loading={true} loading={true}
order={undefined} order={undefined}
warehouses={undefined} warehouse={undefined}
/> />
)) ))
.add("error", () => ( .add("error", () => (
@ -44,6 +44,4 @@ storiesOf("Views / Orders / Fulfill order", module)
]} ]}
/> />
)) ))
.add("one warehouse", () => ( .add("one warehouse", () => <OrderFulfillPage {...props} />);
<OrderFulfillPage {...props} warehouses={warehouseList.slice(0, 1)} />
));

View file

@ -1,6 +1,5 @@
import { import {
Card, Card,
CardActions,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
@ -8,19 +7,20 @@ import {
TextField, TextField,
Typography Typography
} from "@material-ui/core"; } from "@material-ui/core";
import { CSSProperties } from "@material-ui/styles";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import { Grid } from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { import {
FulfillOrderMutation, FulfillOrderMutation,
OrderErrorCode,
OrderFulfillDataQuery, OrderFulfillDataQuery,
OrderFulfillLineFragment,
OrderFulfillStockInput, OrderFulfillStockInput,
ShopOrderSettingsFragment, ShopOrderSettingsFragment,
WarehouseFragment WarehouseFragment
@ -28,91 +28,25 @@ import {
import { SubmitPromise } from "@saleor/hooks/useForm"; import { SubmitPromise } from "@saleor/hooks/useForm";
import useFormset, { FormsetData } from "@saleor/hooks/useFormset"; import useFormset, { FormsetData } from "@saleor/hooks/useFormset";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { import { Backlink, ConfirmButtonTransitionState } from "@saleor/macaw-ui";
Backlink,
ConfirmButtonTransitionState,
makeStyles
} from "@saleor/macaw-ui";
import { renderCollection } from "@saleor/misc"; import { renderCollection } from "@saleor/misc";
import { import {
getToFulfillOrderLines, getAttributesCaption,
isStockError getToFulfillOrderLines
} from "@saleor/orders/utils/data"; } from "@saleor/orders/utils/data";
import { update } from "@saleor/utils/lists";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import OrderFulfillLine from "../OrderFulfillLine/OrderFulfillLine";
import OrderFulfillStockExceededDialog from "../OrderFulfillStockExceededDialog";
import { messages } from "./messages"; import { messages } from "./messages";
import { useStyles } from "./styles";
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" }
);
interface OrderFulfillFormData { interface OrderFulfillFormData {
sendInfo: boolean; sendInfo: boolean;
trackingNumber: string;
allowStockToBeExceeded: boolean;
} }
export interface OrderFulfillSubmitData extends OrderFulfillFormData { export interface OrderFulfillSubmitData extends OrderFulfillFormData {
items: FormsetData<null, OrderFulfillStockInput[]>; items: FormsetData<null, OrderFulfillStockInput[]>;
@ -122,14 +56,16 @@ export interface OrderFulfillPageProps {
errors: FulfillOrderMutation["orderFulfill"]["errors"]; errors: FulfillOrderMutation["orderFulfill"]["errors"];
order: OrderFulfillDataQuery["order"]; order: OrderFulfillDataQuery["order"];
saveButtonBar: ConfirmButtonTransitionState; saveButtonBar: ConfirmButtonTransitionState;
warehouses: WarehouseFragment[]; warehouse: WarehouseFragment;
shopSettings?: ShopOrderSettingsFragment; shopSettings?: ShopOrderSettingsFragment;
onBack: () => void; onBack: () => void;
onSubmit: (data: OrderFulfillSubmitData) => SubmitPromise; onSubmit: (data: OrderFulfillSubmitData) => SubmitPromise;
} }
const initialFormData: OrderFulfillFormData = { const initialFormData: OrderFulfillFormData = {
sendInfo: true sendInfo: true,
trackingNumber: "",
allowStockToBeExceeded: false
}; };
const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => { const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
@ -138,7 +74,7 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
errors, errors,
order, order,
saveButtonBar, saveButtonBar,
warehouses, warehouse,
shopSettings, shopSettings,
onBack, onBack,
onSubmit onSubmit
@ -151,28 +87,50 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
null, null,
OrderFulfillStockInput[] OrderFulfillStockInput[]
>( >(
getToFulfillOrderLines(order?.lines).map(line => ({ (getToFulfillOrderLines(order?.lines) as OrderFulfillLineFragment[]).map(
data: null, line => ({
id: line.id, data: null,
label: line.variant?.attributes id: line.id,
.map(attribute => label: getAttributesCaption(line?.variant?.attributes),
attribute.values value: line?.variant?.preorder
.map(attributeValue => attributeValue.name) ? null
.join(" , ") : [
) {
.join(" / "), quantity: line.quantityToFulfill,
value: line.variant?.stocks?.map(stock => ({ warehouse: warehouse?.id
quantity: 0, }
warehouse: stock.warehouse.id ]
})) })
})) )
); );
const handleSubmit = (formData: OrderFulfillFormData) => const [
onSubmit({ displayStockExceededDialog,
setDisplayStockExceededDialog
] = React.useState(false);
const handleSubmit = ({
formData,
allowStockToBeExceeded
}: {
formData: OrderFulfillFormData;
allowStockToBeExceeded: boolean;
}) => {
setDisplayStockExceededDialog(false);
return onSubmit({
...formData, ...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 = const notAllowedToFulfillUnpaid =
shopSettings?.fulfillmentAutoApprove && shopSettings?.fulfillmentAutoApprove &&
@ -188,26 +146,21 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
return false; return false;
} }
const isAtLeastOneFulfilled = formsetData?.some(({ value }) => const isAtLeastOneFulfilled = formsetData?.some(
value?.some(({ quantity }) => quantity > 0) el => el.value?.[0]?.quantity > 0
); );
const areProperlyFulfilled = formsetData?.every(({ id, value }) => { const overfulfill = formsetData
const { lines } = order; .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( return !overfulfill && isAtLeastOneFulfilled;
({ id: lineId }) => lineId === id
);
const formQuantityFulfilled = value?.reduce(
(result, { quantity }) => result + quantity,
0
);
return formQuantityFulfilled <= quantityToFulfill;
});
return isAtLeastOneFulfilled && areProperlyFulfilled;
}; };
return ( return (
@ -224,247 +177,94 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
orderNumber: order?.number 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 }) => ( {({ change, data, submit }) => (
<> <>
<Card> <Grid>
<CardTitle <Card>
title={intl.formatMessage(messages.itemsReadyToShip)} <CardTitle
/> title={intl.formatMessage(messages.itemsReadyToShip)}
<ResponsiveTable className={classes.table}> />
<TableHead> {warehouse ? (
<TableRow> <ResponsiveTable className={classes.table}>
<TableCell className={classes.colName}> <TableHead>
<FormattedMessage {...messages.productName} /> <TableRow>
</TableCell> <TableCell className={classes.colName}>
<TableCell className={classes.colSku}> <FormattedMessage {...messages.productName} />
<FormattedMessage {...messages.sku} /> </TableCell>
</TableCell> <TableCell className={classes.colSku}>
{warehouses?.map(warehouse => ( <FormattedMessage {...messages.sku} />
<TableCell </TableCell>
key={warehouse.id} <TableCell
className={classNames( className={classNames(
classes.colQuantity, classes.colQuantity,
classes.colQuantityHeader classes.colQuantityHeader
)} )}
> >
{warehouse.name} <FormattedMessage {...messages.quantity} />
</TableCell> </TableCell>
))} <TableCell className={classes.colStock}>
<TableCell className={classes.colQuantityTotal}> <FormattedMessage {...messages.stock} />
<FormattedMessage {...messages.quantityToFulfill} /> </TableCell>
</TableCell> </TableRow>
</TableRow> </TableHead>
</TableHead> <TableBody>
<TableBody> {renderCollection(
{renderCollection( getToFulfillOrderLines(order?.lines),
getToFulfillOrderLines(order?.lines), (line: OrderFulfillLineFragment, lineIndex) => (
( <OrderFulfillLine
line: OrderFulfillDataQuery["order"]["lines"][0], line={line}
lineIndex lineIndex={lineIndex}
) => { warehouseId={warehouse?.id}
if (!line) { formsetData={formsetData}
return ( formsetChange={formsetChange}
<TableRow key={lineIndex}> />
<TableCellAvatar className={classes.colName}> )
<Skeleton /> )}
</TableCellAvatar> </TableBody>
<TableCell className={classes.colSku}> </ResponsiveTable>
<Skeleton /> ) : (
</TableCell> <Skeleton />
{warehouses?.map(warehouse => ( )}
<TableCell </Card>
className={classes.colQuantity}
key={warehouse.id}
>
<Skeleton />
</TableCell>
))}
<TableCell className={classes.colQuantityTotal}>
{" "}
<Skeleton />
</TableCell>
</TableRow>
);
}
const remainingQuantity = line.quantityToFulfill; <Card className={classes.shipmentInformationCard}>
const quantityToFulfill = formsetData[ <Typography className={classes.supportHeader}>
lineIndex <FormattedMessage {...messages.shipmentInformation} />
].value?.reduce( </Typography>
(quantityToFulfill, lineInput) => <TextField
quantityToFulfill + (lineInput.quantity || 0), value={data.trackingNumber}
0 name="trackingNumber"
); label={intl.formatMessage(messages.trackingNumber)}
const overfulfill = remainingQuantity < quantityToFulfill; fullWidth
const isPreorder = !!line.variant?.preorder; onChange={change}
/>
return ( {shopSettings?.fulfillmentAutoApprove && (
<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}>
<ControlledCheckbox <ControlledCheckbox
checked={data.sendInfo} checked={data.sendInfo}
label={intl.formatMessage(messages.sentShipmentDetails)} label={intl.formatMessage(messages.sentShipmentDetails)}
name="sendInfo" name="sendInfo"
onChange={change} onChange={change}
/> />
</CardActions> )}
)} </Card>
</Card> </Grid>
<Savebar <Savebar
disabled={!shouldEnableSave()} disabled={!shouldEnableSave()}
labels={{ labels={{
@ -481,6 +281,15 @@ const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
onSubmit={submit} onSubmit={submit}
onCancel={onBack} onCancel={onBack}
/> />
<OrderFulfillStockExceededDialog
open={displayStockExceededDialog}
lines={order?.lines}
formsetData={formsetData}
warehouseId={warehouse?.id}
confirmButtonState={saveButtonBar}
onSubmit={submit}
onClose={() => setDisplayStockExceededDialog(false)}
/>
</> </>
)} )}
</Form> </Form>

View file

@ -37,6 +37,14 @@ export const messages = defineMessages({
defaultMessage: "Quantity to fulfill", defaultMessage: "Quantity to fulfill",
description: "quantity of fulfilled products" description: "quantity of fulfilled products"
}, },
quantity: {
defaultMessage: "Quantity",
description: "Header row quantity label"
},
stock: {
defaultMessage: "Stock",
description: "Header row stock label"
},
noStock: { noStock: {
defaultMessage: "No Stock", defaultMessage: "No Stock",
description: "no variant stock in warehouse" description: "no variant stock in warehouse"
@ -44,5 +52,17 @@ export const messages = defineMessages({
sentShipmentDetails: { sentShipmentDetails: {
defaultMessage: "Send shipment details to customer", defaultMessage: "Send shipment details to customer",
description: "checkbox label" 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 React from "react";
import { renderCollection } from "../../../misc"; import { renderCollection } from "../../../misc";
import OrderCardTitle from "../OrderCardTitle";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader"; import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow"; import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import CardTitle from "../OrderReturnPage/OrderReturnRefundItemsCard/CardTitle";
import ActionButtons from "./ActionButtons"; import ActionButtons from "./ActionButtons";
import ExtraInfoLines from "./ExtraInfoLines"; import ExtraInfoLines from "./ExtraInfoLines";
import useStyles from "./styles"; import useStyles from "./styles";
@ -63,7 +63,7 @@ const OrderFulfilledProductsCard: React.FC<OrderFulfilledProductsCardProps> = pr
return ( return (
<> <>
<Card> <Card>
<CardTitle <OrderCardTitle
withStatus withStatus
lines={fulfillment?.lines} lines={fulfillment?.lines}
fulfillmentOrder={fulfillment?.fulfillmentOrder} fulfillmentOrder={fulfillment?.fulfillmentOrder}
@ -87,7 +87,7 @@ const OrderFulfilledProductsCard: React.FC<OrderFulfilledProductsCardProps> = pr
<TableHeader /> <TableHeader />
<TableBody> <TableBody>
{renderCollection(getLines(), line => ( {renderCollection(getLines(), line => (
<TableLine line={line} /> <TableLine key={line.id} line={line} />
))} ))}
</TableBody> </TableBody>
<ExtraInfoLines fulfillment={fulfillment} /> <ExtraInfoLines fulfillment={fulfillment} />

View file

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

View file

@ -1,16 +1,17 @@
import { Card, CardActions, TableBody, Typography } from "@material-ui/core"; import { Card, CardActions, TableBody, Typography } from "@material-ui/core";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; 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 { 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 { renderCollection } from "@saleor/misc";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import OrderCardTitle from "../OrderCardTitle";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader"; import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow"; import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import CardTitle from "../OrderReturnPage/OrderReturnRefundItemsCard/CardTitle";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
@ -26,6 +27,41 @@ const useStyles = makeStyles(
} }
}, },
tableLayout: "fixed" 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" } { name: "OrderUnfulfilledItems" }
@ -36,6 +72,8 @@ interface OrderUnfulfilledProductsCardProps {
notAllowedToFulfillUnpaid: boolean; notAllowedToFulfillUnpaid: boolean;
lines: OrderLineFragment[]; lines: OrderLineFragment[];
onFulfill: () => void; onFulfill: () => void;
selectedWarehouse: WarehouseFragment;
onWarehouseChange: () => null;
} }
const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps> = props => { const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps> = props => {
@ -43,7 +81,9 @@ const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps>
showFulfillmentAction, showFulfillmentAction,
notAllowedToFulfillUnpaid, notAllowedToFulfillUnpaid,
lines, lines,
onFulfill onFulfill,
selectedWarehouse,
onWarehouseChange
} = props; } = props;
const classes = useStyles({}); const classes = useStyles({});
@ -54,19 +94,30 @@ const OrderUnfulfilledProductsCard: React.FC<OrderUnfulfilledProductsCardProps>
return ( return (
<> <>
<Card> <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}> <ResponsiveTable className={classes.table}>
<TableHeader /> <TableHeader />
<TableBody> <TableBody>
{renderCollection(lines, line => ( {renderCollection(lines, line => (
<TableLine isOrderLine line={line} /> <TableLine key={line.id} isOrderLine line={line} />
))} ))}
</TableBody> </TableBody>
</ResponsiveTable> </ResponsiveTable>
{showFulfillmentAction && ( {showFulfillmentAction && (
<CardActions> <CardActions className={classes.actions}>
<Button <Button
variant="tertiary" variant="primary"
onClick={onFulfill} onClick={onFulfill}
disabled={notAllowedToFulfillUnpaid} disabled={notAllowedToFulfillUnpaid}
> >

View file

@ -14,6 +14,7 @@ import {
PaymentChargeStatusEnum, PaymentChargeStatusEnum,
SearchCustomersQuery, SearchCustomersQuery,
SearchOrderVariantQuery, SearchOrderVariantQuery,
SearchWarehousesQuery,
ShopOrderSettingsFragment, ShopOrderSettingsFragment,
WeightUnitsEnum WeightUnitsEnum
} from "@saleor/graphql"; } from "@saleor/graphql";
@ -1073,6 +1074,18 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
quantity: 2, quantity: 2,
quantityFulfilled: 2, quantityFulfilled: 2,
quantityToFulfill: 0, quantityToFulfill: 0,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: { thumbnail: {
__typename: "Image" as "Image", __typename: "Image" as "Image",
url: placeholder url: placeholder
@ -1116,7 +1129,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant", __typename: "ProductVariant",
id: "dsfsfuhb", id: "dsfsfuhb",
quantityAvailable: 10, 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 quantity: 1
@ -1143,6 +1182,18 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
quantity: 2, quantity: 2,
quantityFulfilled: 2, quantityFulfilled: 2,
quantityToFulfill: 0, quantityToFulfill: 0,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: { thumbnail: {
__typename: "Image" as "Image", __typename: "Image" as "Image",
url: placeholder url: placeholder
@ -1186,7 +1237,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant", __typename: "ProductVariant",
id: "dsfsfuhb", id: "dsfsfuhb",
quantityAvailable: 10, 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 quantity: 1
@ -1221,6 +1298,18 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
quantity: 3, quantity: 3,
quantityFulfilled: 0, quantityFulfilled: 0,
quantityToFulfill: 3, quantityToFulfill: 3,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: { thumbnail: {
__typename: "Image" as "Image", __typename: "Image" as "Image",
url: placeholder url: placeholder
@ -1264,7 +1353,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant", __typename: "ProductVariant",
id: "dsfsfuhb", id: "dsfsfuhb",
quantityAvailable: 10, 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, quantity: 2,
quantityFulfilled: 2, quantityFulfilled: 2,
quantityToFulfill: 0, quantityToFulfill: 0,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: { thumbnail: {
__typename: "Image" as "Image", __typename: "Image" as "Image",
url: placeholder url: placeholder
@ -1320,7 +1447,33 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant", __typename: "ProductVariant",
id: "dsfsfuhb", id: "dsfsfuhb",
quantityAvailable: 10, 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, quantity: 2,
quantityFulfilled: 0, quantityFulfilled: 0,
quantityToFulfill: 2, quantityToFulfill: 2,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: { thumbnail: {
__typename: "Image" as "Image", __typename: "Image" as "Image",
url: placeholder url: placeholder
@ -1514,7 +1679,33 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant", __typename: "ProductVariant",
id: "dsfsfuhb", id: "dsfsfuhb",
quantityAvailable: 10, 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, quantity: 2,
quantityFulfilled: 0, quantityFulfilled: 0,
quantityToFulfill: 2, quantityToFulfill: 2,
allocations: [
{
id: "allocation_test_id",
warehouse: {
id:
"V2FyZWhvdXNlOjk1NWY0ZDk2LWRmNTAtNGY0Zi1hOTM4LWM5MTYzYTA4YTViNg==",
__typename: "Warehouse"
},
quantity: 1,
__typename: "Allocation"
}
],
thumbnail: { thumbnail: {
__typename: "Image" as "Image", __typename: "Image" as "Image",
url: placeholder url: placeholder
@ -1569,7 +1772,33 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
__typename: "ProductVariant", __typename: "ProductVariant",
id: "dsfsfuhb", id: "dsfsfuhb",
quantityAvailable: 10, 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, fulfillmentAutoApprove: true,
fulfillmentAllowUnpaid: 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, OrderDraftListUrlQueryParams,
OrderDraftListUrlSortField, OrderDraftListUrlSortField,
orderFulfillPath, orderFulfillPath,
OrderFulfillUrlQueryParams,
orderListPath, orderListPath,
OrderListUrlQueryParams, OrderListUrlQueryParams,
OrderListUrlSortField, OrderListUrlSortField,
@ -61,9 +62,19 @@ const OrderDetails: React.FC<RouteComponentProps<any>> = ({
return <OrderDetailsComponent id={decodeURIComponent(id)} params={params} />; return <OrderDetailsComponent id={decodeURIComponent(id)} params={params} />;
}; };
const OrderFulfill: React.FC<RouteComponentProps<any>> = ({ match }) => ( const OrderFulfill: React.FC<RouteComponentProps<any>> = ({
<OrderFulfillComponent orderId={decodeURIComponent(match.params.id)} /> 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 }) => ( const OrderRefund: React.FC<RouteComponentProps<any>> = ({ match }) => (
<OrderRefundComponent orderId={decodeURIComponent(match.params.id)} /> <OrderRefundComponent orderId={decodeURIComponent(match.params.id)} />

View file

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

View file

@ -130,44 +130,7 @@ export const orderFulfillData = gql`
} }
} }
lines { lines {
id ...OrderFulfillLine
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
}
} }
number number
} }

View file

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

View file

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

View file

@ -3,13 +3,15 @@ import {
AddressFragment, AddressFragment,
AddressInput, AddressInput,
CountryCode, CountryCode,
FulfillmentFragment,
FulfillmentStatus, FulfillmentStatus,
FulfillOrderMutation,
OrderDetailsFragment, OrderDetailsFragment,
OrderErrorCode, OrderFulfillLineFragment,
OrderFulfillDataQuery, OrderFulfillStockInput,
OrderLineFragment, OrderLineFragment,
OrderLineStockDataFragment,
OrderRefundDataQuery, OrderRefundDataQuery,
StockFragment,
WarehouseFragment WarehouseFragment
} from "@saleor/graphql"; } from "@saleor/graphql";
import { FormsetData } from "@saleor/hooks/useFormset"; import { FormsetData } from "@saleor/hooks/useFormset";
@ -36,9 +38,7 @@ export interface OrderLineWithStockWarehouses {
}; };
} }
export function getToFulfillOrderLines( export function getToFulfillOrderLines(lines?: OrderLineStockDataFragment[]) {
lines?: OrderFulfillDataQuery["order"]["lines"]
) {
return lines?.filter(line => line.quantityToFulfill > 0) || []; return lines?.filter(line => line.quantityToFulfill > 0) || [];
} }
@ -276,31 +276,6 @@ export function mergeRepeatedOrderLines(
}, Array<OrderDetailsFragment["fulfillments"][0]["lines"][0]>()); }, 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>( export function addressToAddressInput<T>(
address: T & AddressFragment address: T & AddressFragment
): AddressInput { ): AddressInput {
@ -324,3 +299,83 @@ export const getVariantSearchAddress = (
return { country: order.channel.defaultCountry.code as CountryCode }; 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 { WindowTitle } from "@saleor/components/WindowTitle";
import { import {
FulfillmentFragment,
FulfillmentStatus, FulfillmentStatus,
OrderDetailsQueryResult,
OrderFulfillmentApproveMutation, OrderFulfillmentApproveMutation,
OrderFulfillmentApproveMutationVariables, OrderFulfillmentApproveMutationVariables,
OrderUpdateMutation, OrderUpdateMutation,
OrderUpdateMutationVariables, OrderUpdateMutationVariables,
useCustomerAddressesQuery, useCustomerAddressesQuery,
useWarehouseListQuery useWarehouseListQuery,
WarehouseFragment
} from "@saleor/graphql"; } from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog"; import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog";
import OrderChangeWarehouseDialog from "@saleor/orders/components/OrderChangeWarehouseDialog";
import { OrderCustomerAddressesEditDialogOutput } from "@saleor/orders/components/OrderCustomerAddressesEditDialog/types"; import { OrderCustomerAddressesEditDialogOutput } from "@saleor/orders/components/OrderCustomerAddressesEditDialog/types";
import OrderFulfillmentApproveDialog from "@saleor/orders/components/OrderFulfillmentApproveDialog"; import OrderFulfillmentApproveDialog from "@saleor/orders/components/OrderFulfillmentApproveDialog";
import OrderFulfillStockExceededDialog from "@saleor/orders/components/OrderFulfillStockExceededDialog";
import OrderInvoiceEmailSendDialog from "@saleor/orders/components/OrderInvoiceEmailSendDialog"; 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 { PartialMutationProviderOutput } from "@saleor/types";
import { mapEdgesToItems } from "@saleor/utils/maps"; import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react"; import React from "react";
@ -42,11 +49,12 @@ import {
OrderUrlQueryParams OrderUrlQueryParams
} from "../../../urls"; } from "../../../urls";
import { isAnyAddressEditModalOpen } from "../OrderDraftDetails"; import { isAnyAddressEditModalOpen } from "../OrderDraftDetails";
import { useDefaultWarehouse } from "./useDefaultWarehouse";
interface OrderNormalDetailsProps { interface OrderNormalDetailsProps {
id: string; id: string;
params: OrderUrlQueryParams; params: OrderUrlQueryParams;
data: any; data: OrderDetailsQueryResult["data"];
orderAddNote: any; orderAddNote: any;
orderInvoiceRequest: any; orderInvoiceRequest: any;
handleSubmit: any; handleSubmit: any;
@ -70,6 +78,10 @@ interface OrderNormalDetailsProps {
openModal: any; openModal: any;
closeModal: any; closeModal: any;
} }
interface ApprovalState {
fulfillment: FulfillmentFragment;
notifyCustomer: boolean;
}
export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
id, id,
@ -96,13 +108,27 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
const shop = data?.shop; const shop = data?.shop;
const navigate = useNavigator(); const navigate = useNavigator();
const warehouses = useWarehouseListQuery({ const {
data: warehousesData,
loading: warehousesLoading
} = useWarehouseListQuery({
displayLoader: true, displayLoader: true,
variables: { variables: {
first: 30 first: 30
} }
}); });
const warehouses = mapEdgesToItems(warehousesData?.warehouses);
const [fulfillmentWarehouse, setFulfillmentWarehouse] = React.useState<
WarehouseFragment
>(null);
useDefaultWarehouse({ warehouses, order, setter: setFulfillmentWarehouse }, [
warehousesData,
warehousesLoading
]);
const { const {
data: customerAddresses, data: customerAddresses,
loading: customerAddressesLoading loading: customerAddressesLoading
@ -125,6 +151,22 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
const handleBack = () => navigate(orderListUrl()); 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 ( return (
<> <>
<WindowTitle <WindowTitle
@ -167,8 +209,11 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
] ]
)} )}
shippingMethods={data?.order?.shippingMethods || []} shippingMethods={data?.order?.shippingMethods || []}
selectedWarehouse={fulfillmentWarehouse}
onOrderCancel={() => openModal("cancel")} onOrderCancel={() => openModal("cancel")}
onOrderFulfill={() => navigate(orderFulfillUrl(id))} onOrderFulfill={() =>
navigate(orderFulfillUrl(id, { warehouse: fulfillmentWarehouse?.id }))
}
onFulfillmentApprove={fulfillmentId => onFulfillmentApprove={fulfillmentId =>
navigate( navigate(
orderUrl(id, { orderUrl(id, {
@ -213,6 +258,7 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
}) })
} }
onInvoiceSend={id => openModal("invoice-send", { id })} onInvoiceSend={id => openModal("invoice-send", { id })}
onWarehouseChange={() => openModal("change-warehouse")}
onSubmit={handleSubmit} onSubmit={handleSubmit}
/> />
<OrderCannotCancelOrderDialog <OrderCannotCancelOrderDialog
@ -279,21 +325,44 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
[] []
} }
open={params.action === "approve-fulfillment"} open={params.action === "approve-fulfillment"}
onConfirm={({ notifyCustomer }) => onConfirm={({ notifyCustomer }) => {
orderFulfillmentApprove.mutate({ setCurrentApproval({
fulfillment: order?.fulfillments.find(getById(params.id)),
notifyCustomer
});
return orderFulfillmentApprove.mutate({
id: params.id, id: params.id,
notifyCustomer notifyCustomer
}) });
} }}
onClose={closeModal} 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 <OrderFulfillmentCancelDialog
confirmButtonState={orderFulfillmentCancel.opts.status} confirmButtonState={orderFulfillmentCancel.opts.status}
errors={ errors={
orderFulfillmentCancel.opts.data?.orderFulfillmentCancel.errors || [] orderFulfillmentCancel.opts.data?.orderFulfillmentCancel.errors || []
} }
open={params.action === "cancel-fulfillment"} open={params.action === "cancel-fulfillment"}
warehouses={mapEdgesToItems(warehouses?.data?.warehouses) || []} warehouses={warehouses || []}
onConfirm={variables => onConfirm={variables =>
orderFulfillmentCancel.mutate({ orderFulfillmentCancel.mutate({
id: params.id, id: params.id,
@ -325,6 +394,13 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
} }
onClose={closeModal} onClose={closeModal}
/> />
<OrderChangeWarehouseDialog
open={params.action === "change-warehouse"}
lines={order?.lines}
currentWarehouse={fulfillmentWarehouse}
onConfirm={warehouse => setFulfillmentWarehouse(warehouse)}
onClose={closeModal}
/>
<OrderInvoiceEmailSendDialog <OrderInvoiceEmailSendDialog
confirmButtonState={orderInvoiceSend.opts.status} confirmButtonState={orderInvoiceSend.opts.status}
errors={orderInvoiceSend.opts.data?.invoiceSendEmail.errors || []} 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 { WindowTitle } from "@saleor/components/WindowTitle";
import { import {
OrderFulfillDataQuery,
useFulfillOrderMutation, useFulfillOrderMutation,
useOrderFulfillDataQuery, useOrderFulfillDataQuery,
useOrderFulfillmentUpdateTrackingMutation,
useOrderFulfillSettingsQuery, useOrderFulfillSettingsQuery,
WarehouseClickAndCollectOptionEnum, useWarehouseDetailsQuery
WarehouseFragment
} from "@saleor/graphql"; } from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { extractMutationErrors } from "@saleor/misc"; import { getMutationErrors } from "@saleor/misc";
import OrderFulfillPage from "@saleor/orders/components/OrderFulfillPage"; import OrderFulfillPage, {
import { orderUrl } from "@saleor/orders/urls"; OrderFulfillSubmitData
import { getWarehousesFromOrderLines } from "@saleor/orders/utils/data"; } from "@saleor/orders/components/OrderFulfillPage";
import { OrderFulfillUrlQueryParams, orderUrl } from "@saleor/orders/urls";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
export interface OrderFulfillProps { export interface OrderFulfillProps {
orderId: string; orderId: string;
params: OrderFulfillUrlQueryParams;
} }
const resolveLocalFulfillment = ( const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId, params }) => {
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 navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); 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({ const [fulfillOrder, fulfillOrderOpts] = useFulfillOrderMutation({
onCompleted: data => { onCompleted: data => {
@ -71,10 +55,11 @@ const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
} }
}); });
const resolvedOrderLinesWarehouses = resolveLocalFulfillment( const { data: warehouseData } = useWarehouseDetailsQuery({
data?.order, variables: {
orderLinesWarehouses id: params?.warehouse
); }
});
return ( return (
<> <>
@ -100,26 +85,44 @@ const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
loading={loading || settingsLoading || fulfillOrderOpts.loading} loading={loading || settingsLoading || fulfillOrderOpts.loading}
errors={fulfillOrderOpts.data?.orderFulfill.errors} errors={fulfillOrderOpts.data?.orderFulfill.errors}
onBack={() => navigate(orderUrl(orderId))} onBack={() => navigate(orderUrl(orderId))}
onSubmit={formData => onSubmit={async (formData: OrderFulfillSubmitData) => {
extractMutationErrors( const res = await fulfillOrder({
fulfillOrder({ variables: {
variables: { input: {
input: { lines: formData.items
lines: formData.items.map(line => ({ .filter(line => !!line?.value)
.map(line => ({
orderLineId: line.id, orderLineId: line.id,
stocks: line.value 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: notifyCustomer:
settings?.shop?.fulfillmentAutoApprove && formData.sendInfo settings?.shop?.fulfillmentAutoApprove && formData.sendInfo
}, }
orderId
} }
}) });
) }
} return getMutationErrors(res);
}}
order={data?.order} order={data?.order}
saveButtonBar="default" saveButtonBar={fulfillOrderOpts.status}
warehouses={resolvedOrderLinesWarehouses} warehouse={warehouseData?.warehouse}
shopSettings={settings?.shop} shopSettings={settings?.shop}
/> />
</> </>

View file

@ -11,6 +11,7 @@ export const searchWarehouses = gql`
search: warehouses( search: warehouses(
after: $after after: $after
first: $first first: $first
sortBy: { direction: ASC, field: NAME }
filter: { search: $query } filter: { search: $query }
) { ) {
edges { 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"> = { const props: Omit<OrderDetailsPageProps, "classes"> = {
disabled: false, disabled: false,
selectedWarehouse: undefined,
onBack: () => undefined, onBack: () => undefined,
onBillingAddressEdit: undefined, onBillingAddressEdit: undefined,
onFulfillmentApprove: () => undefined, onFulfillmentApprove: () => undefined,
@ -39,6 +40,7 @@ const props: Omit<OrderDetailsPageProps, "classes"> = {
onProductClick: undefined, onProductClick: undefined,
onProfileView: () => undefined, onProfileView: () => undefined,
onShippingAddressEdit: undefined, onShippingAddressEdit: undefined,
onWarehouseChange: undefined,
onSubmit: () => undefined, onSubmit: () => undefined,
order, order,
shop: shopFixture, shop: shopFixture,