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 { t } = useTranslation()
|
||||||
|
|
||||||
const prices = variant.prices
|
const prices = variant.prices
|
||||||
.filter((p) => !p.rules?.length)
|
.filter((p) => !Object.keys(p.rules || {}).length) // display just currency prices
|
||||||
.sort((p1, p2) => p1.currency_code?.localeCompare(p2.currency_code)) // display just currency prices
|
.sort((p1, p2) => p1.currency_code?.localeCompare(p2.currency_code))
|
||||||
|
|
||||||
const [current, setCurrent] = useState(Math.min(prices.length, 3))
|
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 { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
@@ -10,6 +10,7 @@ import { DataGridMeta } from "../../../components/grid/types"
|
|||||||
import { useCurrencies } from "../../../hooks/api/currencies"
|
import { useCurrencies } from "../../../hooks/api/currencies"
|
||||||
import { useStore } from "../../../hooks/api/store"
|
import { useStore } from "../../../hooks/api/store"
|
||||||
import { ProductCreateSchema } from "../product-create/constants"
|
import { ProductCreateSchema } from "../product-create/constants"
|
||||||
|
import { useRegions } from "../../../hooks/api/regions.tsx"
|
||||||
|
|
||||||
type VariantPricingFormProps = {
|
type VariantPricingFormProps = {
|
||||||
form: UseFormReturn<ProductCreateSchema>
|
form: UseFormReturn<ProductCreateSchema>
|
||||||
@@ -27,8 +28,11 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { regions } = useRegions({ limit: 9999 })
|
||||||
|
|
||||||
const columns = useVariantPriceGridColumns({
|
const columns = useVariantPriceGridColumns({
|
||||||
currencies,
|
currencies,
|
||||||
|
regions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const variants = useWatch({
|
const variants = useWatch({
|
||||||
@@ -52,8 +56,10 @@ const columnHelper = createColumnHelper<HttpTypes.AdminProductVariant>()
|
|||||||
|
|
||||||
export const useVariantPriceGridColumns = ({
|
export const useVariantPriceGridColumns = ({
|
||||||
currencies = [],
|
currencies = [],
|
||||||
|
regions = [],
|
||||||
}: {
|
}: {
|
||||||
currencies?: CurrencyDTO[]
|
currencies?: CurrencyDTO[]
|
||||||
|
regions?: RegionDTO[]
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
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
|
return colDefs
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-2
@@ -21,6 +21,7 @@ import { ProductCreateInventoryKitForm } from "../product-create-inventory-kit-f
|
|||||||
import { ProductCreateVariantsForm } from "../product-create-variants-form"
|
import { ProductCreateVariantsForm } from "../product-create-variants-form"
|
||||||
import { isFetchError } from "../../../../../lib/is-fetch-error"
|
import { isFetchError } from "../../../../../lib/is-fetch-error"
|
||||||
import { sdk } from "../../../../../lib/client"
|
import { sdk } from "../../../../../lib/client"
|
||||||
|
import { useRegions } from "../../../../../hooks/api/regions.tsx"
|
||||||
|
|
||||||
enum Tab {
|
enum Tab {
|
||||||
DETAILS = "details",
|
DETAILS = "details",
|
||||||
@@ -62,10 +63,22 @@ export const ProductCreateForm = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isPending } = useCreateProduct()
|
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
|
* 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({
|
const watchedVariants = useWatch({
|
||||||
@@ -123,10 +136,10 @@ export const ProductCreateForm = ({
|
|||||||
|
|
||||||
const { product } = await mutateAsync(
|
const { product } = await mutateAsync(
|
||||||
normalizeProductFormValues({
|
normalizeProductFormValues({
|
||||||
// TODO: workflow should handle inventory creation
|
|
||||||
...payload,
|
...payload,
|
||||||
media: uploadedMedia,
|
media: uploadedMedia,
|
||||||
status: (isDraftSubmission ? "draft" : "published") as any,
|
status: (isDraftSubmission ? "draft" : "published") as any,
|
||||||
|
regionsCurrencyMap,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { ProductCreateSchemaType } from "./types"
|
|||||||
import { castNumber } from "../../../lib/cast-number"
|
import { castNumber } from "../../../lib/cast-number"
|
||||||
|
|
||||||
export const normalizeProductFormValues = (
|
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 thumbnail = values.media?.find((media) => media.isThumbnail)?.url
|
||||||
const images = values.media
|
const images = values.media
|
||||||
@@ -39,13 +42,15 @@ export const normalizeProductFormValues = (
|
|||||||
weight: values.weight ? parseFloat(values.weight) : undefined,
|
weight: values.weight ? parseFloat(values.weight) : undefined,
|
||||||
options: values.options.filter((o) => o.title), // clean temp. values
|
options: values.options.filter((o) => o.title), // clean temp. values
|
||||||
variants: normalizeVariants(
|
variants: normalizeVariants(
|
||||||
values.variants.filter((variant) => variant.should_create)
|
values.variants.filter((variant) => variant.should_create),
|
||||||
|
values.regionsCurrencyMap
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const normalizeVariants = (
|
export const normalizeVariants = (
|
||||||
variants: ProductCreateSchemaType["variants"]
|
variants: ProductCreateSchemaType["variants"],
|
||||||
|
regionsCurrencyMap: Record<string, string>
|
||||||
) => {
|
) => {
|
||||||
return variants.map((variant) => ({
|
return variants.map((variant) => ({
|
||||||
title:
|
title:
|
||||||
@@ -71,8 +76,11 @@ export const normalizeVariants = (
|
|||||||
prices: Object.entries(variant.prices || {})
|
prices: Object.entries(variant.prices || {})
|
||||||
.map(([key, value]: any) => {
|
.map(([key, value]: any) => {
|
||||||
if (key.startsWith("reg_")) {
|
if (key.startsWith("reg_")) {
|
||||||
// TODO: route needs to accept region prices as well
|
return {
|
||||||
return undefined
|
currency_code: regionsCurrencyMap[key],
|
||||||
|
amount: castNumber(value),
|
||||||
|
rules: { region_id: key },
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
currency_code: key,
|
currency_code: key,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { RouteFocusModal, useRouteModal } from "../../../components/route-modal"
|
|||||||
import { useUpdateProductVariantsBatch } from "../../../hooks/api/products"
|
import { useUpdateProductVariantsBatch } from "../../../hooks/api/products"
|
||||||
import { VariantPricingForm } from "../common/variant-pricing-form"
|
import { VariantPricingForm } from "../common/variant-pricing-form"
|
||||||
import { castNumber } from "../../../lib/cast-number"
|
import { castNumber } from "../../../lib/cast-number"
|
||||||
|
import { useRegions } from "../../../hooks/api/regions.tsx"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
export const UpdateVariantPricesSchema = zod.object({
|
export const UpdateVariantPricesSchema = zod.object({
|
||||||
variants: zod.array(
|
variants: zod.array(
|
||||||
@@ -34,6 +36,18 @@ export const PricingEdit = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
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
|
const variants = variantId
|
||||||
? product.variants.filter((v) => v.id === variantId)
|
? product.variants.filter((v) => v.id === variantId)
|
||||||
: product.variants
|
: product.variants
|
||||||
@@ -43,7 +57,11 @@ export const PricingEdit = ({
|
|||||||
variants: variants.map((variant: any) => ({
|
variants: variants.map((variant: any) => ({
|
||||||
title: variant.title,
|
title: variant.title,
|
||||||
prices: variant.prices.reduce((acc: any, price: any) => {
|
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
|
return acc
|
||||||
}, {}),
|
}, {}),
|
||||||
})) as any,
|
})) as any,
|
||||||
@@ -59,19 +77,45 @@ export const PricingEdit = ({
|
|||||||
const reqData = values.variants.map((variant, ind) => ({
|
const reqData = values.variants.map((variant, ind) => ({
|
||||||
id: variants[ind].id,
|
id: variants[ind].id,
|
||||||
prices: Object.entries(variant.prices || {}).map(
|
prices: Object.entries(variant.prices || {}).map(
|
||||||
([currency_code, value]: any) => {
|
([currencyCodeOrRegionId, value]: any) => {
|
||||||
const id = variants[ind].prices.find(
|
const regionId = currencyCodeOrRegionId.startsWith("reg_")
|
||||||
(p) => p.currency_code === currency_code
|
? currencyCodeOrRegionId
|
||||||
)?.id
|
: 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)
|
const amount = castNumber(value)
|
||||||
|
|
||||||
return id
|
const pricePayload = existingId
|
||||||
? { id, amount, currency_code }
|
? {
|
||||||
: { currency_code, amount }
|
id: existingId,
|
||||||
|
amount,
|
||||||
|
currency_code: currencyCode,
|
||||||
|
}
|
||||||
|
: { currency_code: currencyCode, amount }
|
||||||
|
|
||||||
|
if (regionId && !existingId) {
|
||||||
|
pricePayload.rules = { region_id: regionId }
|
||||||
|
}
|
||||||
|
|
||||||
|
return pricePayload
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await mutateAsync(reqData, {
|
await mutateAsync(reqData, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
handleSuccess("..")
|
handleSuccess("..")
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { useProduct } from "../../../hooks/api/products"
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
import { PricingEdit } from "./pricing-edit"
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { PricingEdit } from "./pricing-edit"
|
||||||
|
|
||||||
export const ProductPrices = () => {
|
export const ProductPrices = () => {
|
||||||
const { id, variant_id } = useParams()
|
const { id, variant_id } = useParams()
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export const AdminUpdateProductOption = z.object({
|
|||||||
values: z.array(z.string()).optional(),
|
values: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Add support for rules
|
|
||||||
export type AdminCreateVariantPriceType = z.infer<
|
export type AdminCreateVariantPriceType = z.infer<
|
||||||
typeof AdminCreateVariantPrice
|
typeof AdminCreateVariantPrice
|
||||||
>
|
>
|
||||||
@@ -109,7 +108,6 @@ export const AdminCreateVariantPrice = z.object({
|
|||||||
rules: z.record(z.string(), z.string()).optional(),
|
rules: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Add support for rules
|
|
||||||
export type AdminUpdateVariantPriceType = z.infer<
|
export type AdminUpdateVariantPriceType = z.infer<
|
||||||
typeof AdminUpdateVariantPrice
|
typeof AdminUpdateVariantPrice
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user