From 47a175ce942db20654b83d6414e29147a0894ee2 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Thu, 11 Apr 2024 19:21:58 +0200 Subject: [PATCH] feat: Add support for price setting and updates for products (#7037) We still need to add a `batch` method for variants, but I'll do that in a separate PR --- .../public/locales/en-US/translation.json | 2 + .../data-grid-root/data-grid-root.tsx | 6 +- .../src/providers/router-provider/v2.tsx | 5 + .../pricing-create-form.tsx | 11 +- .../products/common/variant-pricing-form.tsx | 95 ++++++++++ .../create-product-form.tsx | 124 ------------- .../components/create-product-form/index.ts | 1 - .../components/create-product.tsx | 175 ++++++++++++++++++ ...etails.tsx => product-attributes-form.tsx} | 44 ++--- .../product-create/product-create.tsx | 4 +- .../products/product-create/schema.ts | 82 ++++++++ .../product-variant-section.tsx | 7 +- .../products/product-prices/index.ts | 1 + .../products/product-prices/pricing-edit.tsx | 85 +++++++++ .../product-prices/product-prices.tsx | 21 +++ 15 files changed, 506 insertions(+), 157 deletions(-) create mode 100644 packages/admin-next/dashboard/src/v2-routes/products/common/variant-pricing-form.tsx delete mode 100644 packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-form.tsx delete mode 100644 packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product.tsx rename packages/admin-next/dashboard/src/v2-routes/products/product-create/components/{create-product-form/create-product-details.tsx => product-attributes-form.tsx} (94%) create mode 100644 packages/admin-next/dashboard/src/v2-routes/products/product-create/schema.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/products/product-prices/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/products/product-prices/pricing-edit.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/products/product-prices/product-prices.tsx diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index f50103cae3..ead37f3bec 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -58,6 +58,7 @@ "actions": { "save": "Save", "saveAsDraft": "Save as draft", + "publish": "Publish", "create": "Create", "delete": "Delete", "remove": "Remove", @@ -157,6 +158,7 @@ "organization": "Organize", "editOrganization": "Edit Organization", "editOptions": "Edit Options", + "editPrices": "Edit prices", "media": { "label": "Media", "editHint": "Add media to the product to showcase it in your storefront.", diff --git a/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx b/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx index 4644bbd8ae..1731fc845a 100644 --- a/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx +++ b/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx @@ -29,19 +29,19 @@ type FieldCoordinates = { export interface DataGridRootProps< TData, - TFieldValues extends FieldValues = FieldValues + TFieldValues extends FieldValues = FieldValues, > { data?: TData[] columns: ColumnDef[] state: UseFormReturn - getSubRows: (row: TData) => TData[] | undefined + getSubRows?: (row: TData) => TData[] } const ROW_HEIGHT = 40 export const DataGridRoot = < TData, - TFieldValues extends FieldValues = FieldValues + TFieldValues extends FieldValues = FieldValues, >({ data = [], columns, diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index 523901e6de..9505e090bc 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -98,6 +98,11 @@ export const v2Routes: RouteObject[] = [ lazy: () => import("../../v2-routes/products/product-media"), }, + { + path: "prices", + lazy: () => + import("../../v2-routes/products/product-prices"), + }, { path: "options/create", lazy: () => diff --git a/packages/admin-next/dashboard/src/v2-routes/pricing/pricing-create/components/pricing-create-form/pricing-create-form.tsx b/packages/admin-next/dashboard/src/v2-routes/pricing/pricing-create/components/pricing-create-form/pricing-create-form.tsx index 36048278f1..bef7902da2 100644 --- a/packages/admin-next/dashboard/src/v2-routes/pricing/pricing-create/components/pricing-create-form/pricing-create-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/pricing/pricing-create/components/pricing-create-form/pricing-create-form.tsx @@ -127,10 +127,13 @@ export const PricingCreateForm = () => { ) => { form.clearErrors(fields) - const values = fields.reduce((acc, key) => { - acc[key] = form.getValues(key) - return acc - }, {} as Record) + const values = fields.reduce( + (acc, key) => { + acc[key] = form.getValues(key) + return acc + }, + {} as Record + ) const validationResult = schema.safeParse(values) diff --git a/packages/admin-next/dashboard/src/v2-routes/products/common/variant-pricing-form.tsx b/packages/admin-next/dashboard/src/v2-routes/products/common/variant-pricing-form.tsx new file mode 100644 index 0000000000..ba17a9d001 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/products/common/variant-pricing-form.tsx @@ -0,0 +1,95 @@ +import { UseFormReturn, useWatch } from "react-hook-form" +import { CreateProductSchemaType } from "../product-create/schema" +import { DataGrid } from "../../../components/grid/data-grid" +import { useCurrencies } from "../../../hooks/api/currencies" +import { useStore } from "../../../hooks/api/store" +import { ColumnDef, createColumnHelper } from "@tanstack/react-table" +import { CurrencyDTO, ProductVariantDTO } from "@medusajs/types" +import { useTranslation } from "react-i18next" +import { useMemo } from "react" +import { ReadonlyCell } from "../../../components/grid/grid-cells/common/readonly-cell" +import { CurrencyCell } from "../../../components/grid/grid-cells/common/currency-cell" +import { DataGridMeta } from "../../../components/grid/types" + +type VariantPricingFormProps = { + form: UseFormReturn +} + +export const VariantPricingForm = ({ form }: VariantPricingFormProps) => { + const { store, isLoading: isStoreLoading } = useStore() + const { currencies, isLoading: isCurrenciesLoading } = useCurrencies( + { + code: store?.supported_currency_codes, + limit: store?.supported_currency_codes?.length, + }, + { + enabled: !!store, + } + ) + + const columns = useVariantPriceGridColumns({ + currencies, + }) + + const variants = useWatch({ + control: form.control, + name: "variants", + }) as any + + return ( +
+ +
+ ) +} + +const columnHelper = createColumnHelper() + +export const useVariantPriceGridColumns = ({ + currencies = [], +}: { + currencies?: CurrencyDTO[] +}) => { + const { t } = useTranslation() + + const colDefs: ColumnDef[] = useMemo(() => { + return [ + columnHelper.display({ + id: t("fields.title"), + header: t("fields.title"), + cell: ({ row }) => { + const entity = row.original + + return ( + +
+ {entity.title} +
+
+ ) + }, + }), + ...currencies.map((currency) => { + return columnHelper.display({ + header: `Price ${currency.code.toUpperCase()}`, + cell: ({ row, table }) => { + return ( + + ) + }, + }) + }), + ] + }, [t, currencies]) + + return colDefs +} diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-form.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-form.tsx deleted file mode 100644 index b251bcff25..0000000000 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-form.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { Button } from "@medusajs/ui" -import { UseFormReturn, useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import * as zod from "zod" - -import { - RouteFocusModal, - useRouteModal, -} from "../../../../../components/route-modal" -import { CreateProductDetails } from "./create-product-details" -import { useCreateProduct } from "../../../../../hooks/api/products" - -const CreateProductSchema = zod.object({ - title: zod.string(), - subtitle: zod.string().optional(), - handle: zod.string().optional(), - description: zod.string().optional(), - discountable: zod.boolean(), - type_id: zod.string().optional(), - collection_id: zod.string().optional(), - category_ids: zod.array(zod.string()).optional(), - tags: zod.array(zod.string()).optional(), - sales_channels: zod.array(zod.string()).optional(), - origin_country: zod.string().optional(), - material: zod.string().optional(), - width: zod.string().optional(), - length: zod.string().optional(), - height: zod.string().optional(), - weight: zod.string().optional(), - mid_code: zod.string().optional(), - hs_code: zod.string().optional(), - options: zod.array( - zod.object({ - title: zod.string(), - values: zod.array(zod.string()), - }) - ), - variants: zod.array( - zod.object({ - title: zod.string(), - options: zod.record(zod.string(), zod.string()), - variant_rank: zod.number(), - }) - ), - images: zod.array(zod.string()).optional(), - thumbnail: zod.string().optional(), -}) - -type Schema = zod.infer -export type CreateProductFormReturn = UseFormReturn - -export const CreateProductForm = () => { - const { t } = useTranslation() - const { handleSuccess } = useRouteModal() - - const form = useForm({ - defaultValues: { - discountable: true, - tags: [], - sales_channels: [], - options: [], - variants: [], - images: [], - }, - resolver: zodResolver(CreateProductSchema), - }) - - const { mutateAsync, isLoading } = useCreateProduct() - - const handleSubmit = form.handleSubmit( - async (values) => { - const reqData = { - ...values, - is_giftcard: false, - tags: values.tags?.map((tag) => ({ value: tag })), - sales_channels: values.sales_channels?.map((sc) => ({ id: sc })), - width: values.width ? parseFloat(values.width) : undefined, - length: values.length ? parseFloat(values.length) : undefined, - height: values.height ? parseFloat(values.height) : undefined, - weight: values.weight ? parseFloat(values.weight) : undefined, - variants: values.variants.map((variant) => ({ - ...variant, - prices: [], - })), - } as any - - await mutateAsync(reqData, { - onSuccess: ({ product }) => { - handleSuccess(`../${product.id}`) - }, - }) - }, - (err) => { - console.log(err) - } - ) - - return ( - -
- -
- - - - -
-
- -
-
- -
-
-
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/index.ts deleted file mode 100644 index fb4ce91618..0000000000 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./create-product-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product.tsx new file mode 100644 index 0000000000..60d0efc771 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product.tsx @@ -0,0 +1,175 @@ +import { Button, ProgressStatus, ProgressTabs } from "@medusajs/ui" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../components/route-modal" +import { useState } from "react" +import { useTranslation } from "react-i18next" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { + CreateProductSchema, + CreateProductSchemaType, + defaults, + normalize, +} from "../schema" +import { useCreateProduct } from "../../../../hooks/api/products" +import { ProductAttributesForm } from "./product-attributes-form" +import { VariantPricingForm } from "../../common/variant-pricing-form" + +enum Tab { + PRODUCT = "product", + PRICE = "price", +} +type TabState = Record + +export const CreateProductPage = () => { + const { t } = useTranslation() + const [tab, setTab] = useState(Tab.PRODUCT) + const { handleSuccess } = useRouteModal() + const form = useForm({ + defaultValues: defaults, + resolver: zodResolver(CreateProductSchema), + }) + + const { mutateAsync, isLoading } = useCreateProduct() + + const handleSubmit = form.handleSubmit( + async (values, e) => { + if (!(e?.nativeEvent instanceof SubmitEvent)) return + const submitter = e?.nativeEvent?.submitter as HTMLButtonElement + if (!(submitter instanceof HTMLButtonElement)) return + const isDraftSubmission = submitter.dataset.name === "save-draft-button" + + await mutateAsync( + normalize({ + ...values, + status: (isDraftSubmission ? "draft" : "published") as any, + }), + { + onSuccess: ({ product }) => { + handleSuccess(`../${product.id}`) + }, + } + ) + }, + (err) => { + console.log(err) + } + ) + const tabState: TabState = { + [Tab.PRODUCT]: tab === Tab.PRODUCT ? "in-progress" : "completed", + [Tab.PRICE]: tab === Tab.PRICE ? "in-progress" : "not-started", + } + + return ( + + +
+ setTab(tab as Tab)} + className="flex h-full flex-col overflow-hidden" + > + +
+
+ + + Products + + + Prices + + +
+
+ + + + + setTab(Tab.PRICE)} + isLoading={isLoading} + /> +
+
+
+ + +
+ +
+
+ + + +
+
+
+
+
+ ) +} + +type PrimaryButtonProps = { + tab: Tab + next: (tab: Tab) => void + isLoading?: boolean +} + +const PrimaryButton = ({ tab, next, isLoading }: PrimaryButtonProps) => { + const { t } = useTranslation() + + if (tab === Tab.PRICE) { + return ( + + ) + } + + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-details.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/product-attributes-form.tsx similarity index 94% rename from packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-details.tsx rename to packages/admin-next/dashboard/src/v2-routes/products/product-create/components/product-attributes-form.tsx index ef242e94a9..4ab238e4b2 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-details.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/product-attributes-form.tsx @@ -14,27 +14,28 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels" import { SalesChannel } from "@medusajs/medusa" import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" import { Fragment, useMemo, useState } from "react" -import { CountrySelect } from "../../../../../components/common/country-select" -import { Form } from "../../../../../components/common/form" -import { HandleInput } from "../../../../../components/common/handle-input" -import { DataTable } from "../../../../../components/table/data-table" -import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns" -import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters" -import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query" -import { useDataTable } from "../../../../../hooks/use-data-table" -import { CreateProductFormReturn } from "./create-product-form" -import { Combobox } from "../../../../../components/common/combobox" -import { FileUpload } from "../../../../../components/common/file-upload" -import { List } from "../../../../../components/common/list" -import { useProductTypes } from "../../../../../hooks/api/product-types" -import { useCollections } from "../../../../../hooks/api/collections" -import { useSalesChannels } from "../../../../../hooks/api/sales-channels" -import { useCategories } from "../../../../../hooks/api/categories" -import { useTags } from "../../../../../hooks/api/tags" -import { Keypair } from "../../../../../components/common/keypair" +import { CountrySelect } from "../../../../components/common/country-select" +import { Form } from "../../../../components/common/form" +import { HandleInput } from "../../../../components/common/handle-input" +import { DataTable } from "../../../../components/table/data-table" +import { useSalesChannelTableColumns } from "../../../../hooks/table/columns/use-sales-channel-table-columns" +import { useSalesChannelTableFilters } from "../../../../hooks/table/filters/use-sales-channel-table-filters" +import { useSalesChannelTableQuery } from "../../../../hooks/table/query/use-sales-channel-table-query" +import { useDataTable } from "../../../../hooks/use-data-table" +import { Combobox } from "../../../../components/common/combobox" +import { FileUpload } from "../../../../components/common/file-upload" +import { List } from "../../../../components/common/list" +import { useProductTypes } from "../../../../hooks/api/product-types" +import { useCollections } from "../../../../hooks/api/collections" +import { useSalesChannels } from "../../../../hooks/api/sales-channels" +import { useCategories } from "../../../../hooks/api/categories" +import { useTags } from "../../../../hooks/api/tags" +import { Keypair } from "../../../../components/common/keypair" +import { UseFormReturn } from "react-hook-form" +import { CreateProductSchemaType } from "../schema" -type CreateProductPropsProps = { - form: CreateProductFormReturn +type ProductAttributesProps = { + form: UseFormReturn } const SUPPORTED_FORMATS = [ @@ -76,7 +77,7 @@ const generateNameFromPermutation = (permutation: { return Object.values(permutation).join(" / ") } -export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { +export const ProductAttributesForm = ({ form }: ProductAttributesProps) => { const { t } = useTranslation() const [open, onOpenChange] = useState(false) const { product_types, isLoading: isLoadingTypes } = useProductTypes() @@ -466,7 +467,6 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { generateNameFromPermutation(options), variant_rank: i, options, - prices: [], } }) ) diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-create/product-create.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-create/product-create.tsx index 4316f5dfe4..5aa1611ae1 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-create/product-create.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-create/product-create.tsx @@ -1,10 +1,10 @@ import { RouteFocusModal } from "../../../components/route-modal" -import { CreateProductForm } from "./components/create-product-form" +import { CreateProductPage } from "./components/create-product" export const ProductCreate = () => { return ( - + ) } diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-create/schema.ts b/packages/admin-next/dashboard/src/v2-routes/products/product-create/schema.ts new file mode 100644 index 0000000000..4b60270bac --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-create/schema.ts @@ -0,0 +1,82 @@ +import { CreateProductDTO, CreateProductVariantDTO } from "@medusajs/types" +import * as zod from "zod" + +export const CreateProductSchema = zod.object({ + title: zod.string(), + subtitle: zod.string().optional(), + handle: zod.string().optional(), + description: zod.string().optional(), + discountable: zod.boolean(), + type_id: zod.string().optional(), + collection_id: zod.string().optional(), + category_ids: zod.array(zod.string()).optional(), + tags: zod.array(zod.string()).optional(), + sales_channels: zod.array(zod.string()).optional(), + origin_country: zod.string().optional(), + material: zod.string().optional(), + width: zod.string().optional(), + length: zod.string().optional(), + height: zod.string().optional(), + weight: zod.string().optional(), + mid_code: zod.string().optional(), + hs_code: zod.string().optional(), + options: zod.array( + zod.object({ + title: zod.string(), + values: zod.array(zod.string()), + }) + ), + variants: zod.array( + zod.object({ + title: zod.string(), + options: zod.record(zod.string(), zod.string()), + variant_rank: zod.number(), + prices: zod.record(zod.string(), zod.string()).optional(), + }) + ), + images: zod.array(zod.string()).optional(), + thumbnail: zod.string().optional(), +}) + +export const defaults = { + discountable: true, + tags: [], + sales_channels: [], + options: [], + variants: [], + images: [], +} + +export const normalize = ( + values: CreateProductSchemaType & { status: CreateProductDTO["status"] } +) => { + const reqData = { + ...values, + is_giftcard: false, + tags: values.tags?.map((tag) => ({ value: tag })), + sales_channels: values.sales_channels?.map((sc) => ({ id: sc })), + width: values.width ? parseFloat(values.width) : undefined, + length: values.length ? parseFloat(values.length) : undefined, + height: values.height ? parseFloat(values.height) : undefined, + weight: values.weight ? parseFloat(values.weight) : undefined, + variants: normalizeVariants(values.variants as any), + } as any + + return reqData +} + +export const normalizeVariants = ( + variants: (Partial & { + prices?: Record + })[] +) => { + return variants.map((variant) => ({ + ...variant, + prices: Object.entries(variant.prices || {}).map(([key, value]: any) => ({ + currency_code: key, + amount: value ? parseFloat(value) : 0, + })), + })) +} + +export type CreateProductSchemaType = zod.infer diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx index 2470f90650..331e461491 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx @@ -1,4 +1,4 @@ -import { Plus } from "@medusajs/icons" +import { PencilSquare, Plus } from "@medusajs/icons" import { Product } from "@medusajs/medusa" import { Container, Heading } from "@medusajs/ui" import { useTranslation } from "react-i18next" @@ -67,6 +67,11 @@ export const ProductVariantSection = ({ to: `variants/create`, icon: , }, + { + label: t("products.editPrices"), + to: `prices`, + icon: , + }, ], }, ]} diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-prices/index.ts b/packages/admin-next/dashboard/src/v2-routes/products/product-prices/index.ts new file mode 100644 index 0000000000..8d0f891069 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-prices/index.ts @@ -0,0 +1 @@ +export { ProductPrices as Component } from "./product-prices" diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-prices/pricing-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-prices/pricing-edit.tsx new file mode 100644 index 0000000000..d4b45be666 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-prices/pricing-edit.tsx @@ -0,0 +1,85 @@ +import { useUpdateProductVariant } from "../../../hooks/api/products" +import { RouteFocusModal, useRouteModal } from "../../../components/route-modal" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { ExtendedProductDTO } from "../../../types/api-responses" +import { VariantPricingForm } from "../common/variant-pricing-form" +import { normalizeVariants } from "../product-create/schema" +import { Button } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +export const UpdateVariantPricesSchema = zod.object({ + variants: zod.array( + zod.object({ + prices: zod.record(zod.string(), zod.string()).optional(), + }) + ), +}) + +export type UpdateVariantPricesSchemaType = zod.infer< + typeof UpdateVariantPricesSchema +> + +export const PricingEdit = ({ product }: { product: ExtendedProductDTO }) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + const form = useForm({ + defaultValues: { + variants: product.variants.map((variant: any) => ({ + prices: variant.prices.reduce((acc: any, price: any) => { + acc[price.currency_code] = price.amount + return acc + }, {}), + })) as any, + }, + + resolver: zodResolver(UpdateVariantPricesSchema, {}), + }) + + // TODO: Add batch update method here + const { mutateAsync, isLoading } = useUpdateProductVariant(product.id, "") + + const handleSubmit = form.handleSubmit( + async (values) => { + const reqData = { variants: normalizeVariants(values.variants) } + await mutateAsync(reqData, { + onSuccess: () => { + handleSuccess(`../${product.id}`) + }, + }) + }, + (err) => { + console.log(err) + } + ) + + return ( + +
+ +
+
+ + + + +
+
+
+ + + +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-prices/product-prices.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-prices/product-prices.tsx new file mode 100644 index 0000000000..81e1f55455 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-prices/product-prices.tsx @@ -0,0 +1,21 @@ +import { useParams } from "react-router-dom" + +import { useProduct } from "../../../hooks/api/products" +import { PricingEdit } from "./pricing-edit" +import { RouteFocusModal } from "../../../components/route-modal" + +export const ProductPrices = () => { + const { id } = useParams() + + const { product, isLoading, isError, error } = useProduct(id!) + + if (isError) { + throw error + } + + return ( + + {!isLoading && product && } + + ) +}