feat(dashboard): shipping & location (#7151)
This commit is contained in:
1 parent
c41e90fa36
commit
e42308557e
74 files changed
+2364
-1120
No files matched your search
@@ -69,7 +69,10 @@
|
|||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"revoke": "Revoke",
|
"revoke": "Revoke",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"enable": "Enable",
|
||||||
|
"disable": "Disable",
|
||||||
"complete": "Complete",
|
"complete": "Complete",
|
||||||
|
"viewDetails": "View details",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
@@ -154,6 +157,9 @@
|
|||||||
"required": "New owner is required."
|
"required": "New owner is required."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sales_channels": {
|
||||||
|
"availableIn": "Available in <0>{{x}}</0> of <1>{{y}}</1> sales channels"
|
||||||
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"domain": "Products",
|
"domain": "Products",
|
||||||
"createProductTitle": "Create Product",
|
"createProductTitle": "Create Product",
|
||||||
@@ -184,7 +190,6 @@
|
|||||||
"noMediaLabel": "The product has no associated media."
|
"noMediaLabel": "The product has no associated media."
|
||||||
},
|
},
|
||||||
"discountableHint": "When unchecked discounts will not be applied to this product.",
|
"discountableHint": "When unchecked discounts will not be applied to this product.",
|
||||||
"availableInSalesChannels": "Available in <0>{{x}}</0> of <1>{{y}}</1> sales channels",
|
|
||||||
"noSalesChannels": "Not available in any sales channels",
|
"noSalesChannels": "Not available in any sales channels",
|
||||||
"variantCount_one": "{{count}} variant",
|
"variantCount_one": "{{count}} variant",
|
||||||
"variantCount_other": "{{count}} variants",
|
"variantCount_other": "{{count}} variants",
|
||||||
@@ -623,15 +628,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shipping": {
|
"shipping": {
|
||||||
"title": "Shipping & Delivery",
|
"title": "Location & Shipping",
|
||||||
"domain": "Shipping & Delivery",
|
"domain": "Location & Shipping",
|
||||||
"description": "Choose where you ship and how much you charge for shipping at checkout. Define shipping options specific for your locations.",
|
"description": "Choose where you ship and how much you charge for shipping at checkout. Define shipping options specific for your locations.",
|
||||||
|
"createLocation": "Create location",
|
||||||
|
"createLocationDetailsHint": "Specify the details of the location.",
|
||||||
|
"deleteLocation": "Delete location",
|
||||||
"from": "Shipping from",
|
"from": "Shipping from",
|
||||||
"add": "Add shipping",
|
"add": "Add shipping",
|
||||||
"connectProvider": "Connect provider",
|
"connectProvider": "Connect provider",
|
||||||
"addZone": "Add shipping zone",
|
"addZone": "Add shipping zone",
|
||||||
"enablePickup": "Enable pickup",
|
"enablePickup": "Enable pickup",
|
||||||
"enableDelivery": "Enable delivery",
|
"enableDelivery": "Enable delivery",
|
||||||
|
"deleteLocation": {
|
||||||
|
"label": "Delete Location",
|
||||||
|
"confirm": "Are you sure you want to delete {{name}} location",
|
||||||
|
"success": "{{name}} location successfully deleted"
|
||||||
|
},
|
||||||
"noRecords": {
|
"noRecords": {
|
||||||
"action": "Add Location",
|
"action": "Add Location",
|
||||||
"title": "No inventory locations",
|
"title": "No inventory locations",
|
||||||
@@ -645,18 +658,25 @@
|
|||||||
},
|
},
|
||||||
"fulfillmentSet": {
|
"fulfillmentSet": {
|
||||||
"placeholder": "Not covered by any shipping zones.",
|
"placeholder": "Not covered by any shipping zones.",
|
||||||
|
"salesChannels": "Connected Sales Channels",
|
||||||
"delete": "Delete shipping",
|
"delete": "Delete shipping",
|
||||||
|
"disableWarning": "Are you sure that you wnat to disable \"{{name}}\"? This will delete all assocciated service zones and shipping options.",
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Add service zone for {{fulfillmentSet}}"
|
"title": "Add service zone for {{fulfillmentSet}}"
|
||||||
},
|
},
|
||||||
|
"toast": {
|
||||||
|
"disable": "\"{{name}}\" disabled"
|
||||||
|
},
|
||||||
"addZone": "Add service zone",
|
"addZone": "Add service zone",
|
||||||
"pickup": {
|
"pickup": {
|
||||||
"title": "Pickup in",
|
"title": "Pick up",
|
||||||
"enable": "Enable pickup"
|
"enable": "Enable pickup",
|
||||||
|
"offers": "Offers pick up in"
|
||||||
},
|
},
|
||||||
"delivery": {
|
"delivery": {
|
||||||
"title": "Shipping to",
|
"title": "Shipping",
|
||||||
"enable": "Enable delivery"
|
"enable": "Enable delivery",
|
||||||
|
"offers": "Offers shippping to"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serviceZone": {
|
"serviceZone": {
|
||||||
@@ -666,12 +686,21 @@
|
|||||||
"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. ",
|
"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"
|
"zoneName": "Zone name"
|
||||||
},
|
},
|
||||||
|
"edit":{
|
||||||
|
"title": "Edit Service Zone"
|
||||||
|
},
|
||||||
|
"deleteWarning": "Are you sure you want to delete \"{{name}}\". This will also delete all assocciated shipping options.",
|
||||||
|
"toast": {
|
||||||
|
"delete": "Zone \"{{name}}\" deleted successfully."
|
||||||
|
},
|
||||||
"editPrices": "Edit prices",
|
"editPrices": "Edit prices",
|
||||||
"editOption": "Edit option",
|
"editOption": "Edit option",
|
||||||
"optionsLength_one": "shipping option",
|
"optionsLength_one": "shipping option",
|
||||||
"optionsLength_other": "shipping options",
|
"optionsLength_other": "shipping options",
|
||||||
|
"returnOptionsLength_one": "return option",
|
||||||
|
"returnOptionsLength_other": "return options",
|
||||||
"shippingOptionsPlaceholder": "Not covered by any shipping options.",
|
"shippingOptionsPlaceholder": "Not covered by any shipping options.",
|
||||||
"addShippingOptions": "Add shipping options",
|
"addOption": "Add option",
|
||||||
"shippingOptions": "Shipping options",
|
"shippingOptions": "Shipping options",
|
||||||
"returnOptions": "Return options",
|
"returnOptions": "Return options",
|
||||||
"areas": {
|
"areas": {
|
||||||
@@ -683,7 +712,7 @@
|
|||||||
},
|
},
|
||||||
"shippingOptions": {
|
"shippingOptions": {
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Create shipping options for {{zone}}",
|
"title": "Create a shipping option for {{zone}}",
|
||||||
"subtitle": "General information",
|
"subtitle": "General information",
|
||||||
"description": "To start selling, all you need is a name and a price",
|
"description": "To start selling, all you need is a name and a price",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
@@ -696,7 +725,26 @@
|
|||||||
"calculated": "Calculated",
|
"calculated": "Calculated",
|
||||||
"calculatedDescription": "Shipping option's price is calculated by the fulfillment provider.",
|
"calculatedDescription": "Shipping option's price is calculated by the fulfillment provider.",
|
||||||
"profile": "Shipping profile"
|
"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": {
|
||||||
|
"title": "Connected Sales Channels",
|
||||||
|
"placeholder": "No connected channels yet.",
|
||||||
|
"connectChannels": "Connect Channels"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shippingProfile": {
|
"shippingProfile": {
|
||||||
@@ -1195,10 +1243,8 @@
|
|||||||
},
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"domain": "Locations",
|
"domain": "Locations",
|
||||||
"createLocation": "Create location",
|
|
||||||
"editLocation": "Edit location",
|
"editLocation": "Edit location",
|
||||||
"addSalesChannels": "Add sales channels",
|
"addSalesChannels": "Add sales channels",
|
||||||
"detailsHint": "Specify the details of the location.",
|
|
||||||
"noLocationsFound": "No locations found",
|
"noLocationsFound": "No locations found",
|
||||||
"selectLocations": "Select locations that stock the item.",
|
"selectLocations": "Select locations that stock the item.",
|
||||||
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.",
|
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.",
|
||||||
@@ -1388,6 +1434,7 @@
|
|||||||
"scheduled": "Scheduled",
|
"scheduled": "Scheduled",
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled"
|
"disabled": "Disabled"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
|||||||
@@ -144,11 +144,6 @@ const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
|
|||||||
label: t("pricing.domain"),
|
label: t("pricing.domain"),
|
||||||
to: "/pricing",
|
to: "/pricing",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: <Envelope />,
|
|
||||||
label: t("shipping.domain"),
|
|
||||||
to: "/shipping",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export const NavItem = ({
|
|||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
|
||||||
{
|
{
|
||||||
"bg-ui-bg-base hover:bg-ui-bg-base-hover shadow-elevation-card-rest":
|
"bg-ui-bg-base hover:bg-ui-bg-base-hover shadow-elevation-card-rest":
|
||||||
location.pathname.startsWith(to),
|
location.pathname === to ||
|
||||||
|
location.pathname.startsWith(to + "/"), // TODO: utilise `NavLink` and `end` prop instead of this manual check
|
||||||
"max-md:hidden": items && items.length > 0,
|
"max-md:hidden": items && items.length > 0,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
+4
@@ -57,6 +57,10 @@ const useSettingRoutes = (): NavItemProps[] => {
|
|||||||
label: t("shippingProfile.domain"),
|
label: t("shippingProfile.domain"),
|
||||||
to: "/settings/shipping-profiles",
|
to: "/settings/shipping-profiles",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("shipping.domain"),
|
||||||
|
to: "/settings/shipping",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||||
|
import { client } from "../../lib/client"
|
||||||
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
|
||||||
|
const FULFILLMENT_PROVIDERS_QUERY_KEY = "f_providers" as const
|
||||||
|
export const fulfillmentProvidersQueryKeys = queryKeysFactory(
|
||||||
|
FULFILLMENT_PROVIDERS_QUERY_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useFulfillmentProviders = (
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<any, Error, any, QueryKey>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.fulfillmentProviders.list(query),
|
||||||
|
queryKey: fulfillmentProvidersQueryKeys.list(query),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
@@ -1,14 +1,39 @@
|
|||||||
import { useMutation, UseMutationOptions } from "@tanstack/react-query"
|
import {
|
||||||
|
QueryKey,
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions,
|
||||||
|
useQuery,
|
||||||
|
UseQueryOptions,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ShippingOptionDeleteRes,
|
ShippingOptionDeleteRes,
|
||||||
ShippingOptionRes,
|
ShippingOptionRes,
|
||||||
} from "../../types/api-responses"
|
} from "../../types/api-responses"
|
||||||
import { CreateShippingOptionReq } from "../../types/api-payloads"
|
import {
|
||||||
|
CreateShippingOptionReq,
|
||||||
|
UpdateShippingOptionReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
import { stockLocationsQueryKeys } from "./stock-locations"
|
import { stockLocationsQueryKeys } from "./stock-locations"
|
||||||
import { queryClient } from "../../lib/medusa"
|
import { queryClient } from "../../lib/medusa"
|
||||||
import { client } from "../../lib/client"
|
import { client } from "../../lib/client"
|
||||||
|
|
||||||
|
export const useShippingOptions = (
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<any, Error, any, QueryKey>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.shippingOptions.list(query),
|
||||||
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
export const useCreateShippingOptions = (
|
export const useCreateShippingOptions = (
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
ShippingOptionRes,
|
ShippingOptionRes,
|
||||||
@@ -20,9 +45,28 @@ export const useCreateShippingOptions = (
|
|||||||
mutationFn: (payload) => client.shippingOptions.create(payload),
|
mutationFn: (payload) => client.shippingOptions.create(payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
})
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateShippingOptions = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
ShippingOptionRes,
|
||||||
|
Error,
|
||||||
|
UpdateShippingOptionReq
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.shippingOptions.update(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
|
})
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
@@ -37,7 +81,7 @@ export const useDeleteShippingOption = (
|
|||||||
mutationFn: () => client.shippingOptions.delete(optionId),
|
mutationFn: () => client.shippingOptions.delete(optionId),
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.all,
|
||||||
})
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
CreateFulfillmentSetReq,
|
CreateFulfillmentSetReq,
|
||||||
CreateServiceZoneReq,
|
CreateServiceZoneReq,
|
||||||
CreateStockLocationReq,
|
CreateStockLocationReq,
|
||||||
|
UpdateServiceZoneReq,
|
||||||
UpdateStockLocationReq,
|
UpdateStockLocationReq,
|
||||||
|
UpdateStockLocationSalesChannelsReq,
|
||||||
} from "../../types/api-payloads"
|
} from "../../types/api-payloads"
|
||||||
import {
|
import {
|
||||||
FulfillmentSetDeleteRes,
|
FulfillmentSetDeleteRes,
|
||||||
@@ -22,6 +24,7 @@ import {
|
|||||||
StockLocationListRes,
|
StockLocationListRes,
|
||||||
StockLocationRes,
|
StockLocationRes,
|
||||||
} from "../../types/api-responses"
|
} from "../../types/api-responses"
|
||||||
|
import { salesChannelsQueryKeys } from "./sales-channels"
|
||||||
|
|
||||||
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(
|
||||||
@@ -38,7 +41,7 @@ export const useStockLocation = (
|
|||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.stockLocations.retrieve(id, query),
|
queryFn: () => client.stockLocations.retrieve(id, query),
|
||||||
queryKey: stockLocationsQueryKeys.detail(id),
|
queryKey: stockLocationsQueryKeys.detail(id, query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -90,7 +93,7 @@ export const useUpdateStockLocation = (
|
|||||||
mutationFn: (payload) => client.stockLocations.update(id, payload),
|
mutationFn: (payload) => client.stockLocations.update(id, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.detail(id),
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
@@ -102,6 +105,30 @@ export const useUpdateStockLocation = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUpdateStockLocationSalesChannels = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
StockLocationRes,
|
||||||
|
Error,
|
||||||
|
UpdateStockLocationSalesChannelsReq
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) =>
|
||||||
|
client.stockLocations.updateSalesChannels(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useDeleteStockLocation = (
|
export const useDeleteStockLocation = (
|
||||||
id: string,
|
id: string,
|
||||||
options?: UseMutationOptions<StockLocationDeleteRes, Error, void>
|
options?: UseMutationOptions<StockLocationDeleteRes, Error, void>
|
||||||
@@ -134,7 +161,7 @@ export const useCreateFulfillmentSet = (
|
|||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.detail(locationId),
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
})
|
})
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
@@ -152,7 +179,33 @@ export const useCreateServiceZone = (
|
|||||||
client.stockLocations.createServiceZone(fulfillmentSetId, payload),
|
client.stockLocations.createServiceZone(fulfillmentSetId, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.detail(locationId),
|
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({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
@@ -173,6 +226,9 @@ export const useDeleteFulfillmentSet = (
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
@@ -191,6 +247,9 @@ export const useDeleteServiceZone = (
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.details(),
|
||||||
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
|
|
||||||
import { queryKeysFactory } from "medusa-react"
|
|
||||||
import { client } from "../../lib/client"
|
import { client } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/medusa"
|
import { queryClient } from "../../lib/medusa"
|
||||||
import { UpdateStoreReq } from "../../types/api-payloads"
|
import { UpdateStoreReq } from "../../types/api-payloads"
|
||||||
import { StoreRes } from "../../types/api-responses"
|
import { StoreRes } from "../../types/api-responses"
|
||||||
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
|
||||||
const STORE_QUERY_KEY = "store" as const
|
const STORE_QUERY_KEY = "store" as const
|
||||||
const storeQueryKeys = queryKeysFactory(STORE_QUERY_KEY)
|
const storeQueryKeys = queryKeysFactory(STORE_QUERY_KEY)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { productTypes } from "./product-types"
|
|||||||
import { products } from "./products"
|
import { products } from "./products"
|
||||||
import { promotions } from "./promotions"
|
import { promotions } from "./promotions"
|
||||||
import { regions } from "./regions"
|
import { regions } from "./regions"
|
||||||
|
import { fulfillmentProviders } from "./fulfillment-providers"
|
||||||
import { reservations } from "./reservations"
|
import { reservations } from "./reservations"
|
||||||
import { salesChannels } from "./sales-channels"
|
import { salesChannels } from "./sales-channels"
|
||||||
import { shippingOptions } from "./shipping-options"
|
import { shippingOptions } from "./shipping-options"
|
||||||
@@ -47,6 +48,7 @@ export const client = {
|
|||||||
invites: invites,
|
invites: invites,
|
||||||
inventoryItems: inventoryItems,
|
inventoryItems: inventoryItems,
|
||||||
reservations: reservations,
|
reservations: reservations,
|
||||||
|
fulfillmentProviders: fulfillmentProviders,
|
||||||
products: products,
|
products: products,
|
||||||
productTypes: productTypes,
|
productTypes: productTypes,
|
||||||
priceLists: priceLists,
|
priceLists: priceLists,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { getRequest } from "./common"
|
||||||
|
import { FulfillmentProvidersListRes } from "../../types/api-responses"
|
||||||
|
|
||||||
|
async function listFulfillmentProviders(query?: Record<string, any>) {
|
||||||
|
return getRequest<FulfillmentProvidersListRes>(
|
||||||
|
`/admin/fulfillment-providers`,
|
||||||
|
query
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fulfillmentProviders = {
|
||||||
|
list: listFulfillmentProviders,
|
||||||
|
}
|
||||||
@@ -1,14 +1,32 @@
|
|||||||
import { deleteRequest, postRequest } from "./common"
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
import {
|
import {
|
||||||
ShippingOptionDeleteRes,
|
ShippingOptionDeleteRes,
|
||||||
|
ShippingOptionListRes,
|
||||||
ShippingOptionRes,
|
ShippingOptionRes,
|
||||||
} from "../../types/api-responses"
|
} from "../../types/api-responses"
|
||||||
import { CreateShippingOptionReq } from "../../types/api-payloads"
|
import {
|
||||||
|
CreateShippingOptionReq,
|
||||||
|
UpdateShippingOptionReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
|
|
||||||
async function createShippingOptions(payload: CreateShippingOptionReq) {
|
async function createShippingOptions(payload: CreateShippingOptionReq) {
|
||||||
return postRequest<ShippingOptionRes>(`/admin/shipping-options`, payload)
|
return postRequest<ShippingOptionRes>(`/admin/shipping-options`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateShippingOptions(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateShippingOptionReq
|
||||||
|
) {
|
||||||
|
return postRequest<ShippingOptionRes>(
|
||||||
|
`/admin/shipping-options/${id}`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listShippingOptions(query?: Record<string, any>) {
|
||||||
|
return getRequest<ShippingOptionListRes>(`/admin/shipping-options`, query)
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteShippingOption(optionId: string) {
|
async function deleteShippingOption(optionId: string) {
|
||||||
return deleteRequest<ShippingOptionDeleteRes>(
|
return deleteRequest<ShippingOptionDeleteRes>(
|
||||||
`/admin/shipping-options/${optionId}`
|
`/admin/shipping-options/${optionId}`
|
||||||
@@ -17,5 +35,7 @@ async function deleteShippingOption(optionId: string) {
|
|||||||
|
|
||||||
export const shippingOptions = {
|
export const shippingOptions = {
|
||||||
create: createShippingOptions,
|
create: createShippingOptions,
|
||||||
|
update: updateShippingOptions,
|
||||||
delete: deleteShippingOption,
|
delete: deleteShippingOption,
|
||||||
|
list: listShippingOptions,
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ import {
|
|||||||
CreateFulfillmentSetReq,
|
CreateFulfillmentSetReq,
|
||||||
CreateServiceZoneReq,
|
CreateServiceZoneReq,
|
||||||
CreateStockLocationReq,
|
CreateStockLocationReq,
|
||||||
|
UpdateServiceZoneReq,
|
||||||
UpdateStockLocationReq,
|
UpdateStockLocationReq,
|
||||||
|
UpdateStockLocationSalesChannelsReq,
|
||||||
} from "../../types/api-payloads"
|
} from "../../types/api-payloads"
|
||||||
import {
|
import {
|
||||||
FulfillmentSetDeleteRes,
|
FulfillmentSetDeleteRes,
|
||||||
@@ -45,6 +47,17 @@ async function createServiceZone(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateServiceZone(
|
||||||
|
fulfillmentSetId: string,
|
||||||
|
serviceZoneId: string,
|
||||||
|
payload: UpdateServiceZoneReq
|
||||||
|
) {
|
||||||
|
return postRequest<StockLocationRes>(
|
||||||
|
`/admin/fulfillment-sets/${fulfillmentSetId}/service-zones/${serviceZoneId}`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function updateStockLocation(
|
async function updateStockLocation(
|
||||||
id: string,
|
id: string,
|
||||||
payload: UpdateStockLocationReq
|
payload: UpdateStockLocationReq
|
||||||
@@ -52,6 +65,16 @@ async function updateStockLocation(
|
|||||||
return postRequest<StockLocationRes>(`/admin/stock-locations/${id}`, payload)
|
return postRequest<StockLocationRes>(`/admin/stock-locations/${id}`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateStockLocationSalesChannels(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateStockLocationSalesChannelsReq
|
||||||
|
) {
|
||||||
|
return postRequest<StockLocationRes>(
|
||||||
|
`/admin/stock-locations/${id}/sales-channels`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteStockLocation(id: string) {
|
async function deleteStockLocation(id: string) {
|
||||||
return deleteRequest<StockLocationDeleteRes>(`/admin/stock-locations/${id}`)
|
return deleteRequest<StockLocationDeleteRes>(`/admin/stock-locations/${id}`)
|
||||||
}
|
}
|
||||||
@@ -74,8 +97,10 @@ export const stockLocations = {
|
|||||||
create: createStockLocation,
|
create: createStockLocation,
|
||||||
update: updateStockLocation,
|
update: updateStockLocation,
|
||||||
delete: deleteStockLocation,
|
delete: deleteStockLocation,
|
||||||
|
updateSalesChannels: updateStockLocationSalesChannels,
|
||||||
createFulfillmentSet,
|
createFulfillmentSet,
|
||||||
deleteFulfillmentSet,
|
deleteFulfillmentSet,
|
||||||
createServiceZone,
|
createServiceZone,
|
||||||
deleteServiceZone,
|
deleteServiceZone,
|
||||||
|
updateServiceZone,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ShippingOptionDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
export function isReturnOption(shippingOption: ShippingOptionDTO) {
|
||||||
|
return !!shippingOption.rules?.find(
|
||||||
|
(r) =>
|
||||||
|
r.attribute === "is_return" && r.value === "true" && r.operator === "eq"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOptionEnabledInStore(shippingOption: ShippingOptionDTO) {
|
||||||
|
return !!shippingOption.rules?.find(
|
||||||
|
(r) =>
|
||||||
|
r.attribute === "enabled_in_store" &&
|
||||||
|
r.value === "true" &&
|
||||||
|
r.operator === "eq"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
AdminCollectionsRes,
|
AdminCollectionsRes,
|
||||||
AdminProductsRes,
|
AdminProductsRes,
|
||||||
AdminPromotionRes,
|
|
||||||
AdminRegionsRes,
|
AdminRegionsRes,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
|
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
|
||||||
@@ -297,44 +296,6 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "shipping",
|
|
||||||
lazy: () => import("../../v2-routes/shipping/locations-list"),
|
|
||||||
handle: {
|
|
||||||
crumb: () => "Shipping",
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "location/:location_id",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "fulfillment-set/:fset_id",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "service-zones/create",
|
|
||||||
lazy: () =>
|
|
||||||
import(
|
|
||||||
"../../v2-routes/shipping/service-zone-create"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "service-zone/:zone_id",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "shipping-options/create",
|
|
||||||
lazy: () =>
|
|
||||||
import(
|
|
||||||
"../../v2-routes/shipping/shipping-options-create"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/customers",
|
path: "/customers",
|
||||||
handle: {
|
handle: {
|
||||||
@@ -641,44 +602,6 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "locations",
|
|
||||||
element: <Outlet />,
|
|
||||||
handle: {
|
|
||||||
crumb: () => "Locations",
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
lazy: () => import("../../v2-routes/locations/location-list"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "create",
|
|
||||||
lazy: () =>
|
|
||||||
import("../../v2-routes/locations/location-create"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":id",
|
|
||||||
lazy: () => import("../../v2-routes/locations/location-detail"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "edit",
|
|
||||||
lazy: () =>
|
|
||||||
import("../../v2-routes/locations/location-edit"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "add-sales-channels",
|
|
||||||
lazy: () =>
|
|
||||||
import(
|
|
||||||
"../../v2-routes/locations/location-add-sales-channels"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "sales-channels",
|
path: "sales-channels",
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
@@ -727,6 +650,89 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "shipping",
|
||||||
|
element: <Outlet />,
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Location & Shipping",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
lazy: () => import("../../v2-routes/shipping/location-list"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () => import("../../v2-routes/shipping/location-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":location_id",
|
||||||
|
lazy: () => import("../../v2-routes/shipping/location-details"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/shipping/location-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "sales-channels/edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/shipping/location-add-sales-channels"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "fulfillment-set/:fset_id",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "service-zones/create",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/shipping/service-zone-create"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "service-zone/:zone_id",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/shipping/service-zone-edit"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shipping-option",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/shipping/shipping-options-create"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":so_id",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/shipping/shipping-option-edit"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "workflows",
|
path: "workflows",
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
CreateShippingProfileDTO,
|
CreateShippingProfileDTO,
|
||||||
CreateStockLocationInput,
|
CreateStockLocationInput,
|
||||||
InventoryNext,
|
InventoryNext,
|
||||||
|
ShippingOptionDTO,
|
||||||
UpdateApiKeyDTO,
|
UpdateApiKeyDTO,
|
||||||
UpdateCampaignDTO,
|
UpdateCampaignDTO,
|
||||||
UpdateCustomerDTO,
|
UpdateCustomerDTO,
|
||||||
@@ -28,6 +29,8 @@ import {
|
|||||||
UpdatePromotionRuleDTO,
|
UpdatePromotionRuleDTO,
|
||||||
UpdateRegionDTO,
|
UpdateRegionDTO,
|
||||||
UpdateSalesChannelDTO,
|
UpdateSalesChannelDTO,
|
||||||
|
UpdateServiceZoneDTO,
|
||||||
|
UpdateShippingOptionDTO,
|
||||||
UpdateStockLocationInput,
|
UpdateStockLocationInput,
|
||||||
UpdateStoreDTO,
|
UpdateStoreDTO,
|
||||||
UpdateUserDTO,
|
UpdateUserDTO,
|
||||||
@@ -66,11 +69,17 @@ export type CreateInviteReq = CreateInviteDTO
|
|||||||
// Stock Locations
|
// Stock Locations
|
||||||
export type CreateStockLocationReq = CreateStockLocationInput
|
export type CreateStockLocationReq = CreateStockLocationInput
|
||||||
export type UpdateStockLocationReq = UpdateStockLocationInput
|
export type UpdateStockLocationReq = UpdateStockLocationInput
|
||||||
|
export type UpdateStockLocationSalesChannelsReq = {
|
||||||
|
add: string[]
|
||||||
|
remove: string[]
|
||||||
|
}
|
||||||
export type CreateFulfillmentSetReq = CreateFulfillmentSetDTO
|
export type CreateFulfillmentSetReq = CreateFulfillmentSetDTO
|
||||||
export type CreateServiceZoneReq = CreateServiceZoneDTO
|
export type CreateServiceZoneReq = CreateServiceZoneDTO
|
||||||
|
export type UpdateServiceZoneReq = UpdateServiceZoneDTO
|
||||||
|
|
||||||
// Shipping Options
|
// Shipping Options
|
||||||
export type CreateShippingOptionReq = CreateShippingOptionDTO
|
export type CreateShippingOptionReq = CreateShippingOptionDTO
|
||||||
|
export type UpdateShippingOptionReq = UpdateShippingOptionDTO
|
||||||
|
|
||||||
// Shipping Profile
|
// Shipping Profile
|
||||||
export type CreateShippingProfileReq = CreateShippingProfileDTO
|
export type CreateShippingProfileReq = CreateShippingProfileDTO
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CampaignDTO,
|
CampaignDTO,
|
||||||
CurrencyDTO,
|
CurrencyDTO,
|
||||||
CustomerGroupDTO,
|
CustomerGroupDTO,
|
||||||
|
FulfillmentProviderDTO,
|
||||||
InventoryNext,
|
InventoryNext,
|
||||||
InviteDTO,
|
InviteDTO,
|
||||||
PaymentProviderDTO,
|
PaymentProviderDTO,
|
||||||
@@ -140,9 +141,17 @@ export type StockLocationDeleteRes = DeleteRes
|
|||||||
export type FulfillmentSetDeleteRes = DeleteRes
|
export type FulfillmentSetDeleteRes = DeleteRes
|
||||||
export type ServiceZoneDeleteRes = DeleteRes
|
export type ServiceZoneDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Fulfillment providers
|
||||||
|
export type FulfillmentProvidersListRes = {
|
||||||
|
fulfillment_providers: FulfillmentProviderDTO
|
||||||
|
} & ListRes
|
||||||
|
|
||||||
// Shipping options
|
// Shipping options
|
||||||
export type ShippingOptionRes = { shipping_option: ShippingOptionDTO }
|
export type ShippingOptionRes = { shipping_option: ShippingOptionDTO }
|
||||||
export type ShippingOptionDeleteRes = DeleteRes
|
export type ShippingOptionDeleteRes = DeleteRes
|
||||||
|
export type ShippingOptionListRes = {
|
||||||
|
shipping_options: ShippingOptionDTO[]
|
||||||
|
} & ListRes
|
||||||
|
|
||||||
// Shipping profile
|
// Shipping profile
|
||||||
export type ShippingProfileRes = { shipping_profile: ShippingProfileDTO }
|
export type ShippingProfileRes = { shipping_profile: ShippingProfileDTO }
|
||||||
|
|||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
|
||||||
|
|
||||||
export const LocationAddSalesChannels = () => {
|
|
||||||
// We need a batch add sales channels endpoint
|
|
||||||
|
|
||||||
return <RouteFocusModal></RouteFocusModal>
|
|
||||||
}
|
|
||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
|
||||||
import type { StockLocationAddressDTO } from "@medusajs/types"
|
|
||||||
import { Container, Heading, Text, clx } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { ExtendedStockLocationDTO } from "../../../../../types/api-responses"
|
|
||||||
|
|
||||||
type LocationGeneralSectionProps = {
|
|
||||||
location: ExtendedStockLocationDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LocationGeneralSection = ({
|
|
||||||
location,
|
|
||||||
}: LocationGeneralSectionProps) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="divide-y p-0">
|
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
|
||||||
<Heading>{location.name}</Heading>
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: `edit`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.address")}
|
|
||||||
</Text>
|
|
||||||
<AddressDisplay address={location.address} />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text
|
|
||||||
size="small"
|
|
||||||
weight="plus"
|
|
||||||
leading="compact"
|
|
||||||
className={clx({
|
|
||||||
"text-ui-fg-subtle": !location.address?.company,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t("fields.company")}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
size="small"
|
|
||||||
leading="compact"
|
|
||||||
className={clx({
|
|
||||||
"text-ui-fg-subtle": !location.address?.company,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{location.address?.company || "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.phone")}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
size="small"
|
|
||||||
leading="compact"
|
|
||||||
className={clx({
|
|
||||||
"text-ui-fg-subtle": !location.address?.phone,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{location.address?.phone || "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddressDisplay = ({
|
|
||||||
address,
|
|
||||||
}: {
|
|
||||||
address: StockLocationAddressDTO | undefined
|
|
||||||
}) => {
|
|
||||||
if (!address) {
|
|
||||||
return (
|
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
|
||||||
-
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { address_1, address_2, city, province, postal_code, country_code } =
|
|
||||||
address
|
|
||||||
|
|
||||||
const addressParts = [
|
|
||||||
address_1,
|
|
||||||
address_2,
|
|
||||||
`${city ? city + " " : ""}${province ? province + " " : ""}${postal_code}`,
|
|
||||||
country_code.toUpperCase(),
|
|
||||||
]
|
|
||||||
|
|
||||||
const addressString = addressParts
|
|
||||||
.filter((part) => part !== null && part !== undefined && part.trim() !== "")
|
|
||||||
.join(", ")
|
|
||||||
|
|
||||||
return <Text size="small">{addressString}</Text>
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./location-sales-channel-section"
|
|
||||||
-151
@@ -1,151 +0,0 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
|
||||||
import { SalesChannelDTO } from "@medusajs/types"
|
|
||||||
import { Button, Container, Heading, toast, usePrompt } from "@medusajs/ui"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useAdminRemoveLocationFromSalesChannel } from "medusa-react"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Link } from "react-router-dom"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
|
||||||
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
|
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
|
||||||
import { ExtendedStockLocationDTO } from "../../../../../types/api-responses"
|
|
||||||
|
|
||||||
type LocationSalesChannelSectionProps = {
|
|
||||||
location: ExtendedStockLocationDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
|
||||||
|
|
||||||
export const LocationSalesChannelSection = ({
|
|
||||||
location,
|
|
||||||
}: LocationSalesChannelSectionProps) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const salesChannels = location.sales_channels
|
|
||||||
const count = location.sales_channels?.length || 0
|
|
||||||
const columns = useColumns()
|
|
||||||
|
|
||||||
const { table } = useDataTable({
|
|
||||||
data: salesChannels ?? [],
|
|
||||||
columns,
|
|
||||||
count,
|
|
||||||
pageSize: PAGE_SIZE,
|
|
||||||
enablePagination: true,
|
|
||||||
getRowId: (row) => row.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="divide-y p-0">
|
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
|
||||||
<Heading level="h2">{t("salesChannels.domain")}</Heading>
|
|
||||||
<Link to={"add-sales-channels"}>
|
|
||||||
<Button size="small" variant="secondary">
|
|
||||||
{t("locations.addSalesChannels")}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<DataTable
|
|
||||||
table={table}
|
|
||||||
pageSize={PAGE_SIZE}
|
|
||||||
count={count}
|
|
||||||
columns={columns}
|
|
||||||
navigateTo={(row) => row.id}
|
|
||||||
pagination
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SalesChannelActions = ({
|
|
||||||
salesChannel,
|
|
||||||
locationId,
|
|
||||||
}: {
|
|
||||||
salesChannel: SalesChannelDTO
|
|
||||||
locationId: string
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const prompt = usePrompt()
|
|
||||||
|
|
||||||
const { mutateAsync } = useAdminRemoveLocationFromSalesChannel()
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const res = await prompt({
|
|
||||||
title: t("general.areYouSure"),
|
|
||||||
description: t("locations.removeSalesChannelsWarning", { count: 1 }),
|
|
||||||
confirmText: t("actions.delete"),
|
|
||||||
cancelText: t("actions.cancel"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mutateAsync({
|
|
||||||
location_id: locationId,
|
|
||||||
sales_channel_id: salesChannel.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
toast.success(t("general.success"), {
|
|
||||||
description: t("locations.toast.removeChannel"),
|
|
||||||
dismissLabel: t("actions.close"),
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("general.error"), {
|
|
||||||
description: e.message,
|
|
||||||
dismissLabel: t("actions.close"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Trash />,
|
|
||||||
label: t("actions.delete"),
|
|
||||||
onClick: handleDelete,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<SalesChannelDTO>()
|
|
||||||
|
|
||||||
const useColumns = () => {
|
|
||||||
const base = useSalesChannelTableColumns()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
...base,
|
|
||||||
columnHelper.display({
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const { locationId } = table.options.meta as {
|
|
||||||
locationId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SalesChannelActions
|
|
||||||
salesChannel={row.original}
|
|
||||||
locationId={locationId}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[base]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LocationDetail as Component } from "./location-detail"
|
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
import { Outlet, useParams } from "react-router-dom"
|
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
|
||||||
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
|
||||||
import { LocationGeneralSection } from "./components/location-general-section"
|
|
||||||
import { LocationSalesChannelSection } from "./components/location-sales-channel-section"
|
|
||||||
|
|
||||||
export const LocationDetail = () => {
|
|
||||||
const { id } = useParams()
|
|
||||||
const {
|
|
||||||
stock_location,
|
|
||||||
isPending: isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useStockLocation(id!, {
|
|
||||||
fields: "*address,*sales_channels",
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isLoading || !stock_location) {
|
|
||||||
return <div>Loading...</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<LocationGeneralSection location={stock_location} />
|
|
||||||
<LocationSalesChannelSection location={stock_location} />
|
|
||||||
<JsonViewSection data={stock_location} />
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./locations-list-table"
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
|
||||||
import { usePrompt } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { useDeleteStockLocation } from "../../../../../hooks/api/stock-locations"
|
|
||||||
import { ExtendedStockLocationDTO } from "../../../../../types/api-responses"
|
|
||||||
|
|
||||||
export const LocationRowActions = ({
|
|
||||||
location,
|
|
||||||
}: {
|
|
||||||
location: ExtendedStockLocationDTO
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const prompt = usePrompt()
|
|
||||||
const { mutateAsync } = useDeleteStockLocation(location.id)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const res = await prompt({
|
|
||||||
title: t("general.areYouSure"),
|
|
||||||
description: t("locations.deleteLocationWarning", {
|
|
||||||
name: location.name,
|
|
||||||
}),
|
|
||||||
verificationText: location.name,
|
|
||||||
verificationInstruction: t("general.typeToConfirm"),
|
|
||||||
confirmText: t("actions.delete"),
|
|
||||||
cancelText: t("actions.cancel"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: `/settings/locations/${location.id}/edit`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Trash />,
|
|
||||||
label: t("actions.delete"),
|
|
||||||
onClick: handleDelete,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
import { Button, Container, Heading } from "@medusajs/ui"
|
|
||||||
import { Link } from "react-router-dom"
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
|
||||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
|
||||||
import { useLocationTableColumns } from "./use-location-table-columns"
|
|
||||||
import { useLocationTableQuery } from "./use-location-table-query"
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
|
||||||
|
|
||||||
export const LocationsListTable = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const { raw, searchParams } = useLocationTableQuery({ pageSize: PAGE_SIZE })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note: The endpoint is bugged and does not return count, causing the table to not render
|
|
||||||
* any rows.
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
stock_locations = [],
|
|
||||||
count,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useStockLocations({
|
|
||||||
...searchParams,
|
|
||||||
fields: "*address",
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns = useLocationTableColumns()
|
|
||||||
|
|
||||||
const { table } = useDataTable({
|
|
||||||
data: stock_locations,
|
|
||||||
columns,
|
|
||||||
count,
|
|
||||||
enablePagination: true,
|
|
||||||
getRowId: (row) => row.id,
|
|
||||||
pageSize: PAGE_SIZE,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="divide-y p-0">
|
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
|
||||||
<Heading level="h2">{t("locations.domain")}</Heading>
|
|
||||||
<div>
|
|
||||||
<Button size="small" variant="secondary" asChild>
|
|
||||||
<Link to="create">{t("actions.create")}</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DataTable
|
|
||||||
table={table}
|
|
||||||
pageSize={PAGE_SIZE}
|
|
||||||
count={count || 1}
|
|
||||||
columns={columns}
|
|
||||||
navigateTo={(row) => row.id}
|
|
||||||
// TODO: revisit loader - on query change this will cause unmounting of the table, rendering loader briefly and again rendering table which will make search input unfocused
|
|
||||||
// isLoading={isLoading}
|
|
||||||
queryObject={raw}
|
|
||||||
pagination
|
|
||||||
search
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
import { createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { ExtendedStockLocationDTO } from "../../../../../types/api-responses"
|
|
||||||
import { LocationRowActions } from "./location-row-actions"
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ExtendedStockLocationDTO>()
|
|
||||||
|
|
||||||
export const useLocationTableColumns = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: (cell) => cell.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("address", {
|
|
||||||
header: t("fields.address"),
|
|
||||||
cell: (cell) => {
|
|
||||||
const value = cell.getValue()
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return "-"
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${value.address_1}${value.city ? `, ${value.city}` : ""}`
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row }) => <LocationRowActions location={row.original} />,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
|
||||||
|
|
||||||
export const useLocationTableQuery = ({
|
|
||||||
pageSize = 20,
|
|
||||||
prefix,
|
|
||||||
}: {
|
|
||||||
pageSize?: number
|
|
||||||
prefix?: string
|
|
||||||
}) => {
|
|
||||||
const raw = useQueryParams(["q", "offset"], prefix)
|
|
||||||
|
|
||||||
const searchParams = {
|
|
||||||
limit: pageSize,
|
|
||||||
offset: raw.offset,
|
|
||||||
q: raw.q,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
searchParams,
|
|
||||||
raw,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LocationList as Component } from "./location-list"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Outlet } from "react-router-dom"
|
|
||||||
import { LocationsListTable } from "./components/locations-list-table"
|
|
||||||
|
|
||||||
export const LocationList = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<LocationsListTable />
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
-1
@@ -83,7 +83,7 @@ export const ProductSalesChannelSection = ({
|
|||||||
<div>
|
<div>
|
||||||
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="products.availableInSalesChannels"
|
i18nKey="sales_channels.availableIn"
|
||||||
values={{
|
values={{
|
||||||
x: availableInSalesChannels.length,
|
x: availableInSalesChannels.length,
|
||||||
y: count ?? 0,
|
y: count ?? 0,
|
||||||
|
|||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
import { SalesChannel } from "@medusajs/medusa"
|
||||||
|
import { SalesChannelDTO, StockLocationDTO } from "@medusajs/types"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import { Button, Checkbox } from "@medusajs/ui"
|
||||||
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
|
||||||
|
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
|
||||||
|
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||||
|
import { useUpdateStockLocationSalesChannels } from "../../../../../hooks/api/stock-locations"
|
||||||
|
|
||||||
|
type EditSalesChannelsFormProps = {
|
||||||
|
location: StockLocationDTO & { sales_channels: SalesChannelDTO[] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditSalesChannelsSchema = zod.object({
|
||||||
|
sales_channels: zod.array(zod.string()).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
export const LocationEditSalesChannelsForm = ({
|
||||||
|
location,
|
||||||
|
}: EditSalesChannelsFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditSalesChannelsSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
sales_channels: location.sales_channels?.map((sc) => sc.id) ?? [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditSalesChannelsSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { setValue } = form
|
||||||
|
|
||||||
|
const initialState =
|
||||||
|
location.sales_channels?.reduce((acc, curr) => {
|
||||||
|
acc[curr.id] = true
|
||||||
|
return acc
|
||||||
|
}, {} as RowSelectionState) ?? {}
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] =
|
||||||
|
useState<RowSelectionState>(initialState)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ids = Object.keys(rowSelection)
|
||||||
|
setValue("sales_channels", ids, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}, [rowSelection, setValue])
|
||||||
|
|
||||||
|
const { searchParams, raw } = useSalesChannelTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { sales_channels, count, isLoading, isError, error } = useSalesChannels(
|
||||||
|
{
|
||||||
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const filters = useSalesChannelTableFilters()
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: sales_channels ?? [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater: setRowSelection,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isPending: isMutating } =
|
||||||
|
useUpdateStockLocationSalesChannels(location.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const originalIds = location.sales_channels.map((sc) => sc.id)
|
||||||
|
|
||||||
|
const arr = data.sales_channels ?? []
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
add: arr.filter((i) => !originalIds.includes(i)),
|
||||||
|
remove: originalIds.filter((i) => !arr.includes(i)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button size="small" isLoading={isMutating} onClick={handleSubmit}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body>
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
isLoading={isLoading}
|
||||||
|
count={count}
|
||||||
|
filters={filters}
|
||||||
|
search
|
||||||
|
pagination
|
||||||
|
orderBy={["name", "created_at", "updated_at"]}
|
||||||
|
queryObject={raw}
|
||||||
|
layout="fill"
|
||||||
|
/>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<SalesChannel>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const columns = useSalesChannelTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...columns,
|
||||||
|
],
|
||||||
|
[columns]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-sales-channels-form"
|
||||||
File renamed without changes.
+30
@@ -0,0 +1,30 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+3
-3
@@ -55,7 +55,7 @@ export const CreateLocationForm = () => {
|
|||||||
address: values.address,
|
address: values.address,
|
||||||
})
|
})
|
||||||
|
|
||||||
handleSuccess("/settings/locations")
|
handleSuccess("/settings/shipping")
|
||||||
|
|
||||||
toast.success(t("general.success"), {
|
toast.success(t("general.success"), {
|
||||||
description: t("locations.toast.create"),
|
description: t("locations.toast.create"),
|
||||||
@@ -92,10 +92,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("locations.createLocation")}
|
{t("shipping.createLocation")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
{t("locations.detailsHint")}
|
{t("shipping.createLocationDetailsHint")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+640
@@ -0,0 +1,640 @@
|
|||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import {
|
||||||
|
FulfillmentSetDTO,
|
||||||
|
ServiceZoneDTO,
|
||||||
|
ShippingOptionDTO,
|
||||||
|
StockLocationDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ChevronDownMini,
|
||||||
|
CurrencyDollar,
|
||||||
|
Map,
|
||||||
|
PencilSquare,
|
||||||
|
Plus,
|
||||||
|
Trash,
|
||||||
|
} from "@medusajs/icons"
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Text,
|
||||||
|
toast,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { countries as staticCountries } from "../../../../../lib/countries"
|
||||||
|
import {
|
||||||
|
useCreateFulfillmentSet,
|
||||||
|
useDeleteFulfillmentSet,
|
||||||
|
useDeleteServiceZone,
|
||||||
|
useDeleteStockLocation,
|
||||||
|
} from "../../../../../hooks/api/stock-locations"
|
||||||
|
import { useDeleteShippingOption } from "../../../../../hooks/api/shipping-options"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||||
|
import {
|
||||||
|
isOptionEnabledInStore,
|
||||||
|
isReturnOption,
|
||||||
|
} from "../../../../../lib/shipping-options"
|
||||||
|
|
||||||
|
type LocationGeneralSectionProps = {
|
||||||
|
location: StockLocationDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LocationGeneralSection = ({
|
||||||
|
location,
|
||||||
|
}: LocationGeneralSectionProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container className="p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{location.name}</Heading>
|
||||||
|
<Actions location={location} />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<FulfillmentSet
|
||||||
|
locationId={location.id}
|
||||||
|
locationName={location.name}
|
||||||
|
type={FulfillmentSetType.Pickup}
|
||||||
|
fulfillmentSet={location.fulfillment_sets.find(
|
||||||
|
(f) => f.type === FulfillmentSetType.Pickup
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FulfillmentSet
|
||||||
|
locationId={location.id}
|
||||||
|
locationName={location.name}
|
||||||
|
type={FulfillmentSetType.Delivery}
|
||||||
|
fulfillmentSet={location.fulfillment_sets.find(
|
||||||
|
(f) => f.type === FulfillmentSetType.Delivery
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShippingOptionProps = {
|
||||||
|
option: ShippingOptionDTO
|
||||||
|
fulfillmentSetId: string
|
||||||
|
locationId: string
|
||||||
|
isReturn?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShippingOption({
|
||||||
|
option,
|
||||||
|
isReturn,
|
||||||
|
fulfillmentSetId,
|
||||||
|
locationId,
|
||||||
|
}: ShippingOptionProps) {
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const isInStore = isOptionEnabledInStore(option)
|
||||||
|
|
||||||
|
const { mutateAsync: deleteOption } = useDeleteShippingOption(option.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("shipping.shippingOptions.deleteWarning", {
|
||||||
|
name: option.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteOption()
|
||||||
|
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("shipping.shippingOptions.toast.delete", {
|
||||||
|
name: option.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="txt-small font-medium">
|
||||||
|
{option.name} - {option.shipping_profile.name} (
|
||||||
|
{formatProvider(option.provider_id)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isInStore && (
|
||||||
|
<Badge className="mr-4" color="purple">
|
||||||
|
{t("shipping.shippingOptions.inStore")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("shipping.serviceZone.editOption"),
|
||||||
|
to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/edit`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("shipping.serviceZone.editPrices"),
|
||||||
|
icon: <CurrencyDollar />,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
icon: <Trash />,
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceZoneOptionsProps = {
|
||||||
|
zone: ServiceZoneDTO
|
||||||
|
locationId: string
|
||||||
|
fulfillmentSetId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServiceZoneOptions({
|
||||||
|
zone,
|
||||||
|
locationId,
|
||||||
|
fulfillmentSetId,
|
||||||
|
}: ServiceZoneOptionsProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const shippingOptions = zone.shipping_options.filter(
|
||||||
|
(o) => !isReturnOption(o)
|
||||||
|
)
|
||||||
|
|
||||||
|
const returnOptions = zone.shipping_options.filter((o) => isReturnOption(o))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mt-4 flex flex-col border-t border-dashed px-6 py-4">
|
||||||
|
<div className="item-center flex justify-between">
|
||||||
|
<span className="text-ui-fg-subtle txt-small self-center font-medium">
|
||||||
|
{t("shipping.serviceZone.shippingOptions")}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("shipping.serviceZone.addOption")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!shippingOptions.length && (
|
||||||
|
<div className="shadow-elevation-card-rest bg-ui-bg-subtle mt-4 grid divide-y rounded-md">
|
||||||
|
{shippingOptions.map((o) => (
|
||||||
|
<ShippingOption
|
||||||
|
key={o.id}
|
||||||
|
option={o}
|
||||||
|
locationId={locationId}
|
||||||
|
fulfillmentSetId={fulfillmentSetId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="-mb-4 flex flex-col border-t border-dashed px-6 py-4">
|
||||||
|
<div className="item-center flex justify-between">
|
||||||
|
<span className="text-ui-fg-subtle txt-small self-center font-medium">
|
||||||
|
{t("shipping.serviceZone.returnOptions")}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
className="text-ui-fg-interactive txt-small px-0 font-medium hover:bg-transparent"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create?is_return`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("shipping.serviceZone.addOption")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!returnOptions.length && (
|
||||||
|
<div className="shadow-elevation-card-rest bg-ui-bg-subtle mt-4 grid divide-y rounded-md">
|
||||||
|
{returnOptions.map((o) => (
|
||||||
|
<ShippingOption
|
||||||
|
key={o.id}
|
||||||
|
isReturn
|
||||||
|
option={o}
|
||||||
|
locationId={locationId}
|
||||||
|
fulfillmentSetId={fulfillmentSetId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceZoneProps = {
|
||||||
|
zone: ServiceZoneDTO
|
||||||
|
locationId: string
|
||||||
|
fulfillmentSetId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const { mutateAsync: deleteZone } = useDeleteServiceZone(
|
||||||
|
fulfillmentSetId,
|
||||||
|
zone.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("shipping.serviceZone.deleteWarning", {
|
||||||
|
name: zone.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteZone()
|
||||||
|
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("shipping.serviceZone.toast.delete", {
|
||||||
|
name: zone.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const countries = useMemo(() => {
|
||||||
|
return zone.geo_zones
|
||||||
|
.filter((g) => g.type === "country")
|
||||||
|
.map((g) => g.country_code)
|
||||||
|
.map((code) => staticCountries.find((c) => c.iso_2 === code))
|
||||||
|
}, zone.geo_zones)
|
||||||
|
|
||||||
|
const [shippingOptionsCount, returnOptionsCount] = useMemo(() => {
|
||||||
|
const optionsCount = zone.shipping_options.filter(
|
||||||
|
(o) => !isReturnOption(o)
|
||||||
|
).length
|
||||||
|
|
||||||
|
const returnOptionsCount = zone.shipping_options.filter((o) =>
|
||||||
|
isReturnOption(o)
|
||||||
|
).length
|
||||||
|
|
||||||
|
return [optionsCount, returnOptionsCount]
|
||||||
|
}, [zone.shipping_options])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="flex flex-row items-center justify-between gap-x-4 px-6">
|
||||||
|
{/*ICON*/}
|
||||||
|
<div className="grow-0 rounded-lg border">
|
||||||
|
<div className="bg-ui-bg-field m-1 rounded-md p-2">
|
||||||
|
<Map className="text-ui-fg-subtle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*INFO*/}
|
||||||
|
<div className="grow-1 flex flex-1 flex-col">
|
||||||
|
<Text weight="plus">{zone.name}</Text>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ListSummary
|
||||||
|
list={countries.map((c) => c.display_name)}
|
||||||
|
inline
|
||||||
|
n={1}
|
||||||
|
/>
|
||||||
|
<span>·</span>
|
||||||
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
|
{shippingOptionsCount}{" "}
|
||||||
|
{t("shipping.serviceZone.optionsLength", {
|
||||||
|
count: shippingOptionsCount,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<span>·</span>
|
||||||
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
|
{returnOptionsCount}{" "}
|
||||||
|
{t("shipping.serviceZone.returnOptionsLength", {
|
||||||
|
count: returnOptionsCount,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*ACTION*/}
|
||||||
|
<div className="itemx-center flex grow-0 gap-1">
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpen((s) => !s)}
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
variant="transparent"
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${!open ? -4 : -2}px)`,
|
||||||
|
transition: ".1s transform ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronDownMini
|
||||||
|
style={{
|
||||||
|
transform: `rotate(${!open ? 0 : 180}deg)`,
|
||||||
|
transition: ".2s transform ease-in-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
// {
|
||||||
|
// label: t("shipping.serviceZone.addOption"),
|
||||||
|
// icon: <Plus />,
|
||||||
|
// to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create`,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
icon: <Trash />,
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div>
|
||||||
|
<ServiceZoneOptions
|
||||||
|
fulfillmentSetId={fulfillmentSetId}
|
||||||
|
locationId={locationId}
|
||||||
|
zone={zone}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FulfillmentSetType {
|
||||||
|
Delivery = "delivery",
|
||||||
|
Pickup = "pickup",
|
||||||
|
}
|
||||||
|
|
||||||
|
type FulfillmentSetProps = {
|
||||||
|
fulfillmentSet?: FulfillmentSetDTO
|
||||||
|
locationName: string
|
||||||
|
locationId: string
|
||||||
|
type: FulfillmentSetType
|
||||||
|
}
|
||||||
|
|
||||||
|
function FulfillmentSet(props: FulfillmentSetProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { fulfillmentSet, locationName, locationId, type } = props
|
||||||
|
|
||||||
|
const fulfillmentSetExists = !!fulfillmentSet
|
||||||
|
|
||||||
|
const hasServiceZones = !!fulfillmentSet?.service_zones.length
|
||||||
|
|
||||||
|
const { mutateAsync: createFulfillmentSet, isPending: isLoading } =
|
||||||
|
useCreateFulfillmentSet(locationId)
|
||||||
|
|
||||||
|
const { mutateAsync: deleteFulfillmentSet } = useDeleteFulfillmentSet(
|
||||||
|
fulfillmentSet?.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
try {
|
||||||
|
await createFulfillmentSet({
|
||||||
|
name: `${locationName} ${
|
||||||
|
type === FulfillmentSetType.Pickup ? "pick up" : type
|
||||||
|
}`,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("shipping.fulfillmentSet.disableWarning", {
|
||||||
|
name: fulfillmentSet?.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteFulfillmentSet()
|
||||||
|
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("shipping.fulfillmentSet.toast.disable", {
|
||||||
|
name: fulfillmentSet?.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0">
|
||||||
|
<div className="flex flex-col divide-y">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Text size="large" weight="plus" className="flex-1" as="div">
|
||||||
|
{t(`shipping.fulfillmentSet.${type}.offers`)}
|
||||||
|
</Text>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<StatusBadge color={fulfillmentSetExists ? "green" : "red"}>
|
||||||
|
{t(
|
||||||
|
fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled"
|
||||||
|
)}
|
||||||
|
</StatusBadge>
|
||||||
|
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Plus />,
|
||||||
|
label: t("shipping.fulfillmentSet.addZone"),
|
||||||
|
onClick: () =>
|
||||||
|
navigate(
|
||||||
|
`/settings/shipping/${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>
|
||||||
|
|
||||||
|
{fulfillmentSetExists && !hasServiceZones && (
|
||||||
|
<div className="text-ui-fg-muted txt-medium flex flex-col items-center justify-center gap-y-4 py-8">
|
||||||
|
<NoRecords
|
||||||
|
message={t("shipping.fulfillmentSet.placeholder")}
|
||||||
|
className="h-fit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("shipping.fulfillmentSet.addZone")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasServiceZones && (
|
||||||
|
<div className="flex flex-col divide-y">
|
||||||
|
{fulfillmentSet?.service_zones.map((zone) => (
|
||||||
|
<ServiceZone
|
||||||
|
zone={zone}
|
||||||
|
key={zone.id}
|
||||||
|
locationId={locationId}
|
||||||
|
fulfillmentSetId={fulfillmentSet.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Actions = ({ location }: { location: StockLocationDTO }) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { mutateAsync } = useDeleteStockLocation(location.id)
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("shipping.deleteLocationWarning", {
|
||||||
|
name: location.name,
|
||||||
|
}),
|
||||||
|
verificationText: location.name,
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mutateAsync(undefined)
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("shipping.toast.delete"),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
navigate("/settings/shipping", { replace: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./locations-sales-channels-section"
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
import { Heading, Text } from "@medusajs/ui"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
import { StockLocationDTO } from "@medusajs/types"
|
||||||
|
import { Channels, PencilSquare } from "@medusajs/icons"
|
||||||
|
|
||||||
|
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||||
|
|
||||||
|
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("shipping.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("shipping.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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { locationLoader as loader } from "./loader"
|
||||||
|
export { LocationDetails as Component } from "./location-details"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
|
import { AdminStockLocationResponse } from "@medusajs/types"
|
||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
|
import { client } from "../../../lib/client"
|
||||||
|
|
||||||
|
const locationQuery = (id: string) => ({
|
||||||
|
queryKey: stockLocationsQueryKeys.detail(id),
|
||||||
|
queryFn: async () =>
|
||||||
|
client.stockLocations.retrieve(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.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const locationLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.location_id
|
||||||
|
const query = locationQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminStockLocationResponse>>(
|
||||||
|
query.queryKey
|
||||||
|
) ?? (await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { LocationGeneralSection } from "./components/location-general-section"
|
||||||
|
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||||
|
import LocationsSalesChannelsSection from "./components/location-sales-channels-section/locations-sales-channels-section"
|
||||||
|
import { locationLoader } from "./loader"
|
||||||
|
|
||||||
|
export const LocationDetails = () => {
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof locationLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { location_id } = useParams()
|
||||||
|
const {
|
||||||
|
stock_location: location,
|
||||||
|
isPending: isLoading,
|
||||||
|
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.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
||||||
|
if (isLoading || !location) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
||||||
|
<div className="flex w-full flex-col gap-y-2">
|
||||||
|
<LocationGeneralSection location={location} />
|
||||||
|
<div className="flex w-full flex-col gap-y-2 xl:hidden">
|
||||||
|
<LocationsSalesChannelsSection location={location} />
|
||||||
|
</div>
|
||||||
|
<JsonViewSection data={location} />
|
||||||
|
</div>
|
||||||
|
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
|
||||||
|
<LocationsSalesChannelsSection location={location} />
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-3
@@ -81,7 +81,7 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
|
|||||||
className="flex flex-1 flex-col overflow-hidden"
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<RouteDrawer.Body className="flex flex-col gap-y-8 overflow-y-auto">
|
<RouteDrawer.Body className="flex flex-col gap-y-8 overflow-y-auto">
|
||||||
<div>
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -97,8 +97,6 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="address.address_1"
|
name="address.address_1"
|
||||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -6,14 +6,14 @@ 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 { id } = useParams()
|
const { location_id } = useParams()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stock_location,
|
stock_location,
|
||||||
isPending: isLoading,
|
isPending: isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useStockLocation(id, {
|
} = useStockLocation(location_id, {
|
||||||
fields: "*address",
|
fields: "*address",
|
||||||
})
|
})
|
||||||
|
|
||||||
File renamed without changes.
+212
@@ -0,0 +1,212 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
StatusBadge,
|
||||||
|
Text,
|
||||||
|
toast,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
FulfillmentSetDTO,
|
||||||
|
SalesChannelDTO,
|
||||||
|
StockLocationDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Buildings, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
import { countries } from "../../../../../lib/countries"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useDeleteStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||||
|
import { BadgeListSummary } from "../../../../../components/common/badge-list-summary"
|
||||||
|
|
||||||
|
type SalesChannelsProps = {
|
||||||
|
salesChannels?: SalesChannelDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function SalesChannels(props: SalesChannelsProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { salesChannels } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col px-6 py-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
weight="plus"
|
||||||
|
className="text-ui-fg-subtle flex-1"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
{t(`shipping.fulfillmentSet.salesChannels`)}
|
||||||
|
</Text>
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
{salesChannels?.length ? (
|
||||||
|
<BadgeListSummary
|
||||||
|
inline
|
||||||
|
n={3}
|
||||||
|
list={salesChannels.map((s) => s.name)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FulfillmentSetType {
|
||||||
|
Delivery = "delivery",
|
||||||
|
Pickup = "pickup",
|
||||||
|
}
|
||||||
|
|
||||||
|
type FulfillmentSetProps = {
|
||||||
|
fulfillmentSet?: FulfillmentSetDTO
|
||||||
|
type: FulfillmentSetType
|
||||||
|
}
|
||||||
|
|
||||||
|
function FulfillmentSet(props: FulfillmentSetProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { fulfillmentSet, type } = props
|
||||||
|
|
||||||
|
const fulfillmentSetExists = !!fulfillmentSet
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col px-6 py-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
weight="plus"
|
||||||
|
className="text-ui-fg-subtle flex-1"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
{t(`shipping.fulfillmentSet.${type}.title`)}
|
||||||
|
</Text>
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<StatusBadge color={fulfillmentSetExists ? "green" : "red"}>
|
||||||
|
{t(fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationProps = {
|
||||||
|
location: StockLocationDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
function Location(props: LocationProps) {
|
||||||
|
const { location } = props
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync: deleteLocation } = useDeleteStockLocation(location.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const result = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("shipping.deleteLocation.confirm", {
|
||||||
|
name: location.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.remove"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteLocation()
|
||||||
|
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("shipping.deleteLocation.success", {
|
||||||
|
name: location.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: e.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex flex-col divide-y p-0">
|
||||||
|
<div className="px-6 py-5">
|
||||||
|
<div className="flex flex-row items-center justify-between gap-x-4">
|
||||||
|
{/*ICON*/}
|
||||||
|
<div className="grow-0 rounded-lg border">
|
||||||
|
<div className="bg-ui-bg-field m-1 rounded-md p-2">
|
||||||
|
<Buildings className="text-ui-fg-subtle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*LOCATION INFO*/}
|
||||||
|
<div className="grow-1 flex flex-1 flex-col">
|
||||||
|
<Text weight="plus">{location.name}</Text>
|
||||||
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
|
{location.address?.city},{" "}
|
||||||
|
{
|
||||||
|
countries.find(
|
||||||
|
(c) =>
|
||||||
|
location.address?.country_code.toLowerCase() === c.iso_2
|
||||||
|
)?.display_name
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*ACTION*/}
|
||||||
|
<div className="flex h-[12px] grow-0 items-center gap-4 divide-x overflow-hidden">
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
to: `/settings/shipping/${location.id}/edit`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("shipping.deleteLocation.label"),
|
||||||
|
icon: <Trash />,
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="text-ui-fg-interactive rounded-none pl-5 hover:bg-transparent"
|
||||||
|
onClick={() => navigate(`/settings/shipping/${location.id}`)}
|
||||||
|
variant="transparent"
|
||||||
|
>
|
||||||
|
{t("actions.viewDetails")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SalesChannels salesChannels={location.sales_channels} />
|
||||||
|
|
||||||
|
<FulfillmentSet
|
||||||
|
type={FulfillmentSetType.Pickup}
|
||||||
|
fulfillmentSet={location.fulfillment_sets.find(
|
||||||
|
(f) => f.type === FulfillmentSetType.Pickup
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FulfillmentSet
|
||||||
|
type={FulfillmentSetType.Delivery}
|
||||||
|
fulfillmentSet={location.fulfillment_sets.find(
|
||||||
|
(f) => f.type === FulfillmentSetType.Delivery
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Location
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// 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"
|
||||||
File renamed without changes.
+5
-6
@@ -1,18 +1,17 @@
|
|||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
import { adminStockLocationsKeys } from "medusa-react"
|
|
||||||
|
|
||||||
import { client } from "../../../lib/client"
|
import { client } from "../../../lib/client"
|
||||||
import { queryClient } from "../../../lib/medusa"
|
import { queryClient } from "../../../lib/medusa"
|
||||||
import { StockLocationListRes } from "../../../types/api-responses"
|
import { StockLocationListRes } from "../../../types/api-responses"
|
||||||
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
|
import { locationListFields } from "./const"
|
||||||
|
|
||||||
const shippingListQuery = () => ({
|
const shippingListQuery = () => ({
|
||||||
queryKey: adminStockLocationsKeys.lists(),
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
client.stockLocations.list({
|
client.stockLocations.list({
|
||||||
// fields: "*fulfillment_sets,*fulfillment_sets.service_zones",
|
// TODO: change this when RQ is fixed
|
||||||
// TODO: change this when RQ is fixed to work with the upper fields definition
|
fields: locationListFields,
|
||||||
fields:
|
|
||||||
"name,address.city,address.country_code,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile",
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, Outlet, useLoaderData } from "react-router-dom"
|
||||||
|
|
||||||
|
import { shippingListLoader } from "./loader"
|
||||||
|
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
||||||
|
import Location from "./components/location/location"
|
||||||
|
import { locationListFields } from "./const"
|
||||||
|
|
||||||
|
export function LocationList() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof shippingListLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
let { stock_locations: stockLocations = [], isPending } = useStockLocations(
|
||||||
|
{
|
||||||
|
fields: locationListFields,
|
||||||
|
},
|
||||||
|
{ initialData }
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="py-4">
|
||||||
|
<Container className="mb-4 flex h-fit items-center justify-between p-8">
|
||||||
|
<div>
|
||||||
|
<Heading className="mb-2">{t("shipping.title")}</Heading>
|
||||||
|
<Text className="text-ui-fg-subtle txt-small">
|
||||||
|
{t("shipping.description")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="secondary" asChild>
|
||||||
|
<Link to="create">{t("shipping.createLocation")}</Link>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
<div className="flex flex-col gap-4 lg:col-span-2">
|
||||||
|
{stockLocations.map((location) => (
|
||||||
|
<Location key={location.id} location={location} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
-392
@@ -1,392 +0,0 @@
|
|||||||
import { Button, Container, Text } from "@medusajs/ui"
|
|
||||||
import {
|
|
||||||
FulfillmentSetDTO,
|
|
||||||
ServiceZoneDTO,
|
|
||||||
ShippingOptionDTO,
|
|
||||||
StockLocationDTO,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import {
|
|
||||||
Buildings,
|
|
||||||
ChevronDown,
|
|
||||||
CurrencyDollar,
|
|
||||||
Map,
|
|
||||||
PencilSquare,
|
|
||||||
Plus,
|
|
||||||
Trash,
|
|
||||||
} from "@medusajs/icons"
|
|
||||||
import { useNavigate } from "react-router-dom"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
import { countries } from "../../../../../lib/countries"
|
|
||||||
import {
|
|
||||||
useCreateFulfillmentSet,
|
|
||||||
useDeleteFulfillmentSet,
|
|
||||||
useDeleteServiceZone,
|
|
||||||
} from "../../../../../hooks/api/stock-locations"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { formatProvider } from "../../../../../lib/format-provider"
|
|
||||||
import { useDeleteShippingOption } from "../../../../../hooks/api/shipping-options.ts"
|
|
||||||
|
|
||||||
type ShippingOptionProps = {
|
|
||||||
option: ShippingOptionDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShippingOption({ option }: ShippingOptionProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const { mutateAsync: deleteOption } = useDeleteShippingOption(option.id)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteOption()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="shadow-elevation-card-rest flex items-center justify-between rounded-md px-4 py-3">
|
|
||||||
<div className="flex-1">
|
|
||||||
<span className="txt-small font-medium">
|
|
||||||
{option.name} - {option.shipping_profile.name} (
|
|
||||||
{formatProvider(option.provider_id)})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("shipping.serviceZone.editOption"),
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("shipping.serviceZone.editPrices"),
|
|
||||||
icon: <CurrencyDollar />,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("actions.delete"),
|
|
||||||
icon: <Trash />,
|
|
||||||
onClick: handleDelete,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceZoneOptionsProps = {
|
|
||||||
zone: ServiceZoneDTO
|
|
||||||
locationId: string
|
|
||||||
fulfillmentSetId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function ServiceZoneOptions({
|
|
||||||
zone,
|
|
||||||
locationId,
|
|
||||||
fulfillmentSetId,
|
|
||||||
}: ServiceZoneOptionsProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const shippingOptions = zone.shipping_options
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col py-4">
|
|
||||||
<Text
|
|
||||||
size="small"
|
|
||||||
weight="plus"
|
|
||||||
className="text-ui-fg-subtle mb-4"
|
|
||||||
as="div"
|
|
||||||
>
|
|
||||||
{t("shipping.serviceZone.shippingOptions")}
|
|
||||||
</Text>
|
|
||||||
{!shippingOptions.length && (
|
|
||||||
<div className="text-ui-fg-muted txt-medium flex h-[120px] flex-col items-center justify-center gap-y-4">
|
|
||||||
<div>{t("shipping.serviceZone.shippingOptionsPlaceholder")}</div>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/shipping/location/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-options/create`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("shipping.serviceZone.addShippingOptions")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!shippingOptions.length && (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{shippingOptions.map((o) => (
|
|
||||||
<ShippingOption key={o.id} option={o} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/*TODO implement return options*/}
|
|
||||||
{/*<div className="py-4">*/}
|
|
||||||
{/* <Text*/}
|
|
||||||
{/* size="small"*/}
|
|
||||||
{/* weight="plus"*/}
|
|
||||||
{/* className="text-ui-fg-subtle mb-4"*/}
|
|
||||||
{/* as="div"*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* {t("shipping.serviceZone.returnOptions")}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceZoneProps = {
|
|
||||||
zone: ServiceZoneDTO
|
|
||||||
locationId: string
|
|
||||||
fulfillmentSetId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
const { mutateAsync: deleteZone } = useDeleteServiceZone(
|
|
||||||
fulfillmentSetId,
|
|
||||||
zone.id
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteZone()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-row items-center justify-between gap-x-4">
|
|
||||||
{/*ICON*/}
|
|
||||||
<div className="grow-0 rounded-lg border">
|
|
||||||
<div className="bg-ui-bg-field m-1 rounded-md p-2">
|
|
||||||
<Map className="text-ui-fg-subtle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*INFO*/}
|
|
||||||
<div className="grow-1 flex flex-1 flex-col">
|
|
||||||
<Text weight="plus">{zone.name}</Text>
|
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
|
||||||
{zone.shipping_options.length}{" "}
|
|
||||||
{t("shipping.serviceZone.optionsLength", {
|
|
||||||
count: zone.shipping_options.length,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*ACTION*/}
|
|
||||||
<div className="itemx-center -m-2 flex grow-0 gap-2">
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("shipping.serviceZone.addShippingOptions"),
|
|
||||||
icon: <Plus />,
|
|
||||||
to: `/shipping/location/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-options/create`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("actions.delete"),
|
|
||||||
icon: <Trash />,
|
|
||||||
onClick: handleDelete,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => setOpen((s) => !s)}
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
variant="transparent"
|
|
||||||
>
|
|
||||||
<ChevronDown
|
|
||||||
style={{
|
|
||||||
transform: `rotate(${!open ? 0 : 180}deg)`,
|
|
||||||
transition: ".2s transform ease-in-out",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{open && (
|
|
||||||
<div>
|
|
||||||
<ServiceZoneOptions
|
|
||||||
fulfillmentSetId={fulfillmentSetId}
|
|
||||||
locationId={locationId}
|
|
||||||
zone={zone}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FulfillmentSetType {
|
|
||||||
Delivery = "delivery",
|
|
||||||
Pickup = "pickup",
|
|
||||||
}
|
|
||||||
|
|
||||||
type FulfillmentSetProps = {
|
|
||||||
fulfillmentSet?: FulfillmentSetDTO
|
|
||||||
locationName: string
|
|
||||||
locationId: string
|
|
||||||
type: FulfillmentSetType
|
|
||||||
}
|
|
||||||
|
|
||||||
function FulfillmentSet(props: FulfillmentSetProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { fulfillmentSet, locationName, locationId, type } = props
|
|
||||||
|
|
||||||
const fulfillmentSetExists = !!fulfillmentSet
|
|
||||||
const hasServiceZones = !!fulfillmentSet?.service_zones?.length
|
|
||||||
|
|
||||||
const { mutateAsync: createFulfillmentSet, isPending: isLoading } =
|
|
||||||
useCreateFulfillmentSet(locationId)
|
|
||||||
|
|
||||||
const { mutateAsync: deleteFulfillmentSet } = useDeleteFulfillmentSet(
|
|
||||||
fulfillmentSet?.id
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
await createFulfillmentSet({
|
|
||||||
name: `${locationName} ${type}`,
|
|
||||||
type: type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteFulfillmentSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col px-6 py-5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Text size="small" weight="plus" className="text-ui-fg-subtle" as="div">
|
|
||||||
{t(`shipping.fulfillmentSet.${type}.title`)}
|
|
||||||
</Text>
|
|
||||||
{!fulfillmentSetExists ? (
|
|
||||||
<Button onClick={handleCreate} variant="secondary">
|
|
||||||
{t(`shipping.fulfillmentSet.${type}.enable`)}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("shipping.fulfillmentSet.addZone"),
|
|
||||||
icon: <Map />,
|
|
||||||
to: `/shipping/location/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("shipping.fulfillmentSet.delete"),
|
|
||||||
icon: <Trash />,
|
|
||||||
onClick: handleDelete,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{fulfillmentSetExists && !hasServiceZones && (
|
|
||||||
<div className="text-ui-fg-muted txt-medium flex h-[120px] flex-col items-center justify-center gap-y-4">
|
|
||||||
<div>{t("shipping.fulfillmentSet.placeholder")}</div>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/shipping/location/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("shipping.fulfillmentSet.addZone")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasServiceZones && (
|
|
||||||
<div className="mt-4 flex flex-col gap-6">
|
|
||||||
{fulfillmentSet?.service_zones.map((zone) => (
|
|
||||||
<ServiceZone
|
|
||||||
key={zone.id}
|
|
||||||
zone={zone}
|
|
||||||
locationId={locationId}
|
|
||||||
fulfillmentSetId={fulfillmentSet.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocationProps = {
|
|
||||||
location: StockLocationDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
function Location(props: LocationProps) {
|
|
||||||
const { location } = props
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="flex flex-col divide-y p-0">
|
|
||||||
<div className="px-6 py-5">
|
|
||||||
<div className="flex flex-row items-center justify-between gap-x-4">
|
|
||||||
{/*ICON*/}
|
|
||||||
<div className="grow-0 rounded-lg border">
|
|
||||||
<div className="bg-ui-bg-field m-1 rounded-md p-2">
|
|
||||||
<Buildings className="text-ui-fg-subtle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*LOCATION INFO*/}
|
|
||||||
<div className="grow-1 flex flex-1 flex-col">
|
|
||||||
<Text weight="plus">{location.name}</Text>
|
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
|
||||||
{location.address?.city},{" "}
|
|
||||||
{
|
|
||||||
countries.find(
|
|
||||||
(c) =>
|
|
||||||
location.address?.country_code.toLowerCase() === c.iso_2
|
|
||||||
)?.display_name
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*ACTION*/}
|
|
||||||
<div className="flex grow-0 gap-2">{/*// TODO*/}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FulfillmentSet
|
|
||||||
locationId={location.id}
|
|
||||||
locationName={location.name}
|
|
||||||
type={FulfillmentSetType.Pickup}
|
|
||||||
fulfillmentSet={location.fulfillment_sets.find(
|
|
||||||
(f) => f.type === FulfillmentSetType.Pickup
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FulfillmentSet
|
|
||||||
locationId={location.id}
|
|
||||||
locationName={location.name}
|
|
||||||
type={FulfillmentSetType.Delivery}
|
|
||||||
fulfillmentSet={location.fulfillment_sets.find(
|
|
||||||
(f) => f.type === FulfillmentSetType.Delivery
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Location
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Container, Heading, Text } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Outlet, useLoaderData } from "react-router-dom"
|
|
||||||
|
|
||||||
import { shippingListLoader } from "./loader"
|
|
||||||
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
|
||||||
import Location from "./components/location/location"
|
|
||||||
import { NoRecords } from "../../../components/common/empty-table-content"
|
|
||||||
|
|
||||||
export function LocationList() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const initialData = useLoaderData() as Awaited<
|
|
||||||
ReturnType<typeof shippingListLoader>
|
|
||||||
>
|
|
||||||
|
|
||||||
let { stock_locations: stockLocations = [], isPending } = useStockLocations(
|
|
||||||
{
|
|
||||||
fields:
|
|
||||||
"name,address.city,address.country_code,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile",
|
|
||||||
},
|
|
||||||
{ initialData }
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-3 gap-x-6 py-4">
|
|
||||||
<Container className="static top-3 col-span-3 mb-4 h-fit p-8 lg:sticky lg:col-span-1">
|
|
||||||
<Heading className="mb-2">{t("shipping.title")}</Heading>
|
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
|
||||||
{t("shipping.description")}
|
|
||||||
</Text>
|
|
||||||
</Container>
|
|
||||||
<div className="col-span-3 flex flex-col gap-4 lg:col-span-2">
|
|
||||||
{!isPending && !stockLocations.length && (
|
|
||||||
<Container>
|
|
||||||
<NoRecords
|
|
||||||
className="h-[180px]"
|
|
||||||
title={t("shipping.noRecords.title")}
|
|
||||||
message={t("shipping.noRecords.message")}
|
|
||||||
action={{
|
|
||||||
to: "/inventory/locations",
|
|
||||||
label: t("shipping.noRecords.action"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)}
|
|
||||||
{stockLocations.map((location) => (
|
|
||||||
<Location key={location.id} location={location} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Outlet />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+65
-26
@@ -7,10 +7,19 @@ import {
|
|||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Alert, Button, Checkbox, Heading, Input, Text } from "@medusajs/ui"
|
import {
|
||||||
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Text,
|
||||||
|
} from "@medusajs/ui"
|
||||||
import { FulfillmentSetDTO, RegionCountryDTO, RegionDTO } from "@medusajs/types"
|
import { FulfillmentSetDTO, RegionCountryDTO, RegionDTO } from "@medusajs/types"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Map } from "@medusajs/icons"
|
import { Map, XMark, XMarkMini } from "@medusajs/icons"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
@@ -26,7 +35,6 @@ import { countries as staticCountries } from "../../../../../lib/countries"
|
|||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
|
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
|
||||||
|
|
||||||
const PREFIX = "ac"
|
const PREFIX = "ac"
|
||||||
const PAGE_SIZE = 50
|
const PAGE_SIZE = 50
|
||||||
@@ -87,7 +95,7 @@ export function CreateServiceZoneForm({
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
handleSuccess("/shipping")
|
handleSuccess()
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
@@ -129,12 +137,28 @@ export function CreateServiceZoneForm({
|
|||||||
prefix: PREFIX,
|
prefix: PREFIX,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const countriesWatch = form.watch("countries")
|
||||||
|
|
||||||
const onCountriesSave = () => {
|
const onCountriesSave = () => {
|
||||||
form.setValue("countries", Object.keys(rowSelection))
|
form.setValue("countries", Object.keys(rowSelection))
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const countriesWatch = form.watch("countries")
|
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(() => {
|
const selectedCountries = useMemo(() => {
|
||||||
return staticCountries.filter((c) => c.iso_2 in rowSelection)
|
return staticCountries.filter((c) => c.iso_2 in rowSelection)
|
||||||
@@ -184,15 +208,6 @@ export function CreateServiceZoneForm({
|
|||||||
})}
|
})}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text weight="plus">
|
|
||||||
{t("shipping.serviceZone.create.subtitle")}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-ui-fg-subtle mb-8 mt-2">
|
|
||||||
{t("shipping.serviceZone.create.description")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex max-w-[340px] flex-col gap-y-6">
|
<div className="flex max-w-[340px] flex-col gap-y-6">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -204,7 +219,7 @@ export function CreateServiceZoneForm({
|
|||||||
{t("shipping.serviceZone.create.zoneName")}
|
{t("shipping.serviceZone.create.zoneName")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} />
|
<Input placeholder={t("fields.name")} {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -214,8 +229,17 @@ export function CreateServiceZoneForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
<Text weight="plus">
|
||||||
|
{t("shipping.serviceZone.create.subtitle")}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-ui-fg-subtle mt-2">
|
||||||
|
{t("shipping.serviceZone.create.description")}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
{/*AREAS*/}
|
{/*AREAS*/}
|
||||||
<div className="container flex items-center justify-between py-8">
|
<div className="container flex items-center justify-between py-8 pr-1">
|
||||||
<div>
|
<div>
|
||||||
<Text weight="plus">
|
<Text weight="plus">
|
||||||
{t("shipping.serviceZone.areas.title")}
|
{t("shipping.serviceZone.areas.title")}
|
||||||
@@ -233,16 +257,31 @@ export function CreateServiceZoneForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!!selectedCountries.length && (
|
{!!selectedCountries.length && (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<div className="grow-0 rounded-lg border">
|
{selectedCountries.map((c) => (
|
||||||
<div className="bg-ui-bg-field m-1 rounded-md p-2">
|
<Badge
|
||||||
<Map className="text-ui-fg-subtle" />
|
key={c.iso_2}
|
||||||
</div>
|
className="text-ui-fg-subtle txt-small flex items-center gap-1 divide-x pr-0"
|
||||||
</div>
|
>
|
||||||
<ListSummary
|
{c.display_name}
|
||||||
inline
|
<IconButton
|
||||||
list={selectedCountries.map((c) => c.display_name)}
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showAreasError && (
|
{showAreasError && (
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { adminStockLocationsKeys } from "medusa-react"
|
|
||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
import { client } from "../../../lib/client"
|
import { client } from "../../../lib/client"
|
||||||
import { queryClient } from "../../../lib/medusa"
|
import { queryClient } from "../../../lib/medusa"
|
||||||
import { StockLocationRes } from "../../../types/api-responses"
|
import { StockLocationRes } from "../../../types/api-responses"
|
||||||
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
|
|
||||||
const fulfillmentSetCreateQuery = (id: string) => ({
|
const fulfillmentSetCreateQuery = (id: string) => ({
|
||||||
queryKey: adminStockLocationsKeys.detail(id),
|
queryKey: stockLocationsQueryKeys.detail(id),
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
client.stockLocations.retrieve(id, {
|
client.stockLocations.retrieve(id, {
|
||||||
fields: "*fulfillment_sets",
|
fields: "*fulfillment_sets",
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { Alert, Button, Input, Text, toast } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { ServiceZoneDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateServiceZone } from "../../../../../hooks/api/stock-locations"
|
||||||
|
|
||||||
|
type EditServiceZoneFormProps = {
|
||||||
|
zone: ServiceZoneDTO
|
||||||
|
fulfillmentSetId: string
|
||||||
|
locationId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditServiceZoneSchema = zod.object({
|
||||||
|
name: zod.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditServiceZoneForm = ({
|
||||||
|
zone,
|
||||||
|
fulfillmentSetId,
|
||||||
|
locationId,
|
||||||
|
}: EditServiceZoneFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditServiceZoneSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: zone.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isPending: isLoading } = useUpdateServiceZone(
|
||||||
|
fulfillmentSetId,
|
||||||
|
zone.id,
|
||||||
|
locationId
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
name: values.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
// description: t("regions.toast.edit"),
|
||||||
|
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-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>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Alert>
|
||||||
|
<Text weight="plus">
|
||||||
|
{t("shipping.serviceZone.create.subtitle")}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-ui-fg-subtle mt-2">
|
||||||
|
{t("shipping.serviceZone.create.description")}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
</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 +1 @@
|
|||||||
|
export * from "./edit-service-zone-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ServiceZoneEdit as Component } from "./service-zone-edit"
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { json, useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { EditServiceZoneForm } from "./components/edit-region-form"
|
||||||
|
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||||
|
|
||||||
|
export const ServiceZoneEdit = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { location_id, fset_id, zone_id } = useParams()
|
||||||
|
|
||||||
|
const { stock_location, isPending, isError, error } = useStockLocation(
|
||||||
|
location_id!,
|
||||||
|
{
|
||||||
|
fields:
|
||||||
|
"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
|
||||||
|
.find((f) => f.id === fset_id)
|
||||||
|
?.service_zones.find((z) => z.id === zone_id)
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPending && !zone) {
|
||||||
|
throw json(
|
||||||
|
{ message: `Service zone with ID ${zone_id} was not found` },
|
||||||
|
404
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("shipping.serviceZone.edit.title")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{!isPending && zone && (
|
||||||
|
<EditServiceZoneForm
|
||||||
|
zone={zone}
|
||||||
|
fulfillmentSetId={fset_id}
|
||||||
|
locationId={location_id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+278
@@ -0,0 +1,278 @@
|
|||||||
|
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 { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
|
||||||
|
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||||
|
import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers"
|
||||||
|
import { isOptionEnabledInStore } from "../../../../../lib/shipping-options"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
import { pick } from "../../../../../lib/common"
|
||||||
|
|
||||||
|
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 sice 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("shipping.shippingOptions.create.allocation")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
className="flex-1"
|
||||||
|
value={ShippingAllocation.FlatRate}
|
||||||
|
label={t("shipping.shippingOptions.create.fixed")}
|
||||||
|
description={t(
|
||||||
|
"shipping.shippingOptions.create.fixedDescription"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
className="flex-1"
|
||||||
|
value={ShippingAllocation.Calculated}
|
||||||
|
label={t(
|
||||||
|
"shipping.shippingOptions.create.calculated"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"shipping.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("shipping.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("shipping.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("shipping.shippingOptions.create.enable")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={!!value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.Hint className="!mt-1">
|
||||||
|
{t(
|
||||||
|
"shipping.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 +1 @@
|
|||||||
|
export * from "./edit-shipping-option-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ShippingOptionEdit as Component } from "./shipping-option-edit"
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { json, useParams, useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { EditShippingOptionForm } from "./components/edit-region-form"
|
||||||
|
import { useShippingOptions } from "../../../hooks/api/shipping-options"
|
||||||
|
|
||||||
|
export const ShippingOptionEdit = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const { location_id, fset_id, zone_id, so_id } = useParams()
|
||||||
|
const isReturn = searchParams.has("is_return")
|
||||||
|
|
||||||
|
const { shipping_options, isPending, isError, error } = useShippingOptions()
|
||||||
|
|
||||||
|
const shippingOption = shipping_options?.find((so) => so.id === so_id)
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPending && !shippingOption) {
|
||||||
|
throw json(
|
||||||
|
{ message: `Shipping option with ID ${so_id} was not found` },
|
||||||
|
404
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("shipping.shippingOptions.edit.title")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{!isPending && shippingOption && (
|
||||||
|
<EditShippingOptionForm
|
||||||
|
shippingOption={shippingOption}
|
||||||
|
isReturn={isReturn}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+96
-15
@@ -27,6 +27,9 @@ import { CreateShippingOptionsPricesForm } from "./create-shipping-options-price
|
|||||||
import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||||
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
|
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
|
||||||
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
import { useRegions } from "../../../../../hooks/api/regions"
|
||||||
|
|
||||||
enum Tab {
|
enum Tab {
|
||||||
DETAILS = "details",
|
DETAILS = "details",
|
||||||
@@ -45,29 +48,42 @@ type StepStatus = {
|
|||||||
const CreateServiceZoneSchema = zod.object({
|
const CreateServiceZoneSchema = zod.object({
|
||||||
name: zod.string().min(1),
|
name: zod.string().min(1),
|
||||||
price_type: zod.nativeEnum(ShippingAllocation),
|
price_type: zod.nativeEnum(ShippingAllocation),
|
||||||
enable_in_store: zod.boolean().optional(),
|
enabled_in_store: zod.boolean().optional(),
|
||||||
shipping_profile_id: zod.string(),
|
shipping_profile_id: zod.string(),
|
||||||
|
provider_id: zod.string().min(1),
|
||||||
region_prices: zod.record(zod.string(), zod.string().optional()),
|
region_prices: zod.record(zod.string(), zod.string().optional()),
|
||||||
currency_prices: zod.record(zod.string(), zod.string().optional()),
|
currency_prices: zod.record(zod.string(), zod.string().optional()),
|
||||||
})
|
})
|
||||||
|
|
||||||
type CreateServiceZoneFormProps = {
|
type CreateServiceZoneFormProps = {
|
||||||
zone: ServiceZoneDTO
|
zone: ServiceZoneDTO
|
||||||
|
isReturn?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreateShippingOptionsForm({
|
export function CreateShippingOptionsForm({
|
||||||
zone,
|
zone,
|
||||||
|
isReturn,
|
||||||
}: CreateServiceZoneFormProps) {
|
}: CreateServiceZoneFormProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const [tab, setTab] = React.useState<Tab>(Tab.DETAILS)
|
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 CreateServiceZoneSchema>>({
|
const form = useForm<zod.infer<typeof CreateServiceZoneSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
price_type: ShippingAllocation.FlatRate,
|
price_type: ShippingAllocation.FlatRate,
|
||||||
enable_in_store: true,
|
enabled_in_store: true,
|
||||||
shipping_profile_id: "",
|
shipping_profile_id: "",
|
||||||
|
provider_id: "",
|
||||||
region_prices: {},
|
region_prices: {},
|
||||||
currency_prices: {},
|
currency_prices: {},
|
||||||
},
|
},
|
||||||
@@ -97,18 +113,41 @@ export function CreateShippingOptionsForm({
|
|||||||
})
|
})
|
||||||
.filter((o) => !!o.amount)
|
.filter((o) => !!o.amount)
|
||||||
|
|
||||||
/**
|
const regionsMap = new Map(regions.map((r) => [r.id, r.currency_code]))
|
||||||
* TODO: region prices
|
|
||||||
*/
|
const regionPrices = Object.entries(data.region_prices)
|
||||||
// Object.entries(data.region_prices).map(([region_id, value]) => {})
|
.map(([region_id, value]) => {
|
||||||
|
const code = regionsMap.get(region_id)
|
||||||
|
|
||||||
|
const amount =
|
||||||
|
value === "" ? undefined : getDbAmount(Number(value), code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
region_id,
|
||||||
|
amount: amount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((o) => !!o.amount)
|
||||||
|
|
||||||
await createShippingOption({
|
await createShippingOption({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
price_type: data.price_type,
|
price_type: data.price_type,
|
||||||
service_zone_id: zone.id,
|
service_zone_id: zone.id,
|
||||||
shipping_profile_id: data.shipping_profile_id,
|
shipping_profile_id: data.shipping_profile_id,
|
||||||
provider_id: "manual_test-provider", // TODO: FETCH PROVIDERS
|
provider_id: data.provider_id,
|
||||||
prices: [...currencyPrices],
|
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: {
|
type: {
|
||||||
// TODO: FETCH TYPES
|
// TODO: FETCH TYPES
|
||||||
label: "Type label",
|
label: "Type label",
|
||||||
@@ -117,7 +156,7 @@ export function CreateShippingOptionsForm({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
handleSuccess("/shipping")
|
handleSuccess()
|
||||||
})
|
})
|
||||||
|
|
||||||
const [status, setStatus] = React.useState<StepStatus>({
|
const [status, setStatus] = React.useState<StepStatus>({
|
||||||
@@ -141,7 +180,9 @@ export function CreateShippingOptionsForm({
|
|||||||
}, [tab])
|
}, [tab])
|
||||||
|
|
||||||
const canMoveToPricing =
|
const canMoveToPricing =
|
||||||
form.watch("name").length && form.watch("shipping_profile_id")
|
form.watch("name").length &&
|
||||||
|
form.watch("shipping_profile_id") &&
|
||||||
|
form.watch("provider_id")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.formState.isDirty) {
|
if (form.formState.isDirty) {
|
||||||
@@ -244,11 +285,17 @@ export function CreateShippingOptionsForm({
|
|||||||
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
||||||
<div className="container mx-auto w-[720px] px-1 py-8">
|
<div className="container mx-auto w-[720px] px-1 py-8">
|
||||||
<Heading className="mb-12 mt-8 text-2xl">
|
<Heading className="mb-12 mt-8 text-2xl">
|
||||||
{t("shipping.shippingOptions.create.title", {
|
{t(
|
||||||
|
`shipping.${
|
||||||
|
isReturn ? "returnOptions" : "shippingOptions"
|
||||||
|
}.create.title`,
|
||||||
|
{
|
||||||
zone: zone.name,
|
zone: zone.name,
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
|
{!isReturn && (
|
||||||
<div>
|
<div>
|
||||||
<Text weight="plus">
|
<Text weight="plus">
|
||||||
{t("shipping.shippingOptions.create.subtitle")}
|
{t("shipping.shippingOptions.create.subtitle")}
|
||||||
@@ -257,6 +304,7 @@ export function CreateShippingOptionsForm({
|
|||||||
{t("shipping.shippingOptions.create.description")}
|
{t("shipping.shippingOptions.create.description")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -299,8 +347,7 @@ export function CreateShippingOptionsForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col divide-y">
|
<div className="mt-8 max-w-[50%] pr-2 ">
|
||||||
<div className="mt-4 grid grid-cols-2 gap-4">
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -316,7 +363,10 @@ export function CreateShippingOptionsForm({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col divide-y">
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="shipping_profile_id"
|
name="shipping_profile_id"
|
||||||
@@ -347,11 +397,42 @@ export function CreateShippingOptionsForm({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="provider_id"
|
||||||
|
render={({ field: { onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("shipping.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>
|
||||||
<div className="mt-8 pt-8">
|
<div className="mt-8 pt-8">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enable_in_store"
|
name="enabled_in_store"
|
||||||
render={({ field: { value, onChange, ...field } }) => (
|
render={({ field: { value, onChange, ...field } }) => (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
import { adminStockLocationsKeys } from "medusa-react"
|
|
||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
import { client } from "../../../lib/client"
|
import { client } from "../../../lib/client"
|
||||||
import { queryClient } from "../../../lib/medusa"
|
import { queryClient } from "../../../lib/medusa"
|
||||||
import { StockLocationRes } from "../../../types/api-responses"
|
import { StockLocationRes } from "../../../types/api-responses"
|
||||||
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
|
|
||||||
const fulfillmentSetCreateQuery = (id: string) => ({
|
const fulfillmentSetCreateQuery = (id: string) => ({
|
||||||
queryKey: adminStockLocationsKeys.list(), // Use the list cache key for now
|
queryKey: stockLocationsQueryKeys.list(), // Use the list cache key for now
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
client.stockLocations.retrieve(id, {
|
client.stockLocations.retrieve(id, {
|
||||||
fields:
|
fields:
|
||||||
|
|||||||
+6
-2
@@ -1,4 +1,4 @@
|
|||||||
import { useLoaderData, useParams } from "react-router-dom"
|
import { useLoaderData, useParams, useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { CreateShippingOptionsForm } from "./components/create-shipping-options-form"
|
import { CreateShippingOptionsForm } from "./components/create-shipping-options-form"
|
||||||
@@ -6,6 +6,10 @@ import { stockLocationLoader } from "./loader"
|
|||||||
|
|
||||||
export function ShippingOptionsCreate() {
|
export function ShippingOptionsCreate() {
|
||||||
const { fset_id, zone_id } = useParams()
|
const { fset_id, zone_id } = useParams()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const isReturn = searchParams.has("is_return")
|
||||||
|
|
||||||
const { stock_location: stockLocation } = useLoaderData() as Awaited<
|
const { stock_location: stockLocation } = useLoaderData() as Awaited<
|
||||||
ReturnType<typeof stockLocationLoader>
|
ReturnType<typeof stockLocationLoader>
|
||||||
>
|
>
|
||||||
@@ -20,7 +24,7 @@ export function ShippingOptionsCreate() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
<CreateShippingOptionsForm zone={zone} />
|
<CreateShippingOptionsForm zone={zone} isReturn={isReturn} />
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
AdminPostStockLocationsReq,
|
AdminPostStockLocationsReq,
|
||||||
AdminStockLocationsListRes,
|
AdminStockLocationsListRes,
|
||||||
AdminStockLocationsDeleteRes,
|
AdminStockLocationsDeleteRes,
|
||||||
|
AdminGetStockLocationsLocationParams,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { ResponsePromise } from "../../typings"
|
import { ResponsePromise } from "../../typings"
|
||||||
import BaseResource from "../base"
|
import BaseResource from "../base"
|
||||||
@@ -65,9 +66,16 @@ class AdminStockLocationsResource extends BaseResource {
|
|||||||
*/
|
*/
|
||||||
retrieve(
|
retrieve(
|
||||||
itemId: string,
|
itemId: string,
|
||||||
|
query?: AdminGetStockLocationsLocationParams,
|
||||||
customHeaders: Record<string, any> = {}
|
customHeaders: Record<string, any> = {}
|
||||||
): ResponsePromise<AdminStockLocationsRes> {
|
): ResponsePromise<AdminStockLocationsRes> {
|
||||||
const path = `/admin/stock-locations/${itemId}`
|
let path = `/admin/stock-locations/${itemId}`
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const queryString = qs.stringify(query)
|
||||||
|
path = `${path}?${queryString}`
|
||||||
|
}
|
||||||
|
|
||||||
return this.client.request("GET", path, undefined, {}, customHeaders)
|
return this.client.request("GET", path, undefined, {}, customHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const defaultAdminShippingOptionFields = [
|
|||||||
"name",
|
"name",
|
||||||
"price_type",
|
"price_type",
|
||||||
"data",
|
"data",
|
||||||
|
"provider_id",
|
||||||
"metadata",
|
"metadata",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export const AdminUpdateShippingOption = z
|
|||||||
data: z.record(z.unknown()).optional(),
|
data: z.record(z.unknown()).optional(),
|
||||||
price_type: z.nativeEnum(ShippingOptionPriceTypeEnum).optional(),
|
price_type: z.nativeEnum(ShippingOptionPriceTypeEnum).optional(),
|
||||||
provider_id: z.string().optional(),
|
provider_id: z.string().optional(),
|
||||||
|
shipping_profile_id: z.string().optional(),
|
||||||
type: AdminCreateShippingOptionTypeObject.optional(),
|
type: AdminCreateShippingOptionTypeObject.optional(),
|
||||||
prices: AdminUpdateShippingOptionPriceWithCurrency.or(
|
prices: AdminUpdateShippingOptionPriceWithCurrency.or(
|
||||||
AdminUpdateShippingOptionPriceWithRegion
|
AdminUpdateShippingOptionPriceWithRegion
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ export interface CreateShippingOptionDTO {
|
|||||||
* The shipping option rules associated with the shipping option.
|
* The shipping option rules associated with the shipping option.
|
||||||
*/
|
*/
|
||||||
rules?: Omit<CreateShippingOptionRuleDTO, "shipping_option_id">[]
|
rules?: Omit<CreateShippingOptionRuleDTO, "shipping_option_id">[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shipping option pricing
|
||||||
|
*/
|
||||||
|
prices: (
|
||||||
|
| { currency_code: string; amount: number }
|
||||||
|
| { region_id: string; amount: number }
|
||||||
|
)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user