feat(dashboard,core-flows,medusa): update fulfillment flows (#7589)
* fix: fulfillment ops * fix: cancel fulfillment route * fix: adjustInventoryLevelsStep throwing * feat: cancel order and fix endpoint * fix: type * feat: order domain sdk * feat: delete unused file * fix: import
This commit is contained in:
+3
-1
@@ -1,5 +1,7 @@
|
||||
import type { FulfillmentStatus } from "@medusajs/medusa"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { FulfillmentStatus } from "@medusajs/types"
|
||||
|
||||
import { getOrderFulfillmentStatus } from "../../../../../lib/order-helpers"
|
||||
import { StatusCell } from "../../common/status-cell"
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||
|
||||
import { client } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const ORDERS_QUERY_KEY = "orders" as const
|
||||
export const ordersQueryKeys = queryKeysFactory(ORDERS_QUERY_KEY)
|
||||
|
||||
export const useOrder = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => client.orders.retrieve(id, query),
|
||||
queryKey: ordersQueryKeys.detail(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useOrders = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => client.orders.list(query),
|
||||
queryKey: ordersQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
QueryKey,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { sdk } from "../../lib/client"
|
||||
|
||||
const ORDERS_QUERY_KEY = "orders" as const
|
||||
export const ordersQueryKeys = queryKeysFactory(ORDERS_QUERY_KEY)
|
||||
|
||||
export const useOrder = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.order.retrieve(id, query),
|
||||
queryKey: ordersQueryKeys.detail(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useOrders = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.order.list(query),
|
||||
queryKey: ordersQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateOrderFulfillment = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<any, Error, any>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: any) =>
|
||||
sdk.admin.order.createFulfillment(orderId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCancelOrderFulfillment = (
|
||||
orderId: string,
|
||||
fulfillmentId: string,
|
||||
options?: UseMutationOptions<any, Error, any>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: { no_notification?: boolean }) =>
|
||||
sdk.admin.order.cancelFulfillment(orderId, fulfillmentId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCancelOrder = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<any, Error, any>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => sdk.admin.order.cancel(orderId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -76,6 +76,7 @@
|
||||
"back": "Back",
|
||||
"close": "Close",
|
||||
"continue": "Continue",
|
||||
"reset": "Reset",
|
||||
"confirm": "Confirm",
|
||||
"edit": "Edit",
|
||||
"download": "Download",
|
||||
@@ -572,6 +573,7 @@
|
||||
"inStock": "In stock",
|
||||
"itemsToFulfillDesc": "Choose items and quantities to fulfill",
|
||||
"locationDescription": "Choose which location you want to fulfill items from.",
|
||||
"sendNotificationHint": "Notify customers about the created fulfillment.",
|
||||
"error": {
|
||||
"wrongQuantity": "Only one item is available for fulfillment",
|
||||
"wrongQuantity_other": "Quantity should be a number between 1 and {{number}}",
|
||||
|
||||
@@ -8,7 +8,6 @@ import { fulfillmentProviders } from "./fulfillment-providers"
|
||||
import { fulfillments } from "./fulfillments"
|
||||
import { inventoryItems } from "./inventory"
|
||||
import { invites } from "./invites"
|
||||
import { orders } from "./orders"
|
||||
import { payments } from "./payments"
|
||||
import { priceLists } from "./price-lists"
|
||||
import { productTypes } from "./product-types"
|
||||
|
||||
@@ -3,10 +3,18 @@ import { CreateFulfillmentDTO } from "@medusajs/types"
|
||||
import { FulfillmentRes } from "../../types/api-responses"
|
||||
import { postRequest } from "./common"
|
||||
|
||||
/**
|
||||
* Create a fulfillment.
|
||||
* NOTE: to create a fulfillment on an order use orders/:id/fulfillments endpoint
|
||||
*/
|
||||
async function createFulfillment(payload: CreateFulfillmentDTO) {
|
||||
return postRequest<FulfillmentRes>(`/admin/fulfillments`, payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a fulfillment.
|
||||
* NOTE: to cancel a fulfillment on an order use orders/:id/fulfillments/:id/cancel endpoint
|
||||
*/
|
||||
async function cancelFulfillment(id: string) {
|
||||
return postRequest<FulfillmentRes>(`/admin/fulfillments/${id}/cancel`)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { getRequest } from "./common"
|
||||
import { OrderListRes, OrderRes } from "../../types/api-responses"
|
||||
|
||||
async function retrieveOrder(id: string, query?: Record<string, any>) {
|
||||
return getRequest<OrderRes, Record<string, any>>(`/admin/orders/${id}`, query)
|
||||
}
|
||||
|
||||
async function listOrders(query?: Record<string, any>) {
|
||||
return getRequest<OrderListRes, Record<string, any>>(`/admin/orders`, query)
|
||||
}
|
||||
|
||||
export const orders = {
|
||||
list: listOrders,
|
||||
retrieve: retrieveOrder,
|
||||
}
|
||||
+10
-10
@@ -14,22 +14,22 @@ import { useTranslation } from "react-i18next"
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { DataGrid } from "../../../../../components/grid/data-grid/index.ts"
|
||||
import { CurrencyCell } from "../../../../../components/grid/grid-cells/common/currency-cell/index.ts"
|
||||
import { DataGridMeta } from "../../../../../components/grid/types.ts"
|
||||
import { DataGrid } from "../../../../../components/grid/data-grid/index"
|
||||
import { CurrencyCell } from "../../../../../components/grid/grid-cells/common/currency-cell/index"
|
||||
import { DataGridMeta } from "../../../../../components/grid/types"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal/index.ts"
|
||||
import { useCurrencies } from "../../../../../hooks/api/currencies.tsx"
|
||||
import { useRegions } from "../../../../../hooks/api/regions.tsx"
|
||||
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options.ts"
|
||||
import { useStore } from "../../../../../hooks/api/store.tsx"
|
||||
} from "../../../../../components/route-modal/index"
|
||||
import { useCurrencies } from "../../../../../hooks/api/currencies"
|
||||
import { useRegions } from "../../../../../hooks/api/regions"
|
||||
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||
import { useStore } from "../../../../../hooks/api/store"
|
||||
import {
|
||||
getDbAmount,
|
||||
getPresentationalAmount,
|
||||
} from "../../../../../lib/money-amount-helpers.ts"
|
||||
import { ExtendedProductDTO } from "../../../../../types/api-responses.ts"
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
import { ExtendedProductDTO } from "../../../../../types/api-responses"
|
||||
|
||||
const getInitialCurrencyPrices = (prices: PriceDTO[]) => {
|
||||
const ret: Record<string, number> = {}
|
||||
|
||||
+45
-85
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { AdminOrder } from "@medusajs/types"
|
||||
import { Alert, Button, Select, toast } from "@medusajs/ui"
|
||||
import { Alert, Button, Select, Switch, toast } from "@medusajs/ui"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
@@ -12,13 +12,12 @@ import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useCreateFulfillment } from "../../../../../hooks/api/fulfillment"
|
||||
import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { cleanNonValues, pick } from "../../../../../lib/common"
|
||||
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
||||
import { CreateFulfillmentSchema } from "./constants"
|
||||
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
|
||||
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
|
||||
import { CreateFulfillmentSchema } from "./constants"
|
||||
|
||||
type OrderCreateFulfillmentFormProps = {
|
||||
order: AdminOrder
|
||||
@@ -30,8 +29,8 @@ export function OrderCreateFulfillmentForm({
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const { mutateAsync: createOrderFulfillment, isLoading: isMutating } =
|
||||
useCreateFulfillment()
|
||||
const { mutateAsync: createOrderFulfillment, isPending: isMutating } =
|
||||
useCreateOrderFulfillment(order.id)
|
||||
|
||||
const { fulfillment_providers } = useFulfillmentProviders({
|
||||
region_id: order.region_id,
|
||||
@@ -43,14 +42,11 @@ export function OrderCreateFulfillmentForm({
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
|
||||
defaultValues: {
|
||||
quantity: fulfillableItems.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.id] = getFulfillableQuantity(item)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, number>
|
||||
),
|
||||
// send_notification: !order.no_notification,
|
||||
quantity: fulfillableItems.reduce((acc, item) => {
|
||||
acc[item.id] = getFulfillableQuantity(item)
|
||||
return acc
|
||||
}, {} as Record<string, number>),
|
||||
send_notification: !order.no_notification,
|
||||
},
|
||||
resolver: zodResolver(CreateFulfillmentSchema),
|
||||
})
|
||||
@@ -61,49 +57,13 @@ export function OrderCreateFulfillmentForm({
|
||||
try {
|
||||
await createOrderFulfillment({
|
||||
location_id: data.location_id,
|
||||
/**
|
||||
* TODO: send notification flag
|
||||
*/
|
||||
// no_notification: !data.send_notification,
|
||||
delivery_address: cleanNonValues(
|
||||
pick(order.shipping_address, [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"phone",
|
||||
"company",
|
||||
"address_1",
|
||||
"address_2",
|
||||
"city",
|
||||
"country_code",
|
||||
"province",
|
||||
"postal_code",
|
||||
"metadata",
|
||||
])
|
||||
), // TODO: this should be pulled from order in the workflow
|
||||
provider_id: fulfillment_providers[0]?.id,
|
||||
no_notification: !data.send_notification,
|
||||
items: Object.entries(data.quantity)
|
||||
.filter(([, value]) => !!value)
|
||||
.map(([item_id, quantity]) => {
|
||||
const item = order.items.find((i) => i.id === item_id)
|
||||
|
||||
return {
|
||||
quantity,
|
||||
line_item_id: item_id,
|
||||
title: item.title,
|
||||
barcode: item.variant.barcode || "",
|
||||
sku: item.variant_sku || "",
|
||||
}
|
||||
}),
|
||||
// TODO: should be optional in the enpoint?
|
||||
labels: [
|
||||
{
|
||||
tracking_number: "TODO",
|
||||
tracking_url: "TODO",
|
||||
label_url: "TODO",
|
||||
},
|
||||
],
|
||||
order: {}, // TODO ?
|
||||
order_id: order.id, // TEMP link for now
|
||||
.map(([id, quantity]) => ({
|
||||
id,
|
||||
quantity,
|
||||
})),
|
||||
})
|
||||
|
||||
handleSuccess(`/orders/${order.id}`)
|
||||
@@ -252,36 +212,36 @@ export function OrderCreateFulfillmentForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/*<div className="mt-8 pt-8 ">*/}
|
||||
{/* <Form.Field*/}
|
||||
{/* control={form.control}*/}
|
||||
{/* name="send_notification"*/}
|
||||
{/* render={({ field: { onChange, value, ...field } }) => {*/}
|
||||
{/* return (*/}
|
||||
{/* <Form.Item>*/}
|
||||
{/* <div className="flex items-center justify-between">*/}
|
||||
{/* <Form.Label>*/}
|
||||
{/* {t("orders.returns.sendNotification")}*/}
|
||||
{/* </Form.Label>*/}
|
||||
{/* <Form.Control>*/}
|
||||
{/* <Form.Control>*/}
|
||||
{/* <Switch*/}
|
||||
{/* checked={!!value}*/}
|
||||
{/* onCheckedChange={onChange}*/}
|
||||
{/* {...field}*/}
|
||||
{/* />*/}
|
||||
{/* </Form.Control>*/}
|
||||
{/* </Form.Control>*/}
|
||||
{/* </div>*/}
|
||||
{/* <Form.Hint className="!mt-1">*/}
|
||||
{/* {t("orders.returns.sendNotificationHint")}*/}
|
||||
{/* </Form.Hint>*/}
|
||||
{/* <Form.ErrorMessage />*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/* )*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/*</div>*/}
|
||||
<div className="mt-8 pt-8 ">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="send_notification"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>
|
||||
{t("orders.returns.sendNotification")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={!!value}
|
||||
onCheckedChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.fulfillment.sendNotificationHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -21,10 +21,10 @@ import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { useCancelFulfillment } from "../../../../../hooks/api/fulfillment"
|
||||
import { useStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||
import { formatProvider } from "../../../../../lib/format-provider"
|
||||
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { useCancelOrderFulfillment } from "../../../../../hooks/api/orders"
|
||||
|
||||
type OrderFulfillmentSectionProps = {
|
||||
order: AdminOrder
|
||||
@@ -186,7 +186,7 @@ const Fulfillment = ({
|
||||
statusTimestamp = fulfillment.shipped_at
|
||||
}
|
||||
|
||||
const { mutateAsync } = useCancelFulfillment(fulfillment.id)
|
||||
const { mutateAsync } = useCancelOrderFulfillment(order.id, fulfillment.id)
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (fulfillment.shipped_at) {
|
||||
@@ -206,7 +206,7 @@ const Fulfillment = ({
|
||||
|
||||
if (res) {
|
||||
try {
|
||||
await mutateAsync(fulfillment.id)
|
||||
await mutateAsync()
|
||||
|
||||
toast.success(t("general.success"), {
|
||||
description: t("orders.fulfillment.toast.canceled"),
|
||||
|
||||
+9
-12
@@ -15,6 +15,7 @@ import {
|
||||
getOrderFulfillmentStatus,
|
||||
getOrderPaymentStatus,
|
||||
} from "../../../../../lib/order-helpers"
|
||||
import { useCancelOrder } from "../../../../../hooks/api/orders"
|
||||
|
||||
type OrderGeneralSectionProps = {
|
||||
order: Order
|
||||
@@ -24,7 +25,7 @@ export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = { mutateAsync: () => {} } // cancel order
|
||||
const { mutateAsync } = useCancelOrder(order.id)
|
||||
|
||||
const handleCancel = async () => {
|
||||
const res = await prompt({
|
||||
@@ -40,7 +41,7 @@ export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined)
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -60,17 +61,18 @@ export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<PaymentBadge order={order} />
|
||||
{/*TODO: SHOW ORDER STATUS INSTEAD OF FULFILLMENT STATUS HERE - if the last fulfillment is canceled it looks like the order is canceled*/}
|
||||
<FulfillmentBadge order={order} />
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
// {
|
||||
// label: t("actions.cancel"),
|
||||
// onClick: handleCancel,
|
||||
// icon: <XCircle />,
|
||||
// },
|
||||
{
|
||||
label: t("actions.cancel"),
|
||||
onClick: handleCancel,
|
||||
icon: <XCircle />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
@@ -83,11 +85,6 @@ export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
||||
const FulfillmentBadge = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* TODO: revisit when Order<>Fulfillment are linked
|
||||
*/
|
||||
return null
|
||||
|
||||
const { label, color } = getOrderFulfillmentStatus(
|
||||
t,
|
||||
order.fulfillment_status
|
||||
|
||||
@@ -3,7 +3,6 @@ const DEFAULT_PROPERTIES = [
|
||||
"status",
|
||||
"created_at",
|
||||
"email",
|
||||
// "fulfillment_status", // -> TODO replacement for this
|
||||
// "payment_status", // -> TODO replacement for this
|
||||
"display_id",
|
||||
"currency_code",
|
||||
|
||||
@@ -4,7 +4,6 @@ const DEFAULT_PROPERTIES = [
|
||||
"created_at",
|
||||
"email",
|
||||
"display_id",
|
||||
// "fulfillment_status", // -> TODO replacement for this
|
||||
// "payment_status", // -> TODO replacement for this
|
||||
"total",
|
||||
"currency_code",
|
||||
|
||||
Reference in New Issue
Block a user