diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index f5f4e41c5969e..fdfb3dd1c4a21 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -62,6 +62,7 @@
},
"actions": {
"save": "Save",
+ "select": "Select",
"saveAsDraft": "Save as draft",
"publish": "Publish",
"create": "Create",
@@ -714,10 +715,12 @@
"edit": {
"title": "Edit Service Zone"
},
+ "editAreasTitle": "Manage {{zone}} areas",
"deleteWarning": "Are you sure you want to delete \"{{name}}\". This will also delete all assocciated shipping options.",
"toast": {
"delete": "Zone \"{{name}}\" deleted successfully."
},
+ "manageAreas": "Manage areas",
"editPrices": "Edit prices",
"editOption": "Edit option",
"optionsLength_one": "shipping option",
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx
index 4128f1ed09b55..8f67613fe76d7 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx
@@ -737,6 +737,13 @@ export const RouteMap: RouteObject[] = [
"../../v2-routes/shipping/service-zone-edit"
),
},
+ {
+ path: "edit-areas",
+ lazy: () =>
+ import(
+ "../../v2-routes/shipping/service-zone-areas-edit"
+ ),
+ },
{
path: "shipping-option",
children: [
diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx
index 6f6aa79847cf5..c99613863edf7 100644
--- a/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/shipping/location-details/components/location-general-section/location-general-section.tsx
@@ -385,16 +385,16 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
groups={[
{
actions: [
- // {
- // label: t("shipping.serviceZone.addOption"),
- // icon: ,
- // to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create`,
- // },
{
label: t("actions.edit"),
icon: ,
to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit`,
},
+ {
+ label: t("shipping.serviceZone.manageAreas"),
+ icon: ,
+ to: `/settings/shipping/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit-areas`,
+ },
{
label: t("actions.delete"),
icon: ,
diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/components/edit-region-areas-form/edit-service-zone-areas-form.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/components/edit-region-areas-form/edit-service-zone-areas-form.tsx
new file mode 100644
index 0000000000000..4074c3c133f2e
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/components/edit-region-areas-form/edit-service-zone-areas-form.tsx
@@ -0,0 +1,346 @@
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import {
+ ColumnDef,
+ createColumnHelper,
+ RowSelectionState,
+} from "@tanstack/react-table"
+import * as zod from "zod"
+
+import {
+ Alert,
+ Badge,
+ Button,
+ Checkbox,
+ Heading,
+ IconButton,
+ Text,
+ toast,
+} from "@medusajs/ui"
+import { RegionCountryDTO, RegionDTO, ServiceZoneDTO } from "@medusajs/types"
+import { useTranslation } from "react-i18next"
+import { XMarkMini } from "@medusajs/icons"
+
+import {
+ RouteFocusModal,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { SplitView } from "../../../../../components/layout/split-view"
+import {
+ useCreateServiceZone,
+ useUpdateServiceZone,
+} from "../../../../../hooks/api/stock-locations"
+import { useEffect, useMemo, useState } from "react"
+import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query"
+import { useCountries } from "../../../../regions/common/hooks/use-countries"
+import { countries as staticCountries } from "../../../../../lib/countries"
+import { useDataTable } from "../../../../../hooks/use-data-table"
+import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
+import { DataTable } from "../../../../../components/table/data-table"
+
+const PREFIX = "ac"
+const PAGE_SIZE = 50
+
+const ConditionsFooter = ({ onSave }: { onSave: () => void }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+const EditeServiceZoneSchema = zod.object({
+ countries: zod.array(zod.string().length(2)).min(1),
+})
+
+type EditServiceZoneAreasFormProps = {
+ fulfillmentSetId: string
+ locationId: string
+ zone: ServiceZoneDTO
+}
+
+export function EditServiceZoneAreasForm({
+ fulfillmentSetId,
+ locationId,
+ zone,
+}: EditServiceZoneAreasFormProps) {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+ const [open, setOpen] = useState(false)
+ const [rowSelection, setRowSelection] = useState(
+ zone.geo_zones
+ .map((z) => z.country_code)
+ .reduce((acc, v) => {
+ acc[v] = true
+ return acc
+ }, {})
+ )
+
+ const form = useForm>({
+ defaultValues: {
+ countries: zone.geo_zones.map((z) => z.country_code),
+ },
+ resolver: zodResolver(EditeServiceZoneSchema),
+ })
+
+ const { mutateAsync: editServiceZone, isPending: isLoading } =
+ useUpdateServiceZone(fulfillmentSetId, zone.id, locationId)
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ try {
+ await editServiceZone({
+ geo_zones: data.countries
+ .sort((a, b) => a.localeCompare(b))
+ .map((iso2) => ({
+ country_code: iso2,
+ type: "country",
+ })),
+ })
+ } catch (e) {
+ toast.error(t("general.error"), {
+ description: e.message,
+ dismissLabel: t("general.close"),
+ })
+ }
+
+ handleSuccess()
+ })
+
+ const handleOpenChange = (open: boolean) => {
+ setOpen(open)
+ }
+
+ const { searchParams, raw } = useCountryTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PREFIX,
+ })
+ const { countries, count } = useCountries({
+ countries: staticCountries.map((c, i) => ({
+ display_name: c.display_name,
+ name: c.name,
+ id: i as any,
+ iso_2: c.iso_2,
+ iso_3: c.iso_3,
+ num_code: c.num_code,
+ region_id: null,
+ region: {} as RegionDTO,
+ })),
+ ...searchParams,
+ })
+
+ const columns = useColumns()
+
+ const { table } = useDataTable({
+ data: countries || [],
+ columns,
+ count,
+ enablePagination: true,
+ enableRowSelection: true,
+ getRowId: (row) => row.iso_2,
+ pageSize: PAGE_SIZE,
+ rowSelection: {
+ state: rowSelection,
+ updater: setRowSelection,
+ },
+ prefix: PREFIX,
+ })
+
+ const countriesWatch = form.watch("countries")
+
+ const onCountriesSave = () => {
+ form.setValue("countries", Object.keys(rowSelection))
+ setOpen(false)
+ }
+
+ const removeCountry = (iso2: string) => {
+ const state = { ...rowSelection }
+ delete state[iso2]
+ setRowSelection(state)
+
+ form.setValue(
+ "countries",
+ countriesWatch.filter((c) => c !== iso2)
+ )
+ }
+
+ const clearAll = () => {
+ setRowSelection({})
+ form.setValue("countries", [])
+ }
+
+ const selectedCountries = useMemo(() => {
+ return staticCountries.filter((c) => c.iso_2 in rowSelection)
+ }, [countriesWatch])
+
+ useEffect(() => {
+ // set selected rows from form state on open
+ if (open) {
+ setRowSelection(
+ countriesWatch.reduce((acc, c) => {
+ acc[c] = true
+ return acc
+ }, {})
+ )
+ }
+ }, [open])
+
+ const showAreasError =
+ form.formState.errors["countries"]?.type === "too_small"
+
+ return (
+
+
+
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const base = useCountryTableColumns()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ const isPreselected = !row.getCanSelect()
+
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ ...base,
+ ],
+ [base]
+ ) as ColumnDef[]
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/components/edit-region-areas-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/components/edit-region-areas-form/index.ts
new file mode 100644
index 0000000000000..522cccf066fa8
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/components/edit-region-areas-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-service-zone-areas-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/index.ts b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/index.ts
new file mode 100644
index 0000000000000..fc088b9f88267
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/index.ts
@@ -0,0 +1 @@
+export { ServiceZoneAreasEdit as Component } from "./service-zone-areas-edit"
diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/service-zone-areas-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/service-zone-areas-edit.tsx
new file mode 100644
index 0000000000000..0bccfa6b4a5b4
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-areas-edit/service-zone-areas-edit.tsx
@@ -0,0 +1,45 @@
+import { json, useParams } from "react-router-dom"
+
+import { RouteFocusModal } from "../../../components/route-modal"
+import { EditServiceZoneAreasForm } from "./components/edit-region-areas-form"
+import { useStockLocation } from "../../../hooks/api/stock-locations"
+
+export const ServiceZoneAreasEdit = () => {
+ const { location_id, fset_id, zone_id } = useParams()
+
+ const { stock_location, isPending, isError, error } = useStockLocation(
+ location_id!,
+ {
+ // NOTE: use same query for all details page subroutes & fetches
+ fields:
+ "name,*sales_channels,address.city,address.country_code,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile",
+ }
+ )
+
+ const zone = stock_location?.fulfillment_sets
+ .find((f) => f.id === fset_id)
+ ?.service_zones.find((z) => z.id === zone_id)
+
+ if (isError) {
+ throw error
+ }
+
+ if (!isPending && !zone) {
+ throw json(
+ { message: `Service zone with ID ${zone_id} was not found` },
+ 404
+ )
+ }
+
+ return (
+
+ {!isPending && zone && (
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx
index 7a0a8321e354c..fb5f4e8304843 100644
--- a/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/shipping/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx
@@ -91,10 +91,12 @@ export function CreateServiceZoneForm({
try {
await createServiceZone({
name: data.name,
- geo_zones: data.countries.map((iso2) => ({
- country_code: iso2,
- type: "country",
- })),
+ geo_zones: data.countries
+ .sort((a, b) => a.localeCompare(b))
+ .map((iso2) => ({
+ country_code: iso2,
+ type: "country",
+ })),
})
} catch (e) {
toast.error(t("general.error"), {
@@ -209,7 +211,7 @@ export function CreateServiceZoneForm({
-
+
{t("shipping.fulfillmentSet.create.title", {
fulfillmentSet: fulfillmentSet.name,