saleor-dashboard/src/orders/components/OrderFulfillPage/OrderFulfillPage.tsx

468 lines
17 KiB
TypeScript
Raw Normal View History

2020-04-20 09:37:32 +00:00
import Card from "@material-ui/core/Card";
2020-04-20 16:23:54 +00:00
import CardActions from "@material-ui/core/CardActions";
import { makeStyles, Theme } from "@material-ui/core/styles";
2020-04-20 09:37:32 +00:00
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";
2020-04-20 16:23:54 +00:00
import Typography from "@material-ui/core/Typography";
import { CSSProperties } from "@material-ui/styles";
import AppHeader from "@saleor/components/AppHeader";
import CardTitle from "@saleor/components/CardTitle";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
2020-04-20 09:37:32 +00:00
import Container from "@saleor/components/Container";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form";
2020-04-20 09:37:32 +00:00
import PageHeader from "@saleor/components/PageHeader";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
2020-04-20 09:37:32 +00:00
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useFormset, { FormsetData } from "@saleor/hooks/useFormset";
import { renderCollection } from "@saleor/misc";
import { FulfillOrder_orderFulfill_errors } from "@saleor/orders/types/FulfillOrder";
2020-04-24 11:56:28 +00:00
import {
OrderFulfillData_order,
OrderFulfillData_order_lines
} from "@saleor/orders/types/OrderFulfillData";
import {
OrderErrorCode,
OrderFulfillStockInput
} from "@saleor/types/globalTypes";
2020-04-20 16:23:54 +00:00
import { update } from "@saleor/utils/lists";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
2020-04-20 09:37:32 +00:00
2020-04-29 14:14:20 +00:00
type ClassKey =
| "actionBar"
| "table"
| "colName"
| "colQuantity"
| "colQuantityHeader"
| "colQuantityTotal"
| "colSku"
| "error"
| "full"
| "quantityInnerInput"
2020-05-08 13:38:44 +00:00
| "quantityInnerInputNoRemaining"
2020-04-29 14:14:20 +00:00
| "remainingQuantity";
const useStyles = makeStyles<Theme, OrderFulfillPageProps, ClassKey>(
2020-05-08 13:38:44 +00:00
theme => {
const inputPadding: CSSProperties = {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2)
};
return {
[theme.breakpoints.up("lg")]: {
colName: {
width: ({ warehouses }) => (warehouses?.length > 3 ? 250 : "auto")
}
},
[theme.breakpoints.only("md")]: {
colName: {
width: ({ warehouses }) => (warehouses?.length > 2 ? 250 : "auto")
}
},
actionBar: {
flexDirection: "row",
paddingLeft: theme.spacing(2) + 2
},
2020-04-29 14:14:20 +00:00
colName: {
2020-05-08 13:38:44 +00:00
width: 250
},
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"
}
2020-04-29 14:14:20 +00:00
}
2020-05-08 13:38:44 +00:00
};
},
2020-04-20 09:37:32 +00:00
{ name: "OrderFulfillPage" }
);
interface OrderFulfillFormData {
sendInfo: boolean;
}
interface OrderFulfillSubmitData extends OrderFulfillFormData {
2020-04-24 11:56:28 +00:00
items: FormsetData<null, OrderFulfillStockInput[]>;
2020-04-20 09:37:32 +00:00
}
export interface OrderFulfillPageProps {
disabled: boolean;
2020-04-27 13:42:56 +00:00
errors: FulfillOrder_orderFulfill_errors[];
2020-04-20 09:37:32 +00:00
order: OrderFulfillData_order;
saveButtonBar: ConfirmButtonTransitionState;
warehouses: WarehouseFragment[];
2020-04-20 17:18:20 +00:00
onBack: () => void;
2020-04-20 09:37:32 +00:00
onSubmit: (data: OrderFulfillSubmitData) => void;
}
const initialFormData: OrderFulfillFormData = {
sendInfo: true
};
2020-04-24 11:56:28 +00:00
function getRemainingQuantity(line: OrderFulfillData_order_lines): number {
return line.quantity - line.quantityFulfilled;
}
2020-04-29 14:14:20 +00:00
const OrderFulfillPage: React.FC<OrderFulfillPageProps> = props => {
const {
disabled,
errors,
order,
saveButtonBar,
warehouses,
onBack,
onSubmit
} = props;
2020-04-20 09:37:32 +00:00
const intl = useIntl();
2020-04-29 14:14:20 +00:00
const classes = useStyles(props);
2020-04-20 09:37:32 +00:00
2020-04-20 16:23:54 +00:00
const { change: formsetChange, data: formsetData } = useFormset<
null,
2020-04-24 11:56:28 +00:00
OrderFulfillStockInput[]
2020-04-20 16:23:54 +00:00
>(
2020-04-24 11:56:28 +00:00
order?.lines
.filter(line => getRemainingQuantity(line) > 0)
.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
}))
2020-04-20 09:37:32 +00:00
}))
);
const handleSubmit = (formData: OrderFulfillFormData) =>
onSubmit({
...formData,
items: formsetData
});
return (
<Container>
2020-04-20 17:24:54 +00:00
<AppHeader onBack={onBack}>
{order?.number
? intl.formatMessage(
{
defaultMessage: "Order #{orderNumber}",
description: "page header with order number"
},
{
orderNumber: order.number
}
)
: intl.formatMessage({
defaultMessage: "Order",
description: "page header"
})}
</AppHeader>
2020-04-20 09:37:32 +00:00
<PageHeader
title={intl.formatMessage(
{
defaultMessage: "Order no. {orderNumber} - Add Fulfillment",
description: "page header"
},
{
orderNumber: order?.number
}
)}
/>
<Form initial={initialFormData} onSubmit={handleSubmit}>
{({ change, data, submit }) => (
<>
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Items ready to ship",
description: "header"
})}
/>
<ResponsiveTable className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
2020-04-20 16:23:54 +00:00
<FormattedMessage defaultMessage="Product name" />
2020-04-20 09:37:32 +00:00
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage
defaultMessage="SKU"
description="product's sku"
/>
</TableCell>
2020-04-20 17:18:20 +00:00
{warehouses?.map(warehouse => (
2020-04-20 09:37:32 +00:00
<TableCell
key={warehouse.id}
2020-04-24 11:56:28 +00:00
className={classNames(
classes.colQuantity,
classes.colQuantityHeader
)}
2020-04-20 09:37:32 +00:00
>
{warehouse.name}
</TableCell>
))}
<TableCell className={classes.colQuantityTotal}>
<FormattedMessage
defaultMessage="Quantity to fulfill"
description="quantity of fulfilled products"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
2020-04-24 11:56:28 +00:00
{renderCollection(
order?.lines.filter(line => getRemainingQuantity(line) > 0),
(line, lineIndex) => {
if (!line) {
return (
<TableRow key={lineIndex}>
2020-04-24 11:56:28 +00:00
<TableCellAvatar className={classes.colName}>
<Skeleton />
</TableCellAvatar>
<TableCell className={classes.colSku}>
2020-04-20 17:18:20 +00:00
<Skeleton />
</TableCell>
2020-04-24 11:56:28 +00:00
{warehouses?.map(warehouse => (
2020-04-20 09:37:32 +00:00
<TableCell
2020-04-24 11:56:28 +00:00
className={classes.colQuantity}
key={warehouse.id}
2020-04-20 09:37:32 +00:00
>
2020-04-24 11:56:28 +00:00
<Skeleton />
2020-04-20 09:37:32 +00:00
</TableCell>
2020-04-24 11:56:28 +00:00
))}
<TableCell className={classes.colQuantityTotal}>
{" "}
<Skeleton />
</TableCell>
</TableRow>
);
}
const remainingQuantity = getRemainingQuantity(line);
const quantityToFulfill = formsetData[
lineIndex
].value.reduce(
(quantityToFulfill, lineInput) =>
quantityToFulfill + (lineInput.quantity || 0),
0
);
const overfulfill = remainingQuantity < quantityToFulfill;
return (
<TableRow key={line.id}>
<TableCellAvatar
className={classes.colName}
thumbnail={line?.thumbnail?.url}
>
{line.productName}
<Typography color="textSecondary" variant="caption">
{line.variant.attributes
.map(attribute =>
attribute.values
.map(attributeValue => attributeValue.name)
.join(", ")
)
.join(" / ")}
</Typography>
</TableCellAvatar>
<TableCell className={classes.colSku}>
{line.variant.sku}
</TableCell>
{warehouses?.map(warehouse => {
const warehouseStock = line.variant.stocks.find(
stock => stock.warehouse.id === warehouse.id
2020-04-20 09:37:32 +00:00
);
2020-04-24 11:56:28 +00:00
const formsetStock = formsetData[
lineIndex
].value.find(
line => line.warehouse === warehouse.id
);
if (!warehouseStock) {
return (
<TableCell
key="skeleton"
2020-04-24 11:56:28 +00:00
className={classNames(
classes.colQuantity,
classes.error
)}
>
<FormattedMessage
defaultMessage="No Stock"
description="no variant stock in warehouse"
/>
</TableCell>
);
}
2020-04-20 09:37:32 +00:00
2020-04-24 11:56:28 +00:00
const availableQuantity =
warehouseStock.quantity -
warehouseStock.quantityAllocated;
2020-04-21 11:13:39 +00:00
2020-04-24 11:56:28 +00:00
return (
<TableCell
className={classes.colQuantity}
key={warehouseStock.id}
>
2020-05-08 13:38:44 +00:00
<TextField
type="number"
inputProps={{
className: classNames(
classes.quantityInnerInput,
{
[classes.quantityInnerInputNoRemaining]: !line
.variant.trackInventory
}
),
max: (
2020-05-08 13:38:44 +00:00
line.variant.trackInventory &&
warehouseStock.quantity
).toString(),
2020-05-08 13:38:44 +00:00
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
2020-04-20 16:23:54 +00:00
)
2020-05-08 13:38:44 +00:00
)
}
error={
overfulfill ||
(line.variant.trackInventory &&
2020-04-27 13:42:56 +00:00
formsetStock.quantity >
2020-05-08 13:38:44 +00:00
availableQuantity) ||
!!errors?.find(
err =>
err.warehouse === warehouse.id &&
err.orderLine === line.id &&
err.code ===
OrderErrorCode.INSUFFICIENT_STOCK
)
}
InputProps={{
endAdornment: line.variant
.trackInventory && (
<div
className={classes.remainingQuantity}
>
/ {availableQuantity}
</div>
)
}}
/>
2020-04-24 11:56:28 +00:00
</TableCell>
);
})}
<TableCell
className={classes.colQuantityTotal}
key="total"
>
2020-04-24 11:56:28 +00:00
<span
className={classNames({
[classes.error]: overfulfill,
[classes.full]:
remainingQuantity <= quantityToFulfill
})}
>
{quantityToFulfill}
</span>{" "}
/ {remainingQuantity}
</TableCell>
</TableRow>
);
}
)}
2020-04-20 09:37:32 +00:00
</TableBody>
</ResponsiveTable>
2020-04-20 16:23:54 +00:00
<CardActions className={classes.actionBar}>
<ControlledCheckbox
checked={data.sendInfo}
label={intl.formatMessage({
defaultMessage: "Send shipment details to customer",
description: "checkbox"
})}
name="sendInfo"
onChange={change}
/>
</CardActions>
2020-04-20 09:37:32 +00:00
</Card>
<SaveButtonBar
disabled={disabled}
2020-04-20 16:23:54 +00:00
labels={{
save: intl.formatMessage({
defaultMessage: "Fulfill",
description: "fulfill order, button"
})
}}
2020-04-20 09:37:32 +00:00
state={saveButtonBar}
onSave={submit}
onCancel={onBack}
/>
</>
)}
</Form>
</Container>
);
};
OrderFulfillPage.displayName = "OrderFulfillPage";
export default OrderFulfillPage;