diff --git a/packages/admin-next/dashboard/src/hooks/api/claims.tsx b/packages/admin-next/dashboard/src/hooks/api/claims.tsx index 8212de51f8..7851ca366c 100644 --- a/packages/admin-next/dashboard/src/hooks/api/claims.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/claims.tsx @@ -11,6 +11,7 @@ import { sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" import { queryKeysFactory } from "../../lib/query-key-factory" import { ordersQueryKeys } from "./orders" +import { returnsQueryKeys } from "./returns" const CLAIMS_QUERY_KEY = "claims" as const export const claimsQueryKeys = queryKeysFactory(CLAIMS_QUERY_KEY) @@ -499,6 +500,10 @@ export const useClaimConfirmRequest = ( mutationFn: (payload: HttpTypes.AdminRequestClaim) => sdk.admin.claim.request(id, payload), onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: returnsQueryKeys.all, + }) + queryClient.invalidateQueries({ queryKey: ordersQueryKeys.details(), }) @@ -509,6 +514,11 @@ export const useClaimConfirmRequest = ( queryClient.invalidateQueries({ queryKey: ordersQueryKeys.preview(orderId), }) + + queryClient.invalidateQueries({ + queryKey: claimsQueryKeys.lists(), + }) + options?.onSuccess?.(data, variables, context) }, ...options, diff --git a/packages/admin-next/dashboard/src/hooks/api/exchanges.tsx b/packages/admin-next/dashboard/src/hooks/api/exchanges.tsx new file mode 100644 index 0000000000..372a7c76a1 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/api/exchanges.tsx @@ -0,0 +1,559 @@ +import { HttpTypes } from "@medusajs/types" +import { + QueryKey, + useMutation, + UseMutationOptions, + useQuery, + UseQueryOptions, +} from "@tanstack/react-query" + +import { sdk } from "../../lib/client" +import { queryClient } from "../../lib/query-client" +import { queryKeysFactory } from "../../lib/query-key-factory" +import { ordersQueryKeys } from "./orders" +import { returnsQueryKeys } from "./returns" + +const EXCHANGES_QUERY_KEY = "exchanges" as const +export const exchangesQueryKeys = queryKeysFactory(EXCHANGES_QUERY_KEY) + +export const useExchange = ( + id: string, + query?: HttpTypes.AdminExchangeListParams, + options?: Omit< + UseQueryOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminExchangeResponse, + QueryKey + >, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: async () => sdk.admin.exchange.retrieve(id, query), + queryKey: exchangesQueryKeys.detail(id, query), + ...options, + }) + + return { ...data, ...rest } +} + +export const useExchanges = ( + query?: HttpTypes.AdminExchangeListParams, + options?: Omit< + UseQueryOptions< + HttpTypes.AdminExchangeListParams, + Error, + HttpTypes.AdminExchangeListResponse, + QueryKey + >, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: async () => sdk.admin.exchange.list(query), + queryKey: exchangesQueryKeys.list(query), + ...options, + }) + + return { ...data, ...rest } +} + +export const useCreateExchange = ( + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminCreateExchange + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminCreateExchange) => + sdk.admin.exchange.create(payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useCancelExchange = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: () => sdk.admin.exchange.cancel(id), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.details(), + }) + + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.lists(), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteExchange = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: () => sdk.admin.exchange.delete(id), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.lists(), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useAddExchangeItems = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminAddExchangeItems + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminAddExchangeItems) => + sdk.admin.exchange.addItems(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateExchangeItems = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminUpdateExchangeItem & { actionId: string } + > +) => { + return useMutation({ + mutationFn: ({ + actionId, + ...payload + }: HttpTypes.AdminUpdateExchangeItem & { actionId: string }) => { + return sdk.admin.exchange.updateItem(id, actionId, payload) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useRemoveExchangeItem = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (actionId: string) => + sdk.admin.return.removeReturnItem(id, actionId), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useAddExchangeInboundItems = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminAddExchangeInboundItems + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminAddExchangeInboundItems) => + sdk.admin.exchange.addInboundItems(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateExchangeInboundItem = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminUpdateExchangeInboundItem & { actionId: string } + > +) => { + return useMutation({ + mutationFn: ({ + actionId, + ...payload + }: HttpTypes.AdminUpdateExchangeInboundItem & { actionId: string }) => { + return sdk.admin.exchange.updateInboundItem(id, actionId, payload) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useRemoveExchangeInboundItem = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (actionId: string) => + sdk.admin.exchange.removeInboundItem(id, actionId), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.all, + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useAddExchangeInboundShipping = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminExchangeAddInboundShipping + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminExchangeAddInboundShipping) => + sdk.admin.exchange.addInboundShipping(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateExchangeInboundShipping = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminExchangeUpdateInboundShipping + > +) => { + return useMutation({ + mutationFn: ({ + actionId, + ...payload + }: HttpTypes.AdminExchangeUpdateInboundShipping & { actionId: string }) => + sdk.admin.exchange.updateInboundShipping(id, actionId, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteExchangeInboundShipping = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (actionId: string) => + sdk.admin.exchange.deleteInboundShipping(id, actionId), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useAddExchangeOutboundItems = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminAddExchangeOutboundItems + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminAddExchangeOutboundItems) => + sdk.admin.exchange.addOutboundItems(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateExchangeOutboundItems = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminUpdateExchangeOutboundItem & { actionId: string } + > +) => { + return useMutation({ + mutationFn: ({ + actionId, + ...payload + }: HttpTypes.AdminUpdateExchangeOutboundItem & { actionId: string }) => { + return sdk.admin.exchange.updateOutboundItem(id, actionId, payload) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useRemoveExchangeOutboundItem = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (actionId: string) => + sdk.admin.exchange.removeOutboundItem(id, actionId), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useAddExchangeOutboundShipping = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminExchangeAddOutboundShipping + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminExchangeAddOutboundShipping) => + sdk.admin.exchange.addOutboundShipping(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateExchangeOutboundShipping = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminExchangeUpdateOutboundShipping + > +) => { + return useMutation({ + mutationFn: ({ + actionId, + ...payload + }: HttpTypes.AdminExchangeUpdateOutboundShipping & { actionId: string }) => + sdk.admin.exchange.updateOutboundShipping(id, actionId, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteExchangeOutboundShipping = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (actionId: string) => + sdk.admin.exchange.deleteOutboundShipping(id, actionId), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useExchangeConfirmRequest = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminExchangeResponse, + Error, + HttpTypes.AdminRequestExchange + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminRequestExchange) => + sdk.admin.exchange.request(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: returnsQueryKeys.all, + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useCancelExchangeRequest = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: () => sdk.admin.exchange.cancelRequest(id), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: exchangesQueryKeys.lists(), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index d39bd74adb..224980d0ba 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -852,7 +852,7 @@ "returnTotal": "Return total", "inboundTotal": "Inbound total", "refundAmount": "Refund amount", - "outstandingAmount": "Difference amount", + "outstandingAmount": "Outstanding amount", "reason": "Reason", "reasonHint": "Choose why the customer want to return items.", "note": "Note", @@ -904,6 +904,25 @@ "onlyReturnShippingOptions": "This list will consist of only return shipping options." } }, + "exchanges": { + "create": "Create Exchange", + "manage": "Manage Exchange", + "outbound": "Outbound", + "outboundItemAdded": "{{itemsCount}}x added through exchange", + "outboundTotal": "Outbound total", + "outboundShipping": "Outbound shipping", + "outboundShippingHint": "Choose which method you want to use.", + "refundAmount": "Estimated difference", + "activeChangeError": "There is an active order change on this order. Please finish or discard the previous change.", + "actions": { + "cancelExchange": { + "successToast": "Exchange was successfully canceled." + } + }, + "tooltips": { + "onlyReturnShippingOptions": "This list will consist of only return shipping options." + } + }, "reservations": { "allocatedLabel": "Allocated", "notAllocatedLabel": "Not allocated" diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx index 5c2c898012..c2c8a35b17 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx @@ -248,6 +248,11 @@ export const RouteMap: RouteObject[] = [ lazy: () => import("../../routes/orders/order-create-claim"), }, + { + path: "exchanges", + lazy: () => + import("../../routes/orders/order-create-exchange"), + }, { path: "payments/:paymentId/refund", lazy: () => diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/add-exchange-inbound-items-table.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/add-exchange-inbound-items-table.tsx new file mode 100644 index 0000000000..e56ae22760 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/add-exchange-inbound-items-table.tsx @@ -0,0 +1,186 @@ +import { AdminOrderLineItem, DateComparisonOperator } from "@medusajs/types" +import { OnChangeFn, RowSelectionState } from "@tanstack/react-table" +import { useMemo, useState } from "react" + +import { DataTable } from "../../../../../components/table/data-table" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { getReturnableQuantity } from "../../../../../lib/rma" +import { useExchangeItemTableColumns } from "./use-exchange-item-table-columns" +import { useExchangeItemTableFilters } from "./use-exchange-item-table-filters" +import { useExchangeItemTableQuery } from "./use-exchange-item-table-query" + +const PAGE_SIZE = 50 +const PREFIX = "rit" + +type AddExchangeInboundItemsTableProps = { + onSelectionChange: (ids: string[]) => void + selectedItems: string[] + items: AdminOrderLineItem[] + currencyCode: string +} + +export const AddExchangeInboundItemsTable = ({ + onSelectionChange, + selectedItems, + items, + currencyCode, +}: AddExchangeInboundItemsTableProps) => { + const [rowSelection, setRowSelection] = useState( + selectedItems.reduce((acc, id) => { + acc[id] = true + return acc + }, {} as RowSelectionState) + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + setRowSelection(newState) + onSelectionChange(Object.keys(newState)) + } + + const { searchParams, raw } = useExchangeItemTableQuery({ + pageSize: PAGE_SIZE, + prefix: PREFIX, + }) + + const queriedItems = useMemo(() => { + const { order, offset, limit, q, created_at, updated_at } = searchParams + + let results: AdminOrderLineItem[] = items + + if (q) { + results = results.filter((i) => { + return ( + i.variant.product.title.toLowerCase().includes(q.toLowerCase()) || + i.variant.title.toLowerCase().includes(q.toLowerCase()) || + i.variant.sku?.toLowerCase().includes(q.toLowerCase()) + ) + }) + } + + if (order) { + const direction = order[0] === "-" ? "desc" : "asc" + const field = order.replace("-", "") + + results = sortItems(results, field, direction) + } + + if (created_at) { + results = filterByDate(results, created_at, "created_at") + } + + if (updated_at) { + results = filterByDate(results, updated_at, "updated_at") + } + + return results.slice(offset, offset + limit) + }, [items, currencyCode, searchParams]) + + const columns = useExchangeItemTableColumns(currencyCode) + const filters = useExchangeItemTableFilters() + + const { table } = useDataTable({ + data: queriedItems as AdminOrderLineItem[], + columns: columns, + count: queriedItems.length, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: (row) => { + return getReturnableQuantity(row.original) > 0 + }, + rowSelection: { + state: rowSelection, + updater, + }, + }) + + return ( +
+ +
+ ) +} + +const sortItems = ( + items: AdminOrderLineItem[], + field: string, + direction: "asc" | "desc" +) => { + return items.sort((a, b) => { + let aValue: any + let bValue: any + + if (field === "product_title") { + aValue = a.variant.product.title + bValue = b.variant.product.title + } else if (field === "variant_title") { + aValue = a.variant.title + bValue = b.variant.title + } else if (field === "sku") { + aValue = a.variant.sku + bValue = b.variant.sku + } + + if (aValue < bValue) { + return direction === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return direction === "asc" ? 1 : -1 + } + return 0 + }) +} + +const filterByDate = ( + items: AdminOrderLineItem[], + date: DateComparisonOperator, + field: "created_at" | "updated_at" +) => { + const { gt, gte, lt, lte } = date + + return items.filter((i) => { + const itemDate = new Date(i[field]) + let isValid = true + + if (gt) { + isValid = isValid && itemDate > new Date(gt) + } + + if (gte) { + isValid = isValid && itemDate >= new Date(gte) + } + + if (lt) { + isValid = isValid && itemDate < new Date(lt) + } + + if (lte) { + isValid = isValid && itemDate <= new Date(lte) + } + + return isValid + }) +} + +const defaultOperators = { + eq: undefined, + gt: undefined, + gte: undefined, + lt: undefined, + lte: undefined, +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/index.ts new file mode 100644 index 0000000000..dc87e2f292 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/index.ts @@ -0,0 +1 @@ +export * from "./add-exchange-inbound-items-table" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-columns.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-columns.tsx new file mode 100644 index 0000000000..5c51be2df5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-columns.tsx @@ -0,0 +1,98 @@ +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +import { + ProductCell, + ProductHeader, +} from "../../../../../components/table/table-cells/product/product-cell" +import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" +import { getReturnableQuantity } from "../../../../../lib/rma" + +const columnHelper = createColumnHelper() + +export const useExchangeItemTableColumns = (currencyCode: string) => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + const isSelectable = row.getCanSelect() + + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.display({ + id: "product", + header: () => , + cell: ({ row }) => ( + + ), + }), + columnHelper.accessor("variant.sku", { + header: t("fields.sku"), + cell: ({ getValue }) => { + return getValue() || "-" + }, + }), + columnHelper.accessor("variant.title", { + header: t("fields.variant"), + }), + columnHelper.accessor("quantity", { + header: () => ( +
+ {t("fields.quantity")} +
+ ), + cell: ({ getValue, row }) => { + return getReturnableQuantity(row.original) + }, + }), + columnHelper.accessor("refundable_total", { + header: () => ( +
+ {t("fields.price")} +
+ ), + cell: ({ getValue }) => { + const amount = getValue() || 0 + + const stylized = getStylizedAmount(amount, currencyCode) + + return ( +
+ {stylized} +
+ ) + }, + }), + ], + [t, currencyCode] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-filters.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-filters.tsx new file mode 100644 index 0000000000..d4283d5279 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-filters.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from "react-i18next" + +import { Filter } from "../../../../../components/table/data-table" + +export const useExchangeItemTableFilters = () => { + const { t } = useTranslation() + + const filters: Filter[] = [ + { + key: "created_at", + label: t("fields.createdAt"), + type: "date", + }, + { + key: "updated_at", + label: t("fields.updatedAt"), + type: "date", + }, + ] + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-query.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-query.tsx new file mode 100644 index 0000000000..d5815fd066 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-inbound-items-table/use-exchange-item-table-query.tsx @@ -0,0 +1,26 @@ +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useExchangeItemTableQuery = ({ + pageSize = 50, + prefix, +}: { + pageSize?: number + prefix?: string +}) => { + const raw = useQueryParams( + ["q", "offset", "order", "created_at", "updated_at"], + prefix + ) + + const { offset, created_at, updated_at, ...rest } = raw + + const searchParams = { + ...rest, + limit: pageSize, + offset: offset ? Number(offset) : 0, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + } + + return { searchParams, raw } +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/add-exchange-outbound-items-table.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/add-exchange-outbound-items-table.tsx new file mode 100644 index 0000000000..4cf84340ea --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/add-exchange-outbound-items-table.tsx @@ -0,0 +1,87 @@ +import { OnChangeFn, RowSelectionState } from "@tanstack/react-table" +import { useState } from "react" + +import { DataTable } from "../../../../../components/table/data-table" +import { useVariants } from "../../../../../hooks/api" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { useExchangeOutboundItemTableColumns } from "./use-exchange-outbound-item-table-columns" +import { useExchangeOutboundItemTableFilters } from "./use-exchange-outbound-item-table-filters" +import { useExchangeOutboundItemTableQuery } from "./use-exchange-outbound-item-table-query" + +const PAGE_SIZE = 50 +const PREFIX = "rit" + +type AddExchangeOutboundItemsTableProps = { + onSelectionChange: (ids: string[]) => void + selectedItems: string[] + currencyCode: string +} + +export const AddExchangeOutboundItemsTable = ({ + onSelectionChange, + selectedItems, + currencyCode, +}: AddExchangeOutboundItemsTableProps) => { + const [rowSelection, setRowSelection] = useState( + selectedItems.reduce((acc, id) => { + acc[id] = true + return acc + }, {} as RowSelectionState) + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + setRowSelection(newState) + onSelectionChange(Object.keys(newState)) + } + + const { searchParams, raw } = useExchangeOutboundItemTableQuery({ + pageSize: PAGE_SIZE, + prefix: PREFIX, + }) + + const { variants = [], count } = useVariants({ + ...searchParams, + fields: "*inventory_items.inventory.location_levels,+inventory_quantity", + }) + + const columns = useExchangeOutboundItemTableColumns(currencyCode) + const filters = useExchangeOutboundItemTableFilters() + + const { table } = useDataTable({ + data: variants, + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: (_row) => { + // TODO: Check inventory here. Check if other validations needs to be made + return true + }, + rowSelection: { + state: rowSelection, + updater, + }, + }) + + return ( +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/index.ts new file mode 100644 index 0000000000..346aa883f8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/index.ts @@ -0,0 +1 @@ +export * from "./add-exchange-outbound-items-table" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-columns.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-columns.tsx new file mode 100644 index 0000000000..466a118ee7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-columns.tsx @@ -0,0 +1,68 @@ +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +import { + ProductCell, + ProductHeader, +} from "../../../../../components/table/table-cells/product/product-cell" + +const columnHelper = createColumnHelper() + +export const useExchangeOutboundItemTableColumns = (currencyCode: string) => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + const isSelectable = row.getCanSelect() + + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.display({ + id: "product", + header: () => , + cell: ({ row }) => { + return + }, + }), + columnHelper.accessor("sku", { + header: t("fields.sku"), + cell: ({ getValue }) => { + return getValue() || "-" + }, + }), + columnHelper.accessor("title", { + header: t("fields.title"), + }), + ], + [t, currencyCode] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-filters.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-filters.tsx new file mode 100644 index 0000000000..2337c99b78 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-filters.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from "react-i18next" + +import { Filter } from "../../../../../components/table/data-table" + +export const useExchangeOutboundItemTableFilters = () => { + const { t } = useTranslation() + + const filters: Filter[] = [ + { + key: "created_at", + label: t("fields.createdAt"), + type: "date", + }, + { + key: "updated_at", + label: t("fields.updatedAt"), + type: "date", + }, + ] + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-query.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-query.tsx new file mode 100644 index 0000000000..52ef521e81 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/add-exchange-outbound-items-table/use-exchange-outbound-item-table-query.tsx @@ -0,0 +1,26 @@ +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useExchangeOutboundItemTableQuery = ({ + pageSize = 50, + prefix, +}: { + pageSize?: number + prefix?: string +}) => { + const raw = useQueryParams( + ["q", "offset", "order", "created_at", "updated_at"], + prefix + ) + + const { offset, created_at, updated_at, ...rest } = raw + + const searchParams = { + ...rest, + limit: pageSize, + offset: offset ? Number(offset) : 0, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + } + + return { searchParams, raw } +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-create-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-create-form.tsx new file mode 100644 index 0000000000..2b39ad9588 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-create-form.tsx @@ -0,0 +1,423 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { PencilSquare } from "@medusajs/icons" +import { AdminExchange, AdminOrder, AdminOrderPreview } from "@medusajs/types" +import { + Button, + CurrencyInput, + Heading, + IconButton, + Switch, + toast, +} from "@medusajs/ui" +import { useEffect, useMemo, useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/modals" + +import { Form } from "../../../../../components/common/form" +import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" +import { CreateExchangeSchemaType, ExchangeCreateSchema } from "./schema" + +import { AdminReturn } from "@medusajs/types" +import { + useCancelExchangeRequest, + useExchangeConfirmRequest, + useUpdateExchangeInboundShipping, +} from "../../../../../hooks/api/exchanges" +import { currencies } from "../../../../../lib/data/currencies" +import { ExchangeInboundSection } from "./exchange-inbound-section.tsx" +import { ExchangeOutboundSection } from "./exchange-outbound-section" + +type ReturnCreateFormProps = { + order: AdminOrder + exchange: AdminExchange + preview: AdminOrderPreview + orderReturn?: AdminReturn +} + +let IS_CANCELING = false + +export const ExchangeCreateForm = ({ + order, + preview, + exchange, + orderReturn, +}: ReturnCreateFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + /** + * STATE + */ + const [isShippingPriceEdit, setIsShippingPriceEdit] = useState(false) + const [customShippingAmount, setCustomShippingAmount] = useState(0) + + /** + * MUTATIONS + */ + const { mutateAsync: confirmExchangeRequest, isPending: isConfirming } = + useExchangeConfirmRequest(exchange.id, order.id) + + const { mutateAsync: cancelExchangeRequest, isPending: isCanceling } = + useCancelExchangeRequest(exchange.id, order.id) + + const { + mutateAsync: updateInboundShipping, + isPending: isUpdatingInboundShipping, + } = useUpdateExchangeInboundShipping(exchange.id, order.id) + + const isRequestLoading = + isConfirming || isCanceling || isUpdatingInboundShipping + + /** + * Only consider items that belong to this exchange. + */ + const previewItems = useMemo( + () => + preview?.items?.filter( + (i) => !!i.actions?.find((a) => a.exchange_id === exchange.id) + ), + [preview.items] + ) + + const inboundPreviewItems = previewItems.filter( + (item) => !!item.actions?.find((a) => a.action === "RETURN_ITEM") + ) + + const outboundPreviewItems = previewItems.filter( + (item) => !!item.actions?.find((a) => a.action === "ITEM_ADD") + ) + + /** + * FORM + */ + const form = useForm({ + defaultValues: () => { + const inboundShippingMethod = preview.shipping_methods.find((s) => { + const action = s.actions?.find((a) => a.action === "SHIPPING_ADD") + + return !!action?.return?.id + }) + + const outboundShippingMethod = preview.shipping_methods.find((s) => { + const action = s.actions?.find((a) => a.action === "SHIPPING_ADD") + + return action && !!!action?.return?.id + }) + + return Promise.resolve({ + inbound_items: inboundPreviewItems.map((i) => { + const inboundAction = i.actions?.find( + (a) => a.action === "RETURN_ITEM" + ) + + return { + item_id: i.id, + variant_id: i.variant_id, + quantity: i.detail.return_requested_quantity, + note: inboundAction?.internal_note, + reason_id: inboundAction?.details?.reason_id as string | undefined, + } + }), + outbound_items: outboundPreviewItems.map((i) => ({ + item_id: i.id, + variant_id: i.variant_id, + quantity: i.detail.quantity, + })), + inbound_option_id: inboundShippingMethod + ? inboundShippingMethod.shipping_option_id + : "", + outbound_option_id: outboundShippingMethod + ? outboundShippingMethod.shipping_option_id + : "", + location_id: orderReturn?.location_id, + send_notification: false, + }) + }, + resolver: zodResolver(ExchangeCreateSchema), + }) + + const outboundShipping = preview.shipping_methods.find((s) => { + const action = s.actions?.find((a) => a.action === "SHIPPING_ADD") + + return action && !!!action?.return?.id + }) + + const shippingOptionId = form.watch("inbound_option_id") + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await confirmExchangeRequest({ no_notification: !data.send_notification }) + + handleSuccess() + } catch (e) { + toast.error(t("general.error"), { + description: e.message, + }) + } + }) + + useEffect(() => { + if (isShippingPriceEdit) { + document.getElementById("js-shipping-input")?.focus() + } + }, [isShippingPriceEdit]) + + useEffect(() => { + /** + * Unmount hook + */ + return () => { + if (IS_CANCELING) { + cancelExchangeRequest(undefined, { + onSuccess: () => { + toast.success( + t("orders.exchanges.actions.cancelExchange.successToast") + ) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + IS_CANCELING = false + } + } + }, []) + + const shippingTotal = useMemo(() => { + const method = preview.shipping_methods.find( + (sm) => !!sm.actions?.find((a) => a.action === "SHIPPING_ADD") + ) + + return (method?.total as number) || 0 + }, [preview.shipping_methods]) + + return ( + +
+ + + +
+ {t("orders.exchanges.create")} + + + + + + {/*TOTALS SECTION*/} +
+
+ + {t("orders.returns.inboundTotal")} + + + + {getStylizedAmount( + inboundPreviewItems.reduce((acc, item) => { + const action = item.actions?.find( + (act) => act.action === "RETURN_ITEM" + ) + acc = acc + (action?.amount || 0) + + return acc + }, 0) * -1, + order.currency_code + )} + +
+ +
+ + {t("orders.exchanges.outboundTotal")} + + + + {getStylizedAmount( + outboundPreviewItems.reduce((acc, item) => { + const action = item.actions?.find( + (act) => act.action === "ITEM_ADD" + ) + acc = acc + (action?.amount || 0) + + return acc + }, 0), + order.currency_code + )} + +
+ +
+ + {t("orders.returns.inboundShipping")} + + + + {!isShippingPriceEdit && ( + setIsShippingPriceEdit(true)} + variant="transparent" + className="text-ui-fg-muted" + disabled={ + !inboundPreviewItems?.length || !shippingOptionId + } + > + + + )} + + {isShippingPriceEdit ? ( + { + let actionId + + preview.shipping_methods.forEach((s) => { + if (s.actions) { + for (let a of s.actions) { + if (a.action === "SHIPPING_ADD") { + actionId = a.id + } + } + } + }) + + if (actionId) { + updateInboundShipping( + { + actionId, + custom_price: + typeof customShippingAmount === "string" + ? null + : customShippingAmount, + }, + { + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + setIsShippingPriceEdit(false) + }} + symbol={ + currencies[order.currency_code.toUpperCase()] + .symbol_native + } + code={order.currency_code} + onValueChange={(value) => + value && setCustomShippingAmount(parseInt(value)) + } + value={customShippingAmount} + disabled={!inboundPreviewItems?.length} + /> + ) : ( + getStylizedAmount(shippingTotal, order.currency_code) + )} + +
+ +
+ + {t("orders.exchanges.outboundShipping")} + + + + {getStylizedAmount( + outboundShipping?.amount ?? 0, + order.currency_code + )} + +
+ +
+ + {t("orders.exchanges.refundAmount")} + + + {getStylizedAmount( + preview.summary.pending_difference, + order.currency_code + )} + +
+
+ {/*SEND NOTIFICATION*/} +
+ { + return ( + +
+ + + +
+ + {t("orders.returns.sendNotification")} + + + {t("orders.returns.sendNotificationHint")} + +
+
+ +
+ ) + }} + /> +
+
+
+ +
+
+ + + + +
+
+
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-item.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-item.tsx new file mode 100644 index 0000000000..21fe6dc7b6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-item.tsx @@ -0,0 +1,244 @@ +import { ChatBubble, DocumentText, XCircle, XMark } from "@medusajs/icons" +import { AdminOrderLineItem, HttpTypes } from "@medusajs/types" +import { IconButton, Input, Text } from "@medusajs/ui" +import { UseFormReturn } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { Form } from "../../../../../components/common/form" +import { Thumbnail } from "../../../../../components/common/thumbnail" +import { Combobox } from "../../../../../components/inputs/combobox" +import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell" +import { useReturnReasons } from "../../../../../hooks/api/return-reasons" + +type ExchangeInboundItemProps = { + item: AdminOrderLineItem + previewItem: AdminOrderLineItem + currencyCode: string + index: number + + onRemove: () => void + onUpdate: (payload: HttpTypes.AdminUpdateReturnItems) => void + + form: UseFormReturn +} + +function ExchangeInboundItem({ + item, + previewItem, + currencyCode, + form, + onRemove, + onUpdate, + index, +}: ExchangeInboundItemProps) { + const { t } = useTranslation() + const { return_reasons = [] } = useReturnReasons({ fields: "+label" }) + + const formItem = form.watch(`inbound_items.${index}`) + const showReturnReason = typeof formItem.reason_id === "string" + const showNote = typeof formItem.note === "string" + + return ( +
+
+
+ + +
+
+ + {item.title}{" "} + + + {item.variant?.sku && ({item.variant.sku})} +
+ + {item.variant?.product?.title} + +
+
+ +
+
+ { + return ( + + + { + const val = e.target.value + const payload = val === "" ? null : Number(val) + + field.onChange(payload) + + if (payload) { + onUpdate({ quantity: payload }) + } + }} + /> + + + + ) + }} + /> + + {t("fields.qty")} + +
+ +
+ +
+ + + form.setValue(`inbound_items.${index}.reason_id`, ""), + icon: , + }, + !showNote && { + label: t("actions.addNote"), + onClick: () => + form.setValue(`inbound_items.${index}.note`, ""), + icon: , + }, + { + label: t("actions.remove"), + onClick: onRemove, + icon: , + }, + ].filter(Boolean), + }, + ]} + /> +
+
+ <> + {/*REASON*/} + {showReturnReason && ( +
+
+ {t("orders.returns.reason")} + + {t("orders.returns.reasonHint")} + +
+ +
+
+ { + return ( + + + { + onUpdate({ reason_id: v }) + onChange(v) + }} + {...field} + options={return_reasons.map((reason) => ({ + label: reason.label, + value: reason.id, + }))} + /> + + + + ) + }} + /> +
+ { + form.setValue(`inbound_items.${index}.reason_id`, null) + + onUpdate({ reason_id: null }) + }} + > + + +
+
+ )} + + {/*NOTE*/} + {showNote && ( +
+
+ {t("orders.returns.note")} + + {t("orders.returns.noteHint")} + +
+ +
+
+ { + return ( + + + { + field.onChange(field.value) + onUpdate({ internal_note: field.value }) + }} + className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover" + /> + + + + ) + }} + /> +
+ + { + form.setValue(`inbound_items.${index}.note`, null) + + onUpdate({ internal_note: null }) + }} + > + + +
+
+ )} + +
+ ) +} + +export { ExchangeInboundItem } diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-section.tsx new file mode 100644 index 0000000000..b2c04f561e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-section.tsx @@ -0,0 +1,527 @@ +import { + AdminExchange, + AdminOrder, + AdminOrderPreview, + AdminReturn, + InventoryLevelDTO, +} from "@medusajs/types" +import { Alert, Button, Heading, Text, toast } from "@medusajs/ui" +import { useEffect, useMemo, useState } from "react" +import { useFieldArray, UseFormReturn } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { Combobox } from "../../../../../components/inputs/combobox" +import { + RouteFocusModal, + StackedFocusModal, + useStackedModal, +} from "../../../../../components/modals" +import { useShippingOptions, useStockLocations } from "../../../../../hooks/api" +import { + useAddExchangeInboundItems, + useAddExchangeInboundShipping, + useDeleteExchangeInboundShipping, + useRemoveExchangeInboundItem, + useUpdateExchangeInboundItem, +} from "../../../../../hooks/api/exchanges" +import { useUpdateReturn } from "../../../../../hooks/api/returns" +import { sdk } from "../../../../../lib/client" +import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder" +import { AddExchangeInboundItemsTable } from "../add-exchange-inbound-items-table" +import { ExchangeInboundItem } from "./exchange-inbound-item" +import { CreateExchangeSchemaType } from "./schema" + +type ExchangeInboundSectionProps = { + order: AdminOrder + orderReturn?: AdminReturn + exchange: AdminExchange + preview: AdminOrderPreview + form: UseFormReturn +} + +let itemsToAdd: string[] = [] +let itemsToRemove: string[] = [] + +export const ExchangeInboundSection = ({ + order, + preview, + exchange, + form, + orderReturn, +}: ExchangeInboundSectionProps) => { + const { t } = useTranslation() + + /** + * STATE + */ + const { setIsOpen } = useStackedModal() + const [inventoryMap, setInventoryMap] = useState< + Record + >({}) + + /** + * MUTATIONS + */ + const { mutateAsync: updateReturn } = useUpdateReturn( + preview?.order_change?.return_id!, + order.id + ) + + const { mutateAsync: addInboundShipping } = useAddExchangeInboundShipping( + exchange.id, + order.id + ) + + const { mutateAsync: deleteInboundShipping } = + useDeleteExchangeInboundShipping(exchange.id, order.id) + + const { mutateAsync: addInboundItem } = useAddExchangeInboundItems( + exchange.id, + order.id + ) + + const { mutateAsync: updateInboundItem } = useUpdateExchangeInboundItem( + exchange.id, + order.id + ) + + const { mutateAsync: removeInboundItem } = useRemoveExchangeInboundItem( + exchange.id, + order.id + ) + + /** + * Only consider items that belong to this exchange. + */ + const previewInboundItems = useMemo( + () => + preview?.items?.filter( + (i) => !!i.actions?.find((a) => a.exchange_id === exchange.id) + ), + [preview.items] + ) + + const inboundPreviewItems = previewInboundItems.filter( + (item) => !!item.actions?.find((a) => a.action === "RETURN_ITEM") + ) + + const itemsMap = useMemo( + () => new Map(order?.items?.map((i) => [i.id, i])), + [order.items] + ) + + const locationId = form.watch("location_id") + + /** + * HOOKS + */ + const { stock_locations = [] } = useStockLocations({ limit: 999 }) + const { shipping_options = [] } = useShippingOptions( + { + limit: 999, + fields: "*prices,+service_zone.fulfillment_set.location.id", + stock_location_id: locationId, + }, + { + enabled: !!locationId, + } + ) + + const inboundShippingOptions = shipping_options.filter( + (shippingOption) => + !!shippingOption.rules.find( + (r) => r.attribute === "is_return" && r.value === "true" + ) + ) + + const { + fields: inboundItems, + append, + remove, + update, + } = useFieldArray({ + name: "inbound_items", + control: form.control, + }) + + const inboundItemsMap = useMemo( + () => new Map(previewInboundItems.map((i) => [i.id, i])), + [previewInboundItems, inboundItems] + ) + + useEffect(() => { + const existingItemsMap: Record = {} + + inboundPreviewItems.forEach((i) => { + const ind = inboundItems.findIndex((field) => field.item_id === i.id) + + existingItemsMap[i.id] = true + + if (ind > -1) { + if (inboundItems[ind].quantity !== i.detail.return_requested_quantity) { + const returnItemAction = i.actions?.find( + (a) => a.action === "RETURN_ITEM" + ) + + update(ind, { + ...inboundItems[ind], + quantity: i.detail.return_requested_quantity, + note: returnItemAction?.internal_note, + reason_id: returnItemAction?.details?.reason_id as string, + }) + } + } else { + append({ item_id: i.id, quantity: i.detail.return_requested_quantity }) + } + }) + + inboundItems.forEach((i, ind) => { + if (!(i.item_id in existingItemsMap)) { + remove(ind) + } + }) + }, [previewInboundItems]) + + useEffect(() => { + const inboundShippingMethod = preview.shipping_methods.find((s) => { + const action = s.actions?.find((a) => a.action === "SHIPPING_ADD") + + return !!action?.return?.id + }) + + if (inboundShippingMethod) { + form.setValue( + "inbound_option_id", + inboundShippingMethod.shipping_option_id + ) + } + }, [preview.shipping_methods]) + + useEffect(() => { + form.setValue("location_id", orderReturn?.location_id) + }, [orderReturn]) + + const showInboundItemsPlaceholder = !inboundItems.length + + const onItemsSelected = async () => { + itemsToAdd.length && + (await addInboundItem( + { + items: itemsToAdd.map((id) => ({ + id, + quantity: 1, + })), + }, + { + onError: (error) => { + toast.error(error.message) + }, + } + )) + + for (const itemToRemove of itemsToRemove) { + const actionId = previewInboundItems + .find((i) => i.id === itemToRemove) + ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id + + if (actionId) { + await removeInboundItem(actionId, { + onError: (error) => { + toast.error(error.message) + }, + }) + } + } + + setIsOpen("inbound-items", false) + } + + const onLocationChange = async (selectedLocationId?: string | null) => { + await updateReturn({ location_id: selectedLocationId }) + } + + const onShippingOptionChange = async (selectedOptionId: string) => { + const inboundShippingMethods = preview.shipping_methods.filter((s) => { + const action = s.actions?.find((a) => a.action === "SHIPPING_ADD") + + return action && !!action?.return?.id + }) + + const promises = inboundShippingMethods + .filter(Boolean) + .map((inboundShippingMethod) => { + const action = inboundShippingMethod.actions?.find( + (a) => a.action === "SHIPPING_ADD" + ) + + if (action) { + deleteInboundShipping(action.id) + } + }) + + await Promise.all(promises) + + await addInboundShipping( + { shipping_option_id: selectedOptionId }, + { + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + + const showLevelsWarning = useMemo(() => { + if (!locationId) { + return false + } + + const allItemsHaveLocation = inboundItems + .map((_i) => { + const item = itemsMap.get(_i.item_id) + if (!item?.variant_id || !item?.variant) { + return true + } + + if (!item.variant.manage_inventory) { + return true + } + + return inventoryMap[item.variant_id]?.find( + (l) => l.location_id === locationId + ) + }) + .every(Boolean) + + return !allItemsHaveLocation + }, [inboundItems, inventoryMap, locationId]) + + useEffect(() => { + const getInventoryMap = async () => { + const ret: Record = {} + + if (!inboundItems.length) { + return ret + } + + const variantIds = inboundItems + .map((item) => item?.variant_id) + .filter(Boolean) + + const variants = ( + await sdk.admin.productVariant.list( + { id: variantIds }, + { fields: "*inventory,*inventory.location_levels" } + ) + ).variants + + variants.forEach((variant) => { + ret[variant.id] = variant.inventory?.[0]?.location_levels || [] + }) + + return ret + } + + getInventoryMap().then((map) => { + setInventoryMap(map) + }) + }, [inboundItems]) + + return ( +
+
+ {t("orders.returns.inbound")} + + + + + {t("actions.addItems")} + + + + + + + i.item_id)} + currencyCode={order.currency_code} + onSelectionChange={(finalSelection) => { + const alreadySelected = inboundItems.map((i) => i.item_id) + + itemsToAdd = finalSelection.filter( + (selection) => !alreadySelected.includes(selection) + ) + itemsToRemove = alreadySelected.filter( + (selection) => !finalSelection.includes(selection) + ) + }} + /> + + +
+
+ + + + +
+
+
+
+
+
+ + {showInboundItemsPlaceholder && } + + {inboundItems.map( + (item, index) => + inboundItemsMap.get(item.item_id) && + itemsMap.get(item.item_id)! && ( + { + const actionId = previewInboundItems + .find((i) => i.id === item.item_id) + ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id + + if (actionId) { + removeInboundItem(actionId, { + onError: (error) => { + toast.error(error.message) + }, + }) + } + }} + onUpdate={(payload) => { + const actionId = previewInboundItems + .find((i) => i.id === item.item_id) + ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id + + if (actionId) { + updateInboundItem( + { ...payload, actionId }, + { + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + }} + index={index} + /> + ) + )} + {!showInboundItemsPlaceholder && ( +
+ {/*LOCATION*/} +
+
+ {t("orders.returns.location")} + + {t("orders.returns.locationHint")} + +
+ + { + return ( + + + { + onChange(v) + onLocationChange(v) + }} + options={(stock_locations ?? []).map( + (stockLocation) => ({ + label: stockLocation.name, + value: stockLocation.id, + }) + )} + /> + + + ) + }} + /> +
+ + {/*INBOUND SHIPPING*/} +
+
+ + {t("orders.returns.inboundShipping")} + + + + {t("orders.returns.inboundShippingHint")} + +
+ + { + return ( + + + { + onChange(val) + val && onShippingOptionChange(val) + }} + {...field} + options={inboundShippingOptions.map((so) => ({ + label: so.name, + value: so.id, + }))} + disabled={!locationId} + /> + + + ) + }} + /> +
+
+ )} + {showLevelsWarning && ( + +
+ {t("orders.returns.noInventoryLevel")} +
+ + {t("orders.returns.noInventoryLevelDesc")} + +
+ )} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-item.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-item.tsx new file mode 100644 index 0000000000..d620f3efbe --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-item.tsx @@ -0,0 +1,121 @@ +import { XCircle } from "@medusajs/icons" +import { AdminOrderLineItem, HttpTypes } from "@medusajs/types" +import { Input, Text } from "@medusajs/ui" +import { UseFormReturn } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { Form } from "../../../../../components/common/form" +import { Thumbnail } from "../../../../../components/common/thumbnail" +import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell" +import { CreateExchangeSchemaType } from "./schema" + +type ExchangeOutboundItemProps = { + previewItem: AdminOrderLineItem + currencyCode: string + index: number + + onRemove: () => void + // TODO: create a payload type for outbound updates + onUpdate: (payload: HttpTypes.AdminUpdateReturnItems) => void + + form: UseFormReturn +} + +function ExchangeOutboundItem({ + previewItem, + currencyCode, + form, + onRemove, + onUpdate, + index, +}: ExchangeOutboundItemProps) { + const { t } = useTranslation() + + return ( +
+
+
+ + +
+
+ + {previewItem.title}{" "} + + + {previewItem.variant_sku && ( + ({previewItem.variant_sku}) + )} +
+ + {previewItem.product_title} + +
+
+ +
+
+ { + return ( + + + { + const val = e.target.value + const payload = val === "" ? null : Number(val) + + field.onChange(payload) + + if (payload) { + onUpdate({ quantity: payload }) + } + }} + /> + + + + ) + }} + /> + + {t("fields.qty")} + +
+ +
+ +
+ + , + }, + ].filter(Boolean), + }, + ]} + /> +
+
+
+ ) +} + +export { ExchangeOutboundItem } diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-section.tsx new file mode 100644 index 0000000000..6f8d3a3b12 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-section.tsx @@ -0,0 +1,423 @@ +import { + AdminExchange, + AdminOrder, + AdminOrderPreview, + InventoryLevelDTO, +} from "@medusajs/types" +import { Alert, Button, Heading, Text, toast } from "@medusajs/ui" +import { useEffect, useMemo, useState } from "react" +import { useFieldArray, UseFormReturn } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { Combobox } from "../../../../../components/inputs/combobox" +import { + RouteFocusModal, + StackedFocusModal, + useStackedModal, +} from "../../../../../components/modals" +import { + useAddExchangeOutboundItems, + useAddExchangeOutboundShipping, + useDeleteExchangeOutboundShipping, + useRemoveExchangeOutboundItem, + useUpdateExchangeOutboundItems, +} from "../../../../../hooks/api/exchanges" +import { useShippingOptions } from "../../../../../hooks/api/shipping-options" +import { sdk } from "../../../../../lib/client" +import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder" +import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table" +import { ExchangeOutboundItem } from "./exchange-outbound-item" +import { CreateExchangeSchemaType } from "./schema" + +type ExchangeOutboundSectionProps = { + order: AdminOrder + exchange: AdminExchange + preview: AdminOrderPreview + form: UseFormReturn +} + +let itemsToAdd: string[] = [] +let itemsToRemove: string[] = [] + +export const ExchangeOutboundSection = ({ + order, + preview, + exchange, + form, +}: ExchangeOutboundSectionProps) => { + const { t } = useTranslation() + + const { setIsOpen } = useStackedModal() + const [inventoryMap, setInventoryMap] = useState< + Record + >({}) + + /** + * HOOKS + */ + const { shipping_options = [] } = useShippingOptions({ + limit: 999, + fields: "*prices,+service_zone.fulfillment_set.location.id", + }) + + const { mutateAsync: addOutboundShipping } = useAddExchangeOutboundShipping( + exchange.id, + order.id + ) + + const { mutateAsync: deleteOutboundShipping } = + useDeleteExchangeOutboundShipping(exchange.id, order.id) + + const { mutateAsync: addOutboundItem } = useAddExchangeOutboundItems( + exchange.id, + order.id + ) + + const { mutateAsync: updateOutboundItem } = useUpdateExchangeOutboundItems( + exchange.id, + order.id + ) + + const { mutateAsync: removeOutboundItem } = useRemoveExchangeOutboundItem( + exchange.id, + order.id + ) + + /** + * Only consider items that belong to this exchange and is an outbound item + */ + const previewOutboundItems = useMemo( + () => + preview?.items?.filter( + (i) => + !!i.actions?.find( + (a) => a.exchange_id === exchange.id && a.action === "ITEM_ADD" + ) + ), + [preview.items] + ) + + const variantItemMap = useMemo( + () => new Map(order?.items?.map((i) => [i.variant_id, i])), + [order.items] + ) + + const { + fields: outboundItems, + append, + remove, + update, + } = useFieldArray({ + name: "outbound_items", + control: form.control, + }) + + const variantOutboundMap = useMemo( + () => new Map(previewOutboundItems.map((i) => [i.variant_id, i])), + [previewOutboundItems, outboundItems] + ) + + useEffect(() => { + const existingItemsMap: Record = {} + + previewOutboundItems.forEach((i) => { + const ind = outboundItems.findIndex((field) => field.item_id === i.id) + + existingItemsMap[i.id] = true + + if (ind > -1) { + if (outboundItems[ind].quantity !== i.detail.quantity) { + update(ind, { + ...outboundItems[ind], + quantity: i.detail.quantity, + }) + } + } else { + append({ + item_id: i.id, + quantity: i.detail.quantity, + variant_id: i.variant_id, + }) + } + }) + + outboundItems.forEach((i, ind) => { + if (!(i.item_id in existingItemsMap)) { + remove(ind) + } + }) + }, [previewOutboundItems]) + + const locationId = form.watch("location_id") + const showOutboundItemsPlaceholder = !outboundItems.length + + const onItemsSelected = async () => { + itemsToAdd.length && + (await addOutboundItem( + { + items: itemsToAdd.map((variantId) => ({ + variant_id: variantId, + quantity: 1, + })), + }, + { + onError: (error) => { + toast.error(error.message) + }, + } + )) + + for (const itemToRemove of itemsToRemove) { + const action = previewOutboundItems + .find((i) => i.variant_id === itemToRemove) + ?.actions?.find((a) => a.action === "ITEM_ADD") + + if (action?.id) { + await removeOutboundItem(action?.id, { + onError: (error) => { + toast.error(error.message) + }, + }) + } + } + + setIsOpen("outbound-items", false) + } + + const onShippingOptionChange = async (selectedOptionId: string) => { + const outboundShippingMethods = preview.shipping_methods.filter((s) => { + const action = s.actions?.find((a) => a.action === "SHIPPING_ADD") + + return action && !!!action?.return?.id + }) + + const promises = outboundShippingMethods + .filter(Boolean) + .map((outboundShippingMethod) => { + const action = outboundShippingMethod.actions?.find( + (a) => a.action === "SHIPPING_ADD" + ) + + if (action) { + deleteOutboundShipping(action.id) + } + }) + + await Promise.all(promises) + + await addOutboundShipping( + { shipping_option_id: selectedOptionId }, + { + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + + const showLevelsWarning = useMemo(() => { + if (!locationId) { + return false + } + + const allItemsHaveLocation = outboundItems + .map((i) => { + const item = variantItemMap.get(i.variant_id) + if (!item?.variant_id || !item?.variant) { + return true + } + + if (!item.variant.manage_inventory) { + return true + } + + return inventoryMap[item.variant_id]?.find( + (l) => l.location_id === locationId + ) + }) + .every(Boolean) + + return !allItemsHaveLocation + }, [outboundItems, inventoryMap, locationId]) + + useEffect(() => { + const getInventoryMap = async () => { + const ret: Record = {} + + if (!outboundItems.length) { + return ret + } + + const variantIds = outboundItems + .map((item) => item?.variant_id) + .filter(Boolean) + const variants = ( + await sdk.admin.productVariant.list( + { id: variantIds }, + { fields: "*inventory,*inventory.location_levels" } + ) + ).variants + + variants.forEach((variant) => { + ret[variant.id] = variant.inventory?.[0]?.location_levels || [] + }) + + return ret + } + + getInventoryMap().then((map) => { + setInventoryMap(map) + }) + }, [outboundItems]) + + return ( +
+
+ {t("orders.returns.outbound")} + + + + + {t("actions.addItems")} + + + + + + i.variant_id)} + currencyCode={order.currency_code} + onSelectionChange={(finalSelection) => { + const alreadySelected = outboundItems.map((i) => i.variant_id) + + itemsToAdd = finalSelection.filter( + (selection) => !alreadySelected.includes(selection) + ) + itemsToRemove = alreadySelected.filter( + (selection) => !finalSelection.includes(selection) + ) + }} + /> + + +
+
+ + + + +
+
+
+
+
+
+ + {showOutboundItemsPlaceholder && } + + {outboundItems.map( + (item, index) => + variantOutboundMap.get(item.variant_id) && ( + { + const actionId = previewOutboundItems + .find((i) => i.id === item.item_id) + ?.actions?.find((a) => a.action === "ITEM_ADD")?.id + + if (actionId) { + removeOutboundItem(actionId, { + onError: (error) => { + toast.error(error.message) + }, + }) + } + }} + onUpdate={(payload) => { + const actionId = previewOutboundItems + .find((i) => i.id === item.item_id) + ?.actions?.find((a) => a.action === "ITEM_ADD")?.id + + if (actionId) { + updateOutboundItem( + { ...payload, actionId }, + { + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + }} + index={index} + /> + ) + )} + {!showOutboundItemsPlaceholder && ( +
+ {/*OUTBOUND SHIPPING*/} +
+
+ {t("orders.exchanges.outboundShipping")} + + {t("orders.exchanges.outboundShippingHint")} + +
+ + { + return ( + + + { + onChange(val) + val && onShippingOptionChange(val) + }} + {...field} + options={shipping_options.map((so) => ({ + label: so.name, + value: so.id, + }))} + disabled={!shipping_options.length} + /> + + + ) + }} + /> +
+
+ )} + + {showLevelsWarning && ( + +
+ {t("orders.returns.noInventoryLevel")} +
+ + {t("orders.returns.noInventoryLevelDesc")} + +
+ )} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/index.ts new file mode 100644 index 0000000000..273fa7c136 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/index.ts @@ -0,0 +1 @@ +export * from "./exchange-create-form" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/schema.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/schema.ts new file mode 100644 index 0000000000..3bb35072c1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export const ExchangeCreateSchema = z.object({ + inbound_items: z.array( + z.object({ + item_id: z.string(), + quantity: z.number(), + reason_id: z.string().nullish(), + note: z.string().nullish(), + }) + ), + outbound_items: z.array( + z.object({ + item_id: z.string(), // TODO: variant id? + quantity: z.number(), + }) + ), + location_id: z.string().optional(), + inbound_option_id: z.string().nullish(), + send_notification: z.boolean().optional(), +}) + +export type CreateExchangeSchemaType = z.infer diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/exchange-create.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/exchange-create.tsx new file mode 100644 index 0000000000..0ff0e637da --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/exchange-create.tsx @@ -0,0 +1,84 @@ +import { toast } from "@medusajs/ui" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useNavigate, useParams } from "react-router-dom" + +import { RouteFocusModal } from "../../../components/modals" +import { useCreateExchange, useExchange } from "../../../hooks/api/exchanges" +import { useOrder, useOrderPreview } from "../../../hooks/api/orders" +import { useReturn } from "../../../hooks/api/returns" +import { DEFAULT_FIELDS } from "../order-detail/constants" +import { ExchangeCreateForm } from "./components/exchange-create-form" + +let IS_REQUEST_RUNNING = false + +export const ExchangeCreate = () => { + const { id } = useParams() + const navigate = useNavigate() + const { t } = useTranslation() + + const { order } = useOrder(id!, { + fields: DEFAULT_FIELDS, + }) + + const { order: preview } = useOrderPreview(id!) + const [activeExchangeId, setActiveExchangeId] = useState() + const { mutateAsync: createExchange } = useCreateExchange(order.id) + + const { exchange } = useExchange(activeExchangeId!, undefined, { + enabled: !!activeExchangeId, + }) + + const { return: orderReturn } = useReturn(exchange?.return_id!, undefined, { + enabled: !!exchange?.return_id, + }) + + useEffect(() => { + async function run() { + if (IS_REQUEST_RUNNING || !preview) { + return + } + + if (preview.order_change) { + if (preview.order_change.change_type === "exchange") { + setActiveExchangeId(preview.order_change.exchange_id) + } else { + navigate(`/orders/${preview.id}`, { replace: true }) + toast.error(t("orders.exchanges.activeChangeError")) + } + + return + } + + IS_REQUEST_RUNNING = true + + try { + const { exchange: createdExchange } = await createExchange({ + order_id: preview.id, + }) + + setActiveExchangeId(createdExchange.id) + } catch (e) { + toast.error(e.message) + navigate(`/orders/${preview.id}`, { replace: true }) + } finally { + IS_REQUEST_RUNNING = false + } + } + + run() + }, [preview]) + + return ( + + {exchange && preview && order && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/index.ts new file mode 100644 index 0000000000..42b2a2b047 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/index.ts @@ -0,0 +1 @@ +export { ExchangeCreate as Component } from "./exchange-create" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx index 26b29426fb..16457326d0 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx @@ -42,10 +42,13 @@ export function OrderCreateFulfillmentForm({ const form = useForm>({ defaultValues: { - quantity: fulfillableItems.reduce((acc, item) => { - acc[item.id] = getFulfillableQuantity(item) - return acc - }, {} as Record), + quantity: fulfillableItems.reduce( + (acc, item) => { + acc[item.id] = getFulfillableQuantity(item) + return acc + }, + {} as Record + ), send_notification: !order.no_notification, }, resolver: zodResolver(CreateFulfillmentSchema), @@ -108,6 +111,16 @@ export function OrderCreateFulfillmentForm({ } }, [fulfillableItems.length]) + useEffect(() => { + const itemsToFulfill = order?.items?.filter( + (item) => getFulfillableQuantity(item) > 0 + ) + + if (itemsToFulfill?.length) { + setFulfillableItems(itemsToFulfill) + } + }, [order.items]) + return (
, + disabled: + !!orderPreview?.order_change?.exchange_id || + !!orderPreview?.order_change?.claim_id, }, { - label: orderPreview?.order_change?.id - ? t("orders.claims.manage") - : t("orders.claims.create"), + label: + orderPreview?.order_change?.id && + orderPreview?.order_change?.exchange_id + ? t("orders.exchanges.manage") + : t("orders.exchanges.create"), + to: `/orders/${order.id}/exchanges`, + icon: , + disabled: + (!!orderPreview?.order_change?.return_id && + !!!orderPreview?.order_change?.exchange_id) || + !!orderPreview?.order_change?.claim_id, + }, + { + label: + orderPreview?.order_change?.id && + orderPreview?.order_change?.claim_id + ? t("orders.claims.manage") + : t("orders.claims.create"), to: `/orders/${order.id}/claims`, icon: , + disabled: + (!!orderPreview?.order_change?.return_id && + !!!orderPreview?.order_change?.claim_id) || + !!orderPreview?.order_change?.exchange_id, }, ], }, @@ -187,15 +212,17 @@ const Item = ({ reservation, returns, claims, + exchanges, }: { - item: OrderLineItemDTO + item: AdminOrderLineItem currencyCode: string reservation?: ReservationItemDTO | null returns: AdminReturn[] claims: AdminClaim[] + exchanges: AdminExchange[] }) => { const { t } = useTranslation() - const isInventoryManaged = item.variant.manage_inventory + const isInventoryManaged = item.variant?.manage_inventory return ( <> @@ -214,6 +241,7 @@ const Item = ({ > {item.title} + {item.variant_sku && (
{item.variant_sku} @@ -221,22 +249,25 @@ const Item = ({
)} - {item.variant?.options.map((o) => o.value).join(" · ")} + {item.variant?.options?.map((o) => o.value).join(" · ")} +
{getLocaleAmount(item.unit_price, currencyCode)}
+
{item.quantity}x
+
{isInventoryManaged && (
+
{getLocaleAmount(item.subtotal || 0, currencyCode)} @@ -257,6 +289,7 @@ const Item = ({
+ {returns.map((r) => ( ))} @@ -264,6 +297,14 @@ const Item = ({ {claims.map((claim) => ( ))} + + {exchanges.map((exchange) => ( + + ))} ) } @@ -273,41 +314,24 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => { line_item_id: order.items.map((i) => i.id), }) - const { claims } = useClaims({ + const { claims = [] } = useClaims({ order_id: order.id, fields: "*additional_items", }) - const { returns } = useReturns({ + const { exchanges = [] } = useExchanges({ + order_id: order.id, + fields: "*additional_items", + }) + + const { returns = [] } = useReturns({ order_id: order.id, fields: "*items,*items.reason", }) - const itemsReturnsMap = useMemo(() => { - if (!returns) { - return {} - } - - const ret = {} - - order.items?.forEach((i) => { - returns.forEach((r) => { - if (r.items.some((ri) => ri.item_id === i.id)) { - if (ret[i.id]) { - ret[i.id].push(r) - } else { - ret[i.id] = [r] - } - } - }) - }) - - return ret - }, [returns]) - return (
- {order.items.map((item) => { + {order.items?.map((item) => { const reservation = reservations ? reservations.find((r) => r.line_item_id === item.id) : null @@ -318,8 +342,9 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => { item={item} currencyCode={order.currency_code} reservation={reservation} - returns={itemsReturnsMap[item.id] || []} - claims={claims || []} + returns={returns} + exchanges={exchanges} + claims={claims} /> ) })} @@ -490,6 +515,45 @@ const ClaimBreakdown = ({ ) } +const ExchangeBreakdown = ({ + exchange, + itemId, +}: { + exchange: AdminExchange + itemId: string +}) => { + const { t } = useTranslation() + const { getRelativeDate } = useDate() + const items = exchange.additional_items.filter( + (item) => item?.item?.id === itemId + ) + + return ( + !!items.length && ( +
+
+ + + {t(`orders.exchanges.outboundItemAdded`, { + itemsCount: items.reduce( + (acc, item) => (acc = acc + item.quantity), + 0 + ), + })} + +
+ + + {getRelativeDate(exchange.created_at)} + +
+ ) + ) +} + const Total = ({ order }: { order: AdminOrder }) => { const { t } = useTranslation() @@ -524,9 +588,13 @@ const Total = ({ order }: { order: AdminOrder }) => { > {t("orders.returns.outstandingAmount")} - + {getStylizedAmount( - order.summary.difference_sum || 0, + order.summary.pending_difference || 0, order.currency_code )} diff --git a/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts b/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts index 7e3acd8be9..f72d74c448 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts @@ -6,7 +6,12 @@ import { OrderExchangeDTO, OrderPreviewDTO, } from "@medusajs/types" -import { ChangeActionType, Modules, OrderChangeStatus } from "@medusajs/utils" +import { + ChangeActionType, + Modules, + OrderChangeStatus, + ReturnStatus, +} from "@medusajs/utils" import { WorkflowResponse, createStep, @@ -19,7 +24,7 @@ import { createRemoteLinkStep, useRemoteQueryStep } from "../../../common" import { reserveInventoryStep } from "../../../definition/cart/steps/reserve-inventory" import { prepareConfirmInventoryInput } from "../../../definition/cart/utils/prepare-confirm-inventory-input" import { createReturnFulfillmentWorkflow } from "../../../fulfillment/workflows/create-return-fulfillment" -import { previewOrderChangeStep } from "../../steps" +import { previewOrderChangeStep, updateReturnsStep } from "../../steps" import { confirmOrderChanges } from "../../steps/confirm-order-changes" import { createOrderExchangeItemsFromActionsStep } from "../../steps/exchange/create-exchange-items-from-actions" import { createReturnItemsFromActionsStep } from "../../steps/return/create-return-items-from-actions" @@ -57,7 +62,6 @@ function prepareFulfillmentData({ items, shippingOption, deliveryAddress, - isReturn, }: { order: OrderDTO items: any[] @@ -74,17 +78,15 @@ function prepareFulfillmentData({ } } deliveryAddress?: Record - isReturn?: boolean }) { const orderItemsMap = new Map["items"][0]>( order.items!.map((i) => [i.id, i]) ) const fulfillmentItems = items.map((i) => { - const orderItem = orderItemsMap.get(i.item_id) ?? i.item + const orderItem = orderItemsMap.get(i.id) ?? i.item return { line_item_id: i.item_id, - quantity: !isReturn ? i.quantity : undefined, - return_quantity: isReturn ? i.quantity : undefined, + quantity: i.quantity, title: orderItem.variant_title ?? orderItem.title, sku: orderItem.variant_sku || "", barcode: orderItem.variant_barcode || "", @@ -191,11 +193,8 @@ export const confirmExchangeRequestWorkflow = createWorkflow( "id", "version", "canceled_at", - "items.id", - "items.title", - "items.variant_title", - "items.variant_sku", - "items.variant_barcode", + "items.*", + "items.item.id", "shipping_address.*", ], variables: { id: orderExchange.order_id }, @@ -248,6 +247,18 @@ export const confirmExchangeRequestWorkflow = createWorkflow( } ) + when({ returnId }, ({ returnId }) => { + return !!returnId + }).then(() => { + updateReturnsStep([ + { + id: returnId, + status: ReturnStatus.REQUESTED, + requested_at: new Date(), + }, + ]) + }) + const exchangeId = transform( { createExchangeItems }, ({ createExchangeItems }) => { @@ -320,9 +331,12 @@ export const confirmExchangeRequestWorkflow = createWorkflow( reserveInventoryStep(formatedInventoryItems) }) - when({ returnShippingMethod }, ({ returnShippingMethod }) => { - return !!returnShippingMethod - }).then(() => { + when( + { returnShippingMethod, returnId }, + ({ returnShippingMethod, returnId }) => { + return !!returnShippingMethod && !!returnId + } + ).then(() => { const returnShippingOption = useRemoteQueryStep({ entry_point: "shipping_options", fields: [ @@ -343,7 +357,6 @@ export const confirmExchangeRequestWorkflow = createWorkflow( order, items: order.items!, shippingOption: returnShippingOption, - isReturn: true, }, prepareFulfillmentData ) diff --git a/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts b/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts index 20ab95534b..0cd6ec773e 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts @@ -19,6 +19,7 @@ import { useRemoteQueryStep } from "../../../common" import { updateOrderExchangesStep } from "../../steps/exchange/update-order-exchanges" import { previewOrderChangeStep } from "../../steps/preview-order-change" import { createReturnsStep } from "../../steps/return/create-returns" +import { updateOrderChangesStep } from "../../steps/update-order-changes" import { throwIfIsCancelled, throwIfItemsDoesNotExistsInOrder, @@ -124,6 +125,17 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow( status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], }) + when({ createdReturn }, ({ createdReturn }) => { + return !!createdReturn?.length + }).then(() => { + updateOrderChangesStep([ + { + id: orderChange.id, + return_id: createdReturn?.[0]?.id, + }, + ]) + }) + exchangeRequestItemReturnValidationStep({ order, items: input.items, diff --git a/packages/core/js-sdk/src/admin/exchange.ts b/packages/core/js-sdk/src/admin/exchange.ts new file mode 100644 index 0000000000..6ee0440000 --- /dev/null +++ b/packages/core/js-sdk/src/admin/exchange.ts @@ -0,0 +1,371 @@ +import { HttpTypes } from "@medusajs/types" + +import { Client } from "../client" +import { ClientHeaders } from "../types" + +export class Exchange { + private client: Client + constructor(client: Client) { + this.client = client + } + + async list( + query?: HttpTypes.AdminExchangeListParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges`, + { + query, + headers, + } + ) + } + + async retrieve( + id: string, + query?: HttpTypes.AdminExchangeParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}`, + { + query, + headers, + } + ) + } + + async create( + body: HttpTypes.AdminCreateExchange, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async cancel( + id: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/cancel`, + { + method: "POST", + headers, + query, + } + ) + } + + async delete( + id: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async addItems( + id: string, + body: HttpTypes.AdminAddExchangeItems, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/exchange-items`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async updateItem( + id: string, + actionId: string, + body: HttpTypes.AdminUpdateExchangeItem, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/exchange-items/${actionId}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async removeItem( + id: string, + actionId: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/exchange-items/${actionId}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async addInboundItems( + id: string, + body: HttpTypes.AdminAddExchangeInboundItems, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/inbound/items`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async updateInboundItem( + id: string, + actionId: string, + body: HttpTypes.AdminUpdateExchangeInboundItem, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/inbound/items/${actionId}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async removeInboundItem( + id: string, + actionId: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/inbound/items/${actionId}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async addInboundShipping( + id: string, + body: HttpTypes.AdminExchangeAddInboundShipping, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/inbound/shipping-method`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async updateInboundShipping( + id: string, + actionId: string, + body: HttpTypes.AdminExchangeUpdateInboundShipping, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/inbound/shipping-method/${actionId}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async deleteInboundShipping( + id: string, + actionId: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/inbound/shipping-method/${actionId}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async addOutboundItems( + id: string, + body: HttpTypes.AdminAddExchangeOutboundItems, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/outbound/items`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async updateOutboundItem( + id: string, + actionId: string, + body: HttpTypes.AdminUpdateExchangeOutboundItem, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/outbound/items/${actionId}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async removeOutboundItem( + id: string, + actionId: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/outbound/items/${actionId}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async addOutboundShipping( + id: string, + body: HttpTypes.AdminExchangeAddOutboundShipping, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/outbound/shipping-method`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async updateOutboundShipping( + id: string, + actionId: string, + body: HttpTypes.AdminExchangeUpdateOutboundShipping, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/outbound/shipping-method/${actionId}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async deleteOutboundShipping( + id: string, + actionId: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/outbound/shipping-method/${actionId}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async request( + id: string, + body: HttpTypes.AdminRequestExchange, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/request`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async cancelRequest( + id: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/exchanges/${id}/request`, + { + method: "DELETE", + headers, + query, + } + ) + } +} diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index a48776f911..1d7a8c7271 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -2,6 +2,7 @@ import { Client } from "../client" import { Claim } from "./claim" import { Currency } from "./currency" import { Customer } from "./customer" +import { Exchange } from "./exchange" import { Fulfillment } from "./fulfillment" import { FulfillmentProvider } from "./fulfillment-provider" import { FulfillmentSet } from "./fulfillment-set" @@ -56,6 +57,7 @@ export class Admin { public order: Order public return: Return public claim: Claim + public exchange: Exchange public taxRate: TaxRate public taxRegion: TaxRegion public store: Store @@ -99,5 +101,6 @@ export class Admin { this.payment = new Payment(client) this.productVariant = new ProductVariant(client) this.refundReason = new RefundReason(client) + this.exchange = new Exchange(client) } } diff --git a/packages/core/types/src/http/exchange/admin/index.ts b/packages/core/types/src/http/exchange/admin/index.ts index 57fa23181b..1f82a2ead5 100644 --- a/packages/core/types/src/http/exchange/admin/index.ts +++ b/packages/core/types/src/http/exchange/admin/index.ts @@ -1,2 +1,4 @@ export * from "./entities" -export * from "./responses" \ No newline at end of file +export * from "./payloads" +export * from "./queries" +export * from "./responses" diff --git a/packages/core/types/src/http/exchange/admin/payloads.ts b/packages/core/types/src/http/exchange/admin/payloads.ts new file mode 100644 index 0000000000..8db33cc1eb --- /dev/null +++ b/packages/core/types/src/http/exchange/admin/payloads.ts @@ -0,0 +1,74 @@ +enum ExchangeReason { + MISSING_ITEM = "missing_item", + WRONG_ITEM = "wrong_item", + PRODUCTION_FAILURE = "production_failure", + OTHER = "other", +} + +interface AdminExchangeAddItems { + items: { + id: string + quantity: number + reason?: ExchangeReason + description?: string + internal_note?: string + }[] +} + +interface AdminExchangeUpdateItem { + quantity?: number + reason_id?: string | null + description?: string + internal_note?: string | null +} + +interface AdminExchangeAddShippingMethod { + shipping_option_id: string + custom_price?: number + description?: string + internal_note?: string + metadata?: Record | null +} + +interface AdminExchangeUpdateShippingMethod { + custom_price?: number | null + internal_note?: string + metadata?: Record | null +} + +export interface AdminCreateExchange { + type: "refund" | "replace" + order_id: string + description?: string + internal_note?: string + metadata?: Record | null +} + +export interface AdminAddExchangeItems extends AdminExchangeAddItems {} +export interface AdminUpdateExchangeItem extends AdminExchangeUpdateItem {} + +export interface AdminAddExchangeInboundItems extends AdminExchangeAddItems {} +export interface AdminUpdateExchangeInboundItem + extends AdminExchangeUpdateItem {} + +export interface AdminAddExchangeOutboundItems extends AdminExchangeAddItems {} +export interface AdminUpdateExchangeOutboundItem + extends AdminExchangeUpdateItem {} + +export interface AdminExchangeAddInboundShipping + extends AdminExchangeAddShippingMethod {} +export interface AdminExchangeUpdateInboundShipping + extends AdminExchangeUpdateShippingMethod {} + +export interface AdminExchangeAddOutboundShipping + extends AdminExchangeAddShippingMethod {} +export interface AdminExchangeUpdateOutboundShipping + extends AdminExchangeUpdateShippingMethod {} + +export interface AdminRequestExchange { + no_notification?: boolean +} + +export interface AdminCancelExchange { + no_notification?: boolean +} diff --git a/packages/core/types/src/http/exchange/admin/queries.ts b/packages/core/types/src/http/exchange/admin/queries.ts new file mode 100644 index 0000000000..47373e0528 --- /dev/null +++ b/packages/core/types/src/http/exchange/admin/queries.ts @@ -0,0 +1,17 @@ +import { BaseFilterable, OperatorMap } from "../../../dal" +import { SelectParams } from "../../common" +import { BaseExchangeListParams } from "../common" + +export interface AdminExchangeListParams + extends BaseExchangeListParams, + BaseFilterable { + deleted_at?: OperatorMap +} + +export interface AdminExchangeParams extends SelectParams { + id?: string | string[] + status?: string | string[] + created_at?: OperatorMap + updated_at?: OperatorMap + deleted_at?: OperatorMap +} diff --git a/packages/core/types/src/http/exchange/admin/responses.ts b/packages/core/types/src/http/exchange/admin/responses.ts index a322dbbcbe..c0e82be33f 100644 --- a/packages/core/types/src/http/exchange/admin/responses.ts +++ b/packages/core/types/src/http/exchange/admin/responses.ts @@ -1,5 +1,5 @@ import { OrderDTO, OrderPreviewDTO } from "../../../order" -import { PaginatedResponse } from "../../common" +import { DeleteResponse, PaginatedResponse } from "../../common" import { AdminReturn } from "../../return" import { AdminExchange } from "./entities" @@ -8,7 +8,7 @@ export interface AdminExchangeResponse { } export type AdminExchangeListResponse = PaginatedResponse<{ - exchanges: AdminExchange + exchanges: AdminExchange[] }> export interface AdminExchangeOrderResponse { @@ -30,3 +30,6 @@ export interface AdminExchangeReturnResponse { order_preview: OrderPreviewDTO return: AdminReturn } + +export interface AdminExchangeDeleteResponse + extends DeleteResponse<"exchange"> {} diff --git a/packages/core/types/src/http/exchange/common.ts b/packages/core/types/src/http/exchange/common.ts index 31062e22d0..235b2b4243 100644 --- a/packages/core/types/src/http/exchange/common.ts +++ b/packages/core/types/src/http/exchange/common.ts @@ -1,5 +1,7 @@ -import { BaseOrder } from "../order/common"; -import { AdminReturnItem, AdminReturn } from "../return"; +import { OperatorMap } from "../../dal" +import { FindParams } from "../common" +import { BaseOrder } from "../order/common" +import { AdminReturn, AdminReturnItem } from "../return" export interface BaseExchangeItem { id: string @@ -12,7 +14,8 @@ export interface BaseExchangeItem { updated_at: string | null } -export interface BaseExchange extends Omit { +export interface BaseExchange + extends Omit { order_id: string return_items: AdminReturnItem[] additional_items: BaseExchangeItem[] @@ -20,4 +23,14 @@ export interface BaseExchange extends Omit + updated_at?: OperatorMap + deleted_at?: OperatorMap +} diff --git a/packages/medusa/src/api/admin/exchanges/[id]/inbound/items/[action_id]/route.ts b/packages/medusa/src/api/admin/exchanges/[id]/inbound/items/[action_id]/route.ts index 0536c93067..9cda724854 100644 --- a/packages/medusa/src/api/admin/exchanges/[id]/inbound/items/[action_id]/route.ts +++ b/packages/medusa/src/api/admin/exchanges/[id]/inbound/items/[action_id]/route.ts @@ -2,6 +2,7 @@ import { removeItemReturnActionWorkflow, updateRequestItemReturnWorkflow, } from "@medusajs/core-flows" +import { HttpTypes } from "@medusajs/types" import { ContainerRegistrationKeys, remoteQueryObjectFromString, @@ -10,9 +11,9 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../../../../types/routing" +import { refetchEntity } from "../../../../../../utils/refetch-entity" import { defaultAdminDetailsReturnFields } from "../../../../../returns/query-config" import { AdminPostExchangesRequestItemsReturnActionReqSchemaType } from "../../../../validators" -import { HttpTypes } from "@medusajs/types" export const POST = async ( req: AuthenticatedMedusaRequest, @@ -69,11 +70,15 @@ export const DELETE = async ( const { id, action_id } = req.params + const exchange = await refetchEntity("order_exchange", id, req.scope, [ + "return_id", + ]) + const { result: orderPreview } = await removeItemReturnActionWorkflow( req.scope ).run({ input: { - return_id: id, + return_id: exchange.return_id, action_id, }, }) @@ -81,7 +86,7 @@ export const DELETE = async ( const queryObject = remoteQueryObjectFromString({ entryPoint: "return", variables: { - id, + id: exchange.return_id, filters: { ...req.filterableFields, }, diff --git a/packages/medusa/src/api/admin/exchanges/[id]/route.ts b/packages/medusa/src/api/admin/exchanges/[id]/route.ts new file mode 100644 index 0000000000..14b6716b7c --- /dev/null +++ b/packages/medusa/src/api/admin/exchanges/[id]/route.ts @@ -0,0 +1,28 @@ +import { AdminExchangeResponse } from "@medusajs/types" +import { MedusaError } from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" +import { refetchEntity } from "../../../utils/refetch-entity" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const exchange = await refetchEntity( + "order_exchange", + req.params.id, + req.scope, + req.remoteQueryConfig.fields + ) + + if (!exchange) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Exchange with id: ${req.params.id} was not found` + ) + } + + res.status(200).json({ exchange }) +}