feat(dashboard): shipping management (#6995)
**What** - shipping flow - shipping profile pages - delete fulfillment set endpoint - delete shipping profile endpoint
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/core-flows": patch
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
feat(medusa, core-flows,types): delete fulfillment set, delete shipping profile
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IFulfillmentModuleService } from "@medusajs/types"
|
||||
import {
|
||||
IFulfillmentModuleService,
|
||||
IStockLocationServiceNext,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
@@ -15,7 +18,7 @@ medusaIntegrationTestRunner({
|
||||
},
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
let appContainer
|
||||
let service: IFulfillmentModuleService
|
||||
let service: IStockLocationServiceNext
|
||||
|
||||
beforeEach(async () => {
|
||||
appContainer = getContainer()
|
||||
@@ -25,6 +28,29 @@ medusaIntegrationTestRunner({
|
||||
service = appContainer.resolve(ModuleRegistrationName.STOCK_LOCATION)
|
||||
})
|
||||
|
||||
describe("POST /admin/fulfillment-sets/:id", () => {
|
||||
it("should delete a fulfillment set", async () => {
|
||||
const fulfillmentService: IFulfillmentModuleService =
|
||||
appContainer.resolve(ModuleRegistrationName.FULFILLMENT)
|
||||
|
||||
const set = await fulfillmentService.create({
|
||||
name: "Test fulfillment set",
|
||||
type: "pickup",
|
||||
})
|
||||
|
||||
const deleteResponse = await api.delete(
|
||||
`/admin/fulfillment-sets/${set.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(deleteResponse.data).toEqual({
|
||||
id: set.id,
|
||||
object: "fulfillment_set",
|
||||
deleted: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/fulfillment-sets/:id/service-zones", () => {
|
||||
it("should create, update, and delete a service zone for a fulfillment set", async () => {
|
||||
const stockLocationResponse = await api.post(
|
||||
|
||||
@@ -35,7 +35,7 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
describe("Admin - Shipping Profiles", () => {
|
||||
// TODO: Missing update and delete tests
|
||||
// TODO: Missing update tests
|
||||
it("should test the entire lifecycle of a shipping profile", async () => {
|
||||
const payload = {
|
||||
name: "test-profile-2023",
|
||||
@@ -84,6 +84,23 @@ medusaIntegrationTestRunner({
|
||||
created_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
|
||||
const { data } = await api.delete(
|
||||
`/admin/shipping-profiles/${shipping_profile.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(data).toEqual({
|
||||
id: retrievedProfile.id,
|
||||
object: "shipping_profile",
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
await api
|
||||
.get(`/admin/shipping-profiles/${shipping_profile.id}`, adminHeaders)
|
||||
.catch((err) => {
|
||||
expect(err.response.status).toEqual(404)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -205,7 +222,6 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
describe("DELETE /admin/shipping-profiles", () => {
|
||||
// TODO: Delete is not added yet
|
||||
it("deletes a shipping profile", async () => {
|
||||
expect.assertions(2)
|
||||
|
||||
|
||||
@@ -565,6 +565,90 @@
|
||||
"invalidEmail": "Email must be a valid email address."
|
||||
}
|
||||
},
|
||||
"shipping": {
|
||||
"title": "Shipping & Delivery",
|
||||
"domain": "Shipping & Delivery",
|
||||
"description": "Choose where you ship and how much you charge for shipping at checkout. Define shipping options specific for your locations.",
|
||||
"from": "Shipping from",
|
||||
"add": "Add shipping",
|
||||
"connectProvider": "Connect provider",
|
||||
"addZone": "Add shipping zone",
|
||||
"enablePickup": "Enable pickup",
|
||||
"enableDelivery": "Enable delivery",
|
||||
"noRecords" : {
|
||||
"action": "Add Location",
|
||||
"title": "No inventory locations",
|
||||
"message": "Please create an invnetory location first."
|
||||
},
|
||||
"create": {
|
||||
"title": "Add shipping for {{location}}",
|
||||
"delivery": "Delivery",
|
||||
"pickup": "Pickup",
|
||||
"type": "Shipping type"
|
||||
},
|
||||
"fulfillmentSet": {
|
||||
"placeholder": "Not covered by any shipping zones.",
|
||||
"delete": "Delete shipping",
|
||||
"create": {
|
||||
"title": "Add service zone for {{fulfillmentSet}}"
|
||||
},
|
||||
"addZone": "Add service zone",
|
||||
"pickup": {
|
||||
"title": "Pickup in",
|
||||
"enable": "Enable pickup"
|
||||
},
|
||||
"delivery": {
|
||||
"title": "Shipping to",
|
||||
"enable": "Enable delivery"
|
||||
}
|
||||
},
|
||||
"serviceZone": {
|
||||
"create": {
|
||||
"title": "Add service zone for {{fulfillmentSet}}",
|
||||
"subtitle": "Service zone",
|
||||
"description": "A service zone is a geographical region that can be shipped to from a specific location. You can later on add any number of shipping options to this zone. ",
|
||||
"zoneName": "Zone name"
|
||||
},
|
||||
"editPrices": "Edit prices",
|
||||
"editOption": "Edit option",
|
||||
"optionsLength_one": "shipping option",
|
||||
"optionsLength_other": "shipping options",
|
||||
"shippingOptionsPlaceholder": "Not covered by any shipping options.",
|
||||
"addShippingOptions": "Add shipping options",
|
||||
"shippingOptions": "Shipping options",
|
||||
"returnOptions": "Return options",
|
||||
"areas": {
|
||||
"title": "Areas affected by this rule",
|
||||
"description": "Select the geographical areas where this shipping zone should apply.",
|
||||
"manage": "Manage areas",
|
||||
"error": "Please select at least one country for this service zone."
|
||||
}
|
||||
},
|
||||
"shippingOptions": {
|
||||
"create": {
|
||||
"title": "Create shipping options for {{zone}}",
|
||||
"subtitle": "General information",
|
||||
"description": "To start selling, all you need is a name and a price",
|
||||
"details": "Details",
|
||||
"pricing": "Pricing",
|
||||
"allocation": "Shipping amount",
|
||||
"fixed": "Fixed",
|
||||
"fixedDescription": "Shipping option's price is always the same amount.",
|
||||
"enable": "Enable in store",
|
||||
"enableDescription": "Enable or disable the shipping option visiblity in store",
|
||||
"calculated": "Calculated",
|
||||
"calculatedDescription": "Shipping option's price is calculated by the fulfillment provider.",
|
||||
"profile": "Shipping profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shippingProfile": {
|
||||
"domain": "Shipping Profiles",
|
||||
"title": "Create a shipping profile",
|
||||
"detailsHint": "Specify the details of the shipping profile",
|
||||
"deleteWaring": "You are about to delete the profile: {{name}}. This action cannot be undone.",
|
||||
"typeHint": "Enter shipping profile type, for example: Express, Freight, etc."
|
||||
},
|
||||
"discounts": {
|
||||
"domain": "Discounts",
|
||||
"startDate": "Start date",
|
||||
@@ -1283,6 +1367,7 @@
|
||||
"issuedDate": "Issued date",
|
||||
"expiryDate": "Expiry date",
|
||||
"price": "Price",
|
||||
"priceTemplate": "Price {{regionOrCountry}}",
|
||||
"height": "Height",
|
||||
"width": "Width",
|
||||
"length": "Length",
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Buildings,
|
||||
ChevronDownMini,
|
||||
CurrencyDollar,
|
||||
Envelope,
|
||||
MinusMini,
|
||||
ReceiptPercent,
|
||||
ShoppingCart,
|
||||
@@ -143,6 +144,17 @@ const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
|
||||
label: t("pricing.domain"),
|
||||
to: "/pricing",
|
||||
},
|
||||
{
|
||||
icon: <Envelope />,
|
||||
label: t("shipping.domain"),
|
||||
to: "/shipping",
|
||||
items: [
|
||||
{
|
||||
label: t("shippingProfile.domain"),
|
||||
to: "/shipping-profiles",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useMutation, UseMutationOptions } from "@tanstack/react-query"
|
||||
|
||||
import {
|
||||
ShippingOptionDeleteRes,
|
||||
ShippingOptionRes,
|
||||
} from "../../types/api-responses"
|
||||
import { CreateShippingOptionReq } from "../../types/api-payloads"
|
||||
import { stockLocationsQueryKeys } from "./stock-locations"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { client } from "../../lib/client"
|
||||
|
||||
export const useCreateShippingOptions = (
|
||||
options?: UseMutationOptions<
|
||||
ShippingOptionRes,
|
||||
Error,
|
||||
CreateShippingOptionReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.shippingOptions.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteShippingOption = (
|
||||
optionId: string,
|
||||
options?: UseMutationOptions<ShippingOptionDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.shippingOptions.delete(optionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
QueryKey,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query"
|
||||
import { CreateShippingProfileReq } from "../../types/api-payloads"
|
||||
import {
|
||||
ShippingProfileDeleteRes,
|
||||
ShippingProfileListRes,
|
||||
ShippingProfileRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const SHIPPING_PROFILE_QUERY_KEY = "shipping_profile" as const
|
||||
export const shippingProfileQueryKeys = queryKeysFactory(
|
||||
SHIPPING_PROFILE_QUERY_KEY
|
||||
)
|
||||
|
||||
export const useCreateShippingProfile = (
|
||||
options?: UseMutationOptions<
|
||||
ShippingProfileRes,
|
||||
Error,
|
||||
CreateShippingProfileReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.shippingProfiles.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: shippingProfileQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useShippingProfiles = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
ShippingProfileListRes,
|
||||
Error,
|
||||
ShippingProfileListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.shippingProfiles.list(query),
|
||||
queryKey: shippingProfileQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useDeleteShippingProfile = (
|
||||
profileId: string,
|
||||
options?: UseMutationOptions<ShippingProfileDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.shippingProfiles.delete(profileId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: shippingProfileQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -10,17 +10,23 @@ import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import {
|
||||
CreateFulfillmentSetReq,
|
||||
CreateServiceZoneReq,
|
||||
CreateStockLocationReq,
|
||||
UpdateStockLocationReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
FulfillmentSetDeleteRes,
|
||||
ServiceZoneDeleteRes,
|
||||
StockLocationDeleteRes,
|
||||
StockLocationListRes,
|
||||
StockLocationRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const STOCK_LOCATIONS_QUERY_KEY = "stock_locations" as const
|
||||
const stockLocationsQueryKeys = queryKeysFactory(STOCK_LOCATIONS_QUERY_KEY)
|
||||
export const stockLocationsQueryKeys = queryKeysFactory(
|
||||
STOCK_LOCATIONS_QUERY_KEY
|
||||
)
|
||||
|
||||
export const useStockLocation = (
|
||||
id: string,
|
||||
@@ -115,3 +121,79 @@ export const useDeleteStockLocation = (
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateFulfillmentSet = (
|
||||
locationId: string,
|
||||
options?: UseMutationOptions<StockLocationRes, Error, CreateFulfillmentSetReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) =>
|
||||
client.stockLocations.createFulfillmentSet(locationId, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.detail(locationId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateServiceZone = (
|
||||
locationId: string,
|
||||
fulfillmentSetId: string,
|
||||
options?: UseMutationOptions<StockLocationRes, Error, CreateServiceZoneReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) =>
|
||||
client.stockLocations.createServiceZone(fulfillmentSetId, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.detail(locationId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteFulfillmentSet = (
|
||||
setId: string,
|
||||
options?: UseMutationOptions<FulfillmentSetDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.stockLocations.deleteFulfillmentSet(setId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteServiceZone = (
|
||||
setId: string,
|
||||
zoneId: string,
|
||||
options?: UseMutationOptions<ServiceZoneDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.stockLocations.deleteServiceZone(setId, zoneId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,12 +15,14 @@ import { products } from "./products"
|
||||
import { promotions } from "./promotions"
|
||||
import { regions } from "./regions"
|
||||
import { salesChannels } from "./sales-channels"
|
||||
import { shippingOptions } from "./shipping-options"
|
||||
import { stockLocations } from "./stock-locations"
|
||||
import { stores } from "./stores"
|
||||
import { tags } from "./tags"
|
||||
import { taxes } from "./taxes"
|
||||
import { users } from "./users"
|
||||
import { workflowExecutions } from "./workflow-executions"
|
||||
import { shippingProfiles } from "./shipping-profiles"
|
||||
|
||||
export const client = {
|
||||
auth: auth,
|
||||
@@ -35,6 +37,8 @@ export const client = {
|
||||
payments: payments,
|
||||
stores: stores,
|
||||
salesChannels: salesChannels,
|
||||
shippingOptions: shippingOptions,
|
||||
shippingProfiles: shippingProfiles,
|
||||
tags: tags,
|
||||
users: users,
|
||||
regions: regions,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { deleteRequest, postRequest } from "./common"
|
||||
import {
|
||||
ShippingOptionDeleteRes,
|
||||
ShippingOptionRes,
|
||||
} from "../../types/api-responses"
|
||||
import { CreateShippingOptionReq } from "../../types/api-payloads"
|
||||
|
||||
async function createShippingOptions(payload: CreateShippingOptionReq) {
|
||||
return postRequest<ShippingOptionRes>(`/admin/shipping-options`, payload)
|
||||
}
|
||||
|
||||
async function deleteShippingOption(optionId: string) {
|
||||
return deleteRequest<ShippingOptionDeleteRes>(
|
||||
`/admin/shipping-options/${optionId}`
|
||||
)
|
||||
}
|
||||
|
||||
export const shippingOptions = {
|
||||
create: createShippingOptions,
|
||||
delete: deleteShippingOption,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
import { CreateShippingProfileReq } from "../../types/api-payloads"
|
||||
import {
|
||||
ShippingProfileDeleteRes,
|
||||
ShippingProfileListRes,
|
||||
ShippingProfileRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
async function createShippingProfile(payload: CreateShippingProfileReq) {
|
||||
return postRequest<ShippingProfileRes>(`/admin/shipping-profiles`, payload)
|
||||
}
|
||||
|
||||
async function listShippingProfiles(query?: Record<string, any>) {
|
||||
return getRequest<ShippingProfileListRes>(`/admin/shipping-profiles`, query)
|
||||
}
|
||||
|
||||
async function deleteShippingProfile(profileId: string) {
|
||||
return deleteRequest<ShippingProfileDeleteRes>(
|
||||
`/admin/shipping-profiles/${profileId}`
|
||||
)
|
||||
}
|
||||
|
||||
export const shippingProfiles = {
|
||||
create: createShippingProfile,
|
||||
list: listShippingProfiles,
|
||||
delete: deleteShippingProfile,
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import {
|
||||
CreateFulfillmentSetReq,
|
||||
CreateServiceZoneReq,
|
||||
CreateStockLocationReq,
|
||||
UpdateStockLocationReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
FulfillmentSetDeleteRes,
|
||||
ServiceZoneDeleteRes,
|
||||
StockLocationDeleteRes,
|
||||
StockLocationListRes,
|
||||
StockLocationRes,
|
||||
@@ -21,6 +25,26 @@ async function createStockLocation(payload: CreateStockLocationReq) {
|
||||
return postRequest<StockLocationRes>(`/admin/stock-locations`, payload)
|
||||
}
|
||||
|
||||
async function createFulfillmentSet(
|
||||
locationId: string,
|
||||
payload: CreateFulfillmentSetReq
|
||||
) {
|
||||
return postRequest<StockLocationRes>(
|
||||
`/admin/stock-locations/${locationId}/fulfillment-sets`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function createServiceZone(
|
||||
fulfillmentSetId: string,
|
||||
payload: CreateServiceZoneReq
|
||||
) {
|
||||
return postRequest<StockLocationRes>(
|
||||
`/admin/fulfillment-sets/${fulfillmentSetId}/service-zones`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function updateStockLocation(
|
||||
id: string,
|
||||
payload: UpdateStockLocationReq
|
||||
@@ -32,10 +56,26 @@ async function deleteStockLocation(id: string) {
|
||||
return deleteRequest<StockLocationDeleteRes>(`/admin/stock-locations/${id}`)
|
||||
}
|
||||
|
||||
async function deleteFulfillmentSet(setId: string) {
|
||||
return deleteRequest<FulfillmentSetDeleteRes>(
|
||||
`/admin/fulfillment-sets/${setId}`
|
||||
)
|
||||
}
|
||||
|
||||
async function deleteServiceZone(setId: string, zoneId: string) {
|
||||
return deleteRequest<ServiceZoneDeleteRes>(
|
||||
`/admin/fulfillment-sets/${setId}/service-zones/${zoneId}`
|
||||
)
|
||||
}
|
||||
|
||||
export const stockLocations = {
|
||||
list: listStockLocations,
|
||||
retrieve: retrieveStockLocation,
|
||||
create: createStockLocation,
|
||||
update: updateStockLocation,
|
||||
delete: deleteStockLocation,
|
||||
createFulfillmentSet,
|
||||
deleteFulfillmentSet,
|
||||
createServiceZone,
|
||||
deleteServiceZone,
|
||||
}
|
||||
|
||||
@@ -291,6 +291,59 @@ 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: "shipping-profiles",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/shipping/shipping-profiles-list"),
|
||||
handle: {
|
||||
crumb: () => "Shipping Profiles",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/shipping/shipping-profile-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/customers",
|
||||
handle: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CreateApiKeyDTO,
|
||||
CreateCampaignDTO,
|
||||
CreateCustomerDTO,
|
||||
CreateFulfillmentSetDTO,
|
||||
CreateInviteDTO,
|
||||
CreatePriceListDTO,
|
||||
CreateProductCollectionDTO,
|
||||
@@ -13,6 +14,9 @@ import {
|
||||
CreatePromotionRuleDTO,
|
||||
CreateRegionDTO,
|
||||
CreateSalesChannelDTO,
|
||||
CreateServiceZoneDTO,
|
||||
CreateShippingOptionDTO,
|
||||
CreateShippingProfileDTO,
|
||||
CreateStockLocationInput,
|
||||
InventoryNext,
|
||||
UpdateApiKeyDTO,
|
||||
@@ -62,6 +66,14 @@ export type CreateInviteReq = CreateInviteDTO
|
||||
// Stock Locations
|
||||
export type CreateStockLocationReq = CreateStockLocationInput
|
||||
export type UpdateStockLocationReq = UpdateStockLocationInput
|
||||
export type CreateFulfillmentSetReq = CreateFulfillmentSetDTO
|
||||
export type CreateServiceZoneReq = CreateServiceZoneDTO
|
||||
|
||||
// Shipping Options
|
||||
export type CreateShippingOptionReq = CreateShippingOptionDTO
|
||||
|
||||
// Shipping Profile
|
||||
export type CreateShippingProfileReq = CreateShippingProfileDTO
|
||||
|
||||
// Product Collections
|
||||
export type CreateProductCollectionReq = CreateProductCollectionDTO
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
PromotionDTO,
|
||||
RegionDTO,
|
||||
SalesChannelDTO,
|
||||
ShippingOptionDTO,
|
||||
ShippingProfileDTO,
|
||||
StockLocationAddressDTO,
|
||||
StockLocationDTO,
|
||||
StoreDTO,
|
||||
@@ -128,8 +130,21 @@ export type StockLocationListRes = {
|
||||
stock_locations: ExtendedStockLocationDTO[]
|
||||
} & ListRes
|
||||
export type StockLocationDeleteRes = DeleteRes
|
||||
export type FulfillmentSetDeleteRes = DeleteRes
|
||||
export type ServiceZoneDeleteRes = DeleteRes
|
||||
|
||||
// Worfklow Executions
|
||||
// Shipping options
|
||||
export type ShippingOptionRes = { shipping_option: ShippingOptionDTO }
|
||||
export type ShippingOptionDeleteRes = DeleteRes
|
||||
|
||||
// Shipping profile
|
||||
export type ShippingProfileRes = { shipping_profile: ShippingProfileDTO }
|
||||
export type ShippingProfileListRes = {
|
||||
shipping_profiles: ShippingProfileDTO[]
|
||||
} & ListRes
|
||||
export type ShippingProfileDeleteRes = DeleteRes
|
||||
|
||||
// Workflow Executions
|
||||
export type WorkflowExecutionRes = { workflow_execution: WorkflowExecutionDTO }
|
||||
export type WorkflowExecutionListRes = {
|
||||
workflow_executions: WorkflowExecutionDTO[]
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./location"
|
||||
+392
@@ -0,0 +1,392 @@
|
||||
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
|
||||
@@ -0,0 +1,2 @@
|
||||
export { shippingListLoader as loader } from "./loader"
|
||||
export { LocationList as Component } from "./location-list"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
import { adminStockLocationsKeys } from "medusa-react"
|
||||
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
import { StockLocationListRes } from "../../../types/api-responses"
|
||||
|
||||
const shippingListQuery = () => ({
|
||||
queryKey: adminStockLocationsKeys.lists(),
|
||||
queryFn: async () =>
|
||||
client.stockLocations.list({
|
||||
// fields: "*fulfillment_sets,*fulfillment_sets.service_zones",
|
||||
// TODO: change this when RQ is fixed to work with the upper fields definition
|
||||
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",
|
||||
}),
|
||||
})
|
||||
|
||||
export const shippingListLoader = async (_: LoaderFunctionArgs) => {
|
||||
const query = shippingListQuery()
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<StockLocationListRes>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
ColumnDef,
|
||||
createColumnHelper,
|
||||
RowSelectionState,
|
||||
} from "@tanstack/react-table"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Alert, Button, Checkbox, Heading, Input, Text } from "@medusajs/ui"
|
||||
import { FulfillmentSetDTO, RegionCountryDTO, RegionDTO } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Map } from "@medusajs/icons"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { SplitView } from "../../../../../components/layout/split-view"
|
||||
import { useCreateServiceZone } from "../../../../../hooks/api/stock-locations"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query"
|
||||
import { useCountries } from "../../../../regions/common/hooks/use-countries"
|
||||
import { countries as staticCountries } from "../../../../../lib/countries"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||
|
||||
const PREFIX = "ac"
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
const ConditionsFooter = ({ onSave }: { onSave: () => void }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
||||
<SplitView.Close type="button" asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</SplitView.Close>
|
||||
<Button size="small" type="button" onClick={onSave}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CreateServiceZoneSchema = zod.object({
|
||||
name: zod.string().min(1),
|
||||
countries: zod.array(zod.string().length(2)).min(1),
|
||||
})
|
||||
|
||||
type CreateServiceZoneFormProps = {
|
||||
fulfillmentSet: FulfillmentSetDTO
|
||||
locationId: string
|
||||
}
|
||||
|
||||
export function CreateServiceZoneForm({
|
||||
fulfillmentSet,
|
||||
locationId,
|
||||
}: CreateServiceZoneFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateServiceZoneSchema>>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
countries: [],
|
||||
},
|
||||
resolver: zodResolver(CreateServiceZoneSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync: createServiceZone, isPending: isLoading } =
|
||||
useCreateServiceZone(locationId, fulfillmentSet.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await createServiceZone({
|
||||
name: data.name,
|
||||
geo_zones: data.countries.map((iso2) => ({
|
||||
country_code: iso2,
|
||||
type: "country",
|
||||
})),
|
||||
})
|
||||
|
||||
handleSuccess("/shipping")
|
||||
})
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const { searchParams, raw } = useCountryTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
const { countries, count } = useCountries({
|
||||
countries: staticCountries.map((c, i) => ({
|
||||
display_name: c.display_name,
|
||||
name: c.name,
|
||||
id: i as any,
|
||||
iso_2: c.iso_2,
|
||||
iso_3: c.iso_3,
|
||||
num_code: c.num_code,
|
||||
region_id: null,
|
||||
region: {} as RegionDTO,
|
||||
})),
|
||||
...searchParams,
|
||||
})
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: countries || [],
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
getRowId: (row) => row.iso_2,
|
||||
pageSize: PAGE_SIZE,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater: setRowSelection,
|
||||
},
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const onCountriesSave = () => {
|
||||
form.setValue("countries", Object.keys(rowSelection))
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const countriesWatch = form.watch("countries")
|
||||
|
||||
const selectedCountries = useMemo(() => {
|
||||
return staticCountries.filter((c) => c.iso_2 in rowSelection)
|
||||
}, [countriesWatch])
|
||||
|
||||
useEffect(() => {
|
||||
// set selected rows from form state on open
|
||||
if (open) {
|
||||
setRowSelection(
|
||||
countriesWatch.reduce((acc, c) => {
|
||||
acc[c] = true
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const showAreasError =
|
||||
form.formState.errors["countries"]?.type === "too_small"
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
|
||||
<RouteFocusModal.Body className="m-auto flex h-full w-full flex-col items-center divide-y overflow-hidden">
|
||||
<SplitView open={open} onOpenChange={handleOpenChange}>
|
||||
<SplitView.Content className="mx-auto max-w-[720px]">
|
||||
<div className="container w-fit px-1 py-8">
|
||||
<Heading className="mb-12 mt-8 text-2xl">
|
||||
{t("shipping.fulfillmentSet.create.title", {
|
||||
fulfillmentSet: fulfillmentSet.name,
|
||||
})}
|
||||
</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">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("shipping.serviceZone.create.zoneName")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*AREAS*/}
|
||||
<div className="container flex items-center justify-between py-8">
|
||||
<div>
|
||||
<Text weight="plus">
|
||||
{t("shipping.serviceZone.areas.title")}
|
||||
</Text>
|
||||
<Text className="text-ui-fg-subtle mt-2">
|
||||
{t("shipping.serviceZone.areas.description")}
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
>
|
||||
{t("shipping.serviceZone.areas.manage")}
|
||||
</Button>
|
||||
</div>
|
||||
{!!selectedCountries.length && (
|
||||
<div className="flex items-center gap-4">
|
||||
<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>
|
||||
<ListSummary
|
||||
inline
|
||||
list={selectedCountries.map((c) => c.display_name)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showAreasError && (
|
||||
<Alert dismissible variant="error">
|
||||
{t("shipping.serviceZone.areas.error")}
|
||||
</Alert>
|
||||
)}
|
||||
</SplitView.Content>
|
||||
<SplitView.Drawer>
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
search
|
||||
pagination
|
||||
layout="fill"
|
||||
orderBy={["name", "code"]}
|
||||
queryObject={raw}
|
||||
prefix={PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={onCountriesSave} />
|
||||
</div>
|
||||
</SplitView.Drawer>
|
||||
</SplitView>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<RegionCountryDTO>()
|
||||
|
||||
const useColumns = () => {
|
||||
const base = useCountryTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isPreselected = !row.getCanSelect()
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected() || isPreselected}
|
||||
disabled={isPreselected}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
],
|
||||
[base]
|
||||
) as ColumnDef<RegionCountryDTO>[]
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./create-service-zone-form"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ServiceZoneCreate as Component } from "./service-zone-create"
|
||||
export { stockLocationLoader as loader } from "./loader"
|
||||
@@ -0,0 +1,24 @@
|
||||
import { adminStockLocationsKeys } from "medusa-react"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
import { StockLocationRes } from "../../../types/api-responses"
|
||||
|
||||
const fulfillmentSetCreateQuery = (id: string) => ({
|
||||
queryKey: adminStockLocationsKeys.detail(id),
|
||||
queryFn: async () =>
|
||||
client.stockLocations.retrieve(id, {
|
||||
fields: "*fulfillment_sets",
|
||||
}),
|
||||
})
|
||||
|
||||
export const stockLocationLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.location_id
|
||||
const query = fulfillmentSetCreateQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<StockLocationRes>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateServiceZoneForm } from "./components/create-service-zone-form"
|
||||
import { stockLocationLoader } from "./loader"
|
||||
|
||||
export function ServiceZoneCreate() {
|
||||
const { fset_id, location_id } = useParams()
|
||||
const { stock_location: stockLocation } = useLoaderData() as Awaited<
|
||||
ReturnType<typeof stockLocationLoader>
|
||||
>
|
||||
|
||||
const fulfillmentSet = stockLocation.fulfillment_sets.find(
|
||||
(f) => f.id === fset_id
|
||||
)
|
||||
|
||||
if (!fulfillmentSet) {
|
||||
throw new Error("Fulfillment set doesn't exist")
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateServiceZoneForm
|
||||
locationId={location_id!}
|
||||
fulfillmentSet={fulfillmentSet}
|
||||
/>
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import React, { useEffect } from "react"
|
||||
import * as zod from "zod"
|
||||
|
||||
import {
|
||||
Button,
|
||||
Heading,
|
||||
Input,
|
||||
ProgressStatus,
|
||||
ProgressTabs,
|
||||
RadioGroup,
|
||||
Switch,
|
||||
Text,
|
||||
clx,
|
||||
Select,
|
||||
} from "@medusajs/ui"
|
||||
import { ServiceZoneDTO } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { CreateShippingOptionsPricesForm } from "./create-shipping-options-prices-form"
|
||||
import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
|
||||
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
enum Tab {
|
||||
DETAILS = "details",
|
||||
PRICING = "pricing",
|
||||
}
|
||||
|
||||
enum ShippingAllocation {
|
||||
FlatRate = "flat",
|
||||
Calculated = "calculated",
|
||||
}
|
||||
|
||||
type StepStatus = {
|
||||
[key in Tab]: ProgressStatus
|
||||
}
|
||||
|
||||
const CreateServiceZoneSchema = zod.object({
|
||||
name: zod.string().min(1),
|
||||
price_type: zod.nativeEnum(ShippingAllocation),
|
||||
enable_in_store: zod.boolean().optional(),
|
||||
shipping_profile_id: zod.string(),
|
||||
region_prices: zod.record(zod.string(), zod.string().optional()),
|
||||
currency_prices: zod.record(zod.string(), zod.string().optional()),
|
||||
})
|
||||
|
||||
type CreateServiceZoneFormProps = {
|
||||
zone: ServiceZoneDTO
|
||||
}
|
||||
|
||||
export function CreateShippingOptionsForm({
|
||||
zone,
|
||||
}: CreateServiceZoneFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const [tab, setTab] = React.useState<Tab>(Tab.DETAILS)
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateServiceZoneSchema>>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
price_type: ShippingAllocation.FlatRate,
|
||||
enable_in_store: true,
|
||||
shipping_profile_id: "",
|
||||
region_prices: {},
|
||||
currency_prices: {},
|
||||
},
|
||||
resolver: zodResolver(CreateServiceZoneSchema),
|
||||
})
|
||||
|
||||
const isCalculatedPriceType =
|
||||
form.watch("price_type") === ShippingAllocation.Calculated
|
||||
|
||||
const { mutateAsync: createShippingOption, isPending: isLoading } =
|
||||
useCreateShippingOptions()
|
||||
|
||||
const { shipping_profiles: shippingProfiles } = useShippingProfiles({
|
||||
limit: 999,
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const currencyPrices = Object.entries(data.currency_prices)
|
||||
.map(([code, value]) => {
|
||||
const amount =
|
||||
value === "" ? undefined : getDbAmount(Number(value), code)
|
||||
|
||||
return {
|
||||
currency_code: code,
|
||||
amount: amount,
|
||||
}
|
||||
})
|
||||
.filter((o) => !!o.amount)
|
||||
|
||||
/**
|
||||
* TODO: region prices
|
||||
*/
|
||||
// Object.entries(data.region_prices).map(([region_id, value]) => {})
|
||||
|
||||
await createShippingOption({
|
||||
name: data.name,
|
||||
price_type: data.price_type,
|
||||
service_zone_id: zone.id,
|
||||
shipping_profile_id: data.shipping_profile_id,
|
||||
provider_id: "manual_test-provider", // TODO: FETCH PROVIDERS
|
||||
prices: [...currencyPrices],
|
||||
type: {
|
||||
// TODO: FETCH TYPES
|
||||
label: "Type label",
|
||||
description: "Type description",
|
||||
code: "type-code",
|
||||
},
|
||||
})
|
||||
|
||||
handleSuccess("/shipping")
|
||||
})
|
||||
|
||||
const [status, setStatus] = React.useState<StepStatus>({
|
||||
[Tab.PRICING]: "not-started",
|
||||
[Tab.DETAILS]: "not-started",
|
||||
})
|
||||
|
||||
const onTabChange = React.useCallback(async (value: Tab) => {
|
||||
setTab(value)
|
||||
}, [])
|
||||
|
||||
const onNext = React.useCallback(async () => {
|
||||
switch (tab) {
|
||||
case Tab.DETAILS: {
|
||||
setTab(Tab.PRICING)
|
||||
break
|
||||
}
|
||||
case Tab.PRICING:
|
||||
break
|
||||
}
|
||||
}, [tab])
|
||||
|
||||
const canMoveToPricing =
|
||||
form.watch("name").length && form.watch("shipping_profile_id")
|
||||
|
||||
useEffect(() => {
|
||||
if (form.formState.isDirty) {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||
} else {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "not-started" }))
|
||||
}
|
||||
}, [form.formState.isDirty])
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === Tab.DETAILS && form.formState.isDirty) {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||
}
|
||||
|
||||
if (tab === Tab.PRICING) {
|
||||
const hasPricingSet = form
|
||||
.getValues(["region_prices", "currency_prices"])
|
||||
.map(Object.keys)
|
||||
.some((i) => i.length)
|
||||
|
||||
setStatus((prev) => ({
|
||||
...prev,
|
||||
[Tab.DETAILS]: "completed",
|
||||
[Tab.PRICING]: hasPricingSet ? "in-progress" : "not-started",
|
||||
}))
|
||||
}
|
||||
}, [tab])
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<ProgressTabs
|
||||
value={tab}
|
||||
className="h-full"
|
||||
onValueChange={(tab) => onTabChange(tab as Tab)}
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<ProgressTabs.List className="border-ui-border-base -my-2 ml-2 min-w-0 flex-1 border-l">
|
||||
<ProgressTabs.Trigger
|
||||
value={Tab.DETAILS}
|
||||
status={status[Tab.DETAILS]}
|
||||
className="w-full max-w-[200px]"
|
||||
>
|
||||
<span className="w-full cursor-auto overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{t("shipping.shippingOptions.create.details")}
|
||||
</span>
|
||||
</ProgressTabs.Trigger>
|
||||
{!isCalculatedPriceType && (
|
||||
<ProgressTabs.Trigger
|
||||
value={Tab.PRICING}
|
||||
className="w-full max-w-[200px]"
|
||||
status={status[Tab.PRICING]}
|
||||
disabled={!canMoveToPricing}
|
||||
>
|
||||
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{t("shipping.shippingOptions.create.pricing")}
|
||||
</span>
|
||||
</ProgressTabs.Trigger>
|
||||
)}
|
||||
</ProgressTabs.List>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
size="small"
|
||||
className="whitespace-nowrap"
|
||||
isLoading={isLoading}
|
||||
onClick={onNext}
|
||||
disabled={!canMoveToPricing}
|
||||
key={
|
||||
tab === Tab.PRICING || isCalculatedPriceType
|
||||
? "details"
|
||||
: "pricing"
|
||||
}
|
||||
type={
|
||||
tab === Tab.PRICING || isCalculatedPriceType
|
||||
? "submit"
|
||||
: "button"
|
||||
}
|
||||
>
|
||||
{tab === Tab.PRICING || isCalculatedPriceType
|
||||
? t("actions.save")
|
||||
: t("general.next")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
|
||||
<RouteFocusModal.Body
|
||||
className={clx(
|
||||
"flex h-full w-fit flex-col items-center divide-y overflow-hidden",
|
||||
{ "mx-auto": tab === Tab.DETAILS }
|
||||
)}
|
||||
>
|
||||
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
||||
<div className="container mx-auto w-[720px] px-1 py-8">
|
||||
<Heading className="mb-12 mt-8 text-2xl">
|
||||
{t("shipping.shippingOptions.create.title", {
|
||||
zone: zone.name,
|
||||
})}
|
||||
</Heading>
|
||||
|
||||
<div>
|
||||
<Text weight="plus">
|
||||
{t("shipping.shippingOptions.create.subtitle")}
|
||||
</Text>
|
||||
<Text className="text-ui-fg-subtle mb-8 mt-2">
|
||||
{t("shipping.shippingOptions.create.description")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<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
|
||||
className="flex justify-between gap-4"
|
||||
{...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="mt-4 flex flex-col divide-y">
|
||||
<div className="mt-4 grid grid-cols-2 gap-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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 pt-8">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="enable_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>
|
||||
</ProgressTabs.Content>
|
||||
|
||||
<ProgressTabs.Content
|
||||
value={Tab.PRICING}
|
||||
className="h-full w-full"
|
||||
style={{ width: "100vw" }}
|
||||
>
|
||||
<CreateShippingOptionsPricesForm form={form} />
|
||||
</ProgressTabs.Content>
|
||||
</RouteFocusModal.Body>
|
||||
</ProgressTabs>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { PropsWithChildren } from "react"
|
||||
import { CurrencyDTO, ProductVariantDTO, RegionDTO } from "@medusajs/types"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { CurrencyCell } from "../../../../../components/grid/grid-cells/common/currency-cell"
|
||||
import { DataGrid } from "../../../../../components/grid/data-grid"
|
||||
import { DataGridMeta } from "../../../../../components/grid/types"
|
||||
import { useCurrencies } from "../../../../../hooks/api/currencies"
|
||||
import { ExtendedProductDTO } from "../../../../../types/api-responses"
|
||||
import { useRegions } from "../../../../../hooks/api/regions"
|
||||
import { useStore } from "../../../../../hooks/api/store"
|
||||
|
||||
const PricingCreateSchemaType = zod.record(
|
||||
zod.object({
|
||||
currency_prices: zod.record(zod.string().optional()),
|
||||
region_prices: zod.record(zod.string().optional()),
|
||||
})
|
||||
)
|
||||
|
||||
type PricingPricesFormProps = {
|
||||
form: UseFormReturn<typeof PricingCreateSchemaType>
|
||||
}
|
||||
|
||||
enum ColumnType {
|
||||
REGION = "region",
|
||||
CURRENCY = "currency",
|
||||
}
|
||||
|
||||
type EnabledColumnRecord = Record<string, ColumnType>
|
||||
|
||||
export const CreateShippingOptionsPricesForm = ({
|
||||
form,
|
||||
}: PricingPricesFormProps) => {
|
||||
const {
|
||||
store,
|
||||
isLoading: isStoreLoading,
|
||||
isError: isStoreError,
|
||||
error: storeError,
|
||||
} = useStore()
|
||||
const { currencies, isLoading: isCurrenciesLoading } = useCurrencies(
|
||||
{
|
||||
code: store?.supported_currency_codes,
|
||||
},
|
||||
{
|
||||
enabled: !!store,
|
||||
}
|
||||
)
|
||||
|
||||
const { regions } = useRegions()
|
||||
|
||||
const [enabledColumns, setEnabledColumns] = useState<EnabledColumnRecord>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
store?.default_currency_code &&
|
||||
Object.keys(enabledColumns).length === 0
|
||||
) {
|
||||
setEnabledColumns({
|
||||
...enabledColumns,
|
||||
[store.default_currency_code]: ColumnType.CURRENCY,
|
||||
})
|
||||
}
|
||||
}, [store, enabledColumns])
|
||||
|
||||
const columns = useColumns({
|
||||
currencies,
|
||||
regions,
|
||||
})
|
||||
|
||||
const initializing =
|
||||
isStoreLoading || isCurrenciesLoading || !store || !currencies
|
||||
|
||||
if (isStoreError) {
|
||||
throw storeError
|
||||
}
|
||||
|
||||
const data = useMemo(
|
||||
() => [[...(currencies || []), ...(regions || [])]],
|
||||
[currencies, regions]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
isLoading={initializing}
|
||||
state={form}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<
|
||||
ExtendedProductDTO | ProductVariantDTO
|
||||
>()
|
||||
|
||||
const useColumns = ({
|
||||
currencies = [],
|
||||
regions = [],
|
||||
}: {
|
||||
currencies?: CurrencyDTO[]
|
||||
regions?: RegionDTO[]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const colDefs: ColumnDef<ExtendedProductDTO | ProductVariantDTO>[] =
|
||||
useMemo(() => {
|
||||
return [
|
||||
...currencies.map((currency) => {
|
||||
return columnHelper.display({
|
||||
header: t("fields.priceTemplate", {
|
||||
regionOrCountry: currency.code.toUpperCase(),
|
||||
}),
|
||||
cell: ({ row, table }) => {
|
||||
return (
|
||||
<CurrencyCell
|
||||
currency={currency}
|
||||
meta={table.options.meta as DataGridMeta<any>}
|
||||
field={`currency_prices.${currency.code}`}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
}),
|
||||
...regions.map((region) => {
|
||||
return columnHelper.display({
|
||||
header: t("fields.priceTemplate", {
|
||||
regionOrCountry: region.name,
|
||||
}),
|
||||
cell: ({ row, table }) => {
|
||||
return (
|
||||
<CurrencyCell
|
||||
currency={currencies.find(
|
||||
(c) => c.code === region.currency_code
|
||||
)}
|
||||
meta={table.options.meta as DataGridMeta<any>}
|
||||
field={`region_prices.${region.id}`}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
}),
|
||||
]
|
||||
}, [t, currencies, regions])
|
||||
|
||||
return colDefs
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./create-shipping-options-form.tsx"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ShippingOptionsCreate as Component } from "./shipping-options-create"
|
||||
export { stockLocationLoader as loader } from "./loader"
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { adminStockLocationsKeys } from "medusa-react"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
import { StockLocationRes } from "../../../types/api-responses"
|
||||
|
||||
const fulfillmentSetCreateQuery = (id: string) => ({
|
||||
queryKey: adminStockLocationsKeys.list(), // Use the list cache key for now
|
||||
queryFn: async () =>
|
||||
client.stockLocations.retrieve(id, {
|
||||
fields:
|
||||
"*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options",
|
||||
}),
|
||||
})
|
||||
|
||||
export const stockLocationLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.location_id
|
||||
const query = fulfillmentSetCreateQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<StockLocationRes>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateShippingOptionsForm } from "./components/create-shipping-options-form"
|
||||
import { stockLocationLoader } from "./loader"
|
||||
|
||||
export function ShippingOptionsCreate() {
|
||||
const { fset_id, zone_id } = useParams()
|
||||
const { stock_location: stockLocation } = useLoaderData() as Awaited<
|
||||
ReturnType<typeof stockLocationLoader>
|
||||
>
|
||||
|
||||
const zone = stockLocation.fulfillment_sets
|
||||
?.find((f) => f.id === fset_id)
|
||||
?.service_zones?.find((z) => z.id === zone_id)
|
||||
|
||||
if (!zone) {
|
||||
throw new Error("Zone set doesn't exist")
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateShippingOptionsForm zone={zone} />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { Button, Heading, Input, Text } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as zod from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { useCreateShippingProfile } from "../../../../../hooks/api/shipping-profiles"
|
||||
|
||||
const CreateShippingOptionsSchema = zod.object({
|
||||
name: zod.string().min(1),
|
||||
type: zod.string().min(1),
|
||||
})
|
||||
|
||||
export function CreateShippingProfileForm() {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateShippingOptionsSchema>>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
type: "",
|
||||
},
|
||||
resolver: zodResolver(CreateShippingOptionsSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useCreateShippingProfile()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync({
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
})
|
||||
handleSuccess("/shipping-profiles")
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
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 type="submit" size="small" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading className="capitalize">
|
||||
{t("shippingProfile.title")}
|
||||
</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("shippingProfile.detailsHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-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="type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label tooltip={t("shippingProfile.typeHint")}>
|
||||
{t("fields.type")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./create-shipping-profile-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { ShippingProfileCreate as Component } from "./shipping-profile-create"
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import React from "react"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateShippingProfileForm } from "./components/create-shipping-profile-form"
|
||||
|
||||
export function ShippingProfileCreate() {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateShippingProfileForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./shipping-profile-list-table"
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { Trash } from "@medusajs/icons"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ShippingProfileDTO } from "@medusajs/types"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { useDeleteShippingProfile } from "../../../../../hooks/api/shipping-profiles"
|
||||
|
||||
export const ShippingOptionsRowActions = ({
|
||||
profile,
|
||||
}: {
|
||||
profile: ShippingProfileDTO
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
// TODO: MISSING ENDPOINT
|
||||
const { mutateAsync } = useDeleteShippingProfile(profile.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("shippingProfile.deleteWaring", {
|
||||
name: profile.name,
|
||||
}),
|
||||
verificationText: profile.name,
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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 { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useShippingProfilesTableColumns } from "./use-shipping-profiles-table-columns"
|
||||
import { useShippingProfilesTableQuery } from "./use-shipping-profiles-table-query"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const ShippingProfileListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { raw, searchParams } = useShippingProfilesTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { shipping_profiles, count, isLoading, isError, error } =
|
||||
useShippingProfiles({
|
||||
...searchParams,
|
||||
})
|
||||
|
||||
const columns = useShippingProfilesTableColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: shipping_profiles,
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const noData = !isLoading && !shipping_profiles.length
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("shippingProfile.domain")}</Heading>
|
||||
<div>
|
||||
<Button size="small" variant="secondary" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{noData ? (
|
||||
<NoRecords className="h-[180px]" title={t("general.noRecordsFound")} />
|
||||
) : (
|
||||
<DataTable
|
||||
table={table}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count || 1}
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
queryObject={raw}
|
||||
pagination
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ShippingProfileDTO } from "@medusajs/types"
|
||||
|
||||
import { ShippingOptionsRowActions } from "./shipping-options-row-actions"
|
||||
|
||||
const columnHelper = createColumnHelper<ShippingProfileDTO>()
|
||||
|
||||
export const useShippingProfilesTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: (cell) => cell.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("type", {
|
||||
header: t("fields.type"),
|
||||
cell: (cell) => cell.getValue(),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <ShippingOptionsRowActions profile={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useShippingProfilesTableQuery = ({
|
||||
pageSize = 20,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(["offset"], prefix)
|
||||
|
||||
const searchParams = {
|
||||
limit: pageSize,
|
||||
offset: raw.offset,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ShippingProfileList as Component } from "./shipping-profile-list"
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { ShippingProfileListTable } from "./components/shipping-profile-list-table"
|
||||
|
||||
export const ShippingProfileList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ShippingProfileListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IFulfillmentModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const deleteFulfillmentSetsStepId = "delete-fulfillment-sets"
|
||||
export const deleteFulfillmentSetsStep = createStep(
|
||||
deleteFulfillmentSetsStepId,
|
||||
async (ids: string[], { container }) => {
|
||||
const service = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
await service.softDelete(ids)
|
||||
|
||||
return new StepResponse(void 0, ids)
|
||||
},
|
||||
async (prevIds, { container }) => {
|
||||
if (!prevIds?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
await service.restore(prevIds)
|
||||
}
|
||||
)
|
||||
@@ -5,6 +5,7 @@ export * from "./create-service-zones"
|
||||
export * from "./upsert-shipping-options"
|
||||
export * from "./delete-service-zones"
|
||||
export * from "./delete-shipping-options"
|
||||
export * from "./delete-fulfillment-sets"
|
||||
export * from "./create-shipping-profiles"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
export * from "./set-shipping-options-prices"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { deleteFulfillmentSetsStep } from "../steps"
|
||||
import { removeRemoteLinkStep } from "../../common"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
export const deleteFulfillmentSetsWorkflowId =
|
||||
"delete-fulfillment-sets-workflow"
|
||||
export const deleteFulfillmentSetsWorkflow = createWorkflow(
|
||||
deleteFulfillmentSetsWorkflowId,
|
||||
(input: WorkflowData<{ ids: string[] }>) => {
|
||||
deleteFulfillmentSetsStep(input.ids)
|
||||
|
||||
removeRemoteLinkStep({
|
||||
[Modules.FULFILLMENT]: { fulfillment_set_id: input.ids },
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -4,6 +4,7 @@ export * from "./create-shipping-options"
|
||||
export * from "./create-shipping-profiles"
|
||||
export * from "./delete-service-zones"
|
||||
export * from "./delete-shipping-options"
|
||||
export * from "./delete-fulfillment-sets"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
export * from "./update-service-zones"
|
||||
export * from "./update-shipping-options"
|
||||
|
||||
@@ -22,6 +22,7 @@ export * from "./reservation"
|
||||
export * from "./region"
|
||||
export * from "./sales-channel"
|
||||
export * from "./shipping-options"
|
||||
export * from "./shipping-profile"
|
||||
export * from "./stock-location"
|
||||
export * from "./store"
|
||||
export * from "./tax"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./steps"
|
||||
export * from "./workflows"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IFulfillmentModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const deleteShippingProfilesStepId = "delete-shipping-profile"
|
||||
export const deleteShippingProfilesStep = createStep(
|
||||
deleteShippingProfilesStepId,
|
||||
async (ids: string[], { container }) => {
|
||||
const service = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
await service.softDeleteShippingProfiles(ids)
|
||||
|
||||
return new StepResponse(void 0, ids)
|
||||
},
|
||||
async (prevIds, { container }) => {
|
||||
if (!prevIds?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
await service.restoreShippingProfiles(prevIds)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./delete-shipping-profile"
|
||||
@@ -0,0 +1,18 @@
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
import { deleteShippingProfilesStep } from "../steps"
|
||||
import { removeRemoteLinkStep } from "../../common"
|
||||
|
||||
export const deleteShippingProfileWorkflowId =
|
||||
"delete-shipping-profile-workflow"
|
||||
export const deleteShippingProfileWorkflow = createWorkflow(
|
||||
deleteShippingProfileWorkflowId,
|
||||
(input: WorkflowData<{ ids: string[] }>) => {
|
||||
deleteShippingProfilesStep(input.ids)
|
||||
|
||||
removeRemoteLinkStep({
|
||||
[Modules.FULFILLMENT]: { shipping_profile_id: input.ids },
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./delete-shipping-profile"
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
AdminFulfillmentSetsDeleteResponse,
|
||||
IFulfillmentModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { deleteFulfillmentSetsWorkflow } from "@medusajs/core-flows"
|
||||
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
|
||||
export const DELETE = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse<AdminFulfillmentSetsDeleteResponse>
|
||||
) => {
|
||||
const { id } = req.params
|
||||
|
||||
const fulfillmentModuleService = req.scope.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
// Test if exists
|
||||
await fulfillmentModuleService.retrieve(id)
|
||||
|
||||
const { errors } = await deleteFulfillmentSetsWorkflow(req.scope).run({
|
||||
input: { ids: [id] },
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
id,
|
||||
object: "fulfillment_set",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
@@ -32,6 +32,11 @@ export const adminFulfillmentSetsRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
matcher: "/admin/fulfillment-sets/:id/service-zones/:zone_id",
|
||||
middlewares: [],
|
||||
},
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/admin/fulfillment-sets/:id",
|
||||
middlewares: [],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/fulfillment-sets/:id/service-zones/:zone_id",
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { AdminShippingProfileResponse } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
AdminShippingProfileDeleteResponse,
|
||||
AdminShippingProfileResponse,
|
||||
IFulfillmentModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { deleteShippingProfileWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
@@ -24,3 +30,32 @@ export const GET = async (
|
||||
|
||||
res.status(200).json({ shipping_profile: shippingProfile })
|
||||
}
|
||||
|
||||
export const DELETE = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse<AdminShippingProfileDeleteResponse>
|
||||
) => {
|
||||
const { id } = req.params
|
||||
|
||||
const fulfillmentModuleService = req.scope.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
// Test if exists
|
||||
await fulfillmentModuleService.retrieveShippingProfile(id)
|
||||
|
||||
const { errors } = await deleteShippingProfileWorkflow(req.scope).run({
|
||||
input: { ids: [id] },
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
id,
|
||||
object: "shipping_profile",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ export type PaginatedResponse<T = unknown> = {
|
||||
/**
|
||||
* The fields returned in the response of a DELETE request.
|
||||
*/
|
||||
export type DeleteResponse = {
|
||||
export type DeleteResponse<T = string> = {
|
||||
/**
|
||||
* The ID of the item that was deleted.
|
||||
*/
|
||||
@@ -259,7 +259,7 @@ export type DeleteResponse = {
|
||||
/**
|
||||
* The type of the item that was deleted.
|
||||
*/
|
||||
object: string
|
||||
object: T
|
||||
|
||||
/**
|
||||
* Whether the item was deleted successfully.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The fields returned in the response of a DELETE request.
|
||||
*/
|
||||
export type DeleteResponse = {
|
||||
export type DeleteResponse<T = string> = {
|
||||
/**
|
||||
* The ID of the item that was deleted.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AdminFulfillmentAddressResponse } from "./fulfillment-address"
|
||||
import { AdminFulfillmentProviderResponse } from "./fulfillment-provider"
|
||||
import { AdminFulfillmentItemResponse } from "./fulfillment-item"
|
||||
import { AdminFulfillmentLabelResponse } from "./fulfillment-label"
|
||||
import { DeleteResponse } from "../../../common"
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
@@ -25,3 +26,9 @@ export interface AdminFulfillmentResponse {
|
||||
updated_at: Date
|
||||
deleted_at: Date | null
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface AdminFulfillmentSetsDeleteResponse
|
||||
extends DeleteResponse<"fulfillment_set"> {}
|
||||
|
||||
@@ -48,4 +48,5 @@ export interface AdminShippingOptionListResponse extends PaginatedResponse {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface AdminShippingOptionDeleteResponse extends DeleteResponse {}
|
||||
export interface AdminShippingOptionDeleteResponse
|
||||
extends DeleteResponse<"shipping_option"> {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PaginatedResponse } from "../../../common"
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../common"
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
@@ -20,3 +20,9 @@ export interface AdminShippingProfileResponse {
|
||||
export interface AdminShippingProfilesResponse extends PaginatedResponse {
|
||||
shipping_profiles: ShippingProfileResponse[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface AdminShippingProfileDeleteResponse
|
||||
extends DeleteResponse<"shipping_profile"> {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseFilterable, OperatorMap } from "../dal"
|
||||
|
||||
import { StringComparisonOperator } from "../common/common"
|
||||
import { FulfillmentSetDTO } from "../fulfillment"
|
||||
|
||||
/**
|
||||
* @schema StockLocationAddressDTO
|
||||
@@ -205,6 +206,11 @@ export type StockLocationDTO = {
|
||||
*/
|
||||
address?: StockLocationAddressDTO
|
||||
|
||||
/**
|
||||
* Fulfillment sets for the location
|
||||
*/
|
||||
fulfillment_sets: FulfillmentSetDTO[]
|
||||
|
||||
/**
|
||||
* The creation date of the stock location.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user