From 189b03c485a2a48d73d0f10cb71ec2e30146d179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:54:22 +0100 Subject: [PATCH] feat(dashboard) admin 3.0 order edit (#6665) **What** - added Order Edit creation flow **NOTES** - since the state is managed on the server upon changing input / adding items a request is fired to update the edit - on save we only confirm the edit --- **TODO** - [x] item removal functionality --- https://github.com/medusajs/medusa/assets/16856471/01aa85ea-1fb1-4dff-9cf4-d8d79029c2cc --- .../public/locales/en-US/translation.json | 14 + .../money-amount-cell/money-amount-cell.tsx | 16 +- .../src/providers/router-provider/v1.tsx | 4 + .../order-summary-section.tsx | 2 +- .../routes/orders/order-detail/constants.ts | 2 +- .../components/order-edit-form/index.tsx | 1 + .../order-edit-form/order-edit-form.tsx | 359 ++++++++++++++++++ .../order-edit-form/order-edit-item.tsx | 112 ++++++ .../components/variant-table/index.tsx | 1 + .../use-variant-table-columns.tsx | 97 +++++ .../use-variant-table-filters.tsx | 17 + .../variant-table/use-variant-table-query.tsx | 32 ++ .../variant-table/variant-table.tsx | 163 ++++++++ .../src/routes/orders/order-edit/index.ts | 2 + .../src/routes/orders/order-edit/loader.ts | 25 ++ .../routes/orders/order-edit/order-edit.tsx | 77 ++++ 16 files changed, 917 insertions(+), 7 deletions(-) create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/index.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-item.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/index.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/use-variant-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/components/variant-table/variant-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-edit/order-edit.tsx diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 055025a412..af1b57a3c0 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -6,6 +6,8 @@ "add": "Add", "start": "Start", "end": "End", + "open": "Open", + "close": "Close", "apply": "Apply", "range": "Range", "search": "Search", @@ -55,6 +57,7 @@ "cancel": "Cancel", "save": "Save", "continue": "Continue", + "confirm": "Confirm", "edit": "Edit", "download": "Download", "clearAll": "Clear all", @@ -257,6 +260,16 @@ "requiresAction": "Requires action" } }, + "edits": { + "title": "Edit order", + "currentItems": "Current items", + "currentItemsDescription": "Adjust item quantity or remove.", + "addItemsDescription": "You can add new items to the order.", + "addItems": "Add items", + "amountPaid": "Amount paid", + "newTotal": "New total", + "differenceDue": "Difference due" + }, "reservations": { "allocatedLabel": "Allocated", "notAllocatedLabel": "Not allocated" @@ -736,6 +749,7 @@ "availability": "Availability", "inventory": "Inventory", "optional": "Optional", + "note": "Note", "taxInclusivePricing": "Tax inclusive pricing", "taxRate": "Tax Rate", "taxCode": "Tax Code", diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx index 56a981054d..06714047ce 100644 --- a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx @@ -6,14 +6,16 @@ type MoneyAmountCellProps = { currencyCode: string amount?: number | null align?: "left" | "right" + className?: string } export const MoneyAmountCell = ({ currencyCode, amount, align = "left", + className, }: MoneyAmountCellProps) => { - if (!amount) { + if (typeof amount === "undefined" || amount === null) { return } @@ -21,10 +23,14 @@ export const MoneyAmountCell = ({ return (
{formatted}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx index 442cd2a6a2..5cd03c2614 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx @@ -120,6 +120,10 @@ export const v1Routes: RouteObject[] = [ lazy: () => import("../../routes/orders/order-transfer-ownership"), }, + { + path: "edit", + lazy: () => import("../../routes/orders/order-edit"), + }, ], }, ], diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index ec6e68a607..8a3d09b182 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -38,7 +38,7 @@ const Header = ({ order }: { order: Order }) => { actions: [ { label: t("orders.summary.editItems"), - to: "#", // TODO: Open modal to edit items + to: `/orders/${order.id}/edit`, icon: , }, { diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts index bd7e8fe093..23e80b0a8e 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts @@ -1,2 +1,2 @@ export const orderExpand = - "items,items.variant,items.variant.options,sales_channel,shipping_methods,shipping_methods.shipping_option,discounts,payments,customer,shipping_address,shipping_address.country,billing_address,billing_address.country,fulfillments,fulfillments.items,fulfillments.items.item,fulfillments.tracking_links,refunds" + "items,items.variant,items.variant.options,sales_channel,shipping_methods,shipping_methods.shipping_option,discounts,payments,customer,shipping_address,shipping_address.country,billing_address,billing_address.country,fulfillments,fulfillments.items,fulfillments.items.item,fulfillments.tracking_links,refunds,edits,edits.items,edits.items.variant,edits.items.variant.product" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/index.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/index.tsx new file mode 100644 index 0000000000..15b6e1faad --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/index.tsx @@ -0,0 +1 @@ +export * from "./order-edit-form" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-form.tsx new file mode 100644 index 0000000000..48f9625a22 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-edit/components/order-edit-form/order-edit-form.tsx @@ -0,0 +1,359 @@ +import React, { useEffect, useMemo, useState } from "react" +import { useForm } from "react-hook-form" +import * as zod from "zod" +import { useTranslation } from "react-i18next" +import { useSearchParams } from "react-router-dom" +import { + adminOrderEditsKeys, + adminOrderKeys, + useAdminCancelOrderEdit, + useAdminConfirmOrderEdit, + useAdminOrderEditAddLineItem, + useAdminUpdateOrderEdit, +} from "medusa-react" + +import { Button, clx, Heading, Text, Textarea } from "@medusajs/ui" +import { Order, OrderEdit } from "@medusajs/medusa" + +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { SplitView } from "../../../../../components/layout/split-view" +import { VariantTable } from "../variant-table" + +import { medusa, queryClient } from "../../../../../lib/medusa.ts" +import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell" +import { Form } from "../../../../../components/common/form" +import { OrderEditItem } from "./order-edit-item" + +type OrderEditFormProps = { + order: Order + orderEdit: OrderEdit +} + +const QuantitiesSchema = zod.union( + zod.record(zod.string(), zod.number().optional()), + zod.object({ note: zod.string().optional() }) +) + +export function OrderEditForm({ order, orderEdit }: OrderEditFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + const [, setSearchParams] = useSearchParams() + + /** + * STATE + */ + const [open, setOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isAddingItems, setIsAddingItems] = useState(false) + + /** + * FORM + */ + const form = useForm>({ + defaultValues: order.items.reduce( + (acc, i) => { + acc[i.id] = i.quantity + return acc + }, + { note: orderEdit.internal_note || "" } + ), + }) + + /** + * CRUD HOOKS + */ + const { mutateAsync: confirmOrderEdit } = useAdminConfirmOrderEdit( + orderEdit.id + ) + const { mutateAsync: cancelOrderEdit } = useAdminCancelOrderEdit(orderEdit.id) + const { mutateAsync: addLineItemToOrderEdit } = useAdminOrderEditAddLineItem( + orderEdit.id + ) + const { mutateAsync: updateOrderEdit } = useAdminUpdateOrderEdit(orderEdit.id) + + const onQuantityChangeComplete = async (itemId: string) => { + setIsSubmitting(true) + + const quantity = form.getValues()[itemId] + + // if (form.getValues()[itemId] === 0) { + // await medusa.admin.orderEdits.removeLineItem(orderEdit.id, itemId) + // } else + + if (quantity > 0) { + await medusa.admin.orderEdits.updateLineItem(orderEdit.id, itemId, { + quantity, + }) + } + await queryClient.invalidateQueries( + adminOrderEditsKeys.detail(orderEdit.id) + ) + + setIsSubmitting(false) + } + + const currentItems = useMemo( + () => + orderEdit?.items + .sort((i1, i2) => i1.id.localeCompare(i2.id)) + .filter((i) => i.original_item_id) || [], + [orderEdit] + ) + const addedItems = useMemo( + () => + orderEdit?.items + .sort((i1, i2) => i1.id.localeCompare(i2.id)) + .filter((i) => !i.original_item_id) || [], + [orderEdit] + ) + + /** + * EFFECTS + */ + useEffect(() => { + if (orderEdit) { + orderEdit?.items.forEach((i) => { + form.setValue(i.id, i.quantity) + }) + } + }, [orderEdit?.items.length]) + + /** + * HANDLERS + */ + const handleOpenChange = (open: boolean) => { + if (!open) { + setSearchParams( + {}, + { + replace: true, + } + ) + } + + setOpen(open) + } + + const onVariantsSelect = async (variantIds: string[]) => { + setIsAddingItems(true) + try { + await Promise.all( + variantIds.map((id) => + addLineItemToOrderEdit({ variant_id: id, quantity: 1 }) + ) + ) + + await queryClient.invalidateQueries(adminOrderKeys.detail(order.id)) + } finally { + setIsAddingItems(false) + } + + setOpen(false) + } + + const onItemRemove = async (itemId: string) => { + setIsSubmitting(true) + + const change = orderEdit.changes.find( + (change) => + change.line_item_id === itemId || + change.original_line_item_id === itemId + ) + try { + if (change) { + if (change.type === "item_add") { + await medusa.admin.orderEdits.deleteItemChange( + orderEdit.id, + change.id + ) + } + if (change.type === "item_update") { + await medusa.admin.orderEdits.deleteItemChange( + orderEdit.id, + change.id + ) + await medusa.admin.orderEdits.removeLineItem(orderEdit.id, itemId) + } + } else { + await medusa.admin.orderEdits.removeLineItem(orderEdit.id, itemId) + } + + await queryClient.invalidateQueries( + adminOrderEditsKeys.detail(orderEdit.id) + ) + } finally { + setIsSubmitting(false) + } + } + + const handleSubmit = form.handleSubmit(async (data) => { + setIsSubmitting(true) + + try { + if (data.note !== orderEdit?.internal_note) { + await updateOrderEdit({ internal_note: data.note }) + } + + await confirmOrderEdit() // TODO error notification if fails + } finally { + setIsSubmitting(false) + } + + handleSuccess(`/orders/${order.id}`) + }) + + // TODO pass on "cancel" close + const handlCancel = async () => { + await cancelOrderEdit() + + handleSuccess(`/orders/${order.id}`) + } + + return ( + +
+ +
+ + + + +
+
+ + + +
+
+ + {t("orders.edits.title")} + + + + {t("orders.edits.currentItems")} + + + {t("orders.edits.currentItemsDescription")} + + + {currentItems.map((item) => ( + + ))} + +
+ + {t("orders.edits.addItems")} + + + {t("orders.edits.addItemsDescription")} + + + {!!addedItems.length && + addedItems.map((item) => ( +
+ +
+ ))} + +
+ +
+
+ +
+
+ + {t("orders.edits.amountPaid")} + + +
+
+ + {t("orders.edits.newTotal")} + + +
+
+ + {t("orders.edits.differenceDue")} + + +
+
+ +
+ { + return ( + + + {t("fields.note")} + + +