diff --git a/src/hooks/useFormset.ts b/src/hooks/useFormset.ts index 8891d48b5..eb506e272 100644 --- a/src/hooks/useFormset.ts +++ b/src/hooks/useFormset.ts @@ -18,8 +18,8 @@ export interface UseFormsetOutput { set: (data: FormsetData) => void; } function useFormset( - initial: FormsetData -): UseFormsetOutput { + initial: FormsetData +): UseFormsetOutput { const [data, setData] = useStateFromProps>( initial || [] ); diff --git a/src/orders/components/OrderFulfillPage/OrderFulfillPage.stories.tsx b/src/orders/components/OrderFulfillPage/OrderFulfillPage.stories.tsx new file mode 100644 index 000000000..58e12a34b --- /dev/null +++ b/src/orders/components/OrderFulfillPage/OrderFulfillPage.stories.tsx @@ -0,0 +1,28 @@ +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "@saleor/storybook/Decorator"; +import { warehouseList } from "@saleor/warehouses/fixtures"; +import OrderFulfillPage, { OrderFulfillPageProps } from "./OrderFulfillPage"; +import { orderToFulfill } from "./fixtures"; + +const props: OrderFulfillPageProps = { + disabled: false, + onBack: () => undefined, + onSubmit: () => undefined, + order: orderToFulfill, + saveButtonBar: "default", + warehouses: warehouseList +}; + +storiesOf("Views / Orders / Fulfill order", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ( + + )); diff --git a/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx b/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx new file mode 100644 index 000000000..fecf6dcda --- /dev/null +++ b/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx @@ -0,0 +1,270 @@ +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import Card from "@material-ui/core/Card"; +import Typography from "@material-ui/core/Typography"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TextField from "@material-ui/core/TextField"; +import classNames from "classnames"; + +import useFormset, { FormsetData } from "@saleor/hooks/useFormset"; +import { StockInput } from "@saleor/types/globalTypes"; +import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment"; +import TableCellAvatar, { + AVATAR_MARGIN +} from "@saleor/components/TableCellAvatar"; +import Container from "@saleor/components/Container"; +import PageHeader from "@saleor/components/PageHeader"; +import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import Form from "@saleor/components/Form"; +import { OrderFulfillData_order } from "@saleor/orders/types/OrderFulfillData"; +import CardTitle from "@saleor/components/CardTitle"; +import ResponsiveTable from "@saleor/components/ResponsiveTable"; +import makeStyles from "@material-ui/core/styles/makeStyles"; + +const useStyles = makeStyles( + theme => ({ + colName: { + width: 300 + }, + colNameLabel: { + marginLeft: AVATAR_MARGIN + }, + colQuantity: { + textAlign: "right", + width: 200 + }, + colQuantityContent: { + alignItems: "center", + display: "inline-flex" + }, + colQuantityTotal: { + textAlign: "right", + width: 180 + }, + colSku: { + textAlign: "right", + width: 120 + }, + error: { + color: theme.palette.error.main + }, + quantityInput: { + width: "4rem" + }, + remainingQuantity: { + marginLeft: theme.spacing(), + paddingTop: 14 + }, + table: { + "&&": { + tableLayout: "fixed" + } + } + }), + { name: "OrderFulfillPage" } +); + +interface OrderFulfillFormData { + sendInfo: boolean; +} +interface OrderFulfillSubmitData extends OrderFulfillFormData { + items: FormsetData; +} +export interface OrderFulfillPageProps { + disabled: boolean; + order: OrderFulfillData_order; + saveButtonBar: ConfirmButtonTransitionState; + warehouses: WarehouseFragment[]; + onBack: () => undefined; + onSubmit: (data: OrderFulfillSubmitData) => void; +} + +const initialFormData: OrderFulfillFormData = { + sendInfo: true +}; + +const OrderFulfillPage: React.FC = ({ + disabled, + order, + saveButtonBar, + warehouses, + onBack, + onSubmit +}) => { + const intl = useIntl(); + const classes = useStyles({}); + + const { change, data: formsetData } = useFormset( + order?.lines.map(line => ({ + data: null, + id: line.id, + label: line.variant.attributes + .map(attribute => + attribute.values + .map(attributeValue => attributeValue.name) + .join(" , ") + ) + .join(" / "), + value: line.variant.stocks.map(stock => ({ + quantity: 0, + warehouse: stock.warehouse.id + })) + })) + ); + + const handleSubmit = (formData: OrderFulfillFormData) => + onSubmit({ + ...formData, + items: formsetData + }); + + return ( + + +
+ {({ change, data, submit }) => ( + <> + + + + + + + + + + + + + + {warehouses.map(warehouse => ( + + {warehouse.name} + + ))} + + + + + + + {order?.lines.map((line, lineIndex) => { + const remainingQuantity = + line.quantity - line.quantityFulfilled; + + return ( + + + {line.productName} + + + {line.variant.sku} + + {warehouses.map(warehouse => { + const warehouseStock = line.variant.stocks.find( + stock => stock.warehouse.id === warehouse.id + ); + if (!warehouseStock) { + return ( + + + + ); + } + + return ( + +
+ undefined} + error={ + remainingQuantity < + formsetData[lineIndex].value[0] + .quantity || + formsetData[lineIndex].value[0].quantity > + warehouseStock.quantity + } + /> +
+ / {warehouseStock.quantity} +
+
+
+ ); + })} + + {formsetData[lineIndex].value.reduce( + (acc, stock) => acc + stock.quantity, + 0 + )}{" "} + / {remainingQuantity} + +
+ ); + })} +
+
+
+ + + )} + +
+ ); +}; + +OrderFulfillPage.displayName = "OrderFulfillPage"; +export default OrderFulfillPage; diff --git a/src/orders/components/OrderFulfillPage/fixtures.ts b/src/orders/components/OrderFulfillPage/fixtures.ts new file mode 100644 index 000000000..1e72213aa --- /dev/null +++ b/src/orders/components/OrderFulfillPage/fixtures.ts @@ -0,0 +1,163 @@ +/* eslint-disable sort-keys */ + +import { OrderFulfillData_order } from "@saleor/orders/types/OrderFulfillData"; +import { warehouseList } from "@saleor/warehouses/fixtures"; +import * as placeholderImage from "@assets/images/sample-product.jpg"; + +export const orderToFulfill: OrderFulfillData_order = { + __typename: "Order", + id: "T3JkZXI6Mg==", + lines: [ + { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjQ=", + isShippingRequired: true, + productName: "T-Shirt", + quantity: 3, + quantityFulfilled: 1, + variant: { + __typename: "ProductVariant", + id: "UHJvZHVjdFZhcmlhbnQ6Mjk2", + name: "S", + sku: "62783187", + attributes: [ + { + __typename: "SelectedAttribute", + values: [ + { + __typename: "AttributeValue", + id: "QXR0cmlidXRlVmFsdWU6MzY=", + name: "S" + } + ] + } + ], + stocks: [ + { + __typename: "Stock", + id: "U3RvY2s6NTIy", + warehouse: warehouseList[0], + quantity: 1217 + }, + { + __typename: "Stock", + id: "U3RvY2s6NTIx", + warehouse: warehouseList[1], + quantity: 1217 + }, + { + __typename: "Stock", + id: "U3RvY2s6NTIz", + warehouse: warehouseList[2], + quantity: 1217 + }, + { + __typename: "Stock", + id: "U3RvY2s6NTI0", + warehouse: warehouseList[3], + quantity: 1220 + } + ] + }, + thumbnail: { + __typename: "Image", + url: placeholderImage + } + }, + { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjU=", + isShippingRequired: true, + productName: "Lemon Juice", + quantity: 4, + quantityFulfilled: 0, + variant: { + __typename: "ProductVariant", + id: "UHJvZHVjdFZhcmlhbnQ6MTgx", + name: "2.5l", + sku: "998323583", + attributes: [ + { + __typename: "SelectedAttribute", + values: [ + { + __typename: "AttributeValue", + id: "QXR0cmlidXRlVmFsdWU6NjE=", + name: "2.5l" + } + ] + } + ], + stocks: [ + { + __typename: "Stock", + id: "U3RvY2s6NTI=", + warehouse: warehouseList[1], + quantity: 760 + }, + { + __typename: "Stock", + id: "U3RvY2s6NTE=", + warehouse: warehouseList[2], + quantity: 760 + }, + { + __typename: "Stock", + id: "U3RvY2s6NTM=", + warehouse: warehouseList[3], + quantity: 760 + } + ] + }, + thumbnail: { + __typename: "Image", + url: placeholderImage + } + }, + { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjY=", + isShippingRequired: true, + productName: "Orange Juice", + quantity: 3, + quantityFulfilled: 2, + variant: { + __typename: "ProductVariant", + id: "UHJvZHVjdFZhcmlhbnQ6MTgy", + name: "5l", + sku: "998323584", + attributes: [ + { + __typename: "SelectedAttribute", + values: [ + { + __typename: "AttributeValue", + id: "QXR0cmlidXRlVmFsdWU6NjI=", + name: "5l" + } + ] + } + ], + stocks: [ + { + __typename: "Stock", + id: "U3RvY2s6NTc=", + warehouse: warehouseList[0], + quantity: 587 + }, + { + __typename: "Stock", + id: "U3RvY2s6NTY=", + warehouse: warehouseList[2], + quantity: 587 + } + ] + }, + thumbnail: { + __typename: "Image", + url: placeholderImage + } + } + ], + number: "9123" +}; diff --git a/src/orders/components/OrderFulfillPage/index.ts b/src/orders/components/OrderFulfillPage/index.ts new file mode 100644 index 000000000..064e689ba --- /dev/null +++ b/src/orders/components/OrderFulfillPage/index.ts @@ -0,0 +1,2 @@ +export * from "./OrderFulfillPage"; +export { default } from "./OrderFulfillPage"; diff --git a/src/orders/queries.ts b/src/orders/queries.ts index b69b3c65c..4089d9ded 100644 --- a/src/orders/queries.ts +++ b/src/orders/queries.ts @@ -13,6 +13,10 @@ import { SearchOrderVariant as SearchOrderVariantType, SearchOrderVariantVariables } from "./types/SearchOrderVariant"; +import { + OrderFulfillData, + OrderFulfillDataVariables +} from "./types/OrderFulfillData"; export const fragmentOrderEvent = gql` fragment OrderEventFragment on OrderEvent { @@ -334,3 +338,44 @@ export const useOrderVariantSearch = makeTopLevelSearch< SearchOrderVariantType, SearchOrderVariantVariables >(searchOrderVariant); + +const orderFulfillData = gql` + query OrderFulfillData($orderId: ID!) { + order(id: $orderId) { + id + lines { + id + isShippingRequired + productName + quantity + quantityFulfilled + variant { + id + name + sku + attributes { + values { + id + name + } + } + stocks { + id + warehouse { + id + } + quantity + } + } + thumbnail(size: 64) { + url + } + } + number + } + } +`; +export const useOrderFulfillData = makeQuery< + OrderFulfillData, + OrderFulfillDataVariables +>(orderFulfillData); diff --git a/src/orders/types/OrderFulfillData.ts b/src/orders/types/OrderFulfillData.ts new file mode 100644 index 000000000..b9fbf9bf4 --- /dev/null +++ b/src/orders/types/OrderFulfillData.ts @@ -0,0 +1,70 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: OrderFulfillData +// ==================================================== + +export interface OrderFulfillData_order_lines_variant_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; +} + +export interface OrderFulfillData_order_lines_variant_attributes { + __typename: "SelectedAttribute"; + values: (OrderFulfillData_order_lines_variant_attributes_values | null)[]; +} + +export interface OrderFulfillData_order_lines_variant_stocks_warehouse { + __typename: "Warehouse"; + id: string; +} + +export interface OrderFulfillData_order_lines_variant_stocks { + __typename: "Stock"; + id: string; + warehouse: OrderFulfillData_order_lines_variant_stocks_warehouse; + quantity: number; +} + +export interface OrderFulfillData_order_lines_variant { + __typename: "ProductVariant"; + id: string; + name: string; + sku: string; + attributes: OrderFulfillData_order_lines_variant_attributes[]; + stocks: (OrderFulfillData_order_lines_variant_stocks | null)[] | null; +} + +export interface OrderFulfillData_order_lines_thumbnail { + __typename: "Image"; + url: string; +} + +export interface OrderFulfillData_order_lines { + __typename: "OrderLine"; + id: string; + isShippingRequired: boolean; + productName: string; + quantity: number; + quantityFulfilled: number; + variant: OrderFulfillData_order_lines_variant | null; + thumbnail: OrderFulfillData_order_lines_thumbnail | null; +} + +export interface OrderFulfillData_order { + __typename: "Order"; + id: string; + lines: (OrderFulfillData_order_lines | null)[]; + number: string | null; +} + +export interface OrderFulfillData { + order: OrderFulfillData_order | null; +} + +export interface OrderFulfillDataVariables { + orderId: string; +} diff --git a/src/orders/views/OrderFulfill/OrderFulfill.tsx b/src/orders/views/OrderFulfill/OrderFulfill.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/orders/views/OrderFulfill/index.ts b/src/orders/views/OrderFulfill/index.ts new file mode 100644 index 000000000..e69de29bb