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 ( + +
+ +
+ + + + +
+
+ + + + +
+ + {t("shipping.serviceZone.editAreasTitle", { + zone: zone.name, + })} + +
+ +
+
+ + {t("shipping.serviceZone.areas.title")} + + + {t("shipping.serviceZone.areas.description")} + +
+ +
+ {!!selectedCountries.length && ( +
+ {selectedCountries.map((c) => ( + + {c.display_name} + removeCountry(c.iso_2)} + className="text-ui-fg-subtle p-0 px-1 pt-[1px]" + variant="transparent" + > + + + + ))} + +
+ )} + {showAreasError && ( + + {t("shipping.serviceZone.areas.error")} + + )} +
+ +
+ + +
+
+
+
+
+
+ ) +} + +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,