fix(dashboard): location UI fixes (#7288)

* fix: a few UI fixes

* fix: domain name

* feat: edit service zone areas

* feat: edit shipping option prices

* fix: sorting
This commit is contained in:
Frane Polić
2024-05-16 19:34:49 +02:00
committed by GitHub
parent a775d57255
commit 694434d51a
18 changed files with 811 additions and 30 deletions

View File

@@ -62,6 +62,7 @@
},
"actions": {
"save": "Save",
"select": "Select",
"saveAsDraft": "Save as draft",
"publish": "Publish",
"create": "Create",
@@ -653,8 +654,8 @@
}
},
"shipping": {
"title": "Location & Shipping",
"domain": "Location & Shipping",
"title": "Locations & Shipping",
"domain": "Locations & Shipping",
"description": "Choose where you ship and how much you charge for shipping at checkout. Define shipping options specific for your locations.",
"createLocation": "Create location",
"createLocationDetailsHint": "Specify the details of the location.",
@@ -714,10 +715,12 @@
"edit": {
"title": "Edit Service Zone"
},
"editAreasTitle": "Manage {{zone}} areas",
"deleteWarning": "Are you sure you want to delete \"{{name}}\". This will also delete all assocciated shipping options.",
"toast": {
"delete": "Zone \"{{name}}\" deleted successfully."
},
"manageAreas": "Manage areas",
"editPrices": "Edit prices",
"editOption": "Edit option",
"optionsLength_one": "shipping option",
@@ -745,8 +748,8 @@
"allocation": "Shipping amount",
"fixed": "Fixed",
"fixedDescription": "Shipping option's price is always the same amount.",
"enable": "Enable in store",
"enableDescription": "Enable or disable the shipping option visiblity in store",
"enable": "Show publicly",
"enableDescription": "When disabled, the shipping option can only be applied by admins.",
"calculated": "Calculated",
"calculatedDescription": "Shipping option's price is calculated by the fulfillment provider.",
"profile": "Shipping profile"

View File

@@ -41,7 +41,7 @@ export const useStockLocation = (
) => {
const { data, ...rest } = useQuery({
queryFn: () => client.stockLocations.retrieve(id, query),
queryKey: stockLocationsQueryKeys.detail(id, query),
queryKey: stockLocationsQueryKeys.details(),
...options,
})

View File

@@ -744,6 +744,13 @@ export const RouteMap: RouteObject[] = [
"../../v2-routes/shipping/service-zone-edit"
),
},
{
path: "edit-areas",
lazy: () =>
import(
"../../v2-routes/shipping/service-zone-areas-edit"
),
},
{
path: "shipping-option",
children: [
@@ -764,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"
),
},
],
},
],

View File

@@ -156,7 +156,7 @@ function ShippingOption({
{
label: t("shipping.serviceZone.editPrices"),
icon: <CurrencyDollar />,
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"),
@@ -199,7 +199,7 @@ function ServiceZoneOptions({
{t("shipping.serviceZone.shippingOptions")}
</span>
<Button
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent"
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent active:bg-transparent"
variant="transparent"
onClick={() =>
navigate(
@@ -231,7 +231,7 @@ function ServiceZoneOptions({
{t("shipping.serviceZone.returnOptions")}
</span>
<Button
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent"
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent active:bg-transparent"
variant="transparent"
onClick={() =>
navigate(
@@ -313,6 +313,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
.filter((g) => g.type === "country")
.map((g) => g.country_code)
.map((code) => staticCountries.find((c) => c.iso_2 === code))
.sort((c1, c2) => c1.name.localeCompare(c2.name))
}, zone.geo_zones)
const [shippingOptionsCount, returnOptionsCount] = useMemo(() => {
@@ -385,16 +386,16 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
groups={[
{
actions: [
// {
// label: t("shipping.serviceZone.addOption"),
// icon: <Plus />,
// to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create`,
// },
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit`,
},
{
label: t("shipping.serviceZone.manageAreas"),
icon: <Map />,
to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit-areas`,
},
{
label: t("actions.delete"),
icon: <Trash />,

View File

@@ -181,7 +181,7 @@ function Location(props: LocationProps) {
]}
/>
<Button
className="text-ui-fg-interactive rounded-none pl-5 hover:bg-transparent"
className="text-ui-fg-interactive rounded-none pl-5 hover:bg-transparent active:bg-transparent"
onClick={() => navigate(`/settings/shipping/${location.id}`)}
variant="transparent"
>

View File

@@ -0,0 +1,344 @@
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import {
ColumnDef,
createColumnHelper,
RowSelectionState,
} from "@tanstack/react-table"
import * as zod from "zod"
import {
Alert,
Badge,
Button,
Checkbox,
Heading,
IconButton,
Text,
toast,
} from "@medusajs/ui"
import { RegionCountryDTO, RegionDTO, ServiceZoneDTO } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import { XMarkMini } from "@medusajs/icons"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { SplitView } from "../../../../../components/layout/split-view"
import {
useCreateServiceZone,
useUpdateServiceZone,
} from "../../../../../hooks/api/stock-locations"
import { useEffect, useMemo, useState } from "react"
import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query"
import { useCountries } from "../../../../regions/common/hooks/use-countries"
import { countries as staticCountries } from "../../../../../lib/countries"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
import { DataTable } from "../../../../../components/table/data-table"
const PREFIX = "ac"
const PAGE_SIZE = 50
const ConditionsFooter = ({ onSave }: { onSave: () => void }) => {
const { t } = useTranslation()
return (
<div className="flex items-center justify-end gap-x-2 border-t p-4">
<SplitView.Close type="button" asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</SplitView.Close>
<Button size="small" type="button" onClick={onSave}>
{t("actions.select")}
</Button>
</div>
)
}
const EditeServiceZoneSchema = zod.object({
countries: zod.array(zod.string().length(2)).min(1),
})
type EditServiceZoneAreasFormProps = {
fulfillmentSetId: string
locationId: string
zone: ServiceZoneDTO
}
export function EditServiceZoneAreasForm({
fulfillmentSetId,
locationId,
zone,
}: EditServiceZoneAreasFormProps) {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const [open, setOpen] = useState(false)
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
zone.geo_zones
.map((z) => z.country_code)
.reduce((acc, v) => {
acc[v] = true
return acc
}, {})
)
const form = useForm<zod.infer<typeof EditeServiceZoneSchema>>({
defaultValues: {
countries: zone.geo_zones.map((z) => z.country_code),
},
resolver: zodResolver(EditeServiceZoneSchema),
})
const { mutateAsync: editServiceZone, isPending: isLoading } =
useUpdateServiceZone(fulfillmentSetId, zone.id, locationId)
const handleSubmit = form.handleSubmit(async (data) => {
try {
await editServiceZone({
geo_zones: data.countries.map((iso2) => ({
country_code: iso2,
type: "country",
})),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("general.close"),
})
}
handleSuccess()
})
const handleOpenChange = (open: boolean) => {
setOpen(open)
}
const { searchParams, raw } = useCountryTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX,
})
const { countries, count } = useCountries({
countries: staticCountries.map((c, i) => ({
display_name: c.display_name,
name: c.name,
id: i as any,
iso_2: c.iso_2,
iso_3: c.iso_3,
num_code: c.num_code,
region_id: null,
region: {} as RegionDTO,
})),
...searchParams,
})
const columns = useColumns()
const { table } = useDataTable({
data: countries || [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.iso_2,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
prefix: PREFIX,
})
const countriesWatch = form.watch("countries")
const onCountriesSave = () => {
form.setValue("countries", Object.keys(rowSelection))
setOpen(false)
}
const removeCountry = (iso2: string) => {
const state = { ...rowSelection }
delete state[iso2]
setRowSelection(state)
form.setValue(
"countries",
countriesWatch.filter((c) => c !== iso2)
)
}
const clearAll = () => {
setRowSelection({})
form.setValue("countries", [])
}
const selectedCountries = useMemo(() => {
return staticCountries.filter((c) => c.iso_2 in rowSelection)
}, [countriesWatch])
useEffect(() => {
// set selected rows from form state on open
if (open) {
setRowSelection(
countriesWatch.reduce((acc, c) => {
acc[c] = true
return acc
}, {})
)
}
}, [open])
const showAreasError =
form.formState.errors["countries"]?.type === "too_small"
return (
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="m-auto flex h-full w-full flex-col items-center divide-y overflow-hidden">
<SplitView open={open} onOpenChange={handleOpenChange}>
<SplitView.Content className="mx-auto max-w-[720px]">
<div className="container w-fit px-1 py-8">
<Heading className="mt-8 text-2xl">
{t("shipping.serviceZone.editAreasTitle", {
zone: zone.name,
})}
</Heading>
</div>
<div className="container flex items-center justify-between py-8 pr-1">
<div>
<Text weight="plus">
{t("shipping.serviceZone.areas.title")}
</Text>
<Text className="text-ui-fg-subtle mt-2">
{t("shipping.serviceZone.areas.description")}
</Text>
</div>
<Button
onClick={() => setOpen(true)}
variant="secondary"
type="button"
>
{t("shipping.serviceZone.areas.manage")}
</Button>
</div>
{!!selectedCountries.length && (
<div className="flex flex-wrap items-center gap-4">
{selectedCountries.map((c) => (
<Badge
key={c.iso_2}
className="text-ui-fg-subtle txt-small flex items-center gap-1 divide-x pr-0"
>
{c.display_name}
<IconButton
type="button"
onClick={() => removeCountry(c.iso_2)}
className="text-ui-fg-subtle p-0 px-1 pt-[1px]"
variant="transparent"
>
<XMarkMini />
</IconButton>
</Badge>
))}
<Button
type="button"
onClick={clearAll}
variant="transparent"
className="txt-small text-ui-fg-muted font-medium"
>
{t("actions.clearAll")}
</Button>
</div>
)}
{showAreasError && (
<Alert dismissible variant="error">
{t("shipping.serviceZone.areas.error")}
</Alert>
)}
</SplitView.Content>
<SplitView.Drawer>
<div className="flex size-full flex-col overflow-hidden">
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
search
pagination
layout="fill"
orderBy={["name", "code"]}
queryObject={raw}
prefix={PREFIX}
/>
<ConditionsFooter onSave={onCountriesSave} />
</div>
</SplitView.Drawer>
</SplitView>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}
const columnHelper = createColumnHelper<RegionCountryDTO>()
const useColumns = () => {
const base = useCountryTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
const isPreselected = !row.getCanSelect()
return (
<Checkbox
checked={row.getIsSelected() || isPreselected}
disabled={isPreselected}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
...base,
],
[base]
) as ColumnDef<RegionCountryDTO>[]
}

View File

@@ -0,0 +1 @@
export * from "./edit-service-zone-areas-form"

View File

@@ -0,0 +1 @@
export { ServiceZoneAreasEdit as Component } from "./service-zone-areas-edit"

View File

@@ -0,0 +1,45 @@
import { json, useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { EditServiceZoneAreasForm } from "./components/edit-region-areas-form"
import { useStockLocation } from "../../../hooks/api/stock-locations"
export const ServiceZoneAreasEdit = () => {
const { location_id, fset_id, zone_id } = useParams()
const { stock_location, isPending, isError, error } = useStockLocation(
location_id!,
{
// NOTE: use same query for all details page subroutes & fetches
fields:
"name,*sales_channels,address.city,address.country_code,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile",
}
)
const zone = stock_location?.fulfillment_sets
.find((f) => f.id === fset_id)
?.service_zones.find((z) => z.id === zone_id)
if (isError) {
throw error
}
if (!isPending && !zone) {
throw json(
{ message: `Service zone with ID ${zone_id} was not found` },
404
)
}
return (
<RouteFocusModal>
{!isPending && zone && (
<EditServiceZoneAreasForm
zone={zone}
fulfillmentSetId={fset_id}
locationId={location_id}
/>
)}
</RouteFocusModal>
)
}

View File

@@ -16,10 +16,11 @@ import {
IconButton,
Input,
Text,
toast,
} from "@medusajs/ui"
import { FulfillmentSetDTO, RegionCountryDTO, RegionDTO } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import { Map, XMark, XMarkMini } from "@medusajs/icons"
import { XMarkMini } from "@medusajs/icons"
import {
RouteFocusModal,
@@ -87,13 +88,20 @@ export function CreateServiceZoneForm({
useCreateServiceZone(locationId, fulfillmentSet.id)
const handleSubmit = form.handleSubmit(async (data) => {
await createServiceZone({
name: data.name,
geo_zones: data.countries.map((iso2) => ({
country_code: iso2,
type: "country",
})),
})
try {
await createServiceZone({
name: data.name,
geo_zones: data.countries.map((iso2) => ({
country_code: iso2,
type: "country",
})),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("general.close"),
})
}
handleSuccess()
})
@@ -201,7 +209,7 @@ export function CreateServiceZoneForm({
<RouteFocusModal.Body className="m-auto flex h-full w-full flex-col items-center divide-y overflow-hidden">
<SplitView open={open} onOpenChange={handleOpenChange}>
<SplitView.Content className="mx-auto max-w-[720px]">
<div className="container w-fit px-1 py-8">
<div className="container w-fit px-1 py-8">
<Heading className="mb-12 mt-8 text-2xl">
{t("shipping.fulfillmentSet.create.title", {
fulfillmentSet: fulfillmentSet.name,

View File

@@ -6,7 +6,9 @@ import { StockLocationRes } from "../../../types/api-responses"
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
const fulfillmentSetCreateQuery = (id: string) => ({
queryKey: stockLocationsQueryKeys.detail(id),
queryKey: stockLocationsQueryKeys.detail(id, {
fields: "*fulfillment_sets",
}),
queryFn: async () =>
client.stockLocations.retrieve(id, {
fields: "*fulfillment_sets",

View File

@@ -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>(Tab.DETAILS)
@@ -77,7 +77,7 @@ export function CreateShippingOptionsForm({
fields: "id,currency_code",
})
const form = useForm<zod.infer<typeof CreateServiceZoneSchema>>({
const form = useForm<zod.infer<typeof CreateShippingOptionSchema>>({
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 =

View File

@@ -6,7 +6,10 @@ import { StockLocationRes } from "../../../types/api-responses"
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
const fulfillmentSetCreateQuery = (id: string) => ({
queryKey: stockLocationsQueryKeys.list(), // Use the list cache key for now
queryKey: stockLocationsQueryKeys.detail(id, {
fields:
"*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options",
}),
queryFn: async () =>
client.stockLocations.retrieve(id, {
fields:

View File

@@ -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<string, number> = {}
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<string, number> = {}
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<string, ColumnType>
type EditShippingOptionPricingFormProps = {
shippingOption: ShippingOptionDTO & { prices: PriceDTO[] }
}
export function EditShippingOptionsPricingForm({
shippingOption,
}: EditShippingOptionPricingFormProps) {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof EditShippingOptionPricingSchema>>({
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<EnabledColumnRecord>({})
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 (
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button
size="small"
className="whitespace-nowrap"
isLoading={isLoading}
onClick={handleSubmit}
type="button"
>
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-fit overflow-auto">
<div
style={{ width: "100vw" }}
className="flex size-full flex-col divide-y"
>
<DataGrid
columns={columns}
data={data}
isLoading={initializing}
state={form}
/>
</div>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}
const columnHelper = createColumnHelper<
ExtendedProductDTO | ProductVariantDTO
>()
const useColumns = ({
currencies = [],
regions = [],
}: {
currencies?: CurrencyDTO[]
regions?: RegionDTO[]
}) => {
const { t } = useTranslation()
const colDefs: ColumnDef<ExtendedProductDTO | ProductVariantDTO>[] =
useMemo(() => {
return [
...currencies.map((currency) => {
return columnHelper.display({
header: t("fields.priceTemplate", {
regionOrCountry: currency.code.toUpperCase(),
}),
cell: ({ row, table }) => {
return (
<CurrencyCell
currency={currency}
meta={table.options.meta as DataGridMeta<any>}
field={`currency_prices.${currency.code}`}
/>
)
},
})
}),
...regions.map((region) => {
return columnHelper.display({
header: t("fields.priceTemplate", {
regionOrCountry: region.name,
}),
cell: ({ row, table }) => {
return (
<CurrencyCell
currency={currencies.find(
(c) => c.code === region.currency_code
)}
meta={table.options.meta as DataGridMeta<any>}
field={`region_prices.${region.id}`}
/>
)
},
})
}),
]
}, [t, currencies, regions])
return colDefs
}

View File

@@ -0,0 +1 @@
export * from "./edit-shipping-options-pricing-form.tsx"

View File

@@ -0,0 +1 @@
export { ShippingOptionsEditPricing as Component } from "./shipping-options-edit-pricing"

View File

@@ -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 (
<RouteFocusModal>
{shippingOption && (
<EditShippingOptionsPricingForm shippingOption={shippingOption} />
)}
</RouteFocusModal>
)
}

View File

@@ -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 }
)[]
}
/**