From 2e8e7b27b65a9fa8d03a6456331caa9c7ce14409 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:10:39 +0200 Subject: [PATCH] fix(dashboard,types,js-sdk): Locations & Shipping fixes and cleanup (#7715) --- .../dashboard/scripts/generate-countries.js | 44 -- .../badge-list-summary/badge-list-summary.tsx | 16 +- .../src/components/common/divider/divider.tsx | 2 +- .../empty-table-content.tsx | 20 +- .../common/icon-avatar/icon-avatar.tsx | 25 + .../components/common/icon-avatar/index.ts | 1 + .../components/common/inline-link/index.ts | 1 - .../common/inline-link/inline-link.tsx | 21 - .../src/components/common/inline-tip/index.ts | 1 + .../common/inline-tip/inline-tip.tsx | 60 ++ .../components/common/link-button/index.ts | 1 + .../common/link-button/link-button.tsx | 29 + .../common/list-summary/list-summary.tsx | 6 +- .../settings-layout/settings-layout.tsx | 2 +- .../error-boundary/error-boundary.tsx | 36 +- .../src/hooks/api/fulfillment-providers.tsx | 17 +- .../src/hooks/api/fulfillment-sets.tsx | 180 ++++++ .../src/hooks/api/shipping-options.tsx | 70 ++- .../src/hooks/api/shipping-profiles.tsx | 49 +- .../src/hooks/api/stock-locations.tsx | 198 ++----- .../use-sales-channel-table-columns.tsx | 5 +- .../dashboard/src/hooks/use-combobox-data.tsx | 8 +- .../dashboard/src/i18n/translations/en.json | 228 ++++---- .../admin-next/dashboard/src/lib/countries.ts | 512 +++++++++--------- .../dashboard/src/lib/shipping-options.ts | 8 +- .../providers/router-provider/route-map.tsx | 32 +- .../category-organize-section.tsx | 12 +- .../geo-zone-form/geo-zone-form.tsx | 312 +++++++++++ .../common/components/geo-zone-form/index.ts | 1 + .../src/routes/locations/common/constants.ts | 9 + .../use-shipping-option-price-columns.tsx | 63 +++ .../location-add-sales-channels/index.ts | 1 - .../location-add-sales-channels.tsx | 30 - .../create-location-form.tsx | 42 +- .../location-general-section/index.ts | 0 .../location-general-section.tsx | 505 +++++++++-------- .../location-sales-channels-section/index.ts | 0 .../locations-sales-channels-section.tsx | 76 +++ .../const.ts | 2 +- .../routes/locations/location-detail/index.ts | 2 + .../locations/location-detail/loader.ts | 36 ++ .../location-detail.tsx} | 6 +- .../locations-sales-channels-section.tsx | 79 --- .../locations/location-details/index.ts | 2 - .../locations/location-details/loader.ts | 25 - .../edit-location-form/edit-location-form.tsx | 43 +- .../locations/location-edit/location-edit.tsx | 21 +- .../components/location-list-header/index.ts | 1 + .../location-list-header.tsx | 21 + .../components/location-list-item/index.ts | 1 + .../location-list-item.tsx} | 111 ++-- .../components/location/index.ts | 1 - .../routes/locations/location-list/const.ts | 3 - .../locations/location-list/constants.ts | 3 + .../routes/locations/location-list/loader.ts | 33 +- .../locations/location-list/location-list.tsx | 37 +- .../edit-sales-channels-form.tsx | 33 +- .../edit-sales-channels-form/index.ts | 0 .../location-sales-channels/index.ts | 1 + .../location-sales-channels.tsx | 27 + .../create-service-zone-form.tsx | 159 ++++++ .../create-service-zone-form/index.ts | 0 .../location-service-zone-create/index.ts | 2 + .../loader.ts | 11 +- .../location-service-zone-create.tsx | 58 ++ .../edit-service-zone-form.tsx | 35 +- .../components/edit-region-form/index.ts | 0 .../location-service-zone-edit/index.ts | 1 + .../location-service-zone-edit.tsx} | 23 +- .../edit-service-zone-areas-form.tsx | 133 +++++ .../edit-region-areas-form/index.ts | 0 .../index.ts | 1 + .../location-service-zone-manage-areas.tsx} | 16 +- .../create-shipping-option-details-form.tsx | 198 +++++++ .../create-shipping-options-form.tsx | 276 ++++++++++ .../create-shipping-options-prices-form.tsx | 64 +++ .../create-shipping-options-form/index.ts | 0 .../create-shipping-options-form/schema.ts | 21 + .../constants.ts | 2 + .../index.ts | 2 + .../loader.ts | 18 +- ...on-service-zone-shipping-option-create.tsx | 59 ++ .../edit-shipping-option-form.tsx | 273 ++++++++++ .../components/edit-region-form/index.ts | 0 .../index.ts | 1 + ...ion-service-zone-shipping-option-edit.tsx} | 16 +- .../edit-shipping-options-pricing-form.tsx | 244 +++++++++ .../create-shipping-options-form/index.ts | 0 .../index.ts | 1 + ...-service-zone-shipping-option-pricing.tsx} | 13 +- .../edit-service-zone-areas-form.tsx | 341 ------------ .../service-zone-areas-edit/index.ts | 1 - .../create-service-zone-form.tsx | 367 ------------- .../locations/service-zone-create/index.ts | 2 - .../service-zone-create.tsx | 29 - .../locations/service-zone-edit/index.ts | 1 - .../edit-shipping-option-form.tsx | 278 ---------- .../locations/shipping-option-edit/index.ts | 1 - .../create-shipping-options-form.tsx | 474 ---------------- .../create-shipping-options-prices-form.tsx | 151 ------ .../shipping-options-create/index.ts | 2 - .../shipping-options-create.tsx | 30 - .../edit-shipping-options-pricing-form.tsx | 309 ----------- .../shipping-options-edit-pricing/index.ts | 1 - ...roduct-create-details-organize-section.tsx | 2 +- .../product-organization-form.tsx | 4 +- .../regions/common/hooks/use-countries.tsx | 5 +- .../hooks/use-country-table-columns.tsx | 6 +- .../use-reservation-table-columns.tsx | 6 +- .../core/js-sdk/src/admin/fulfillment-set.ts | 6 +- packages/core/js-sdk/src/admin/index.ts | 3 + .../fulfillment-provider/admin/entities.ts | 6 +- .../http/fulfillment-set/admin/entities.ts | 4 +- .../http/shipping-option/admin/entities.ts | 13 +- .../http/shipping-option/admin/payloads.ts | 17 +- .../http/stock-locations/admin/entities.ts | 3 +- 116 files changed, 3512 insertions(+), 3288 deletions(-) delete mode 100644 packages/admin-next/dashboard/scripts/generate-countries.js create mode 100644 packages/admin-next/dashboard/src/components/common/icon-avatar/icon-avatar.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/icon-avatar/index.ts delete mode 100644 packages/admin-next/dashboard/src/components/common/inline-link/index.ts delete mode 100644 packages/admin-next/dashboard/src/components/common/inline-link/inline-link.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/inline-tip/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/inline-tip/inline-tip.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/link-button/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/link-button/link-button.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/api/fulfillment-sets.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/geo-zone-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/common/constants.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx rename packages/admin-next/dashboard/src/routes/locations/{location-details => location-detail}/components/location-general-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/locations/{location-details => location-detail}/components/location-general-section/location-general-section.tsx (53%) rename packages/admin-next/dashboard/src/routes/locations/{location-details => location-detail}/components/location-sales-channels-section/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channels-section/locations-sales-channels-section.tsx rename packages/admin-next/dashboard/src/routes/locations/{location-details => location-detail}/const.ts (85%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/loader.ts rename packages/admin-next/dashboard/src/routes/locations/{location-details/location-details.tsx => location-detail/location-detail.tsx} (92%) delete mode 100644 packages/admin-next/dashboard/src/routes/locations/location-details/components/location-sales-channels-section/locations-sales-channels-section.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/location-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/location-details/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/location-list-header.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-item/index.ts rename packages/admin-next/dashboard/src/routes/locations/location-list/components/{location/location.tsx => location-list-item/location-list-item.tsx} (65%) delete mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/components/location/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/const.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/constants.ts rename packages/admin-next/dashboard/src/routes/locations/{location-add-sales-channels => location-sales-channels}/components/edit-sales-channels-form/edit-sales-channels-form.tsx (85%) rename packages/admin-next/dashboard/src/routes/locations/{location-add-sales-channels => location-sales-channels}/components/edit-sales-channels-form/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-sales-channels/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-sales-channels/location-sales-channels.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx rename packages/admin-next/dashboard/src/routes/locations/{service-zone-create => location-service-zone-create}/components/create-service-zone-form/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/index.ts rename packages/admin-next/dashboard/src/routes/locations/{service-zone-create => location-service-zone-create}/loader.ts (68%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/location-service-zone-create.tsx rename packages/admin-next/dashboard/src/routes/locations/{service-zone-edit => location-service-zone-edit}/components/edit-region-form/edit-service-zone-form.tsx (76%) rename packages/admin-next/dashboard/src/routes/locations/{service-zone-edit => location-service-zone-edit}/components/edit-region-form/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/index.ts rename packages/admin-next/dashboard/src/routes/locations/{service-zone-edit/service-zone-edit.tsx => location-service-zone-edit/location-service-zone-edit.tsx} (57%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/components/edit-region-areas-form/edit-service-zone-areas-form.tsx rename packages/admin-next/dashboard/src/routes/locations/{service-zone-areas-edit => location-service-zone-manage-areas}/components/edit-region-areas-form/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/index.ts rename packages/admin-next/dashboard/src/routes/locations/{service-zone-areas-edit/service-zone-areas-edit.tsx => location-service-zone-manage-areas/location-service-zone-manage-areas.tsx} (58%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx rename packages/admin-next/dashboard/src/routes/locations/{shipping-options-create => location-service-zone-shipping-option-create}/components/create-shipping-options-form/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/constants.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/index.ts rename packages/admin-next/dashboard/src/routes/locations/{shipping-options-create => location-service-zone-shipping-option-create}/loader.ts (50%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx rename packages/admin-next/dashboard/src/routes/locations/{shipping-option-edit => location-service-zone-shipping-option-edit}/components/edit-region-form/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/index.ts rename packages/admin-next/dashboard/src/routes/locations/{shipping-option-edit/shipping-option-edit.tsx => location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx} (70%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx rename packages/admin-next/dashboard/src/routes/locations/{shipping-options-edit-pricing => location-service-zone-shipping-option-pricing}/components/create-shipping-options-form/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/index.ts rename packages/admin-next/dashboard/src/routes/locations/{shipping-options-edit-pricing/shipping-options-edit-pricing.tsx => location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx} (68%) delete mode 100644 packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/components/edit-region-areas-form/edit-service-zone-areas-form.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/service-zone-create/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/service-zone-create/service-zone-create.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/service-zone-edit/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-options-create/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-options-create/shipping-options-create.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/index.ts diff --git a/packages/admin-next/dashboard/scripts/generate-countries.js b/packages/admin-next/dashboard/scripts/generate-countries.js deleted file mode 100644 index ccce255034..0000000000 --- a/packages/admin-next/dashboard/scripts/generate-countries.js +++ /dev/null @@ -1,44 +0,0 @@ -async function generateCountries() { - const { countries } = await import("@medusajs/medusa/dist/utils/countries.js") - const fs = await import("fs") - const path = await import("path") - - const arr = countries.map((c) => { - const iso_2 = c.alpha2.toLowerCase() - const iso_3 = c.alpha3.toLowerCase() - const num_code = parseInt(c.numeric, 10) - const name = c.name.toUpperCase() - const display_name = c.name - - return { - iso_2, - iso_3, - num_code, - name, - display_name, - } - }) - - const json = JSON.stringify(arr, null, 2) - - const dest = path.join(__dirname, "../src/lib/countries.ts") - const destDir = path.dirname(dest) - - const fileContent = `/** This file is auto-generated. Do not modify it manually. */\nimport type { RegionCountryDTO } from "@medusajs/types"\n\nexport const countries: Omit[] = ${json}` - - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }) - } - - fs.writeFileSync(dest, fileContent) -} - -;(async () => { - console.log("Generating countries") - try { - await generateCountries() - console.log("Countries generated") - } catch (e) { - console.error(e) - } -})() diff --git a/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx b/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx index eb0887e210..af4259d1ee 100644 --- a/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx +++ b/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx @@ -16,7 +16,10 @@ type BadgeListSummaryProps = { * Determines whether the center text is truncated if there is no space in the container */ inline?: boolean - + /** + * Whether the badges should be rounded + */ + rounded?: boolean className?: string } @@ -24,6 +27,7 @@ export const BadgeListSummary = ({ list, className, inline, + rounded = false, n = 2, }: BadgeListSummaryProps) => { const { t } = useTranslation() @@ -35,7 +39,7 @@ export const BadgeListSummary = ({ return (
{list.slice(0, n).map((item) => { return ( - + {item} ) @@ -62,7 +66,11 @@ export const BadgeListSummary = ({ } > - + {title} diff --git a/packages/admin-next/dashboard/src/components/common/divider/divider.tsx b/packages/admin-next/dashboard/src/components/common/divider/divider.tsx index a91b46cef3..913aa95514 100644 --- a/packages/admin-next/dashboard/src/components/common/divider/divider.tsx +++ b/packages/admin-next/dashboard/src/components/common/divider/divider.tsx @@ -18,7 +18,7 @@ export const Divider = ({ aria-orientation={orientation} role="separator" className={clx( - "border-ui-border-strong", + "border-ui-border-base", { "w-full border-t": orientation === "horizontal" && variant === "solid", diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx index 5aac770ac7..0db1997a4c 100644 --- a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx +++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx @@ -76,20 +76,22 @@ export const NoRecords = ({ return (
-
- +
+ - - {title ?? t("general.noRecordsTitle")} - +
+ + {title ?? t("general.noRecordsTitle")} + - - {message ?? t("general.noRecordsMessage")} - + + {message ?? t("general.noRecordsMessage")} + +
{buttonVariant === "default" && } diff --git a/packages/admin-next/dashboard/src/components/common/icon-avatar/icon-avatar.tsx b/packages/admin-next/dashboard/src/components/common/icon-avatar/icon-avatar.tsx new file mode 100644 index 0000000000..ab7f739e63 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/icon-avatar/icon-avatar.tsx @@ -0,0 +1,25 @@ +import { clx } from "@medusajs/ui" +import { PropsWithChildren } from "react" + +type IconAvatarProps = PropsWithChildren<{ + className?: string +}> + +/** + * Use this component when a design calls for an avatar with an icon. + * + * The `` component from `@medusajs/ui` does not support passing an icon as a child. + */ +export const IconAvatar = ({ children, className }: IconAvatarProps) => { + return ( +
div]:bg-ui-bg-field [&>div]:text-ui-fg-subtle [&>div]:flex [&>div]:size-6 [&>div]:items-center [&>div]:justify-center [&>div]:rounded-[4px]", + className + )} + > +
{children}
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/common/icon-avatar/index.ts b/packages/admin-next/dashboard/src/components/common/icon-avatar/index.ts new file mode 100644 index 0000000000..ee6f486168 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/icon-avatar/index.ts @@ -0,0 +1 @@ +export * from "./icon-avatar" diff --git a/packages/admin-next/dashboard/src/components/common/inline-link/index.ts b/packages/admin-next/dashboard/src/components/common/inline-link/index.ts deleted file mode 100644 index 0a2021c6aa..0000000000 --- a/packages/admin-next/dashboard/src/components/common/inline-link/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./inline-link" diff --git a/packages/admin-next/dashboard/src/components/common/inline-link/inline-link.tsx b/packages/admin-next/dashboard/src/components/common/inline-link/inline-link.tsx deleted file mode 100644 index 2acdccbf5a..0000000000 --- a/packages/admin-next/dashboard/src/components/common/inline-link/inline-link.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { clx } from "@medusajs/ui" -import { ComponentPropsWithoutRef } from "react" -import { Link } from "react-router-dom" - -export const InlineLink = ({ - className, - ...props -}: ComponentPropsWithoutRef) => { - return ( - { - e.stopPropagation() - }} - className={clx( - "text-ui-fg-interactive transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover rounded-md outline-none", - className - )} - {...props} - /> - ) -} diff --git a/packages/admin-next/dashboard/src/components/common/inline-tip/index.ts b/packages/admin-next/dashboard/src/components/common/inline-tip/index.ts new file mode 100644 index 0000000000..1fb75a3601 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/inline-tip/index.ts @@ -0,0 +1 @@ +export * from "./inline-tip" diff --git a/packages/admin-next/dashboard/src/components/common/inline-tip/inline-tip.tsx b/packages/admin-next/dashboard/src/components/common/inline-tip/inline-tip.tsx new file mode 100644 index 0000000000..352f8c537f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/inline-tip/inline-tip.tsx @@ -0,0 +1,60 @@ +import { clx } from "@medusajs/ui" +import { ComponentPropsWithoutRef, forwardRef } from "react" +import { useTranslation } from "react-i18next" + +interface InlineTipProps extends ComponentPropsWithoutRef<"div"> { + /** + * The label to display in the tip. + */ + label?: string + /** + * The variant of the tip. + */ + variant?: "tip" | "warning" +} + +/** + * A component for rendering inline tips. Useful for providing additional information or context. + * + * @example + * ```tsx + * + * This is an info tip. + * + * ``` + * + * TODO: Move to `@medusajs/ui` package. + */ +export const InlineTip = forwardRef( + ({ variant = "tip", label, className, children, ...props }, ref) => { + const { t } = useTranslation() + const labelValue = + label || (variant === "warning" ? t("general.warning") : t("general.tip")) + + return ( +
+
+
+ + {labelValue}: + {" "} + {children} +
+
+ ) + } +) + +InlineTip.displayName = "InlineTip" diff --git a/packages/admin-next/dashboard/src/components/common/link-button/index.ts b/packages/admin-next/dashboard/src/components/common/link-button/index.ts new file mode 100644 index 0000000000..b71efb563c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/link-button/index.ts @@ -0,0 +1 @@ +export * from "./link-button" diff --git a/packages/admin-next/dashboard/src/components/common/link-button/link-button.tsx b/packages/admin-next/dashboard/src/components/common/link-button/link-button.tsx new file mode 100644 index 0000000000..16ad64597d --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/link-button/link-button.tsx @@ -0,0 +1,29 @@ +import { clx } from "@medusajs/ui" +import { ComponentPropsWithoutRef } from "react" +import { Link } from "react-router-dom" + +interface LinkButtonProps extends ComponentPropsWithoutRef { + variant?: "primary" | "interactive" +} + +export const LinkButton = ({ + className, + variant = "interactive", + ...props +}: LinkButtonProps) => { + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx b/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx index ab18f60b1f..27cd5f66e5 100644 --- a/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx +++ b/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx @@ -16,6 +16,7 @@ type ListSummaryProps = { * Determines whether the center text is truncated if there is no space in the container */ inline?: boolean + variant?: "base" | "compact" className?: string } @@ -23,6 +24,7 @@ type ListSummaryProps = { export const ListSummary = ({ list, className, + variant = "compact", inline, n = 2, }: ListSummaryProps) => { @@ -35,10 +37,12 @@ export const ListSummary = ({ return (
{ to: "/settings/shipping-profiles", }, { - label: t("location.domain"), + label: t("stockLocations.domain"), to: "/settings/locations", }, ], diff --git a/packages/admin-next/dashboard/src/components/utilities/error-boundary/error-boundary.tsx b/packages/admin-next/dashboard/src/components/utilities/error-boundary/error-boundary.tsx index 8750412056..7d9c158682 100644 --- a/packages/admin-next/dashboard/src/components/utilities/error-boundary/error-boundary.tsx +++ b/packages/admin-next/dashboard/src/components/utilities/error-boundary/error-boundary.tsx @@ -1,9 +1,10 @@ -import { Navigate, useLocation, useRouteError } from "react-router-dom" - import { ExclamationCircle } from "@medusajs/icons" import { Text } from "@medusajs/ui" import { useTranslation } from "react-i18next" +import { Navigate, useLocation, useRouteError } from "react-router-dom" + import { isFetchError } from "../../../lib/is-fetch-error" +import { InlineTip } from "../../common/inline-tip" // WIP - Need to allow wrapping with ErrorBoundary for more granular error handling. export const ErrorBoundary = () => { @@ -53,7 +54,38 @@ export const ErrorBoundary = () => { {message} +
) } + +/** + * Component that renders an error stack trace in development mode. + * + * We don't want to show stack traces in production, so this component is only + * rendered when the `NODE_ENV` is set to `development`. + * + * The reason for adding this is that `react-router-dom` can swallow certain types + * of errors, e.g. a missing export from a module that is exported, and will instead + * log a vague warning related to the route not exporting a Component. + */ +const DevelopmentStack = ({ error }: { error: unknown }) => { + const stack = error instanceof Error ? error.stack : null + const [stackType, stackMessage] = stack?.split(":") ?? ["", ""] + const isDevelopment = process.env.NODE_ENV === "development" + + if (!isDevelopment) { + return null + } + + return ( + + {stackMessage} + + ) +} diff --git a/packages/admin-next/dashboard/src/hooks/api/fulfillment-providers.tsx b/packages/admin-next/dashboard/src/hooks/api/fulfillment-providers.tsx index 4cb2060595..5d17085c50 100644 --- a/packages/admin-next/dashboard/src/hooks/api/fulfillment-providers.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/fulfillment-providers.tsx @@ -1,21 +1,28 @@ +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query" -import { client } from "../../lib/client" +import { sdk } from "../../lib/client" import { queryKeysFactory } from "../../lib/query-key-factory" -const FULFILLMENT_PROVIDERS_QUERY_KEY = "f_providers" as const +const FULFILLMENT_PROVIDERS_QUERY_KEY = "fulfillment_providers" as const export const fulfillmentProvidersQueryKeys = queryKeysFactory( FULFILLMENT_PROVIDERS_QUERY_KEY ) export const useFulfillmentProviders = ( - query?: Record, + query?: HttpTypes.AdminFulfillmentProviderListParams, options?: Omit< - UseQueryOptions, + UseQueryOptions< + HttpTypes.AdminFulfillmentProviderListResponse, + FetchError, + HttpTypes.AdminFulfillmentProviderListResponse, + QueryKey + >, "queryFn" | "queryKey" > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.fulfillmentProviders.list(query), + queryFn: () => sdk.admin.fulfillmentProvider.list(query), queryKey: fulfillmentProvidersQueryKeys.list(query), ...options, }) diff --git a/packages/admin-next/dashboard/src/hooks/api/fulfillment-sets.tsx b/packages/admin-next/dashboard/src/hooks/api/fulfillment-sets.tsx new file mode 100644 index 0000000000..bc32eac659 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/api/fulfillment-sets.tsx @@ -0,0 +1,180 @@ +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" +import { + QueryKey, + UseMutationOptions, + UseQueryOptions, + useMutation, + useQuery, +} from "@tanstack/react-query" +import { sdk } from "../../lib/client" +import { queryClient } from "../../lib/query-client" +import { queryKeysFactory } from "../../lib/query-key-factory" +import { shippingOptionsQueryKeys } from "./shipping-options" +import { stockLocationsQueryKeys } from "./stock-locations" + +const FULFILLMENT_SETS_QUERY_KEY = "fulfillment_sets" as const +export const fulfillmentSetsQueryKeys = queryKeysFactory( + FULFILLMENT_SETS_QUERY_KEY +) + +export const useDeleteFulfillmentSet = ( + id: string, + options?: Omit< + UseMutationOptions< + HttpTypes.AdminFulfillmentSetDeleteResponse, + FetchError, + void + >, + "mutationFn" + > +) => { + return useMutation({ + mutationFn: () => sdk.admin.fulfillmentSet.delete(id), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ + queryKey: fulfillmentSetsQueryKeys.detail(id), + }) + await queryClient.invalidateQueries({ + queryKey: fulfillmentSetsQueryKeys.lists(), + }) + + // We need to invalidate all related entities. We invalidate using `all` keys to ensure that all relevant entities are invalidated. + await queryClient.invalidateQueries({ + queryKey: stockLocationsQueryKeys.all, + }) + await queryClient.invalidateQueries({ + queryKey: shippingOptionsQueryKeys.all, + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useFulfillmentSetServiceZone = ( + fulfillmentSetId: string, + serviceZoneId: string, + query?: HttpTypes.SelectParams, + options?: Omit< + UseQueryOptions< + HttpTypes.AdminServiceZoneResponse, + FetchError, + HttpTypes.AdminServiceZoneResponse, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => + sdk.admin.fulfillmentSet.retrieveServiceZone( + fulfillmentSetId, + serviceZoneId, + query + ), + queryKey: fulfillmentSetsQueryKeys.detail(fulfillmentSetId, query), + ...options, + }) + + return { ...data, ...rest } +} + +export const useCreateFulfillmentSetServiceZone = ( + fulfillmentSetId: string, + options?: Omit< + UseMutationOptions< + HttpTypes.AdminFulfillmentSetResponse, + FetchError, + HttpTypes.AdminCreateFulfillmentSetServiceZone, + QueryKey + >, + "mutationFn" + > +) => { + return useMutation({ + mutationFn: (payload) => + sdk.admin.fulfillmentSet.createServiceZone(fulfillmentSetId, payload), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ + queryKey: fulfillmentSetsQueryKeys.lists(), + }) + await queryClient.invalidateQueries({ + queryKey: stockLocationsQueryKeys.all, + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateFulfillmentSetServiceZone = ( + fulfillmentSetId: string, + serviceZoneId: string, + options?: Omit< + UseMutationOptions< + HttpTypes.AdminFulfillmentSetResponse, + FetchError, + HttpTypes.AdminUpdateFulfillmentSetServiceZone, + QueryKey + >, + "mutationFn" + > +) => { + return useMutation({ + mutationFn: (payload) => + sdk.admin.fulfillmentSet.updateServiceZone( + fulfillmentSetId, + serviceZoneId, + payload + ), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ + queryKey: fulfillmentSetsQueryKeys.lists(), + }) + await queryClient.invalidateQueries({ + queryKey: stockLocationsQueryKeys.all, + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteFulfillmentServiceZone = ( + fulfillmentSetId: string, + serviceZoneId: string, + options?: Omit< + UseMutationOptions< + HttpTypes.AdminServiceZoneDeleteResponse, + FetchError, + void + >, + "mutationFn" + > +) => { + return useMutation({ + mutationFn: () => + sdk.admin.fulfillmentSet.deleteServiceZone( + fulfillmentSetId, + serviceZoneId + ), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ + queryKey: fulfillmentSetsQueryKeys.lists(), + }) + await queryClient.invalidateQueries({ + queryKey: shippingOptionsQueryKeys.lists(), + }) + await queryClient.invalidateQueries({ + queryKey: stockLocationsQueryKeys.all, + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin-next/dashboard/src/hooks/api/shipping-options.tsx b/packages/admin-next/dashboard/src/hooks/api/shipping-options.tsx index 538fa3b04e..0c7ba9414f 100644 --- a/packages/admin-next/dashboard/src/hooks/api/shipping-options.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/shipping-options.tsx @@ -6,33 +6,51 @@ import { UseQueryOptions, } from "@tanstack/react-query" -import { client } from "../../lib/client" +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" +import { sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" -import { - CreateShippingOptionReq, - UpdateShippingOptionReq, -} from "../../types/api-payloads" -import { - ShippingOptionDeleteRes, - ShippingOptionRes, -} from "../../types/api-responses" -import { stockLocationsQueryKeys } from "./stock-locations" import { queryKeysFactory } from "../../lib/query-key-factory" +import { stockLocationsQueryKeys } from "./stock-locations" const SHIPPING_OPTIONS_QUERY_KEY = "shipping_options" as const export const shippingOptionsQueryKeys = queryKeysFactory( SHIPPING_OPTIONS_QUERY_KEY ) +// TODO: Endpoint is not implemented yet +// export const useShippingOption = ( +// id: string, +// options?: UseQueryOptions< +// HttpTypes.AdminShippingOptionResponse, +// Error, +// HttpTypes.AdminShippingOptionResponse, +// QueryKey +// > +// ) => { +// const { data, ...rest } = useQuery({ +// queryFn: () => sdk.admin.shippingOption.retrieve(id), +// queryKey: shippingOptionsQueryKeys.retrieve(id), +// ...options, +// }) + +// return { ...data, ...rest } +// } + export const useShippingOptions = ( - query?: Record, + query?: HttpTypes.AdminShippingOptionListParams, options?: Omit< - UseQueryOptions, + UseQueryOptions< + HttpTypes.AdminShippingOptionListResponse, + Error, + HttpTypes.AdminShippingOptionListResponse, + QueryKey + >, "queryFn" | "queryKey" > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.shippingOptions.list(query), + queryFn: () => sdk.admin.shippingOption.list(query), queryKey: shippingOptionsQueryKeys.list(query), ...options, }) @@ -42,13 +60,13 @@ export const useShippingOptions = ( export const useCreateShippingOptions = ( options?: UseMutationOptions< - ShippingOptionRes, - Error, - CreateShippingOptionReq + HttpTypes.AdminShippingOptionResponse, + FetchError, + HttpTypes.AdminCreateShippingOption > ) => { return useMutation({ - mutationFn: (payload) => client.shippingOptions.create(payload), + mutationFn: (payload) => sdk.admin.shippingOption.create(payload), onSuccess: (data, variables, context) => { queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.all, @@ -65,13 +83,13 @@ export const useCreateShippingOptions = ( export const useUpdateShippingOptions = ( id: string, options?: UseMutationOptions< - ShippingOptionRes, - Error, - UpdateShippingOptionReq + HttpTypes.AdminShippingOptionResponse, + FetchError, + HttpTypes.AdminUpdateShippingOption > ) => { return useMutation({ - mutationFn: (payload) => client.shippingOptions.update(id, payload), + mutationFn: (payload) => sdk.admin.shippingOption.update(id, payload), onSuccess: (data, variables, context) => { queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.all, @@ -87,11 +105,15 @@ export const useUpdateShippingOptions = ( export const useDeleteShippingOption = ( optionId: string, - options?: UseMutationOptions + options?: UseMutationOptions< + HttpTypes.AdminShippingOptionDeleteResponse, + FetchError, + void + > ) => { return useMutation({ - mutationFn: () => client.shippingOptions.delete(optionId), - onSuccess: (data: any, variables: any, context: any) => { + mutationFn: () => sdk.admin.shippingOption.delete(optionId), + onSuccess: (data, variables, context) => { queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.all, }) diff --git a/packages/admin-next/dashboard/src/hooks/api/shipping-profiles.tsx b/packages/admin-next/dashboard/src/hooks/api/shipping-profiles.tsx index 7775cc7590..07aa0d860c 100644 --- a/packages/admin-next/dashboard/src/hooks/api/shipping-profiles.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/shipping-profiles.tsx @@ -5,14 +5,10 @@ import { useQuery, UseQueryOptions, } from "@tanstack/react-query" -import { CreateShippingProfileReq } from "../../types/api-payloads" -import { - ShippingProfileListRes, - ShippingProfileRes, -} from "../../types/api-responses" -import { DeleteResponse } from "@medusajs/types" -import { client } from "../../lib/client" +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" +import { sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" import { queryKeysFactory } from "../../lib/query-key-factory" @@ -23,13 +19,13 @@ export const shippingProfileQueryKeys = queryKeysFactory( export const useCreateShippingProfile = ( options?: UseMutationOptions< - ShippingProfileRes, - Error, - CreateShippingProfileReq + HttpTypes.AdminShippingProfileResponse, + FetchError, + HttpTypes.AdminCreateShippingProfile > ) => { return useMutation({ - mutationFn: (payload) => client.shippingProfiles.create(payload), + mutationFn: (payload) => sdk.admin.shippingProfile.create(payload), onSuccess: (data, variables, context) => { queryClient.invalidateQueries({ queryKey: shippingProfileQueryKeys.lists(), @@ -45,14 +41,14 @@ export const useShippingProfile = ( id: string, query?: Record, options?: UseQueryOptions< - ShippingProfileRes, - Error, - ShippingProfileRes, + HttpTypes.AdminShippingProfileResponse, + FetchError, + HttpTypes.AdminShippingProfileResponse, QueryKey > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.shippingProfiles.retrieve(id, query), + queryFn: () => sdk.admin.shippingProfile.retrieve(id, query), queryKey: shippingProfileQueryKeys.detail(id, query), ...options, }) @@ -61,19 +57,19 @@ export const useShippingProfile = ( } export const useShippingProfiles = ( - query?: Record, + query?: HttpTypes.AdminShippingProfileListParams, options?: Omit< UseQueryOptions< - ShippingProfileListRes, - Error, - ShippingProfileListRes, + HttpTypes.AdminShippingProfileListResponse, + FetchError, + HttpTypes.AdminShippingProfileListResponse, QueryKey >, "queryFn" | "queryKey" > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.shippingProfiles.list(query), + queryFn: () => sdk.admin.shippingProfile.list(query), queryKey: shippingProfileQueryKeys.list(query), ...options, }) @@ -82,12 +78,19 @@ export const useShippingProfiles = ( } export const useDeleteShippingProfile = ( - profileId: string, - options?: UseMutationOptions + id: string, + options?: UseMutationOptions< + HttpTypes.AdminShippingProfileDeleteResponse, + FetchError, + void + > ) => { return useMutation({ - mutationFn: () => client.shippingProfiles.delete(profileId), + mutationFn: () => sdk.admin.shippingProfile.delete(id), onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: shippingProfileQueryKeys.detail(id), + }) queryClient.invalidateQueries({ queryKey: shippingProfileQueryKeys.lists(), }) diff --git a/packages/admin-next/dashboard/src/hooks/api/stock-locations.tsx b/packages/admin-next/dashboard/src/hooks/api/stock-locations.tsx index 828457ea5a..21aad9dda3 100644 --- a/packages/admin-next/dashboard/src/hooks/api/stock-locations.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/stock-locations.tsx @@ -6,24 +6,11 @@ import { useQuery, } from "@tanstack/react-query" -import { client } from "../../lib/client" +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" +import { sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" import { queryKeysFactory } from "../../lib/query-key-factory" -import { - CreateFulfillmentSetReq, - CreateServiceZoneReq, - CreateStockLocationReq, - UpdateServiceZoneReq, - UpdateStockLocationReq, - UpdateStockLocationSalesChannelsReq, -} from "../../types/api-payloads" -import { - FulfillmentSetDeleteRes, - ServiceZoneDeleteRes, - StockLocationDeleteRes, - StockLocationListRes, - StockLocationRes, -} from "../../types/api-responses" const STOCK_LOCATIONS_QUERY_KEY = "stock_locations" as const export const stockLocationsQueryKeys = queryKeysFactory( @@ -32,14 +19,19 @@ export const stockLocationsQueryKeys = queryKeysFactory( export const useStockLocation = ( id: string, - query?: Record, + query?: HttpTypes.SelectParams, options?: Omit< - UseQueryOptions, + UseQueryOptions< + HttpTypes.AdminStockLocationResponse, + FetchError, + HttpTypes.AdminStockLocationResponse, + QueryKey + >, "queryKey" | "queryFn" > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.stockLocations.retrieve(id, query), + queryFn: () => sdk.admin.stockLocation.retrieve(id, query), queryKey: stockLocationsQueryKeys.detail(id, query), ...options, }) @@ -48,19 +40,19 @@ export const useStockLocation = ( } export const useStockLocations = ( - query?: Record, + query?: HttpTypes.AdminStockLocationListParams, options?: Omit< UseQueryOptions< - StockLocationListRes, - Error, - StockLocationListRes, + HttpTypes.AdminStockLocationListResponse, + FetchError, + HttpTypes.AdminStockLocationListResponse, QueryKey >, "queryKey" | "queryFn" > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.stockLocations.list(query), + queryFn: () => sdk.admin.stockLocation.list(query), queryKey: stockLocationsQueryKeys.list(query), ...options, }) @@ -69,12 +61,16 @@ export const useStockLocations = ( } export const useCreateStockLocation = ( - options?: UseMutationOptions + options?: UseMutationOptions< + HttpTypes.AdminStockLocationResponse, + FetchError, + HttpTypes.AdminCreateStockLocation + > ) => { return useMutation({ - mutationFn: (payload) => client.stockLocations.create(payload), - onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ + mutationFn: (payload) => sdk.admin.stockLocation.create(payload), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.lists(), }) @@ -86,15 +82,19 @@ export const useCreateStockLocation = ( export const useUpdateStockLocation = ( id: string, - options?: UseMutationOptions + options?: UseMutationOptions< + HttpTypes.AdminStockLocationResponse, + FetchError, + HttpTypes.AdminUpdateStockLocation + > ) => { return useMutation({ - mutationFn: (payload) => client.stockLocations.update(id, payload), - onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ + mutationFn: (payload) => sdk.admin.stockLocation.update(id, payload), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.details(), }) - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.lists(), }) @@ -107,21 +107,22 @@ export const useUpdateStockLocation = ( export const useUpdateStockLocationSalesChannels = ( id: string, options?: UseMutationOptions< - StockLocationRes, - Error, - UpdateStockLocationSalesChannelsReq + HttpTypes.AdminStockLocationResponse, + FetchError, + HttpTypes.AdminUpdateStockLocationSalesChannels > ) => { return useMutation({ mutationFn: (payload) => - client.stockLocations.updateSalesChannels(id, payload), - onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ + sdk.admin.stockLocation.updateSalesChannels(id, payload), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.details(), }) - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.lists(), }) + options?.onSuccess?.(data, variables, context) }, ...options, @@ -130,15 +131,19 @@ export const useUpdateStockLocationSalesChannels = ( export const useDeleteStockLocation = ( id: string, - options?: UseMutationOptions + options?: UseMutationOptions< + HttpTypes.AdminStockLocationDeleteResponse, + FetchError, + void + > ) => { return useMutation({ - mutationFn: () => client.stockLocations.delete(id), - onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ + mutationFn: () => sdk.admin.stockLocation.delete(id), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.lists(), }) - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.detail(id), }) @@ -148,105 +153,22 @@ export const useDeleteStockLocation = ( }) } -export const useCreateFulfillmentSet = ( +export const useCreateStockLocationFulfillmentSet = ( locationId: string, - options?: UseMutationOptions + options?: UseMutationOptions< + HttpTypes.AdminStockLocationResponse, + FetchError, + HttpTypes.AdminCreateStockLocationFulfillmentSet + > ) => { return useMutation({ mutationFn: (payload) => - client.stockLocations.createFulfillmentSet(locationId, payload), - onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ + sdk.admin.stockLocation.createFulfillmentSet(locationId, payload), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.lists(), }) - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.details(), - }) - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} - -export const useCreateServiceZone = ( - locationId: string, - fulfillmentSetId: string, - options?: UseMutationOptions -) => { - return useMutation({ - mutationFn: (payload) => - client.stockLocations.createServiceZone(fulfillmentSetId, payload), - onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.details(), - }) - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.lists(), - }) - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} - -export const useUpdateServiceZone = ( - fulfillmentSetId: string, - serviceZoneId: string, - locationId: string, - options?: UseMutationOptions -) => { - return useMutation({ - mutationFn: (payload) => - client.stockLocations.updateServiceZone( - fulfillmentSetId, - serviceZoneId, - payload - ), - onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.details(), - }) - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.lists(), - }) - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} - -export const useDeleteFulfillmentSet = ( - setId: string, - options?: UseMutationOptions -) => { - return useMutation({ - mutationFn: () => client.stockLocations.deleteFulfillmentSet(setId), - onSuccess: (data: any, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.lists(), - }) - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.details(), - }) - - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} - -export const useDeleteServiceZone = ( - setId: string, - zoneId: string, - options?: UseMutationOptions -) => { - return useMutation({ - mutationFn: () => client.stockLocations.deleteServiceZone(setId, zoneId), - onSuccess: (data: any, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: stockLocationsQueryKeys.lists(), - }) - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: stockLocationsQueryKeys.details(), }) diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx index b7b9b14dc9..89662d222a 100644 --- a/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx @@ -1,8 +1,9 @@ import { createColumnHelper } from "@tanstack/react-table" -import { SalesChannelDTO } from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" import { useMemo } from "react" import { useTranslation } from "react-i18next" + import { StatusCell } from "../../../components/table/table-cells/common/status-cell" import { TextHeader } from "../../../components/table/table-cells/common/text-cell" import { @@ -14,7 +15,7 @@ import { NameHeader, } from "../../../components/table/table-cells/sales-channel/name-cell" -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() export const useSalesChannelTableColumns = () => { const { t } = useTranslation() diff --git a/packages/admin-next/dashboard/src/hooks/use-combobox-data.tsx b/packages/admin-next/dashboard/src/hooks/use-combobox-data.tsx index 10cb4e0fec..a695ff935e 100644 --- a/packages/admin-next/dashboard/src/hooks/use-combobox-data.tsx +++ b/packages/admin-next/dashboard/src/hooks/use-combobox-data.tsx @@ -53,7 +53,7 @@ export const useComboboxData = < const { data, ...rest } = useInfiniteQuery({ queryKey: [...queryKey, query], queryFn: async ({ pageParam = 0 }) => { - return queryFn({ + return await queryFn({ q: query, limit: pageSize, offset: pageParam, @@ -77,7 +77,11 @@ export const useComboboxData = < const disabled = !rest.isPending && !options.length && !searchValue // // make sure that the default value is included in the option, if its not in options already - if (defaultValue && !options.find((o) => o.value === defaultValue)) { + if ( + defaultValue && + defaultOptions.length && + !options.find((o) => o.value === defaultValue) + ) { options.unshift(defaultOptions[0]) } diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index ebb2bce571..79f41669c2 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -20,6 +20,7 @@ "timeline": "Timeline", "success": "Success", "warning": "Warning", + "tip": "Tip", "error": "Error", "select": "Select", "selected": "Selected", @@ -766,124 +767,135 @@ "invalidEmail": "Email must be a valid email address." } }, - "location": { - "title": "Locations & Shipping", - "domain": "Location & Shipping", - "description": "Choose where you ship and how much you charge for shipping at checkout. Define shipping options specific for your locations.", - "createLocation": "Create location", - "createLocationDetailsHint": "Specify the details of the location.", - "deleteLocation": "Delete location", - "from": "Shipping from", - "add": "Add shipping", - "connectProvider": "Connect provider", - "addZone": "Add shipping zone", - "enablePickup": "Enable pickup", - "enableDelivery": "Enable delivery", - "deleteLocation": { - "label": "Delete Location", - "confirm": "Are you sure you want to delete {{name}} location", - "success": "{{name}} location successfully deleted" - }, - "noRecords": { - "action": "Add Location", - "title": "No inventory locations", - "message": "Please create an invnetory location first." + "stockLocations": { + "domain": "Locations & Shipping", + "list": { + "description": "Manage your store's stock locations and shipping options." }, "create": { - "title": "Add shipping for {{location}}", - "delivery": "Delivery", - "pickup": "Pickup", - "type": "Shipping type" + "header": "Create Stock Location", + "hint": "A stock location is a physical site where products are stored and shipped from.", + "successToast": "Location {{name}} was successfully created." }, - "fulfillmentSet": { - "placeholder": "Not covered by any shipping zones.", - "salesChannels": "Connected Sales Channels", - "delete": "Delete shipping", - "disableWarning": "Are you sure that you wnat to disable \"{{name}}\"? This will delete all assocciated service zones and shipping options.", - "create": { - "title": "Add service zone for {{fulfillmentSet}}" - }, - "toast": { - "disable": "\"{{name}}\" disabled" - }, - "addZone": "Add service zone", + "edit": { + "header": "Edit Stock Location", + "viewInventory": "View inventory", + "successToast": "Location {{name}} was successfully updated." + }, + "delete": { + "confirmation": "You are about to delete the stock location {{name}}. This action cannot be undone." + }, + "fulfillmentSets": { "pickup": { - "title": "Pick up", - "enable": "Enable pickup", - "offers": "Offers pick up in" + "header": "Pickup" }, - "delivery": { - "title": "Shipping", - "enable": "Enable delivery", - "offers": "Offers shippping to" - } - }, - "serviceZone": { - "create": { - "title": "Add service zone for {{fulfillmentSet}}", - "subtitle": "Service zone", - "description": "A service zone is a geographical region that can be shipped to from a specific location. You can later on add any number of shipping options to this zone. ", - "zoneName": "Zone name" + "shipping": { + "header": "Shipping" }, - "edit": { - "title": "Edit Service Zone" + "disable": { + "confirmation": "Are you sure that you want to disable {{name}}? This will delete all associated service zones and shipping options, and cannot be undone.", + "pickup": "Pickup was successfully disabled.", + "shipping": "Shipping was successfully disabled." }, - "deleteWarning": "Are you sure you want to delete \"{{name}}\". This will also delete all assocciated shipping options.", - "toast": { - "delete": "Zone \"{{name}}\" deleted successfully." - }, - "editPrices": "Edit prices", - "editOption": "Edit option", - "optionsLength_one": "shipping option", - "optionsLength_other": "shipping options", - "returnOptionsLength_one": "return option", - "returnOptionsLength_other": "return options", - "shippingOptionsPlaceholder": "Not covered by any shipping options.", - "addOption": "Add option", - "shippingOptions": "Shipping options", - "returnOptions": "Return options", - "areas": { - "title": "Areas affected by this rule", - "description": "Select the geographical areas where this shipping zone should apply.", - "manage": "Manage areas", - "error": "Please select at least one country for this service zone." - } - }, - "shippingOptions": { - "create": { - "title": "Create a shipping option for {{zone}}", - "subtitle": "General information", - "description": "To start selling, all you need is a name and a price", - "details": "Details", - "pricing": "Pricing", - "allocation": "Shipping amount", - "fixed": "Fixed", - "fixedDescription": "Shipping option's price is always the same amount.", - "enable": "Enable in store", - "enableDescription": "Enable or disable the shipping option visiblity in store", - "calculated": "Calculated", - "calculatedDescription": "Shipping option's price is calculated by the fulfillment provider.", - "profile": "Shipping profile" - }, - "deleteWarning": "Are you sure you want to delete \"{{name}}\"?", - "toast": { - "delete": "Shipping option \"{{name}}\" deleted successfully." - }, - "inStore": "Store", - "edit": { - "title": "Edit Shipping Option", - "provider": "Fulfillment provider" - } - }, - "returnOptions": { - "create": { - "title": "Create a return option for {{zone}}" + "enable": { + "pickup": "Pickup was successfully enabled.", + "shipping": "Shipping was successfully enabled." } }, "salesChannels": { - "title": "Connected Sales Channels", - "placeholder": "No connected channels yet.", - "connectChannels": "Connect Channels" + "header": "Sales Channels", + "label": "Connected sales channels", + "connectedTo": "Connected to {{count}} of {{total}} sales channels", + "noChannels": "The location is not connected to any sales channels.", + "action": "Connect sales channels", + "successToast": "Sales channels were successfully updated." + }, + "shippingOptions": { + "create": { + "shipping": { + "header": "Create Shipping Option for {{zone}}", + "hint": "Create a new shipping option to define how products are shipped from this location.", + "label": "Shipping options", + "successToast": "Shipping option {{name}} was successfully created." + }, + "returns": { + "header": "Create a Return Option for {{zone}}", + "hint": "Create a new return option to define how products are returned to this location.", + "label": "Return options", + "successToast": "Return option {{name}} was successfully created." + }, + "tabs": { + "details": "Details", + "prices": "Prices" + }, + "action": "Create option" + }, + "delete": { + "confirmation": "You are about to delete the shipping option {{name}}. This action cannot be undone.", + "successToast": "Shipping option {{name}} was successfully deleted." + }, + "edit": { + "header": "Edit Shipping Option", + "action": "Edit option", + "successToast": "Shipping option {{name}} was successfully updated." + }, + "pricing": { + "action": "Edit prices" + }, + "fields": { + "count": { + "shipping_one": "{{count}} shipping option", + "shipping_other": "{{count}} shipping options", + "returns_one": "{{count}} return option", + "returns_other": "{{count}} return options" + }, + "priceType": { + "label": "Price type", + "options": { + "fixed": { + "label": "Fixed", + "hint": "The shipping option's price is fixed and does not change based on the order's contents." + }, + "calculated": { + "label": "Calculated", + "hint": "The shipping option's price is calculated by the fulfillment provider." + } + } + }, + "enableInStore": { + "label": "Enable in store", + "hint": "Control the visibility of this option to customers in your store." + }, + "provider": "Fulfillment provider", + "profile": "Shipping profile" + } + }, + "serviceZones": { + "create": { + "headerPickup": "Create Service Zone for Pickup from {{location}}", + "headerShipping": "Create Service Zone for Shipping from {{location}}", + "action": "Create service zone", + "successToast": "Service zone {{name}} was successfully created." + }, + "edit": { + "header": "Edit Service Zone", + "successToast": "Service zone {{name}} was successfully updated." + }, + "delete": { + "confirmation": "You are about to delete the service zone {{name}}. This action cannot be undone.", + "successToast": "Service zone {{name}} was successfully deleted." + }, + "manageAreas": { + "header": "Manage Areas for {{name}}", + "action": "Manage areas", + "label": "Areas", + "hint": "Select the geographical areas that the service zone covers.", + "successToast": "Areas for {{name}} were successfully updated." + }, + "fields": { + "noRecords": "Currently not covered by any service zones.", + "tip": "A service zone is a collection of geographical zones or areas. It's used to restrict available shipping options to a defined set of locations." + } } }, "shippingProfile": { diff --git a/packages/admin-next/dashboard/src/lib/countries.ts b/packages/admin-next/dashboard/src/lib/countries.ts index bf1fa276d9..aee1cedaf9 100644 --- a/packages/admin-next/dashboard/src/lib/countries.ts +++ b/packages/admin-next/dashboard/src/lib/countries.ts @@ -1,1764 +1,1766 @@ -/** This file is auto-generated. Do not modify it manually. */ -import type { RegionCountryDTO } from "@medusajs/types" +import type { HttpTypes } from "@medusajs/types" + +export interface StaticCountry + extends Required> {} export function getCountryByIso2( iso2: string | null -): Omit | undefined { +): StaticCountry | undefined { if (!iso2) { return } - return countries.find((c) => c.iso_2 === iso2) + return countries.find((c) => c.iso_2.toLowerCase() === iso2.toLowerCase()) } -export const countries: Omit[] = [ +export const countries: StaticCountry[] = [ { iso_2: "af", iso_3: "afg", - num_code: 4, + num_code: "4", name: "AFGHANISTAN", display_name: "Afghanistan", }, { iso_2: "al", iso_3: "alb", - num_code: 8, + num_code: "8", name: "ALBANIA", display_name: "Albania", }, { iso_2: "dz", iso_3: "dza", - num_code: 12, + num_code: "12", name: "ALGERIA", display_name: "Algeria", }, { iso_2: "as", iso_3: "asm", - num_code: 16, + num_code: "16", name: "AMERICAN SAMOA", display_name: "American Samoa", }, { iso_2: "ad", iso_3: "and", - num_code: 20, + num_code: "20", name: "ANDORRA", display_name: "Andorra", }, { iso_2: "ao", iso_3: "ago", - num_code: 24, + num_code: "24", name: "ANGOLA", display_name: "Angola", }, { iso_2: "ai", iso_3: "aia", - num_code: 660, + num_code: "660", name: "ANGUILLA", display_name: "Anguilla", }, { iso_2: "aq", iso_3: "ata", - num_code: 10, + num_code: "10", name: "ANTARCTICA", display_name: "Antarctica", }, { iso_2: "ag", iso_3: "atg", - num_code: 28, + num_code: "28", name: "ANTIGUA AND BARBUDA", display_name: "Antigua and Barbuda", }, { iso_2: "ar", iso_3: "arg", - num_code: 32, + num_code: "32", name: "ARGENTINA", display_name: "Argentina", }, { iso_2: "am", iso_3: "arm", - num_code: 51, + num_code: "51", name: "ARMENIA", display_name: "Armenia", }, { iso_2: "aw", iso_3: "abw", - num_code: 533, + num_code: "533", name: "ARUBA", display_name: "Aruba", }, { iso_2: "au", iso_3: "aus", - num_code: 36, + num_code: "36", name: "AUSTRALIA", display_name: "Australia", }, { iso_2: "at", iso_3: "aut", - num_code: 40, + num_code: "40", name: "AUSTRIA", display_name: "Austria", }, { iso_2: "az", iso_3: "aze", - num_code: 31, + num_code: "31", name: "AZERBAIJAN", display_name: "Azerbaijan", }, { iso_2: "bs", iso_3: "bhs", - num_code: 44, + num_code: "44", name: "BAHAMAS", display_name: "Bahamas", }, { iso_2: "bh", iso_3: "bhr", - num_code: 48, + num_code: "48", name: "BAHRAIN", display_name: "Bahrain", }, { iso_2: "bd", iso_3: "bgd", - num_code: 50, + num_code: "50", name: "BANGLADESH", display_name: "Bangladesh", }, { iso_2: "bb", iso_3: "brb", - num_code: 52, + num_code: "52", name: "BARBADOS", display_name: "Barbados", }, { iso_2: "by", iso_3: "blr", - num_code: 112, + num_code: "112", name: "BELARUS", display_name: "Belarus", }, { iso_2: "be", iso_3: "bel", - num_code: 56, + num_code: "56", name: "BELGIUM", display_name: "Belgium", }, { iso_2: "bz", iso_3: "blz", - num_code: 84, + num_code: "84", name: "BELIZE", display_name: "Belize", }, { iso_2: "bj", iso_3: "ben", - num_code: 204, + num_code: "204", name: "BENIN", display_name: "Benin", }, { iso_2: "bm", iso_3: "bmu", - num_code: 60, + num_code: "60", name: "BERMUDA", display_name: "Bermuda", }, { iso_2: "bt", iso_3: "btn", - num_code: 64, + num_code: "64", name: "BHUTAN", display_name: "Bhutan", }, { iso_2: "bo", iso_3: "bol", - num_code: 68, + num_code: "68", name: "BOLIVIA", display_name: "Bolivia", }, { iso_2: "bq", iso_3: "bes", - num_code: 535, + num_code: "535", name: "BONAIRE, SINT EUSTATIUS AND SABA", display_name: "Bonaire, Sint Eustatius and Saba", }, { iso_2: "ba", iso_3: "bih", - num_code: 70, + num_code: "70", name: "BOSNIA AND HERZEGOVINA", display_name: "Bosnia and Herzegovina", }, { iso_2: "bw", iso_3: "bwa", - num_code: 72, + num_code: "72", name: "BOTSWANA", display_name: "Botswana", }, { iso_2: "bv", iso_3: "bvd", - num_code: 74, + num_code: "74", name: "BOUVET ISLAND", display_name: "Bouvet Island", }, { iso_2: "br", iso_3: "bra", - num_code: 76, + num_code: "76", name: "BRAZIL", display_name: "Brazil", }, { iso_2: "io", iso_3: "iot", - num_code: 86, + num_code: "86", name: "BRITISH INDIAN OCEAN TERRITORY", display_name: "British Indian Ocean Territory", }, { iso_2: "bn", iso_3: "brn", - num_code: 96, + num_code: "96", name: "BRUNEI DARUSSALAM", display_name: "Brunei Darussalam", }, { iso_2: "bg", iso_3: "bgr", - num_code: 100, + num_code: "100", name: "BULGARIA", display_name: "Bulgaria", }, { iso_2: "bf", iso_3: "bfa", - num_code: 854, + num_code: "854", name: "BURKINA FASO", display_name: "Burkina Faso", }, { iso_2: "bi", iso_3: "bdi", - num_code: 108, + num_code: "108", name: "BURUNDI", display_name: "Burundi", }, { iso_2: "kh", iso_3: "khm", - num_code: 116, + num_code: "116", name: "CAMBODIA", display_name: "Cambodia", }, { iso_2: "cm", iso_3: "cmr", - num_code: 120, + num_code: "120", name: "CAMEROON", display_name: "Cameroon", }, { iso_2: "ca", iso_3: "can", - num_code: 124, + num_code: "124", name: "CANADA", display_name: "Canada", }, { iso_2: "cv", iso_3: "cpv", - num_code: 132, + num_code: "132", name: "CAPE VERDE", display_name: "Cape Verde", }, { iso_2: "ky", iso_3: "cym", - num_code: 136, + num_code: "136", name: "CAYMAN ISLANDS", display_name: "Cayman Islands", }, { iso_2: "cf", iso_3: "caf", - num_code: 140, + num_code: "140", name: "CENTRAL AFRICAN REPUBLIC", display_name: "Central African Republic", }, { iso_2: "td", iso_3: "tcd", - num_code: 148, + num_code: "148", name: "CHAD", display_name: "Chad", }, { iso_2: "cl", iso_3: "chl", - num_code: 152, + num_code: "152", name: "CHILE", display_name: "Chile", }, { iso_2: "cn", iso_3: "chn", - num_code: 156, + num_code: "156", name: "CHINA", display_name: "China", }, { iso_2: "cx", iso_3: "cxr", - num_code: 162, + num_code: "162", name: "CHRISTMAS ISLAND", display_name: "Christmas Island", }, { iso_2: "cc", iso_3: "cck", - num_code: 166, + num_code: "166", name: "COCOS (KEELING) ISLANDS", display_name: "Cocos (Keeling) Islands", }, { iso_2: "co", iso_3: "col", - num_code: 170, + num_code: "170", name: "COLOMBIA", display_name: "Colombia", }, { iso_2: "km", iso_3: "com", - num_code: 174, + num_code: "174", name: "COMOROS", display_name: "Comoros", }, { iso_2: "cg", iso_3: "cog", - num_code: 178, + num_code: "178", name: "CONGO", display_name: "Congo", }, { iso_2: "cd", iso_3: "cod", - num_code: 180, + num_code: "180", name: "CONGO, THE DEMOCRATIC REPUBLIC OF THE", display_name: "Congo, the Democratic Republic of the", }, { iso_2: "ck", iso_3: "cok", - num_code: 184, + num_code: "184", name: "COOK ISLANDS", display_name: "Cook Islands", }, { iso_2: "cr", iso_3: "cri", - num_code: 188, + num_code: "188", name: "COSTA RICA", display_name: "Costa Rica", }, { iso_2: "ci", iso_3: "civ", - num_code: 384, + num_code: "384", name: "COTE D'IVOIRE", display_name: "Cote D'Ivoire", }, { iso_2: "hr", iso_3: "hrv", - num_code: 191, + num_code: "191", name: "CROATIA", display_name: "Croatia", }, { iso_2: "cu", iso_3: "cub", - num_code: 192, + num_code: "192", name: "CUBA", display_name: "Cuba", }, { iso_2: "cw", iso_3: "cuw", - num_code: 531, + num_code: "531", name: "CURAÇAO", display_name: "Curaçao", }, { iso_2: "cy", iso_3: "cyp", - num_code: 196, + num_code: "196", name: "CYPRUS", display_name: "Cyprus", }, { iso_2: "cz", iso_3: "cze", - num_code: 203, + num_code: "203", name: "CZECH REPUBLIC", display_name: "Czech Republic", }, { iso_2: "dk", iso_3: "dnk", - num_code: 208, + num_code: "208", name: "DENMARK", display_name: "Denmark", }, { iso_2: "dj", iso_3: "dji", - num_code: 262, + num_code: "262", name: "DJIBOUTI", display_name: "Djibouti", }, { iso_2: "dm", iso_3: "dma", - num_code: 212, + num_code: "212", name: "DOMINICA", display_name: "Dominica", }, { iso_2: "do", iso_3: "dom", - num_code: 214, + num_code: "214", name: "DOMINICAN REPUBLIC", display_name: "Dominican Republic", }, { iso_2: "ec", iso_3: "ecu", - num_code: 218, + num_code: "218", name: "ECUADOR", display_name: "Ecuador", }, { iso_2: "eg", iso_3: "egy", - num_code: 818, + num_code: "818", name: "EGYPT", display_name: "Egypt", }, { iso_2: "sv", iso_3: "slv", - num_code: 222, + num_code: "222", name: "EL SALVADOR", display_name: "El Salvador", }, { iso_2: "gq", iso_3: "gnq", - num_code: 226, + num_code: "226", name: "EQUATORIAL GUINEA", display_name: "Equatorial Guinea", }, { iso_2: "er", iso_3: "eri", - num_code: 232, + num_code: "232", name: "ERITREA", display_name: "Eritrea", }, { iso_2: "ee", iso_3: "est", - num_code: 233, + num_code: "233", name: "ESTONIA", display_name: "Estonia", }, { iso_2: "et", iso_3: "eth", - num_code: 231, + num_code: "231", name: "ETHIOPIA", display_name: "Ethiopia", }, { iso_2: "fk", iso_3: "flk", - num_code: 238, + num_code: "238", name: "FALKLAND ISLANDS (MALVINAS)", display_name: "Falkland Islands (Malvinas)", }, { iso_2: "fo", iso_3: "fro", - num_code: 234, + num_code: "234", name: "FAROE ISLANDS", display_name: "Faroe Islands", }, { iso_2: "fj", iso_3: "fji", - num_code: 242, + num_code: "242", name: "FIJI", display_name: "Fiji", }, { iso_2: "fi", iso_3: "fin", - num_code: 246, + num_code: "246", name: "FINLAND", display_name: "Finland", }, { iso_2: "fr", iso_3: "fra", - num_code: 250, + num_code: "250", name: "FRANCE", display_name: "France", }, { iso_2: "gf", iso_3: "guf", - num_code: 254, + num_code: "254", name: "FRENCH GUIANA", display_name: "French Guiana", }, { iso_2: "pf", iso_3: "pyf", - num_code: 258, + num_code: "258", name: "FRENCH POLYNESIA", display_name: "French Polynesia", }, { iso_2: "tf", iso_3: "atf", - num_code: 260, + num_code: "260", name: "FRENCH SOUTHERN TERRITORIES", display_name: "French Southern Territories", }, { iso_2: "ga", iso_3: "gab", - num_code: 266, + num_code: "266", name: "GABON", display_name: "Gabon", }, { iso_2: "gm", iso_3: "gmb", - num_code: 270, + num_code: "270", name: "GAMBIA", display_name: "Gambia", }, { iso_2: "ge", iso_3: "geo", - num_code: 268, + num_code: "268", name: "GEORGIA", display_name: "Georgia", }, { iso_2: "de", iso_3: "deu", - num_code: 276, + num_code: "276", name: "GERMANY", display_name: "Germany", }, { iso_2: "gh", iso_3: "gha", - num_code: 288, + num_code: "288", name: "GHANA", display_name: "Ghana", }, { iso_2: "gi", iso_3: "gib", - num_code: 292, + num_code: "292", name: "GIBRALTAR", display_name: "Gibraltar", }, { iso_2: "gr", iso_3: "grc", - num_code: 300, + num_code: "300", name: "GREECE", display_name: "Greece", }, { iso_2: "gl", iso_3: "grl", - num_code: 304, + num_code: "304", name: "GREENLAND", display_name: "Greenland", }, { iso_2: "gd", iso_3: "grd", - num_code: 308, + num_code: "308", name: "GRENADA", display_name: "Grenada", }, { iso_2: "gp", iso_3: "glp", - num_code: 312, + num_code: "312", name: "GUADELOUPE", display_name: "Guadeloupe", }, { iso_2: "gu", iso_3: "gum", - num_code: 316, + num_code: "316", name: "GUAM", display_name: "Guam", }, { iso_2: "gt", iso_3: "gtm", - num_code: 320, + num_code: "320", name: "GUATEMALA", display_name: "Guatemala", }, { iso_2: "gg", iso_3: "ggy", - num_code: 831, + num_code: "831", name: "GUERNSEY", display_name: "Guernsey", }, { iso_2: "gn", iso_3: "gin", - num_code: 324, + num_code: "324", name: "GUINEA", display_name: "Guinea", }, { iso_2: "gw", iso_3: "gnb", - num_code: 624, + num_code: "624", name: "GUINEA-BISSAU", display_name: "Guinea-Bissau", }, { iso_2: "gy", iso_3: "guy", - num_code: 328, + num_code: "328", name: "GUYANA", display_name: "Guyana", }, { iso_2: "ht", iso_3: "hti", - num_code: 332, + num_code: "332", name: "HAITI", display_name: "Haiti", }, { iso_2: "hm", iso_3: "hmd", - num_code: 334, + num_code: "334", name: "HEARD ISLAND AND MCDONALD ISLANDS", display_name: "Heard Island And Mcdonald Islands", }, { iso_2: "va", iso_3: "vat", - num_code: 336, + num_code: "336", name: "HOLY SEE (VATICAN CITY STATE)", display_name: "Holy See (Vatican City State)", }, { iso_2: "hn", iso_3: "hnd", - num_code: 340, + num_code: "340", name: "HONDURAS", display_name: "Honduras", }, { iso_2: "hk", iso_3: "hkg", - num_code: 344, + num_code: "344", name: "HONG KONG", display_name: "Hong Kong", }, { iso_2: "hu", iso_3: "hun", - num_code: 348, + num_code: "348", name: "HUNGARY", display_name: "Hungary", }, { iso_2: "is", iso_3: "isl", - num_code: 352, + num_code: "352", name: "ICELAND", display_name: "Iceland", }, { iso_2: "in", iso_3: "ind", - num_code: 356, + num_code: "356", name: "INDIA", display_name: "India", }, { iso_2: "id", iso_3: "idn", - num_code: 360, + num_code: "360", name: "INDONESIA", display_name: "Indonesia", }, { iso_2: "ir", iso_3: "irn", - num_code: 364, + num_code: "364", name: "IRAN, ISLAMIC REPUBLIC OF", display_name: "Iran, Islamic Republic of", }, { iso_2: "iq", iso_3: "irq", - num_code: 368, + num_code: "368", name: "IRAQ", display_name: "Iraq", }, { iso_2: "ie", iso_3: "irl", - num_code: 372, + num_code: "372", name: "IRELAND", display_name: "Ireland", }, { iso_2: "im", iso_3: "imn", - num_code: 833, + num_code: "833", name: "ISLE OF MAN", display_name: "Isle Of Man", }, { iso_2: "il", iso_3: "isr", - num_code: 376, + num_code: "376", name: "ISRAEL", display_name: "Israel", }, { iso_2: "it", iso_3: "ita", - num_code: 380, + num_code: "380", name: "ITALY", display_name: "Italy", }, { iso_2: "jm", iso_3: "jam", - num_code: 388, + num_code: "388", name: "JAMAICA", display_name: "Jamaica", }, { iso_2: "jp", iso_3: "jpn", - num_code: 392, + num_code: "392", name: "JAPAN", display_name: "Japan", }, { iso_2: "je", iso_3: "jey", - num_code: 832, + num_code: "832", name: "JERSEY", display_name: "Jersey", }, { iso_2: "jo", iso_3: "jor", - num_code: 400, + num_code: "400", name: "JORDAN", display_name: "Jordan", }, { iso_2: "kz", iso_3: "kaz", - num_code: 398, + num_code: "398", name: "KAZAKHSTAN", display_name: "Kazakhstan", }, { iso_2: "ke", iso_3: "ken", - num_code: 404, + num_code: "404", name: "KENYA", display_name: "Kenya", }, { iso_2: "ki", iso_3: "kir", - num_code: 296, + num_code: "296", name: "KIRIBATI", display_name: "Kiribati", }, { iso_2: "kp", iso_3: "prk", - num_code: 408, + num_code: "408", name: "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF", display_name: "Korea, Democratic People's Republic of", }, { iso_2: "kr", iso_3: "kor", - num_code: 410, + num_code: "410", name: "KOREA, REPUBLIC OF", display_name: "Korea, Republic of", }, { iso_2: "xk", iso_3: "xkx", - num_code: 900, + num_code: "900", name: "KOSOVO", display_name: "Kosovo", }, { iso_2: "kw", iso_3: "kwt", - num_code: 414, + num_code: "414", name: "KUWAIT", display_name: "Kuwait", }, { iso_2: "kg", iso_3: "kgz", - num_code: 417, + num_code: "417", name: "KYRGYZSTAN", display_name: "Kyrgyzstan", }, { iso_2: "la", iso_3: "lao", - num_code: 418, + num_code: "418", name: "LAO PEOPLE'S DEMOCRATIC REPUBLIC", display_name: "Lao People's Democratic Republic", }, { iso_2: "lv", iso_3: "lva", - num_code: 428, + num_code: "428", name: "LATVIA", display_name: "Latvia", }, { iso_2: "lb", iso_3: "lbn", - num_code: 422, + num_code: "422", name: "LEBANON", display_name: "Lebanon", }, { iso_2: "ls", iso_3: "lso", - num_code: 426, + num_code: "426", name: "LESOTHO", display_name: "Lesotho", }, { iso_2: "lr", iso_3: "lbr", - num_code: 430, + num_code: "430", name: "LIBERIA", display_name: "Liberia", }, { iso_2: "ly", iso_3: "lby", - num_code: 434, + num_code: "434", name: "LIBYA", display_name: "Libya", }, { iso_2: "li", iso_3: "lie", - num_code: 438, + num_code: "438", name: "LIECHTENSTEIN", display_name: "Liechtenstein", }, { iso_2: "lt", iso_3: "ltu", - num_code: 440, + num_code: "440", name: "LITHUANIA", display_name: "Lithuania", }, { iso_2: "lu", iso_3: "lux", - num_code: 442, + num_code: "442", name: "LUXEMBOURG", display_name: "Luxembourg", }, { iso_2: "mo", iso_3: "mac", - num_code: 446, + num_code: "446", name: "MACAO", display_name: "Macao", }, { iso_2: "mk", iso_3: "mkd", - num_code: 807, + num_code: "807", name: "MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF", display_name: "Macedonia, the Former Yugoslav Republic of", }, { iso_2: "mg", iso_3: "mdg", - num_code: 450, + num_code: "450", name: "MADAGASCAR", display_name: "Madagascar", }, { iso_2: "mw", iso_3: "mwi", - num_code: 454, + num_code: "454", name: "MALAWI", display_name: "Malawi", }, { iso_2: "my", iso_3: "mys", - num_code: 458, + num_code: "458", name: "MALAYSIA", display_name: "Malaysia", }, { iso_2: "mv", iso_3: "mdv", - num_code: 462, + num_code: "462", name: "MALDIVES", display_name: "Maldives", }, { iso_2: "ml", iso_3: "mli", - num_code: 466, + num_code: "466", name: "MALI", display_name: "Mali", }, { iso_2: "mt", iso_3: "mlt", - num_code: 470, + num_code: "470", name: "MALTA", display_name: "Malta", }, { iso_2: "mh", iso_3: "mhl", - num_code: 584, + num_code: "584", name: "MARSHALL ISLANDS", display_name: "Marshall Islands", }, { iso_2: "mq", iso_3: "mtq", - num_code: 474, + num_code: "474", name: "MARTINIQUE", display_name: "Martinique", }, { iso_2: "mr", iso_3: "mrt", - num_code: 478, + num_code: "478", name: "MAURITANIA", display_name: "Mauritania", }, { iso_2: "mu", iso_3: "mus", - num_code: 480, + num_code: "480", name: "MAURITIUS", display_name: "Mauritius", }, { iso_2: "yt", iso_3: "myt", - num_code: 175, + num_code: "175", name: "MAYOTTE", display_name: "Mayotte", }, { iso_2: "mx", iso_3: "mex", - num_code: 484, + num_code: "484", name: "MEXICO", display_name: "Mexico", }, { iso_2: "fm", iso_3: "fsm", - num_code: 583, + num_code: "583", name: "MICRONESIA, FEDERATED STATES OF", display_name: "Micronesia, Federated States of", }, { iso_2: "md", iso_3: "mda", - num_code: 498, + num_code: "498", name: "MOLDOVA, REPUBLIC OF", display_name: "Moldova, Republic of", }, { iso_2: "mc", iso_3: "mco", - num_code: 492, + num_code: "492", name: "MONACO", display_name: "Monaco", }, { iso_2: "mn", iso_3: "mng", - num_code: 496, + num_code: "496", name: "MONGOLIA", display_name: "Mongolia", }, { iso_2: "me", iso_3: "mne", - num_code: 499, + num_code: "499", name: "MONTENEGRO", display_name: "Montenegro", }, { iso_2: "ms", iso_3: "msr", - num_code: 500, + num_code: "500", name: "MONTSERRAT", display_name: "Montserrat", }, { iso_2: "ma", iso_3: "mar", - num_code: 504, + num_code: "504", name: "MOROCCO", display_name: "Morocco", }, { iso_2: "mz", iso_3: "moz", - num_code: 508, + num_code: "508", name: "MOZAMBIQUE", display_name: "Mozambique", }, { iso_2: "mm", iso_3: "mmr", - num_code: 104, + num_code: "104", name: "MYANMAR", display_name: "Myanmar", }, { iso_2: "na", iso_3: "nam", - num_code: 516, + num_code: "516", name: "NAMIBIA", display_name: "Namibia", }, { iso_2: "nr", iso_3: "nru", - num_code: 520, + num_code: "520", name: "NAURU", display_name: "Nauru", }, { iso_2: "np", iso_3: "npl", - num_code: 524, + num_code: "524", name: "NEPAL", display_name: "Nepal", }, { iso_2: "nl", iso_3: "nld", - num_code: 528, + num_code: "528", name: "NETHERLANDS", display_name: "Netherlands", }, { iso_2: "nc", iso_3: "ncl", - num_code: 540, + num_code: "540", name: "NEW CALEDONIA", display_name: "New Caledonia", }, { iso_2: "nz", iso_3: "nzl", - num_code: 554, + num_code: "554", name: "NEW ZEALAND", display_name: "New Zealand", }, { iso_2: "ni", iso_3: "nic", - num_code: 558, + num_code: "558", name: "NICARAGUA", display_name: "Nicaragua", }, { iso_2: "ne", iso_3: "ner", - num_code: 562, + num_code: "562", name: "NIGER", display_name: "Niger", }, { iso_2: "ng", iso_3: "nga", - num_code: 566, + num_code: "566", name: "NIGERIA", display_name: "Nigeria", }, { iso_2: "nu", iso_3: "niu", - num_code: 570, + num_code: "570", name: "NIUE", display_name: "Niue", }, { iso_2: "nf", iso_3: "nfk", - num_code: 574, + num_code: "574", name: "NORFOLK ISLAND", display_name: "Norfolk Island", }, { iso_2: "mp", iso_3: "mnp", - num_code: 580, + num_code: "580", name: "NORTHERN MARIANA ISLANDS", display_name: "Northern Mariana Islands", }, { iso_2: "no", iso_3: "nor", - num_code: 578, + num_code: "578", name: "NORWAY", display_name: "Norway", }, { iso_2: "om", iso_3: "omn", - num_code: 512, + num_code: "512", name: "OMAN", display_name: "Oman", }, { iso_2: "pk", iso_3: "pak", - num_code: 586, + num_code: "586", name: "PAKISTAN", display_name: "Pakistan", }, { iso_2: "pw", iso_3: "plw", - num_code: 585, + num_code: "585", name: "PALAU", display_name: "Palau", }, { iso_2: "ps", iso_3: "pse", - num_code: 275, + num_code: "275", name: "PALESTINIAN TERRITORY, OCCUPIED", display_name: "Palestinian Territory, Occupied", }, { iso_2: "pa", iso_3: "pan", - num_code: 591, + num_code: "591", name: "PANAMA", display_name: "Panama", }, { iso_2: "pg", iso_3: "png", - num_code: 598, + num_code: "598", name: "PAPUA NEW GUINEA", display_name: "Papua New Guinea", }, { iso_2: "py", iso_3: "pry", - num_code: 600, + num_code: "600", name: "PARAGUAY", display_name: "Paraguay", }, { iso_2: "pe", iso_3: "per", - num_code: 604, + num_code: "604", name: "PERU", display_name: "Peru", }, { iso_2: "ph", iso_3: "phl", - num_code: 608, + num_code: "608", name: "PHILIPPINES", display_name: "Philippines", }, { iso_2: "pn", iso_3: "pcn", - num_code: 612, + num_code: "612", name: "PITCAIRN", display_name: "Pitcairn", }, { iso_2: "pl", iso_3: "pol", - num_code: 616, + num_code: "616", name: "POLAND", display_name: "Poland", }, { iso_2: "pt", iso_3: "prt", - num_code: 620, + num_code: "620", name: "PORTUGAL", display_name: "Portugal", }, { iso_2: "pr", iso_3: "pri", - num_code: 630, + num_code: "630", name: "PUERTO RICO", display_name: "Puerto Rico", }, { iso_2: "qa", iso_3: "qat", - num_code: 634, + num_code: "634", name: "QATAR", display_name: "Qatar", }, { iso_2: "re", iso_3: "reu", - num_code: 638, + num_code: "638", name: "REUNION", display_name: "Reunion", }, { iso_2: "ro", iso_3: "rom", - num_code: 642, + num_code: "642", name: "ROMANIA", display_name: "Romania", }, { iso_2: "ru", iso_3: "rus", - num_code: 643, + num_code: "643", name: "RUSSIAN FEDERATION", display_name: "Russian Federation", }, { iso_2: "rw", iso_3: "rwa", - num_code: 646, + num_code: "646", name: "RWANDA", display_name: "Rwanda", }, { iso_2: "bl", iso_3: "blm", - num_code: 652, + num_code: "652", name: "SAINT BARTHÉLEMY", display_name: "Saint Barthélemy", }, { iso_2: "sh", iso_3: "shn", - num_code: 654, + num_code: "654", name: "SAINT HELENA", display_name: "Saint Helena", }, { iso_2: "kn", iso_3: "kna", - num_code: 659, + num_code: "659", name: "SAINT KITTS AND NEVIS", display_name: "Saint Kitts and Nevis", }, { iso_2: "lc", iso_3: "lca", - num_code: 662, + num_code: "662", name: "SAINT LUCIA", display_name: "Saint Lucia", }, { iso_2: "mf", iso_3: "maf", - num_code: 663, + num_code: "663", name: "SAINT MARTIN (FRENCH PART)", display_name: "Saint Martin (French part)", }, { iso_2: "pm", iso_3: "spm", - num_code: 666, + num_code: "666", name: "SAINT PIERRE AND MIQUELON", display_name: "Saint Pierre and Miquelon", }, { iso_2: "vc", iso_3: "vct", - num_code: 670, + num_code: "670", name: "SAINT VINCENT AND THE GRENADINES", display_name: "Saint Vincent and the Grenadines", }, { iso_2: "ws", iso_3: "wsm", - num_code: 882, + num_code: "882", name: "SAMOA", display_name: "Samoa", }, { iso_2: "sm", iso_3: "smr", - num_code: 674, + num_code: "674", name: "SAN MARINO", display_name: "San Marino", }, { iso_2: "st", iso_3: "stp", - num_code: 678, + num_code: "678", name: "SAO TOME AND PRINCIPE", display_name: "Sao Tome and Principe", }, { iso_2: "sa", iso_3: "sau", - num_code: 682, + num_code: "682", name: "SAUDI ARABIA", display_name: "Saudi Arabia", }, { iso_2: "sn", iso_3: "sen", - num_code: 686, + num_code: "686", name: "SENEGAL", display_name: "Senegal", }, { iso_2: "rs", iso_3: "srb", - num_code: 688, + num_code: "688", name: "SERBIA", display_name: "Serbia", }, { iso_2: "sc", iso_3: "syc", - num_code: 690, + num_code: "690", name: "SEYCHELLES", display_name: "Seychelles", }, { iso_2: "sl", iso_3: "sle", - num_code: 694, + num_code: "694", name: "SIERRA LEONE", display_name: "Sierra Leone", }, { iso_2: "sg", iso_3: "sgp", - num_code: 702, + num_code: "702", name: "SINGAPORE", display_name: "Singapore", }, { iso_2: "sx", iso_3: "sxm", - num_code: 534, + num_code: "534", name: "SINT MAARTEN", display_name: "Sint Maarten", }, { iso_2: "sk", iso_3: "svk", - num_code: 703, + num_code: "703", name: "SLOVAKIA", display_name: "Slovakia", }, { iso_2: "si", iso_3: "svn", - num_code: 705, + num_code: "705", name: "SLOVENIA", display_name: "Slovenia", }, { iso_2: "sb", iso_3: "slb", - num_code: 90, + num_code: "90", name: "SOLOMON ISLANDS", display_name: "Solomon Islands", }, { iso_2: "so", iso_3: "som", - num_code: 706, + num_code: "706", name: "SOMALIA", display_name: "Somalia", }, { iso_2: "za", iso_3: "zaf", - num_code: 710, + num_code: "710", name: "SOUTH AFRICA", display_name: "South Africa", }, { iso_2: "gs", iso_3: "sgs", - num_code: 239, + num_code: "239", name: "SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS", display_name: "South Georgia and the South Sandwich Islands", }, { iso_2: "ss", iso_3: "ssd", - num_code: 728, + num_code: "728", name: "SOUTH SUDAN", display_name: "South Sudan", }, { iso_2: "es", iso_3: "esp", - num_code: 724, + num_code: "724", name: "SPAIN", display_name: "Spain", }, { iso_2: "lk", iso_3: "lka", - num_code: 144, + num_code: "144", name: "SRI LANKA", display_name: "Sri Lanka", }, { iso_2: "sd", iso_3: "sdn", - num_code: 729, + num_code: "729", name: "SUDAN", display_name: "Sudan", }, { iso_2: "sr", iso_3: "sur", - num_code: 740, + num_code: "740", name: "SURINAME", display_name: "Suriname", }, { iso_2: "sj", iso_3: "sjm", - num_code: 744, + num_code: "744", name: "SVALBARD AND JAN MAYEN", display_name: "Svalbard and Jan Mayen", }, { iso_2: "sz", iso_3: "swz", - num_code: 748, + num_code: "748", name: "SWAZILAND", display_name: "Swaziland", }, { iso_2: "se", iso_3: "swe", - num_code: 752, + num_code: "752", name: "SWEDEN", display_name: "Sweden", }, { iso_2: "ch", iso_3: "che", - num_code: 756, + num_code: "756", name: "SWITZERLAND", display_name: "Switzerland", }, { iso_2: "sy", iso_3: "syr", - num_code: 760, + num_code: "760", name: "SYRIAN ARAB REPUBLIC", display_name: "Syrian Arab Republic", }, { iso_2: "tw", iso_3: "twn", - num_code: 158, + num_code: "158", name: "TAIWAN, PROVINCE OF CHINA", display_name: "Taiwan, Province of China", }, { iso_2: "tj", iso_3: "tjk", - num_code: 762, + num_code: "762", name: "TAJIKISTAN", display_name: "Tajikistan", }, { iso_2: "tz", iso_3: "tza", - num_code: 834, + num_code: "834", name: "TANZANIA, UNITED REPUBLIC OF", display_name: "Tanzania, United Republic of", }, { iso_2: "th", iso_3: "tha", - num_code: 764, + num_code: "764", name: "THAILAND", display_name: "Thailand", }, { iso_2: "tl", iso_3: "tls", - num_code: 626, + num_code: "626", name: "TIMOR LESTE", display_name: "Timor Leste", }, { iso_2: "tg", iso_3: "tgo", - num_code: 768, + num_code: "768", name: "TOGO", display_name: "Togo", }, { iso_2: "tk", iso_3: "tkl", - num_code: 772, + num_code: "772", name: "TOKELAU", display_name: "Tokelau", }, { iso_2: "to", iso_3: "ton", - num_code: 776, + num_code: "776", name: "TONGA", display_name: "Tonga", }, { iso_2: "tt", iso_3: "tto", - num_code: 780, + num_code: "780", name: "TRINIDAD AND TOBAGO", display_name: "Trinidad and Tobago", }, { iso_2: "tn", iso_3: "tun", - num_code: 788, + num_code: "788", name: "TUNISIA", display_name: "Tunisia", }, { iso_2: "tr", iso_3: "tur", - num_code: 792, + num_code: "792", name: "TURKEY", display_name: "Turkey", }, { iso_2: "tm", iso_3: "tkm", - num_code: 795, + num_code: "795", name: "TURKMENISTAN", display_name: "Turkmenistan", }, { iso_2: "tc", iso_3: "tca", - num_code: 796, + num_code: "796", name: "TURKS AND CAICOS ISLANDS", display_name: "Turks and Caicos Islands", }, { iso_2: "tv", iso_3: "tuv", - num_code: 798, + num_code: "798", name: "TUVALU", display_name: "Tuvalu", }, { iso_2: "ug", iso_3: "uga", - num_code: 800, + num_code: "800", name: "UGANDA", display_name: "Uganda", }, { iso_2: "ua", iso_3: "ukr", - num_code: 804, + num_code: "804", name: "UKRAINE", display_name: "Ukraine", }, { iso_2: "ae", iso_3: "are", - num_code: 784, + num_code: "784", name: "UNITED ARAB EMIRATES", display_name: "United Arab Emirates", }, { iso_2: "gb", iso_3: "gbr", - num_code: 826, + num_code: "826", name: "UNITED KINGDOM", display_name: "United Kingdom", }, { iso_2: "us", iso_3: "usa", - num_code: 840, + num_code: "840", name: "UNITED STATES", display_name: "United States", }, { iso_2: "um", iso_3: "umi", - num_code: 581, + num_code: "581", name: "UNITED STATES MINOR OUTLYING ISLANDS", display_name: "United States Minor Outlying Islands", }, { iso_2: "uy", iso_3: "ury", - num_code: 858, + num_code: "858", name: "URUGUAY", display_name: "Uruguay", }, { iso_2: "uz", iso_3: "uzb", - num_code: 860, + num_code: "860", name: "UZBEKISTAN", display_name: "Uzbekistan", }, { iso_2: "vu", iso_3: "vut", - num_code: 548, + num_code: "548", name: "VANUATU", display_name: "Vanuatu", }, { iso_2: "ve", iso_3: "ven", - num_code: 862, + num_code: "862", name: "VENEZUELA", display_name: "Venezuela", }, { iso_2: "vn", iso_3: "vnm", - num_code: 704, + num_code: "704", name: "VIET NAM", display_name: "Viet Nam", }, { iso_2: "vg", iso_3: "vgb", - num_code: 92, + num_code: "92", name: "VIRGIN ISLANDS, BRITISH", display_name: "Virgin Islands, British", }, { iso_2: "vi", iso_3: "vir", - num_code: 850, + num_code: "850", name: "VIRGIN ISLANDS, U.S.", display_name: "Virgin Islands, U.S.", }, { iso_2: "wf", iso_3: "wlf", - num_code: 876, + num_code: "876", name: "WALLIS AND FUTUNA", display_name: "Wallis and Futuna", }, { iso_2: "eh", iso_3: "esh", - num_code: 732, + num_code: "732", name: "WESTERN SAHARA", display_name: "Western Sahara", }, { iso_2: "ye", iso_3: "yem", - num_code: 887, + num_code: "887", name: "YEMEN", display_name: "Yemen", }, { iso_2: "zm", iso_3: "zmb", - num_code: 894, + num_code: "894", name: "ZAMBIA", display_name: "Zambia", }, { iso_2: "zw", iso_3: "zwe", - num_code: 716, + num_code: "716", name: "ZIMBABWE", display_name: "Zimbabwe", }, { iso_2: "ax", iso_3: "ala", - num_code: 248, + num_code: "248", name: "ÅLAND ISLANDS", display_name: "Åland Islands", }, diff --git a/packages/admin-next/dashboard/src/lib/shipping-options.ts b/packages/admin-next/dashboard/src/lib/shipping-options.ts index 6aaf4fb91a..c623a2a694 100644 --- a/packages/admin-next/dashboard/src/lib/shipping-options.ts +++ b/packages/admin-next/dashboard/src/lib/shipping-options.ts @@ -1,13 +1,15 @@ -import { ShippingOptionDTO } from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" -export function isReturnOption(shippingOption: ShippingOptionDTO) { +export function isReturnOption(shippingOption: HttpTypes.AdminShippingOption) { return !!shippingOption.rules?.find( (r) => r.attribute === "is_return" && r.value === "true" && r.operator === "eq" ) } -export function isOptionEnabledInStore(shippingOption: ShippingOptionDTO) { +export function isOptionEnabledInStore( + shippingOption: HttpTypes.AdminShippingOption +) { return !!shippingOption.rules?.find( (r) => r.attribute === "enabled_in_store" && 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 3ac0520989..a3635fbfc1 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 @@ -18,6 +18,7 @@ import { PriceListRes } from "../../types/api-responses" import { RouteExtensions } from "./route-extensions" import { SettingsExtensions } from "./settings-extensions" +// TODO: Add translations for all breadcrumbs export const RouteMap: RouteObject[] = [ { path: "/login", @@ -733,18 +734,20 @@ export const RouteMap: RouteObject[] = [ }, { path: ":location_id", - lazy: () => import("../../routes/locations/location-details"), + lazy: () => import("../../routes/locations/location-detail"), + handle: { + crumb: (data: HttpTypes.AdminStockLocationResponse) => + data.stock_location.name, + }, children: [ { path: "edit", lazy: () => import("../../routes/locations/location-edit"), }, { - path: "sales-channels/edit", + path: "sales-channels", lazy: () => - import( - "../../routes/locations/location-add-sales-channels" - ), + import("../../routes/locations/location-sales-channels"), }, { path: "fulfillment-set/:fset_id", @@ -752,7 +755,9 @@ export const RouteMap: RouteObject[] = [ { path: "service-zones/create", lazy: () => - import("../../routes/locations/service-zone-create"), + import( + "../../routes/locations/location-service-zone-create" + ), }, { path: "service-zone/:zone_id", @@ -761,14 +766,14 @@ export const RouteMap: RouteObject[] = [ path: "edit", lazy: () => import( - "../../routes/locations/service-zone-edit" + "../../routes/locations/location-service-zone-edit" ), }, { - path: "edit-areas", + path: "areas", lazy: () => import( - "../../routes/locations/service-zone-areas-edit" + "../../routes/locations/location-service-zone-manage-areas" ), }, { @@ -778,7 +783,7 @@ export const RouteMap: RouteObject[] = [ path: "create", lazy: () => import( - "../../routes/locations/shipping-options-create" + "../../routes/locations/location-service-zone-shipping-option-create" ), }, { @@ -788,14 +793,14 @@ export const RouteMap: RouteObject[] = [ path: "edit", lazy: () => import( - "../../routes/locations/shipping-option-edit" + "../../routes/locations/location-service-zone-shipping-option-edit" ), }, { - path: "edit-pricing", + path: "pricing", lazy: () => import( - "../../routes/locations/shipping-options-edit-pricing" + "../../routes/locations/location-service-zone-shipping-option-pricing" ), }, ], @@ -810,6 +815,7 @@ export const RouteMap: RouteObject[] = [ }, ], }, + { path: "workflows", element: , diff --git a/packages/admin-next/dashboard/src/routes/categories/category-detail/components/category-organize-section/category-organize-section.tsx b/packages/admin-next/dashboard/src/routes/categories/category-detail/components/category-organize-section/category-organize-section.tsx index e34865fd9c..e278077452 100644 --- a/packages/admin-next/dashboard/src/routes/categories/category-detail/components/category-organize-section/category-organize-section.tsx +++ b/packages/admin-next/dashboard/src/routes/categories/category-detail/components/category-organize-section/category-organize-section.tsx @@ -3,13 +3,13 @@ import { PencilSquare, TriangleRightMini, } from "@medusajs/icons" -import { AdminProductCategoryResponse, HttpTypes } from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" import { Badge, Container, Heading, Text, Tooltip } from "@medusajs/ui" import { useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" -import { InlineLink } from "../../../../../components/common/inline-link" +import { LinkButton } from "../../../../../components/common/link-button" import { Skeleton } from "../../../../../components/common/skeleton" import { useProductCategory } from "../../../../../hooks/api/categories" import { getCategoryChildren, getCategoryPath } from "../../../common/utils" @@ -60,7 +60,7 @@ export const CategoryOrganizeSection = ({ const PathDisplay = ({ category, }: { - category: AdminProductCategoryResponse["product_category"] + category: HttpTypes.AdminProductCategory }) => { const [expanded, setExpanded] = useState(false) @@ -137,12 +137,12 @@ const PathDisplay = ({ {chip.name} ) : ( - {chip.name} - + )} {index < chips.length - 1 && }
@@ -170,7 +170,7 @@ const PathDisplay = ({ const ChildrenDisplay = ({ category, }: { - category: AdminProductCategoryResponse["product_category"] + category: HttpTypes.AdminProductCategory }) => { const { product_category: withChildren, diff --git a/packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/geo-zone-form.tsx b/packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/geo-zone-form.tsx new file mode 100644 index 0000000000..07653eb628 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/geo-zone-form.tsx @@ -0,0 +1,312 @@ +import { Button, Checkbox } from "@medusajs/ui" +import { + OnChangeFn, + RowSelectionState, + createColumnHelper, +} from "@tanstack/react-table" +import { useEffect, useMemo, useState } from "react" +import { UseFormReturn, useFieldArray } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { z } from "zod" + +import { ChipGroup } from "../../../../../components/common/chip-group" +import { Form } from "../../../../../components/common/form" +import { SplitView } from "../../../../../components/layout/split-view" +import { DataTable } from "../../../../../components/table/data-table" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { + StaticCountry, + countries as staticCountries, +} from "../../../../../lib/countries" +import { useCountries } from "../../../../regions/common/hooks/use-countries" +import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns" +import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query" + +const GeoZoneSchema = z.object({ + countries: z.array( + z.object({ iso_2: z.string().min(2), display_name: z.string() }) + ), +}) + +type GeoZoneFormImplProps> = { + form: TForm + onOpenChange: (open: boolean) => void +} + +const GeoZoneFormImpl = >({ + form, + onOpenChange, +}: GeoZoneFormImplProps) => { + const castForm = form as unknown as UseFormReturn< + z.infer + > + + const { t } = useTranslation() + + const { fields, remove, replace } = useFieldArray({ + control: castForm.control, + name: "countries", + keyName: "iso_2", + }) + + const handleClearAll = () => { + replace([]) + } + + validateForm(form) + + return ( + { + return ( + +
+
+ + {t("stockLocations.serviceZones.manageAreas.label")} + + + {t("stockLocations.serviceZones.manageAreas.hint")} + +
+ +
+ + + {fields.length > 0 && ( + + {fields.map((field, index) => ( + + {field.display_name} + + ))} + + )} + +
+ ) + }} + /> + ) +} + +type AreasDrawerProps> = { + form: TForm + open: boolean + onOpenChange: (open: boolean) => void +} + +const PREFIX = "ac" +const PAGE_SIZE = 50 + +const AreaDrawer = >({ + form, + open, + onOpenChange, +}: AreasDrawerProps) => { + const castForm = form as unknown as UseFormReturn< + z.infer + > + + const { t } = useTranslation() + const { getValues, setValue } = castForm + + const [selection, setSelection] = useState({}) + const [state, setState] = useState<{ iso_2: string; display_name: string }[]>( + [] + ) + + const { searchParams, raw } = useCountryTableQuery({ + pageSize: PAGE_SIZE, + prefix: PREFIX, + }) + const { countries, count } = useCountries({ + countries: staticCountries.map((c) => ({ + display_name: c.display_name, + name: c.name, + iso_2: c.iso_2, + iso_3: c.iso_3, + num_code: c.num_code, + })), + ...searchParams, + }) + + useEffect(() => { + if (!open) { + return + } + + const countries = getValues("countries") + + if (countries) { + setState( + countries.map((country) => ({ + iso_2: country.iso_2, + display_name: country.display_name, + })) + ) + + setSelection( + countries.reduce( + (acc, country) => ({ + ...acc, + [country.iso_2]: true, + }), + {} + ) + ) + } + }, [open, getValues]) + + const updater: OnChangeFn = (fn) => { + const value = typeof fn === "function" ? fn(selection) : fn + const ids = Object.keys(value) + + const addedIdsSet = new Set(ids.filter((id) => value[id] && !selection[id])) + + const addedCountries: { iso_2: string; display_name: string }[] = [] + + if (addedIdsSet.size > 0) { + const countriesToAdd = + countries?.filter((country) => addedIdsSet.has(country.iso_2!)) ?? [] + + for (const country of countriesToAdd) { + addedCountries.push({ + iso_2: country.iso_2!, + display_name: country.display_name!, + }) + } + } + + setState((prev) => { + const filteredPrev = prev.filter((country) => value[country.iso_2]) + return Array.from(new Set([...filteredPrev, ...addedCountries])) + }) + setSelection(value) + } + + const handleAdd = () => { + setValue("countries", state, { + shouldDirty: true, + shouldTouch: true, + }) + onOpenChange(false) + } + + const columns = useColumns() + + const { table } = useDataTable({ + data: countries || [], + columns, + count, + enablePagination: true, + enableRowSelection: true, + getRowId: (row) => row.iso_2!, + pageSize: PAGE_SIZE, + rowSelection: { + state: selection, + updater, + }, + prefix: PREFIX, + }) + + validateForm(form) + + return ( + +
+ +
+ + + + +
+
+
+ ) +} + +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] + ) +} + +function validateForm(form: UseFormReturn) { + if (form.getValues("countries") === undefined) { + throw new Error( + "The form does not have a field named 'countries'. This field is required to use the GeoZoneForm component." + ) + } +} + +export const GeoZoneForm = Object.assign(GeoZoneFormImpl, { + AreaDrawer, +}) diff --git a/packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/index.ts new file mode 100644 index 0000000000..b8a190ca57 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/common/components/geo-zone-form/index.ts @@ -0,0 +1 @@ +export * from "./geo-zone-form" diff --git a/packages/admin-next/dashboard/src/routes/locations/common/constants.ts b/packages/admin-next/dashboard/src/routes/locations/common/constants.ts new file mode 100644 index 0000000000..cb1ee99f1d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/common/constants.ts @@ -0,0 +1,9 @@ +export enum FulfillmentSetType { + Shipping = "shipping", + Pickup = "pickup", +} + +export enum ShippingOptionPriceType { + FlatRate = "flat", + Calculated = "calculated", +} diff --git a/packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx b/packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx new file mode 100644 index 0000000000..3dd2c94803 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx @@ -0,0 +1,63 @@ +import { HttpTypes } from "@medusajs/types" +import { ColumnDef } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { DataGridCurrencyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-currency-cell" +import { createDataGridHelper } from "../../../../components/data-grid/utils" + +const columnHelper = createDataGridHelper() + +export const useShippingOptionPriceColumns = ({ + currencies = [], + regions = [], +}: { + currencies?: string[] + regions?: HttpTypes.AdminRegion[] +}) => { + const { t } = useTranslation() + + return useMemo(() => { + return [ + ...currencies.map((currency) => { + return columnHelper.column({ + id: `currency_prices.${currency}`, + name: t("fields.priceTemplate", { + regionOrCountry: currency.toUpperCase(), + }), + header: t("fields.priceTemplate", { + regionOrCountry: currency.toUpperCase(), + }), + cell: (context) => { + return ( + + ) + }, + }) + }), + ...regions.map((region) => { + return columnHelper.column({ + id: `region_prices.${region.id}`, + name: t("fields.priceTemplate", { + regionOrCountry: region.name, + }), + header: t("fields.priceTemplate", { + regionOrCountry: region.name, + }), + cell: (context) => { + return ( + + ) + }, + }) + }), + ] as ColumnDef<(string | HttpTypes.AdminRegion)[]>[] + }, [t, currencies, regions]) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts deleted file mode 100644 index eb7ea74f11..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LocationAddSalesChannels as Component } from "./location-add-sales-channels" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx deleted file mode 100644 index 950393de4a..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useParams } from "react-router-dom" - -import { RouteFocusModal } from "../../../components/route-modal" -import { LocationEditSalesChannelsForm } from "./components/edit-sales-channels-form" -import { useStockLocation } from "../../../hooks/api/stock-locations" - -export const LocationAddSalesChannels = () => { - const { location_id } = useParams() - const { - stock_location = {}, - isPending: isLocationLoading, - isError, - error, - } = useStockLocation(location_id!, { - fields: - "name,*sales_channels,address.city,address.country_code,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile", - }) - - if (isError) { - throw error - } - - return ( - - {!isLocationLoading && stock_location && ( - - )} - - ) -} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx index 97622f42f1..503392c70c 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx @@ -21,7 +21,7 @@ const CreateLocationSchema = zod.object({ postal_code: zod.string().optional(), province: zod.string().optional(), company: zod.string().optional(), - phone: zod.string().optional(), // TODO: Add validation + phone: zod.string().optional(), }), }) @@ -49,24 +49,30 @@ export const CreateLocationForm = () => { const { mutateAsync, isPending } = useCreateStockLocation() const handleSubmit = form.handleSubmit(async (values) => { - try { - await mutateAsync({ + await mutateAsync( + { name: values.name, address: values.address, - }) + }, + { + onSuccess: ({ stock_location }) => { + toast.success(t("general.success"), { + description: t("locations.toast.create"), + dismissable: true, + dismissLabel: t("actions.close"), + }) - handleSuccess("/settings/locations") - - toast.success(t("general.success"), { - description: t("locations.toast.create"), - dismissLabel: t("actions.close"), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } + handleSuccess(`/settings/locations/${stock_location.id}`) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + } + ) }) return ( @@ -92,10 +98,10 @@ export const CreateLocationForm = () => {
- {t("location.createLocation")} + {t("stockLocations.create.header")} - {t("location.createLocationDetailsHint")} + {t("stockLocations.create.hint")}
diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/components/location-general-section/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/location-details/components/location-general-section/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/components/location-general-section/location-general-section.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx similarity index 53% rename from packages/admin-next/dashboard/src/routes/locations/location-details/components/location-general-section/location-general-section.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx index 59dc7eb360..af81f95492 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-details/components/location-general-section/location-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx @@ -1,22 +1,18 @@ import { - ChevronDownMini, + ArchiveBox, CurrencyDollar, Map, PencilSquare, Plus, Trash, + TriangleDownMini, } from "@medusajs/icons" -import { - FulfillmentSetDTO, - ServiceZoneDTO, - ShippingOptionDTO, - StockLocationDTO, -} from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" import { Badge, - Button, Container, Heading, + IconButton, StatusBadge, Text, toast, @@ -27,25 +23,34 @@ import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" +import { Divider } from "../../../../../components/common/divider" import { NoRecords } from "../../../../../components/common/empty-table-content" +import { IconAvatar } from "../../../../../components/common/icon-avatar" +import { LinkButton } from "../../../../../components/common/link-button" import { ListSummary } from "../../../../../components/common/list-summary" +import { + useDeleteFulfillmentServiceZone, + useDeleteFulfillmentSet, +} from "../../../../../hooks/api/fulfillment-sets" import { useDeleteShippingOption } from "../../../../../hooks/api/shipping-options" import { - useCreateFulfillmentSet, - useDeleteFulfillmentSet, - useDeleteServiceZone, + useCreateStockLocationFulfillmentSet, useDeleteStockLocation, } from "../../../../../hooks/api/stock-locations" -import { countries as staticCountries } from "../../../../../lib/countries" +import { getFormattedAddress } from "../../../../../lib/addresses" +import { + StaticCountry, + countries as staticCountries, +} from "../../../../../lib/countries" import { formatProvider } from "../../../../../lib/format-provider" import { isOptionEnabledInStore, isReturnOption, } from "../../../../../lib/shipping-options" -import { getFormattedAddress } from "../../../../../lib/addresses" +import { FulfillmentSetType } from "../../../common/constants" type LocationGeneralSectionProps = { - location: StockLocationDTO + location: HttpTypes.AdminStockLocation } export const LocationGeneralSection = ({ @@ -69,7 +74,7 @@ export const LocationGeneralSection = ({ locationId={location.id} locationName={location.name} type={FulfillmentSetType.Pickup} - fulfillmentSet={location.fulfillment_sets.find( + fulfillmentSet={location.fulfillment_sets?.find( (f) => f.type === FulfillmentSetType.Pickup )} /> @@ -77,9 +82,9 @@ export const LocationGeneralSection = ({ f.type === FulfillmentSetType.Delivery + type={FulfillmentSetType.Shipping} + fulfillmentSet={location.fulfillment_sets?.find( + (f) => f.type === FulfillmentSetType.Shipping )} /> @@ -87,31 +92,31 @@ export const LocationGeneralSection = ({ } type ShippingOptionProps = { - option: ShippingOptionDTO + option: HttpTypes.AdminShippingOption fulfillmentSetId: string locationId: string - isReturn?: boolean } function ShippingOption({ option, - isReturn, fulfillmentSetId, locationId, }: ShippingOptionProps) { const prompt = usePrompt() const { t } = useTranslation() - const isInStore = isOptionEnabledInStore(option) + const isStoreOption = isOptionEnabledInStore(option) - const { mutateAsync: deleteOption } = useDeleteShippingOption(option.id) + const { mutateAsync } = useDeleteShippingOption(option.id) const handleDelete = async () => { const res = await prompt({ title: t("general.areYouSure"), - description: t("location.shippingOptions.deleteWarning", { + description: t("stockLocations.shippingOptions.delete.confirmation", { name: option.name, }), + verificationInstruction: t("general.typeToConfirm"), + verificationText: option.name, confirmText: t("actions.delete"), cancelText: t("actions.cancel"), }) @@ -120,50 +125,60 @@ function ShippingOption({ return } - try { - await deleteOption() - - toast.success(t("general.success"), { - description: t("location.shippingOptions.toast.delete", { - name: option.name, - }), - dismissLabel: t("actions.close"), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } + await mutateAsync(undefined, { + onSuccess: () => { + toast.success(t("general.success"), { + description: t("stockLocations.shippingOptions.delete.successToast", { + name: option.name, + }), + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + }) } return ( -
+
- + {option.name} - {option.shipping_profile.name} ( {formatProvider(option.provider_id)}) - +
- {isInStore && ( - - {t("location.shippingOptions.inStore")} - - )} + + {isStoreOption ? t("general.store") : t("general.admin")} + , - label: t("location.serviceZone.editOption"), + label: t("stockLocations.shippingOptions.edit.action"), to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/edit`, }, { - label: t("location.serviceZone.editPrices"), + label: t("stockLocations.shippingOptions.pricing.action"), icon: , - to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/edit-pricing`, + to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/pricing`, }, + ], + }, + { + actions: [ { label: t("actions.delete"), icon: , @@ -178,7 +193,7 @@ function ShippingOption({ } type ServiceZoneOptionsProps = { - zone: ServiceZoneDTO + zone: HttpTypes.AdminServiceZone locationId: string fulfillmentSetId: string } @@ -189,7 +204,6 @@ function ServiceZoneOptions({ fulfillmentSetId, }: ServiceZoneOptionsProps) { const { t } = useTranslation() - const navigate = useNavigate() const shippingOptions = zone.shipping_options.filter( (o) => !isReturnOption(o) @@ -198,27 +212,22 @@ function ServiceZoneOptions({ const returnOptions = zone.shipping_options.filter((o) => isReturnOption(o)) return ( - <> -
+
+ +
- {t("location.serviceZone.shippingOptions")} + {t("stockLocations.shippingOptions.create.shipping.label")} - + {t("stockLocations.shippingOptions.create.action")} +
{!!shippingOptions.length && ( -
+
{shippingOptions.map((o) => ( -
+ + +
- {t("location.serviceZone.returnOptions")} + {t("stockLocations.shippingOptions.create.returns.label")} - + {t("stockLocations.shippingOptions.create.action")} +
{!!returnOptions.length && ( -
+
{returnOptions.map((o) => ( )}
- +
) } type ServiceZoneProps = { - zone: ServiceZoneDTO + zone: HttpTypes.AdminServiceZone locationId: string fulfillmentSetId: string } @@ -278,7 +282,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) { const prompt = usePrompt() const [open, setOpen] = useState(false) - const { mutateAsync: deleteZone } = useDeleteServiceZone( + const { mutateAsync: deleteZone } = useDeleteFulfillmentServiceZone( fulfillmentSetId, zone.id ) @@ -286,7 +290,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) { const handleDelete = async () => { const res = await prompt({ title: t("general.areYouSure"), - description: t("location.serviceZone.deleteWarning", { + description: t("stockLocations.serviceZones.delete.confirmation", { name: zone.name, }), confirmText: t("actions.delete"), @@ -297,97 +301,104 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) { return } - try { - await deleteZone() - - toast.success(t("general.success"), { - description: t("location.serviceZone.toast.delete", { - name: zone.name, - }), - dismissLabel: t("actions.close"), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } + await deleteZone(undefined, { + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + }, + onSuccess: () => { + toast.success(t("general.success"), { + description: t("stockLocations.serviceZones.delete.successToast", { + name: zone.name, + }), + dismissLabel: t("actions.close"), + }) + }, + }) } const countries = useMemo(() => { - return zone.geo_zones - .filter((g) => g.type === "country") - .map((g) => g.country_code) - .map((code) => staticCountries.find((c) => c.iso_2 === code)) - .sort((c1, c2) => c1.name.localeCompare(c2.name)) - }, zone.geo_zones) + const countryGeoZones = zone.geo_zones.filter((g) => g.type === "country") + + const countries = countryGeoZones + .map(({ country_code }) => + staticCountries.find((c) => c.iso_2 === country_code) + ) + .filter((c) => !!c) as StaticCountry[] + + if ( + process.env.NODE_ENV === "development" && + countryGeoZones.length !== countries.length + ) { + console.warn( + "Some countries are missing in the static countries list", + countryGeoZones + .filter((g) => !countries.find((c) => c.iso_2 === g.country_code)) + .map((g) => g.country_code) + ) + } + + return countries.sort((c1, c2) => c1.name.localeCompare(c2.name)) + }, [zone.geo_zones]) const [shippingOptionsCount, returnOptionsCount] = useMemo(() => { - const optionsCount = zone.shipping_options.filter( - (o) => !isReturnOption(o) - ).length + const options = zone.shipping_options - const returnOptionsCount = zone.shipping_options.filter((o) => - isReturnOption(o) - ).length + const optionsCount = options.filter((o) => !isReturnOption(o)).length + + const returnOptionsCount = options.filter(isReturnOption).length return [optionsCount, returnOptionsCount] }, [zone.shipping_options]) return ( -
-
- {/* ICON*/} -
-
- -
-
+
+
+ + + - {/* INFO*/}
- {zone.name} + + {zone.name} +
c.display_name)} inline n={1} /> · - {shippingOptionsCount}{" "} - {t("location.serviceZone.optionsLength", { + {t("stockLocations.shippingOptions.fields.count.shipping", { count: shippingOptionsCount, })} · - {returnOptionsCount}{" "} - {t("location.serviceZone.returnOptionsLength", { + {t("stockLocations.shippingOptions.fields.count.returns", { count: returnOptionsCount, })}
- {/* ACTION*/} -
- + , to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/edit-areas`, }, + ], + }, + { + actions: [ { label: t("actions.delete"), icon: , @@ -414,25 +429,18 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
{open && ( -
- -
+ )}
) } -enum FulfillmentSetType { - Delivery = "delivery", - Pickup = "pickup", -} - type FulfillmentSetProps = { - fulfillmentSet?: FulfillmentSetDTO + fulfillmentSet?: HttpTypes.AdminFulfillmentSet locationName: string locationId: string type: FulfillmentSetType @@ -441,7 +449,6 @@ type FulfillmentSetProps = { function FulfillmentSet(props: FulfillmentSetProps) { const { t } = useTranslation() const prompt = usePrompt() - const navigate = useNavigate() const { fulfillmentSet, locationName, locationId, type } = props @@ -449,36 +456,47 @@ function FulfillmentSet(props: FulfillmentSetProps) { const hasServiceZones = !!fulfillmentSet?.service_zones.length - const { mutateAsync: createFulfillmentSet, isPending: isLoading } = - useCreateFulfillmentSet(locationId) + const { mutateAsync: createFulfillmentSet } = + useCreateStockLocationFulfillmentSet(locationId) const { mutateAsync: deleteFulfillmentSet } = useDeleteFulfillmentSet( - fulfillmentSet?.id + fulfillmentSet?.id! ) const handleCreate = async () => { - try { - await createFulfillmentSet({ + await createFulfillmentSet( + { name: `${locationName} ${ type === FulfillmentSetType.Pickup ? "pick up" : type }`, type, - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } + }, + { + onSuccess: () => { + toast.success(t("general.success"), { + description: t(`stockLocations.fulfillmentSets.enable.${type}`), + dismissLabel: t("actions.close"), + dismissable: true, + }) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + dismissable: true, + }) + }, + } + ) } const handleDelete = async () => { const res = await prompt({ title: t("general.areYouSure"), - description: t("location.fulfillmentSet.disableWarning", { + description: t(`stockLocations.fulfillmentSets.disable.confirmation`, { name: fulfillmentSet?.name, }), - confirmText: t("actions.delete"), + confirmText: t("actions.disable"), cancelText: t("actions.cancel"), }) @@ -486,83 +504,85 @@ function FulfillmentSet(props: FulfillmentSetProps) { return } - try { - await deleteFulfillmentSet() - - toast.success(t("general.success"), { - description: t("location.fulfillmentSet.toast.disable", { - name: fulfillmentSet?.name, - }), - dismissLabel: t("actions.close"), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } + await deleteFulfillmentSet(undefined, { + onSuccess: () => { + toast.success(t("general.success"), { + description: t(`stockLocations.fulfillmentSets.disable.${type}`), + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + }) } + const groups = fulfillmentSet + ? [ + { + actions: [ + { + icon: , + label: t("stockLocations.serviceZones.create.action"), + to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create`, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("actions.disable"), + onClick: handleDelete, + }, + ], + }, + ] + : [ + { + actions: [ + { + icon: , + label: t("actions.enable"), + onClick: handleCreate, + }, + ], + }, + ] + return (
- - {t(`location.fulfillmentSet.${type}.offers`)} - + + {t(`stockLocations.fulfillmentSets.${type}.header`)} +
- + {t( fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled" )} - , - label: t("location.fulfillmentSet.addZone"), - onClick: () => - navigate( - `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSet.id}/service-zones/create` - ), - disabled: !fulfillmentSetExists, - }, - { - icon: , - label: fulfillmentSetExists - ? t("actions.disable") - : t("actions.enable"), - onClick: fulfillmentSetExists - ? handleDelete - : handleCreate, - }, - ], - }, - ]} - /> +
{fulfillmentSetExists && !hasServiceZones && ( -
+
- -
)} @@ -583,7 +603,7 @@ function FulfillmentSet(props: FulfillmentSetProps) { ) } -const Actions = ({ location }: { location: StockLocationDTO }) => { +const Actions = ({ location }: { location: HttpTypes.AdminStockLocation }) => { const navigate = useNavigate() const { t } = useTranslation() const { mutateAsync } = useDeleteStockLocation(location.id) @@ -592,7 +612,7 @@ const Actions = ({ location }: { location: StockLocationDTO }) => { const handleDelete = async () => { const res = await prompt({ title: t("general.areYouSure"), - description: t("location.deleteLocationWarning", { + description: t("stockLocations.delete.confirmation", { name: location.name, }), verificationText: location.name, @@ -605,19 +625,25 @@ const Actions = ({ location }: { location: StockLocationDTO }) => { return } - try { - await mutateAsync(undefined) - toast.success(t("general.success"), { - description: t("location.toast.delete"), - dismissLabel: t("actions.close"), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } - navigate("/settings/locations", { replace: true }) + await mutateAsync(undefined, { + onSuccess: () => { + toast.success(t("general.success"), { + description: t("stockLocations.create.successToast", { + name: location.name, + }), + dismissable: true, + dismissLabel: t("actions.close"), + }) + navigate("/settings/locations", { replace: true }) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + }) } return ( @@ -630,6 +656,11 @@ const Actions = ({ location }: { location: StockLocationDTO }) => { label: t("actions.edit"), to: `edit`, }, + { + icon: , + label: t("stockLocations.edit.viewInventory"), + to: `/inventory?location_id=${location.id}`, + }, ], }, { diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/components/location-sales-channels-section/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channels-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/location-details/components/location-sales-channels-section/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channels-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channels-section/locations-sales-channels-section.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channels-section/locations-sales-channels-section.tsx new file mode 100644 index 0000000000..caaf23af7c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channels-section/locations-sales-channels-section.tsx @@ -0,0 +1,76 @@ +import { Channels, PencilSquare } from "@medusajs/icons" +import { HttpTypes } from "@medusajs/types" +import { Container, Heading, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { NoRecords } from "../../../../../components/common/empty-table-content" +import { IconAvatar } from "../../../../../components/common/icon-avatar" +import { ListSummary } from "../../../../../components/common/list-summary" +import { useSalesChannels } from "../../../../../hooks/api/sales-channels" + +type LocationsSalesChannelsSectionProps = { + location: HttpTypes.AdminStockLocation +} + +function LocationsSalesChannelsSection({ + location, +}: LocationsSalesChannelsSectionProps) { + const { t } = useTranslation() + const { count } = useSalesChannels({ limit: 1, fields: "id" }) + + const hasConnectedChannels = !!location.sales_channels?.length + + return ( + +
+ {t("stockLocations.salesChannels.header")} + , + }, + ], + }, + ]} + /> +
+ {hasConnectedChannels ? ( +
+
+ + + + sc.name) ?? []} + /> +
+ + {t("stockLocations.salesChannels.connectedTo", { + count: location.sales_channels?.length, + total: count, + })} + +
+ ) : ( + + )} +
+ ) +} + +export default LocationsSalesChannelsSection diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/const.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/const.ts similarity index 85% rename from packages/admin-next/dashboard/src/routes/locations/location-details/const.ts rename to packages/admin-next/dashboard/src/routes/locations/location-detail/const.ts index 50aec4d35e..4a22fff87d 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-details/const.ts +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/const.ts @@ -1,2 +1,2 @@ export const detailsFields = - "name,*sales_channels,*address,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile,*fulfillment_sets.service_zones.shipping_options.prices" + "name,*sales_channels,*address,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts new file mode 100644 index 0000000000..7aab908774 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts @@ -0,0 +1,2 @@ +export { locationLoader as loader } from "./loader" +export { LocationDetail as Component } from "./location-detail" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/loader.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/loader.ts new file mode 100644 index 0000000000..66c525e8d8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/loader.ts @@ -0,0 +1,36 @@ +import { HttpTypes } from "@medusajs/types" +import { LoaderFunctionArgs, redirect } from "react-router-dom" + +import { FetchError } from "@medusajs/js-sdk" +import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations" +import { sdk } from "../../../lib/client" +import { queryClient } from "../../../lib/query-client" +import { detailsFields } from "./const" + +const locationQuery = (id: string) => ({ + queryKey: stockLocationsQueryKeys.detail(id), + queryFn: async () => { + return await sdk.admin.stockLocation + .retrieve(id, { + fields: detailsFields, + }) + .catch((error: FetchError) => { + if (error.status === 401) { + throw redirect("/login") + } + + throw error + }) + }, +}) + +export const locationLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.location_id + const query = locationQuery(id!) + + return ( + queryClient.getQueryData<{ stock_location: HttpTypes.AdminStockLocation }>( + query.queryKey + ) ?? (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/location-details.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx similarity index 92% rename from packages/admin-next/dashboard/src/routes/locations/location-details/location-details.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx index 69eb600247..8102f2bdad 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-details/location-details.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx @@ -12,7 +12,7 @@ import sideAfter from "virtual:medusa/widgets/location/details/side/after" import sideBefore from "virtual:medusa/widgets/location/details/side/before" import { detailsFields } from "./const" -export const LocationDetails = () => { +export const LocationDetail = () => { const initialData = useLoaderData() as Awaited< ReturnType > @@ -51,7 +51,7 @@ export const LocationDetails = () => {
) })} -
+
{after.widgets.map((w, i) => { @@ -65,7 +65,7 @@ export const LocationDetails = () => {
-
+
{sideBefore.widgets.map((w, i) => { return (
diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/components/location-sales-channels-section/locations-sales-channels-section.tsx b/packages/admin-next/dashboard/src/routes/locations/location-details/components/location-sales-channels-section/locations-sales-channels-section.tsx deleted file mode 100644 index e78d2984c2..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/location-details/components/location-sales-channels-section/locations-sales-channels-section.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Channels, PencilSquare } from "@medusajs/icons" -import { StockLocationDTO } from "@medusajs/types" -import { Heading, Text } from "@medusajs/ui" -import { Trans, useTranslation } from "react-i18next" - -import { ActionMenu } from "../../../../../components/common/action-menu" -import { ListSummary } from "../../../../../components/common/list-summary" -import { useSalesChannels } from "../../../../../hooks/api/sales-channels" - -type Props = { - location: StockLocationDTO -} - -function LocationsSalesChannelsSection({ location }: Props) { - const { t } = useTranslation() - const { count } = useSalesChannels() - - const noChannels = !location.sales_channels?.length - - return ( -
-
- {t("location.salesChannels.title")} - , - }, - ], - }, - ]} - /> -
-
-
-
- -
-
- {noChannels ? ( - - {t("location.salesChannels.placeholder")} - - ) : ( - sc.name)} - /> - )} -
- - , - , - ]} - /> - -
- ) -} - -export default LocationsSalesChannelsSection diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-details/index.ts deleted file mode 100644 index 83965180c1..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/location-details/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { locationLoader as loader } from "./loader" -export { LocationDetails as Component } from "./location-details" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-details/loader.ts b/packages/admin-next/dashboard/src/routes/locations/location-details/loader.ts deleted file mode 100644 index 42be40ff8d..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/location-details/loader.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AdminStockLocationResponse } from "@medusajs/types" -import { LoaderFunctionArgs } from "react-router-dom" - -import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations" -import { client } from "../../../lib/client" -import { queryClient } from "../../../lib/query-client" -import { detailsFields } from "./const" - -const locationQuery = (id: string) => ({ - queryKey: stockLocationsQueryKeys.detail(id), - queryFn: async () => - client.stockLocations.retrieve(id, { - fields: detailsFields, - }), -}) - -export const locationLoader = async ({ params }: LoaderFunctionArgs) => { - const id = params.location_id - const query = locationQuery(id!) - - return ( - queryClient.getQueryData(query.queryKey) ?? - (await queryClient.fetchQuery(query)) - ) -} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx index 3f9a43df0f..3be9c1a3b7 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx @@ -1,4 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" import { Button, Input, toast } from "@medusajs/ui" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" @@ -11,10 +12,9 @@ import { useRouteModal, } from "../../../../../components/route-modal" import { useUpdateStockLocation } from "../../../../../hooks/api/stock-locations" -import { ExtendedStockLocationDTO } from "../../../../../types/api-responses" type EditLocationFormProps = { - location: ExtendedStockLocationDTO + location: HttpTypes.AdminStockLocation } const EditLocationSchema = zod.object({ @@ -55,23 +55,30 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => { const { mutateAsync, isPending } = useUpdateStockLocation(location.id) const handleSubmit = form.handleSubmit(async (values) => { - try { - await mutateAsync({ - name: values.name, - address: values.address, - }) - handleSuccess() + const { name, address } = values - toast.success(t("general.success"), { - description: t("locations.toast.update"), - dismissLabel: t("actions.close"), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } + await mutateAsync( + { + name: name, + address: address, + }, + { + onSuccess: () => { + toast.success(t("general.success"), { + description: t("stockLocations.edit.successToast"), + dismissable: true, + dismissLabel: t("actions.close"), + }) + handleSuccess() + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + }, + } + ) }) return ( diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx b/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx index 1c761d8322..2701bae9f6 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx @@ -6,18 +6,17 @@ import { useStockLocation } from "../../../hooks/api/stock-locations" import { EditLocationForm } from "./components/edit-location-form" export const LocationEdit = () => { + const { t } = useTranslation() const { location_id } = useParams() - const { - stock_location, - isPending: isLoading, - isError, - error, - } = useStockLocation(location_id, { - fields: "*address", - }) + const { stock_location, isPending, isError, error } = useStockLocation( + location_id!, + { + fields: "*address", + } + ) - const { t } = useTranslation() + const ready = !isPending && !!stock_location if (isError) { throw error @@ -28,9 +27,7 @@ export const LocationEdit = () => { {t("locations.editLocation")} - {!isLoading && !!stock_location && ( - - )} + {ready && } ) } diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/index.ts new file mode 100644 index 0000000000..3f8a119f76 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/index.ts @@ -0,0 +1 @@ +export * from "./location-list-header" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/location-list-header.tsx b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/location-list-header.tsx new file mode 100644 index 0000000000..8747eeb112 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-header/location-list-header.tsx @@ -0,0 +1,21 @@ +import { Button, Container, Heading, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" + +export const LocationListHeader = () => { + const { t } = useTranslation() + + return ( + +
+ {t("stockLocations.domain")} + + {t("stockLocations.list.description")} + +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-item/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-item/index.ts new file mode 100644 index 0000000000..7cbfc12073 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-item/index.ts @@ -0,0 +1 @@ +export * from "./location-list-item" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/location/location.tsx b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-item/location-list-item.tsx similarity index 65% rename from packages/admin-next/dashboard/src/routes/locations/location-list/components/location/location.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-item/location-list-item.tsx index 676da44a53..b3bd548527 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-list/components/location/location.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location-list-item/location-list-item.tsx @@ -1,27 +1,17 @@ import { Buildings, PencilSquare, Trash } from "@medusajs/icons" -import { - FulfillmentSetDTO, - SalesChannelDTO, - StockLocationDTO, -} from "@medusajs/types" -import { - Button, - Container, - StatusBadge, - Text, - toast, - usePrompt, -} from "@medusajs/ui" +import { HttpTypes } from "@medusajs/types" +import { Container, StatusBadge, Text, toast, usePrompt } from "@medusajs/ui" import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" import { BadgeListSummary } from "../../../../../components/common/badge-list-summary" +import { LinkButton } from "../../../../../components/common/link-button" import { useDeleteStockLocation } from "../../../../../hooks/api/stock-locations" import { getFormattedAddress } from "../../../../../lib/addresses" +import { FulfillmentSetType } from "../../../common/constants" type SalesChannelsProps = { - salesChannels?: SalesChannelDTO[] + salesChannels?: HttpTypes.AdminSalesChannel[] | null } function SalesChannels(props: SalesChannelsProps) { @@ -29,7 +19,7 @@ function SalesChannels(props: SalesChannelsProps) { const { salesChannels } = props return ( -
+
- {t(`location.fulfillmentSet.salesChannels`)} + {t(`stockLocations.salesChannels.label`)}
{salesChannels?.length ? ( s.name)} @@ -55,13 +46,8 @@ function SalesChannels(props: SalesChannelsProps) { ) } -enum FulfillmentSetType { - Delivery = "delivery", - Pickup = "pickup", -} - type FulfillmentSetProps = { - fulfillmentSet?: FulfillmentSetDTO + fulfillmentSet?: HttpTypes.AdminFulfillmentSet type: FulfillmentSetType } @@ -72,7 +58,7 @@ function FulfillmentSet(props: FulfillmentSetProps) { const fulfillmentSetExists = !!fulfillmentSet return ( -
+
- {t(`location.fulfillmentSet.${type}.title`)} + {t(`stockLocations.fulfillmentSets.${type}.header`)}
- + {t(fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled")}
@@ -93,13 +79,12 @@ function FulfillmentSet(props: FulfillmentSetProps) { } type LocationProps = { - location: StockLocationDTO + location: HttpTypes.AdminStockLocation } -function Location(props: LocationProps) { +function LocationListItem(props: LocationProps) { const { location } = props const { t } = useTranslation() - const navigate = useNavigate() const prompt = usePrompt() const { mutateAsync: deleteLocation } = useDeleteStockLocation(location.id) @@ -107,7 +92,7 @@ function Location(props: LocationProps) { const handleDelete = async () => { const result = await prompt({ title: t("general.areYouSure"), - description: t("location.deleteLocation.confirm", { + description: t("stockLocations.delete.confirmation", { name: location.name, }), confirmText: t("actions.remove"), @@ -118,35 +103,34 @@ function Location(props: LocationProps) { return } - try { - await deleteLocation() - - toast.success(t("general.success"), { - description: t("location.deleteLocation.success", { - name: location.name, - }), - dismissLabel: t("general.close"), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - } + await deleteLocation(undefined, { + onSuccess: () => { + toast.success(t("general.success"), { + description: t("shippingProfile.delete.successToast", { + name: location.name, + }), + dismissLabel: t("general.close"), + }) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + }, + }) } return ( -
+
- {/* ICON*/} -
-
+
+
- {/* LOCATION INFO*/}
{location.name} @@ -154,8 +138,7 @@ function Location(props: LocationProps) {
- {/* ACTION*/} -
+
, to: `/settings/locations/${location.id}/edit`, }, + ], + }, + { + actions: [ { - label: t("location.deleteLocation.label"), + label: t("actions.delete"), icon: , onClick: handleDelete, }, @@ -175,13 +162,9 @@ function Location(props: LocationProps) { ]} />
- +
@@ -190,18 +173,18 @@ function Location(props: LocationProps) { f.type === FulfillmentSetType.Pickup )} /> f.type === FulfillmentSetType.Delivery + type={FulfillmentSetType.Shipping} + fulfillmentSet={location.fulfillment_sets?.find( + (f) => f.type === FulfillmentSetType.Shipping )} /> ) } -export default Location +export default LocationListItem diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/location/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/components/location/index.ts deleted file mode 100644 index 2774358cc3..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/location-list/components/location/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./location" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/const.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/const.ts deleted file mode 100644 index 70caf9e463..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/location-list/const.ts +++ /dev/null @@ -1,3 +0,0 @@ -// TODO: change this when RQ is fixed (address is not joined when *address) -export const locationListFields = - "name,*sales_channels,address.city,address.country_code,*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/constants.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/constants.ts new file mode 100644 index 0000000000..201f2ef580 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/constants.ts @@ -0,0 +1,3 @@ +// TODO: change this when RQ is fixed (address is not joined when *address) +export const LOCATION_LIST_FIELDS = + "name,*sales_channels,*address,*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/loader.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/loader.ts index e355f98822..19e3cb7fc2 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-list/loader.ts +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/loader.ts @@ -1,25 +1,36 @@ -import { LoaderFunctionArgs } from "react-router-dom" +import { FetchError } from "@medusajs/js-sdk" +import { LoaderFunctionArgs, redirect } from "react-router-dom" +import { HttpTypes } from "@medusajs/types" import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations" -import { client } from "../../../lib/client" +import { sdk } from "../../../lib/client" import { queryClient } from "../../../lib/query-client" -import { StockLocationListRes } from "../../../types/api-responses" -import { locationListFields } from "./const" +import { LOCATION_LIST_FIELDS } from "./constants" const shippingListQuery = () => ({ queryKey: stockLocationsQueryKeys.lists(), - queryFn: async () => - client.stockLocations.list({ - // TODO: change this when RQ is fixed - fields: locationListFields, - }), + queryFn: async () => { + return await sdk.admin.stockLocation + .list({ + // TODO: change this when RQ is fixed + fields: LOCATION_LIST_FIELDS, + }) + .catch((error: FetchError) => { + if (error.status === 401) { + throw redirect("/login") + } + + throw error + }) + }, }) export const shippingListLoader = async (_: LoaderFunctionArgs) => { const query = shippingListQuery() return ( - queryClient.getQueryData(query.queryKey) ?? - (await queryClient.fetchQuery(query)) + queryClient.getQueryData( + query.queryKey + ) ?? (await queryClient.fetchQuery(query)) ) } diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx b/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx index 08e5e86e91..ebb7f9a3aa 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx @@ -1,29 +1,34 @@ -import { Button, Container, Heading, Text } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { Link, Outlet, useLoaderData } from "react-router-dom" +import { Outlet, useLoaderData } from "react-router-dom" import { useStockLocations } from "../../../hooks/api/stock-locations" -import Location from "./components/location/location" -import { locationListFields } from "./const" +import LocationListItem from "./components/location-list-item/location-list-item" +import { LOCATION_LIST_FIELDS } from "./constants" import { shippingListLoader } from "./loader" import after from "virtual:medusa/widgets/location/list/after" import before from "virtual:medusa/widgets/location/list/before" +import { LocationListHeader } from "./components/location-list-header" export function LocationList() { - const { t } = useTranslation() - const initialData = useLoaderData() as Awaited< ReturnType > - const { stock_locations: stockLocations = [], isPending } = useStockLocations( + const { + stock_locations: stockLocations = [], + isError, + error, + } = useStockLocations( { - fields: locationListFields, + fields: LOCATION_LIST_FIELDS, }, { initialData } ) + if (isError) { + throw error + } + return (
{before.widgets.map((w, i) => { @@ -33,20 +38,10 @@ export function LocationList() {
) })} - -
- {t("location.title")} - - {t("location.description")} - -
- -
+
{stockLocations.map((location) => ( - + ))}
{after.widgets.map((w, i) => { diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx similarity index 85% rename from packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx index 12fd7c2b55..eb691d9f07 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx @@ -1,7 +1,6 @@ -import { SalesChannel } from "@medusajs/medusa" -import { SalesChannelDTO, StockLocationDTO } from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" +import { Button, Checkbox, toast } from "@medusajs/ui" import { keepPreviousData } from "@tanstack/react-query" -import { Button, Checkbox } from "@medusajs/ui" import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" @@ -14,15 +13,15 @@ import { useRouteModal, } from "../../../../../components/route-modal" import { DataTable } from "../../../../../components/table/data-table" +import { useSalesChannels } from "../../../../../hooks/api/sales-channels" +import { useUpdateStockLocationSalesChannels } from "../../../../../hooks/api/stock-locations" import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns" import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters" import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query" import { useDataTable } from "../../../../../hooks/use-data-table" -import { useSalesChannels } from "../../../../../hooks/api/sales-channels" -import { useUpdateStockLocationSalesChannels } from "../../../../../hooks/api/stock-locations" type EditSalesChannelsFormProps = { - location: StockLocationDTO & { sales_channels: SalesChannelDTO[] } + location: HttpTypes.AdminStockLocation } const EditSalesChannelsSchema = zod.object({ @@ -97,18 +96,30 @@ export const LocationEditSalesChannelsForm = ({ useUpdateStockLocationSalesChannels(location.id) const handleSubmit = form.handleSubmit(async (data) => { - const originalIds = location.sales_channels.map((sc) => sc.id) + const originalIds = location.sales_channels?.map((sc) => sc.id) const arr = data.sales_channels ?? [] await mutateAsync( { - add: arr.filter((i) => !originalIds.includes(i)), - remove: originalIds.filter((i) => !arr.includes(i)), + add: arr.filter((i) => !originalIds?.includes(i)), + remove: originalIds?.filter((i) => !arr.includes(i)), }, { onSuccess: () => { - handleSuccess() + toast.success(t("general.success"), { + description: t("stockLocations.salesChannels.successToast"), + dismissable: true, + dismissLabel: t("actions.close"), + }) + handleSuccess(`/settings/locations/${location.id}`) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("actions.close"), + }) }, } ) @@ -153,7 +164,7 @@ export const LocationEditSalesChannelsForm = ({ ) } -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() const useColumns = () => { const columns = useSalesChannelTableColumns() diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/components/edit-sales-channels-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/components/edit-sales-channels-form/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/index.ts new file mode 100644 index 0000000000..87dae2e029 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/index.ts @@ -0,0 +1 @@ +export { LocationSalesChannels as Component } from "./location-sales-channels" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/location-sales-channels.tsx b/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/location-sales-channels.tsx new file mode 100644 index 0000000000..816ffb7880 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-sales-channels/location-sales-channels.tsx @@ -0,0 +1,27 @@ +import { useParams } from "react-router-dom" + +import { RouteFocusModal } from "../../../components/route-modal" +import { useStockLocation } from "../../../hooks/api/stock-locations" +import { LocationEditSalesChannelsForm } from "./components/edit-sales-channels-form" + +export const LocationSalesChannels = () => { + const { location_id } = useParams() + const { stock_location, isPending, isError, error } = useStockLocation( + location_id!, + { + fields: "id,*sales_channels", + } + ) + + const ready = !isPending && !!stock_location + + if (isError) { + throw error + } + + return ( + + {ready && } + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx new file mode 100644 index 0000000000..2305d9550b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx @@ -0,0 +1,159 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Heading, Input, toast } from "@medusajs/ui" +import { useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { z } from "zod" + +import { Form } from "../../../../../components/common/form" +import { InlineTip } from "../../../../../components/common/inline-tip" +import { SplitView } from "../../../../../components/layout/split-view" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { useCreateFulfillmentSetServiceZone } from "../../../../../hooks/api/fulfillment-sets" +import { GeoZoneForm } from "../../../common/components/geo-zone-form" +import { FulfillmentSetType } from "../../../common/constants" + +const CreateServiceZoneSchema = z.object({ + name: z.string().min(1), + countries: z + .array(z.object({ iso_2: z.string().min(2), display_name: z.string() })) + .min(1), +}) + +type CreateServiceZoneFormProps = { + fulfillmentSet: HttpTypes.AdminFulfillmentSet + type: FulfillmentSetType + location: HttpTypes.AdminStockLocation +} + +export function CreateServiceZoneForm({ + fulfillmentSet, + type, + location, +}: CreateServiceZoneFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const [open, setOpen] = useState(false) + + const form = useForm>({ + defaultValues: { + name: "", + countries: [], + }, + resolver: zodResolver(CreateServiceZoneSchema), + }) + + const { mutateAsync, isPending } = useCreateFulfillmentSetServiceZone( + fulfillmentSet.id + ) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + name: data.name, + geo_zones: data.countries.map(({ iso_2 }) => ({ + country_code: iso_2, + type: "country", + })), + }, + { + onSuccess: () => { + toast.success(t("general.success"), { + description: t("stockLocations.serviceZones.create.successToast", { + name: data.name, + }), + dismissable: true, + dismissLabel: t("general.close"), + }) + + handleSuccess(`/settings/locations/${location.id}`) + }, + onError: (e) => { + console.error(e) + + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("general.close"), + }) + }, + } + ) + }) + + return ( + +
+ +
+ + + + +
+
+ + + + +
+
+ + {type === FulfillmentSetType.Pickup + ? t("stockLocations.serviceZones.create.headerPickup", { + location: location.name, + }) + : t("stockLocations.serviceZones.create.headerShipping", { + location: location.name, + })} + + +
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+ + + {t("stockLocations.serviceZones.fields.tip")} + + + +
+
+
+ +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/components/create-service-zone-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/service-zone-create/components/create-service-zone-form/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/index.ts new file mode 100644 index 0000000000..22d9a0b340 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/index.ts @@ -0,0 +1,2 @@ +export { stockLocationLoader as loader } from "./loader" +export { LocationCreateServiceZone as Component } from "./location-service-zone-create" diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/loader.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/loader.ts similarity index 68% rename from packages/admin-next/dashboard/src/routes/locations/service-zone-create/loader.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/loader.ts index 26f2bdd87d..a506793271 100644 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/loader.ts +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/loader.ts @@ -1,16 +1,16 @@ +import { HttpTypes } from "@medusajs/types" import { LoaderFunctionArgs } from "react-router-dom" import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations" -import { client } from "../../../lib/client" +import { sdk } from "../../../lib/client" import { queryClient } from "../../../lib/query-client" -import { StockLocationRes } from "../../../types/api-responses" const fulfillmentSetCreateQuery = (id: string) => ({ queryKey: stockLocationsQueryKeys.detail(id, { fields: "*fulfillment_sets", }), queryFn: async () => - client.stockLocations.retrieve(id, { + sdk.admin.stockLocation.retrieve(id, { fields: "*fulfillment_sets", }), }) @@ -20,7 +20,8 @@ export const stockLocationLoader = async ({ params }: LoaderFunctionArgs) => { const query = fulfillmentSetCreateQuery(id!) return ( - queryClient.getQueryData(query.queryKey) ?? - (await queryClient.fetchQuery(query)) + queryClient.getQueryData( + query.queryKey + ) ?? (await queryClient.fetchQuery(query)) ) } diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/location-service-zone-create.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/location-service-zone-create.tsx new file mode 100644 index 0000000000..671fa693b5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-create/location-service-zone-create.tsx @@ -0,0 +1,58 @@ +import { json, useLoaderData, useParams } from "react-router-dom" + +import { RouteFocusModal } from "../../../components/route-modal" +import { useStockLocation } from "../../../hooks/api/stock-locations" +import { FulfillmentSetType } from "../common/constants" +import { CreateServiceZoneForm } from "./components/create-service-zone-form" +import { stockLocationLoader } from "./loader" + +export function LocationCreateServiceZone() { + const { fset_id, location_id } = useParams() + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { stock_location, isLoading, isError, error } = useStockLocation( + location_id!, + { + fields: "*fulfillment_sets", + }, + { + initialData, + } + ) + + const fulfillmentSet = stock_location?.fulfillment_sets?.find( + (f) => f.id === fset_id + ) + + const ready = !isLoading && !!stock_location && !!fulfillmentSet + + const type: FulfillmentSetType = + fulfillmentSet?.type === FulfillmentSetType.Pickup + ? FulfillmentSetType.Pickup + : FulfillmentSetType.Shipping + + if (!isLoading && !fulfillmentSet) { + throw json( + { message: `Fulfillment set with ID: ${fset_id} was not found.` }, + 404 + ) + } + + if (isError) { + throw error + } + + return ( + + {ready && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/components/edit-region-form/edit-service-zone-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/components/edit-region-form/edit-service-zone-form.tsx similarity index 76% rename from packages/admin-next/dashboard/src/routes/locations/service-zone-edit/components/edit-region-form/edit-service-zone-form.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/components/edit-region-form/edit-service-zone-form.tsx index 3ed4f37649..8296835969 100644 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/components/edit-region-form/edit-service-zone-form.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/components/edit-region-form/edit-service-zone-form.tsx @@ -1,18 +1,19 @@ -import { ServiceZoneDTO } from "@medusajs/types" -import { Alert, Button, Input, Text, toast } from "@medusajs/ui" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, toast } from "@medusajs/ui" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import * as zod from "zod" import { Form } from "../../../../../components/common/form" +import { InlineTip } from "../../../../../components/common/inline-tip" import { - RouteDrawer, - useRouteModal, + RouteDrawer, + useRouteModal, } from "../../../../../components/route-modal" -import { useUpdateServiceZone } from "../../../../../hooks/api/stock-locations" +import { useUpdateFulfillmentSetServiceZone } from "../../../../../hooks/api/fulfillment-sets" type EditServiceZoneFormProps = { - zone: ServiceZoneDTO + zone: HttpTypes.AdminServiceZone fulfillmentSetId: string locationId: string } @@ -35,11 +36,8 @@ export const EditServiceZoneForm = ({ }, }) - const { mutateAsync, isPending: isLoading } = useUpdateServiceZone( - fulfillmentSetId, - zone.id, - locationId - ) + const { mutateAsync, isPending: isLoading } = + useUpdateFulfillmentSetServiceZone(fulfillmentSetId, zone.id) const handleSubmit = form.handleSubmit(async (values) => { await mutateAsync( @@ -49,10 +47,12 @@ export const EditServiceZoneForm = ({ { onSuccess: () => { toast.success(t("general.success"), { - // description: t("regions.toast.edit"), + description: t("stockLocations.serviceZones.edit.successToast", { + name: values.name, + }), dismissLabel: t("actions.close"), }) - handleSuccess() + handleSuccess(`/settings/locations/${locationId}`) }, onError: (e) => { toast.error(t("general.error"), { @@ -86,14 +86,7 @@ export const EditServiceZoneForm = ({ }} />
- - - {t("location.serviceZone.create.subtitle")} - - - {t("location.serviceZone.create.description")} - - + {t("stockLocations.serviceZones.fields.tip")}
diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/components/edit-region-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/components/edit-region-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/service-zone-edit/components/edit-region-form/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/components/edit-region-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/index.ts new file mode 100644 index 0000000000..5a1d72cd81 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/index.ts @@ -0,0 +1 @@ +export { LocationServiceZoneEdit as Component } from "./location-service-zone-edit" diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/service-zone-edit.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/location-service-zone-edit.tsx similarity index 57% rename from packages/admin-next/dashboard/src/routes/locations/service-zone-edit/service-zone-edit.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/location-service-zone-edit.tsx index ee6727f788..21bfce7524 100644 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/service-zone-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-edit/location-service-zone-edit.tsx @@ -6,27 +6,26 @@ import { RouteDrawer } from "../../../components/route-modal" import { useStockLocation } from "../../../hooks/api/stock-locations" import { EditServiceZoneForm } from "./components/edit-region-form" -export const ServiceZoneEdit = () => { +export const LocationServiceZoneEdit = () => { const { t } = useTranslation() const { location_id, fset_id, zone_id } = useParams() const { stock_location, isPending, isError, error } = useStockLocation( location_id!, { - fields: - "name,address.city,address.country_code,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.shipping_profile", + fields: "*fulfillment_sets.service_zones", } ) - const zone = stock_location?.fulfillment_sets - .find((f) => f.id === fset_id) + const serviceZone = 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) { + if (!isPending && !serviceZone) { throw json( { message: `Service zone with ID ${zone_id} was not found` }, 404 @@ -34,15 +33,15 @@ export const ServiceZoneEdit = () => { } return ( - + - {t("location.serviceZone.edit.title")} + {t("stockLocations.serviceZones.edit.header")} - {!isPending && zone && ( + {!isPending && serviceZone && ( )} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/components/edit-region-areas-form/edit-service-zone-areas-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/components/edit-region-areas-form/edit-service-zone-areas-form.tsx new file mode 100644 index 0000000000..79d486f7e5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/components/edit-region-areas-form/edit-service-zone-areas-form.tsx @@ -0,0 +1,133 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Heading, toast } from "@medusajs/ui" +import { useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { z } from "zod" + +import { SplitView } from "../../../../../components/layout/split-view" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { useUpdateFulfillmentSetServiceZone } from "../../../../../hooks/api/fulfillment-sets" +import { countries } from "../../../../../lib/countries" +import { GeoZoneForm } from "../../../common/components/geo-zone-form" + +const EditeServiceZoneSchema = z.object({ + countries: z + .array(z.object({ iso_2: z.string().min(2), display_name: z.string() })) + .min(1), +}) + +type EditServiceZoneAreasFormProps = { + fulfillmentSetId: string + locationId: string + zone: HttpTypes.AdminServiceZone +} + +export function EditServiceZoneAreasForm({ + fulfillmentSetId, + locationId, + zone, +}: EditServiceZoneAreasFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + const [open, setOpen] = useState(false) + + const form = useForm>({ + defaultValues: { + countries: zone.geo_zones.map((z) => { + const country = countries.find((c) => c.iso_2 === z.country_code) + + return { + iso_2: z.country_code, + display_name: country?.display_name || z.country_code.toUpperCase(), + } + }), + }, + resolver: zodResolver(EditeServiceZoneSchema), + }) + + const { mutateAsync: editServiceZone, isPending: isLoading } = + useUpdateFulfillmentSetServiceZone(fulfillmentSetId, zone.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await editServiceZone( + { + geo_zones: data.countries.map(({ iso_2 }) => ({ + country_code: iso_2, + type: "country", + })), + }, + { + onSuccess: () => { + toast.success(t("general.success"), { + description: t( + "stockLocations.serviceZones.manageAreas.successToast", + { + name: zone.name, + } + ), + dismissable: true, + dismissLabel: t("general.close"), + }) + + handleSuccess(`/settings/locations/${locationId}`) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("general.close"), + }) + }, + } + ) + }) + + return ( + +
+ +
+ + + + +
+
+ + + + +
+
+ + {t("stockLocations.serviceZones.manageAreas.header", { + name: zone.name, + })} + + +
+
+
+ +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/components/edit-region-areas-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/components/edit-region-areas-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/components/edit-region-areas-form/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/components/edit-region-areas-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/index.ts new file mode 100644 index 0000000000..df25944d36 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/index.ts @@ -0,0 +1 @@ +export { LocationServiceZoneManageAreas as Component } from "./location-service-zone-manage-areas" diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/service-zone-areas-edit.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/location-service-zone-manage-areas.tsx similarity index 58% rename from packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/service-zone-areas-edit.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/location-service-zone-manage-areas.tsx index 0bccfa6b4a..13b6b385aa 100644 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/service-zone-areas-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-manage-areas/location-service-zone-manage-areas.tsx @@ -1,23 +1,21 @@ 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" +import { EditServiceZoneAreasForm } from "./components/edit-region-areas-form" -export const ServiceZoneAreasEdit = () => { +export const LocationServiceZoneManageAreas = () => { 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", + fields: "*fulfillment_sets.service_zones.geo_zones", } ) const zone = stock_location?.fulfillment_sets - .find((f) => f.id === fset_id) + ?.find((f) => f.id === fset_id) ?.service_zones.find((z) => z.id === zone_id) if (isError) { @@ -32,12 +30,12 @@ export const ServiceZoneAreasEdit = () => { } return ( - + {!isPending && zone && ( )} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx new file mode 100644 index 0000000000..f94e6b36e9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx @@ -0,0 +1,198 @@ +import { Heading, Input, RadioGroup, Text } from "@medusajs/ui" +import { UseFormReturn } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { HttpTypes } from "@medusajs/types" +import { Divider } from "../../../../../components/common/divider" +import { Form } from "../../../../../components/common/form" +import { SwitchBox } from "../../../../../components/common/switch-box" +import { Combobox } from "../../../../../components/inputs/combobox" +import { useComboboxData } from "../../../../../hooks/use-combobox-data" +import { sdk } from "../../../../../lib/client" +import { formatProvider } from "../../../../../lib/format-provider" +import { ShippingOptionPriceType } from "../../../common/constants" +import { CreateShippingOptionSchema } from "./schema" + +type CreateShippingOptionDetailsFormProps = { + form: UseFormReturn + isReturn?: boolean + zone: HttpTypes.AdminServiceZone +} + +export const CreateShippingOptionDetailsForm = ({ + form, + isReturn = false, + zone, +}: CreateShippingOptionDetailsFormProps) => { + const { t } = useTranslation() + + const shippingProfiles = useComboboxData({ + queryFn: (params) => sdk.admin.shippingProfile.list(params), + queryKey: ["shipping_profiles"], + getOptions: (data) => + data.shipping_profiles.map((profile) => ({ + label: profile.name, + value: profile.id, + })), + }) + + const fulfillmentProviders = useComboboxData({ + queryFn: (params) => sdk.admin.fulfillmentProvider.list(params), + queryKey: ["fulfillment_providers"], + getOptions: (data) => + data.fulfillment_providers.map((provider) => ({ + label: formatProvider(provider.id), + value: provider.id, + })), + }) + + return ( +
+
+
+ + {t( + `stockLocations.shippingOptions.create.${ + isReturn ? "returns" : "shipping" + }.header`, + { + zone: zone.name, + } + )} + + + {t( + `stockLocations.shippingOptions.create.${ + isReturn ? "returns" : "shipping" + }.hint` + )} + +
+ + { + return ( + + + {t("stockLocations.shippingOptions.fields.priceType.label")} + + + + + + + + + + ) + }} + /> + +
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+ +
+ { + return ( + + + {t("stockLocations.shippingOptions.fields.profile")} + + + + + + + ) + }} + /> + + { + return ( + + + {t("stockLocations.shippingOptions.fields.provider")} + + + + + + + ) + }} + /> +
+ + + + +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx new file mode 100644 index 0000000000..698958a235 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx @@ -0,0 +1,276 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, ProgressStatus, ProgressTabs, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { useState } from "react" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options" +import { castNumber } from "../../../../../lib/cast-number" +import { ShippingOptionPriceType } from "../../../common/constants" +import { CreateShippingOptionDetailsForm } from "./create-shipping-option-details-form" +import { CreateShippingOptionsPricesForm } from "./create-shipping-options-prices-form" +import { + CreateShippingOptionDetailsSchema, + CreateShippingOptionSchema, +} from "./schema" + +enum Tab { + DETAILS = "details", + PRICING = "pricing", +} + +type CreateShippingOptionFormProps = { + zone: HttpTypes.AdminServiceZone + locationId: string + isReturn?: boolean +} + +export function CreateShippingOptionsForm({ + zone, + isReturn, + locationId, +}: CreateShippingOptionFormProps) { + const [activeTab, setActiveTab] = useState(Tab.DETAILS) + const [validDetails, setValidDetails] = useState(false) + + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm({ + defaultValues: { + name: "", + price_type: ShippingOptionPriceType.FlatRate, + enabled_in_store: true, + shipping_profile_id: "", + provider_id: "", + region_prices: {}, + currency_prices: {}, + }, + resolver: zodResolver(CreateShippingOptionSchema), + }) + + const isCalculatedPriceType = + form.watch("price_type") === ShippingOptionPriceType.Calculated + + const { mutateAsync, isPending: isLoading } = useCreateShippingOptions() + + const handleSubmit = form.handleSubmit(async (data) => { + const currencyPrices = Object.entries(data.currency_prices) + .map(([code, value]) => { + const amount = value ? castNumber(value) : undefined + + return { + currency_code: code, + amount: amount, + } + }) + .filter((o) => !!o.amount) as { currency_code: string; amount: number }[] + + const regionPrices = Object.entries(data.region_prices) + .map(([region_id, value]) => { + const amount = value ? castNumber(value) : undefined + + return { + region_id, + amount: amount, + } + }) + .filter((o) => !!o.amount) as { region_id: string; amount: number }[] + + await mutateAsync( + { + name: data.name, + price_type: data.price_type, + service_zone_id: zone.id, + shipping_profile_id: data.shipping_profile_id, + provider_id: data.provider_id, + prices: [...currencyPrices, ...regionPrices], + rules: [ + { + // eslint-disable-next-line + value: isReturn ? '"true"' : '"false"', // we want JSONB saved as string + attribute: "is_return", + operator: "eq", + }, + { + // eslint-disable-next-line + value: data.enabled_in_store ? '"true"' : '"false"', // we want JSONB saved as string + attribute: "enabled_in_store", + operator: "eq", + }, + ], + type: { + // TODO: FETCH TYPES + label: "Type label", + description: "Type description", + code: "type-code", + }, + }, + { + onSuccess: ({ shipping_option }) => { + toast.success(t("general.success"), { + description: t( + `stockLocations.shippingOptions.create.${ + isReturn ? "returns" : "shipping" + }.successToast`, + { + name: shipping_option.name, + } + ), + dismissable: true, + dismissLabel: t("general.close"), + }) + + handleSuccess(`/settings/locations/${locationId}`) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + } + ) + }) + + const onTabChange = (tab: Tab) => { + if (tab === Tab.PRICING) { + form.clearErrors() + + const result = CreateShippingOptionDetailsSchema.safeParse({ + ...form.getValues(), + }) + + if (!result.success) { + const [firstError, ...rest] = result.error.errors + + for (const error of rest) { + const _path = error.path.join(".") as keyof CreateShippingOptionSchema + + form.setError(_path, { + message: error.message, + type: error.code, + }) + } + + // Focus the first error + form.setError( + firstError.path.join(".") as keyof CreateShippingOptionSchema, + { + message: firstError.message, + type: firstError.code, + }, + { + shouldFocus: true, + } + ) + + setValidDetails(false) + return + } + + setValidDetails(true) + } + + setActiveTab(tab) + } + + const pricesStatus: ProgressStatus = + form.getFieldState("currency_prices")?.isDirty || + form.getFieldState("region_prices")?.isDirty || + activeTab === Tab.PRICING + ? "in-progress" + : "not-started" + + const detailsStatus: ProgressStatus = validDetails + ? "completed" + : "in-progress" + + return ( + + onTabChange(tab as Tab)} + > +
+ + + + + {t("stockLocations.shippingOptions.create.tabs.details")} + + + {!isCalculatedPriceType && ( + + + {t("stockLocations.shippingOptions.create.tabs.prices")} + + + )} + +
+ + + + {activeTab === Tab.PRICING || isCalculatedPriceType ? ( + + ) : ( + + )} +
+
+ + + + + + + + + +
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx new file mode 100644 index 0000000000..cae6f2aeba --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx @@ -0,0 +1,64 @@ +import { useMemo } from "react" +import { UseFormReturn } from "react-hook-form" + +import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root" +import { useRegions } from "../../../../../hooks/api/regions" +import { useStore } from "../../../../../hooks/api/store" +import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns" +import { CreateShippingOptionSchema } from "./schema" + +type PricingPricesFormProps = { + form: UseFormReturn +} + +export const CreateShippingOptionsPricesForm = ({ + form, +}: PricingPricesFormProps) => { + const { + store, + isLoading: isStoreLoading, + isError: isStoreError, + error: storeError, + } = useStore() + + const currencies = useMemo( + () => store?.supported_currency_codes || [], + [store] + ) + + const { + regions, + isLoading: isRegionsLoading, + isError: isRegionsError, + error: regionsError, + } = useRegions({ + fields: "id,name,currency_code", + limit: 999, + }) + + const columns = useShippingOptionPriceColumns({ + currencies, + regions, + }) + + const initializing = isStoreLoading || !store || isRegionsLoading || !regions + + const data = useMemo( + () => [[...(currencies || []), ...(regions || [])]], + [currencies, regions] + ) + + if (isStoreError) { + throw storeError + } + + if (isRegionsError) { + throw regionsError + } + + return ( +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts new file mode 100644 index 0000000000..a9553e504e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod" +import { ShippingOptionPriceType } from "../../../common/constants" + +export type CreateShippingOptionSchema = z.infer< + typeof CreateShippingOptionSchema +> + +export const CreateShippingOptionDetailsSchema = z.object({ + name: z.string().min(1), + price_type: z.nativeEnum(ShippingOptionPriceType), + enabled_in_store: z.boolean(), + shipping_profile_id: z.string().min(1), + provider_id: z.string().min(1), +}) + +export const CreateShippingOptionSchema = z + .object({ + region_prices: z.record(z.string(), z.string().optional()), + currency_prices: z.record(z.string(), z.string().optional()), + }) + .merge(CreateShippingOptionDetailsSchema) diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/constants.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/constants.ts new file mode 100644 index 0000000000..a616c286c9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/constants.ts @@ -0,0 +1,2 @@ +export const LOC_CREATE_SHIPPING_OPTION_FIELDS = + "*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/index.ts new file mode 100644 index 0000000000..70721d78d0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/index.ts @@ -0,0 +1,2 @@ +export { stockLocationLoader as loader } from "./loader" +export { LocationServiceZoneShippingOptionCreate as Component } from "./location-service-zone-shipping-option-create" diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/loader.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/loader.ts similarity index 50% rename from packages/admin-next/dashboard/src/routes/locations/shipping-options-create/loader.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/loader.ts index 6c3347c87e..eb128e9c1b 100644 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/loader.ts +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/loader.ts @@ -1,19 +1,18 @@ import { LoaderFunctionArgs } from "react-router-dom" +import { HttpTypes } from "@medusajs/types" import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations" -import { client } from "../../../lib/client" +import { sdk } from "../../../lib/client" import { queryClient } from "../../../lib/query-client" -import { StockLocationRes } from "../../../types/api-responses" +import { LOC_CREATE_SHIPPING_OPTION_FIELDS } from "./constants" const fulfillmentSetCreateQuery = (id: string) => ({ queryKey: stockLocationsQueryKeys.detail(id, { - fields: - "*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options", + fields: LOC_CREATE_SHIPPING_OPTION_FIELDS, }), queryFn: async () => - client.stockLocations.retrieve(id, { - fields: - "*fulfillment_sets,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options", + sdk.admin.stockLocation.retrieve(id, { + fields: LOC_CREATE_SHIPPING_OPTION_FIELDS, }), }) @@ -22,7 +21,8 @@ export const stockLocationLoader = async ({ params }: LoaderFunctionArgs) => { const query = fulfillmentSetCreateQuery(id!) return ( - queryClient.getQueryData(query.queryKey) ?? - (await queryClient.fetchQuery(query)) + queryClient.getQueryData( + query.queryKey + ) ?? (await queryClient.fetchQuery(query)) ) } diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx new file mode 100644 index 0000000000..4164dfbbc0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx @@ -0,0 +1,59 @@ +import { + json, + useLoaderData, + useParams, + useSearchParams, +} from "react-router-dom" + +import { RouteFocusModal } from "../../../components/route-modal" +import { useStockLocation } from "../../../hooks/api/stock-locations" +import { CreateShippingOptionsForm } from "./components/create-shipping-options-form" +import { LOC_CREATE_SHIPPING_OPTION_FIELDS } from "./constants" +import { stockLocationLoader } from "./loader" + +export function LocationServiceZoneShippingOptionCreate() { + const { location_id, fset_id, zone_id } = useParams() + const [searchParams] = useSearchParams() + const isReturn = searchParams.has("is_return") + + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { stock_location, isPending, isError, error } = useStockLocation( + location_id!, + { + fields: LOC_CREATE_SHIPPING_OPTION_FIELDS, + }, + { initialData } + ) + + const zone = stock_location?.fulfillment_sets + ?.find((f) => f.id === fset_id) + ?.service_zones?.find((z) => z.id === zone_id) + + if (!zone) { + throw json( + { message: `Service zone with ID ${zone_id} was not found` }, + 404 + ) + } + + if (isError) { + throw error + } + + const ready = !isPending && !!zone + + return ( + + {ready && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx new file mode 100644 index 0000000000..006b32038a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx @@ -0,0 +1,273 @@ +import { HttpTypes } from "@medusajs/types" +import { Button, Input, RadioGroup, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +import { Divider } from "../../../../../components/common/divider" +import { Form } from "../../../../../components/common/form" +import { SwitchBox } from "../../../../../components/common/switch-box" +import { Combobox } from "../../../../../components/inputs/combobox" +import { + RouteDrawer, + useRouteModal, +} from "../../../../../components/route-modal" +import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options" +import { useComboboxData } from "../../../../../hooks/use-combobox-data" +import { sdk } from "../../../../../lib/client" +import { pick } from "../../../../../lib/common" +import { formatProvider } from "../../../../../lib/format-provider" +import { isOptionEnabledInStore } from "../../../../../lib/shipping-options" +import { ShippingOptionPriceType } from "../../../common/constants" + +type EditShippingOptionFormProps = { + locationId: string + shippingOption: HttpTypes.AdminShippingOption +} + +const EditShippingOptionSchema = zod.object({ + name: zod.string().min(1), + price_type: zod.nativeEnum(ShippingOptionPriceType), + enabled_in_store: zod.boolean().optional(), + shipping_profile_id: zod.string(), + provider_id: zod.string(), +}) + +export const EditShippingOptionForm = ({ + locationId, + shippingOption, +}: EditShippingOptionFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const shippingProfiles = useComboboxData({ + queryFn: (params) => sdk.admin.shippingProfile.list(params), + queryKey: ["shipping_profiles"], + getOptions: (data) => + data.shipping_profiles.map((profile) => ({ + label: profile.name, + value: profile.id, + })), + defaultValue: shippingOption.shipping_profile_id, + }) + + const fulfillmentProviders = useComboboxData({ + queryFn: (params) => sdk.admin.fulfillmentProvider.list(params), + queryKey: ["fulfillment_providers"], + getOptions: (data) => + data.fulfillment_providers.map((provider) => ({ + label: formatProvider(provider.id), + value: provider.id, + })), + defaultValue: shippingOption.provider_id, + }) + + const form = useForm>({ + defaultValues: { + name: shippingOption.name, + price_type: shippingOption.price_type as ShippingOptionPriceType, + enabled_in_store: isOptionEnabledInStore(shippingOption), + shipping_profile_id: shippingOption.shipping_profile_id, + provider_id: shippingOption.provider_id, + }, + }) + + const { mutateAsync, isPending: isLoading } = useUpdateShippingOptions( + shippingOption.id + ) + + const handleSubmit = form.handleSubmit(async (values) => { + const rules = shippingOption.rules.map((r) => ({ + ...pick(r, ["id", "attribute", "operator", "value"]), + })) as HttpTypes.AdminUpdateShippingOptionRule[] + + const storeRule = rules.find((r) => r.attribute === "enabled_in_store") + + if (!storeRule) { + // NOTE: should always exist since we always create this rule when we create a shipping option + rules.push({ + value: values.enabled_in_store ? "true" : "false", + attribute: "enabled_in_store", + operator: "eq", + }) + } else { + storeRule.value = values.enabled_in_store ? "true" : "false" + } + + await mutateAsync( + { + name: values.name, + price_type: values.price_type, + shipping_profile_id: values.shipping_profile_id, + provider_id: values.provider_id, + rules, + }, + { + onSuccess: ({ shipping_option }) => { + toast.success(t("general.success"), { + description: t("stockLocations.shippingOptions.edit.successToast", { + name: shipping_option.name, + }), + dismissable: true, + dismissLabel: t("actions.close"), + }) + handleSuccess(`/settings/locations/${locationId}`) + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissable: true, + dismissLabel: t("actions.close"), + }) + }, + } + ) + }) + + return ( + +
+ +
+
+ { + return ( + + + {t( + "stockLocations.shippingOptions.fields.priceType.label" + )} + + + + + + + + + + ) + }} + /> + +
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + + { + return ( + + + {t("stockLocations.shippingOptions.fields.profile")} + + + + + + + ) + }} + /> + { + return ( + + + {t("stockLocations.shippingOptions.fields.provider")} + + + + + + + ) + }} + /> +
+ + + + +
+
+
+ +
+ + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/components/edit-region-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/components/edit-region-form/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/index.ts new file mode 100644 index 0000000000..5284b367ce --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/index.ts @@ -0,0 +1 @@ +export { LocationServiceZoneShippingOptionEdit as Component } from "./location-service-zone-shipping-option-edit" diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/shipping-option-edit.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx similarity index 70% rename from packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/shipping-option-edit.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx index 7cdb510798..df0187371d 100644 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/shipping-option-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx @@ -1,19 +1,19 @@ import { Heading } from "@medusajs/ui" import { useTranslation } from "react-i18next" -import { json, useParams, useSearchParams } from "react-router-dom" +import { json, useParams } from "react-router-dom" import { RouteDrawer } from "../../../components/route-modal" import { useShippingOptions } from "../../../hooks/api/shipping-options" import { EditShippingOptionForm } from "./components/edit-region-form" -export const ShippingOptionEdit = () => { +export const LocationServiceZoneShippingOptionEdit = () => { const { t } = useTranslation() - const [searchParams] = useSearchParams() - const { location_id, fset_id, zone_id, so_id } = useParams() - const isReturn = searchParams.has("is_return") + const { location_id, so_id } = useParams() - const { shipping_options, isPending, isError, error } = useShippingOptions() + const { shipping_options, isPending, isError, error } = useShippingOptions({ + id: so_id, + }) const shippingOption = shipping_options?.find((so) => so.id === so_id) @@ -31,12 +31,12 @@ export const ShippingOptionEdit = () => { return ( - {t("location.shippingOptions.edit.title")} + {t("stockLocations.shippingOptions.edit.header")} {!isPending && shippingOption && ( )} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx new file mode 100644 index 0000000000..9e583fc2e8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx @@ -0,0 +1,244 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useMemo } from "react" +import { useForm } from "react-hook-form" +import * as zod from "zod" + +import { HttpTypes } from "@medusajs/types" +import { Button, toast } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal/index" +import { useRegions } from "../../../../../hooks/api/regions" +import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options" +import { useStore } from "../../../../../hooks/api/store" +import { castNumber } from "../../../../../lib/cast-number" +import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns" + +const getInitialCurrencyPrices = ( + prices: HttpTypes.AdminShippingOptionPrice[] +) => { + const ret: Record = {} + prices.forEach((p) => { + if (p.price_rules!.length) { + // this is a region price + return + } + ret[p.currency_code!] = p.amount + }) + return ret +} + +const getInitialRegionPrices = ( + prices: HttpTypes.AdminShippingOptionPrice[] +) => { + const ret: Record = {} + prices.forEach((p) => { + if (p.price_rules!.length) { + const regionId = p.price_rules![0].value + ret[regionId] = p.amount + } + }) + + return ret +} + +type PriceRecord = { + id?: string + currency_code?: string + region_id?: string + amount: number +} + +const EditShippingOptionPricingSchema = zod.object({ + region_prices: zod.record( + zod.string(), + zod.string().or(zod.number()).optional() + ), + currency_prices: zod.record( + zod.string(), + zod.string().or(zod.number()).optional() + ), +}) + +type EditShippingOptionPricingFormProps = { + shippingOption: HttpTypes.AdminShippingOption +} + +export function EditShippingOptionsPricingForm({ + shippingOption, +}: EditShippingOptionPricingFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + region_prices: getInitialRegionPrices(shippingOption.prices), + currency_prices: getInitialCurrencyPrices(shippingOption.prices), + }, + resolver: zodResolver(EditShippingOptionPricingSchema), + }) + + const { mutateAsync, isPending: isLoading } = useUpdateShippingOptions( + shippingOption.id + ) + + const { + store, + isLoading: isStoreLoading, + isError: isStoreError, + error: storeError, + } = useStore() + + const currencies = useMemo( + () => store?.supported_currency_codes || [], + [store] + ) + + const { + regions, + isLoading: isRegionsLoading, + isError: isRegionsError, + error: regionsError, + } = useRegions({ + fields: "id,name,currency_code", + limit: 999, + }) + + const columns = useShippingOptionPriceColumns({ + currencies, + regions, + }) + + const data = useMemo( + () => [[...(currencies || []), ...(regions || [])]], + [currencies, regions] + ) + + const handleSubmit = form.handleSubmit(async (data) => { + const currencyPrices = Object.entries(data.currency_prices) + .map(([code, value]) => { + if (value === "" || value === undefined) { + return undefined + } + + const amount = castNumber(value) + + const priceRecord: PriceRecord = { + currency_code: code, + amount: amount, + } + + const price = shippingOption.prices.find( + (p) => p.currency_code === code && !p.price_rules!.length + ) + + // if that currency price is already defined for the SO, we will do an update + if (price) { + priceRecord["id"] = price.id + } + + return priceRecord + }) + .filter((p) => !!p) as PriceRecord[] + + const regionPrices = Object.entries(data.region_prices) + .map(([region_id, value]) => { + if (value === "" || value === undefined) { + return undefined + } + + const amount = castNumber(value) + + const priceRecord: PriceRecord = { + region_id, + amount: amount, + } + + /** + * HACK - when trying to update prices which already have a region price + * we get error: `Price rule with price_id: , rule_type_id: already exist`, + * so for now, we recreate region prices. + */ + + // const price = shippingOption.prices.find( + // (p) => p.price_rules?.[0]?.value === region_id + // ) + + // if (price) { + // priceRecord["id"] = price.id + // } + + return priceRecord + }) + .filter((p) => !!p) as PriceRecord[] + + await mutateAsync( + { + prices: [...currencyPrices, ...regionPrices], + }, + { + onSuccess: () => { + toast.success(t("general.success"), { + dismissLabel: t("general.close"), + }) + handleSuccess() + }, + onError: (e) => { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("general.close"), + }) + }, + } + ) + }) + + const initializing = + isStoreLoading || isRegionsLoading || !currencies || !regions + + if (isStoreError) { + throw storeError + } + + if (isRegionsError) { + throw regionsError + } + + return ( + +
+ +
+ + + + +
+
+ + +
+ +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/components/create-shipping-options-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/components/create-shipping-options-form/index.ts rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/index.ts new file mode 100644 index 0000000000..e291243f3d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/index.ts @@ -0,0 +1 @@ +export { LocationServiceZoneShippingOptionPricing as Component } from "./location-service-zone-shipping-option-pricing" diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/shipping-options-edit-pricing.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx similarity index 68% rename from packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/shipping-options-edit-pricing.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx index 9c4c70fa84..0ce1247d7a 100644 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/shipping-options-edit-pricing.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx @@ -1,27 +1,26 @@ -import { useParams } from "react-router-dom" +import { json, useParams } from "react-router-dom" import { RouteFocusModal } from "../../../components/route-modal" import { useShippingOptions } from "../../../hooks/api/shipping-options" import { EditShippingOptionsPricingForm } from "./components/create-shipping-options-form" -export function ShippingOptionsEditPricing() { - const { so_id } = useParams() +export function LocationServiceZoneShippingOptionPricing() { + const { so_id, location_id } = useParams() const { shipping_options, isPending } = useShippingOptions({ // TODO: change this when GET option by id endpoint is implemented - id: [so_id], + id: [so_id!], fields: "*prices,*prices.price_rules", - limit: 999, }) const shippingOption = shipping_options?.find((so) => so.id === so_id) if (!isPending && !shippingOption) { - throw new Error(`Shipping option with id: ${so_id} not found`) + throw json(`Shipping option with id: ${so_id} not found`, { status: 404 }) } return ( - + {shippingOption && ( )} diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/components/edit-region-areas-form/edit-service-zone-areas-form.tsx b/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/components/edit-region-areas-form/edit-service-zone-areas-form.tsx deleted file mode 100644 index 6b5d4a4a89..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/components/edit-region-areas-form/edit-service-zone-areas-form.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { - ColumnDef, - createColumnHelper, - RowSelectionState, -} from "@tanstack/react-table" -import { useForm } from "react-hook-form" -import * as zod from "zod" - -import { XMarkMini } from "@medusajs/icons" -import { HttpTypes, ServiceZoneDTO } from "@medusajs/types" -import { - Alert, - Badge, - Button, - Checkbox, - Heading, - IconButton, - Text, - toast, -} from "@medusajs/ui" -import { useTranslation } from "react-i18next" - -import { useEffect, useMemo, useState } from "react" -import { SplitView } from "../../../../../components/layout/split-view" -import { - RouteFocusModal, - useRouteModal, -} from "../../../../../components/route-modal" -import { DataTable } from "../../../../../components/table/data-table" -import { useUpdateServiceZone } from "../../../../../hooks/api/stock-locations" -import { useDataTable } from "../../../../../hooks/use-data-table" -import { countries as staticCountries } from "../../../../../lib/countries" -import { useCountries } from "../../../../regions/common/hooks/use-countries" -import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns" -import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query" - -const PREFIX = "ac" -const PAGE_SIZE = 50 - -const ConditionsFooter = ({ onSave }: { onSave: () => void }) => { - const { t } = useTranslation() - - return ( -
- - - - -
- ) -} - -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.map((iso2) => ({ - country_code: iso2, - type: "country", - })), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("general.close"), - }) - } - - handleSuccess() - }) - - const handleOpenChange = (open: boolean) => { - setOpen(open) - } - - const { searchParams, raw } = useCountryTableQuery({ - pageSize: PAGE_SIZE, - prefix: PREFIX, - }) - const { countries, count } = useCountries({ - countries: staticCountries.map((c, i) => ({ - display_name: c.display_name, - name: c.name, - id: i as any, - iso_2: c.iso_2, - iso_3: c.iso_3, - num_code: c.num_code, - region_id: null, - region: {} as HttpTypes.AdminRegion, - })), - ...searchParams, - }) - - const columns = useColumns() - - const { table } = useDataTable({ - data: countries || [], - columns, - count, - enablePagination: true, - enableRowSelection: true, - getRowId: (row) => row.iso_2, - pageSize: PAGE_SIZE, - rowSelection: { - state: rowSelection, - updater: setRowSelection, - }, - prefix: PREFIX, - }) - - const countriesWatch = form.watch("countries") - - const onCountriesSave = () => { - form.setValue("countries", Object.keys(rowSelection)) - setOpen(false) - } - - const removeCountry = (iso2: string) => { - const state = { ...rowSelection } - delete state[iso2] - setRowSelection(state) - - form.setValue( - "countries", - countriesWatch.filter((c) => c !== iso2) - ) - } - - const clearAll = () => { - setRowSelection({}) - form.setValue("countries", []) - } - - const selectedCountries = useMemo(() => { - return staticCountries.filter((c) => c.iso_2 in rowSelection) - }, [countriesWatch]) - - useEffect(() => { - // set selected rows from form state on open - if (open) { - setRowSelection( - countriesWatch.reduce((acc, c) => { - acc[c] = true - return acc - }, {}) - ) - } - }, [open]) - - const showAreasError = - form.formState.errors["countries"]?.type === "too_small" - - return ( - -
- -
- - - - -
-
- - - - -
- - {t("location.serviceZone.editAreasTitle", { - zone: zone.name, - })} - -
- -
-
- - {t("location.serviceZone.areas.title")} - - - {t("location.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("location.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/routes/locations/service-zone-areas-edit/index.ts b/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/index.ts deleted file mode 100644 index fc088b9f88..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-areas-edit/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ServiceZoneAreasEdit as Component } from "./service-zone-areas-edit" diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx b/packages/admin-next/dashboard/src/routes/locations/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx deleted file mode 100644 index bd2bea678a..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { - ColumnDef, - createColumnHelper, - RowSelectionState, -} from "@tanstack/react-table" -import { useForm } from "react-hook-form" -import * as zod from "zod" - -import { XMarkMini } from "@medusajs/icons" -import { FulfillmentSetDTO, HttpTypes } from "@medusajs/types" -import { - Alert, - Badge, - Button, - Checkbox, - Heading, - IconButton, - Input, - Text, - toast, -} from "@medusajs/ui" -import { useTranslation } from "react-i18next" - -import { useEffect, useMemo, useState } from "react" -import { Form } from "../../../../../components/common/form" -import { SplitView } from "../../../../../components/layout/split-view" -import { - RouteFocusModal, - useRouteModal, -} from "../../../../../components/route-modal" -import { DataTable } from "../../../../../components/table/data-table" -import { useCreateServiceZone } from "../../../../../hooks/api/stock-locations" -import { useDataTable } from "../../../../../hooks/use-data-table" -import { countries as staticCountries } from "../../../../../lib/countries" -import { useCountries } from "../../../../regions/common/hooks/use-countries" -import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns" -import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query" - -const PREFIX = "ac" -const PAGE_SIZE = 50 - -const ConditionsFooter = ({ onSave }: { onSave: () => void }) => { - const { t } = useTranslation() - - return ( -
- - - - -
- ) -} - -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({}) - - const form = useForm>({ - defaultValues: { - name: "", - countries: [], - }, - resolver: zodResolver(CreateServiceZoneSchema), - }) - - const { mutateAsync: createServiceZone, isPending: isLoading } = - useCreateServiceZone(locationId, fulfillmentSet.id) - - const handleSubmit = form.handleSubmit(async (data) => { - try { - await createServiceZone({ - name: data.name, - geo_zones: data.countries.map((iso2) => ({ - country_code: iso2, - type: "country", - })), - }) - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("general.close"), - }) - } - - handleSuccess() - }) - - const handleOpenChange = (open: boolean) => { - setOpen(open) - } - - const { searchParams, raw } = useCountryTableQuery({ - pageSize: PAGE_SIZE, - prefix: PREFIX, - }) - const { countries, count } = useCountries({ - countries: staticCountries.map((c, i) => ({ - display_name: c.display_name, - name: c.name, - id: i as any, - iso_2: c.iso_2, - iso_3: c.iso_3, - num_code: c.num_code, - region_id: null, - region: {} as HttpTypes.AdminRegion, - })), - ...searchParams, - }) - - const columns = useColumns() - - const { table } = useDataTable({ - data: countries || [], - columns, - count, - enablePagination: true, - enableRowSelection: true, - getRowId: (row) => row.iso_2, - pageSize: PAGE_SIZE, - rowSelection: { - state: rowSelection, - updater: setRowSelection, - }, - prefix: PREFIX, - }) - - const countriesWatch = form.watch("countries") - - const onCountriesSave = () => { - form.setValue("countries", Object.keys(rowSelection)) - setOpen(false) - } - - const removeCountry = (iso2: string) => { - const state = { ...rowSelection } - delete state[iso2] - setRowSelection(state) - - form.setValue( - "countries", - countriesWatch.filter((c) => c !== iso2) - ) - } - - const clearAll = () => { - setRowSelection({}) - form.setValue("countries", []) - } - - const selectedCountries = useMemo(() => { - return staticCountries.filter((c) => c.iso_2 in rowSelection) - }, [countriesWatch]) - - useEffect(() => { - // set selected rows from form state on open - if (open) { - setRowSelection( - countriesWatch.reduce((acc, c) => { - acc[c] = true - return acc - }, {}) - ) - } - }, [open]) - - const showAreasError = - form.formState.errors["countries"]?.type === "too_small" - - return ( - -
- -
- - - - -
-
- - - - -
- - {t("location.fulfillmentSet.create.title", { - fulfillmentSet: fulfillmentSet.name, - })} - - -
- { - return ( - - - {t("location.serviceZone.create.zoneName")} - - - - - - - ) - }} - /> -
-
- - - - {t("location.serviceZone.create.subtitle")} - - - {t("location.serviceZone.create.description")} - - - - {/*AREAS*/} -
-
- - {t("location.serviceZone.areas.title")} - - - {t("location.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("location.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/routes/locations/service-zone-create/index.ts b/packages/admin-next/dashboard/src/routes/locations/service-zone-create/index.ts deleted file mode 100644 index 6072fba7d2..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ServiceZoneCreate as Component } from "./service-zone-create" -export { stockLocationLoader as loader } from "./loader" diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/service-zone-create.tsx b/packages/admin-next/dashboard/src/routes/locations/service-zone-create/service-zone-create.tsx deleted file mode 100644 index 981aac711a..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-create/service-zone-create.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useLoaderData, useParams } from "react-router-dom" - -import { RouteFocusModal } from "../../../components/route-modal" -import { CreateServiceZoneForm } from "./components/create-service-zone-form" -import { stockLocationLoader } from "./loader" - -export function ServiceZoneCreate() { - const { fset_id, location_id } = useParams() - const { stock_location: stockLocation } = useLoaderData() as Awaited< - ReturnType - > - - const fulfillmentSet = stockLocation.fulfillment_sets.find( - (f) => f.id === fset_id - ) - - if (!fulfillmentSet) { - throw new Error("Fulfillment set doesn't exist") - } - - return ( - - - - ) -} diff --git a/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/index.ts b/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/index.ts deleted file mode 100644 index dc69fc18e2..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/service-zone-edit/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ServiceZoneEdit as Component } from "./service-zone-edit" diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx b/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx deleted file mode 100644 index 10f086eca2..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { ShippingOptionDTO } from "@medusajs/types" -import { Button, Input, RadioGroup, Select, Switch, toast } from "@medusajs/ui" -import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import * as zod from "zod" - -import { Form } from "../../../../../components/common/form" -import { - RouteDrawer, - useRouteModal, -} from "../../../../../components/route-modal" -import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers" -import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options" -import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles" -import { pick } from "../../../../../lib/common" -import { formatProvider } from "../../../../../lib/format-provider" -import { isOptionEnabledInStore } from "../../../../../lib/shipping-options" - -enum ShippingAllocation { - FlatRate = "flat", - Calculated = "calculated", -} - -type EditShippingOptionFormProps = { - isReturn?: boolean - shippingOption: ShippingOptionDTO -} - -const EditShippingOptionSchema = zod.object({ - name: zod.string().min(1), - price_type: zod.nativeEnum(ShippingAllocation), - enabled_in_store: zod.boolean().optional(), - shipping_profile_id: zod.string(), - provider_id: zod.string(), -}) - -export const EditShippingOptionForm = ({ - shippingOption, - isReturn, -}: EditShippingOptionFormProps) => { - const { t } = useTranslation() - const { handleSuccess } = useRouteModal() - - const { shipping_profiles: shippingProfiles } = useShippingProfiles({ - limit: 999, - }) - - const { fulfillment_providers = [] } = useFulfillmentProviders({ - is_enabled: true, - }) - - const form = useForm>({ - defaultValues: { - name: shippingOption.name, - price_type: shippingOption.price_type as ShippingAllocation, - enabled_in_store: isOptionEnabledInStore(shippingOption), - shipping_profile_id: shippingOption.shipping_profile_id, - provider_id: shippingOption.provider_id, - }, - }) - - const { mutateAsync, isPending: isLoading } = useUpdateShippingOptions( - shippingOption.id - ) - - const handleSubmit = form.handleSubmit(async (values) => { - const rules = shippingOption.rules.map((r) => ({ - ...pick(r, ["id", "attribute", "operator", "value"]), - })) - - const storeRule = rules.find((r) => r.attribute === "enabled_in_store") - if (!storeRule) { - // NOTE: should always exist since we always create this rule when we create a shipping option - rules.push({ - value: values.enabled_in_store ? "true" : "false", - attribute: "enabled_in_store", - operator: "eq", - }) - } else { - storeRule.value = values.enabled_in_store ? "true" : "false" - } - - await mutateAsync( - { - name: values.name, - price_type: values.price_type, - shipping_profile_id: values.shipping_profile_id, - provider_id: values.provider_id, - rules, - }, - { - onSuccess: () => { - toast.success(t("general.success"), { - dismissLabel: t("actions.close"), - }) - handleSuccess() - }, - onError: (e) => { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("actions.close"), - }) - }, - } - ) - }) - - return ( - -
- -
-
- { - return ( - - - {t("location.shippingOptions.create.allocation")} - - - - - - - - - - ) - }} - /> - -
-
- { - return ( - - {t("fields.name")} - - - - - - ) - }} - /> - - { - return ( - - - {t("location.shippingOptions.create.profile")} - - - - - - ) - }} - /> - { - return ( - - - {t("location.shippingOptions.edit.provider")} - - - - - - ) - }} - /> -
- -
- ( - -
- - {t("location.shippingOptions.create.enable")} - - - - -
- - {t( - "location.shippingOptions.create.enableDescription" - )} - - -
- )} - /> -
-
-
-
-
- -
- - - - -
-
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/index.ts b/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/index.ts deleted file mode 100644 index bbc89cc44d..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-option-edit/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ShippingOptionEdit as Component } from "./shipping-option-edit" diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx b/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx deleted file mode 100644 index fb60a5bb24..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-form.tsx +++ /dev/null @@ -1,474 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import React, { useEffect } from "react" -import { useForm } from "react-hook-form" -import * as zod from "zod" - -import { ServiceZoneDTO } from "@medusajs/types" -import { - Button, - Heading, - Input, - ProgressStatus, - ProgressTabs, - RadioGroup, - Select, - Switch, - Text, - clx, -} from "@medusajs/ui" -import { useTranslation } from "react-i18next" - -import { Form } from "../../../../../components/common/form" -import { - RouteFocusModal, - useRouteModal, -} from "../../../../../components/route-modal" -import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers" -import { useRegions } from "../../../../../hooks/api/regions" -import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options" -import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles" -import { formatProvider } from "../../../../../lib/format-provider" -import { CreateShippingOptionsPricesForm } from "./create-shipping-options-prices-form" -import { castNumber } from "../../../../../lib/cast-number" - -enum Tab { - DETAILS = "details", - PRICING = "pricing", -} - -enum ShippingAllocation { - FlatRate = "flat", - Calculated = "calculated", -} - -type StepStatus = { - [key in Tab]: ProgressStatus -} - -const CreateShippingOptionSchema = zod.object({ - name: zod.string().min(1), - price_type: zod.nativeEnum(ShippingAllocation), - enabled_in_store: zod.boolean().optional(), - shipping_profile_id: zod.string(), - provider_id: zod.string().min(1), - region_prices: zod.record(zod.string(), zod.string().optional()), - currency_prices: zod.record(zod.string(), zod.string().optional()), -}) - -type CreateShippingOptionFormProps = { - zone: ServiceZoneDTO - isReturn?: boolean -} - -export function CreateShippingOptionsForm({ - zone, - isReturn, -}: CreateShippingOptionFormProps) { - const { t } = useTranslation() - const { handleSuccess } = useRouteModal() - const [tab, setTab] = React.useState(Tab.DETAILS) - - const { fulfillment_providers = [] } = useFulfillmentProviders({ - is_enabled: true, - }) - - const { regions = [] } = useRegions({ - limit: 999, - fields: "id,currency_code", - }) - - const form = useForm>({ - defaultValues: { - name: "", - price_type: ShippingAllocation.FlatRate, - enabled_in_store: true, - shipping_profile_id: "", - provider_id: "", - region_prices: {}, - currency_prices: {}, - }, - resolver: zodResolver(CreateShippingOptionSchema), - }) - - const isCalculatedPriceType = - form.watch("price_type") === ShippingAllocation.Calculated - - const { mutateAsync: createShippingOption, isPending: isLoading } = - useCreateShippingOptions() - - const { shipping_profiles: shippingProfiles } = useShippingProfiles({ - limit: 999, - }) - - const handleSubmit = form.handleSubmit(async (data) => { - const currencyPrices = Object.entries(data.currency_prices) - .map(([code, value]) => { - const amount = value === "" ? undefined : castNumber(value) - - return { - currency_code: code, - amount: amount, - } - }) - .filter((o) => !!o.amount) - - const regionsMap = new Map(regions.map((r) => [r.id, r.currency_code])) - - const regionPrices = Object.entries(data.region_prices) - .map(([region_id, value]) => { - const code = regionsMap.get(region_id) - - const amount = value === "" ? undefined : castNumber(value) - - return { - region_id, - amount: amount, - } - }) - .filter((o) => !!o.amount) - - await createShippingOption({ - name: data.name, - price_type: data.price_type, - service_zone_id: zone.id, - shipping_profile_id: data.shipping_profile_id, - provider_id: data.provider_id, - prices: [...currencyPrices, ...regionPrices], - rules: [ - { - value: isReturn ? '"true"' : '"false"', // we want JSONB saved as string - attribute: "is_return", - operator: "eq", - }, - { - value: data.enabled_in_store ? '"true"' : '"false"', // we want JSONB saved as string - attribute: "enabled_in_store", - operator: "eq", - }, - ], - type: { - // TODO: FETCH TYPES - label: "Type label", - description: "Type description", - code: "type-code", - }, - }) - - handleSuccess() - }) - - const [status, setStatus] = React.useState({ - [Tab.PRICING]: "not-started", - [Tab.DETAILS]: "not-started", - }) - - const onTabChange = React.useCallback(async (value: Tab) => { - setTab(value) - }, []) - - const onNext = React.useCallback(async () => { - switch (tab) { - case Tab.DETAILS: { - setTab(Tab.PRICING) - break - } - case Tab.PRICING: - break - } - }, [tab]) - - const canMoveToPricing = - form.watch("name").length && - form.watch("shipping_profile_id") && - form.watch("provider_id") - - useEffect(() => { - if (form.formState.isDirty) { - setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" })) - } else { - setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "not-started" })) - } - }, [form.formState.isDirty]) - - useEffect(() => { - if (tab === Tab.DETAILS && form.formState.isDirty) { - setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" })) - } - - if (tab === Tab.PRICING) { - const hasPricingSet = form - .getValues(["region_prices", "currency_prices"]) - .map(Object.keys) - .some((i) => i.length) - - setStatus((prev) => ({ - ...prev, - [Tab.DETAILS]: "completed", - [Tab.PRICING]: hasPricingSet ? "in-progress" : "not-started", - })) - } - }, [tab]) - - return ( - -
- onTabChange(tab as Tab)} - > - - - - - {t("location.shippingOptions.create.details")} - - - {!isCalculatedPriceType && ( - - - {t("location.shippingOptions.create.pricing")} - - - )} - -
- - - - -
-
- - - -
- - {t( - `location.${ - isReturn ? "returnOptions" : "shippingOptions" - }.create.title`, - { - zone: zone.name, - } - )} - - - {!isReturn && ( -
- - {t("location.shippingOptions.create.subtitle")} - - - {t("location.shippingOptions.create.description")} - -
- )} - - { - return ( - - - {t("location.shippingOptions.create.allocation")} - - - - - - - - - - ) - }} - /> - -
- { - return ( - - {t("fields.name")} - - - - - - ) - }} - /> -
- -
-
- { - return ( - - - {t("location.shippingOptions.create.profile")} - - - - - - ) - }} - /> - - { - return ( - - - {t("location.shippingOptions.edit.provider")} - - - - - - ) - }} - /> -
-
- ( - -
- - {t("location.shippingOptions.create.enable")} - - - - -
- - {t( - "location.shippingOptions.create.enableDescription" - )} - - -
- )} - /> -
-
-
-
- - - - -
-
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx b/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx deleted file mode 100644 index e1859b2d68..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { CurrencyDTO, HttpTypes } from "@medusajs/types" -import { ColumnDef, createColumnHelper } from "@tanstack/react-table" -import { useEffect, useMemo, useState } from "react" -import { UseFormReturn } from "react-hook-form" -import { useTranslation } from "react-i18next" -import * as zod from "zod" - -import { CurrencyCell } from "../../../../../components/grid/grid-cells/common/currency-cell" -import { DataGrid } from "../../../../../components/grid/data-grid" -import { DataGridMeta } from "../../../../../components/grid/types" -import { useCurrencies } from "../../../../../hooks/api/currencies" -import { useRegions } from "../../../../../hooks/api/regions" -import { useStore } from "../../../../../hooks/api/store" - -const PricingCreateSchemaType = zod.record( - zod.object({ - currency_prices: zod.record(zod.string().optional()), - region_prices: zod.record(zod.string().optional()), - }) -) - -type PricingPricesFormProps = { - form: UseFormReturn -} - -enum ColumnType { - REGION = "region", - CURRENCY = "currency", -} - -type EnabledColumnRecord = Record - -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({}) - - 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 ( -
- -
- ) -} - -const columnHelper = createColumnHelper< - HttpTypes.AdminProduct | HttpTypes.AdminProductVariant ->() - -const useColumns = ({ - currencies = [], - regions = [], -}: { - currencies?: CurrencyDTO[] - regions?: HttpTypes.AdminRegion[] -}) => { - const { t } = useTranslation() - - const colDefs: ColumnDef< - HttpTypes.AdminProduct | HttpTypes.AdminProductVariant - >[] = useMemo(() => { - return [ - ...currencies.map((currency) => { - return columnHelper.display({ - header: t("fields.priceTemplate", { - regionOrCountry: currency.code.toUpperCase(), - }), - cell: ({ row, table }) => { - return ( - } - field={`currency_prices.${currency.code}`} - /> - ) - }, - }) - }), - ...regions.map((region) => { - return columnHelper.display({ - header: t("fields.priceTemplate", { - regionOrCountry: region.name, - }), - cell: ({ row, table }) => { - return ( - c.code === region.currency_code - )} - meta={table.options.meta as DataGridMeta} - field={`region_prices.${region.id}`} - /> - ) - }, - }) - }), - ] - }, [t, currencies, regions]) - - return colDefs -} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/index.ts b/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/index.ts deleted file mode 100644 index 135ecb2465..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ShippingOptionsCreate as Component } from "./shipping-options-create" -export { stockLocationLoader as loader } from "./loader" diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/shipping-options-create.tsx b/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/shipping-options-create.tsx deleted file mode 100644 index 384ededeb4..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-create/shipping-options-create.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useLoaderData, useParams, useSearchParams } 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 [searchParams] = useSearchParams() - - const isReturn = searchParams.has("is_return") - - const { stock_location: stockLocation } = useLoaderData() as Awaited< - ReturnType - > - - 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 ( - - - - ) -} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx b/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx deleted file mode 100644 index 4de2657700..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useMemo, useState } from "react" -import { useForm } from "react-hook-form" -import * as zod from "zod" - -import { - CurrencyDTO, - PriceDTO, - ShippingOptionDTO, - HttpTypes, -} from "@medusajs/types" -import { Button, toast } from "@medusajs/ui" -import { useTranslation } from "react-i18next" - -import { ColumnDef, createColumnHelper } from "@tanstack/react-table" -import { DataGrid } from "../../../../../components/grid/data-grid/index" -import { CurrencyCell } from "../../../../../components/grid/grid-cells/common/currency-cell/index" -import { DataGridMeta } from "../../../../../components/grid/types" -import { - RouteFocusModal, - useRouteModal, -} from "../../../../../components/route-modal/index" -import { useCurrencies } from "../../../../../hooks/api/currencies" -import { useRegions } from "../../../../../hooks/api/regions" -import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options" -import { useStore } from "../../../../../hooks/api/store" -import { castNumber } from "../../../../../lib/cast-number.ts" - -const getInitialCurrencyPrices = (prices: PriceDTO[]) => { - const ret: Record = {} - prices.forEach((p) => { - if (p.price_rules!.length) { - // this is a region price - return - } - ret[p.currency_code!] = castNumber(p.amount) - }) - return ret -} - -const getInitialRegionPrices = (prices: PriceDTO[]) => { - const ret: Record = {} - prices.forEach((p) => { - if (p.price_rules!.length) { - const regionId = p.price_rules![0].value - ret[regionId] = castNumber(p.amount) - } - }) - - return ret -} - -const EditShippingOptionPricingSchema = zod.object({ - region_prices: zod.record( - zod.string(), - zod.string().or(zod.number()).optional() - ), - currency_prices: zod.record( - zod.string(), - zod.string().or(zod.number()).optional() - ), -}) - -enum ColumnType { - REGION = "region", - CURRENCY = "currency", -} - -type EnabledColumnRecord = Record - -type EditShippingOptionPricingFormProps = { - shippingOption: ShippingOptionDTO & { prices: PriceDTO[] } -} - -export function EditShippingOptionsPricingForm({ - shippingOption, -}: EditShippingOptionPricingFormProps) { - const { t } = useTranslation() - const { handleSuccess } = useRouteModal() - - const form = useForm>({ - defaultValues: { - region_prices: getInitialRegionPrices(shippingOption.prices), - currency_prices: getInitialCurrencyPrices(shippingOption.prices), - }, - resolver: zodResolver(EditShippingOptionPricingSchema), - }) - - const { mutateAsync, isPending: isLoading } = useUpdateShippingOptions( - shippingOption.id - ) - - const { regions } = useRegions() - - const { store, isLoading: isStoreLoading } = useStore() - - const { currencies, isLoading: isCurrenciesLoading } = useCurrencies( - { - code: store?.supported_currency_codes, - }, - { - enabled: !!store, - } - ) - - const [enabledColumns, setEnabledColumns] = useState({}) - - 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 data = useMemo( - () => [[...(currencies || []), ...(regions || [])]], - [currencies, regions] - ) - - const handleSubmit = form.handleSubmit(async (data) => { - const currencyPrices = Object.entries(data.currency_prices) - .map(([code, value]) => { - if (value === "") { - return undefined - } - - const amount = castNumber(value) - - const priceRecord = { - currency_code: code, - amount: amount, - } - - const price = shippingOption.prices.find( - (p) => p.currency_code === code && !p.price_rules!.length - ) - - // if that currency price is already defined for the SO, we will do an update - if (price) { - priceRecord["id"] = price.id - } - - return priceRecord - }) - .filter((p) => !!p) - - const regionsMap = new Map(regions.map((r) => [r.id, r.currency_code])) - - const regionPrices = Object.entries(data.region_prices) - .map(([region_id, value]) => { - if (value === "") { - return undefined - } - - const code = regionsMap.get(region_id)! - - const amount = getDbAmount(Number(value), code) - - const priceRecord = { - region_id, - amount: amount, - } - - /** - * HACK - when trying to update prices which already have a region price - * we get error: `Price rule with price_id: , rule_type_id: already exist`, - * so for now, we recreate region prices. - */ - - // const price = shippingOption.prices.find( - // (p) => p.price_rules?.[0]?.value === region_id - // ) - // - // if (price) { - // priceRecord["id"] = price.id - // } - - return priceRecord - }) - .filter((p) => !!p) - - try { - await mutateAsync({ - prices: [...currencyPrices, ...regionPrices], - }) - toast.success(t("general.success"), { - dismissLabel: t("general.close"), - }) - handleSuccess() - } catch (e) { - toast.error(t("general.error"), { - description: e.message, - dismissLabel: t("general.close"), - }) - } - }) - - const initializing = - isStoreLoading || isCurrenciesLoading || !store || !currencies - - return ( - -
- -
- - - - -
-
- - -
- -
-
-
-
- ) -} - -const columnHelper = createColumnHelper< - HttpTypes.AdminProduct | HttpTypes.AdminProductVariant ->() - -const useColumns = ({ - currencies = [], - regions = [], -}: { - currencies?: CurrencyDTO[] - regions?: HttpTypes.AdminRegion[] -}) => { - const { t } = useTranslation() - - const colDefs: ColumnDef< - HttpTypes.AdminProduct | HttpTypes.AdminProductVariant - >[] = useMemo(() => { - return [ - ...currencies.map((currency) => { - return columnHelper.display({ - header: t("fields.priceTemplate", { - regionOrCountry: currency.code.toUpperCase(), - }), - cell: ({ row, table }) => { - return ( - } - field={`currency_prices.${currency.code}`} - /> - ) - }, - }) - }), - ...regions.map((region) => { - return columnHelper.display({ - header: t("fields.priceTemplate", { - regionOrCountry: region.name, - }), - cell: ({ row, table }) => { - return ( - c.code === region.currency_code - )} - meta={table.options.meta as DataGridMeta} - field={`region_prices.${region.id}`} - /> - ) - }, - }) - }), - ] - }, [t, currencies, regions]) - - return colDefs -} diff --git a/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/index.ts b/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/index.ts deleted file mode 100644 index d841b62cdb..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/shipping-options-edit-pricing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ShippingOptionsEditPricing as Component } from "./shipping-options-edit-pricing" diff --git a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-organize-form/components/product-create-organize-section/product-create-details-organize-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-organize-form/components/product-create-organize-section/product-create-details-organize-section.tsx index 91d5542202..3c7f2da605 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-organize-form/components/product-create-organize-section/product-create-details-organize-section.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-organize-form/components/product-create-organize-section/product-create-details-organize-section.tsx @@ -24,7 +24,7 @@ export const ProductCreateOrganizationSection = ({ const collections = useComboboxData({ queryKey: ["product_collections"], - queryFn: sdk.admin.productCollection.list, + queryFn: (params) => sdk.admin.productCollection.list(params), getOptions: (data) => data.collections.map((collection) => ({ label: collection.title!, diff --git a/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx index 7ceed6195d..bdcac097b1 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx @@ -1,4 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" import { Button, toast } from "@medusajs/ui" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" @@ -13,7 +14,6 @@ import { useUpdateProduct } from "../../../../../hooks/api/products" import { useComboboxData } from "../../../../../hooks/use-combobox-data" import { client, sdk } from "../../../../../lib/client" import { CategoryCombobox } from "../../../common/components/category-combobox" -import { HttpTypes } from "@medusajs/types" type ProductOrganizationFormProps = { product: HttpTypes.AdminProduct @@ -34,7 +34,7 @@ export const ProductOrganizationForm = ({ const collections = useComboboxData({ queryKey: ["product_collections"], - queryFn: sdk.admin.productCollection.list, + queryFn: (params) => sdk.admin.productCollection.list(params), getOptions: (data) => data.collections.map((collection) => ({ label: collection.title!, diff --git a/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-countries.tsx b/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-countries.tsx index a92974ae8d..9ca3e4a6f4 100644 --- a/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-countries.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-countries.tsx @@ -1,5 +1,6 @@ -import { json } from "react-router-dom" import { RegionCountryDTO } from "@medusajs/types" +import { json } from "react-router-dom" +import { StaticCountry } from "../../../../lib/countries" const acceptedOrderKeys = ["name", "code"] @@ -14,7 +15,7 @@ export const useCountries = ({ limit, offset = 0, }: { - countries: RegionCountryDTO[] + countries: StaticCountry[] limit: number offset?: number order?: string diff --git a/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-country-table-columns.tsx b/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-country-table-columns.tsx index 6a98ae8f9d..e83d6c5b25 100644 --- a/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-country-table-columns.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/common/hooks/use-country-table-columns.tsx @@ -1,10 +1,10 @@ -import { RegionCountryDTO } from "@medusajs/types" - import { createColumnHelper } from "@tanstack/react-table" import { useMemo } from "react" import { useTranslation } from "react-i18next" -const columnHelper = createColumnHelper() +import { StaticCountry } from "../../../../lib/countries" + +const columnHelper = createColumnHelper() export const useCountryTableColumns = () => { const { t } = useTranslation() diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-columns.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-columns.tsx index 317295ab4b..84faf34525 100644 --- a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-columns.tsx +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-columns.tsx @@ -2,7 +2,7 @@ import { ExtendedReservationItem } from "@medusajs/medusa" import { createColumnHelper } from "@tanstack/react-table" import { useMemo } from "react" import { useTranslation } from "react-i18next" -import { InlineLink } from "../../../../../components/common/inline-link" +import { LinkButton } from "../../../../../components/common/link-button" import { DateCell } from "../../../../../components/table/table-cells/common/date-cell" import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" import { ReservationActions } from "./reservation-actions" @@ -41,11 +41,11 @@ export const useReservationTableColumns = () => { return (
- + #{inventoryItem.order.display_id} - +
) }, diff --git a/packages/core/js-sdk/src/admin/fulfillment-set.ts b/packages/core/js-sdk/src/admin/fulfillment-set.ts index c477d45f72..817ab14d5f 100644 --- a/packages/core/js-sdk/src/admin/fulfillment-set.ts +++ b/packages/core/js-sdk/src/admin/fulfillment-set.ts @@ -24,7 +24,7 @@ export class FulfillmentSet { query?: HttpTypes.SelectParams, headers?: ClientHeaders ) { - return await this.client.fetch( + return await this.client.fetch( `/admin/fulfillment-sets/${id}/service-zones`, { method: "POST", @@ -35,7 +35,7 @@ export class FulfillmentSet { ) } - async retrieveServiceZones( + async retrieveServiceZone( fulfillmentSetId: string, serviceZoneId: string, query?: HttpTypes.SelectParams, @@ -58,7 +58,7 @@ export class FulfillmentSet { query?: HttpTypes.SelectParams, headers?: ClientHeaders ) { - return await this.client.fetch( + return await this.client.fetch( `/admin/fulfillment-sets/${fulfillmentSetId}/service-zones/${serviceZoneId}`, { method: "POST", diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index 600512d179..f79be661d6 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -1,6 +1,7 @@ import { Client } from "../client" import { Customer } from "./customer" import { Fulfillment } from "./fulfillment" +import { FulfillmentProvider } from "./fulfillment-provider" import { FulfillmentSet } from "./fulfillment-set" import { InventoryItem } from "./inventory-item" import { Invite } from "./invite" @@ -31,6 +32,7 @@ export class Admin { public salesChannel: SalesChannel public fulfillmentSet: FulfillmentSet public fulfillment: Fulfillment + public fulfillmentProvider: FulfillmentProvider public shippingOption: ShippingOption public shippingProfile: ShippingProfile public inventoryItem: InventoryItem @@ -51,6 +53,7 @@ export class Admin { this.salesChannel = new SalesChannel(client) this.fulfillmentSet = new FulfillmentSet(client) this.fulfillment = new Fulfillment(client) + this.fulfillmentProvider = new FulfillmentProvider(client) this.shippingOption = new ShippingOption(client) this.shippingProfile = new ShippingProfile(client) this.inventoryItem = new InventoryItem(client) diff --git a/packages/core/types/src/http/fulfillment-provider/admin/entities.ts b/packages/core/types/src/http/fulfillment-provider/admin/entities.ts index ff49c2d9db..ec716a6d6e 100644 --- a/packages/core/types/src/http/fulfillment-provider/admin/entities.ts +++ b/packages/core/types/src/http/fulfillment-provider/admin/entities.ts @@ -1,8 +1,4 @@ export interface AdminFulfillmentProvider { id: string - name: string - metadata: Record | null - created_at: string - updated_at: string - deleted_at: string | null + is_enabled: boolean } diff --git a/packages/core/types/src/http/fulfillment-set/admin/entities.ts b/packages/core/types/src/http/fulfillment-set/admin/entities.ts index 7f6bd69c61..41629d5f8e 100644 --- a/packages/core/types/src/http/fulfillment-set/admin/entities.ts +++ b/packages/core/types/src/http/fulfillment-set/admin/entities.ts @@ -1,4 +1,5 @@ import { GeoZoneType } from "../../../fulfillment" +import { AdminShippingOption } from "../../shipping-option" export interface AdminGeoZone { id: string @@ -14,9 +15,10 @@ export interface AdminGeoZone { export interface AdminServiceZone { id: string + name: string fulfillment_set_id: string geo_zones: AdminGeoZone[] - shipping_options: any[] + shipping_options: AdminShippingOption[] created_at: string updated_at: string deleted_at: string | null diff --git a/packages/core/types/src/http/shipping-option/admin/entities.ts b/packages/core/types/src/http/shipping-option/admin/entities.ts index dbdfd0cd23..5a9c16396b 100644 --- a/packages/core/types/src/http/shipping-option/admin/entities.ts +++ b/packages/core/types/src/http/shipping-option/admin/entities.ts @@ -26,6 +26,17 @@ export interface AdminShippingOptionRule { deleted_at: string | null } +// TODO: This type is complete, but it's not clear what the `rules` field is supposed to return in all cases. +export interface AdminShippingOptionPriceRule { + id: string + value: string +} + +export interface AdminShippingOptionPrice extends AdminPrice { + price_rules: AdminShippingOptionPriceRule[] + rules_count: number +} + export interface AdminShippingOption { id: string name: string @@ -39,7 +50,7 @@ export interface AdminShippingOption { shipping_profile_id: string shipping_profile: AdminShippingProfile rules: AdminShippingOptionRule[] - prices: AdminPrice[] + prices: AdminShippingOptionPrice[] data: Record | null metadata: Record | null created_at: Date diff --git a/packages/core/types/src/http/shipping-option/admin/payloads.ts b/packages/core/types/src/http/shipping-option/admin/payloads.ts index d8b9fc65a2..969a7496e2 100644 --- a/packages/core/types/src/http/shipping-option/admin/payloads.ts +++ b/packages/core/types/src/http/shipping-option/admin/payloads.ts @@ -1,24 +1,24 @@ import { RuleOperatorType } from "../../../common" import { ShippingOptionPriceType } from "../../../fulfillment" -interface AdminCreateShippingOptionRule { +export interface AdminCreateShippingOptionRule { operator: RuleOperatorType attribute: string value: string | string[] } -interface AdminCreateShippingOptionType { +export interface AdminCreateShippingOptionType { label: string description: string code: string } -interface AdminCreateShippingOptionPriceWithCurrency { +export interface AdminCreateShippingOptionPriceWithCurrency { currency_code: string amount: number } -interface AdminCreateShippingOptionPriceWithRegion { +export interface AdminCreateShippingOptionPriceWithRegion { region_id: string amount: number } @@ -38,17 +38,18 @@ export interface AdminCreateShippingOption { rules?: AdminCreateShippingOptionRule[] } -interface AdminUpdateShippingOptionRule extends AdminCreateShippingOptionRule { - id: string +export interface AdminUpdateShippingOptionRule + extends AdminCreateShippingOptionRule { + id?: string } -interface AdminUpdateShippingOptionPriceWithCurrency { +export interface AdminUpdateShippingOptionPriceWithCurrency { id?: string currency_code?: string amount?: number } -interface AdminUpdateShippingOptionPriceWithRegion { +export interface AdminUpdateShippingOptionPriceWithRegion { id?: string region_id?: string amount?: number diff --git a/packages/core/types/src/http/stock-locations/admin/entities.ts b/packages/core/types/src/http/stock-locations/admin/entities.ts index d91f33ebe8..c5120126e6 100644 --- a/packages/core/types/src/http/stock-locations/admin/entities.ts +++ b/packages/core/types/src/http/stock-locations/admin/entities.ts @@ -1,3 +1,4 @@ +import { AdminFulfillmentSet } from "../../fulfillment-set" import { AdminSalesChannel } from "../../sales-channel" export interface AdminStockLocationAddress { @@ -18,5 +19,5 @@ export interface AdminStockLocation { address_id: string address?: AdminStockLocationAddress sales_channels?: AdminSalesChannel[] - fulfillment_sets?: any[] + fulfillment_sets?: AdminFulfillmentSet[] }