feat(dashboard): handle region pricing on create/edit (#7811)
**What** - allow for region prices creation on product create flow - editing of region prices in prices edit form
This commit is contained in:
+2
-2
@@ -17,8 +17,8 @@ export function VariantPricesSection({ variant }: VariantPricesSectionProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const prices = variant.prices
|
||||
.filter((p) => !p.rules?.length)
|
||||
.sort((p1, p2) => p1.currency_code?.localeCompare(p2.currency_code)) // display just currency prices
|
||||
.filter((p) => !Object.keys(p.rules || {}).length) // display just currency prices
|
||||
.sort((p1, p2) => p1.currency_code?.localeCompare(p2.currency_code))
|
||||
|
||||
const [current, setCurrent] = useState(Math.min(prices.length, 3))
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CurrencyDTO, HttpTypes } from "@medusajs/types"
|
||||
import { CurrencyDTO, HttpTypes, RegionDTO } from "@medusajs/types"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||
@@ -10,6 +10,7 @@ import { DataGridMeta } from "../../../components/grid/types"
|
||||
import { useCurrencies } from "../../../hooks/api/currencies"
|
||||
import { useStore } from "../../../hooks/api/store"
|
||||
import { ProductCreateSchema } from "../product-create/constants"
|
||||
import { useRegions } from "../../../hooks/api/regions.tsx"
|
||||
|
||||
type VariantPricingFormProps = {
|
||||
form: UseFormReturn<ProductCreateSchema>
|
||||
@@ -27,8 +28,11 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
||||
}
|
||||
)
|
||||
|
||||
const { regions } = useRegions({ limit: 9999 })
|
||||
|
||||
const columns = useVariantPriceGridColumns({
|
||||
currencies,
|
||||
regions,
|
||||
})
|
||||
|
||||
const variants = useWatch({
|
||||
@@ -52,8 +56,10 @@ const columnHelper = createColumnHelper<HttpTypes.AdminProductVariant>()
|
||||
|
||||
export const useVariantPriceGridColumns = ({
|
||||
currencies = [],
|
||||
regions = [],
|
||||
}: {
|
||||
currencies?: CurrencyDTO[]
|
||||
regions?: RegionDTO[]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -87,8 +93,24 @@ export const useVariantPriceGridColumns = ({
|
||||
},
|
||||
})
|
||||
}),
|
||||
...regions.map((region) => {
|
||||
return columnHelper.display({
|
||||
header: `Price ${region.name}`,
|
||||
cell: ({ row, table }) => {
|
||||
return (
|
||||
<CurrencyCell
|
||||
currency={currencies.find(
|
||||
(c) => c.code === region.currency_code
|
||||
)}
|
||||
meta={table.options.meta as DataGridMeta}
|
||||
field={`variants.${row.index}.prices.${region.id}`}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
}),
|
||||
]
|
||||
}, [t, currencies])
|
||||
}, [t, currencies, regions])
|
||||
|
||||
return colDefs
|
||||
}
|
||||
|
||||
+15
-2
@@ -21,6 +21,7 @@ import { ProductCreateInventoryKitForm } from "../product-create-inventory-kit-f
|
||||
import { ProductCreateVariantsForm } from "../product-create-variants-form"
|
||||
import { isFetchError } from "../../../../../lib/is-fetch-error"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { useRegions } from "../../../../../hooks/api/regions.tsx"
|
||||
|
||||
enum Tab {
|
||||
DETAILS = "details",
|
||||
@@ -62,10 +63,22 @@ export const ProductCreateForm = ({
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useCreateProduct()
|
||||
const { regions } = useRegions({ limit: 9999 })
|
||||
|
||||
const regionsCurrencyMap = useMemo(() => {
|
||||
if (!regions?.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return regions.reduce((acc, reg) => {
|
||||
acc[reg.id] = reg.currency_code
|
||||
return acc
|
||||
}, {})
|
||||
}, regions)
|
||||
|
||||
/**
|
||||
* TODO: Important to revisit this - use variants watch so high in the tree can cause needless rerenders of the entire page
|
||||
* which is suboptimal when rereners are caused by bulk editor changes
|
||||
* which is suboptimal when rerenders are caused by bulk editor changes
|
||||
*/
|
||||
|
||||
const watchedVariants = useWatch({
|
||||
@@ -123,10 +136,10 @@ export const ProductCreateForm = ({
|
||||
|
||||
const { product } = await mutateAsync(
|
||||
normalizeProductFormValues({
|
||||
// TODO: workflow should handle inventory creation
|
||||
...payload,
|
||||
media: uploadedMedia,
|
||||
status: (isDraftSubmission ? "draft" : "published") as any,
|
||||
regionsCurrencyMap,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { ProductCreateSchemaType } from "./types"
|
||||
import { castNumber } from "../../../lib/cast-number"
|
||||
|
||||
export const normalizeProductFormValues = (
|
||||
values: ProductCreateSchemaType & { status: HttpTypes.AdminProductStatus }
|
||||
values: ProductCreateSchemaType & {
|
||||
status: HttpTypes.AdminProductStatus
|
||||
regionsCurrencyMap: Record<string, string>
|
||||
}
|
||||
) => {
|
||||
const thumbnail = values.media?.find((media) => media.isThumbnail)?.url
|
||||
const images = values.media
|
||||
@@ -39,13 +42,15 @@ export const normalizeProductFormValues = (
|
||||
weight: values.weight ? parseFloat(values.weight) : undefined,
|
||||
options: values.options.filter((o) => o.title), // clean temp. values
|
||||
variants: normalizeVariants(
|
||||
values.variants.filter((variant) => variant.should_create)
|
||||
values.variants.filter((variant) => variant.should_create),
|
||||
values.regionsCurrencyMap
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeVariants = (
|
||||
variants: ProductCreateSchemaType["variants"]
|
||||
variants: ProductCreateSchemaType["variants"],
|
||||
regionsCurrencyMap: Record<string, string>
|
||||
) => {
|
||||
return variants.map((variant) => ({
|
||||
title:
|
||||
@@ -71,8 +76,11 @@ export const normalizeVariants = (
|
||||
prices: Object.entries(variant.prices || {})
|
||||
.map(([key, value]: any) => {
|
||||
if (key.startsWith("reg_")) {
|
||||
// TODO: route needs to accept region prices as well
|
||||
return undefined
|
||||
return {
|
||||
currency_code: regionsCurrencyMap[key],
|
||||
amount: castNumber(value),
|
||||
rules: { region_id: key },
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
currency_code: key,
|
||||
|
||||
@@ -9,6 +9,8 @@ import { RouteFocusModal, useRouteModal } from "../../../components/route-modal"
|
||||
import { useUpdateProductVariantsBatch } from "../../../hooks/api/products"
|
||||
import { VariantPricingForm } from "../common/variant-pricing-form"
|
||||
import { castNumber } from "../../../lib/cast-number"
|
||||
import { useRegions } from "../../../hooks/api/regions.tsx"
|
||||
import { useMemo } from "react"
|
||||
|
||||
export const UpdateVariantPricesSchema = zod.object({
|
||||
variants: zod.array(
|
||||
@@ -34,6 +36,18 @@ export const PricingEdit = ({
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const { regions } = useRegions({ limit: 9999 })
|
||||
const regionsCurrencyMap = useMemo(() => {
|
||||
if (!regions?.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return regions.reduce((acc, reg) => {
|
||||
acc[reg.id] = reg.currency_code
|
||||
return acc
|
||||
}, {})
|
||||
}, regions)
|
||||
|
||||
const variants = variantId
|
||||
? product.variants.filter((v) => v.id === variantId)
|
||||
: product.variants
|
||||
@@ -43,7 +57,11 @@ export const PricingEdit = ({
|
||||
variants: variants.map((variant: any) => ({
|
||||
title: variant.title,
|
||||
prices: variant.prices.reduce((acc: any, price: any) => {
|
||||
acc[price.currency_code] = price.amount
|
||||
if (price.rules?.region_id) {
|
||||
acc[price.rules.region_id] = price.amount
|
||||
} else {
|
||||
acc[price.currency_code] = price.amount
|
||||
}
|
||||
return acc
|
||||
}, {}),
|
||||
})) as any,
|
||||
@@ -59,19 +77,45 @@ export const PricingEdit = ({
|
||||
const reqData = values.variants.map((variant, ind) => ({
|
||||
id: variants[ind].id,
|
||||
prices: Object.entries(variant.prices || {}).map(
|
||||
([currency_code, value]: any) => {
|
||||
const id = variants[ind].prices.find(
|
||||
(p) => p.currency_code === currency_code
|
||||
)?.id
|
||||
([currencyCodeOrRegionId, value]: any) => {
|
||||
const regionId = currencyCodeOrRegionId.startsWith("reg_")
|
||||
? currencyCodeOrRegionId
|
||||
: undefined
|
||||
const currencyCode = currencyCodeOrRegionId.startsWith("reg_")
|
||||
? regionsCurrencyMap[regionId]
|
||||
: currencyCodeOrRegionId
|
||||
|
||||
let existingId = undefined
|
||||
|
||||
if (regionId) {
|
||||
existingId = variants[ind].prices.find(
|
||||
(p) => p.rules["region_id"] === regionId
|
||||
)?.id
|
||||
} else {
|
||||
existingId = variants[ind].prices.find(
|
||||
(p) => p.currency_code === currencyCode
|
||||
)?.id
|
||||
}
|
||||
|
||||
const amount = castNumber(value)
|
||||
|
||||
return id
|
||||
? { id, amount, currency_code }
|
||||
: { currency_code, amount }
|
||||
const pricePayload = existingId
|
||||
? {
|
||||
id: existingId,
|
||||
amount,
|
||||
currency_code: currencyCode,
|
||||
}
|
||||
: { currency_code: currencyCode, amount }
|
||||
|
||||
if (regionId && !existingId) {
|
||||
pricePayload.rules = { region_id: regionId }
|
||||
}
|
||||
|
||||
return pricePayload
|
||||
}
|
||||
),
|
||||
}))
|
||||
|
||||
await mutateAsync(reqData, {
|
||||
onSuccess: () => {
|
||||
handleSuccess("..")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { PricingEdit } from "./pricing-edit"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { PricingEdit } from "./pricing-edit"
|
||||
|
||||
export const ProductPrices = () => {
|
||||
const { id, variant_id } = useParams()
|
||||
|
||||
Reference in New Issue
Block a user