fix(dashboard,types,js-sdk): Locations & Shipping fixes and cleanup (#7715)
This commit is contained in:
1 parent
bc0c65c6b3
commit
2e8e7b27b6
116 files changed
+3512
-3288
No files matched your search
@@ -1,44 +0,0 @@
|
|||||||
async function generateCountries() {
|
|
||||||
const { countries } = await import("@medusajs/medusa/dist/utils/countries.js")
|
|
||||||
const fs = await import("fs")
|
|
||||||
const path = await import("path")
|
|
||||||
|
|
||||||
const arr = countries.map((c) => {
|
|
||||||
const iso_2 = c.alpha2.toLowerCase()
|
|
||||||
const iso_3 = c.alpha3.toLowerCase()
|
|
||||||
const num_code = parseInt(c.numeric, 10)
|
|
||||||
const name = c.name.toUpperCase()
|
|
||||||
const display_name = c.name
|
|
||||||
|
|
||||||
return {
|
|
||||||
iso_2,
|
|
||||||
iso_3,
|
|
||||||
num_code,
|
|
||||||
name,
|
|
||||||
display_name,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const json = JSON.stringify(arr, null, 2)
|
|
||||||
|
|
||||||
const dest = path.join(__dirname, "../src/lib/countries.ts")
|
|
||||||
const destDir = path.dirname(dest)
|
|
||||||
|
|
||||||
const fileContent = `/** This file is auto-generated. Do not modify it manually. */\nimport type { RegionCountryDTO } from "@medusajs/types"\n\nexport const countries: Omit<RegionCountryDTO, "id">[] = ${json}`
|
|
||||||
|
|
||||||
if (!fs.existsSync(destDir)) {
|
|
||||||
fs.mkdirSync(destDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(dest, fileContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
;(async () => {
|
|
||||||
console.log("Generating countries")
|
|
||||||
try {
|
|
||||||
await generateCountries()
|
|
||||||
console.log("Countries generated")
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
+12
-4
@@ -16,7 +16,10 @@ type BadgeListSummaryProps = {
|
|||||||
* Determines whether the center text is truncated if there is no space in the container
|
* Determines whether the center text is truncated if there is no space in the container
|
||||||
*/
|
*/
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
|
/**
|
||||||
|
* Whether the badges should be rounded
|
||||||
|
*/
|
||||||
|
rounded?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +27,7 @@ export const BadgeListSummary = ({
|
|||||||
list,
|
list,
|
||||||
className,
|
className,
|
||||||
inline,
|
inline,
|
||||||
|
rounded = false,
|
||||||
n = 2,
|
n = 2,
|
||||||
}: BadgeListSummaryProps) => {
|
}: BadgeListSummaryProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -35,7 +39,7 @@ export const BadgeListSummary = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"ml-2 text-ui-fg-subtle txt-compact-small gap-x-2 overflow-hidden",
|
"text-ui-fg-subtle txt-compact-small gap-x-2 overflow-hidden",
|
||||||
{
|
{
|
||||||
"inline-flex": inline,
|
"inline-flex": inline,
|
||||||
flex: !inline,
|
flex: !inline,
|
||||||
@@ -45,7 +49,7 @@ export const BadgeListSummary = ({
|
|||||||
>
|
>
|
||||||
{list.slice(0, n).map((item) => {
|
{list.slice(0, n).map((item) => {
|
||||||
return (
|
return (
|
||||||
<Badge key={item} size="2xsmall">
|
<Badge rounded={rounded ? "full" : "base"} key={item} size="2xsmall">
|
||||||
{item}
|
{item}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
@@ -62,7 +66,11 @@ export const BadgeListSummary = ({
|
|||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Badge size="2xsmall" className="cursor-default whitespace-nowrap">
|
<Badge
|
||||||
|
rounded={rounded ? "full" : "base"}
|
||||||
|
size="2xsmall"
|
||||||
|
className="cursor-default whitespace-nowrap"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const Divider = ({
|
|||||||
aria-orientation={orientation}
|
aria-orientation={orientation}
|
||||||
role="separator"
|
role="separator"
|
||||||
className={clx(
|
className={clx(
|
||||||
"border-ui-border-strong",
|
"border-ui-border-base",
|
||||||
{
|
{
|
||||||
"w-full border-t":
|
"w-full border-t":
|
||||||
orientation === "horizontal" && variant === "solid",
|
orientation === "horizontal" && variant === "solid",
|
||||||
|
|||||||
+11
-9
@@ -76,20 +76,22 @@ export const NoRecords = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"flex h-[400px] w-full flex-col items-center justify-center gap-y-6",
|
"flex h-[400px] w-full flex-col items-center justify-center gap-y-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-y-2">
|
<div className="flex flex-col items-center gap-y-3">
|
||||||
<ExclamationCircle />
|
<ExclamationCircle className="text-ui-fg-subtle" />
|
||||||
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<div className="flex flex-col items-center gap-y-1">
|
||||||
{title ?? t("general.noRecordsTitle")}
|
<Text size="small" leading="compact" weight="plus">
|
||||||
</Text>
|
{title ?? t("general.noRecordsTitle")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Text size="small" className="text-ui-fg-muted">
|
<Text size="small" className="text-ui-fg-muted">
|
||||||
{message ?? t("general.noRecordsMessage")}
|
{message ?? t("general.noRecordsMessage")}
|
||||||
</Text>
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{buttonVariant === "default" && <DefaultButton action={action} />}
|
{buttonVariant === "default" && <DefaultButton action={action} />}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { clx } from "@medusajs/ui"
|
||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
type IconAvatarProps = PropsWithChildren<{
|
||||||
|
className?: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this component when a design calls for an avatar with an icon.
|
||||||
|
*
|
||||||
|
* The `<Avatar/>` component from `@medusajs/ui` does not support passing an icon as a child.
|
||||||
|
*/
|
||||||
|
export const IconAvatar = ({ children, className }: IconAvatarProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"shadow-borders-base flex size-7 items-center justify-center rounded-md",
|
||||||
|
"[&>div]:bg-ui-bg-field [&>div]:text-ui-fg-subtle [&>div]:flex [&>div]:size-6 [&>div]:items-center [&>div]:justify-center [&>div]:rounded-[4px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./icon-avatar"
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./inline-link"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { clx } from "@medusajs/ui"
|
|
||||||
import { ComponentPropsWithoutRef } from "react"
|
|
||||||
import { Link } from "react-router-dom"
|
|
||||||
|
|
||||||
export const InlineLink = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComponentPropsWithoutRef<typeof Link>) => {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
className={clx(
|
|
||||||
"text-ui-fg-interactive transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover rounded-md outline-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./inline-tip"
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { clx } from "@medusajs/ui"
|
||||||
|
import { ComponentPropsWithoutRef, forwardRef } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
interface InlineTipProps extends ComponentPropsWithoutRef<"div"> {
|
||||||
|
/**
|
||||||
|
* The label to display in the tip.
|
||||||
|
*/
|
||||||
|
label?: string
|
||||||
|
/**
|
||||||
|
* The variant of the tip.
|
||||||
|
*/
|
||||||
|
variant?: "tip" | "warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component for rendering inline tips. Useful for providing additional information or context.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <InlineTip label="Info">
|
||||||
|
* This is an info tip.
|
||||||
|
* </InlineTip>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* TODO: Move to `@medusajs/ui` package.
|
||||||
|
*/
|
||||||
|
export const InlineTip = forwardRef<HTMLDivElement, InlineTipProps>(
|
||||||
|
({ variant = "tip", label, className, children, ...props }, ref) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const labelValue =
|
||||||
|
label || (variant === "warning" ? t("general.warning") : t("general.tip"))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-component txt-small text-ui-fg-subtle grid grid-cols-[4px_1fr] items-start gap-3 rounded-lg border p-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
className={clx("w-4px bg-ui-tag-neutral-icon h-full rounded-full", {
|
||||||
|
"bg-ui-tag-orange-icon": variant === "warning",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div className="text-pretty">
|
||||||
|
<strong className="txt-small-plus text-ui-fg-base">
|
||||||
|
{labelValue}:
|
||||||
|
</strong>{" "}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
InlineTip.displayName = "InlineTip"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./link-button"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { clx } from "@medusajs/ui"
|
||||||
|
import { ComponentPropsWithoutRef } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
interface LinkButtonProps extends ComponentPropsWithoutRef<typeof Link> {
|
||||||
|
variant?: "primary" | "interactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkButton = ({
|
||||||
|
className,
|
||||||
|
variant = "interactive",
|
||||||
|
...props
|
||||||
|
}: LinkButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={clx(
|
||||||
|
" transition-fg txt-compact-small-plus rounded-[4px] outline-none",
|
||||||
|
"focus-visible:shadow-borders-focus",
|
||||||
|
{
|
||||||
|
"text-ui-fg-interactive hover:text-ui-fg-interactive-hover":
|
||||||
|
variant === "interactive",
|
||||||
|
"text-ui-fg-base hover:text-ui-fg-subtle": variant === "primary",
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ type ListSummaryProps = {
|
|||||||
* Determines whether the center text is truncated if there is no space in the container
|
* Determines whether the center text is truncated if there is no space in the container
|
||||||
*/
|
*/
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
|
variant?: "base" | "compact"
|
||||||
|
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@@ -23,6 +24,7 @@ type ListSummaryProps = {
|
|||||||
export const ListSummary = ({
|
export const ListSummary = ({
|
||||||
list,
|
list,
|
||||||
className,
|
className,
|
||||||
|
variant = "compact",
|
||||||
inline,
|
inline,
|
||||||
n = 2,
|
n = 2,
|
||||||
}: ListSummaryProps) => {
|
}: ListSummaryProps) => {
|
||||||
@@ -35,10 +37,12 @@ export const ListSummary = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"text-ui-fg-subtle txt-compact-small gap-x-1 overflow-hidden",
|
"text-ui-fg-subtle gap-x-1 overflow-hidden",
|
||||||
{
|
{
|
||||||
"inline-flex": inline,
|
"inline-flex": inline,
|
||||||
flex: !inline,
|
flex: !inline,
|
||||||
|
"txt-compact-small": variant === "compact",
|
||||||
|
"txt-small": variant === "base",
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ const useSettingRoutes = (): NavItemProps[] => {
|
|||||||
to: "/settings/shipping-profiles",
|
to: "/settings/shipping-profiles",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("location.domain"),
|
label: t("stockLocations.domain"),
|
||||||
to: "/settings/locations",
|
to: "/settings/locations",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+34
-2
@@ -1,9 +1,10 @@
|
|||||||
import { Navigate, useLocation, useRouteError } from "react-router-dom"
|
|
||||||
|
|
||||||
import { ExclamationCircle } from "@medusajs/icons"
|
import { ExclamationCircle } from "@medusajs/icons"
|
||||||
import { Text } from "@medusajs/ui"
|
import { Text } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Navigate, useLocation, useRouteError } from "react-router-dom"
|
||||||
|
|
||||||
import { isFetchError } from "../../../lib/is-fetch-error"
|
import { isFetchError } from "../../../lib/is-fetch-error"
|
||||||
|
import { InlineTip } from "../../common/inline-tip"
|
||||||
|
|
||||||
// WIP - Need to allow wrapping <Outlet> with ErrorBoundary for more granular error handling.
|
// WIP - Need to allow wrapping <Outlet> with ErrorBoundary for more granular error handling.
|
||||||
export const ErrorBoundary = () => {
|
export const ErrorBoundary = () => {
|
||||||
@@ -53,7 +54,38 @@ export const ErrorBoundary = () => {
|
|||||||
<Text size="small" className="text-ui-fg-muted">
|
<Text size="small" className="text-ui-fg-muted">
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
|
<DevelopmentStack error={error} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders an error stack trace in development mode.
|
||||||
|
*
|
||||||
|
* We don't want to show stack traces in production, so this component is only
|
||||||
|
* rendered when the `NODE_ENV` is set to `development`.
|
||||||
|
*
|
||||||
|
* The reason for adding this is that `react-router-dom` can swallow certain types
|
||||||
|
* of errors, e.g. a missing export from a module that is exported, and will instead
|
||||||
|
* log a vague warning related to the route not exporting a Component.
|
||||||
|
*/
|
||||||
|
const DevelopmentStack = ({ error }: { error: unknown }) => {
|
||||||
|
const stack = error instanceof Error ? error.stack : null
|
||||||
|
const [stackType, stackMessage] = stack?.split(":") ?? ["", ""]
|
||||||
|
const isDevelopment = process.env.NODE_ENV === "development"
|
||||||
|
|
||||||
|
if (!isDevelopment) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InlineTip
|
||||||
|
variant="warning"
|
||||||
|
label={stackType}
|
||||||
|
className="mx-auto w-full max-w-[500px]"
|
||||||
|
>
|
||||||
|
<span className="font-mono">{stackMessage}</span>
|
||||||
|
</InlineTip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,21 +1,28 @@
|
|||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||||
import { client } from "../../lib/client"
|
import { sdk } from "../../lib/client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
|
||||||
const FULFILLMENT_PROVIDERS_QUERY_KEY = "f_providers" as const
|
const FULFILLMENT_PROVIDERS_QUERY_KEY = "fulfillment_providers" as const
|
||||||
export const fulfillmentProvidersQueryKeys = queryKeysFactory(
|
export const fulfillmentProvidersQueryKeys = queryKeysFactory(
|
||||||
FULFILLMENT_PROVIDERS_QUERY_KEY
|
FULFILLMENT_PROVIDERS_QUERY_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
export const useFulfillmentProviders = (
|
export const useFulfillmentProviders = (
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminFulfillmentProviderListParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<any, Error, any, QueryKey>,
|
UseQueryOptions<
|
||||||
|
HttpTypes.AdminFulfillmentProviderListResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminFulfillmentProviderListResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.fulfillmentProviders.list(query),
|
queryFn: () => sdk.admin.fulfillmentProvider.list(query),
|
||||||
queryKey: fulfillmentProvidersQueryKeys.list(query),
|
queryKey: fulfillmentProvidersQueryKeys.list(query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
QueryKey,
|
||||||
|
UseMutationOptions,
|
||||||
|
UseQueryOptions,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
|
import { sdk } from "../../lib/client"
|
||||||
|
import { queryClient } from "../../lib/query-client"
|
||||||
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import { shippingOptionsQueryKeys } from "./shipping-options"
|
||||||
|
import { stockLocationsQueryKeys } from "./stock-locations"
|
||||||
|
|
||||||
|
const FULFILLMENT_SETS_QUERY_KEY = "fulfillment_sets" as const
|
||||||
|
export const fulfillmentSetsQueryKeys = queryKeysFactory(
|
||||||
|
FULFILLMENT_SETS_QUERY_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useDeleteFulfillmentSet = (
|
||||||
|
id: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseMutationOptions<
|
||||||
|
HttpTypes.AdminFulfillmentSetDeleteResponse,
|
||||||
|
FetchError,
|
||||||
|
void
|
||||||
|
>,
|
||||||
|
"mutationFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => sdk.admin.fulfillmentSet.delete(id),
|
||||||
|
onSuccess: async (data, variables, context) => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: fulfillmentSetsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: fulfillmentSetsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// We need to invalidate all related entities. We invalidate using `all` keys to ensure that all relevant entities are invalidated.
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: shippingOptionsQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFulfillmentSetServiceZone = (
|
||||||
|
fulfillmentSetId: string,
|
||||||
|
serviceZoneId: string,
|
||||||
|
query?: HttpTypes.SelectParams,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
HttpTypes.AdminServiceZoneResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminServiceZoneResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () =>
|
||||||
|
sdk.admin.fulfillmentSet.retrieveServiceZone(
|
||||||
|
fulfillmentSetId,
|
||||||
|
serviceZoneId,
|
||||||
|
query
|
||||||
|
),
|
||||||
|
queryKey: fulfillmentSetsQueryKeys.detail(fulfillmentSetId, query),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateFulfillmentSetServiceZone = (
|
||||||
|
fulfillmentSetId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseMutationOptions<
|
||||||
|
HttpTypes.AdminFulfillmentSetResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminCreateFulfillmentSetServiceZone,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"mutationFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) =>
|
||||||
|
sdk.admin.fulfillmentSet.createServiceZone(fulfillmentSetId, payload),
|
||||||
|
onSuccess: async (data, variables, context) => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: fulfillmentSetsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateFulfillmentSetServiceZone = (
|
||||||
|
fulfillmentSetId: string,
|
||||||
|
serviceZoneId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseMutationOptions<
|
||||||
|
HttpTypes.AdminFulfillmentSetResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminUpdateFulfillmentSetServiceZone,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"mutationFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) =>
|
||||||
|
sdk.admin.fulfillmentSet.updateServiceZone(
|
||||||
|
fulfillmentSetId,
|
||||||
|
serviceZoneId,
|
||||||
|
payload
|
||||||
|
),
|
||||||
|
onSuccess: async (data, variables, context) => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: fulfillmentSetsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteFulfillmentServiceZone = (
|
||||||
|
fulfillmentSetId: string,
|
||||||
|
serviceZoneId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseMutationOptions<
|
||||||
|
HttpTypes.AdminServiceZoneDeleteResponse,
|
||||||
|
FetchError,
|
||||||
|
void
|
||||||
|
>,
|
||||||
|
"mutationFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
sdk.admin.fulfillmentSet.deleteServiceZone(
|
||||||
|
fulfillmentSetId,
|
||||||
|
serviceZoneId
|
||||||
|
),
|
||||||
|
onSuccess: async (data, variables, context) => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: fulfillmentSetsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: shippingOptionsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,33 +6,51 @@ import {
|
|||||||
UseQueryOptions,
|
UseQueryOptions,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
|
|
||||||
import { client } from "../../lib/client"
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import {
|
|
||||||
CreateShippingOptionReq,
|
|
||||||
UpdateShippingOptionReq,
|
|
||||||
} from "../../types/api-payloads"
|
|
||||||
import {
|
|
||||||
ShippingOptionDeleteRes,
|
|
||||||
ShippingOptionRes,
|
|
||||||
} from "../../types/api-responses"
|
|
||||||
import { stockLocationsQueryKeys } from "./stock-locations"
|
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import { stockLocationsQueryKeys } from "./stock-locations"
|
||||||
|
|
||||||
const SHIPPING_OPTIONS_QUERY_KEY = "shipping_options" as const
|
const SHIPPING_OPTIONS_QUERY_KEY = "shipping_options" as const
|
||||||
export const shippingOptionsQueryKeys = queryKeysFactory(
|
export const shippingOptionsQueryKeys = queryKeysFactory(
|
||||||
SHIPPING_OPTIONS_QUERY_KEY
|
SHIPPING_OPTIONS_QUERY_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: Endpoint is not implemented yet
|
||||||
|
// export const useShippingOption = (
|
||||||
|
// id: string,
|
||||||
|
// options?: UseQueryOptions<
|
||||||
|
// HttpTypes.AdminShippingOptionResponse,
|
||||||
|
// Error,
|
||||||
|
// HttpTypes.AdminShippingOptionResponse,
|
||||||
|
// QueryKey
|
||||||
|
// >
|
||||||
|
// ) => {
|
||||||
|
// const { data, ...rest } = useQuery({
|
||||||
|
// queryFn: () => sdk.admin.shippingOption.retrieve(id),
|
||||||
|
// queryKey: shippingOptionsQueryKeys.retrieve(id),
|
||||||
|
// ...options,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return { ...data, ...rest }
|
||||||
|
// }
|
||||||
|
|
||||||
export const useShippingOptions = (
|
export const useShippingOptions = (
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminShippingOptionListParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<any, Error, any, QueryKey>,
|
UseQueryOptions<
|
||||||
|
HttpTypes.AdminShippingOptionListResponse,
|
||||||
|
Error,
|
||||||
|
HttpTypes.AdminShippingOptionListResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.shippingOptions.list(query),
|
queryFn: () => sdk.admin.shippingOption.list(query),
|
||||||
queryKey: shippingOptionsQueryKeys.list(query),
|
queryKey: shippingOptionsQueryKeys.list(query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -42,13 +60,13 @@ export const useShippingOptions = (
|
|||||||
|
|
||||||
export const useCreateShippingOptions = (
|
export const useCreateShippingOptions = (
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
ShippingOptionRes,
|
HttpTypes.AdminShippingOptionResponse,
|
||||||
Error,
|
FetchError,
|
||||||
CreateShippingOptionReq
|
HttpTypes.AdminCreateShippingOption
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) => client.shippingOptions.create(payload),
|
mutationFn: (payload) => sdk.admin.shippingOption.create(payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.all,
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
@@ -65,13 +83,13 @@ export const useCreateShippingOptions = (
|
|||||||
export const useUpdateShippingOptions = (
|
export const useUpdateShippingOptions = (
|
||||||
id: string,
|
id: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
ShippingOptionRes,
|
HttpTypes.AdminShippingOptionResponse,
|
||||||
Error,
|
FetchError,
|
||||||
UpdateShippingOptionReq
|
HttpTypes.AdminUpdateShippingOption
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) => client.shippingOptions.update(id, payload),
|
mutationFn: (payload) => sdk.admin.shippingOption.update(id, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.all,
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
@@ -87,11 +105,15 @@ export const useUpdateShippingOptions = (
|
|||||||
|
|
||||||
export const useDeleteShippingOption = (
|
export const useDeleteShippingOption = (
|
||||||
optionId: string,
|
optionId: string,
|
||||||
options?: UseMutationOptions<ShippingOptionDeleteRes, Error, void>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminShippingOptionDeleteResponse,
|
||||||
|
FetchError,
|
||||||
|
void
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => client.shippingOptions.delete(optionId),
|
mutationFn: () => sdk.admin.shippingOption.delete(optionId),
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.all,
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
UseQueryOptions,
|
UseQueryOptions,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
import { CreateShippingProfileReq } from "../../types/api-payloads"
|
|
||||||
import {
|
|
||||||
ShippingProfileListRes,
|
|
||||||
ShippingProfileRes,
|
|
||||||
} from "../../types/api-responses"
|
|
||||||
|
|
||||||
import { DeleteResponse } from "@medusajs/types"
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
import { client } from "../../lib/client"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
|
||||||
@@ -23,13 +19,13 @@ export const shippingProfileQueryKeys = queryKeysFactory(
|
|||||||
|
|
||||||
export const useCreateShippingProfile = (
|
export const useCreateShippingProfile = (
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
ShippingProfileRes,
|
HttpTypes.AdminShippingProfileResponse,
|
||||||
Error,
|
FetchError,
|
||||||
CreateShippingProfileReq
|
HttpTypes.AdminCreateShippingProfile
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) => client.shippingProfiles.create(payload),
|
mutationFn: (payload) => sdk.admin.shippingProfile.create(payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: shippingProfileQueryKeys.lists(),
|
queryKey: shippingProfileQueryKeys.lists(),
|
||||||
@@ -45,14 +41,14 @@ export const useShippingProfile = (
|
|||||||
id: string,
|
id: string,
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
options?: UseQueryOptions<
|
options?: UseQueryOptions<
|
||||||
ShippingProfileRes,
|
HttpTypes.AdminShippingProfileResponse,
|
||||||
Error,
|
FetchError,
|
||||||
ShippingProfileRes,
|
HttpTypes.AdminShippingProfileResponse,
|
||||||
QueryKey
|
QueryKey
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.shippingProfiles.retrieve(id, query),
|
queryFn: () => sdk.admin.shippingProfile.retrieve(id, query),
|
||||||
queryKey: shippingProfileQueryKeys.detail(id, query),
|
queryKey: shippingProfileQueryKeys.detail(id, query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -61,19 +57,19 @@ export const useShippingProfile = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useShippingProfiles = (
|
export const useShippingProfiles = (
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminShippingProfileListParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
ShippingProfileListRes,
|
HttpTypes.AdminShippingProfileListResponse,
|
||||||
Error,
|
FetchError,
|
||||||
ShippingProfileListRes,
|
HttpTypes.AdminShippingProfileListResponse,
|
||||||
QueryKey
|
QueryKey
|
||||||
>,
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.shippingProfiles.list(query),
|
queryFn: () => sdk.admin.shippingProfile.list(query),
|
||||||
queryKey: shippingProfileQueryKeys.list(query),
|
queryKey: shippingProfileQueryKeys.list(query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -82,12 +78,19 @@ export const useShippingProfiles = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useDeleteShippingProfile = (
|
export const useDeleteShippingProfile = (
|
||||||
profileId: string,
|
id: string,
|
||||||
options?: UseMutationOptions<DeleteResponse, Error, void>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminShippingProfileDeleteResponse,
|
||||||
|
FetchError,
|
||||||
|
void
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => client.shippingProfiles.delete(profileId),
|
mutationFn: () => sdk.admin.shippingProfile.delete(id),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: shippingProfileQueryKeys.detail(id),
|
||||||
|
})
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: shippingProfileQueryKeys.lists(),
|
queryKey: shippingProfileQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,24 +6,11 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
|
|
||||||
import { client } from "../../lib/client"
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
import {
|
|
||||||
CreateFulfillmentSetReq,
|
|
||||||
CreateServiceZoneReq,
|
|
||||||
CreateStockLocationReq,
|
|
||||||
UpdateServiceZoneReq,
|
|
||||||
UpdateStockLocationReq,
|
|
||||||
UpdateStockLocationSalesChannelsReq,
|
|
||||||
} from "../../types/api-payloads"
|
|
||||||
import {
|
|
||||||
FulfillmentSetDeleteRes,
|
|
||||||
ServiceZoneDeleteRes,
|
|
||||||
StockLocationDeleteRes,
|
|
||||||
StockLocationListRes,
|
|
||||||
StockLocationRes,
|
|
||||||
} from "../../types/api-responses"
|
|
||||||
|
|
||||||
const STOCK_LOCATIONS_QUERY_KEY = "stock_locations" as const
|
const STOCK_LOCATIONS_QUERY_KEY = "stock_locations" as const
|
||||||
export const stockLocationsQueryKeys = queryKeysFactory(
|
export const stockLocationsQueryKeys = queryKeysFactory(
|
||||||
@@ -32,14 +19,19 @@ export const stockLocationsQueryKeys = queryKeysFactory(
|
|||||||
|
|
||||||
export const useStockLocation = (
|
export const useStockLocation = (
|
||||||
id: string,
|
id: string,
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.SelectParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<StockLocationRes, Error, StockLocationRes, QueryKey>,
|
UseQueryOptions<
|
||||||
|
HttpTypes.AdminStockLocationResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminStockLocationResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryKey" | "queryFn"
|
"queryKey" | "queryFn"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.stockLocations.retrieve(id, query),
|
queryFn: () => sdk.admin.stockLocation.retrieve(id, query),
|
||||||
queryKey: stockLocationsQueryKeys.detail(id, query),
|
queryKey: stockLocationsQueryKeys.detail(id, query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -48,19 +40,19 @@ export const useStockLocation = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useStockLocations = (
|
export const useStockLocations = (
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminStockLocationListParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
StockLocationListRes,
|
HttpTypes.AdminStockLocationListResponse,
|
||||||
Error,
|
FetchError,
|
||||||
StockLocationListRes,
|
HttpTypes.AdminStockLocationListResponse,
|
||||||
QueryKey
|
QueryKey
|
||||||
>,
|
>,
|
||||||
"queryKey" | "queryFn"
|
"queryKey" | "queryFn"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.stockLocations.list(query),
|
queryFn: () => sdk.admin.stockLocation.list(query),
|
||||||
queryKey: stockLocationsQueryKeys.list(query),
|
queryKey: stockLocationsQueryKeys.list(query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -69,12 +61,16 @@ export const useStockLocations = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useCreateStockLocation = (
|
export const useCreateStockLocation = (
|
||||||
options?: UseMutationOptions<StockLocationRes, Error, CreateStockLocationReq>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminStockLocationResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminCreateStockLocation
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) => client.stockLocations.create(payload),
|
mutationFn: (payload) => sdk.admin.stockLocation.create(payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: async (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -86,15 +82,19 @@ export const useCreateStockLocation = (
|
|||||||
|
|
||||||
export const useUpdateStockLocation = (
|
export const useUpdateStockLocation = (
|
||||||
id: string,
|
id: string,
|
||||||
options?: UseMutationOptions<StockLocationRes, Error, UpdateStockLocationReq>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminStockLocationResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminUpdateStockLocation
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) => client.stockLocations.update(id, payload),
|
mutationFn: (payload) => sdk.admin.stockLocation.update(id, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: async (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.details(),
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -107,21 +107,22 @@ export const useUpdateStockLocation = (
|
|||||||
export const useUpdateStockLocationSalesChannels = (
|
export const useUpdateStockLocationSalesChannels = (
|
||||||
id: string,
|
id: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
StockLocationRes,
|
HttpTypes.AdminStockLocationResponse,
|
||||||
Error,
|
FetchError,
|
||||||
UpdateStockLocationSalesChannelsReq
|
HttpTypes.AdminUpdateStockLocationSalesChannels
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) =>
|
mutationFn: (payload) =>
|
||||||
client.stockLocations.updateSalesChannels(id, payload),
|
sdk.admin.stockLocation.updateSalesChannels(id, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: async (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.details(),
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
@@ -130,15 +131,19 @@ export const useUpdateStockLocationSalesChannels = (
|
|||||||
|
|
||||||
export const useDeleteStockLocation = (
|
export const useDeleteStockLocation = (
|
||||||
id: string,
|
id: string,
|
||||||
options?: UseMutationOptions<StockLocationDeleteRes, Error, void>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminStockLocationDeleteResponse,
|
||||||
|
FetchError,
|
||||||
|
void
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => client.stockLocations.delete(id),
|
mutationFn: () => sdk.admin.stockLocation.delete(id),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: async (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.detail(id),
|
queryKey: stockLocationsQueryKeys.detail(id),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -148,105 +153,22 @@ export const useDeleteStockLocation = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCreateFulfillmentSet = (
|
export const useCreateStockLocationFulfillmentSet = (
|
||||||
locationId: string,
|
locationId: string,
|
||||||
options?: UseMutationOptions<StockLocationRes, Error, CreateFulfillmentSetReq>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminStockLocationResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminCreateStockLocationFulfillmentSet
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) =>
|
mutationFn: (payload) =>
|
||||||
client.stockLocations.createFulfillmentSet(locationId, payload),
|
sdk.admin.stockLocation.createFulfillmentSet(locationId, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: async (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.details(),
|
|
||||||
})
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCreateServiceZone = (
|
|
||||||
locationId: string,
|
|
||||||
fulfillmentSetId: string,
|
|
||||||
options?: UseMutationOptions<StockLocationRes, Error, CreateServiceZoneReq>
|
|
||||||
) => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (payload) =>
|
|
||||||
client.stockLocations.createServiceZone(fulfillmentSetId, payload),
|
|
||||||
onSuccess: (data, variables, context) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.details(),
|
|
||||||
})
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
|
||||||
})
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUpdateServiceZone = (
|
|
||||||
fulfillmentSetId: string,
|
|
||||||
serviceZoneId: string,
|
|
||||||
locationId: string,
|
|
||||||
options?: UseMutationOptions<StockLocationRes, Error, UpdateServiceZoneReq>
|
|
||||||
) => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (payload) =>
|
|
||||||
client.stockLocations.updateServiceZone(
|
|
||||||
fulfillmentSetId,
|
|
||||||
serviceZoneId,
|
|
||||||
payload
|
|
||||||
),
|
|
||||||
onSuccess: (data, variables, context) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.details(),
|
|
||||||
})
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
|
||||||
})
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDeleteFulfillmentSet = (
|
|
||||||
setId: string,
|
|
||||||
options?: UseMutationOptions<FulfillmentSetDeleteRes, Error, void>
|
|
||||||
) => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: () => client.stockLocations.deleteFulfillmentSet(setId),
|
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
|
||||||
})
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.details(),
|
|
||||||
})
|
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDeleteServiceZone = (
|
|
||||||
setId: string,
|
|
||||||
zoneId: string,
|
|
||||||
options?: UseMutationOptions<ServiceZoneDeleteRes, Error, void>
|
|
||||||
) => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: () => client.stockLocations.deleteServiceZone(setId, zoneId),
|
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
|
||||||
})
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: stockLocationsQueryKeys.details(),
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
|
||||||
import { SalesChannelDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { StatusCell } from "../../../components/table/table-cells/common/status-cell"
|
import { StatusCell } from "../../../components/table/table-cells/common/status-cell"
|
||||||
import { TextHeader } from "../../../components/table/table-cells/common/text-cell"
|
import { TextHeader } from "../../../components/table/table-cells/common/text-cell"
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +15,7 @@ import {
|
|||||||
NameHeader,
|
NameHeader,
|
||||||
} from "../../../components/table/table-cells/sales-channel/name-cell"
|
} from "../../../components/table/table-cells/sales-channel/name-cell"
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<SalesChannelDTO>()
|
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>()
|
||||||
|
|
||||||
export const useSalesChannelTableColumns = () => {
|
export const useSalesChannelTableColumns = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const useComboboxData = <
|
|||||||
const { data, ...rest } = useInfiniteQuery({
|
const { data, ...rest } = useInfiniteQuery({
|
||||||
queryKey: [...queryKey, query],
|
queryKey: [...queryKey, query],
|
||||||
queryFn: async ({ pageParam = 0 }) => {
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
return queryFn({
|
return await queryFn({
|
||||||
q: query,
|
q: query,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: pageParam,
|
offset: pageParam,
|
||||||
@@ -77,7 +77,11 @@ export const useComboboxData = <
|
|||||||
const disabled = !rest.isPending && !options.length && !searchValue
|
const disabled = !rest.isPending && !options.length && !searchValue
|
||||||
|
|
||||||
// // make sure that the default value is included in the option, if its not in options already
|
// // make sure that the default value is included in the option, if its not in options already
|
||||||
if (defaultValue && !options.find((o) => o.value === defaultValue)) {
|
if (
|
||||||
|
defaultValue &&
|
||||||
|
defaultOptions.length &&
|
||||||
|
!options.find((o) => o.value === defaultValue)
|
||||||
|
) {
|
||||||
options.unshift(defaultOptions[0])
|
options.unshift(defaultOptions[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
|
"tip": "Tip",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
@@ -766,124 +767,135 @@
|
|||||||
"invalidEmail": "Email must be a valid email address."
|
"invalidEmail": "Email must be a valid email address."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"location": {
|
"stockLocations": {
|
||||||
"title": "Locations & Shipping",
|
"domain": "Locations & Shipping",
|
||||||
"domain": "Location & Shipping",
|
"list": {
|
||||||
"description": "Choose where you ship and how much you charge for shipping at checkout. Define shipping options specific for your locations.",
|
"description": "Manage your store's stock locations and shipping options."
|
||||||
"createLocation": "Create location",
|
|
||||||
"createLocationDetailsHint": "Specify the details of the location.",
|
|
||||||
"deleteLocation": "Delete location",
|
|
||||||
"from": "Shipping from",
|
|
||||||
"add": "Add shipping",
|
|
||||||
"connectProvider": "Connect provider",
|
|
||||||
"addZone": "Add shipping zone",
|
|
||||||
"enablePickup": "Enable pickup",
|
|
||||||
"enableDelivery": "Enable delivery",
|
|
||||||
"deleteLocation": {
|
|
||||||
"label": "Delete Location",
|
|
||||||
"confirm": "Are you sure you want to delete {{name}} location",
|
|
||||||
"success": "{{name}} location successfully deleted"
|
|
||||||
},
|
|
||||||
"noRecords": {
|
|
||||||
"action": "Add Location",
|
|
||||||
"title": "No inventory locations",
|
|
||||||
"message": "Please create an invnetory location first."
|
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Add shipping for {{location}}",
|
"header": "Create Stock Location",
|
||||||
"delivery": "Delivery",
|
"hint": "A stock location is a physical site where products are stored and shipped from.",
|
||||||
"pickup": "Pickup",
|
"successToast": "Location {{name}} was successfully created."
|
||||||
"type": "Shipping type"
|
|
||||||
},
|
},
|
||||||
"fulfillmentSet": {
|
"edit": {
|
||||||
"placeholder": "Not covered by any shipping zones.",
|
"header": "Edit Stock Location",
|
||||||
"salesChannels": "Connected Sales Channels",
|
"viewInventory": "View inventory",
|
||||||
"delete": "Delete shipping",
|
"successToast": "Location {{name}} was successfully updated."
|
||||||
"disableWarning": "Are you sure that you wnat to disable \"{{name}}\"? This will delete all assocciated service zones and shipping options.",
|
},
|
||||||
"create": {
|
"delete": {
|
||||||
"title": "Add service zone for {{fulfillmentSet}}"
|
"confirmation": "You are about to delete the stock location {{name}}. This action cannot be undone."
|
||||||
},
|
},
|
||||||
"toast": {
|
"fulfillmentSets": {
|
||||||
"disable": "\"{{name}}\" disabled"
|
|
||||||
},
|
|
||||||
"addZone": "Add service zone",
|
|
||||||
"pickup": {
|
"pickup": {
|
||||||
"title": "Pick up",
|
"header": "Pickup"
|
||||||
"enable": "Enable pickup",
|
|
||||||
"offers": "Offers pick up in"
|
|
||||||
},
|
},
|
||||||
"delivery": {
|
"shipping": {
|
||||||
"title": "Shipping",
|
"header": "Shipping"
|
||||||
"enable": "Enable delivery",
|
|
||||||
"offers": "Offers shippping to"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"serviceZone": {
|
|
||||||
"create": {
|
|
||||||
"title": "Add service zone for {{fulfillmentSet}}",
|
|
||||||
"subtitle": "Service zone",
|
|
||||||
"description": "A service zone is a geographical region that can be shipped to from a specific location. You can later on add any number of shipping options to this zone. ",
|
|
||||||
"zoneName": "Zone name"
|
|
||||||
},
|
},
|
||||||
"edit": {
|
"disable": {
|
||||||
"title": "Edit Service Zone"
|
"confirmation": "Are you sure that you want to disable {{name}}? This will delete all associated service zones and shipping options, and cannot be undone.",
|
||||||
|
"pickup": "Pickup was successfully disabled.",
|
||||||
|
"shipping": "Shipping was successfully disabled."
|
||||||
},
|
},
|
||||||
"deleteWarning": "Are you sure you want to delete \"{{name}}\". This will also delete all assocciated shipping options.",
|
"enable": {
|
||||||
"toast": {
|
"pickup": "Pickup was successfully enabled.",
|
||||||
"delete": "Zone \"{{name}}\" deleted successfully."
|
"shipping": "Shipping was successfully enabled."
|
||||||
},
|
|
||||||
"editPrices": "Edit prices",
|
|
||||||
"editOption": "Edit option",
|
|
||||||
"optionsLength_one": "shipping option",
|
|
||||||
"optionsLength_other": "shipping options",
|
|
||||||
"returnOptionsLength_one": "return option",
|
|
||||||
"returnOptionsLength_other": "return options",
|
|
||||||
"shippingOptionsPlaceholder": "Not covered by any shipping options.",
|
|
||||||
"addOption": "Add option",
|
|
||||||
"shippingOptions": "Shipping options",
|
|
||||||
"returnOptions": "Return options",
|
|
||||||
"areas": {
|
|
||||||
"title": "Areas affected by this rule",
|
|
||||||
"description": "Select the geographical areas where this shipping zone should apply.",
|
|
||||||
"manage": "Manage areas",
|
|
||||||
"error": "Please select at least one country for this service zone."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shippingOptions": {
|
|
||||||
"create": {
|
|
||||||
"title": "Create a shipping option for {{zone}}",
|
|
||||||
"subtitle": "General information",
|
|
||||||
"description": "To start selling, all you need is a name and a price",
|
|
||||||
"details": "Details",
|
|
||||||
"pricing": "Pricing",
|
|
||||||
"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",
|
|
||||||
"calculated": "Calculated",
|
|
||||||
"calculatedDescription": "Shipping option's price is calculated by the fulfillment provider.",
|
|
||||||
"profile": "Shipping profile"
|
|
||||||
},
|
|
||||||
"deleteWarning": "Are you sure you want to delete \"{{name}}\"?",
|
|
||||||
"toast": {
|
|
||||||
"delete": "Shipping option \"{{name}}\" deleted successfully."
|
|
||||||
},
|
|
||||||
"inStore": "Store",
|
|
||||||
"edit": {
|
|
||||||
"title": "Edit Shipping Option",
|
|
||||||
"provider": "Fulfillment provider"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"returnOptions": {
|
|
||||||
"create": {
|
|
||||||
"title": "Create a return option for {{zone}}"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"salesChannels": {
|
"salesChannels": {
|
||||||
"title": "Connected Sales Channels",
|
"header": "Sales Channels",
|
||||||
"placeholder": "No connected channels yet.",
|
"label": "Connected sales channels",
|
||||||
"connectChannels": "Connect Channels"
|
"connectedTo": "Connected to {{count}} of {{total}} sales channels",
|
||||||
|
"noChannels": "The location is not connected to any sales channels.",
|
||||||
|
"action": "Connect sales channels",
|
||||||
|
"successToast": "Sales channels were successfully updated."
|
||||||
|
},
|
||||||
|
"shippingOptions": {
|
||||||
|
"create": {
|
||||||
|
"shipping": {
|
||||||
|
"header": "Create Shipping Option for {{zone}}",
|
||||||
|
"hint": "Create a new shipping option to define how products are shipped from this location.",
|
||||||
|
"label": "Shipping options",
|
||||||
|
"successToast": "Shipping option {{name}} was successfully created."
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"header": "Create a Return Option for {{zone}}",
|
||||||
|
"hint": "Create a new return option to define how products are returned to this location.",
|
||||||
|
"label": "Return options",
|
||||||
|
"successToast": "Return option {{name}} was successfully created."
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"details": "Details",
|
||||||
|
"prices": "Prices"
|
||||||
|
},
|
||||||
|
"action": "Create option"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"confirmation": "You are about to delete the shipping option {{name}}. This action cannot be undone.",
|
||||||
|
"successToast": "Shipping option {{name}} was successfully deleted."
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"header": "Edit Shipping Option",
|
||||||
|
"action": "Edit option",
|
||||||
|
"successToast": "Shipping option {{name}} was successfully updated."
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"action": "Edit prices"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"count": {
|
||||||
|
"shipping_one": "{{count}} shipping option",
|
||||||
|
"shipping_other": "{{count}} shipping options",
|
||||||
|
"returns_one": "{{count}} return option",
|
||||||
|
"returns_other": "{{count}} return options"
|
||||||
|
},
|
||||||
|
"priceType": {
|
||||||
|
"label": "Price type",
|
||||||
|
"options": {
|
||||||
|
"fixed": {
|
||||||
|
"label": "Fixed",
|
||||||
|
"hint": "The shipping option's price is fixed and does not change based on the order's contents."
|
||||||
|
},
|
||||||
|
"calculated": {
|
||||||
|
"label": "Calculated",
|
||||||
|
"hint": "The shipping option's price is calculated by the fulfillment provider."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enableInStore": {
|
||||||
|
"label": "Enable in store",
|
||||||
|
"hint": "Control the visibility of this option to customers in your store."
|
||||||
|
},
|
||||||
|
"provider": "Fulfillment provider",
|
||||||
|
"profile": "Shipping profile"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serviceZones": {
|
||||||
|
"create": {
|
||||||
|
"headerPickup": "Create Service Zone for Pickup from {{location}}",
|
||||||
|
"headerShipping": "Create Service Zone for Shipping from {{location}}",
|
||||||
|
"action": "Create service zone",
|
||||||
|
"successToast": "Service zone {{name}} was successfully created."
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"header": "Edit Service Zone",
|
||||||
|
"successToast": "Service zone {{name}} was successfully updated."
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"confirmation": "You are about to delete the service zone {{name}}. This action cannot be undone.",
|
||||||
|
"successToast": "Service zone {{name}} was successfully deleted."
|
||||||
|
},
|
||||||
|
"manageAreas": {
|
||||||
|
"header": "Manage Areas for {{name}}",
|
||||||
|
"action": "Manage areas",
|
||||||
|
"label": "Areas",
|
||||||
|
"hint": "Select the geographical areas that the service zone covers.",
|
||||||
|
"successToast": "Areas for {{name}} were successfully updated."
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"noRecords": "Currently not covered by any service zones.",
|
||||||
|
"tip": "A service zone is a collection of geographical zones or areas. It's used to restrict available shipping options to a defined set of locations."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shippingProfile": {
|
"shippingProfile": {
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,13 +1,15 @@
|
|||||||
import { ShippingOptionDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
export function isReturnOption(shippingOption: ShippingOptionDTO) {
|
export function isReturnOption(shippingOption: HttpTypes.AdminShippingOption) {
|
||||||
return !!shippingOption.rules?.find(
|
return !!shippingOption.rules?.find(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.attribute === "is_return" && r.value === "true" && r.operator === "eq"
|
r.attribute === "is_return" && r.value === "true" && r.operator === "eq"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOptionEnabledInStore(shippingOption: ShippingOptionDTO) {
|
export function isOptionEnabledInStore(
|
||||||
|
shippingOption: HttpTypes.AdminShippingOption
|
||||||
|
) {
|
||||||
return !!shippingOption.rules?.find(
|
return !!shippingOption.rules?.find(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.attribute === "enabled_in_store" &&
|
r.attribute === "enabled_in_store" &&
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { PriceListRes } from "../../types/api-responses"
|
|||||||
import { RouteExtensions } from "./route-extensions"
|
import { RouteExtensions } from "./route-extensions"
|
||||||
import { SettingsExtensions } from "./settings-extensions"
|
import { SettingsExtensions } from "./settings-extensions"
|
||||||
|
|
||||||
|
// TODO: Add translations for all breadcrumbs
|
||||||
export const RouteMap: RouteObject[] = [
|
export const RouteMap: RouteObject[] = [
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
@@ -733,18 +734,20 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":location_id",
|
path: ":location_id",
|
||||||
lazy: () => import("../../routes/locations/location-details"),
|
lazy: () => import("../../routes/locations/location-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: HttpTypes.AdminStockLocationResponse) =>
|
||||||
|
data.stock_location.name,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "edit",
|
path: "edit",
|
||||||
lazy: () => import("../../routes/locations/location-edit"),
|
lazy: () => import("../../routes/locations/location-edit"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "sales-channels/edit",
|
path: "sales-channels",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import("../../routes/locations/location-sales-channels"),
|
||||||
"../../routes/locations/location-add-sales-channels"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "fulfillment-set/:fset_id",
|
path: "fulfillment-set/:fset_id",
|
||||||
@@ -752,7 +755,9 @@ export const RouteMap: RouteObject[] = [
|
|||||||
{
|
{
|
||||||
path: "service-zones/create",
|
path: "service-zones/create",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import("../../routes/locations/service-zone-create"),
|
import(
|
||||||
|
"../../routes/locations/location-service-zone-create"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "service-zone/:zone_id",
|
path: "service-zone/:zone_id",
|
||||||
@@ -761,14 +766,14 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "edit",
|
path: "edit",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
"../../routes/locations/service-zone-edit"
|
"../../routes/locations/location-service-zone-edit"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "edit-areas",
|
path: "areas",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
"../../routes/locations/service-zone-areas-edit"
|
"../../routes/locations/location-service-zone-manage-areas"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -778,7 +783,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "create",
|
path: "create",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
"../../routes/locations/shipping-options-create"
|
"../../routes/locations/location-service-zone-shipping-option-create"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -788,14 +793,14 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "edit",
|
path: "edit",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
"../../routes/locations/shipping-option-edit"
|
"../../routes/locations/location-service-zone-shipping-option-edit"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "edit-pricing",
|
path: "pricing",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
"../../routes/locations/shipping-options-edit-pricing"
|
"../../routes/locations/location-service-zone-shipping-option-pricing"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -810,6 +815,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "workflows",
|
path: "workflows",
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
|
|||||||
+6
-6
@@ -3,13 +3,13 @@ import {
|
|||||||
PencilSquare,
|
PencilSquare,
|
||||||
TriangleRightMini,
|
TriangleRightMini,
|
||||||
} from "@medusajs/icons"
|
} from "@medusajs/icons"
|
||||||
import { AdminProductCategoryResponse, HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Badge, Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
import { Badge, Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { InlineLink } from "../../../../../components/common/inline-link"
|
import { LinkButton } from "../../../../../components/common/link-button"
|
||||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||||
import { useProductCategory } from "../../../../../hooks/api/categories"
|
import { useProductCategory } from "../../../../../hooks/api/categories"
|
||||||
import { getCategoryChildren, getCategoryPath } from "../../../common/utils"
|
import { getCategoryChildren, getCategoryPath } from "../../../common/utils"
|
||||||
@@ -60,7 +60,7 @@ export const CategoryOrganizeSection = ({
|
|||||||
const PathDisplay = ({
|
const PathDisplay = ({
|
||||||
category,
|
category,
|
||||||
}: {
|
}: {
|
||||||
category: AdminProductCategoryResponse["product_category"]
|
category: HttpTypes.AdminProductCategory
|
||||||
}) => {
|
}) => {
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
@@ -137,12 +137,12 @@ const PathDisplay = ({
|
|||||||
{chip.name}
|
{chip.name}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<InlineLink
|
<LinkButton
|
||||||
to={`/categories/${chip.id}`}
|
to={`/categories/${chip.id}`}
|
||||||
className="txt-compact-xsmall-plus text-ui-fg-subtle hover:text-ui-fg-base focus-visible:text-ui-fg-base"
|
className="txt-compact-xsmall-plus text-ui-fg-subtle hover:text-ui-fg-base focus-visible:text-ui-fg-base"
|
||||||
>
|
>
|
||||||
{chip.name}
|
{chip.name}
|
||||||
</InlineLink>
|
</LinkButton>
|
||||||
)}
|
)}
|
||||||
{index < chips.length - 1 && <TriangleRightMini />}
|
{index < chips.length - 1 && <TriangleRightMini />}
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +170,7 @@ const PathDisplay = ({
|
|||||||
const ChildrenDisplay = ({
|
const ChildrenDisplay = ({
|
||||||
category,
|
category,
|
||||||
}: {
|
}: {
|
||||||
category: AdminProductCategoryResponse["product_category"]
|
category: HttpTypes.AdminProductCategory
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
product_category: withChildren,
|
product_category: withChildren,
|
||||||
|
|||||||
+312
@@ -0,0 +1,312 @@
|
|||||||
|
import { Button, Checkbox } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
OnChangeFn,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { UseFormReturn, useFieldArray } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { ChipGroup } from "../../../../../components/common/chip-group"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import {
|
||||||
|
StaticCountry,
|
||||||
|
countries as staticCountries,
|
||||||
|
} from "../../../../../lib/countries"
|
||||||
|
import { useCountries } from "../../../../regions/common/hooks/use-countries"
|
||||||
|
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
|
||||||
|
import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query"
|
||||||
|
|
||||||
|
const GeoZoneSchema = z.object({
|
||||||
|
countries: z.array(
|
||||||
|
z.object({ iso_2: z.string().min(2), display_name: z.string() })
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
type GeoZoneFormImplProps<TForm extends UseFormReturn<any>> = {
|
||||||
|
form: TForm
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const GeoZoneFormImpl = <TForm extends UseFormReturn<any>>({
|
||||||
|
form,
|
||||||
|
onOpenChange,
|
||||||
|
}: GeoZoneFormImplProps<TForm>) => {
|
||||||
|
const castForm = form as unknown as UseFormReturn<
|
||||||
|
z.infer<typeof GeoZoneSchema>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { fields, remove, replace } = useFieldArray({
|
||||||
|
control: castForm.control,
|
||||||
|
name: "countries",
|
||||||
|
keyName: "iso_2",
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClearAll = () => {
|
||||||
|
replace([])
|
||||||
|
}
|
||||||
|
|
||||||
|
validateForm(form)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="countries"
|
||||||
|
render={() => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex items-center justify-between gap-x-4">
|
||||||
|
<div>
|
||||||
|
<Form.Label>
|
||||||
|
{t("stockLocations.serviceZones.manageAreas.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint>
|
||||||
|
{t("stockLocations.serviceZones.manageAreas.hint")}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenChange(true)}
|
||||||
|
>
|
||||||
|
{t("stockLocations.serviceZones.manageAreas.action")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
<Form.Control className="mt-0">
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<ChipGroup
|
||||||
|
onClearAll={handleClearAll}
|
||||||
|
onRemove={remove}
|
||||||
|
className="py-4"
|
||||||
|
>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<ChipGroup.Chip key={field.iso_2} index={index}>
|
||||||
|
{field.display_name}
|
||||||
|
</ChipGroup.Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
)}
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AreasDrawerProps<TForm extends UseFormReturn<any>> = {
|
||||||
|
form: TForm
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIX = "ac"
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
const AreaDrawer = <TForm extends UseFormReturn<any>>({
|
||||||
|
form,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: AreasDrawerProps<TForm>) => {
|
||||||
|
const castForm = form as unknown as UseFormReturn<
|
||||||
|
z.infer<typeof GeoZoneSchema>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { getValues, setValue } = castForm
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<RowSelectionState>({})
|
||||||
|
const [state, setState] = useState<{ iso_2: string; display_name: string }[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { searchParams, raw } = useCountryTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
const { countries, count } = useCountries({
|
||||||
|
countries: staticCountries.map((c) => ({
|
||||||
|
display_name: c.display_name,
|
||||||
|
name: c.name,
|
||||||
|
iso_2: c.iso_2,
|
||||||
|
iso_3: c.iso_3,
|
||||||
|
num_code: c.num_code,
|
||||||
|
})),
|
||||||
|
...searchParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const countries = getValues("countries")
|
||||||
|
|
||||||
|
if (countries) {
|
||||||
|
setState(
|
||||||
|
countries.map((country) => ({
|
||||||
|
iso_2: country.iso_2,
|
||||||
|
display_name: country.display_name,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
setSelection(
|
||||||
|
countries.reduce(
|
||||||
|
(acc, country) => ({
|
||||||
|
...acc,
|
||||||
|
[country.iso_2]: true,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [open, getValues])
|
||||||
|
|
||||||
|
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||||
|
const value = typeof fn === "function" ? fn(selection) : fn
|
||||||
|
const ids = Object.keys(value)
|
||||||
|
|
||||||
|
const addedIdsSet = new Set(ids.filter((id) => value[id] && !selection[id]))
|
||||||
|
|
||||||
|
const addedCountries: { iso_2: string; display_name: string }[] = []
|
||||||
|
|
||||||
|
if (addedIdsSet.size > 0) {
|
||||||
|
const countriesToAdd =
|
||||||
|
countries?.filter((country) => addedIdsSet.has(country.iso_2!)) ?? []
|
||||||
|
|
||||||
|
for (const country of countriesToAdd) {
|
||||||
|
addedCountries.push({
|
||||||
|
iso_2: country.iso_2!,
|
||||||
|
display_name: country.display_name!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prev) => {
|
||||||
|
const filteredPrev = prev.filter((country) => value[country.iso_2])
|
||||||
|
return Array.from(new Set([...filteredPrev, ...addedCountries]))
|
||||||
|
})
|
||||||
|
setSelection(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setValue("countries", state, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: countries || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
getRowId: (row) => row.iso_2!,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
rowSelection: {
|
||||||
|
state: selection,
|
||||||
|
updater,
|
||||||
|
},
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
validateForm(form)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SplitView.Drawer>
|
||||||
|
<div className="flex h-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}
|
||||||
|
/>
|
||||||
|
<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={handleAdd}>
|
||||||
|
{t("actions.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<StaticCountry>()
|
||||||
|
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm(form: UseFormReturn) {
|
||||||
|
if (form.getValues("countries") === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"The form does not have a field named 'countries'. This field is required to use the GeoZoneForm component."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GeoZoneForm = Object.assign(GeoZoneFormImpl, {
|
||||||
|
AreaDrawer,
|
||||||
|
})
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./geo-zone-form"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export enum FulfillmentSetType {
|
||||||
|
Shipping = "shipping",
|
||||||
|
Pickup = "pickup",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ShippingOptionPriceType {
|
||||||
|
FlatRate = "flat",
|
||||||
|
Calculated = "calculated",
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { DataGridCurrencyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-currency-cell"
|
||||||
|
import { createDataGridHelper } from "../../../../components/data-grid/utils"
|
||||||
|
|
||||||
|
const columnHelper = createDataGridHelper<string | HttpTypes.AdminRegion>()
|
||||||
|
|
||||||
|
export const useShippingOptionPriceColumns = ({
|
||||||
|
currencies = [],
|
||||||
|
regions = [],
|
||||||
|
}: {
|
||||||
|
currencies?: string[]
|
||||||
|
regions?: HttpTypes.AdminRegion[]
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return [
|
||||||
|
...currencies.map((currency) => {
|
||||||
|
return columnHelper.column({
|
||||||
|
id: `currency_prices.${currency}`,
|
||||||
|
name: t("fields.priceTemplate", {
|
||||||
|
regionOrCountry: currency.toUpperCase(),
|
||||||
|
}),
|
||||||
|
header: t("fields.priceTemplate", {
|
||||||
|
regionOrCountry: currency.toUpperCase(),
|
||||||
|
}),
|
||||||
|
cell: (context) => {
|
||||||
|
return (
|
||||||
|
<DataGridCurrencyCell
|
||||||
|
code={currency}
|
||||||
|
context={context}
|
||||||
|
field={`currency_prices.${currency}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
...regions.map((region) => {
|
||||||
|
return columnHelper.column({
|
||||||
|
id: `region_prices.${region.id}`,
|
||||||
|
name: t("fields.priceTemplate", {
|
||||||
|
regionOrCountry: region.name,
|
||||||
|
}),
|
||||||
|
header: t("fields.priceTemplate", {
|
||||||
|
regionOrCountry: region.name,
|
||||||
|
}),
|
||||||
|
cell: (context) => {
|
||||||
|
return (
|
||||||
|
<DataGridCurrencyCell
|
||||||
|
code={region.currency_code}
|
||||||
|
context={context}
|
||||||
|
field={`region_prices.${region.id}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
] as ColumnDef<(string | HttpTypes.AdminRegion)[]>[]
|
||||||
|
}, [t, currencies, regions])
|
||||||
|
}
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export { LocationAddSalesChannels as Component } from "./location-add-sales-channels"
|
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
import { useParams } from "react-router-dom"
|
|
||||||
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
|
||||||
import { LocationEditSalesChannelsForm } from "./components/edit-sales-channels-form"
|
|
||||||
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
|
||||||
|
|
||||||
export const LocationAddSalesChannels = () => {
|
|
||||||
const { location_id } = useParams()
|
|
||||||
const {
|
|
||||||
stock_location = {},
|
|
||||||
isPending: isLocationLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useStockLocation(location_id!, {
|
|
||||||
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.shipping_profile",
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteFocusModal>
|
|
||||||
{!isLocationLoading && stock_location && (
|
|
||||||
<LocationEditSalesChannelsForm location={stock_location} />
|
|
||||||
)}
|
|
||||||
</RouteFocusModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+24
-18
@@ -21,7 +21,7 @@ const CreateLocationSchema = zod.object({
|
|||||||
postal_code: zod.string().optional(),
|
postal_code: zod.string().optional(),
|
||||||
province: zod.string().optional(),
|
province: zod.string().optional(),
|
||||||
company: zod.string().optional(),
|
company: zod.string().optional(),
|
||||||
phone: zod.string().optional(), // TODO: Add validation
|
phone: zod.string().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -49,24 +49,30 @@ export const CreateLocationForm = () => {
|
|||||||
const { mutateAsync, isPending } = useCreateStockLocation()
|
const { mutateAsync, isPending } = useCreateStockLocation()
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
try {
|
await mutateAsync(
|
||||||
await mutateAsync({
|
{
|
||||||
name: values.name,
|
name: values.name,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ stock_location }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("locations.toast.create"),
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
|
||||||
handleSuccess("/settings/locations")
|
handleSuccess(`/settings/locations/${stock_location.id}`)
|
||||||
|
},
|
||||||
toast.success(t("general.success"), {
|
onError: (e) => {
|
||||||
description: t("locations.toast.create"),
|
toast.error(t("general.error"), {
|
||||||
dismissLabel: t("actions.close"),
|
description: e.message,
|
||||||
})
|
dismissable: true,
|
||||||
} catch (e) {
|
dismissLabel: t("actions.close"),
|
||||||
toast.error(t("general.error"), {
|
})
|
||||||
description: e.message,
|
},
|
||||||
dismissLabel: t("actions.close"),
|
}
|
||||||
})
|
)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,10 +98,10 @@ export const CreateLocationForm = () => {
|
|||||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||||
<div>
|
<div>
|
||||||
<Heading className="capitalize">
|
<Heading className="capitalize">
|
||||||
{t("location.createLocation")}
|
{t("stockLocations.create.header")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
{t("location.createLocationDetailsHint")}
|
{t("stockLocations.create.hint")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
File renamed without changes.
+268
-237
@@ -1,22 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
ChevronDownMini,
|
ArchiveBox,
|
||||||
CurrencyDollar,
|
CurrencyDollar,
|
||||||
Map,
|
Map,
|
||||||
PencilSquare,
|
PencilSquare,
|
||||||
Plus,
|
Plus,
|
||||||
Trash,
|
Trash,
|
||||||
|
TriangleDownMini,
|
||||||
} from "@medusajs/icons"
|
} from "@medusajs/icons"
|
||||||
import {
|
import { HttpTypes } from "@medusajs/types"
|
||||||
FulfillmentSetDTO,
|
|
||||||
ServiceZoneDTO,
|
|
||||||
ShippingOptionDTO,
|
|
||||||
StockLocationDTO,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
|
||||||
Container,
|
Container,
|
||||||
Heading,
|
Heading,
|
||||||
|
IconButton,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
Text,
|
Text,
|
||||||
toast,
|
toast,
|
||||||
@@ -27,25 +23,34 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
import { IconAvatar } from "../../../../../components/common/icon-avatar"
|
||||||
|
import { LinkButton } from "../../../../../components/common/link-button"
|
||||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||||
|
import {
|
||||||
|
useDeleteFulfillmentServiceZone,
|
||||||
|
useDeleteFulfillmentSet,
|
||||||
|
} from "../../../../../hooks/api/fulfillment-sets"
|
||||||
import { useDeleteShippingOption } from "../../../../../hooks/api/shipping-options"
|
import { useDeleteShippingOption } from "../../../../../hooks/api/shipping-options"
|
||||||
import {
|
import {
|
||||||
useCreateFulfillmentSet,
|
useCreateStockLocationFulfillmentSet,
|
||||||
useDeleteFulfillmentSet,
|
|
||||||
useDeleteServiceZone,
|
|
||||||
useDeleteStockLocation,
|
useDeleteStockLocation,
|
||||||
} from "../../../../../hooks/api/stock-locations"
|
} from "../../../../../hooks/api/stock-locations"
|
||||||
import { countries as staticCountries } from "../../../../../lib/countries"
|
import { getFormattedAddress } from "../../../../../lib/addresses"
|
||||||
|
import {
|
||||||
|
StaticCountry,
|
||||||
|
countries as staticCountries,
|
||||||
|
} from "../../../../../lib/countries"
|
||||||
import { formatProvider } from "../../../../../lib/format-provider"
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
import {
|
import {
|
||||||
isOptionEnabledInStore,
|
isOptionEnabledInStore,
|
||||||
isReturnOption,
|
isReturnOption,
|
||||||
} from "../../../../../lib/shipping-options"
|
} from "../../../../../lib/shipping-options"
|
||||||
import { getFormattedAddress } from "../../../../../lib/addresses"
|
import { FulfillmentSetType } from "../../../common/constants"
|
||||||
|
|
||||||
type LocationGeneralSectionProps = {
|
type LocationGeneralSectionProps = {
|
||||||
location: StockLocationDTO
|
location: HttpTypes.AdminStockLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocationGeneralSection = ({
|
export const LocationGeneralSection = ({
|
||||||
@@ -69,7 +74,7 @@ export const LocationGeneralSection = ({
|
|||||||
locationId={location.id}
|
locationId={location.id}
|
||||||
locationName={location.name}
|
locationName={location.name}
|
||||||
type={FulfillmentSetType.Pickup}
|
type={FulfillmentSetType.Pickup}
|
||||||
fulfillmentSet={location.fulfillment_sets.find(
|
fulfillmentSet={location.fulfillment_sets?.find(
|
||||||
(f) => f.type === FulfillmentSetType.Pickup
|
(f) => f.type === FulfillmentSetType.Pickup
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -77,9 +82,9 @@ export const LocationGeneralSection = ({
|
|||||||
<FulfillmentSet
|
<FulfillmentSet
|
||||||
locationId={location.id}
|
locationId={location.id}
|
||||||
locationName={location.name}
|
locationName={location.name}
|
||||||
type={FulfillmentSetType.Delivery}
|
type={FulfillmentSetType.Shipping}
|
||||||
fulfillmentSet={location.fulfillment_sets.find(
|
fulfillmentSet={location.fulfillment_sets?.find(
|
||||||
(f) => f.type === FulfillmentSetType.Delivery
|
(f) => f.type === FulfillmentSetType.Shipping
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -87,31 +92,31 @@ export const LocationGeneralSection = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ShippingOptionProps = {
|
type ShippingOptionProps = {
|
||||||
option: ShippingOptionDTO
|
option: HttpTypes.AdminShippingOption
|
||||||
fulfillmentSetId: string
|
fulfillmentSetId: string
|
||||||
locationId: string
|
locationId: string
|
||||||
isReturn?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShippingOption({
|
function ShippingOption({
|
||||||
option,
|
option,
|
||||||
isReturn,
|
|
||||||
fulfillmentSetId,
|
fulfillmentSetId,
|
||||||
locationId,
|
locationId,
|
||||||
}: ShippingOptionProps) {
|
}: ShippingOptionProps) {
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const isInStore = isOptionEnabledInStore(option)
|
const isStoreOption = isOptionEnabledInStore(option)
|
||||||
|
|
||||||
const { mutateAsync: deleteOption } = useDeleteShippingOption(option.id)
|
const { mutateAsync } = useDeleteShippingOption(option.id)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("location.shippingOptions.deleteWarning", {
|
description: t("stockLocations.shippingOptions.delete.confirmation", {
|
||||||
name: option.name,
|
name: option.name,
|
||||||
}),
|
}),
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
verificationText: option.name,
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.delete"),
|
||||||
cancelText: t("actions.cancel"),
|
cancelText: t("actions.cancel"),
|
||||||
})
|
})
|
||||||
@@ -120,50 +125,60 @@ function ShippingOption({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await mutateAsync(undefined, {
|
||||||
await deleteOption()
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
toast.success(t("general.success"), {
|
description: t("stockLocations.shippingOptions.delete.successToast", {
|
||||||
description: t("location.shippingOptions.toast.delete", {
|
name: option.name,
|
||||||
name: option.name,
|
}),
|
||||||
}),
|
dismissable: true,
|
||||||
dismissLabel: t("actions.close"),
|
dismissLabel: t("actions.close"),
|
||||||
})
|
})
|
||||||
} catch (e) {
|
},
|
||||||
toast.error(t("general.error"), {
|
onError: (e) => {
|
||||||
description: e.message,
|
toast.error(t("general.error"), {
|
||||||
dismissLabel: t("actions.close"),
|
description: e.message,
|
||||||
})
|
dismissable: true,
|
||||||
}
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<span className="txt-small font-medium">
|
<Text size="small" weight="plus">
|
||||||
{option.name} - {option.shipping_profile.name} (
|
{option.name} - {option.shipping_profile.name} (
|
||||||
{formatProvider(option.provider_id)})
|
{formatProvider(option.provider_id)})
|
||||||
</span>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{isInStore && (
|
<Badge
|
||||||
<Badge className="mr-4" color="purple">
|
className="mr-4"
|
||||||
{t("location.shippingOptions.inStore")}
|
color={isStoreOption ? "grey" : "purple"}
|
||||||
</Badge>
|
size="2xsmall"
|
||||||
)}
|
rounded="full"
|
||||||
|
>
|
||||||
|
{isStoreOption ? t("general.store") : t("general.admin")}
|
||||||
|
</Badge>
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
groups={[
|
groups={[
|
||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
label: t("location.serviceZone.editOption"),
|
label: t("stockLocations.shippingOptions.edit.action"),
|
||||||
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/edit`,
|
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/edit`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("location.serviceZone.editPrices"),
|
label: t("stockLocations.shippingOptions.pricing.action"),
|
||||||
icon: <CurrencyDollar />,
|
icon: <CurrencyDollar />,
|
||||||
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/edit-pricing`,
|
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/pricing`,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
{
|
{
|
||||||
label: t("actions.delete"),
|
label: t("actions.delete"),
|
||||||
icon: <Trash />,
|
icon: <Trash />,
|
||||||
@@ -178,7 +193,7 @@ function ShippingOption({
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServiceZoneOptionsProps = {
|
type ServiceZoneOptionsProps = {
|
||||||
zone: ServiceZoneDTO
|
zone: HttpTypes.AdminServiceZone
|
||||||
locationId: string
|
locationId: string
|
||||||
fulfillmentSetId: string
|
fulfillmentSetId: string
|
||||||
}
|
}
|
||||||
@@ -189,7 +204,6 @@ function ServiceZoneOptions({
|
|||||||
fulfillmentSetId,
|
fulfillmentSetId,
|
||||||
}: ServiceZoneOptionsProps) {
|
}: ServiceZoneOptionsProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const shippingOptions = zone.shipping_options.filter(
|
const shippingOptions = zone.shipping_options.filter(
|
||||||
(o) => !isReturnOption(o)
|
(o) => !isReturnOption(o)
|
||||||
@@ -198,27 +212,22 @@ function ServiceZoneOptions({
|
|||||||
const returnOptions = zone.shipping_options.filter((o) => isReturnOption(o))
|
const returnOptions = zone.shipping_options.filter((o) => isReturnOption(o))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<div className="mt-4 flex flex-col border-t border-dashed px-6 py-4">
|
<Divider variant="dashed" />
|
||||||
|
<div className="flex flex-col gap-y-4 px-6 py-4">
|
||||||
<div className="item-center flex justify-between">
|
<div className="item-center flex justify-between">
|
||||||
<span className="text-ui-fg-subtle txt-small self-center font-medium">
|
<span className="text-ui-fg-subtle txt-small self-center font-medium">
|
||||||
{t("location.serviceZone.shippingOptions")}
|
{t("stockLocations.shippingOptions.create.shipping.label")}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<LinkButton
|
||||||
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent active:bg-transparent"
|
to={`/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create`}
|
||||||
variant="transparent"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t("location.serviceZone.addOption")}
|
{t("stockLocations.shippingOptions.create.action")}
|
||||||
</Button>
|
</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!shippingOptions.length && (
|
{!!shippingOptions.length && (
|
||||||
<div className="shadow-elevation-card-rest bg-ui-bg-subtle mt-4 grid divide-y rounded-md">
|
<div className="shadow-elevation-card-rest bg-ui-bg-subtle grid divide-y rounded-md">
|
||||||
{shippingOptions.map((o) => (
|
{shippingOptions.map((o) => (
|
||||||
<ShippingOption
|
<ShippingOption
|
||||||
key={o.id}
|
key={o.id}
|
||||||
@@ -231,30 +240,25 @@ function ServiceZoneOptions({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="-mb-4 flex flex-col border-t border-dashed px-6 py-4">
|
<Divider variant="dashed" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-4 px-6 py-4">
|
||||||
<div className="item-center flex justify-between">
|
<div className="item-center flex justify-between">
|
||||||
<span className="text-ui-fg-subtle txt-small self-center font-medium">
|
<span className="text-ui-fg-subtle txt-small self-center font-medium">
|
||||||
{t("location.serviceZone.returnOptions")}
|
{t("stockLocations.shippingOptions.create.returns.label")}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<LinkButton
|
||||||
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent active:bg-transparent"
|
to={`/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create?is_return`}
|
||||||
variant="transparent"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create?is_return`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t("location.serviceZone.addOption")}
|
{t("stockLocations.shippingOptions.create.action")}
|
||||||
</Button>
|
</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!returnOptions.length && (
|
{!!returnOptions.length && (
|
||||||
<div className="shadow-elevation-card-rest bg-ui-bg-subtle mt-4 grid divide-y rounded-md">
|
<div className="shadow-elevation-card-rest bg-ui-bg-subtle grid divide-y rounded-md">
|
||||||
{returnOptions.map((o) => (
|
{returnOptions.map((o) => (
|
||||||
<ShippingOption
|
<ShippingOption
|
||||||
key={o.id}
|
key={o.id}
|
||||||
isReturn
|
|
||||||
option={o}
|
option={o}
|
||||||
locationId={locationId}
|
locationId={locationId}
|
||||||
fulfillmentSetId={fulfillmentSetId}
|
fulfillmentSetId={fulfillmentSetId}
|
||||||
@@ -263,12 +267,12 @@ function ServiceZoneOptions({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceZoneProps = {
|
type ServiceZoneProps = {
|
||||||
zone: ServiceZoneDTO
|
zone: HttpTypes.AdminServiceZone
|
||||||
locationId: string
|
locationId: string
|
||||||
fulfillmentSetId: string
|
fulfillmentSetId: string
|
||||||
}
|
}
|
||||||
@@ -278,7 +282,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
|
|||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const { mutateAsync: deleteZone } = useDeleteServiceZone(
|
const { mutateAsync: deleteZone } = useDeleteFulfillmentServiceZone(
|
||||||
fulfillmentSetId,
|
fulfillmentSetId,
|
||||||
zone.id
|
zone.id
|
||||||
)
|
)
|
||||||
@@ -286,7 +290,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("location.serviceZone.deleteWarning", {
|
description: t("stockLocations.serviceZones.delete.confirmation", {
|
||||||
name: zone.name,
|
name: zone.name,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.delete"),
|
||||||
@@ -297,97 +301,104 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await deleteZone(undefined, {
|
||||||
await deleteZone()
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
toast.success(t("general.success"), {
|
description: e.message,
|
||||||
description: t("location.serviceZone.toast.delete", {
|
dismissLabel: t("actions.close"),
|
||||||
name: zone.name,
|
})
|
||||||
}),
|
},
|
||||||
dismissLabel: t("actions.close"),
|
onSuccess: () => {
|
||||||
})
|
toast.success(t("general.success"), {
|
||||||
} catch (e) {
|
description: t("stockLocations.serviceZones.delete.successToast", {
|
||||||
toast.error(t("general.error"), {
|
name: zone.name,
|
||||||
description: e.message,
|
}),
|
||||||
dismissLabel: t("actions.close"),
|
dismissLabel: t("actions.close"),
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const countries = useMemo(() => {
|
const countries = useMemo(() => {
|
||||||
return zone.geo_zones
|
const countryGeoZones = zone.geo_zones.filter((g) => g.type === "country")
|
||||||
.filter((g) => g.type === "country")
|
|
||||||
.map((g) => g.country_code)
|
const countries = countryGeoZones
|
||||||
.map((code) => staticCountries.find((c) => c.iso_2 === code))
|
.map(({ country_code }) =>
|
||||||
.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
staticCountries.find((c) => c.iso_2 === country_code)
|
||||||
}, zone.geo_zones)
|
)
|
||||||
|
.filter((c) => !!c) as StaticCountry[]
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === "development" &&
|
||||||
|
countryGeoZones.length !== countries.length
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
"Some countries are missing in the static countries list",
|
||||||
|
countryGeoZones
|
||||||
|
.filter((g) => !countries.find((c) => c.iso_2 === g.country_code))
|
||||||
|
.map((g) => g.country_code)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return countries.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
||||||
|
}, [zone.geo_zones])
|
||||||
|
|
||||||
const [shippingOptionsCount, returnOptionsCount] = useMemo(() => {
|
const [shippingOptionsCount, returnOptionsCount] = useMemo(() => {
|
||||||
const optionsCount = zone.shipping_options.filter(
|
const options = zone.shipping_options
|
||||||
(o) => !isReturnOption(o)
|
|
||||||
).length
|
|
||||||
|
|
||||||
const returnOptionsCount = zone.shipping_options.filter((o) =>
|
const optionsCount = options.filter((o) => !isReturnOption(o)).length
|
||||||
isReturnOption(o)
|
|
||||||
).length
|
const returnOptionsCount = options.filter(isReturnOption).length
|
||||||
|
|
||||||
return [optionsCount, returnOptionsCount]
|
return [optionsCount, returnOptionsCount]
|
||||||
}, [zone.shipping_options])
|
}, [zone.shipping_options])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-4">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row items-center justify-between gap-x-4 px-6">
|
<div className="flex flex-row items-center justify-between gap-x-4 px-6 py-4">
|
||||||
{/* ICON*/}
|
<IconAvatar>
|
||||||
<div className="grow-0 rounded-lg border">
|
<Map />
|
||||||
<div className="bg-ui-bg-field m-1 rounded-md p-2">
|
</IconAvatar>
|
||||||
<Map className="text-ui-fg-subtle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* INFO*/}
|
|
||||||
<div className="grow-1 flex flex-1 flex-col">
|
<div className="grow-1 flex flex-1 flex-col">
|
||||||
<Text weight="plus">{zone.name}</Text>
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{zone.name}
|
||||||
|
</Text>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ListSummary
|
<ListSummary
|
||||||
|
variant="base"
|
||||||
list={countries.map((c) => c.display_name)}
|
list={countries.map((c) => c.display_name)}
|
||||||
inline
|
inline
|
||||||
n={1}
|
n={1}
|
||||||
/>
|
/>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
{shippingOptionsCount}{" "}
|
{t("stockLocations.shippingOptions.fields.count.shipping", {
|
||||||
{t("location.serviceZone.optionsLength", {
|
|
||||||
count: shippingOptionsCount,
|
count: shippingOptionsCount,
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
{returnOptionsCount}{" "}
|
{t("stockLocations.shippingOptions.fields.count.returns", {
|
||||||
{t("location.serviceZone.returnOptionsLength", {
|
|
||||||
count: returnOptionsCount,
|
count: returnOptionsCount,
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ACTION*/}
|
<div className="flex grow-0 items-center gap-4">
|
||||||
<div className="itemx-center flex grow-0 gap-1">
|
<IconButton
|
||||||
<Button
|
size="small"
|
||||||
onClick={() => setOpen((s) => !s)}
|
onClick={() => setOpen((s) => !s)}
|
||||||
className="flex items-center justify-center"
|
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
style={{
|
|
||||||
transform: `translateY(${!open ? -4 : -2}px)`,
|
|
||||||
transition: ".1s transform ease-in-out",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ChevronDownMini
|
<TriangleDownMini
|
||||||
style={{
|
style={{
|
||||||
transform: `rotate(${!open ? 0 : 180}deg)`,
|
transform: `rotate(${!open ? 0 : 180}deg)`,
|
||||||
transition: ".2s transform ease-in-out",
|
transition: ".2s transform ease-in-out",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</IconButton>
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
groups={[
|
groups={[
|
||||||
{
|
{
|
||||||
@@ -398,10 +409,14 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
|
|||||||
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit`,
|
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("location.serviceZone.areas.manage"),
|
label: t("stockLocations.serviceZones.manageAreas.action"),
|
||||||
icon: <Map />,
|
icon: <Map />,
|
||||||
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit-areas`,
|
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit-areas`,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
{
|
{
|
||||||
label: t("actions.delete"),
|
label: t("actions.delete"),
|
||||||
icon: <Trash />,
|
icon: <Trash />,
|
||||||
@@ -414,25 +429,18 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
<div>
|
<ServiceZoneOptions
|
||||||
<ServiceZoneOptions
|
fulfillmentSetId={fulfillmentSetId}
|
||||||
fulfillmentSetId={fulfillmentSetId}
|
locationId={locationId}
|
||||||
locationId={locationId}
|
zone={zone}
|
||||||
zone={zone}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FulfillmentSetType {
|
|
||||||
Delivery = "delivery",
|
|
||||||
Pickup = "pickup",
|
|
||||||
}
|
|
||||||
|
|
||||||
type FulfillmentSetProps = {
|
type FulfillmentSetProps = {
|
||||||
fulfillmentSet?: FulfillmentSetDTO
|
fulfillmentSet?: HttpTypes.AdminFulfillmentSet
|
||||||
locationName: string
|
locationName: string
|
||||||
locationId: string
|
locationId: string
|
||||||
type: FulfillmentSetType
|
type: FulfillmentSetType
|
||||||
@@ -441,7 +449,6 @@ type FulfillmentSetProps = {
|
|||||||
function FulfillmentSet(props: FulfillmentSetProps) {
|
function FulfillmentSet(props: FulfillmentSetProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const { fulfillmentSet, locationName, locationId, type } = props
|
const { fulfillmentSet, locationName, locationId, type } = props
|
||||||
|
|
||||||
@@ -449,36 +456,47 @@ function FulfillmentSet(props: FulfillmentSetProps) {
|
|||||||
|
|
||||||
const hasServiceZones = !!fulfillmentSet?.service_zones.length
|
const hasServiceZones = !!fulfillmentSet?.service_zones.length
|
||||||
|
|
||||||
const { mutateAsync: createFulfillmentSet, isPending: isLoading } =
|
const { mutateAsync: createFulfillmentSet } =
|
||||||
useCreateFulfillmentSet(locationId)
|
useCreateStockLocationFulfillmentSet(locationId)
|
||||||
|
|
||||||
const { mutateAsync: deleteFulfillmentSet } = useDeleteFulfillmentSet(
|
const { mutateAsync: deleteFulfillmentSet } = useDeleteFulfillmentSet(
|
||||||
fulfillmentSet?.id
|
fulfillmentSet?.id!
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
await createFulfillmentSet(
|
||||||
await createFulfillmentSet({
|
{
|
||||||
name: `${locationName} ${
|
name: `${locationName} ${
|
||||||
type === FulfillmentSetType.Pickup ? "pick up" : type
|
type === FulfillmentSetType.Pickup ? "pick up" : type
|
||||||
}`,
|
}`,
|
||||||
type,
|
type,
|
||||||
})
|
},
|
||||||
} catch (e) {
|
{
|
||||||
toast.error(t("general.error"), {
|
onSuccess: () => {
|
||||||
description: e.message,
|
toast.success(t("general.success"), {
|
||||||
dismissLabel: t("actions.close"),
|
description: t(`stockLocations.fulfillmentSets.enable.${type}`),
|
||||||
})
|
dismissLabel: t("actions.close"),
|
||||||
}
|
dismissable: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
dismissable: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("location.fulfillmentSet.disableWarning", {
|
description: t(`stockLocations.fulfillmentSets.disable.confirmation`, {
|
||||||
name: fulfillmentSet?.name,
|
name: fulfillmentSet?.name,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.disable"),
|
||||||
cancelText: t("actions.cancel"),
|
cancelText: t("actions.cancel"),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -486,83 +504,85 @@ function FulfillmentSet(props: FulfillmentSetProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await deleteFulfillmentSet(undefined, {
|
||||||
await deleteFulfillmentSet()
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
toast.success(t("general.success"), {
|
description: t(`stockLocations.fulfillmentSets.disable.${type}`),
|
||||||
description: t("location.fulfillmentSet.toast.disable", {
|
dismissable: true,
|
||||||
name: fulfillmentSet?.name,
|
dismissLabel: t("actions.close"),
|
||||||
}),
|
})
|
||||||
dismissLabel: t("actions.close"),
|
},
|
||||||
})
|
onError: (e) => {
|
||||||
} catch (e) {
|
toast.error(t("general.error"), {
|
||||||
toast.error(t("general.error"), {
|
description: e.message,
|
||||||
description: e.message,
|
dismissable: true,
|
||||||
dismissLabel: t("actions.close"),
|
dismissLabel: t("actions.close"),
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const groups = fulfillmentSet
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Plus />,
|
||||||
|
label: t("stockLocations.serviceZones.create.action"),
|
||||||
|
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.disable"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Plus />,
|
||||||
|
label: t("actions.enable"),
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="p-0">
|
<Container className="p-0">
|
||||||
<div className="flex flex-col divide-y">
|
<div className="flex flex-col divide-y">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Text size="large" weight="plus" className="flex-1" as="div">
|
<Heading level="h2">
|
||||||
{t(`location.fulfillmentSet.${type}.offers`)}
|
{t(`stockLocations.fulfillmentSets.${type}.header`)}
|
||||||
</Text>
|
</Heading>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<StatusBadge color={fulfillmentSetExists ? "green" : "red"}>
|
<StatusBadge color={fulfillmentSetExists ? "green" : "grey"}>
|
||||||
{t(
|
{t(
|
||||||
fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled"
|
fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled"
|
||||||
)}
|
)}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
|
|
||||||
<ActionMenu
|
<ActionMenu groups={groups} />
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: <Plus />,
|
|
||||||
label: t("location.fulfillmentSet.addZone"),
|
|
||||||
onClick: () =>
|
|
||||||
navigate(
|
|
||||||
`/settings/locations/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`
|
|
||||||
),
|
|
||||||
disabled: !fulfillmentSetExists,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
label: fulfillmentSetExists
|
|
||||||
? t("actions.disable")
|
|
||||||
: t("actions.enable"),
|
|
||||||
onClick: fulfillmentSetExists
|
|
||||||
? handleDelete
|
|
||||||
: handleCreate,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fulfillmentSetExists && !hasServiceZones && (
|
{fulfillmentSetExists && !hasServiceZones && (
|
||||||
<div className="text-ui-fg-muted txt-medium flex flex-col items-center justify-center gap-y-4 py-8">
|
<div className="flex items-center justify-center py-8 pt-6">
|
||||||
<NoRecords
|
<NoRecords
|
||||||
message={t("location.fulfillmentSet.placeholder")}
|
message={t("stockLocations.serviceZones.fields.noRecords")}
|
||||||
className="h-fit"
|
className="h-fit"
|
||||||
|
action={{
|
||||||
|
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`,
|
||||||
|
label: t("stockLocations.serviceZones.create.action"),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/settings/locations/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("location.fulfillmentSet.addZone")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -583,7 +603,7 @@ function FulfillmentSet(props: FulfillmentSetProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Actions = ({ location }: { location: StockLocationDTO }) => {
|
const Actions = ({ location }: { location: HttpTypes.AdminStockLocation }) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mutateAsync } = useDeleteStockLocation(location.id)
|
const { mutateAsync } = useDeleteStockLocation(location.id)
|
||||||
@@ -592,7 +612,7 @@ const Actions = ({ location }: { location: StockLocationDTO }) => {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("location.deleteLocationWarning", {
|
description: t("stockLocations.delete.confirmation", {
|
||||||
name: location.name,
|
name: location.name,
|
||||||
}),
|
}),
|
||||||
verificationText: location.name,
|
verificationText: location.name,
|
||||||
@@ -605,19 +625,25 @@ const Actions = ({ location }: { location: StockLocationDTO }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await mutateAsync(undefined, {
|
||||||
await mutateAsync(undefined)
|
onSuccess: () => {
|
||||||
toast.success(t("general.success"), {
|
toast.success(t("general.success"), {
|
||||||
description: t("location.toast.delete"),
|
description: t("stockLocations.create.successToast", {
|
||||||
dismissLabel: t("actions.close"),
|
name: location.name,
|
||||||
})
|
}),
|
||||||
} catch (e) {
|
dismissable: true,
|
||||||
toast.error(t("general.error"), {
|
dismissLabel: t("actions.close"),
|
||||||
description: e.message,
|
})
|
||||||
dismissLabel: t("actions.close"),
|
navigate("/settings/locations", { replace: true })
|
||||||
})
|
},
|
||||||
}
|
onError: (e) => {
|
||||||
navigate("/settings/locations", { replace: true })
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -630,6 +656,11 @@ const Actions = ({ location }: { location: StockLocationDTO }) => {
|
|||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
to: `edit`,
|
to: `edit`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <ArchiveBox />,
|
||||||
|
label: t("stockLocations.edit.viewInventory"),
|
||||||
|
to: `/inventory?location_id=${location.id}`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
File renamed without changes.
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import { Channels, PencilSquare } from "@medusajs/icons"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
import { IconAvatar } from "../../../../../components/common/icon-avatar"
|
||||||
|
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||||
|
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||||
|
|
||||||
|
type LocationsSalesChannelsSectionProps = {
|
||||||
|
location: HttpTypes.AdminStockLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
function LocationsSalesChannelsSection({
|
||||||
|
location,
|
||||||
|
}: LocationsSalesChannelsSectionProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { count } = useSalesChannels({ limit: 1, fields: "id" })
|
||||||
|
|
||||||
|
const hasConnectedChannels = !!location.sales_channels?.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex flex-col px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Heading level="h2">{t("stockLocations.salesChannels.header")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: "sales-channels",
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{hasConnectedChannels ? (
|
||||||
|
<div className="flex flex-col gap-y-4 pt-4">
|
||||||
|
<div className="grid grid-cols-[28px_1fr] items-center gap-x-3">
|
||||||
|
<IconAvatar>
|
||||||
|
<Channels className="text-ui-fg-subtle" />
|
||||||
|
</IconAvatar>
|
||||||
|
<ListSummary
|
||||||
|
n={3}
|
||||||
|
className="text-ui-fg-base"
|
||||||
|
inline
|
||||||
|
list={location.sales_channels?.map((sc) => sc.name) ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
||||||
|
{t("stockLocations.salesChannels.connectedTo", {
|
||||||
|
count: location.sales_channels?.length,
|
||||||
|
total: count,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<NoRecords
|
||||||
|
className="h-fit pb-2 pt-6"
|
||||||
|
action={{
|
||||||
|
label: t("stockLocations.salesChannels.action"),
|
||||||
|
to: "sales-channels",
|
||||||
|
}}
|
||||||
|
message={t("stockLocations.salesChannels.noChannels")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LocationsSalesChannelsSection
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
export const detailsFields =
|
export const detailsFields =
|
||||||
"name,*sales_channels,*address,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,*fulfillment_sets.service_zones.shipping_options.prices"
|
"name,*sales_channels,*address,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"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { locationLoader as loader } from "./loader"
|
||||||
|
export { LocationDetail as Component } from "./location-detail"
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { LoaderFunctionArgs, redirect } from "react-router-dom"
|
||||||
|
|
||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
|
import { sdk } from "../../../lib/client"
|
||||||
|
import { queryClient } from "../../../lib/query-client"
|
||||||
|
import { detailsFields } from "./const"
|
||||||
|
|
||||||
|
const locationQuery = (id: string) => ({
|
||||||
|
queryKey: stockLocationsQueryKeys.detail(id),
|
||||||
|
queryFn: async () => {
|
||||||
|
return await sdk.admin.stockLocation
|
||||||
|
.retrieve(id, {
|
||||||
|
fields: detailsFields,
|
||||||
|
})
|
||||||
|
.catch((error: FetchError) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
throw redirect("/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const locationLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.location_id
|
||||||
|
const query = locationQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<{ stock_location: HttpTypes.AdminStockLocation }>(
|
||||||
|
query.queryKey
|
||||||
|
) ?? (await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+3
-3
@@ -12,7 +12,7 @@ import sideAfter from "virtual:medusa/widgets/location/details/side/after"
|
|||||||
import sideBefore from "virtual:medusa/widgets/location/details/side/before"
|
import sideBefore from "virtual:medusa/widgets/location/details/side/before"
|
||||||
import { detailsFields } from "./const"
|
import { detailsFields } from "./const"
|
||||||
|
|
||||||
export const LocationDetails = () => {
|
export const LocationDetail = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
ReturnType<typeof locationLoader>
|
ReturnType<typeof locationLoader>
|
||||||
>
|
>
|
||||||
@@ -51,7 +51,7 @@ export const LocationDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
|
<div className="flex flex-col gap-y-3 xl:flex-row xl:items-start xl:gap-x-4">
|
||||||
<div className="flex w-full flex-col gap-y-3">
|
<div className="flex w-full flex-col gap-y-3">
|
||||||
<LocationGeneralSection location={location} />
|
<LocationGeneralSection location={location} />
|
||||||
{after.widgets.map((w, i) => {
|
{after.widgets.map((w, i) => {
|
||||||
@@ -65,7 +65,7 @@ export const LocationDetails = () => {
|
|||||||
<JsonViewSection data={location} />
|
<JsonViewSection data={location} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
|
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
|
||||||
{sideBefore.widgets.map((w, i) => {
|
{sideBefore.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
-79
@@ -1,79 +0,0 @@
|
|||||||
import { Channels, PencilSquare } from "@medusajs/icons"
|
|
||||||
import { StockLocationDTO } from "@medusajs/types"
|
|
||||||
import { Heading, Text } from "@medusajs/ui"
|
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
|
||||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
location: StockLocationDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
function LocationsSalesChannelsSection({ location }: Props) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { count } = useSalesChannels()
|
|
||||||
|
|
||||||
const noChannels = !location.sales_channels?.length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="shadow-elevation-card-rest bg-ui-bg-base rounded-md p-4">
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<Heading level="h2">{t("location.salesChannels.title")}</Heading>
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: "sales-channels/edit",
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-[28px_1fr] items-center gap-x-3">
|
|
||||||
<div className="bg-ui-bg-base shadow-borders-base flex size-7 items-center justify-center rounded-md">
|
|
||||||
<div className="bg-ui-bg-component flex size-6 items-center justify-center rounded-[4px]">
|
|
||||||
<Channels className="text-ui-fg-subtle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{noChannels ? (
|
|
||||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
|
||||||
{t("location.salesChannels.placeholder")}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<ListSummary
|
|
||||||
n={3}
|
|
||||||
inline
|
|
||||||
list={location.sales_channels.map((sc) => sc.name)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Text className="text-ui-fg-subtle mt-4" size="small" leading="compact">
|
|
||||||
<Trans
|
|
||||||
i18nKey="sales_channels.availableIn"
|
|
||||||
values={{
|
|
||||||
x: location.sales_channels.length,
|
|
||||||
y: count,
|
|
||||||
}}
|
|
||||||
components={[
|
|
||||||
<span
|
|
||||||
key="x"
|
|
||||||
className="text-ui-fg-base txt-compact-medium-plus"
|
|
||||||
/>,
|
|
||||||
<span
|
|
||||||
key="y"
|
|
||||||
className="text-ui-fg-base txt-compact-medium-plus"
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LocationsSalesChannelsSection
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { locationLoader as loader } from "./loader"
|
|
||||||
export { LocationDetails as Component } from "./location-details"
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { AdminStockLocationResponse } from "@medusajs/types"
|
|
||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
|
||||||
|
|
||||||
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
|
||||||
import { client } from "../../../lib/client"
|
|
||||||
import { queryClient } from "../../../lib/query-client"
|
|
||||||
import { detailsFields } from "./const"
|
|
||||||
|
|
||||||
const locationQuery = (id: string) => ({
|
|
||||||
queryKey: stockLocationsQueryKeys.detail(id),
|
|
||||||
queryFn: async () =>
|
|
||||||
client.stockLocations.retrieve(id, {
|
|
||||||
fields: detailsFields,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const locationLoader = async ({ params }: LoaderFunctionArgs) => {
|
|
||||||
const id = params.location_id
|
|
||||||
const query = locationQuery(id!)
|
|
||||||
|
|
||||||
return (
|
|
||||||
queryClient.getQueryData<AdminStockLocationResponse>(query.queryKey) ??
|
|
||||||
(await queryClient.fetchQuery(query))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+25
-18
@@ -1,4 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Button, Input, toast } from "@medusajs/ui"
|
import { Button, Input, toast } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -11,10 +12,9 @@ import {
|
|||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { useUpdateStockLocation } from "../../../../../hooks/api/stock-locations"
|
import { useUpdateStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||||
import { ExtendedStockLocationDTO } from "../../../../../types/api-responses"
|
|
||||||
|
|
||||||
type EditLocationFormProps = {
|
type EditLocationFormProps = {
|
||||||
location: ExtendedStockLocationDTO
|
location: HttpTypes.AdminStockLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditLocationSchema = zod.object({
|
const EditLocationSchema = zod.object({
|
||||||
@@ -55,23 +55,30 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
|
|||||||
const { mutateAsync, isPending } = useUpdateStockLocation(location.id)
|
const { mutateAsync, isPending } = useUpdateStockLocation(location.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
try {
|
const { name, address } = values
|
||||||
await mutateAsync({
|
|
||||||
name: values.name,
|
|
||||||
address: values.address,
|
|
||||||
})
|
|
||||||
handleSuccess()
|
|
||||||
|
|
||||||
toast.success(t("general.success"), {
|
await mutateAsync(
|
||||||
description: t("locations.toast.update"),
|
{
|
||||||
dismissLabel: t("actions.close"),
|
name: name,
|
||||||
})
|
address: address,
|
||||||
} catch (e) {
|
},
|
||||||
toast.error(t("general.error"), {
|
{
|
||||||
description: e.message,
|
onSuccess: () => {
|
||||||
dismissLabel: t("actions.close"),
|
toast.success(t("general.success"), {
|
||||||
})
|
description: t("stockLocations.edit.successToast"),
|
||||||
}
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,18 +6,17 @@ import { useStockLocation } from "../../../hooks/api/stock-locations"
|
|||||||
import { EditLocationForm } from "./components/edit-location-form"
|
import { EditLocationForm } from "./components/edit-location-form"
|
||||||
|
|
||||||
export const LocationEdit = () => {
|
export const LocationEdit = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { location_id } = useParams()
|
const { location_id } = useParams()
|
||||||
|
|
||||||
const {
|
const { stock_location, isPending, isError, error } = useStockLocation(
|
||||||
stock_location,
|
location_id!,
|
||||||
isPending: isLoading,
|
{
|
||||||
isError,
|
fields: "*address",
|
||||||
error,
|
}
|
||||||
} = useStockLocation(location_id, {
|
)
|
||||||
fields: "*address",
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const ready = !isPending && !!stock_location
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
@@ -28,9 +27,7 @@ export const LocationEdit = () => {
|
|||||||
<RouteDrawer.Header>
|
<RouteDrawer.Header>
|
||||||
<Heading className="capitalize">{t("locations.editLocation")}</Heading>
|
<Heading className="capitalize">{t("locations.editLocation")}</Heading>
|
||||||
</RouteDrawer.Header>
|
</RouteDrawer.Header>
|
||||||
{!isLoading && !!stock_location && (
|
{ready && <EditLocationForm location={stock_location} />}
|
||||||
<EditLocationForm location={stock_location} />
|
|
||||||
)}
|
|
||||||
</RouteDrawer>
|
</RouteDrawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./location-list-header"
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
export const LocationListHeader = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex h-fit items-center justify-between gap-x-4 px-6 py-4">
|
||||||
|
<div>
|
||||||
|
<Heading>{t("stockLocations.domain")}</Heading>
|
||||||
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
|
{t("stockLocations.list.description")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" className="shrink-0" variant="secondary" asChild>
|
||||||
|
<Link to="create">{t("actions.create")}</Link>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./location-list-item"
|
||||||
+47
-64
@@ -1,27 +1,17 @@
|
|||||||
import { Buildings, PencilSquare, Trash } from "@medusajs/icons"
|
import { Buildings, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import {
|
import { HttpTypes } from "@medusajs/types"
|
||||||
FulfillmentSetDTO,
|
import { Container, StatusBadge, Text, toast, usePrompt } from "@medusajs/ui"
|
||||||
SalesChannelDTO,
|
|
||||||
StockLocationDTO,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
StatusBadge,
|
|
||||||
Text,
|
|
||||||
toast,
|
|
||||||
usePrompt,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { BadgeListSummary } from "../../../../../components/common/badge-list-summary"
|
import { BadgeListSummary } from "../../../../../components/common/badge-list-summary"
|
||||||
|
import { LinkButton } from "../../../../../components/common/link-button"
|
||||||
import { useDeleteStockLocation } from "../../../../../hooks/api/stock-locations"
|
import { useDeleteStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||||
import { getFormattedAddress } from "../../../../../lib/addresses"
|
import { getFormattedAddress } from "../../../../../lib/addresses"
|
||||||
|
import { FulfillmentSetType } from "../../../common/constants"
|
||||||
|
|
||||||
type SalesChannelsProps = {
|
type SalesChannelsProps = {
|
||||||
salesChannels?: SalesChannelDTO[]
|
salesChannels?: HttpTypes.AdminSalesChannel[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function SalesChannels(props: SalesChannelsProps) {
|
function SalesChannels(props: SalesChannelsProps) {
|
||||||
@@ -29,7 +19,7 @@ function SalesChannels(props: SalesChannelsProps) {
|
|||||||
const { salesChannels } = props
|
const { salesChannels } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col px-6 py-5">
|
<div className="flex flex-col px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Text
|
<Text
|
||||||
size="small"
|
size="small"
|
||||||
@@ -37,11 +27,12 @@ function SalesChannels(props: SalesChannelsProps) {
|
|||||||
className="text-ui-fg-subtle flex-1"
|
className="text-ui-fg-subtle flex-1"
|
||||||
as="div"
|
as="div"
|
||||||
>
|
>
|
||||||
{t(`location.fulfillmentSet.salesChannels`)}
|
{t(`stockLocations.salesChannels.label`)}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left">
|
||||||
{salesChannels?.length ? (
|
{salesChannels?.length ? (
|
||||||
<BadgeListSummary
|
<BadgeListSummary
|
||||||
|
rounded
|
||||||
inline
|
inline
|
||||||
n={3}
|
n={3}
|
||||||
list={salesChannels.map((s) => s.name)}
|
list={salesChannels.map((s) => s.name)}
|
||||||
@@ -55,13 +46,8 @@ function SalesChannels(props: SalesChannelsProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FulfillmentSetType {
|
|
||||||
Delivery = "delivery",
|
|
||||||
Pickup = "pickup",
|
|
||||||
}
|
|
||||||
|
|
||||||
type FulfillmentSetProps = {
|
type FulfillmentSetProps = {
|
||||||
fulfillmentSet?: FulfillmentSetDTO
|
fulfillmentSet?: HttpTypes.AdminFulfillmentSet
|
||||||
type: FulfillmentSetType
|
type: FulfillmentSetType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +58,7 @@ function FulfillmentSet(props: FulfillmentSetProps) {
|
|||||||
const fulfillmentSetExists = !!fulfillmentSet
|
const fulfillmentSetExists = !!fulfillmentSet
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col px-6 py-5">
|
<div className="flex flex-col px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Text
|
<Text
|
||||||
size="small"
|
size="small"
|
||||||
@@ -80,10 +66,10 @@ function FulfillmentSet(props: FulfillmentSetProps) {
|
|||||||
className="text-ui-fg-subtle flex-1"
|
className="text-ui-fg-subtle flex-1"
|
||||||
as="div"
|
as="div"
|
||||||
>
|
>
|
||||||
{t(`location.fulfillmentSet.${type}.title`)}
|
{t(`stockLocations.fulfillmentSets.${type}.header`)}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left">
|
||||||
<StatusBadge color={fulfillmentSetExists ? "green" : "red"}>
|
<StatusBadge color={fulfillmentSetExists ? "green" : "grey"}>
|
||||||
{t(fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled")}
|
{t(fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled")}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,13 +79,12 @@ function FulfillmentSet(props: FulfillmentSetProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LocationProps = {
|
type LocationProps = {
|
||||||
location: StockLocationDTO
|
location: HttpTypes.AdminStockLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
function Location(props: LocationProps) {
|
function LocationListItem(props: LocationProps) {
|
||||||
const { location } = props
|
const { location } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
const { mutateAsync: deleteLocation } = useDeleteStockLocation(location.id)
|
const { mutateAsync: deleteLocation } = useDeleteStockLocation(location.id)
|
||||||
@@ -107,7 +92,7 @@ function Location(props: LocationProps) {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const result = await prompt({
|
const result = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("location.deleteLocation.confirm", {
|
description: t("stockLocations.delete.confirmation", {
|
||||||
name: location.name,
|
name: location.name,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.remove"),
|
confirmText: t("actions.remove"),
|
||||||
@@ -118,35 +103,34 @@ function Location(props: LocationProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await deleteLocation(undefined, {
|
||||||
await deleteLocation()
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
toast.success(t("general.success"), {
|
description: t("shippingProfile.delete.successToast", {
|
||||||
description: t("location.deleteLocation.success", {
|
name: location.name,
|
||||||
name: location.name,
|
}),
|
||||||
}),
|
dismissLabel: t("general.close"),
|
||||||
dismissLabel: t("general.close"),
|
})
|
||||||
})
|
},
|
||||||
} catch (e) {
|
onError: (e) => {
|
||||||
toast.error(t("general.error"), {
|
toast.error(t("general.error"), {
|
||||||
description: e.message,
|
description: e.message,
|
||||||
dismissLabel: t("actions.close"),
|
dismissLabel: t("actions.close"),
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="flex flex-col divide-y p-0">
|
<Container className="flex flex-col divide-y p-0">
|
||||||
<div className="px-6 py-5">
|
<div className="px-6 py-4">
|
||||||
<div className="flex flex-row items-center justify-between gap-x-4">
|
<div className="flex flex-row items-center justify-between gap-x-4">
|
||||||
{/* ICON*/}
|
<div className="shadow-borders-base flex size-7 items-center justify-center rounded-md">
|
||||||
<div className="grow-0 rounded-lg border">
|
<div className="bg-ui-bg-field flex size-6 items-center justify-center rounded-[4px]">
|
||||||
<div className="bg-ui-bg-field m-1 rounded-md p-2">
|
|
||||||
<Buildings className="text-ui-fg-subtle" />
|
<Buildings className="text-ui-fg-subtle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* LOCATION INFO*/}
|
|
||||||
<div className="grow-1 flex flex-1 flex-col">
|
<div className="grow-1 flex flex-1 flex-col">
|
||||||
<Text weight="plus">{location.name}</Text>
|
<Text weight="plus">{location.name}</Text>
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
@@ -154,8 +138,7 @@ function Location(props: LocationProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ACTION*/}
|
<div className="flex grow-0 items-center gap-4">
|
||||||
<div className="flex grow-0 items-center gap-4 overflow-hidden">
|
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
groups={[
|
groups={[
|
||||||
{
|
{
|
||||||
@@ -165,8 +148,12 @@ function Location(props: LocationProps) {
|
|||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
to: `/settings/locations/${location.id}/edit`,
|
to: `/settings/locations/${location.id}/edit`,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
{
|
{
|
||||||
label: t("location.deleteLocation.label"),
|
label: t("actions.delete"),
|
||||||
icon: <Trash />,
|
icon: <Trash />,
|
||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
},
|
},
|
||||||
@@ -175,13 +162,9 @@ function Location(props: LocationProps) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="bg-ui-border-strong h-[12px] w-[1px]" />
|
<div className="bg-ui-border-strong h-[12px] w-[1px]" />
|
||||||
<Button
|
<LinkButton to={`/settings/locations/${location.id}`}>
|
||||||
className="text-ui-fg-interactive -ml-1 rounded-none"
|
|
||||||
onClick={() => navigate(`/settings/locations/${location.id}`)}
|
|
||||||
variant="transparent"
|
|
||||||
>
|
|
||||||
{t("actions.viewDetails")}
|
{t("actions.viewDetails")}
|
||||||
</Button>
|
</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,18 +173,18 @@ function Location(props: LocationProps) {
|
|||||||
|
|
||||||
<FulfillmentSet
|
<FulfillmentSet
|
||||||
type={FulfillmentSetType.Pickup}
|
type={FulfillmentSetType.Pickup}
|
||||||
fulfillmentSet={location.fulfillment_sets.find(
|
fulfillmentSet={location.fulfillment_sets?.find(
|
||||||
(f) => f.type === FulfillmentSetType.Pickup
|
(f) => f.type === FulfillmentSetType.Pickup
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FulfillmentSet
|
<FulfillmentSet
|
||||||
type={FulfillmentSetType.Delivery}
|
type={FulfillmentSetType.Shipping}
|
||||||
fulfillmentSet={location.fulfillment_sets.find(
|
fulfillmentSet={location.fulfillment_sets?.find(
|
||||||
(f) => f.type === FulfillmentSetType.Delivery
|
(f) => f.type === FulfillmentSetType.Shipping
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Location
|
export default LocationListItem
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./location"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// TODO: change this when RQ is fixed (address is not joined when *address)
|
|
||||||
export const locationListFields =
|
|
||||||
"name,*sales_channels,address.city,address.country_code,*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile"
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// TODO: change this when RQ is fixed (address is not joined when *address)
|
||||||
|
export const LOCATION_LIST_FIELDS =
|
||||||
|
"name,*sales_channels,*address,*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile"
|
||||||
@@ -1,25 +1,36 @@
|
|||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { LoaderFunctionArgs, redirect } from "react-router-dom"
|
||||||
|
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
import { client } from "../../../lib/client"
|
import { sdk } from "../../../lib/client"
|
||||||
import { queryClient } from "../../../lib/query-client"
|
import { queryClient } from "../../../lib/query-client"
|
||||||
import { StockLocationListRes } from "../../../types/api-responses"
|
import { LOCATION_LIST_FIELDS } from "./constants"
|
||||||
import { locationListFields } from "./const"
|
|
||||||
|
|
||||||
const shippingListQuery = () => ({
|
const shippingListQuery = () => ({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
queryFn: async () =>
|
queryFn: async () => {
|
||||||
client.stockLocations.list({
|
return await sdk.admin.stockLocation
|
||||||
// TODO: change this when RQ is fixed
|
.list({
|
||||||
fields: locationListFields,
|
// TODO: change this when RQ is fixed
|
||||||
}),
|
fields: LOCATION_LIST_FIELDS,
|
||||||
|
})
|
||||||
|
.catch((error: FetchError) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
throw redirect("/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const shippingListLoader = async (_: LoaderFunctionArgs) => {
|
export const shippingListLoader = async (_: LoaderFunctionArgs) => {
|
||||||
const query = shippingListQuery()
|
const query = shippingListQuery()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
queryClient.getQueryData<StockLocationListRes>(query.queryKey) ??
|
queryClient.getQueryData<HttpTypes.AdminStockLocationListResponse>(
|
||||||
(await queryClient.fetchQuery(query))
|
query.queryKey
|
||||||
|
) ?? (await queryClient.fetchQuery(query))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+16
-21
@@ -1,29 +1,34 @@
|
|||||||
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
import { Outlet, useLoaderData } from "react-router-dom"
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Link, Outlet, useLoaderData } from "react-router-dom"
|
|
||||||
|
|
||||||
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
||||||
import Location from "./components/location/location"
|
import LocationListItem from "./components/location-list-item/location-list-item"
|
||||||
import { locationListFields } from "./const"
|
import { LOCATION_LIST_FIELDS } from "./constants"
|
||||||
import { shippingListLoader } from "./loader"
|
import { shippingListLoader } from "./loader"
|
||||||
|
|
||||||
import after from "virtual:medusa/widgets/location/list/after"
|
import after from "virtual:medusa/widgets/location/list/after"
|
||||||
import before from "virtual:medusa/widgets/location/list/before"
|
import before from "virtual:medusa/widgets/location/list/before"
|
||||||
|
import { LocationListHeader } from "./components/location-list-header"
|
||||||
|
|
||||||
export function LocationList() {
|
export function LocationList() {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
ReturnType<typeof shippingListLoader>
|
ReturnType<typeof shippingListLoader>
|
||||||
>
|
>
|
||||||
|
|
||||||
const { stock_locations: stockLocations = [], isPending } = useStockLocations(
|
const {
|
||||||
|
stock_locations: stockLocations = [],
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useStockLocations(
|
||||||
{
|
{
|
||||||
fields: locationListFields,
|
fields: LOCATION_LIST_FIELDS,
|
||||||
},
|
},
|
||||||
{ initialData }
|
{ initialData }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
{before.widgets.map((w, i) => {
|
{before.widgets.map((w, i) => {
|
||||||
@@ -33,20 +38,10 @@ export function LocationList() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<Container className="flex h-fit items-center justify-between p-8">
|
<LocationListHeader />
|
||||||
<div>
|
|
||||||
<Heading className="mb-2">{t("location.title")}</Heading>
|
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
|
||||||
{t("location.description")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Button size="small" variant="secondary" asChild>
|
|
||||||
<Link to="create">{t("location.createLocation")}</Link>
|
|
||||||
</Button>
|
|
||||||
</Container>
|
|
||||||
<div className="flex flex-col gap-3 lg:col-span-2">
|
<div className="flex flex-col gap-3 lg:col-span-2">
|
||||||
{stockLocations.map((location) => (
|
{stockLocations.map((location) => (
|
||||||
<Location key={location.id} location={location} />
|
<LocationListItem key={location.id} location={location} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{after.widgets.map((w, i) => {
|
{after.widgets.map((w, i) => {
|
||||||
|
|||||||
+22
-11
@@ -1,7 +1,6 @@
|
|||||||
import { SalesChannel } from "@medusajs/medusa"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { SalesChannelDTO, StockLocationDTO } from "@medusajs/types"
|
import { Button, Checkbox, toast } from "@medusajs/ui"
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { Button, Checkbox } from "@medusajs/ui"
|
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -14,15 +13,15 @@ import {
|
|||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||||
|
import { useUpdateStockLocationSalesChannels } from "../../../../../hooks/api/stock-locations"
|
||||||
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
|
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
|
||||||
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
|
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
|
||||||
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
|
||||||
import { useUpdateStockLocationSalesChannels } from "../../../../../hooks/api/stock-locations"
|
|
||||||
|
|
||||||
type EditSalesChannelsFormProps = {
|
type EditSalesChannelsFormProps = {
|
||||||
location: StockLocationDTO & { sales_channels: SalesChannelDTO[] }
|
location: HttpTypes.AdminStockLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditSalesChannelsSchema = zod.object({
|
const EditSalesChannelsSchema = zod.object({
|
||||||
@@ -97,18 +96,30 @@ export const LocationEditSalesChannelsForm = ({
|
|||||||
useUpdateStockLocationSalesChannels(location.id)
|
useUpdateStockLocationSalesChannels(location.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
const originalIds = location.sales_channels.map((sc) => sc.id)
|
const originalIds = location.sales_channels?.map((sc) => sc.id)
|
||||||
|
|
||||||
const arr = data.sales_channels ?? []
|
const arr = data.sales_channels ?? []
|
||||||
|
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
add: arr.filter((i) => !originalIds.includes(i)),
|
add: arr.filter((i) => !originalIds?.includes(i)),
|
||||||
remove: originalIds.filter((i) => !arr.includes(i)),
|
remove: originalIds?.filter((i) => !arr.includes(i)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
handleSuccess()
|
toast.success(t("general.success"), {
|
||||||
|
description: t("stockLocations.salesChannels.successToast"),
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
handleSuccess(`/settings/locations/${location.id}`)
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -153,7 +164,7 @@ export const LocationEditSalesChannelsForm = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<SalesChannel>()
|
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>()
|
||||||
|
|
||||||
const useColumns = () => {
|
const useColumns = () => {
|
||||||
const columns = useSalesChannelTableColumns()
|
const columns = useSalesChannelTableColumns()
|
||||||
File renamed without changes.
@@ -0,0 +1 @@
|
|||||||
|
export { LocationSalesChannels as Component } from "./location-sales-channels"
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||||
|
import { LocationEditSalesChannelsForm } from "./components/edit-sales-channels-form"
|
||||||
|
|
||||||
|
export const LocationSalesChannels = () => {
|
||||||
|
const { location_id } = useParams()
|
||||||
|
const { stock_location, isPending, isError, error } = useStockLocation(
|
||||||
|
location_id!,
|
||||||
|
{
|
||||||
|
fields: "id,*sales_channels",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const ready = !isPending && !!stock_location
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
{ready && <LocationEditSalesChannelsForm location={stock_location} />}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, Heading, Input, toast } from "@medusajs/ui"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { InlineTip } from "../../../../../components/common/inline-tip"
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useCreateFulfillmentSetServiceZone } from "../../../../../hooks/api/fulfillment-sets"
|
||||||
|
import { GeoZoneForm } from "../../../common/components/geo-zone-form"
|
||||||
|
import { FulfillmentSetType } from "../../../common/constants"
|
||||||
|
|
||||||
|
const CreateServiceZoneSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
countries: z
|
||||||
|
.array(z.object({ iso_2: z.string().min(2), display_name: z.string() }))
|
||||||
|
.min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
type CreateServiceZoneFormProps = {
|
||||||
|
fulfillmentSet: HttpTypes.AdminFulfillmentSet
|
||||||
|
type: FulfillmentSetType
|
||||||
|
location: HttpTypes.AdminStockLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateServiceZoneForm({
|
||||||
|
fulfillmentSet,
|
||||||
|
type,
|
||||||
|
location,
|
||||||
|
}: CreateServiceZoneFormProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof CreateServiceZoneSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
countries: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CreateServiceZoneSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useCreateFulfillmentSetServiceZone(
|
||||||
|
fulfillmentSet.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
name: data.name,
|
||||||
|
geo_zones: data.countries.map(({ iso_2 }) => ({
|
||||||
|
country_code: iso_2,
|
||||||
|
type: "country",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("stockLocations.serviceZones.create.successToast", {
|
||||||
|
name: data.name,
|
||||||
|
}),
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
|
||||||
|
handleSuccess(`/settings/locations/${location.id}`)
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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={isPending}>
|
||||||
|
{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={setOpen}>
|
||||||
|
<SplitView.Content>
|
||||||
|
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||||
|
<Heading>
|
||||||
|
{type === FulfillmentSetType.Pickup
|
||||||
|
? t("stockLocations.serviceZones.create.headerPickup", {
|
||||||
|
location: location.name,
|
||||||
|
})
|
||||||
|
: t("stockLocations.serviceZones.create.headerShipping", {
|
||||||
|
location: location.name,
|
||||||
|
})}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InlineTip>
|
||||||
|
{t("stockLocations.serviceZones.fields.tip")}
|
||||||
|
</InlineTip>
|
||||||
|
|
||||||
|
<GeoZoneForm form={form} onOpenChange={setOpen} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Content>
|
||||||
|
<GeoZoneForm.AreaDrawer
|
||||||
|
form={form}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
/>
|
||||||
|
</SplitView>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
File renamed without changes.
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { stockLocationLoader as loader } from "./loader"
|
||||||
|
export { LocationCreateServiceZone as Component } from "./location-service-zone-create"
|
||||||
+6
-5
@@ -1,16 +1,16 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
import { client } from "../../../lib/client"
|
import { sdk } from "../../../lib/client"
|
||||||
import { queryClient } from "../../../lib/query-client"
|
import { queryClient } from "../../../lib/query-client"
|
||||||
import { StockLocationRes } from "../../../types/api-responses"
|
|
||||||
|
|
||||||
const fulfillmentSetCreateQuery = (id: string) => ({
|
const fulfillmentSetCreateQuery = (id: string) => ({
|
||||||
queryKey: stockLocationsQueryKeys.detail(id, {
|
queryKey: stockLocationsQueryKeys.detail(id, {
|
||||||
fields: "*fulfillment_sets",
|
fields: "*fulfillment_sets",
|
||||||
}),
|
}),
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
client.stockLocations.retrieve(id, {
|
sdk.admin.stockLocation.retrieve(id, {
|
||||||
fields: "*fulfillment_sets",
|
fields: "*fulfillment_sets",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -20,7 +20,8 @@ export const stockLocationLoader = async ({ params }: LoaderFunctionArgs) => {
|
|||||||
const query = fulfillmentSetCreateQuery(id!)
|
const query = fulfillmentSetCreateQuery(id!)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
queryClient.getQueryData<StockLocationRes>(query.queryKey) ??
|
queryClient.getQueryData<HttpTypes.AdminStockLocationResponse>(
|
||||||
(await queryClient.fetchQuery(query))
|
query.queryKey
|
||||||
|
) ?? (await queryClient.fetchQuery(query))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
import { json, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||||
|
import { FulfillmentSetType } from "../common/constants"
|
||||||
|
import { CreateServiceZoneForm } from "./components/create-service-zone-form"
|
||||||
|
import { stockLocationLoader } from "./loader"
|
||||||
|
|
||||||
|
export function LocationCreateServiceZone() {
|
||||||
|
const { fset_id, location_id } = useParams()
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof stockLocationLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { stock_location, isLoading, isError, error } = useStockLocation(
|
||||||
|
location_id!,
|
||||||
|
{
|
||||||
|
fields: "*fulfillment_sets",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const fulfillmentSet = stock_location?.fulfillment_sets?.find(
|
||||||
|
(f) => f.id === fset_id
|
||||||
|
)
|
||||||
|
|
||||||
|
const ready = !isLoading && !!stock_location && !!fulfillmentSet
|
||||||
|
|
||||||
|
const type: FulfillmentSetType =
|
||||||
|
fulfillmentSet?.type === FulfillmentSetType.Pickup
|
||||||
|
? FulfillmentSetType.Pickup
|
||||||
|
: FulfillmentSetType.Shipping
|
||||||
|
|
||||||
|
if (!isLoading && !fulfillmentSet) {
|
||||||
|
throw json(
|
||||||
|
{ message: `Fulfillment set with ID: ${fset_id} was not found.` },
|
||||||
|
404
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal prev={`/settings/locations/${location_id}`}>
|
||||||
|
{ready && (
|
||||||
|
<CreateServiceZoneForm
|
||||||
|
fulfillmentSet={fulfillmentSet}
|
||||||
|
type={type}
|
||||||
|
location={stock_location}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+14
-21
@@ -1,18 +1,19 @@
|
|||||||
import { ServiceZoneDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Alert, Button, Input, Text, toast } from "@medusajs/ui"
|
import { Button, Input, toast } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { InlineTip } from "../../../../../components/common/inline-tip"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { useUpdateServiceZone } from "../../../../../hooks/api/stock-locations"
|
import { useUpdateFulfillmentSetServiceZone } from "../../../../../hooks/api/fulfillment-sets"
|
||||||
|
|
||||||
type EditServiceZoneFormProps = {
|
type EditServiceZoneFormProps = {
|
||||||
zone: ServiceZoneDTO
|
zone: HttpTypes.AdminServiceZone
|
||||||
fulfillmentSetId: string
|
fulfillmentSetId: string
|
||||||
locationId: string
|
locationId: string
|
||||||
}
|
}
|
||||||
@@ -35,11 +36,8 @@ export const EditServiceZoneForm = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isPending: isLoading } = useUpdateServiceZone(
|
const { mutateAsync, isPending: isLoading } =
|
||||||
fulfillmentSetId,
|
useUpdateFulfillmentSetServiceZone(fulfillmentSetId, zone.id)
|
||||||
zone.id,
|
|
||||||
locationId
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
@@ -49,10 +47,12 @@ export const EditServiceZoneForm = ({
|
|||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("general.success"), {
|
toast.success(t("general.success"), {
|
||||||
// description: t("regions.toast.edit"),
|
description: t("stockLocations.serviceZones.edit.successToast", {
|
||||||
|
name: values.name,
|
||||||
|
}),
|
||||||
dismissLabel: t("actions.close"),
|
dismissLabel: t("actions.close"),
|
||||||
})
|
})
|
||||||
handleSuccess()
|
handleSuccess(`/settings/locations/${locationId}`)
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast.error(t("general.error"), {
|
toast.error(t("general.error"), {
|
||||||
@@ -86,14 +86,7 @@ export const EditServiceZoneForm = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Alert>
|
<InlineTip>{t("stockLocations.serviceZones.fields.tip")}</InlineTip>
|
||||||
<Text weight="plus">
|
|
||||||
{t("location.serviceZone.create.subtitle")}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-ui-fg-subtle mt-2">
|
|
||||||
{t("location.serviceZone.create.description")}
|
|
||||||
</Text>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
</RouteDrawer.Body>
|
</RouteDrawer.Body>
|
||||||
<RouteDrawer.Footer>
|
<RouteDrawer.Footer>
|
||||||
File renamed without changes.
@@ -0,0 +1 @@
|
|||||||
|
export { LocationServiceZoneEdit as Component } from "./location-service-zone-edit"
|
||||||
+11
-12
@@ -6,27 +6,26 @@ import { RouteDrawer } from "../../../components/route-modal"
|
|||||||
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||||
import { EditServiceZoneForm } from "./components/edit-region-form"
|
import { EditServiceZoneForm } from "./components/edit-region-form"
|
||||||
|
|
||||||
export const ServiceZoneEdit = () => {
|
export const LocationServiceZoneEdit = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { location_id, fset_id, zone_id } = useParams()
|
const { location_id, fset_id, zone_id } = useParams()
|
||||||
|
|
||||||
const { stock_location, isPending, isError, error } = useStockLocation(
|
const { stock_location, isPending, isError, error } = useStockLocation(
|
||||||
location_id!,
|
location_id!,
|
||||||
{
|
{
|
||||||
fields:
|
fields: "*fulfillment_sets.service_zones",
|
||||||
"name,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.shipping_profile",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const zone = stock_location?.fulfillment_sets
|
const serviceZone = stock_location?.fulfillment_sets
|
||||||
.find((f) => f.id === fset_id)
|
?.find((f) => f.id === fset_id)
|
||||||
?.service_zones.find((z) => z.id === zone_id)
|
?.service_zones.find((z) => z.id === zone_id)
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPending && !zone) {
|
if (!isPending && !serviceZone) {
|
||||||
throw json(
|
throw json(
|
||||||
{ message: `Service zone with ID ${zone_id} was not found` },
|
{ message: `Service zone with ID ${zone_id} was not found` },
|
||||||
404
|
404
|
||||||
@@ -34,15 +33,15 @@ export const ServiceZoneEdit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer>
|
<RouteDrawer prev={`/settings/locations/${location_id}`}>
|
||||||
<RouteDrawer.Header>
|
<RouteDrawer.Header>
|
||||||
<Heading>{t("location.serviceZone.edit.title")}</Heading>
|
<Heading>{t("stockLocations.serviceZones.edit.header")}</Heading>
|
||||||
</RouteDrawer.Header>
|
</RouteDrawer.Header>
|
||||||
{!isPending && zone && (
|
{!isPending && serviceZone && (
|
||||||
<EditServiceZoneForm
|
<EditServiceZoneForm
|
||||||
zone={zone}
|
zone={serviceZone}
|
||||||
fulfillmentSetId={fset_id}
|
fulfillmentSetId={fset_id!}
|
||||||
locationId={location_id}
|
locationId={location_id!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RouteDrawer>
|
</RouteDrawer>
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, Heading, toast } from "@medusajs/ui"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateFulfillmentSetServiceZone } from "../../../../../hooks/api/fulfillment-sets"
|
||||||
|
import { countries } from "../../../../../lib/countries"
|
||||||
|
import { GeoZoneForm } from "../../../common/components/geo-zone-form"
|
||||||
|
|
||||||
|
const EditeServiceZoneSchema = z.object({
|
||||||
|
countries: z
|
||||||
|
.array(z.object({ iso_2: z.string().min(2), display_name: z.string() }))
|
||||||
|
.min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
type EditServiceZoneAreasFormProps = {
|
||||||
|
fulfillmentSetId: string
|
||||||
|
locationId: string
|
||||||
|
zone: HttpTypes.AdminServiceZone
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditServiceZoneAreasForm({
|
||||||
|
fulfillmentSetId,
|
||||||
|
locationId,
|
||||||
|
zone,
|
||||||
|
}: EditServiceZoneAreasFormProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof EditeServiceZoneSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
countries: zone.geo_zones.map((z) => {
|
||||||
|
const country = countries.find((c) => c.iso_2 === z.country_code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
iso_2: z.country_code,
|
||||||
|
display_name: country?.display_name || z.country_code.toUpperCase(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditeServiceZoneSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync: editServiceZone, isPending: isLoading } =
|
||||||
|
useUpdateFulfillmentSetServiceZone(fulfillmentSetId, zone.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await editServiceZone(
|
||||||
|
{
|
||||||
|
geo_zones: data.countries.map(({ iso_2 }) => ({
|
||||||
|
country_code: iso_2,
|
||||||
|
type: "country",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t(
|
||||||
|
"stockLocations.serviceZones.manageAreas.successToast",
|
||||||
|
{
|
||||||
|
name: zone.name,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
|
||||||
|
handleSuccess(`/settings/locations/${locationId}`)
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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="flex flex-col overflow-hidden">
|
||||||
|
<SplitView open={open} onOpenChange={setOpen}>
|
||||||
|
<SplitView.Content>
|
||||||
|
<div className="flex flex-col items-center p-16">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
|
<Heading>
|
||||||
|
{t("stockLocations.serviceZones.manageAreas.header", {
|
||||||
|
name: zone.name,
|
||||||
|
})}
|
||||||
|
</Heading>
|
||||||
|
<GeoZoneForm form={form} onOpenChange={setOpen} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Content>
|
||||||
|
<GeoZoneForm.AreaDrawer
|
||||||
|
form={form}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
/>
|
||||||
|
</SplitView>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
File renamed without changes.
+1
@@ -0,0 +1 @@
|
|||||||
|
export { LocationServiceZoneManageAreas as Component } from "./location-service-zone-manage-areas"
|
||||||
+7
-9
@@ -1,23 +1,21 @@
|
|||||||
import { json, useParams } from "react-router-dom"
|
import { json, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { EditServiceZoneAreasForm } from "./components/edit-region-areas-form"
|
|
||||||
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||||
|
import { EditServiceZoneAreasForm } from "./components/edit-region-areas-form"
|
||||||
|
|
||||||
export const ServiceZoneAreasEdit = () => {
|
export const LocationServiceZoneManageAreas = () => {
|
||||||
const { location_id, fset_id, zone_id } = useParams()
|
const { location_id, fset_id, zone_id } = useParams()
|
||||||
|
|
||||||
const { stock_location, isPending, isError, error } = useStockLocation(
|
const { stock_location, isPending, isError, error } = useStockLocation(
|
||||||
location_id!,
|
location_id!,
|
||||||
{
|
{
|
||||||
// NOTE: use same query for all details page subroutes & fetches
|
fields: "*fulfillment_sets.service_zones.geo_zones",
|
||||||
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
|
const zone = stock_location?.fulfillment_sets
|
||||||
.find((f) => f.id === fset_id)
|
?.find((f) => f.id === fset_id)
|
||||||
?.service_zones.find((z) => z.id === zone_id)
|
?.service_zones.find((z) => z.id === zone_id)
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@@ -32,12 +30,12 @@ export const ServiceZoneAreasEdit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal prev={`/settings/locations/${location_id}`}>
|
||||||
{!isPending && zone && (
|
{!isPending && zone && (
|
||||||
<EditServiceZoneAreasForm
|
<EditServiceZoneAreasForm
|
||||||
zone={zone}
|
zone={zone}
|
||||||
fulfillmentSetId={fset_id}
|
fulfillmentSetId={fset_id!}
|
||||||
locationId={location_id}
|
locationId={location_id!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
+198
@@ -0,0 +1,198 @@
|
|||||||
|
import { Heading, Input, RadioGroup, Text } from "@medusajs/ui"
|
||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { SwitchBox } from "../../../../../components/common/switch-box"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
|
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||||
|
import { sdk } from "../../../../../lib/client"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
import { ShippingOptionPriceType } from "../../../common/constants"
|
||||||
|
import { CreateShippingOptionSchema } from "./schema"
|
||||||
|
|
||||||
|
type CreateShippingOptionDetailsFormProps = {
|
||||||
|
form: UseFormReturn<CreateShippingOptionSchema>
|
||||||
|
isReturn?: boolean
|
||||||
|
zone: HttpTypes.AdminServiceZone
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateShippingOptionDetailsForm = ({
|
||||||
|
form,
|
||||||
|
isReturn = false,
|
||||||
|
zone,
|
||||||
|
}: CreateShippingOptionDetailsFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const shippingProfiles = useComboboxData({
|
||||||
|
queryFn: (params) => sdk.admin.shippingProfile.list(params),
|
||||||
|
queryKey: ["shipping_profiles"],
|
||||||
|
getOptions: (data) =>
|
||||||
|
data.shipping_profiles.map((profile) => ({
|
||||||
|
label: profile.name,
|
||||||
|
value: profile.id,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const fulfillmentProviders = useComboboxData({
|
||||||
|
queryFn: (params) => sdk.admin.fulfillmentProvider.list(params),
|
||||||
|
queryKey: ["fulfillment_providers"],
|
||||||
|
getOptions: (data) =>
|
||||||
|
data.fulfillment_providers.map((provider) => ({
|
||||||
|
label: formatProvider(provider.id),
|
||||||
|
value: provider.id,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||||
|
<div>
|
||||||
|
<Heading>
|
||||||
|
{t(
|
||||||
|
`stockLocations.shippingOptions.create.${
|
||||||
|
isReturn ? "returns" : "shipping"
|
||||||
|
}.header`,
|
||||||
|
{
|
||||||
|
zone: zone.name,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t(
|
||||||
|
`stockLocations.shippingOptions.create.${
|
||||||
|
isReturn ? "returns" : "shipping"
|
||||||
|
}.hint`
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="price_type"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("stockLocations.shippingOptions.fields.priceType.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
className="grid grid-cols-1 gap-4 md:grid-cols-2"
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
className="flex-1"
|
||||||
|
value={ShippingOptionPriceType.FlatRate}
|
||||||
|
label={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.fixed.label"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.fixed.hint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
className="flex-1"
|
||||||
|
value={ShippingOptionPriceType.Calculated}
|
||||||
|
label={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.calculated.label"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.calculated.hint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="shipping_profile_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("stockLocations.shippingOptions.fields.profile")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
options={shippingProfiles.options}
|
||||||
|
searchValue={shippingProfiles.searchValue}
|
||||||
|
onSearchValueChange={shippingProfiles.onSearchValueChange}
|
||||||
|
disabled={shippingProfiles.disabled}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="provider_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("stockLocations.shippingOptions.fields.provider")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
options={fulfillmentProviders.options}
|
||||||
|
searchValue={fulfillmentProviders.searchValue}
|
||||||
|
onSearchValueChange={
|
||||||
|
fulfillmentProviders.onSearchValueChange
|
||||||
|
}
|
||||||
|
disabled={fulfillmentProviders.disabled}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<SwitchBox
|
||||||
|
control={form.control}
|
||||||
|
name="enabled_in_store"
|
||||||
|
label={t("stockLocations.shippingOptions.fields.enableInStore.label")}
|
||||||
|
description={t(
|
||||||
|
"stockLocations.shippingOptions.fields.enableInStore.hint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, ProgressStatus, ProgressTabs, toast } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||||
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
|
import { ShippingOptionPriceType } from "../../../common/constants"
|
||||||
|
import { CreateShippingOptionDetailsForm } from "./create-shipping-option-details-form"
|
||||||
|
import { CreateShippingOptionsPricesForm } from "./create-shipping-options-prices-form"
|
||||||
|
import {
|
||||||
|
CreateShippingOptionDetailsSchema,
|
||||||
|
CreateShippingOptionSchema,
|
||||||
|
} from "./schema"
|
||||||
|
|
||||||
|
enum Tab {
|
||||||
|
DETAILS = "details",
|
||||||
|
PRICING = "pricing",
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateShippingOptionFormProps = {
|
||||||
|
zone: HttpTypes.AdminServiceZone
|
||||||
|
locationId: string
|
||||||
|
isReturn?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateShippingOptionsForm({
|
||||||
|
zone,
|
||||||
|
isReturn,
|
||||||
|
locationId,
|
||||||
|
}: CreateShippingOptionFormProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState<Tab>(Tab.DETAILS)
|
||||||
|
const [validDetails, setValidDetails] = useState(false)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<CreateShippingOptionSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
price_type: ShippingOptionPriceType.FlatRate,
|
||||||
|
enabled_in_store: true,
|
||||||
|
shipping_profile_id: "",
|
||||||
|
provider_id: "",
|
||||||
|
region_prices: {},
|
||||||
|
currency_prices: {},
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CreateShippingOptionSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const isCalculatedPriceType =
|
||||||
|
form.watch("price_type") === ShippingOptionPriceType.Calculated
|
||||||
|
|
||||||
|
const { mutateAsync, isPending: isLoading } = useCreateShippingOptions()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const currencyPrices = Object.entries(data.currency_prices)
|
||||||
|
.map(([code, value]) => {
|
||||||
|
const amount = value ? castNumber(value) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
currency_code: code,
|
||||||
|
amount: amount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((o) => !!o.amount) as { currency_code: string; amount: number }[]
|
||||||
|
|
||||||
|
const regionPrices = Object.entries(data.region_prices)
|
||||||
|
.map(([region_id, value]) => {
|
||||||
|
const amount = value ? castNumber(value) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
region_id,
|
||||||
|
amount: amount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((o) => !!o.amount) as { region_id: string; amount: number }[]
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
name: data.name,
|
||||||
|
price_type: data.price_type,
|
||||||
|
service_zone_id: zone.id,
|
||||||
|
shipping_profile_id: data.shipping_profile_id,
|
||||||
|
provider_id: data.provider_id,
|
||||||
|
prices: [...currencyPrices, ...regionPrices],
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line
|
||||||
|
value: isReturn ? '"true"' : '"false"', // we want JSONB saved as string
|
||||||
|
attribute: "is_return",
|
||||||
|
operator: "eq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line
|
||||||
|
value: data.enabled_in_store ? '"true"' : '"false"', // we want JSONB saved as string
|
||||||
|
attribute: "enabled_in_store",
|
||||||
|
operator: "eq",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: {
|
||||||
|
// TODO: FETCH TYPES
|
||||||
|
label: "Type label",
|
||||||
|
description: "Type description",
|
||||||
|
code: "type-code",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ shipping_option }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t(
|
||||||
|
`stockLocations.shippingOptions.create.${
|
||||||
|
isReturn ? "returns" : "shipping"
|
||||||
|
}.successToast`,
|
||||||
|
{
|
||||||
|
name: shipping_option.name,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
|
||||||
|
handleSuccess(`/settings/locations/${locationId}`)
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onTabChange = (tab: Tab) => {
|
||||||
|
if (tab === Tab.PRICING) {
|
||||||
|
form.clearErrors()
|
||||||
|
|
||||||
|
const result = CreateShippingOptionDetailsSchema.safeParse({
|
||||||
|
...form.getValues(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const [firstError, ...rest] = result.error.errors
|
||||||
|
|
||||||
|
for (const error of rest) {
|
||||||
|
const _path = error.path.join(".") as keyof CreateShippingOptionSchema
|
||||||
|
|
||||||
|
form.setError(_path, {
|
||||||
|
message: error.message,
|
||||||
|
type: error.code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the first error
|
||||||
|
form.setError(
|
||||||
|
firstError.path.join(".") as keyof CreateShippingOptionSchema,
|
||||||
|
{
|
||||||
|
message: firstError.message,
|
||||||
|
type: firstError.code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldFocus: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
setValidDetails(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidDetails(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveTab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pricesStatus: ProgressStatus =
|
||||||
|
form.getFieldState("currency_prices")?.isDirty ||
|
||||||
|
form.getFieldState("region_prices")?.isDirty ||
|
||||||
|
activeTab === Tab.PRICING
|
||||||
|
? "in-progress"
|
||||||
|
: "not-started"
|
||||||
|
|
||||||
|
const detailsStatus: ProgressStatus = validDetails
|
||||||
|
? "completed"
|
||||||
|
: "in-progress"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<ProgressTabs
|
||||||
|
value={activeTab}
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
onValueChange={(tab) => onTabChange(tab as Tab)}
|
||||||
|
>
|
||||||
|
<form className="flex h-full flex-col" onSubmit={handleSubmit}>
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<ProgressTabs.List className="border-ui-border-base -my-2 ml-2 min-w-0 flex-1 border-l">
|
||||||
|
<ProgressTabs.Trigger
|
||||||
|
value={Tab.DETAILS}
|
||||||
|
status={detailsStatus}
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
>
|
||||||
|
<span className="w-full cursor-auto overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{t("stockLocations.shippingOptions.create.tabs.details")}
|
||||||
|
</span>
|
||||||
|
</ProgressTabs.Trigger>
|
||||||
|
{!isCalculatedPriceType && (
|
||||||
|
<ProgressTabs.Trigger
|
||||||
|
value={Tab.PRICING}
|
||||||
|
status={pricesStatus}
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
>
|
||||||
|
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{t("stockLocations.shippingOptions.create.tabs.prices")}
|
||||||
|
</span>
|
||||||
|
</ProgressTabs.Trigger>
|
||||||
|
)}
|
||||||
|
</ProgressTabs.List>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
{activeTab === Tab.PRICING || isCalculatedPriceType ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
isLoading={isLoading}
|
||||||
|
key="submit-btn"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={() => onTabChange(Tab.PRICING)}
|
||||||
|
key="continue-btn"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{t("actions.continue")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
|
||||||
|
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||||
|
<ProgressTabs.Content
|
||||||
|
value={Tab.DETAILS}
|
||||||
|
className="size-full overflow-y-auto"
|
||||||
|
>
|
||||||
|
<CreateShippingOptionDetailsForm
|
||||||
|
form={form}
|
||||||
|
zone={zone}
|
||||||
|
isReturn={isReturn}
|
||||||
|
/>
|
||||||
|
</ProgressTabs.Content>
|
||||||
|
<ProgressTabs.Content value={Tab.PRICING} className="size-full">
|
||||||
|
<CreateShippingOptionsPricesForm form={form} />
|
||||||
|
</ProgressTabs.Content>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</ProgressTabs>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import { useMemo } from "react"
|
||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
|
||||||
|
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||||
|
import { useRegions } from "../../../../../hooks/api/regions"
|
||||||
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
|
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
|
||||||
|
import { CreateShippingOptionSchema } from "./schema"
|
||||||
|
|
||||||
|
type PricingPricesFormProps = {
|
||||||
|
form: UseFormReturn<CreateShippingOptionSchema>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateShippingOptionsPricesForm = ({
|
||||||
|
form,
|
||||||
|
}: PricingPricesFormProps) => {
|
||||||
|
const {
|
||||||
|
store,
|
||||||
|
isLoading: isStoreLoading,
|
||||||
|
isError: isStoreError,
|
||||||
|
error: storeError,
|
||||||
|
} = useStore()
|
||||||
|
|
||||||
|
const currencies = useMemo(
|
||||||
|
() => store?.supported_currency_codes || [],
|
||||||
|
[store]
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
regions,
|
||||||
|
isLoading: isRegionsLoading,
|
||||||
|
isError: isRegionsError,
|
||||||
|
error: regionsError,
|
||||||
|
} = useRegions({
|
||||||
|
fields: "id,name,currency_code",
|
||||||
|
limit: 999,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useShippingOptionPriceColumns({
|
||||||
|
currencies,
|
||||||
|
regions,
|
||||||
|
})
|
||||||
|
|
||||||
|
const initializing = isStoreLoading || !store || isRegionsLoading || !regions
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() => [[...(currencies || []), ...(regions || [])]],
|
||||||
|
[currencies, regions]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isStoreError) {
|
||||||
|
throw storeError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRegionsError) {
|
||||||
|
throw regionsError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
|
<DataGridRoot data={data} columns={columns} state={form} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
File renamed without changes.
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { ShippingOptionPriceType } from "../../../common/constants"
|
||||||
|
|
||||||
|
export type CreateShippingOptionSchema = z.infer<
|
||||||
|
typeof CreateShippingOptionSchema
|
||||||
|
>
|
||||||
|
|
||||||
|
export const CreateShippingOptionDetailsSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
price_type: z.nativeEnum(ShippingOptionPriceType),
|
||||||
|
enabled_in_store: z.boolean(),
|
||||||
|
shipping_profile_id: z.string().min(1),
|
||||||
|
provider_id: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateShippingOptionSchema = z
|
||||||
|
.object({
|
||||||
|
region_prices: z.record(z.string(), z.string().optional()),
|
||||||
|
currency_prices: z.record(z.string(), z.string().optional()),
|
||||||
|
})
|
||||||
|
.merge(CreateShippingOptionDetailsSchema)
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export const LOC_CREATE_SHIPPING_OPTION_FIELDS =
|
||||||
|
"*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options"
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { stockLocationLoader as loader } from "./loader"
|
||||||
|
export { LocationServiceZoneShippingOptionCreate as Component } from "./location-service-zone-shipping-option-create"
|
||||||
+9
-9
@@ -1,19 +1,18 @@
|
|||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
import { client } from "../../../lib/client"
|
import { sdk } from "../../../lib/client"
|
||||||
import { queryClient } from "../../../lib/query-client"
|
import { queryClient } from "../../../lib/query-client"
|
||||||
import { StockLocationRes } from "../../../types/api-responses"
|
import { LOC_CREATE_SHIPPING_OPTION_FIELDS } from "./constants"
|
||||||
|
|
||||||
const fulfillmentSetCreateQuery = (id: string) => ({
|
const fulfillmentSetCreateQuery = (id: string) => ({
|
||||||
queryKey: stockLocationsQueryKeys.detail(id, {
|
queryKey: stockLocationsQueryKeys.detail(id, {
|
||||||
fields:
|
fields: LOC_CREATE_SHIPPING_OPTION_FIELDS,
|
||||||
"*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options",
|
|
||||||
}),
|
}),
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
client.stockLocations.retrieve(id, {
|
sdk.admin.stockLocation.retrieve(id, {
|
||||||
fields:
|
fields: LOC_CREATE_SHIPPING_OPTION_FIELDS,
|
||||||
"*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options",
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -22,7 +21,8 @@ export const stockLocationLoader = async ({ params }: LoaderFunctionArgs) => {
|
|||||||
const query = fulfillmentSetCreateQuery(id!)
|
const query = fulfillmentSetCreateQuery(id!)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
queryClient.getQueryData<StockLocationRes>(query.queryKey) ??
|
queryClient.getQueryData<HttpTypes.AdminStockLocationResponse>(
|
||||||
(await queryClient.fetchQuery(query))
|
query.queryKey
|
||||||
|
) ?? (await queryClient.fetchQuery(query))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
json,
|
||||||
|
useLoaderData,
|
||||||
|
useParams,
|
||||||
|
useSearchParams,
|
||||||
|
} from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||||
|
import { CreateShippingOptionsForm } from "./components/create-shipping-options-form"
|
||||||
|
import { LOC_CREATE_SHIPPING_OPTION_FIELDS } from "./constants"
|
||||||
|
import { stockLocationLoader } from "./loader"
|
||||||
|
|
||||||
|
export function LocationServiceZoneShippingOptionCreate() {
|
||||||
|
const { location_id, fset_id, zone_id } = useParams()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const isReturn = searchParams.has("is_return")
|
||||||
|
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof stockLocationLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { stock_location, isPending, isError, error } = useStockLocation(
|
||||||
|
location_id!,
|
||||||
|
{
|
||||||
|
fields: LOC_CREATE_SHIPPING_OPTION_FIELDS,
|
||||||
|
},
|
||||||
|
{ initialData }
|
||||||
|
)
|
||||||
|
|
||||||
|
const zone = stock_location?.fulfillment_sets
|
||||||
|
?.find((f) => f.id === fset_id)
|
||||||
|
?.service_zones?.find((z) => z.id === zone_id)
|
||||||
|
|
||||||
|
if (!zone) {
|
||||||
|
throw json(
|
||||||
|
{ message: `Service zone with ID ${zone_id} was not found` },
|
||||||
|
404
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready = !isPending && !!zone
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal prev={`/settings/locations/${location_id}`}>
|
||||||
|
{ready && (
|
||||||
|
<CreateShippingOptionsForm
|
||||||
|
zone={zone}
|
||||||
|
isReturn={isReturn}
|
||||||
|
locationId={location_id!}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+273
@@ -0,0 +1,273 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, Input, RadioGroup, toast } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { SwitchBox } from "../../../../../components/common/switch-box"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||||
|
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||||
|
import { sdk } from "../../../../../lib/client"
|
||||||
|
import { pick } from "../../../../../lib/common"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
import { isOptionEnabledInStore } from "../../../../../lib/shipping-options"
|
||||||
|
import { ShippingOptionPriceType } from "../../../common/constants"
|
||||||
|
|
||||||
|
type EditShippingOptionFormProps = {
|
||||||
|
locationId: string
|
||||||
|
shippingOption: HttpTypes.AdminShippingOption
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditShippingOptionSchema = zod.object({
|
||||||
|
name: zod.string().min(1),
|
||||||
|
price_type: zod.nativeEnum(ShippingOptionPriceType),
|
||||||
|
enabled_in_store: zod.boolean().optional(),
|
||||||
|
shipping_profile_id: zod.string(),
|
||||||
|
provider_id: zod.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditShippingOptionForm = ({
|
||||||
|
locationId,
|
||||||
|
shippingOption,
|
||||||
|
}: EditShippingOptionFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const shippingProfiles = useComboboxData({
|
||||||
|
queryFn: (params) => sdk.admin.shippingProfile.list(params),
|
||||||
|
queryKey: ["shipping_profiles"],
|
||||||
|
getOptions: (data) =>
|
||||||
|
data.shipping_profiles.map((profile) => ({
|
||||||
|
label: profile.name,
|
||||||
|
value: profile.id,
|
||||||
|
})),
|
||||||
|
defaultValue: shippingOption.shipping_profile_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fulfillmentProviders = useComboboxData({
|
||||||
|
queryFn: (params) => sdk.admin.fulfillmentProvider.list(params),
|
||||||
|
queryKey: ["fulfillment_providers"],
|
||||||
|
getOptions: (data) =>
|
||||||
|
data.fulfillment_providers.map((provider) => ({
|
||||||
|
label: formatProvider(provider.id),
|
||||||
|
value: provider.id,
|
||||||
|
})),
|
||||||
|
defaultValue: shippingOption.provider_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditShippingOptionSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: shippingOption.name,
|
||||||
|
price_type: shippingOption.price_type as ShippingOptionPriceType,
|
||||||
|
enabled_in_store: isOptionEnabledInStore(shippingOption),
|
||||||
|
shipping_profile_id: shippingOption.shipping_profile_id,
|
||||||
|
provider_id: shippingOption.provider_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isPending: isLoading } = useUpdateShippingOptions(
|
||||||
|
shippingOption.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
const rules = shippingOption.rules.map((r) => ({
|
||||||
|
...pick(r, ["id", "attribute", "operator", "value"]),
|
||||||
|
})) as HttpTypes.AdminUpdateShippingOptionRule[]
|
||||||
|
|
||||||
|
const storeRule = rules.find((r) => r.attribute === "enabled_in_store")
|
||||||
|
|
||||||
|
if (!storeRule) {
|
||||||
|
// NOTE: should always exist since we always create this rule when we create a shipping option
|
||||||
|
rules.push({
|
||||||
|
value: values.enabled_in_store ? "true" : "false",
|
||||||
|
attribute: "enabled_in_store",
|
||||||
|
operator: "eq",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
storeRule.value = values.enabled_in_store ? "true" : "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
name: values.name,
|
||||||
|
price_type: values.price_type,
|
||||||
|
shipping_profile_id: values.shipping_profile_id,
|
||||||
|
provider_id: values.provider_id,
|
||||||
|
rules,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ shipping_option }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("stockLocations.shippingOptions.edit.successToast", {
|
||||||
|
name: shipping_option.name,
|
||||||
|
}),
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
handleSuccess(`/settings/locations/${locationId}`)
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissable: true,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="price_type"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.label"
|
||||||
|
)}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
className="flex-1"
|
||||||
|
value={ShippingOptionPriceType.FlatRate}
|
||||||
|
label={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.fixed.label"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.fixed.hint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
className="flex-1"
|
||||||
|
value={ShippingOptionPriceType.Calculated}
|
||||||
|
label={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.calculated.label"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"stockLocations.shippingOptions.fields.priceType.options.calculated.hint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="shipping_profile_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("stockLocations.shippingOptions.fields.profile")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
options={shippingProfiles.options}
|
||||||
|
searchValue={shippingProfiles.searchValue}
|
||||||
|
onSearchValueChange={
|
||||||
|
shippingProfiles.onSearchValueChange
|
||||||
|
}
|
||||||
|
disabled={shippingProfiles.disabled}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="provider_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("stockLocations.shippingOptions.fields.provider")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
options={fulfillmentProviders.options}
|
||||||
|
searchValue={fulfillmentProviders.searchValue}
|
||||||
|
onSearchValueChange={
|
||||||
|
fulfillmentProviders.onSearchValueChange
|
||||||
|
}
|
||||||
|
disabled={fulfillmentProviders.disabled}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<SwitchBox
|
||||||
|
control={form.control}
|
||||||
|
name="enabled_in_store"
|
||||||
|
label={t(
|
||||||
|
"stockLocations.shippingOptions.fields.enableInStore.label"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"stockLocations.shippingOptions.fields.enableInStore.hint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
File renamed without changes.
+1
@@ -0,0 +1 @@
|
|||||||
|
export { LocationServiceZoneShippingOptionEdit as Component } from "./location-service-zone-shipping-option-edit"
|
||||||
+8
-8
@@ -1,19 +1,19 @@
|
|||||||
import { Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { json, useParams, useSearchParams } from "react-router-dom"
|
import { json, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { useShippingOptions } from "../../../hooks/api/shipping-options"
|
import { useShippingOptions } from "../../../hooks/api/shipping-options"
|
||||||
import { EditShippingOptionForm } from "./components/edit-region-form"
|
import { EditShippingOptionForm } from "./components/edit-region-form"
|
||||||
|
|
||||||
export const ShippingOptionEdit = () => {
|
export const LocationServiceZoneShippingOptionEdit = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
|
|
||||||
const { location_id, fset_id, zone_id, so_id } = useParams()
|
const { location_id, so_id } = useParams()
|
||||||
const isReturn = searchParams.has("is_return")
|
|
||||||
|
|
||||||
const { shipping_options, isPending, isError, error } = useShippingOptions()
|
const { shipping_options, isPending, isError, error } = useShippingOptions({
|
||||||
|
id: so_id,
|
||||||
|
})
|
||||||
|
|
||||||
const shippingOption = shipping_options?.find((so) => so.id === so_id)
|
const shippingOption = shipping_options?.find((so) => so.id === so_id)
|
||||||
|
|
||||||
@@ -31,12 +31,12 @@ export const ShippingOptionEdit = () => {
|
|||||||
return (
|
return (
|
||||||
<RouteDrawer>
|
<RouteDrawer>
|
||||||
<RouteDrawer.Header>
|
<RouteDrawer.Header>
|
||||||
<Heading>{t("location.shippingOptions.edit.title")}</Heading>
|
<Heading>{t("stockLocations.shippingOptions.edit.header")}</Heading>
|
||||||
</RouteDrawer.Header>
|
</RouteDrawer.Header>
|
||||||
{!isPending && shippingOption && (
|
{!isPending && shippingOption && (
|
||||||
<EditShippingOptionForm
|
<EditShippingOptionForm
|
||||||
shippingOption={shippingOption}
|
shippingOption={shippingOption}
|
||||||
isReturn={isReturn}
|
locationId={location_id!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RouteDrawer>
|
</RouteDrawer>
|
||||||
+244
@@ -0,0 +1,244 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, toast } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal/index"
|
||||||
|
import { useRegions } from "../../../../../hooks/api/regions"
|
||||||
|
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||||
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
|
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
|
||||||
|
|
||||||
|
const getInitialCurrencyPrices = (
|
||||||
|
prices: HttpTypes.AdminShippingOptionPrice[]
|
||||||
|
) => {
|
||||||
|
const ret: Record<string, number> = {}
|
||||||
|
prices.forEach((p) => {
|
||||||
|
if (p.price_rules!.length) {
|
||||||
|
// this is a region price
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ret[p.currency_code!] = p.amount
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialRegionPrices = (
|
||||||
|
prices: HttpTypes.AdminShippingOptionPrice[]
|
||||||
|
) => {
|
||||||
|
const ret: Record<string, number> = {}
|
||||||
|
prices.forEach((p) => {
|
||||||
|
if (p.price_rules!.length) {
|
||||||
|
const regionId = p.price_rules![0].value
|
||||||
|
ret[regionId] = p.amount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriceRecord = {
|
||||||
|
id?: string
|
||||||
|
currency_code?: string
|
||||||
|
region_id?: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
type EditShippingOptionPricingFormProps = {
|
||||||
|
shippingOption: HttpTypes.AdminShippingOption
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
store,
|
||||||
|
isLoading: isStoreLoading,
|
||||||
|
isError: isStoreError,
|
||||||
|
error: storeError,
|
||||||
|
} = useStore()
|
||||||
|
|
||||||
|
const currencies = useMemo(
|
||||||
|
() => store?.supported_currency_codes || [],
|
||||||
|
[store]
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
regions,
|
||||||
|
isLoading: isRegionsLoading,
|
||||||
|
isError: isRegionsError,
|
||||||
|
error: regionsError,
|
||||||
|
} = useRegions({
|
||||||
|
fields: "id,name,currency_code",
|
||||||
|
limit: 999,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useShippingOptionPriceColumns({
|
||||||
|
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 === "" || value === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = castNumber(value)
|
||||||
|
|
||||||
|
const priceRecord: 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) as PriceRecord[]
|
||||||
|
|
||||||
|
const regionPrices = Object.entries(data.region_prices)
|
||||||
|
.map(([region_id, value]) => {
|
||||||
|
if (value === "" || value === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = castNumber(value)
|
||||||
|
|
||||||
|
const priceRecord: 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) as PriceRecord[]
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
prices: [...currencyPrices, ...regionPrices],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const initializing =
|
||||||
|
isStoreLoading || isRegionsLoading || !currencies || !regions
|
||||||
|
|
||||||
|
if (isStoreError) {
|
||||||
|
throw storeError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRegionsError) {
|
||||||
|
throw regionsError
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
|
<DataGridRoot data={data} columns={columns} state={form} />
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
File renamed without changes.
+1
@@ -0,0 +1 @@
|
|||||||
|
export { LocationServiceZoneShippingOptionPricing as Component } from "./location-service-zone-shipping-option-pricing"
|
||||||
+6
-7
@@ -1,27 +1,26 @@
|
|||||||
import { useParams } from "react-router-dom"
|
import { json, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { useShippingOptions } from "../../../hooks/api/shipping-options"
|
import { useShippingOptions } from "../../../hooks/api/shipping-options"
|
||||||
import { EditShippingOptionsPricingForm } from "./components/create-shipping-options-form"
|
import { EditShippingOptionsPricingForm } from "./components/create-shipping-options-form"
|
||||||
|
|
||||||
export function ShippingOptionsEditPricing() {
|
export function LocationServiceZoneShippingOptionPricing() {
|
||||||
const { so_id } = useParams()
|
const { so_id, location_id } = useParams()
|
||||||
|
|
||||||
const { shipping_options, isPending } = useShippingOptions({
|
const { shipping_options, isPending } = useShippingOptions({
|
||||||
// TODO: change this when GET option by id endpoint is implemented
|
// TODO: change this when GET option by id endpoint is implemented
|
||||||
id: [so_id],
|
id: [so_id!],
|
||||||
fields: "*prices,*prices.price_rules",
|
fields: "*prices,*prices.price_rules",
|
||||||
limit: 999,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const shippingOption = shipping_options?.find((so) => so.id === so_id)
|
const shippingOption = shipping_options?.find((so) => so.id === so_id)
|
||||||
|
|
||||||
if (!isPending && !shippingOption) {
|
if (!isPending && !shippingOption) {
|
||||||
throw new Error(`Shipping option with id: ${so_id} not found`)
|
throw json(`Shipping option with id: ${so_id} not found`, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal prev={`/settings/locations/${location_id}`}>
|
||||||
{shippingOption && (
|
{shippingOption && (
|
||||||
<EditShippingOptionsPricingForm shippingOption={shippingOption} />
|
<EditShippingOptionsPricingForm shippingOption={shippingOption} />
|
||||||
)}
|
)}
|
||||||
-341
@@ -1,341 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
createColumnHelper,
|
|
||||||
RowSelectionState,
|
|
||||||
} from "@tanstack/react-table"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
import { XMarkMini } from "@medusajs/icons"
|
|
||||||
import { HttpTypes, ServiceZoneDTO } from "@medusajs/types"
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Text,
|
|
||||||
toast,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
|
||||||
import { SplitView } from "../../../../../components/layout/split-view"
|
|
||||||
import {
|
|
||||||
RouteFocusModal,
|
|
||||||
useRouteModal,
|
|
||||||
} from "../../../../../components/route-modal"
|
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
|
||||||
import { useUpdateServiceZone } from "../../../../../hooks/api/stock-locations"
|
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
|
||||||
import { countries as staticCountries } from "../../../../../lib/countries"
|
|
||||||
import { useCountries } from "../../../../regions/common/hooks/use-countries"
|
|
||||||
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
|
|
||||||
import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query"
|
|
||||||
|
|
||||||
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 HttpTypes.AdminRegion,
|
|
||||||
})),
|
|
||||||
...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("location.serviceZone.editAreasTitle", {
|
|
||||||
zone: zone.name,
|
|
||||||
})}
|
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container flex items-center justify-between py-8 pr-1">
|
|
||||||
<div>
|
|
||||||
<Text weight="plus">
|
|
||||||
{t("location.serviceZone.areas.title")}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-ui-fg-subtle mt-2">
|
|
||||||
{t("location.serviceZone.areas.description")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{t("location.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("location.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<HttpTypes.AdminRegionCountry>()
|
|
||||||
|
|
||||||
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<HttpTypes.AdminRegionCountry>[]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ServiceZoneAreasEdit as Component } from "./service-zone-areas-edit"
|
|
||||||
-367
@@ -1,367 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
createColumnHelper,
|
|
||||||
RowSelectionState,
|
|
||||||
} from "@tanstack/react-table"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
import { XMarkMini } from "@medusajs/icons"
|
|
||||||
import { FulfillmentSetDTO, HttpTypes } from "@medusajs/types"
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Text,
|
|
||||||
toast,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
|
||||||
import { SplitView } from "../../../../../components/layout/split-view"
|
|
||||||
import {
|
|
||||||
RouteFocusModal,
|
|
||||||
useRouteModal,
|
|
||||||
} from "../../../../../components/route-modal"
|
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
|
||||||
import { useCreateServiceZone } from "../../../../../hooks/api/stock-locations"
|
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
|
||||||
import { countries as staticCountries } from "../../../../../lib/countries"
|
|
||||||
import { useCountries } from "../../../../regions/common/hooks/use-countries"
|
|
||||||
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
|
|
||||||
import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query"
|
|
||||||
|
|
||||||
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.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateServiceZoneSchema = zod.object({
|
|
||||||
name: zod.string().min(1),
|
|
||||||
countries: zod.array(zod.string().length(2)).min(1),
|
|
||||||
})
|
|
||||||
|
|
||||||
type CreateServiceZoneFormProps = {
|
|
||||||
fulfillmentSet: FulfillmentSetDTO
|
|
||||||
locationId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateServiceZoneForm({
|
|
||||||
fulfillmentSet,
|
|
||||||
locationId,
|
|
||||||
}: CreateServiceZoneFormProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { handleSuccess } = useRouteModal()
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateServiceZoneSchema>>({
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
countries: [],
|
|
||||||
},
|
|
||||||
resolver: zodResolver(CreateServiceZoneSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { mutateAsync: createServiceZone, isPending: isLoading } =
|
|
||||||
useCreateServiceZone(locationId, fulfillmentSet.id)
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
|
|
||||||
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 HttpTypes.AdminRegion,
|
|
||||||
})),
|
|
||||||
...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="mb-12 mt-8 text-2xl">
|
|
||||||
{t("location.fulfillmentSet.create.title", {
|
|
||||||
fulfillmentSet: fulfillmentSet.name,
|
|
||||||
})}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<div className="flex max-w-[340px] flex-col gap-y-6">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.serviceZone.create.zoneName")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input placeholder={t("fields.name")} {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert>
|
|
||||||
<Text weight="plus">
|
|
||||||
{t("location.serviceZone.create.subtitle")}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-ui-fg-subtle mt-2">
|
|
||||||
{t("location.serviceZone.create.description")}
|
|
||||||
</Text>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{/*AREAS*/}
|
|
||||||
<div className="container flex items-center justify-between py-8 pr-1">
|
|
||||||
<div>
|
|
||||||
<Text weight="plus">
|
|
||||||
{t("location.serviceZone.areas.title")}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-ui-fg-subtle mt-2">
|
|
||||||
{t("location.serviceZone.areas.description")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{t("location.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("location.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<HttpTypes.AdminRegionCountry>()
|
|
||||||
|
|
||||||
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<HttpTypes.AdminRegionCountry>[]
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { ServiceZoneCreate as Component } from "./service-zone-create"
|
|
||||||
export { stockLocationLoader as loader } from "./loader"
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
import { useLoaderData, useParams } from "react-router-dom"
|
|
||||||
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
|
||||||
import { CreateServiceZoneForm } from "./components/create-service-zone-form"
|
|
||||||
import { stockLocationLoader } from "./loader"
|
|
||||||
|
|
||||||
export function ServiceZoneCreate() {
|
|
||||||
const { fset_id, location_id } = useParams()
|
|
||||||
const { stock_location: stockLocation } = useLoaderData() as Awaited<
|
|
||||||
ReturnType<typeof stockLocationLoader>
|
|
||||||
>
|
|
||||||
|
|
||||||
const fulfillmentSet = stockLocation.fulfillment_sets.find(
|
|
||||||
(f) => f.id === fset_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!fulfillmentSet) {
|
|
||||||
throw new Error("Fulfillment set doesn't exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteFocusModal>
|
|
||||||
<CreateServiceZoneForm
|
|
||||||
locationId={location_id!}
|
|
||||||
fulfillmentSet={fulfillmentSet}
|
|
||||||
/>
|
|
||||||
</RouteFocusModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ServiceZoneEdit as Component } from "./service-zone-edit"
|
|
||||||
-278
@@ -1,278 +0,0 @@
|
|||||||
import { ShippingOptionDTO } from "@medusajs/types"
|
|
||||||
import { Button, Input, RadioGroup, Select, Switch, toast } from "@medusajs/ui"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
|
||||||
import {
|
|
||||||
RouteDrawer,
|
|
||||||
useRouteModal,
|
|
||||||
} from "../../../../../components/route-modal"
|
|
||||||
import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers"
|
|
||||||
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
|
||||||
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
|
|
||||||
import { pick } from "../../../../../lib/common"
|
|
||||||
import { formatProvider } from "../../../../../lib/format-provider"
|
|
||||||
import { isOptionEnabledInStore } from "../../../../../lib/shipping-options"
|
|
||||||
|
|
||||||
enum ShippingAllocation {
|
|
||||||
FlatRate = "flat",
|
|
||||||
Calculated = "calculated",
|
|
||||||
}
|
|
||||||
|
|
||||||
type EditShippingOptionFormProps = {
|
|
||||||
isReturn?: boolean
|
|
||||||
shippingOption: ShippingOptionDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditShippingOptionSchema = zod.object({
|
|
||||||
name: zod.string().min(1),
|
|
||||||
price_type: zod.nativeEnum(ShippingAllocation),
|
|
||||||
enabled_in_store: zod.boolean().optional(),
|
|
||||||
shipping_profile_id: zod.string(),
|
|
||||||
provider_id: zod.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const EditShippingOptionForm = ({
|
|
||||||
shippingOption,
|
|
||||||
isReturn,
|
|
||||||
}: EditShippingOptionFormProps) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { handleSuccess } = useRouteModal()
|
|
||||||
|
|
||||||
const { shipping_profiles: shippingProfiles } = useShippingProfiles({
|
|
||||||
limit: 999,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { fulfillment_providers = [] } = useFulfillmentProviders({
|
|
||||||
is_enabled: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof EditShippingOptionSchema>>({
|
|
||||||
defaultValues: {
|
|
||||||
name: shippingOption.name,
|
|
||||||
price_type: shippingOption.price_type as ShippingAllocation,
|
|
||||||
enabled_in_store: isOptionEnabledInStore(shippingOption),
|
|
||||||
shipping_profile_id: shippingOption.shipping_profile_id,
|
|
||||||
provider_id: shippingOption.provider_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const { mutateAsync, isPending: isLoading } = useUpdateShippingOptions(
|
|
||||||
shippingOption.id
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
|
||||||
const rules = shippingOption.rules.map((r) => ({
|
|
||||||
...pick(r, ["id", "attribute", "operator", "value"]),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const storeRule = rules.find((r) => r.attribute === "enabled_in_store")
|
|
||||||
if (!storeRule) {
|
|
||||||
// NOTE: should always exist since we always create this rule when we create a shipping option
|
|
||||||
rules.push({
|
|
||||||
value: values.enabled_in_store ? "true" : "false",
|
|
||||||
attribute: "enabled_in_store",
|
|
||||||
operator: "eq",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
storeRule.value = values.enabled_in_store ? "true" : "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync(
|
|
||||||
{
|
|
||||||
name: values.name,
|
|
||||||
price_type: values.price_type,
|
|
||||||
shipping_profile_id: values.shipping_profile_id,
|
|
||||||
provider_id: values.provider_id,
|
|
||||||
rules,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success(t("general.success"), {
|
|
||||||
dismissLabel: t("actions.close"),
|
|
||||||
})
|
|
||||||
handleSuccess()
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
toast.error(t("general.error"), {
|
|
||||||
description: e.message,
|
|
||||||
dismissLabel: t("actions.close"),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteDrawer.Form form={form}>
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
|
||||||
<RouteDrawer.Body>
|
|
||||||
<div className="flex flex-col gap-y-8">
|
|
||||||
<div className="flex flex-col gap-y-8">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="price_type"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.create.allocation")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
className="flex-1"
|
|
||||||
value={ShippingAllocation.FlatRate}
|
|
||||||
label={t("location.shippingOptions.create.fixed")}
|
|
||||||
description={t(
|
|
||||||
"location.shippingOptions.create.fixedDescription"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
className="flex-1"
|
|
||||||
value={ShippingAllocation.Calculated}
|
|
||||||
label={t(
|
|
||||||
"location.shippingOptions.create.calculated"
|
|
||||||
)}
|
|
||||||
description={t(
|
|
||||||
"location.shippingOptions.create.calculatedDescription"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid gap-y-8 divide-y">
|
|
||||||
<div className="grid gap-y-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("fields.name")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="shipping_profile_id"
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.create.profile")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select {...field} onValueChange={onChange}>
|
|
||||||
<Select.Trigger ref={field.ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{(shippingProfiles ?? []).map((profile) => (
|
|
||||||
<Select.Item
|
|
||||||
key={profile.id}
|
|
||||||
value={profile.id}
|
|
||||||
>
|
|
||||||
{profile.name}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="provider_id"
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.edit.provider")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select {...field} onValueChange={onChange}>
|
|
||||||
<Select.Trigger ref={field.ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{fulfillment_providers.map((provider) => (
|
|
||||||
<Select.Item
|
|
||||||
key={provider.id}
|
|
||||||
value={provider.id}
|
|
||||||
>
|
|
||||||
{formatProvider(provider.id)}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-6">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="enabled_in_store"
|
|
||||||
render={({ field: { value, onChange, ...field } }) => (
|
|
||||||
<Form.Item>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.create.enable")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
checked={!!value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</div>
|
|
||||||
<Form.Hint className="!mt-1">
|
|
||||||
{t(
|
|
||||||
"location.shippingOptions.create.enableDescription"
|
|
||||||
)}
|
|
||||||
</Form.Hint>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RouteDrawer.Body>
|
|
||||||
<RouteDrawer.Footer>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<RouteDrawer.Close asChild>
|
|
||||||
<Button size="small" variant="secondary">
|
|
||||||
{t("actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
</RouteDrawer.Close>
|
|
||||||
<Button size="small" type="submit" isLoading={isLoading}>
|
|
||||||
{t("actions.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</RouteDrawer.Footer>
|
|
||||||
</form>
|
|
||||||
</RouteDrawer.Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ShippingOptionEdit as Component } from "./shipping-option-edit"
|
|
||||||
-474
@@ -1,474 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import React, { useEffect } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
import { ServiceZoneDTO } from "@medusajs/types"
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Heading,
|
|
||||||
Input,
|
|
||||||
ProgressStatus,
|
|
||||||
ProgressTabs,
|
|
||||||
RadioGroup,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
Text,
|
|
||||||
clx,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
|
||||||
import {
|
|
||||||
RouteFocusModal,
|
|
||||||
useRouteModal,
|
|
||||||
} from "../../../../../components/route-modal"
|
|
||||||
import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers"
|
|
||||||
import { useRegions } from "../../../../../hooks/api/regions"
|
|
||||||
import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
|
||||||
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
|
|
||||||
import { formatProvider } from "../../../../../lib/format-provider"
|
|
||||||
import { CreateShippingOptionsPricesForm } from "./create-shipping-options-prices-form"
|
|
||||||
import { castNumber } from "../../../../../lib/cast-number"
|
|
||||||
|
|
||||||
enum Tab {
|
|
||||||
DETAILS = "details",
|
|
||||||
PRICING = "pricing",
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ShippingAllocation {
|
|
||||||
FlatRate = "flat",
|
|
||||||
Calculated = "calculated",
|
|
||||||
}
|
|
||||||
|
|
||||||
type StepStatus = {
|
|
||||||
[key in Tab]: ProgressStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateShippingOptionSchema = zod.object({
|
|
||||||
name: zod.string().min(1),
|
|
||||||
price_type: zod.nativeEnum(ShippingAllocation),
|
|
||||||
enabled_in_store: zod.boolean().optional(),
|
|
||||||
shipping_profile_id: zod.string(),
|
|
||||||
provider_id: zod.string().min(1),
|
|
||||||
region_prices: zod.record(zod.string(), zod.string().optional()),
|
|
||||||
currency_prices: zod.record(zod.string(), zod.string().optional()),
|
|
||||||
})
|
|
||||||
|
|
||||||
type CreateShippingOptionFormProps = {
|
|
||||||
zone: ServiceZoneDTO
|
|
||||||
isReturn?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateShippingOptionsForm({
|
|
||||||
zone,
|
|
||||||
isReturn,
|
|
||||||
}: CreateShippingOptionFormProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { handleSuccess } = useRouteModal()
|
|
||||||
const [tab, setTab] = React.useState<Tab>(Tab.DETAILS)
|
|
||||||
|
|
||||||
const { fulfillment_providers = [] } = useFulfillmentProviders({
|
|
||||||
is_enabled: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { regions = [] } = useRegions({
|
|
||||||
limit: 999,
|
|
||||||
fields: "id,currency_code",
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateShippingOptionSchema>>({
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
price_type: ShippingAllocation.FlatRate,
|
|
||||||
enabled_in_store: true,
|
|
||||||
shipping_profile_id: "",
|
|
||||||
provider_id: "",
|
|
||||||
region_prices: {},
|
|
||||||
currency_prices: {},
|
|
||||||
},
|
|
||||||
resolver: zodResolver(CreateShippingOptionSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
const isCalculatedPriceType =
|
|
||||||
form.watch("price_type") === ShippingAllocation.Calculated
|
|
||||||
|
|
||||||
const { mutateAsync: createShippingOption, isPending: isLoading } =
|
|
||||||
useCreateShippingOptions()
|
|
||||||
|
|
||||||
const { shipping_profiles: shippingProfiles } = useShippingProfiles({
|
|
||||||
limit: 999,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
|
||||||
const currencyPrices = Object.entries(data.currency_prices)
|
|
||||||
.map(([code, value]) => {
|
|
||||||
const amount = value === "" ? undefined : castNumber(value)
|
|
||||||
|
|
||||||
return {
|
|
||||||
currency_code: code,
|
|
||||||
amount: amount,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((o) => !!o.amount)
|
|
||||||
|
|
||||||
const regionsMap = new Map(regions.map((r) => [r.id, r.currency_code]))
|
|
||||||
|
|
||||||
const regionPrices = Object.entries(data.region_prices)
|
|
||||||
.map(([region_id, value]) => {
|
|
||||||
const code = regionsMap.get(region_id)
|
|
||||||
|
|
||||||
const amount = value === "" ? undefined : castNumber(value)
|
|
||||||
|
|
||||||
return {
|
|
||||||
region_id,
|
|
||||||
amount: amount,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((o) => !!o.amount)
|
|
||||||
|
|
||||||
await createShippingOption({
|
|
||||||
name: data.name,
|
|
||||||
price_type: data.price_type,
|
|
||||||
service_zone_id: zone.id,
|
|
||||||
shipping_profile_id: data.shipping_profile_id,
|
|
||||||
provider_id: data.provider_id,
|
|
||||||
prices: [...currencyPrices, ...regionPrices],
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
value: isReturn ? '"true"' : '"false"', // we want JSONB saved as string
|
|
||||||
attribute: "is_return",
|
|
||||||
operator: "eq",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: data.enabled_in_store ? '"true"' : '"false"', // we want JSONB saved as string
|
|
||||||
attribute: "enabled_in_store",
|
|
||||||
operator: "eq",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
type: {
|
|
||||||
// TODO: FETCH TYPES
|
|
||||||
label: "Type label",
|
|
||||||
description: "Type description",
|
|
||||||
code: "type-code",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
handleSuccess()
|
|
||||||
})
|
|
||||||
|
|
||||||
const [status, setStatus] = React.useState<StepStatus>({
|
|
||||||
[Tab.PRICING]: "not-started",
|
|
||||||
[Tab.DETAILS]: "not-started",
|
|
||||||
})
|
|
||||||
|
|
||||||
const onTabChange = React.useCallback(async (value: Tab) => {
|
|
||||||
setTab(value)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onNext = React.useCallback(async () => {
|
|
||||||
switch (tab) {
|
|
||||||
case Tab.DETAILS: {
|
|
||||||
setTab(Tab.PRICING)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case Tab.PRICING:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}, [tab])
|
|
||||||
|
|
||||||
const canMoveToPricing =
|
|
||||||
form.watch("name").length &&
|
|
||||||
form.watch("shipping_profile_id") &&
|
|
||||||
form.watch("provider_id")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (form.formState.isDirty) {
|
|
||||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
|
||||||
} else {
|
|
||||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "not-started" }))
|
|
||||||
}
|
|
||||||
}, [form.formState.isDirty])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (tab === Tab.DETAILS && form.formState.isDirty) {
|
|
||||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tab === Tab.PRICING) {
|
|
||||||
const hasPricingSet = form
|
|
||||||
.getValues(["region_prices", "currency_prices"])
|
|
||||||
.map(Object.keys)
|
|
||||||
.some((i) => i.length)
|
|
||||||
|
|
||||||
setStatus((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[Tab.DETAILS]: "completed",
|
|
||||||
[Tab.PRICING]: hasPricingSet ? "in-progress" : "not-started",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}, [tab])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteFocusModal.Form form={form}>
|
|
||||||
<form
|
|
||||||
className="flex h-full flex-col overflow-hidden"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<ProgressTabs
|
|
||||||
value={tab}
|
|
||||||
className="h-full"
|
|
||||||
onValueChange={(tab) => onTabChange(tab as Tab)}
|
|
||||||
>
|
|
||||||
<RouteFocusModal.Header>
|
|
||||||
<ProgressTabs.List className="border-ui-border-base -my-2 ml-2 min-w-0 flex-1 border-l">
|
|
||||||
<ProgressTabs.Trigger
|
|
||||||
value={Tab.DETAILS}
|
|
||||||
status={status[Tab.DETAILS]}
|
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
>
|
|
||||||
<span className="w-full cursor-auto overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
{t("location.shippingOptions.create.details")}
|
|
||||||
</span>
|
|
||||||
</ProgressTabs.Trigger>
|
|
||||||
{!isCalculatedPriceType && (
|
|
||||||
<ProgressTabs.Trigger
|
|
||||||
value={Tab.PRICING}
|
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
status={status[Tab.PRICING]}
|
|
||||||
disabled={!canMoveToPricing}
|
|
||||||
>
|
|
||||||
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
{t("location.shippingOptions.create.pricing")}
|
|
||||||
</span>
|
|
||||||
</ProgressTabs.Trigger>
|
|
||||||
)}
|
|
||||||
</ProgressTabs.List>
|
|
||||||
<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={onNext}
|
|
||||||
disabled={!canMoveToPricing}
|
|
||||||
key={
|
|
||||||
tab === Tab.PRICING || isCalculatedPriceType
|
|
||||||
? "details"
|
|
||||||
: "pricing"
|
|
||||||
}
|
|
||||||
type={
|
|
||||||
tab === Tab.PRICING || isCalculatedPriceType
|
|
||||||
? "submit"
|
|
||||||
: "button"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{tab === Tab.PRICING || isCalculatedPriceType
|
|
||||||
? t("actions.save")
|
|
||||||
: t("general.next")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</RouteFocusModal.Header>
|
|
||||||
|
|
||||||
<RouteFocusModal.Body
|
|
||||||
className={clx(
|
|
||||||
"flex h-full w-fit flex-col items-center divide-y overflow-hidden",
|
|
||||||
{ "mx-auto": tab === Tab.DETAILS }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
|
||||||
<div className="container mx-auto w-[720px] px-1 py-8">
|
|
||||||
<Heading className="mb-12 mt-8 text-2xl">
|
|
||||||
{t(
|
|
||||||
`location.${
|
|
||||||
isReturn ? "returnOptions" : "shippingOptions"
|
|
||||||
}.create.title`,
|
|
||||||
{
|
|
||||||
zone: zone.name,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
{!isReturn && (
|
|
||||||
<div>
|
|
||||||
<Text weight="plus">
|
|
||||||
{t("location.shippingOptions.create.subtitle")}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-ui-fg-subtle mb-8 mt-2">
|
|
||||||
{t("location.shippingOptions.create.description")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="price_type"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.create.allocation")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<RadioGroup
|
|
||||||
className="flex justify-between gap-4"
|
|
||||||
{...field}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
>
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
className="flex-1"
|
|
||||||
value={ShippingAllocation.FlatRate}
|
|
||||||
label={t("location.shippingOptions.create.fixed")}
|
|
||||||
description={t(
|
|
||||||
"location.shippingOptions.create.fixedDescription"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<RadioGroup.ChoiceBox
|
|
||||||
className="flex-1"
|
|
||||||
value={ShippingAllocation.Calculated}
|
|
||||||
label={t(
|
|
||||||
"location.shippingOptions.create.calculated"
|
|
||||||
)}
|
|
||||||
description={t(
|
|
||||||
"location.shippingOptions.create.calculatedDescription"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-8 max-w-[50%] pr-2 ">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("fields.name")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col divide-y">
|
|
||||||
<div className="mt-4 grid grid-cols-2 gap-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="shipping_profile_id"
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.create.profile")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select {...field} onValueChange={onChange}>
|
|
||||||
<Select.Trigger ref={field.ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{(shippingProfiles ?? []).map((profile) => (
|
|
||||||
<Select.Item
|
|
||||||
key={profile.id}
|
|
||||||
value={profile.id}
|
|
||||||
>
|
|
||||||
{profile.name}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="provider_id"
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.edit.provider")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select {...field} onValueChange={onChange}>
|
|
||||||
<Select.Trigger ref={field.ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{fulfillment_providers.map((provider) => (
|
|
||||||
<Select.Item
|
|
||||||
key={provider.id}
|
|
||||||
value={provider.id}
|
|
||||||
>
|
|
||||||
{formatProvider(provider.id)}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 pt-8">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="enabled_in_store"
|
|
||||||
render={({ field: { value, onChange, ...field } }) => (
|
|
||||||
<Form.Item>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Form.Label>
|
|
||||||
{t("location.shippingOptions.create.enable")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
checked={!!value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</div>
|
|
||||||
<Form.Hint className="!mt-1">
|
|
||||||
{t(
|
|
||||||
"location.shippingOptions.create.enableDescription"
|
|
||||||
)}
|
|
||||||
</Form.Hint>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ProgressTabs.Content>
|
|
||||||
|
|
||||||
<ProgressTabs.Content
|
|
||||||
value={Tab.PRICING}
|
|
||||||
className="h-full w-full"
|
|
||||||
style={{ width: "100vw" }}
|
|
||||||
>
|
|
||||||
<CreateShippingOptionsPricesForm form={form} />
|
|
||||||
</ProgressTabs.Content>
|
|
||||||
</RouteFocusModal.Body>
|
|
||||||
</ProgressTabs>
|
|
||||||
</form>
|
|
||||||
</RouteFocusModal.Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-151
@@ -1,151 +0,0 @@
|
|||||||
import { CurrencyDTO, HttpTypes } from "@medusajs/types"
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
|
||||||
import { UseFormReturn } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
import { CurrencyCell } from "../../../../../components/grid/grid-cells/common/currency-cell"
|
|
||||||
import { DataGrid } from "../../../../../components/grid/data-grid"
|
|
||||||
import { DataGridMeta } from "../../../../../components/grid/types"
|
|
||||||
import { useCurrencies } from "../../../../../hooks/api/currencies"
|
|
||||||
import { useRegions } from "../../../../../hooks/api/regions"
|
|
||||||
import { useStore } from "../../../../../hooks/api/store"
|
|
||||||
|
|
||||||
const PricingCreateSchemaType = zod.record(
|
|
||||||
zod.object({
|
|
||||||
currency_prices: zod.record(zod.string().optional()),
|
|
||||||
region_prices: zod.record(zod.string().optional()),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
type PricingPricesFormProps = {
|
|
||||||
form: UseFormReturn<typeof PricingCreateSchemaType>
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ColumnType {
|
|
||||||
REGION = "region",
|
|
||||||
CURRENCY = "currency",
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnabledColumnRecord = Record<string, ColumnType>
|
|
||||||
|
|
||||||
export const CreateShippingOptionsPricesForm = ({
|
|
||||||
form,
|
|
||||||
}: PricingPricesFormProps) => {
|
|
||||||
const {
|
|
||||||
store,
|
|
||||||
isLoading: isStoreLoading,
|
|
||||||
isError: isStoreError,
|
|
||||||
error: storeError,
|
|
||||||
} = useStore()
|
|
||||||
const { currencies, isLoading: isCurrenciesLoading } = useCurrencies(
|
|
||||||
{
|
|
||||||
code: store?.supported_currency_codes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!store,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { regions } = useRegions()
|
|
||||||
|
|
||||||
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 initializing =
|
|
||||||
isStoreLoading || isCurrenciesLoading || !store || !currencies
|
|
||||||
|
|
||||||
if (isStoreError) {
|
|
||||||
throw storeError
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = useMemo(
|
|
||||||
() => [[...(currencies || []), ...(regions || [])]],
|
|
||||||
[currencies, regions]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
|
||||||
<DataGrid
|
|
||||||
columns={columns}
|
|
||||||
data={data}
|
|
||||||
isLoading={initializing}
|
|
||||||
state={form}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<
|
|
||||||
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
|
|
||||||
>()
|
|
||||||
|
|
||||||
const useColumns = ({
|
|
||||||
currencies = [],
|
|
||||||
regions = [],
|
|
||||||
}: {
|
|
||||||
currencies?: CurrencyDTO[]
|
|
||||||
regions?: HttpTypes.AdminRegion[]
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const colDefs: ColumnDef<
|
|
||||||
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
|
|
||||||
>[] = 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
|
|
||||||
}
|
|
||||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user