From eb8941fe0cb1509db3f64d7625f45b89cc3659ea Mon Sep 17 00:00:00 2001 From: fPolic Date: Mon, 13 May 2024 14:09:30 +0200 Subject: [PATCH] feat: edit shipping option prices --- .../providers/router-provider/route-map.tsx | 7 + .../location-general-section.tsx | 2 +- .../create-shipping-options-form.tsx | 10 +- .../edit-shipping-options-pricing-form.tsx | 319 ++++++++++++++++++ .../create-shipping-options-form/index.ts | 1 + .../shipping-options-edit-pricing/index.ts | 1 + .../shipping-options-edit-pricing.tsx | 30 ++ .../fulfillment/mutations/shipping-option.ts | 8 + 8 files changed, 372 insertions(+), 6 deletions(-) create mode 100644 packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/shipping-options-edit-pricing.tsx 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 6d19df59f8678..a365d8f5f19b0 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 @@ -771,6 +771,13 @@ export const RouteMap: RouteObject[] = [ "../../v2-routes/shipping/shipping-option-edit" ), }, + { + path: "edit-pricing", + lazy: () => + import( + "../../v2-routes/shipping/shipping-options-edit-pricing" + ), + }, ], }, ], diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx index c99613863edf7..2bd363f14cab4 100644 --- a/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx @@ -156,7 +156,7 @@ function ShippingOption({ { label: t("shipping.serviceZone.editPrices"), icon: , - disabled: true, + to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/edit-pricing`, }, { label: t("actions.delete"), diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx index 4698810e5104b..87283058ec99c 100644 --- a/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx @@ -45,7 +45,7 @@ type StepStatus = { [key in Tab]: ProgressStatus } -const CreateServiceZoneSchema = zod.object({ +const CreateShippingOptionSchema = zod.object({ name: zod.string().min(1), price_type: zod.nativeEnum(ShippingAllocation), enabled_in_store: zod.boolean().optional(), @@ -55,7 +55,7 @@ const CreateServiceZoneSchema = zod.object({ currency_prices: zod.record(zod.string(), zod.string().optional()), }) -type CreateServiceZoneFormProps = { +type CreateShippingOptionFormProps = { zone: ServiceZoneDTO isReturn?: boolean } @@ -63,7 +63,7 @@ type CreateServiceZoneFormProps = { export function CreateShippingOptionsForm({ zone, isReturn, -}: CreateServiceZoneFormProps) { +}: CreateShippingOptionFormProps) { const { t } = useTranslation() const { handleSuccess } = useRouteModal() const [tab, setTab] = React.useState(Tab.DETAILS) @@ -77,7 +77,7 @@ export function CreateShippingOptionsForm({ fields: "id,currency_code", }) - const form = useForm>({ + const form = useForm>({ defaultValues: { name: "", price_type: ShippingAllocation.FlatRate, @@ -87,7 +87,7 @@ export function CreateShippingOptionsForm({ region_prices: {}, currency_prices: {}, }, - resolver: zodResolver(CreateServiceZoneSchema), + resolver: zodResolver(CreateShippingOptionSchema), }) const isCalculatedPriceType = diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx new file mode 100644 index 0000000000000..c5d0dbcc68256 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx @@ -0,0 +1,319 @@ +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import React, { useEffect, useMemo, useState } from "react" +import * as zod from "zod" + +import { Button, toast } from "@medusajs/ui" +import { + CurrencyDTO, + PriceDTO, + ProductVariantDTO, + RegionDTO, + ShippingOptionDTO, +} from "@medusajs/types" +import { useTranslation } from "react-i18next" + +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { + getDbAmount, + getPresentationalAmount, +} from "../../../../../lib/money-amount-helpers" +import { useRegions } from "../../../../../hooks/api/regions" +import { useStore } from "../../../../../hooks/api/store.tsx" +import { useCurrencies } from "../../../../../hooks/api/currencies" +import { ColumnDef, createColumnHelper } from "@tanstack/react-table" +import { ExtendedProductDTO } from "../../../../../types/api-responses" +import { CurrencyCell } from "../../../../../components/grid/grid-cells/common/currency-cell" +import { DataGridMeta } from "../../../../../components/grid/types" +import { DataGrid } from "../../../../../components/grid/data-grid" +import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options.ts" + +const getInitialCurrencyPrices = (prices: PriceDTO[]) => { + const ret: Record = {} + prices.forEach((p) => { + if (p.price_rules!.length) { + // this is a region price + return + } + ret[p.currency_code!] = getPresentationalAmount( + p.amount as number, + p.currency_code! + ) + }) + return ret +} + +const getInitialRegionPrices = (prices: PriceDTO[]) => { + const ret: Record = {} + prices.forEach((p) => { + if (p.price_rules!.length) { + const regionId = p.price_rules![0].value + ret[regionId] = getPresentationalAmount( + p.amount as number, + p.currency_code! + ) + } + }) + + return ret +} + +const EditShippingOptionPricingSchema = zod.object({ + region_prices: zod.record( + zod.string(), + zod.string().or(zod.number()).optional() + ), + currency_prices: zod.record( + zod.string(), + zod.string().or(zod.number()).optional() + ), +}) + +enum ColumnType { + REGION = "region", + CURRENCY = "currency", +} + +type EnabledColumnRecord = Record + +type EditShippingOptionPricingFormProps = { + shippingOption: ShippingOptionDTO & { prices: PriceDTO[] } +} + +export function EditShippingOptionsPricingForm({ + shippingOption, +}: EditShippingOptionPricingFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + region_prices: getInitialRegionPrices(shippingOption.prices), + currency_prices: getInitialCurrencyPrices(shippingOption.prices), + }, + resolver: zodResolver(EditShippingOptionPricingSchema), + }) + + const { mutateAsync, isPending: isLoading } = useUpdateShippingOptions( + shippingOption.id + ) + + const { regions } = useRegions() + + const { store, isLoading: isStoreLoading } = useStore() + + const { currencies, isLoading: isCurrenciesLoading } = useCurrencies( + { + code: store?.supported_currency_codes, + }, + { + enabled: !!store, + } + ) + + const [enabledColumns, setEnabledColumns] = useState({}) + + useEffect(() => { + if ( + store?.default_currency_code && + Object.keys(enabledColumns).length === 0 + ) { + setEnabledColumns({ + ...enabledColumns, + [store.default_currency_code]: ColumnType.CURRENCY, + }) + } + }, [store, enabledColumns]) + + const columns = useColumns({ + currencies, + regions, + }) + + const data = useMemo( + () => [[...(currencies || []), ...(regions || [])]], + [currencies, regions] + ) + + const handleSubmit = form.handleSubmit(async (data) => { + const currencyPrices = Object.entries(data.currency_prices) + .map(([code, value]) => { + if (value === "") { + return undefined + } + + const amount = getDbAmount(Number(value), code) + + const priceRecord = { + currency_code: code, + amount: amount, + } + + const price = shippingOption.prices.find( + (p) => p.currency_code === code && !p.price_rules!.length + ) + + // if that currency price is already defined for the SO, we will do an update + if (price) { + priceRecord["id"] = price.id + } + + return priceRecord + }) + .filter((p) => !!p) + + const regionsMap = new Map(regions.map((r) => [r.id, r.currency_code])) + + const regionPrices = Object.entries(data.region_prices) + .map(([region_id, value]) => { + if (value === "") { + return undefined + } + + const code = regionsMap.get(region_id)! + + const amount = getDbAmount(Number(value), code) + + const priceRecord = { + region_id, + amount: amount, + } + + /** + * HACK - when trying to update prices which already have a region price + * we get error: `Price rule with price_id: , rule_type_id: already exist`, + * so for now, we recreate region prices. + */ + + // const price = shippingOption.prices.find( + // (p) => p.price_rules?.[0]?.value === region_id + // ) + // + // if (price) { + // priceRecord["id"] = price.id + // } + + return priceRecord + }) + .filter((p) => !!p) + + try { + await mutateAsync({ + prices: [...currencyPrices, ...regionPrices], + }) + toast.error(t("general.success"), { + dismissLabel: t("general.close"), + }) + handleSuccess() + } catch (e) { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("general.close"), + }) + } + }) + + const initializing = + isStoreLoading || isCurrenciesLoading || !store || !currencies + + return ( + +
+ +
+ + + + +
+
+ + +
+ +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper< + ExtendedProductDTO | ProductVariantDTO +>() + +const useColumns = ({ + currencies = [], + regions = [], +}: { + currencies?: CurrencyDTO[] + regions?: RegionDTO[] +}) => { + const { t } = useTranslation() + + const colDefs: ColumnDef[] = + useMemo(() => { + return [ + ...currencies.map((currency) => { + return columnHelper.display({ + header: t("fields.priceTemplate", { + regionOrCountry: currency.code.toUpperCase(), + }), + cell: ({ row, table }) => { + return ( + } + field={`currency_prices.${currency.code}`} + /> + ) + }, + }) + }), + ...regions.map((region) => { + return columnHelper.display({ + header: t("fields.priceTemplate", { + regionOrCountry: region.name, + }), + cell: ({ row, table }) => { + return ( + c.code === region.currency_code + )} + meta={table.options.meta as DataGridMeta} + field={`region_prices.${region.id}`} + /> + ) + }, + }) + }), + ] + }, [t, currencies, regions]) + + return colDefs +} diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/index.ts new file mode 100644 index 0000000000000..9839576e71f48 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/components/create-shipping-options-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-shipping-options-pricing-form.tsx" diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/index.ts b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/index.ts new file mode 100644 index 0000000000000..d841b62cdbcd4 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/index.ts @@ -0,0 +1 @@ +export { ShippingOptionsEditPricing as Component } from "./shipping-options-edit-pricing" diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/shipping-options-edit-pricing.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/shipping-options-edit-pricing.tsx new file mode 100644 index 0000000000000..9c4c70fa84b5b --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/shipping/shipping-options-edit-pricing/shipping-options-edit-pricing.tsx @@ -0,0 +1,30 @@ +import { useParams } from "react-router-dom" + +import { RouteFocusModal } from "../../../components/route-modal" +import { useShippingOptions } from "../../../hooks/api/shipping-options" +import { EditShippingOptionsPricingForm } from "./components/create-shipping-options-form" + +export function ShippingOptionsEditPricing() { + const { so_id } = useParams() + + const { shipping_options, isPending } = useShippingOptions({ + // TODO: change this when GET option by id endpoint is implemented + id: [so_id], + fields: "*prices,*prices.price_rules", + limit: 999, + }) + + const shippingOption = shipping_options?.find((so) => so.id === so_id) + + if (!isPending && !shippingOption) { + throw new Error(`Shipping option with id: ${so_id} not found`) + } + + return ( + + {shippingOption && ( + + )} + + ) +} diff --git a/packages/core/types/src/fulfillment/mutations/shipping-option.ts b/packages/core/types/src/fulfillment/mutations/shipping-option.ts index a4931eb8fbd3e..06ab9c209c6f6 100644 --- a/packages/core/types/src/fulfillment/mutations/shipping-option.ts +++ b/packages/core/types/src/fulfillment/mutations/shipping-option.ts @@ -120,6 +120,14 @@ export interface UpdateShippingOptionDTO { id: string } )[] + + /** + * The shipping option pricing + */ + prices: ( + | { currency_code: string; amount: number; id?: string } + | { region_id: string; amount: number; id?: string } + )[] } /**