From c2d56ca12b89af078b885a0acced20e29bf6f8f5 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Sat, 9 Mar 2024 14:19:29 +0100 Subject: [PATCH] feat(medusa,dashboard): Tax settings pages and fixes to list tax rates endpoint (#6606) **What** - Adds pages for managing tax settings and tax rates for regions. - Fixes `list` tax rates endpoint, which was missing pagination, search, order, and filters. **Note** The fix to the tax rate list endpoint is very rough, as I have had to reimplement most of the logic from our transformQuery middleware. This is because this endpoints does not follow normal convention for fields and expand and uses string arrays instead of strings separated by commas. Our middleware does not support this, and changing the endpoint to align with other endpoints on the expand and fields params would be a breaking change. Since this is very temporary until 2.0 I think it's okay for the time being. CLOSES CORE-1654 --- .changeset/nasty-nails-judge.md | 6 + .../public/locales/en-US/translation.json | 52 +- .../common/percentage-input/index.ts | 1 + .../percentage-input/percentage-input.tsx | 32 ++ .../data-table-filter/date-filter.tsx | 2 +- .../data-table-skeleton.tsx | 155 +++--- .../table/data-table/data-table.tsx | 9 +- .../admin-only-cell/admin-only-cell.tsx | 25 + .../shipping-option/admin-only-cell/index.ts | 1 + .../shipping-option/is-return-cell/index.ts | 1 + .../is-return-cell/is-return-cell.tsx | 27 + .../shipping-option/price-type-cell/index.ts | 1 + .../price-type-cell/price-type-cell.tsx | 34 ++ .../shipping-option-cell/index.ts | 1 + .../shipping-option-cell.tsx | 28 ++ .../shipping-price-cell.tsx | 31 ++ .../subtotal-requirement-cell/index.ts | 1 + .../subtotal-requirement-cell.tsx | 43 ++ .../use-shipping-option-table-columns.tsx | 107 ++++ .../use-shipping-option-table-filters.tsx | 2 +- .../use-shipping-option-table-query.tsx | 2 +- .../dashboard/src/lib/percentage-helpers.ts | 27 + .../router-provider/router-provider.tsx | 28 +- .../edit-product-form/edit-product-form.tsx | 7 +- .../region-shipping-option-section.tsx | 91 +++- .../use-shipping-option-table-columns.tsx | 179 ------- .../routes/regions/region-detail/loader.ts | 4 +- .../regions/region-detail/region-detail.tsx | 11 +- .../common/components/override-chip/index.ts | 1 + .../override-chip/override-chip.tsx | 24 + .../common/components/override-grid/index.ts | 1 + .../override-grid/override-grid.tsx | 42 ++ .../components/overrides-drawer/index.ts | 1 + .../overrides-drawer/overrides-drawer.tsx | 394 +++++++++++++++ .../src/routes/taxes/common/constants.ts | 5 + .../use-product-override-table-columns.tsx | 46 ++ ...se-product-type-override-table-columns.tsx | 49 ++ ...shipping-option-override-table-columns.tsx | 46 ++ ...se-product-type-override-table-filters.tsx | 17 + .../use-product-type-override-table-query.tsx | 29 ++ .../src/routes/taxes/common/types.ts | 10 + .../components/tax-details-section/index.ts | 1 - .../tax-details-section.tsx | 85 ---- .../tax-rates-section/tax-rates-section.tsx | 64 --- .../components/tax-countries-section/index.ts | 1 + .../tax-countries-section.tsx | 69 +++ .../tax-default-tax-rate-section/index.ts | 1 + .../tax-default-tax-rate-section.tsx | 56 +++ .../components/tax-general-section/index.ts | 1 + .../tax-general-section.tsx | 93 ++++ .../components/tax-rates-section/index.ts | 0 .../tax-rates-section/tax-rate-actions.tsx | 61 +++ .../tax-rates-section/tax-rates-section.tsx | 93 ++++ .../use-tax-rate-table-columns.tsx | 127 +++++ .../use-tax-rate-table-filters.tsx | 19 + .../use-tax-rate-table-query.tsx | 29 ++ .../src/routes/taxes/tax-detail/index.ts | 2 + .../src/routes/taxes/tax-detail/loader.ts | 21 + .../routes/taxes/tax-detail/tax-detail.tsx | 45 ++ .../edit-tax-form/edit-tax-form.tsx | 257 ++++++++++ .../components/edit-tax-form/index.ts | 1 + .../src/routes/taxes/tax-edit/index.ts | 1 + .../src/routes/taxes/tax-edit/tax-edit.tsx | 40 ++ .../components/tax-list-callout/index.ts | 1 + .../tax-list-callout/tax-list-callout.tsx | 24 + .../components/tax-list-table/index.ts | 1 + .../tax-list-table/tax-list-table.tsx | 70 +++ .../taxes/{views => }/tax-list/index.ts | 0 .../src/routes/taxes/tax-list/tax-list.tsx | 11 + .../create-tax-rate-form.tsx | 474 ++++++++++++++++++ .../components/create-tax-rate-form/index.ts | 1 + .../src/routes/taxes/tax-rate-create/index.ts | 1 + .../taxes/tax-rate-create/tax-rate-create.tsx | 10 + .../edit-tax-rate-overrides-form.tsx | 414 +++++++++++++++ .../edit-tax-rate-overrides-form/index.ts | 1 + .../taxes/tax-rate-edit-overrides/index.ts | 1 + .../tax-rate-edit-overrides.tsx | 24 + .../edit-tax-rate-form/edit-tax-rate-form.tsx | 133 +++++ .../components/edit-tax-rate-form/index.ts | 1 + .../src/routes/taxes/tax-rate-edit/index.ts | 1 + .../taxes/tax-rate-edit/tax-rate-edit.tsx | 28 ++ .../routes/taxes/views/tax-details/index.ts | 1 - .../taxes/views/tax-details/tax-details.tsx | 34 -- .../routes/taxes/views/tax-list/tax-list.tsx | 26 - .../src/lib/models/AdminGetTaxRatesParams.ts | 71 +++ .../routes/admin/tax-rates/list-tax-rates.ts | 147 +++++- packages/medusa/src/services/tax-rate.ts | 32 +- packages/medusa/src/types/tax-rate.ts | 1 + 88 files changed, 3659 insertions(+), 489 deletions(-) create mode 100644 .changeset/nasty-nails-judge.md create mode 100644 packages/admin-next/dashboard/src/components/common/percentage-input/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/percentage-input/percentage-input.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/admin-only-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/is-return-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/price-type-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/shipping-option-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-price-cell/shipping-price-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/subtotal-requirement-cell.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/columns/use-shipping-option-table-columns.tsx rename packages/admin-next/dashboard/src/{routes/regions/region-detail/components/region-shipping-option-section => hooks/table/filters}/use-shipping-option-table-filters.tsx (93%) rename packages/admin-next/dashboard/src/{routes/regions/region-detail/components/region-shipping-option-section => hooks/table/query}/use-shipping-option-table-query.tsx (93%) create mode 100644 packages/admin-next/dashboard/src/lib/percentage-helpers.ts delete mode 100644 packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/override-chip.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/override-grid.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/overrides-drawer.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/constants.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-override-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-type-override-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-shipping-option-override-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/hooks/filters/use-product-type-override-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/hooks/query/use-product-type-override-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/common/types.ts delete mode 100644 packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/tax-details-section.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/taxes/components/tax-rates-section/tax-rates-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/tax-countries-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/tax-default-tax-rate-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/tax-general-section.tsx rename packages/admin-next/dashboard/src/routes/taxes/{ => tax-detail}/components/tax-rates-section/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rate-actions.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rates-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-detail/tax-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/edit-tax-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-edit/tax-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/tax-list-callout.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/tax-list-table.tsx rename packages/admin-next/dashboard/src/routes/taxes/{views => }/tax-list/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-list/tax-list.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/create-tax-rate-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/tax-rate-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/edit-tax-rate-overrides-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/tax-rate-edit-overrides.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/edit-tax-rate-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/tax-rate-edit.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/taxes/views/tax-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/taxes/views/tax-details/tax-details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/taxes/views/tax-list/tax-list.tsx diff --git a/.changeset/nasty-nails-judge.md b/.changeset/nasty-nails-judge.md new file mode 100644 index 0000000000..2393844864 --- /dev/null +++ b/.changeset/nasty-nails-judge.md @@ -0,0 +1,6 @@ +--- +"@medusajs/client-types": patch +"@medusajs/medusa": patch +--- + +fix(medusa): Fixes pagination on list Tax Rate endpoint, and also adds missing query params like order, search and filters. diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 03ce6c3021..f1d4210ee7 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -347,7 +347,7 @@ "removeCountriesWarning_one": "You are about to remove {{count}} country from the region. This action cannot be undone.", "removeCountriesWarning_other": "You are about to remove {{count}} countries from the region. This action cannot be undone.", "removeCountryWarning": "You are about to remove the country {{name}} from the region. This action cannot be undone.", - "taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.", + "taxInclusiveHint": "When enabled prices in the region will be tax inclusive.", "providersHint": " Add which fulfillment and payment providers should be available in this region.", "shippingOptions": "Shipping Options", "deleteShippingOptionWarning": "You are about to delete the shipping option {{name}}. This action cannot be undone.", @@ -382,6 +382,50 @@ } } }, + "taxes": { + "domain": "Taxes", + "countries": { + "taxCountriesHint": "Tax settings apply to the listed countries." + }, + "settings": { + "editTaxSettings": "Edit Tax Settings", + "taxProviderLabel": "Tax provider", + "systemTaxProviderLabel": "System Tax Provider", + "calculateTaxesAutomaticallyLabel": "Calculate taxes automatically", + "calculateTaxesAutomaticallyHint": "When enabled, tax rates will be calculated automatically and applied to carts. When disabled, taxes must be manually computed at checkout. Manual taxes are recommended for usage with third-party tax providers.", + "applyTaxesOnGiftCardsLabel": "Apply taxes on gift cards", + "applyTaxesOnGiftCardsHint": "When enabled, taxes will be applied to gift cards at checkout. In some countries, tax regulations require the application of taxes to gift cards upon purchase.", + "defaultTaxRateLabel": "Default tax rate", + "defaultTaxCodeLabel": "Default tax code" + }, + "defaultRate": { + "sectionTitle": "Default Tax Rate" + }, + "taxRate": { + "sectionTitle": "Tax Rates", + "createTaxRate": "Create Tax Rate", + "createTaxRateHint": "Create a new tax rate for the region.", + "deleteRateDescription": "You are about to delete the tax rate {{name}}. This action cannot be undone.", + "editTaxRate": "Edit Tax Rate", + "editRateAction": "Edit rate", + "editOverridesAction": "Edit overrides", + "editOverridesTitle": "Edit Tax Rate Overrides", + "editOverridesHint": "Specify the overrides for the tax rate.", + "deleteTaxRateWarning": "You are about to delete the tax rate {{name}}. This action cannot be undone.", + "productOverridesLabel": "Product overrides", + "productOverridesHint": "Specify the product overrides for the tax rate.", + "addProductOverridesAction": "Add product overrides", + "productTypeOverridesLabel": "Product type overrides", + "productTypeOverridesHint": "Specify the product type overrides for the tax rate.", + "addProductTypeOverridesAction": "Add product type overrides", + "shippingOptionOverridesLabel": "Shipping option overrides", + "shippingOptionOverridesHint": "Specify the shipping option overrides for the tax rate.", + "addShippingOptionOverridesAction": "Add shipping option overrides", + "productOverridesHeader": "Products", + "productTypeOverridesHeader": "Product Types", + "shippingOptionOverridesHeader": "Shipping Options" + } + }, "locations": { "domain": "Locations", "createLocation": "Create location", @@ -503,7 +547,7 @@ "availability": "Availability", "inventory": "Inventory", "optional": "Optional", - "taxInclusivePricing": "Tax Inclusive Pricing", + "taxInclusivePricing": "Tax inclusive pricing", "taxRate": "Tax Rate", "taxCode": "Tax Code", "currency": "Currency", @@ -568,9 +612,9 @@ "maxSubtotal": "Max. Subtotal", "shippingProfile": "Shipping Profile", "summary": "Summary", - "shippingProfile": "Shipping Profile" + "rate": "Rate" }, - "dateTime" : { + "dateTime": { "years_one": "Year", "years_other": "Years", "months_one": "Month", diff --git a/packages/admin-next/dashboard/src/components/common/percentage-input/index.ts b/packages/admin-next/dashboard/src/components/common/percentage-input/index.ts new file mode 100644 index 0000000000..69f623aa65 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/percentage-input/index.ts @@ -0,0 +1 @@ +export * from "./percentage-input" diff --git a/packages/admin-next/dashboard/src/components/common/percentage-input/percentage-input.tsx b/packages/admin-next/dashboard/src/components/common/percentage-input/percentage-input.tsx new file mode 100644 index 0000000000..31d4f3a86a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/percentage-input/percentage-input.tsx @@ -0,0 +1,32 @@ +import { Input, Text } from "@medusajs/ui" +import { ComponentProps, ElementRef, forwardRef } from "react" + +export const PercentageInput = forwardRef< + ElementRef, + Omit, "type"> +>(({ min = 0, max = 100, step = 0.0001, ...props }, ref) => { + return ( +
+
+ + % + +
+ +
+ ) +}) +PercentageInput.displayName = "HandleInput" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx index fd851b26b2..5c6a604b5d 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx @@ -109,7 +109,7 @@ export const DateFilter = ({ sideOffset={8} collisionPadding={24} className={clx( - "bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] w-[300px] overflow-hidden rounded-lg" + "bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout h-full max-h-[var(--radix-popper-available-height)] w-[300px] overflow-auto rounded-lg" )} onInteractOutside={(e) => { if (e.target instanceof HTMLElement) { diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx index 56174606a5..5bbc986fbb 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx @@ -9,6 +9,7 @@ type DataTableSkeletonProps = { orderBy: boolean filterable: boolean pagination: boolean + layout?: "fit" | "fill" } export const DataTableSkeleton = ({ @@ -18,6 +19,7 @@ export const DataTableSkeleton = ({ searchable, orderBy, pagination, + layout = "fit", }: DataTableSkeletonProps) => { const rows = Array.from({ length: rowCount }, (_, i) => i) @@ -30,7 +32,11 @@ export const DataTableSkeleton = ({ const colWidth = 100 / colCount return ( -
+
{hasToolbar && (
{filterable && } @@ -42,74 +48,91 @@ export const DataTableSkeleton = ({ )}
)} - - - +
+
+ + + {columns.map((col, i) => { + const isSelectHeader = col.id === "select" + const isActionsHeader = col.id === "actions" + + const isSpecialHeader = isSelectHeader || isActionsHeader + + return ( + + {isActionsHeader ? null : ( + + )} + + ) + })} + + + + {rows.map((_, j) => ( + + {columns.map((col, k) => { + const isSpecialCell = + col.id === "select" || col.id === "actions" + + return ( + + + + ) + })} + + ))} + +
+
+ {pagination && ( +
- {columns.map((col, i) => { - const isSelectHeader = col.id === "select" - const isActionsHeader = col.id === "actions" - - const isSpecialHeader = isSelectHeader || isActionsHeader - - return ( - - {isActionsHeader ? null : ( - - )} - - ) - })} - - - - {rows.map((_, j) => ( - - {columns.map((col, k) => { - const isSpecialCell = - col.id === "select" || col.id === "actions" - - return ( - - - - ) - })} - - ))} - - - {pagination && ( -
- -
- - - + +
+ + + +
-
- )} + )} +
) } diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx index 88a30c9a4e..47163264b2 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx @@ -36,6 +36,7 @@ export const DataTable = ({ if (isLoading) { return ( ({ const noRecords = !isLoading && count === 0 && noQuery if (noRecords) { - return + return ( + + ) } return ( diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/admin-only-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/admin-only-cell.tsx new file mode 100644 index 0000000000..5cfce2582b --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/admin-only-cell.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from "react-i18next" +import { StatusCell } from "../../common/status-cell" + +type AdminOnlyCellProps = { + adminOnly: boolean +} + +export const AdminOnlyCell = ({ adminOnly }: AdminOnlyCellProps) => { + const { t } = useTranslation() + + const color = adminOnly ? "blue" : "green" + const text = adminOnly ? t("general.admin") : t("general.store") + + return {text} +} + +export const AdminOnlyHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.availability")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/index.ts new file mode 100644 index 0000000000..681a74277f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/admin-only-cell/index.ts @@ -0,0 +1 @@ +export * from "./admin-only-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/index.ts new file mode 100644 index 0000000000..d2c98c203c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/index.ts @@ -0,0 +1 @@ +export * from "./is-return-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/is-return-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/is-return-cell.tsx new file mode 100644 index 0000000000..ac90b44b37 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/is-return-cell/is-return-cell.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from "react-i18next" + +type IsReturnCellProps = { + isReturn?: boolean +} + +export const IsReturnCell = ({ isReturn }: IsReturnCellProps) => { + const { t } = useTranslation() + + return ( +
+ + {isReturn ? t("regions.return") : t("regions.outbound")} + +
+ ) +} + +export const IsReturnHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.type")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/index.ts new file mode 100644 index 0000000000..74030ad1f4 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/index.ts @@ -0,0 +1 @@ +export * from "./price-type-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/price-type-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/price-type-cell.tsx new file mode 100644 index 0000000000..5483fddc1d --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/price-type-cell/price-type-cell.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from "react-i18next" +import { PlaceholderCell } from "../../common/placeholder-cell" + +type PriceTypeCellProps = { + priceType?: "flat_rate" | "calculated" +} + +export const PriceTypeCell = ({ priceType }: PriceTypeCellProps) => { + const { t } = useTranslation() + + if (!priceType) { + return + } + + const isFlatRate = priceType === "flat_rate" + + return ( +
+ + {isFlatRate ? t("regions.flatRate") : t("regions.calculated")} + +
+ ) +} + +export const PriceTypeHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("regions.priceType")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/index.ts new file mode 100644 index 0000000000..5b5fcbb46c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/index.ts @@ -0,0 +1 @@ +export * from "./shipping-option-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/shipping-option-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/shipping-option-cell.tsx new file mode 100644 index 0000000000..d4b7c43763 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-option-cell/shipping-option-cell.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from "react-i18next" +import { PlaceholderCell } from "../../common/placeholder-cell" + +type ShippingOptionCellProps = { + name?: string | null +} + +export const ShippingOptionCell = ({ name }: ShippingOptionCellProps) => { + if (!name) { + return + } + + return ( +
+ {name} +
+ ) +} + +export const ShippingOptionHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.name")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-price-cell/shipping-price-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-price-cell/shipping-price-cell.tsx new file mode 100644 index 0000000000..f9cb39ec05 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/shipping-price-cell/shipping-price-cell.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next" +import { MoneyAmountCell } from "../../common/money-amount-cell" +import { PlaceholderCell } from "../../common/placeholder-cell" + +type ShippingPriceCellProps = { + isCalculated: boolean + price?: number | null + currencyCode: string +} + +export const ShippingPriceCell = ({ + price, + currencyCode, + isCalculated, +}: ShippingPriceCellProps) => { + if (isCalculated || !price) { + return + } + + return +} + +export const ShippingPriceHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.price")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/index.ts new file mode 100644 index 0000000000..8ee08b37d7 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/index.ts @@ -0,0 +1 @@ +export * from "./subtotal-requirement-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/subtotal-requirement-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/subtotal-requirement-cell.tsx new file mode 100644 index 0000000000..d9a5733bfa --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/shipping-option/subtotal-requirement-cell/subtotal-requirement-cell.tsx @@ -0,0 +1,43 @@ +import { ShippingOption } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" +import { MoneyAmountCell } from "../../common/money-amount-cell" +import { PlaceholderCell } from "../../common/placeholder-cell" + +type SubtotalType = "min" | "max" + +type SubtotalRequirementCellProps = { + type: SubtotalType + shippingOption: ShippingOption +} + +export const SubtotalRequirementCell = ( + props: SubtotalRequirementCellProps +) => { + const requirement = props.shippingOption.requirements?.find( + (r) => r.type === `${props.type}_subtotal` + ) + + if (!requirement) { + return + } + + return ( + + ) +} + +export const SubtotalRequirementHeader = ({ type }: { type: SubtotalType }) => { + const { t } = useTranslation() + + const header = + type === "min" ? t("fields.minSubtotal") : t("fields.maxSubtotal") + + return ( +
+ {header} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-shipping-option-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-shipping-option-table-columns.tsx new file mode 100644 index 0000000000..53085e90aa --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-shipping-option-table-columns.tsx @@ -0,0 +1,107 @@ +import { ShippingOption } from "@medusajs/medusa" +import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" + +import { + AdminOnlyCell, + AdminOnlyHeader, +} from "../../../components/table/table-cells/shipping-option/admin-only-cell" +import { + IsReturnCell, + IsReturnHeader, +} from "../../../components/table/table-cells/shipping-option/is-return-cell" +import { + PriceTypeCell, + PriceTypeHeader, +} from "../../../components/table/table-cells/shipping-option/price-type-cell" +import { + ShippingOptionCell, + ShippingOptionHeader, +} from "../../../components/table/table-cells/shipping-option/shipping-option-cell" +import { + ShippingPriceCell, + ShippingPriceHeader, +} from "../../../components/table/table-cells/shipping-option/shipping-price-cell/shipping-price-cell" +import { + SubtotalRequirementCell, + SubtotalRequirementHeader, +} from "../../../components/table/table-cells/shipping-option/subtotal-requirement-cell" + +const columnHelper = createColumnHelper() + +export const useShippingOptionTableColumns = () => { + return useMemo( + () => [ + columnHelper.accessor("name", { + header: () => , + cell: ({ getValue }) => , + }), + columnHelper.accessor("is_return", { + header: () => , + cell: (cell) => { + const value = cell.getValue() + + return + }, + }), + columnHelper.accessor("price_type", { + header: () => , + cell: ({ getValue }) => { + const type = getValue() + + return + }, + }), + columnHelper.accessor("price_incl_tax", { + header: () => , + cell: ({ getValue, row }) => { + const isCalculated = row.original.price_type === "calculated" + const amount = getValue() + const currencyCode = row.original.region!.currency_code + + return ( + + ) + }, + }), + columnHelper.display({ + id: "min_amount", + header: () => , + cell: ({ row }) => { + return ( + + ) + }, + }), + columnHelper.display({ + id: "max_amount", + header: () => , + cell: ({ row }) => { + return ( + + ) + }, + }), + columnHelper.accessor("admin_only", { + header: () => , + cell: (cell) => { + const value = cell.getValue() || false + + return + }, + }), + ], + [] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-shipping-option-table-filters.tsx similarity index 93% rename from packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-filters.tsx rename to packages/admin-next/dashboard/src/hooks/table/filters/use-shipping-option-table-filters.tsx index aa618865fa..aed7c43d98 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-filters.tsx +++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-shipping-option-table-filters.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next" -import { Filter } from "../../../../../components/table/data-table" +import { Filter } from "../../../components/table/data-table" export const useShippingOptionTableFilters = () => { const { t } = useTranslation() diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-shipping-option-table-query.tsx similarity index 93% rename from packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-query.tsx rename to packages/admin-next/dashboard/src/hooks/table/query/use-shipping-option-table-query.tsx index 348c370926..d3fd484471 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-query.tsx +++ b/packages/admin-next/dashboard/src/hooks/table/query/use-shipping-option-table-query.tsx @@ -1,5 +1,5 @@ import { AdminGetShippingOptionsParams } from "@medusajs/medusa" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { useQueryParams } from "../../use-query-params" type UseShippingOptionTableQueryProps = { regionId: string diff --git a/packages/admin-next/dashboard/src/lib/percentage-helpers.ts b/packages/admin-next/dashboard/src/lib/percentage-helpers.ts new file mode 100644 index 0000000000..6f7ba54a01 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/percentage-helpers.ts @@ -0,0 +1,27 @@ +const formatter = new Intl.NumberFormat(undefined, { + style: "percent", + minimumFractionDigits: 2, +}) + +/** + * Formats a number as a percentage + * @param value - The value to format + * @param isPercentageValue - Whether the value is already a percentage value (where `0` is 0%, `0.5` is 50%, `0.75` is 75%, etc). Defaults to false + * @returns The formatted percentage in the form of a localized string + * + * @example + * formatPercentage(0.5, true) // "50%" + * formatPercentage(50) // "50%" + */ +export const formatPercentage = ( + value?: number | null, + isPercentageValue = false +) => { + let val = value || 0 + + if (!isPercentageValue) { + val = val / 100 + } + + return formatter.format(val) +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index c5424203e1..7172a1354d 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -559,12 +559,34 @@ const router = createBrowserRouter([ }, children: [ { - index: true, - lazy: () => import("../../routes/taxes/views/tax-list"), + path: "", + lazy: () => import("../../routes/taxes/tax-list"), }, { path: ":id", - lazy: () => import("../../routes/taxes/views/tax-details"), + lazy: () => import("../../routes/taxes/tax-detail"), + handle: { + crumb: (data: AdminRegionsRes) => data.region.name, + }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/taxes/tax-edit"), + }, + { + path: "tax-rates/create", + lazy: () => import("../../routes/taxes/tax-rate-create"), + }, + { + path: "tax-rates/:rate_id/edit", + lazy: () => import("../../routes/taxes/tax-rate-edit"), + }, + { + path: "tax-rates/:rate_id/edit-overrides", + lazy: () => + import("../../routes/taxes/tax-rate-edit-overrides"), + }, + ], }, ], }, diff --git a/packages/admin-next/dashboard/src/routes/products/product-edit/components/edit-product-form/edit-product-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-edit/components/edit-product-form/edit-product-form.tsx index aff799ac51..04ddbd50e7 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-edit/components/edit-product-form/edit-product-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-edit/components/edit-product-form/edit-product-form.tsx @@ -62,8 +62,11 @@ export const EditProductForm = ({ product }: EditProductFormProps) => { return ( -
- + +
) } + +const ShippingOptionActions = ({ + shippingOption, +}: { + shippingOption: PricedShippingOption +}) => { + const { t } = useTranslation() + const prompt = usePrompt() + + const { mutateAsync } = useAdminDeleteShippingOption(shippingOption.id!) + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("regions.deleteShippingOptionWarning", { + name: shippingOption.name, + }), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + , + }, + ], + }, + { + actions: [ + { + label: t("actions.delete"), + onClick: handleDelete, + icon: , + }, + ], + }, + ]} + /> + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const base = useShippingOptionTableColumns() + + return useMemo( + () => [ + ...base, + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + return + }, + }), + ], + [base] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-columns.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-columns.tsx deleted file mode 100644 index 8c90709e07..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/use-shipping-option-table-columns.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { PencilSquare, Trash } from "@medusajs/icons" -import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing" -import { usePrompt } from "@medusajs/ui" -import { createColumnHelper } from "@tanstack/react-table" -import { useAdminDeleteShippingOption } from "medusa-react" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" -import { ActionMenu } from "../../../../../components/common/action-menu" -import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell" -import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" -import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell" - -const columnHelper = createColumnHelper() - -export const useShippingOptionColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - columnHelper.accessor("name", { - header: t("fields.name"), - cell: ({ getValue }) => ( -
- {getValue()} -
- ), - }), - columnHelper.accessor("is_return", { - header: t("fields.type"), - cell: (cell) => { - const value = cell.getValue() - - return value ? t("regions.return") : t("regions.outbound") - }, - }), - columnHelper.accessor("price_type", { - header: t("regions.priceType"), - cell: ({ getValue }) => { - const type = getValue() - - return type === "flat_rate" - ? t("regions.flatRate") - : t("regions.calculated") - }, - }), - columnHelper.accessor("price_incl_tax", { - header: t("fields.price"), - cell: ({ getValue, row }) => { - const isCalculated = row.original.price_type === "calculated" - - if (isCalculated) { - return - } - - const amount = getValue() - const currencyCode = row.original.region!.currency_code - - return - }, - }), - columnHelper.display({ - id: "min_amount", - header: () => ( -
- {t("fields.minSubtotal")} -
- ), - cell: ({ row }) => { - const minAmountReq = row.original.requirements?.find( - (r) => r.type === "min_subtotal" - ) - - if (!minAmountReq) { - return - } - - const amount = minAmountReq.amount - const currencyCode = row.original.region!.currency_code - - return - }, - }), - columnHelper.display({ - id: "max_amount", - header: () => ( -
- {t("fields.maxSubtotal")} -
- ), - cell: ({ row }) => { - const maxAmountReq = row.original.requirements?.find( - (r) => r.type === "max_subtotal" - ) - - if (!maxAmountReq) { - return - } - - const amount = maxAmountReq.amount - const currencyCode = row.original.region!.currency_code - - return - }, - }), - columnHelper.accessor("admin_only", { - header: t("fields.availability"), - cell: (cell) => { - const value = cell.getValue() - - return ( - - {value ? t("general.admin") : t("general.store")} - - ) - }, - }), - columnHelper.display({ - id: "actions", - cell: ({ row }) => { - return - }, - }), - ], - [t] - ) -} - -const ShippingOptionActions = ({ - shippingOption, -}: { - shippingOption: PricedShippingOption -}) => { - const { t } = useTranslation() - const prompt = usePrompt() - - const { mutateAsync } = useAdminDeleteShippingOption(shippingOption.id!) - - const handleDelete = async () => { - const res = await prompt({ - title: t("general.areYouSure"), - description: t("regions.deleteShippingOptionWarning", { - name: shippingOption.name, - }), - confirmText: t("actions.delete"), - cancelText: t("actions.cancel"), - }) - - if (!res) { - return - } - - await mutateAsync() - } - - return ( - , - }, - ], - }, - { - actions: [ - { - label: t("actions.delete"), - onClick: handleDelete, - icon: , - }, - ], - }, - ]} - /> - ) -} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts b/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts index 2e00f4fc67..f49ba72e67 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts @@ -1,4 +1,4 @@ -import { AdminProductsRes } from "@medusajs/medusa" +import { AdminRegionsRes } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { adminRegionKeys } from "medusa-react" import { LoaderFunctionArgs } from "react-router-dom" @@ -15,7 +15,7 @@ export const regionLoader = async ({ params }: LoaderFunctionArgs) => { const query = regionQuery(id!) return ( - queryClient.getQueryData>(query.queryKey) ?? + queryClient.getQueryData>(query.queryKey) ?? (await queryClient.fetchQuery(query)) ) } diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx index 3b8f68cb99..3f47372f13 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx @@ -1,14 +1,21 @@ import { useAdminRegion } from "medusa-react" -import { Outlet, useParams } from "react-router-dom" +import { Outlet, useLoaderData, useParams } from "react-router-dom" import { JsonViewSection } from "../../../components/common/json-view-section" import { RegionCountrySection } from "./components/region-country-section" import { RegionGeneralSection } from "./components/region-general-section" import { RegionShippingOptionSection } from "./components/region-shipping-option-section" +import { regionLoader } from "./loader" export const RegionDetail = () => { + const initialData = useLoaderData() as Awaited< + ReturnType + > + const { id } = useParams() - const { region, isLoading, isError, error } = useAdminRegion(id!) + const { region, isLoading, isError, error } = useAdminRegion(id!, { + initialData, + }) // TODO: Move to loading.tsx and set as Suspense fallback for the route if (isLoading || !region) { diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/index.ts b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/index.ts new file mode 100644 index 0000000000..1e406647de --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/index.ts @@ -0,0 +1 @@ +export * from "./override-chip" diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/override-chip.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/override-chip.tsx new file mode 100644 index 0000000000..3cd46dd22b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-chip/override-chip.tsx @@ -0,0 +1,24 @@ +import { XMarkMini } from "@medusajs/icons" +import { OverrideOption } from "../../types" + +type OverrideChipProps = { + override: OverrideOption + onRemove: (value: string) => void +} + +export const OverrideChip = ({ override, onRemove }: OverrideChipProps) => { + return ( +
+
+ {override.label} +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/index.ts b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/index.ts new file mode 100644 index 0000000000..cbe1f25826 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/index.ts @@ -0,0 +1 @@ +export * from "./override-grid" diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/override-grid.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/override-grid.tsx new file mode 100644 index 0000000000..c1fc79a9ea --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/components/override-grid/override-grid.tsx @@ -0,0 +1,42 @@ +import { Button } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { OverrideOption } from "../../types" +import { OverrideChip } from "../override-chip" + +type OverrideGridProps = { + overrides: OverrideOption[] + onRemove: (value: string) => void + onClear: () => void +} + +export const OverrideGrid = ({ + overrides, + onRemove, + onClear, +}: OverrideGridProps) => { + const { t } = useTranslation() + + if (!overrides.length) { + return null + } + + return ( +
+ {overrides.map((override) => ( + + ))} + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/index.ts b/packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/index.ts new file mode 100644 index 0000000000..d245a68c1e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/index.ts @@ -0,0 +1 @@ +export * from "./overrides-drawer" diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/overrides-drawer.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/overrides-drawer.tsx new file mode 100644 index 0000000000..271c20bd79 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/components/overrides-drawer/overrides-drawer.tsx @@ -0,0 +1,394 @@ +import { Product } from "@medusajs/medusa" +import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing" +import { Button } from "@medusajs/ui" +import { OnChangeFn, RowSelectionState } from "@tanstack/react-table" +import { + useAdminProductTypes, + useAdminProducts, + useAdminShippingOptions, +} from "medusa-react" +import { useState } from "react" +import { useTranslation } from "react-i18next" + +import { SplitView } from "../../../../../components/layout/split-view" +import { DataTable } from "../../../../../components/table/data-table" +import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters" +import { useShippingOptionTableFilters } from "../../../../../hooks/table/filters/use-shipping-option-table-filters" +import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query" +import { useShippingOptionTableQuery } from "../../../../../hooks/table/query/use-shipping-option-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { Override } from "../../constants" +import { useProductOverrideTableColumns } from "../../hooks/columns/use-product-override-table-columns" +import { useProductTypeOverrideTableColumns } from "../../hooks/columns/use-product-type-override-table-columns" +import { useShippingOptionOverrideTableColumns } from "../../hooks/columns/use-shipping-option-override-table-columns" +import { useProductTypeOverrideTableFilters } from "../../hooks/filters/use-product-type-override-table-filters" +import { useProductTypeOverrideTableQuery } from "../../hooks/query/use-product-type-override-table-query" +import { OverrideOption } from "../../types" + +const PAGE_SIZE = 50 + +const PRODUCT_PREFIX = "product" +const PRODUCT_TYPE_PREFIX = "product_type" +const SHIPPING_OPTION_PREFIX = "shipping_option" + +type OverrideProps = { + selected: OverrideOption[] + onSave: (options: OverrideOption[]) => void +} + +const initRowState = (selected: OverrideOption[] = []): RowSelectionState => { + return selected.reduce((acc, { value }) => { + acc[value] = true + return acc + }, {} as RowSelectionState) +} + +const OverrideFooter = ({ onSave }: { onSave: () => void }) => { + const { t } = useTranslation() + + return ( +
+ + + + +
+ ) +} + +const ProductOverrideTable = ({ selected = [], onSave }: OverrideProps) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useProductTableQuery({ + pageSize: PAGE_SIZE, + prefix: PRODUCT_PREFIX, + }) + const { products, count, isLoading, isError, error } = useAdminProducts( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const diff = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (diff.length === 0) { + return + } + + const addedProducts = (products?.filter((p) => diff.includes(p.id!)) ?? + []) as Product[] + + if (addedProducts.length > 0) { + const newOverrides = addedProducts.map((p) => ({ + label: p.title, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => + Object.keys(newState).includes(p.value) + ) + const update = Array.from(new Set([...filteredPrev, ...newOverrides])) + return update + }) + } + + setRowSelection(newState) + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useProductOverrideTableColumns() + const filters = useProductTableFilters() + + const { table } = useDataTable({ + data: (products ?? []) as Product[], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: PRODUCT_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
+ + +
+ ) +} + +const ProductTypeOverrideTable = ({ onSave, selected = [] }: OverrideProps) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useProductTypeOverrideTableQuery({ + pageSize: PAGE_SIZE, + prefix: PRODUCT_TYPE_PREFIX, + }) + const { product_types, count, isLoading, isError, error } = + useAdminProductTypes( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const diff = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (diff.length === 0) { + return + } + + const addedProductTypes = + product_types?.filter((p) => diff.includes(p.id!)) ?? [] + + if (addedProductTypes.length > 0) { + const newOverrides = addedProductTypes.map((p) => ({ + label: p.value, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => + Object.keys(newState).includes(p.value) + ) + const update = Array.from(new Set([...filteredPrev, ...newOverrides])) + return update + }) + } + + setRowSelection(newState) + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useProductTypeOverrideTableColumns() + const filters = useProductTypeOverrideTableFilters() + + const { table } = useDataTable({ + data: product_types ?? [], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: PRODUCT_TYPE_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
+ + +
+ ) +} + +const ShippingOptionOverrideTable = ({ + onSave, + selected = [], + regionId, +}: OverrideProps & { regionId: string }) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useShippingOptionTableQuery({ + regionId, + pageSize: PAGE_SIZE, + prefix: SHIPPING_OPTION_PREFIX, + }) + const { shipping_options, count, isLoading, isError, error } = + useAdminShippingOptions( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const diff = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (diff.length === 0) { + return + } + + const addedShippingOptions = + shipping_options?.filter((p) => diff.includes(p.id!)) ?? [] + + if (addedShippingOptions.length > 0) { + const newOverrides = addedShippingOptions.map((p) => ({ + label: p.name, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => + Object.keys(newState).includes(p.value) + ) + const update = Array.from(new Set([...filteredPrev, ...newOverrides])) + return update + }) + } + + setRowSelection(newState) + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useShippingOptionOverrideTableColumns() + const filters = useShippingOptionTableFilters() + + const { table } = useDataTable({ + data: (shipping_options ?? []) as unknown as PricedShippingOption[], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id!, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: SHIPPING_OPTION_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
+ + +
+ ) +} + +type OverrideTableProps = { + product: OverrideProps + productType: OverrideProps + shippingOption: OverrideProps + regionId: string + selected: Override | null +} + +export const OverrideTable = ({ + product, + productType, + shippingOption, + regionId, + selected, +}: OverrideTableProps) => { + switch (selected) { + case Override.PRODUCT: + return + case Override.PRODUCT_TYPE: + return + case Override.SHIPPING_OPTION: + return ( + + ) + default: + return null + } +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/constants.ts b/packages/admin-next/dashboard/src/routes/taxes/common/constants.ts new file mode 100644 index 0000000000..a3b64e64ea --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/constants.ts @@ -0,0 +1,5 @@ +export enum Override { + PRODUCT = "product", + PRODUCT_TYPE = "product_type", + SHIPPING_OPTION = "shipping_option", +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-override-table-columns.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-override-table-columns.tsx new file mode 100644 index 0000000000..83c74c5139 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-override-table-columns.tsx @@ -0,0 +1,46 @@ +import { Product } from "@medusajs/medusa" +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns" + +const columnHelper = createColumnHelper() + +export const useProductOverrideTableColumns = () => { + const base = useProductTableColumns() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + ...base, + ], + [base] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-type-override-table-columns.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-type-override-table-columns.tsx new file mode 100644 index 0000000000..3c653659c7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-product-type-override-table-columns.tsx @@ -0,0 +1,49 @@ +import { ProductType } from "@medusajs/medusa" +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +const columnHelper = createColumnHelper() + +export const useProductTypeOverrideTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.accessor("value", { + header: t("fields.value"), + cell: ({ getValue }) => getValue(), + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-shipping-option-override-table-columns.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-shipping-option-override-table-columns.tsx new file mode 100644 index 0000000000..541c024060 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/columns/use-shipping-option-override-table-columns.tsx @@ -0,0 +1,46 @@ +import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing" +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useShippingOptionTableColumns } from "../../../../../hooks/table/columns/use-shipping-option-table-columns" + +const columnHelper = createColumnHelper() + +export const useShippingOptionOverrideTableColumns = () => { + const base = useShippingOptionTableColumns() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + ...base, + ], + [base] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/hooks/filters/use-product-type-override-table-filters.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/filters/use-product-type-override-table-filters.tsx new file mode 100644 index 0000000000..3acaf64870 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/filters/use-product-type-override-table-filters.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useProductTypeOverrideTableFilters = () => { + const { t } = useTranslation() + + const filters: Filter[] = [ + { label: t("fields.createdAt"), key: "created_at" }, + { label: t("fields.updatedAt"), key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/hooks/query/use-product-type-override-table-query.tsx b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/query/use-product-type-override-table-query.tsx new file mode 100644 index 0000000000..163f069d56 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/hooks/query/use-product-type-override-table-query.tsx @@ -0,0 +1,29 @@ +import { AdminGetProductTypesParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useProductTypeOverrideTableQuery = ({ + pageSize = 50, + prefix, +}: { + pageSize?: number + prefix: string +}) => { + const raw = useQueryParams( + ["offset", "q", "order", "created_at", "updated_at"], + prefix + ) + + const searchParams: AdminGetProductTypesParams = { + limit: pageSize, + offset: raw.offset ? Number(raw.offset) : 0, + q: raw.q, + created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined, + updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined, + order: raw.order, + } + + return { + searchParams, + raw, + } +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/common/types.ts b/packages/admin-next/dashboard/src/routes/taxes/common/types.ts new file mode 100644 index 0000000000..814de0e349 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/common/types.ts @@ -0,0 +1,10 @@ +import { Override } from "./constants" + +export type OverrideOption = { + value: string + label: string +} + +export type OverrideState = { + [K in Override]: boolean +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/index.ts b/packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/index.ts deleted file mode 100644 index 342d8c2cd6..0000000000 --- a/packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./tax-details-section" diff --git a/packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/tax-details-section.tsx b/packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/tax-details-section.tsx deleted file mode 100644 index a027cd1804..0000000000 --- a/packages/admin-next/dashboard/src/routes/taxes/components/tax-details-section/tax-details-section.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Region } from "@medusajs/medusa" -import { Container, Heading, StatusBadge, Text, Tooltip } from "@medusajs/ui" -import { useTranslation } from "react-i18next" - -type Props = { - region: Region -} - -export const TaxDetailsSection = ({ region }: Props) => { - const { t } = useTranslation() - - return ( - -
- {region.name} -
- - {region.countries - .slice(0, 2) - .map((c) => c.display_name) - .join(", ")} - - {region.countries.length > 2 && ( - - {region.countries.slice(2).map((c) => ( -
  • {c.display_name}
  • - ))} - - } - > - - {t("general.plusCountMore", { - count: region.countries.length - 2, - })} - -
    - )} -
    -
    -
    - - Tax Provider - - - {region.tax_provider_id - ? region.tax_provider_id - : "System Tax Provider"} - -
    -
    - - Automatic Tax Calculation - - - {region.automatic_taxes - ? t("general.enabled") - : t("general.disabled")} - -
    -
    - - Gift Cards Taxable - - - {region.gift_cards_taxable - ? t("general.enabled") - : t("general.disabled")} - -
    -
    - ) -} diff --git a/packages/admin-next/dashboard/src/routes/taxes/components/tax-rates-section/tax-rates-section.tsx b/packages/admin-next/dashboard/src/routes/taxes/components/tax-rates-section/tax-rates-section.tsx deleted file mode 100644 index a2c318dbd1..0000000000 --- a/packages/admin-next/dashboard/src/routes/taxes/components/tax-rates-section/tax-rates-section.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { TaxRate } from "@medusajs/medusa" -import { Button, Container, Heading } from "@medusajs/ui" -import { - createColumnHelper, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" -import { useAdminTaxRates } from "medusa-react" -import { useMemo, useState } from "react" -import { useTranslation } from "react-i18next" -import { useParams } from "react-router-dom" -import { DebouncedSearch } from "../../../../components/common/debounced-search" - -export const TaxRatesSection = () => { - const { id } = useParams() - const [query, setQuery] = useState("") - - const { tax_rates, isLoading } = useAdminTaxRates( - { - region_id: id, - }, - { - enabled: !!id, - } - ) - - const columns = useColumns() - - const table = useReactTable({ - data: tax_rates ?? [], - columns, - getCoreRowModel: getCoreRowModel(), - }) - - return ( - -
    - Tax Rates -
    - - -
    -
    -
    - ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - columnHelper.accessor("name", { - header: t("fields.name"), - cell: ({ getValue }) => { - return {getValue()} - }, - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/index.ts new file mode 100644 index 0000000000..5455db9edf --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/index.ts @@ -0,0 +1 @@ +export * from "./tax-countries-section" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/tax-countries-section.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/tax-countries-section.tsx new file mode 100644 index 0000000000..61966e38c9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-countries-section/tax-countries-section.tsx @@ -0,0 +1,69 @@ +import { GlobeEurope } from "@medusajs/icons" +import { Region } from "@medusajs/medusa" +import { Container, Heading, Text, Tooltip } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +type TaxCountriesSectionProps = { + region: Region +} + +export const TaxCountriesSection = ({ region }: TaxCountriesSectionProps) => { + const { t } = useTranslation() + + const countries = region.countries ?? [] + + const firstCountries = countries.slice(0, 3) + const restCountries = countries.slice(3) + + return ( + +
    + {t("fields.countries")} +
    +
    +
    +
    + +
    +
    + {countries.length > 0 ? ( +
    + + {firstCountries.map((sc) => sc.display_name).join(", ")} + + {restCountries.length > 0 && ( + + {restCountries.map((sc) => ( +
  • {sc.display_name}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: restCountries.length, + })} + +
    + )} +
    + ) : ( + + {t("products.noSalesChannels")} + + )} +
    +
    + + {t("taxes.countries.taxCountriesHint")} + +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/index.ts new file mode 100644 index 0000000000..d578a89519 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/index.ts @@ -0,0 +1 @@ +export * from "./tax-default-tax-rate-section" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/tax-default-tax-rate-section.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/tax-default-tax-rate-section.tsx new file mode 100644 index 0000000000..9ac611b00d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-default-tax-rate-section/tax-default-tax-rate-section.tsx @@ -0,0 +1,56 @@ +import { PencilSquare } from "@medusajs/icons" +import { Region } from "@medusajs/medusa" +import { Container, Heading, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { formatPercentage } from "../../../../../lib/percentage-helpers" + +type TaxDefaultRateSectionProps = { + region: Region +} + +export const TaxDefaultTaxRateSection = ({ + region, +}: TaxDefaultRateSectionProps) => { + const { t } = useTranslation() + + const defaultTaxCode = region.tax_code + const defaultTaxRate = region.tax_rate + + return ( + +
    + {t("taxes.defaultRate.sectionTitle")} + , + }, + ], + }, + ]} + /> +
    +
    + + {t("fields.rate")} + + + {formatPercentage(defaultTaxRate)} + +
    +
    + + {t("fields.code")} + + + {defaultTaxCode || "-"} + +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/index.ts new file mode 100644 index 0000000000..01d3c1d183 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/index.ts @@ -0,0 +1 @@ +export * from "./tax-general-section" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/tax-general-section.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/tax-general-section.tsx new file mode 100644 index 0000000000..fe54cf11c1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-general-section/tax-general-section.tsx @@ -0,0 +1,93 @@ +import { PencilSquare } from "@medusajs/icons" +import { Region } from "@medusajs/medusa" +import { Badge, Container, Heading, StatusBadge, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { formatPercentage } from "../../../../../lib/percentage-helpers" + +type Props = { + region: Region +} + +export const TaxDetailsSection = ({ region }: Props) => { + const { t } = useTranslation() + + return ( + +
    + {region.name} + , + to: "edit", + }, + ], + }, + ]} + /> +
    +
    + + {t("taxes.settings.taxProviderLabel")} + + + {region.tax_provider_id + ? region.tax_provider_id + : t("taxes.settings.systemTaxProviderLabel")} + +
    +
    + + {t("taxes.settings.calculateTaxesAutomaticallyLabel")} + + + {region.automatic_taxes + ? t("general.enabled") + : t("general.disabled")} + +
    +
    + + {t("taxes.settings.applyTaxesOnGiftCardsLabel")} + + + {region.gift_cards_taxable + ? t("general.enabled") + : t("general.disabled")} + +
    +
    + + {t("fields.taxInclusivePricing")} + + + {region.includes_tax ? t("general.enabled") : t("general.disabled")} + +
    +
    + + {t("taxes.settings.defaultTaxRateLabel")} + +
    + + {formatPercentage(region.tax_rate)} + + {region.tax_code && {region.tax_code}} +
    +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/components/tax-rates-section/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/taxes/components/tax-rates-section/index.ts rename to packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rate-actions.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rate-actions.tsx new file mode 100644 index 0000000000..3029700aeb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rate-actions.tsx @@ -0,0 +1,61 @@ +import type { TaxRate } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +import { ExclamationCircle, PencilSquare, Trash } from "@medusajs/icons" +import { usePrompt } from "@medusajs/ui" +import { useAdminDeleteTaxRate } from "medusa-react" +import { ActionMenu } from "../../../../../components/common/action-menu" + +export const TaxRateActions = ({ taxRate }: { taxRate: TaxRate }) => { + const { t } = useTranslation() + const prompt = usePrompt() + + const { mutateAsync } = useAdminDeleteTaxRate(taxRate.id) + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("taxes.taxRate.deleteRateDescription", { + name: taxRate.name, + }), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + , + }, + { + label: t("taxes.taxRate.editOverridesAction"), + to: `tax-rates/${taxRate.id}/edit-overrides`, + icon: , + }, + ], + }, + { + actions: [ + { + label: t("actions.delete"), + icon: , + onClick: handleDelete, + }, + ], + }, + ]} + /> + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rates-section.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rates-section.tsx new file mode 100644 index 0000000000..a5e242b677 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/tax-rates-section.tsx @@ -0,0 +1,93 @@ +import { Plus } from "@medusajs/icons" +import { Region } from "@medusajs/medusa" +import { Container, Heading } from "@medusajs/ui" +import { useAdminTaxRates } from "medusa-react" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { DataTable } from "../../../../../components/table/data-table" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { useTaxRateTableColumns } from "./use-tax-rate-table-columns" +import { useTaxRateTableFilters } from "./use-tax-rate-table-filters" +import { useTaxRateTableQuery } from "./use-tax-rate-table-query" + +type TaxRatesSectionProps = { + region: Region +} + +const PAGE_SIZE = 10 + +export const TaxRatesSection = ({ region }: TaxRatesSectionProps) => { + const { t } = useTranslation() + + const { searchParams, raw } = useTaxRateTableQuery({ pageSize: PAGE_SIZE }) + const { tax_rates, count, isLoading, isError, error } = useAdminTaxRates( + { + region_id: region.id, + expand: ["products", "product_types", "shipping_options"], + fields: [ + "id", + "name", + "code", + "rate", + "created_at", + "updated_at", + "products.id", + "product_types.id", + "shipping_options.id", + ], + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const filters = useTaxRateTableFilters() + const columns = useTaxRateTableColumns() + + const { table } = useDataTable({ + data: tax_rates ?? [], + columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + +
    + {t("taxes.taxRate.sectionTitle")} + , + }, + ], + }, + ]} + /> +
    + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-columns.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-columns.tsx new file mode 100644 index 0000000000..60a794dbc1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-columns.tsx @@ -0,0 +1,127 @@ +import type { TaxRate } from "@medusajs/medusa" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" +import { formatPercentage } from "../../../../../lib/percentage-helpers" +import { TaxRateActions } from "./tax-rate-actions" + +const columnHelper = createColumnHelper() + +export const useTaxRateTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: ({ getValue }) => { + return ( +
    + {getValue()} +
    + ) + }, + }), + columnHelper.accessor("code", { + header: t("fields.code"), + cell: ({ getValue }) => { + return ( +
    + {getValue()} +
    + ) + }, + }), + columnHelper.accessor("rate", { + header: t("fields.rate"), + cell: ({ getValue }) => { + const rate = getValue() + + return ( +
    + {formatPercentage(rate)} +
    + ) + }, + }), + columnHelper.accessor("products", { + header: () => { + return ( +
    + + {t("taxes.taxRate.productOverridesHeader")} + +
    + ) + }, + cell: ({ getValue }) => { + const count = getValue()?.length + + if (!count) { + return + } + + return ( +
    + {count} +
    + ) + }, + }), + columnHelper.accessor("product_types", { + header: () => { + return ( +
    + + {t("taxes.taxRate.productTypeOverridesHeader")} + +
    + ) + }, + cell: ({ getValue }) => { + const count = getValue()?.length + + if (!count) { + return + } + + return ( +
    + {count} +
    + ) + }, + }), + columnHelper.accessor("shipping_options", { + header: () => { + return ( +
    + + {t("taxes.taxRate.shippingOptionOverridesHeader")} + +
    + ) + }, + cell: ({ getValue }) => { + const count = getValue()?.length + + if (!count) { + return + } + + return ( +
    + {count} +
    + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-filters.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-filters.tsx new file mode 100644 index 0000000000..415d2b0b52 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-filters.tsx @@ -0,0 +1,19 @@ +import { useTranslation } from "react-i18next" +import type { Filter } from "../../../../../components/table/data-table" + +export const useTaxRateTableFilters = () => { + const { t } = useTranslation() + + const dateFilters: Filter[] = [ + { label: t("fields.createdAt"), key: "created_at" }, + { label: t("fields.updatedAt"), key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + const filters = [...dateFilters] + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-query.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-query.tsx new file mode 100644 index 0000000000..7745062340 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/components/tax-rates-section/use-tax-rate-table-query.tsx @@ -0,0 +1,29 @@ +import { AdminGetTaxRatesParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useTaxRateTableQuery = ({ + pageSize = 10, + prefix, +}: { + pageSize?: number + prefix?: string +}) => { + const raw = useQueryParams( + ["offset", "q", "order", "created_at", "updated_at"], + prefix + ) + + const searchParams: AdminGetTaxRatesParams = { + limit: pageSize, + offset: raw.offset ? parseInt(raw.offset) : 0, + q: raw.q, + order: raw.order, + created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined, + updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined, + } + + return { + searchParams, + raw, + } +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/index.ts new file mode 100644 index 0000000000..6d75308596 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/index.ts @@ -0,0 +1,2 @@ +export { taxRegionLoader as loader } from "./loader" +export { TaxDetail as Component } from "./tax-detail" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/loader.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/loader.ts new file mode 100644 index 0000000000..1c455cbbd1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/loader.ts @@ -0,0 +1,21 @@ +import { AdminRegionsRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { adminRegionKeys } from "medusa-react" +import { LoaderFunctionArgs } from "react-router-dom" + +import { medusa, queryClient } from "../../../lib/medusa" + +const regionQuery = (id: string) => ({ + queryKey: adminRegionKeys.detail(id), + queryFn: async () => medusa.admin.regions.retrieve(id), +}) + +export const taxRegionLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = regionQuery(id!) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-detail/tax-detail.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/tax-detail.tsx new file mode 100644 index 0000000000..caf757a27d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-detail/tax-detail.tsx @@ -0,0 +1,45 @@ +import { useAdminRegion } from "medusa-react" +import { Outlet, useLoaderData, useParams } from "react-router-dom" +import { JsonViewSection } from "../../../components/common/json-view-section" +import { TaxCountriesSection } from "./components/tax-countries-section" +import { TaxDetailsSection } from "./components/tax-general-section" +import { TaxRatesSection } from "./components/tax-rates-section" +import { taxRegionLoader } from "./loader" + +export const TaxDetail = () => { + const { id } = useParams() + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { region, isLoading, isError, error } = useAdminRegion(id!, { + initialData, + }) + + if (isLoading || !region) { + return
    Loading...
    + } + + if (isError) { + throw error + } + + return ( +
    +
    +
    + + +
    + +
    + +
    +
    + +
    +
    + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/edit-tax-form.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/edit-tax-form.tsx new file mode 100644 index 0000000000..57012bf82e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/edit-tax-form.tsx @@ -0,0 +1,257 @@ +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { zodResolver } from "@hookform/resolvers/zod" +import { Region, TaxProvider } from "@medusajs/medusa" +import { Button, Input, Select, Switch } from "@medusajs/ui" +import { useAdminUpdateRegion } from "medusa-react" +import { useTranslation } from "react-i18next" +import { Form } from "../../../../../components/common/form" +import { PercentageInput } from "../../../../../components/common/percentage-input" +import { + RouteDrawer, + useRouteModal, +} from "../../../../../components/route-modal" +import { formatProvider } from "../../../../../lib/format-provider" + +type EditTaxRateFormProps = { + region: Region + taxProviders: TaxProvider[] +} + +const EditTaxSettingsSchema = z.object({ + tax_code: z.string().optional(), + tax_rate: z.union([z.string(), z.number()]).refine((value) => { + if (value === "") { + return false + } + + const num = Number(value) + + if (num >= 0 && num <= 100) { + return true + } + + return false + }, "Default tax rate must be a number between 0 and 100"), + includes_tax: z.boolean(), + automatic_taxes: z.boolean(), + gift_cards_taxable: z.boolean(), + tax_provider_id: z.string(), +}) + +const SYSTEM_PROVIDER_ID = "system" + +export const EditTaxSettingsForm = ({ + region, + taxProviders, +}: EditTaxRateFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + tax_code: region.tax_code || "", + tax_rate: region.tax_rate, + includes_tax: region.includes_tax, + automatic_taxes: region.automatic_taxes, + gift_cards_taxable: region.gift_cards_taxable, + tax_provider_id: region.tax_provider_id || SYSTEM_PROVIDER_ID, + }, + resolver: zodResolver(EditTaxSettingsSchema), + }) + + const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id) + + const handleSubmit = form.handleSubmit(async (data) => { + const { tax_provider_id, tax_rate, ...rest } = data + + await mutateAsync( + { + tax_provider_id: + tax_provider_id === SYSTEM_PROVIDER_ID ? null : tax_provider_id, + tax_rate: Number(tax_rate), + ...rest, + }, + { + onSuccess: () => { + handleSuccess() + }, + } + ) + }) + + return ( + + + +
    +
    + { + return ( + + + {t("taxes.settings.taxProviderLabel")} + + + + + + + ) + }} + /> + { + return ( + + + {t("taxes.settings.defaultTaxRateLabel")} + + + + + + + ) + }} + /> + { + return ( + + + {t("taxes.settings.defaultTaxCodeLabel")} + + + + + + + ) + }} + /> +
    + { + return ( + +
    +
    + + {t("taxes.settings.calculateTaxesAutomaticallyLabel")} + + + {t("taxes.settings.calculateTaxesAutomaticallyHint")} + +
    + + + +
    + +
    + ) + }} + /> + { + return ( + +
    +
    + + {t("taxes.settings.applyTaxesOnGiftCardsLabel")} + + + {t("taxes.settings.applyTaxesOnGiftCardsHint")} + +
    + + + +
    + +
    + ) + }} + /> + { + return ( + +
    +
    + + {t("fields.taxInclusivePricing")} + + + {t("regions.taxInclusiveHint")} + +
    + + + +
    + +
    + ) + }} + /> +
    +
    + +
    + + + + +
    +
    + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/index.ts new file mode 100644 index 0000000000..56956a34fa --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/components/edit-tax-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-tax-form" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-edit/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/index.ts new file mode 100644 index 0000000000..614c5e55d8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/index.ts @@ -0,0 +1 @@ +export { TaxEdit as Component } from "./tax-edit" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-edit/tax-edit.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/tax-edit.tsx new file mode 100644 index 0000000000..7e77eefc11 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-edit/tax-edit.tsx @@ -0,0 +1,40 @@ +import { Heading } from "@medusajs/ui" +import { useAdminRegion, useAdminStoreTaxProviders } from "medusa-react" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { RouteDrawer } from "../../../components/route-modal" +import { EditTaxSettingsForm } from "./components/edit-tax-form" + +export const TaxEdit = () => { + const { id } = useParams() + const { t } = useTranslation() + + const { region, isLoading, isError, error } = useAdminRegion(id!) + const { + tax_providers, + isLoading: isLoadingTaxProvider, + isError: isTaxProviderError, + error: taxProviderError, + } = useAdminStoreTaxProviders() + + const ready = !isLoading && region && !isLoadingTaxProvider && tax_providers + + if (isError) { + throw error + } + + if (isTaxProviderError) { + throw taxProviderError + } + + return ( + + + {t("taxes.settings.editTaxSettings")} + + {ready && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/index.ts new file mode 100644 index 0000000000..b5c05581cd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/index.ts @@ -0,0 +1 @@ +export * from "./tax-list-callout" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/tax-list-callout.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/tax-list-callout.tsx new file mode 100644 index 0000000000..bfa15baf49 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-callout/tax-list-callout.tsx @@ -0,0 +1,24 @@ +import { Button, Container, Heading, Text } from "@medusajs/ui" +import { Link } from "react-router-dom" + +export const TaxListCallout = () => { + return ( + +
    + Taxes + + Tax settings are specific to each region. To modify tax settings, + please select a region from the list. + +
    + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/index.ts new file mode 100644 index 0000000000..1f23c91012 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/index.ts @@ -0,0 +1 @@ +export * from "../tax-list-table/tax-list-table" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/tax-list-table.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/tax-list-table.tsx new file mode 100644 index 0000000000..b321fc56f7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-list/components/tax-list-table/tax-list-table.tsx @@ -0,0 +1,70 @@ +import { Container, Heading } from "@medusajs/ui" +import { useAdminRegions } from "medusa-react" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { DataTable } from "../../../../../components/table/data-table" +import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns" +import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters" +import { useRegionTableQuery } from "../../../../../hooks/table/query/use-region-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" + +const PAGE_SIZE = 20 + +export const TaxListTable = () => { + const { t } = useTranslation() + + const { searchParams, raw } = useRegionTableQuery({ + pageSize: PAGE_SIZE, + }) + const { regions, count, isLoading, isError, error } = useAdminRegions( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const columns = useColumns() + const filters = useRegionTableFilters() + + const { table } = useDataTable({ + data: regions || [], + columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + +
    + {t("regions.domain")} +
    + `${row.original.id}`} + orderBy={["name", "created_at", "updated_at"]} + /> +
    + ) +} + +const useColumns = () => { + const base = useRegionTableColumns() + + return useMemo(() => [...base], [base]) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/views/tax-list/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-list/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/taxes/views/tax-list/index.ts rename to packages/admin-next/dashboard/src/routes/taxes/tax-list/index.ts diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-list/tax-list.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-list/tax-list.tsx new file mode 100644 index 0000000000..80c5b79522 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-list/tax-list.tsx @@ -0,0 +1,11 @@ +import { TaxListCallout } from "./components/tax-list-callout" +import { TaxListTable } from "./components/tax-list-table" + +export const TaxList = () => { + return ( +
    + + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/create-tax-rate-form.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/create-tax-rate-form.tsx new file mode 100644 index 0000000000..3c744aa678 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/create-tax-rate-form.tsx @@ -0,0 +1,474 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, Heading, Input, Switch, Text, clx } from "@medusajs/ui" +import * as Collapsible from "@radix-ui/react-collapsible" +import { useAdminCreateTaxRate } from "medusa-react" +import { useState } from "react" +import { useForm, useWatch } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { useParams, useSearchParams } from "react-router-dom" +import { z } from "zod" + +import { Form } from "../../../../../components/common/form" +import { PercentageInput } from "../../../../../components/common/percentage-input" +import { SplitView } from "../../../../../components/layout/split-view" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { OverrideGrid } from "../../../common/components/override-grid" +import { OverrideTable } from "../../../common/components/overrides-drawer/overrides-drawer" +import { Override } from "../../../common/constants" +import { OverrideOption, OverrideState } from "../../../common/types" + +const CreateTaxRateSchema = z.object({ + name: z.string().min(1), + code: z.string().min(1), + rate: z.union([z.string(), z.number()]).refine((value) => { + if (value === "") { + return false + } + + const num = Number(value) + + if (num >= 0 && num <= 100) { + return true + } + + return false + }, "Tax rate must be a number between 0 and 100"), + products: z.array( + z.object({ + label: z.string(), + value: z.string(), + }) + ), + product_types: z.array( + z.object({ + label: z.string(), + value: z.string(), + }) + ), + shipping_options: z.array( + z.object({ + label: z.string(), + value: z.string(), + }) + ), +}) + +export const CreateTaxRateForm = () => { + const [open, setOpen] = useState(false) + + const [override, setOverride] = useState(null) + const [state, setState] = useState({ + [Override.PRODUCT]: false, + [Override.PRODUCT_TYPE]: false, + [Override.SHIPPING_OPTION]: false, + }) + + const [, setSearchParams] = useSearchParams() + + const { id } = useParams() + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + name: "", + code: "", + rate: "", + products: [], + product_types: [], + shipping_options: [], + }, + resolver: zodResolver(CreateTaxRateSchema), + }) + + const selectedProducts = useWatch({ + control: form.control, + name: "products", + defaultValue: [], + }) + const selectedProductTypes = useWatch({ + control: form.control, + name: "product_types", + defaultValue: [], + }) + + const selectedShippingOptions = useWatch({ + control: form.control, + name: "shipping_options", + defaultValue: [], + }) + + const { mutateAsync, isLoading } = useAdminCreateTaxRate() + + const handleSubmit = form.handleSubmit(async (data) => { + const { rate, products, product_types, shipping_options, ...rest } = data + + const productsPaylaod = state[Override.PRODUCT] + ? products.map((p) => p.value) + : undefined + + const productTypesPayload = state[Override.PRODUCT_TYPE] + ? product_types.map((p) => p.value) + : undefined + + const shippingOptionsPayload = state[Override.SHIPPING_OPTION] + ? shipping_options.map((p) => p.value) + : undefined + + await mutateAsync( + { + region_id: id!, + rate: Number(rate), + products: productsPaylaod, + product_types: productTypesPayload, + shipping_options: shippingOptionsPayload, + ...rest, + }, + { + onSuccess: () => { + handleSuccess() + }, + } + ) + }) + + const handleOpenDrawer = (override: Override) => { + setOverride(override) + setOpen(true) + } + + const handleSaveOverrides = (override: Override) => { + let field: "products" | "product_types" | "shipping_options" + + switch (override) { + case Override.PRODUCT: + field = "products" + break + case Override.PRODUCT_TYPE: + field = "product_types" + break + case Override.SHIPPING_OPTION: + field = "shipping_options" + break + } + + return (options: OverrideOption[]) => { + form.setValue(field, options, { + shouldDirty: true, + shouldTouch: true, + }) + + setOpen(false) + } + } + + const handleRemoveOverride = (override: Override) => { + let field: "products" | "product_types" | "shipping_options" + + switch (override) { + case Override.PRODUCT: + field = "products" + break + case Override.PRODUCT_TYPE: + field = "product_types" + break + case Override.SHIPPING_OPTION: + field = "shipping_options" + break + } + + return (value: string) => { + const current = form.getValues(field) + + const newValue = current.filter((c: OverrideOption) => c.value !== value) + + form.setValue(field, newValue, { + shouldDirty: true, + shouldTouch: true, + }) + } + } + + const handleClearOverrides = (override: Override) => { + let field: "products" | "product_types" | "shipping_options" + + switch (override) { + case Override.PRODUCT: + field = "products" + break + case Override.PRODUCT_TYPE: + field = "product_types" + break + case Override.SHIPPING_OPTION: + field = "shipping_options" + break + } + + return () => { + form.setValue(field, [], { + shouldDirty: true, + shouldTouch: true, + }) + } + } + + const handleStateChange = (override: Override) => { + return (enabled: boolean) => { + setState((prev) => ({ + ...prev, + [override]: enabled, + })) + } + } + + const handleOpenChange = (open: boolean) => { + if (!open) { + setOverride(null) + setSearchParams( + {}, + { + replace: true, + } + ) + } + + setOpen(open) + } + + return ( + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + {t("taxes.taxRate.createTaxRate")} + + {t("taxes.taxRate.createTaxRateHint")} + +
    +
    +
    + { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.rate")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.code")} + + + + + + ) + }} + /> +
    +
    +
    + +
    +
    + + {t("taxes.taxRate.productOverridesLabel")} + + + {t("taxes.taxRate.productOverridesHint")} + +
    + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + + {t("taxes.taxRate.productTypeOverridesLabel")} + + + {t("taxes.taxRate.productTypeOverridesHint")} + +
    + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + + {t("taxes.taxRate.shippingOptionOverridesLabel")} + + + {t("taxes.taxRate.shippingOptionOverridesHint")} + +
    + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/index.ts new file mode 100644 index 0000000000..d13cabc07e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/components/create-tax-rate-form/index.ts @@ -0,0 +1 @@ +export * from "./create-tax-rate-form" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/index.ts new file mode 100644 index 0000000000..e956477331 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/index.ts @@ -0,0 +1 @@ +export { TaxRateCreate as Component } from "./tax-rate-create" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/tax-rate-create.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/tax-rate-create.tsx new file mode 100644 index 0000000000..f33d0095e7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-create/tax-rate-create.tsx @@ -0,0 +1,10 @@ +import { RouteFocusModal } from "../../../components/route-modal" +import { CreateTaxRateForm } from "./components/create-tax-rate-form" + +export const TaxRateCreate = () => { + return ( + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/edit-tax-rate-overrides-form.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/edit-tax-rate-overrides-form.tsx new file mode 100644 index 0000000000..3664600fdf --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/edit-tax-rate-overrides-form.tsx @@ -0,0 +1,414 @@ +import { TaxRate } from "@medusajs/medusa" +import { Button, Heading, Switch, Text, clx } from "@medusajs/ui" +import * as Collapsible from "@radix-ui/react-collapsible" +import { useAdminUpdateTaxRate } from "medusa-react" +import { useState } from "react" +import { useForm, useWatch } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { z } from "zod" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useSearchParams } from "react-router-dom" +import { SplitView } from "../../../../../components/layout/split-view" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { OverrideGrid } from "../../../common/components/override-grid" +import { OverrideTable } from "../../../common/components/overrides-drawer/overrides-drawer" +import { Override } from "../../../common/constants" +import { OverrideOption, OverrideState } from "../../../common/types" + +type EditTaxRateOverridesFormProps = { + taxRate: TaxRate +} + +const EditTaxRateOverridesSchema = z.object({ + products: z.array( + z.object({ + label: z.string(), + value: z.string(), + }) + ), + product_types: z.array( + z.object({ + label: z.string(), + value: z.string(), + }) + ), + shipping_options: z.array( + z.object({ + label: z.string(), + value: z.string(), + }) + ), +}) + +export const EditTaxRateOverridesForm = ({ + taxRate, +}: EditTaxRateOverridesFormProps) => { + const [open, setOpen] = useState(false) + + const [override, setOverride] = useState(null) + const [state, setState] = useState({ + [Override.PRODUCT]: taxRate.products?.length > 0, + [Override.PRODUCT_TYPE]: taxRate.product_types?.length > 0, + [Override.SHIPPING_OPTION]: taxRate.shipping_options?.length > 0, + }) + + const [, setSearchParams] = useSearchParams() + + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + products: + taxRate.products?.map((p) => ({ + label: p.title, + value: p.id, + })) || [], + product_types: + taxRate.product_types?.map((pt) => ({ + label: pt.value, + value: pt.id, + })) || [], + shipping_options: + taxRate.shipping_options?.map((so) => ({ + label: so.name, + value: so.id, + })) || [], + }, + resolver: zodResolver(EditTaxRateOverridesSchema), + }) + + const selectedProducts = useWatch({ + control: form.control, + name: "products", + }) + const selectedProductTypes = useWatch({ + control: form.control, + name: "product_types", + }) + + const selectedShippingOptions = useWatch({ + control: form.control, + name: "shipping_options", + }) + + const { mutateAsync, isLoading } = useAdminUpdateTaxRate(taxRate.id) + const handleSubmit = form.handleSubmit(async (data) => { + const { products, product_types, shipping_options } = data + + const productsPaylaod = state[Override.PRODUCT] + ? products.map((p) => p.value) + : [] + + const productTypesPayload = state[Override.PRODUCT_TYPE] + ? product_types.map((p) => p.value) + : [] + + const shippingOptionsPayload = state[Override.SHIPPING_OPTION] + ? shipping_options.map((p) => p.value) + : [] + + await mutateAsync( + { + products: productsPaylaod, + product_types: productTypesPayload, + shipping_options: shippingOptionsPayload, + }, + { + onSuccess: () => { + handleSuccess() + }, + } + ) + }) + + const handleOpenDrawer = (override: Override) => { + setOverride(override) + setOpen(true) + } + + const handleSaveOverrides = (override: Override) => { + let field: "products" | "product_types" | "shipping_options" + + switch (override) { + case Override.PRODUCT: + field = "products" + break + case Override.PRODUCT_TYPE: + field = "product_types" + break + case Override.SHIPPING_OPTION: + field = "shipping_options" + break + } + + return (options: OverrideOption[]) => { + form.setValue(field, options, { + shouldDirty: true, + shouldTouch: true, + }) + + setOpen(false) + } + } + + const handleRemoveOverride = (override: Override) => { + let field: "products" | "product_types" | "shipping_options" + + switch (override) { + case Override.PRODUCT: + field = "products" + break + case Override.PRODUCT_TYPE: + field = "product_types" + break + case Override.SHIPPING_OPTION: + field = "shipping_options" + break + } + + return (value: string) => { + const current = form.getValues(field) + + const newValue = current.filter((c: OverrideOption) => c.value !== value) + + form.setValue(field, newValue, { + shouldDirty: true, + shouldTouch: true, + }) + } + } + + const handleClearOverrides = (override: Override) => { + let field: "products" | "product_types" | "shipping_options" + + switch (override) { + case Override.PRODUCT: + field = "products" + break + case Override.PRODUCT_TYPE: + field = "product_types" + break + case Override.SHIPPING_OPTION: + field = "shipping_options" + break + } + + return () => { + form.setValue(field, [], { + shouldDirty: true, + shouldTouch: true, + }) + } + } + + const handleStateChange = (override: Override) => { + return (enabled: boolean) => { + setState((prev) => ({ + ...prev, + [override]: enabled, + })) + } + } + + const handleOpenChange = (open: boolean) => { + if (!open) { + setOverride(null) + setSearchParams( + {}, + { + replace: true, + } + ) + } + + setOpen(open) + } + + return ( + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + {t("taxes.taxRate.editOverridesTitle")} + + {t("taxes.taxRate.editOverridesHint")} + +
    + +
    +
    + + {t("taxes.taxRate.productOverridesLabel")} + + + {t("taxes.taxRate.productOverridesHint")} + +
    + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + + {t("taxes.taxRate.productTypeOverridesLabel")} + + + {t("taxes.taxRate.productTypeOverridesHint")} + +
    + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + + {t("taxes.taxRate.shippingOptionOverridesLabel")} + + + {t("taxes.taxRate.shippingOptionOverridesHint")} + +
    + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/index.ts new file mode 100644 index 0000000000..5ed67b5511 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/components/edit-tax-rate-overrides-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-tax-rate-overrides-form" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/index.ts new file mode 100644 index 0000000000..1344dc3e28 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/index.ts @@ -0,0 +1 @@ +export { TaxRateEditOverrides as Component } from "./tax-rate-edit-overrides" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/tax-rate-edit-overrides.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/tax-rate-edit-overrides.tsx new file mode 100644 index 0000000000..e06d15e9d1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit-overrides/tax-rate-edit-overrides.tsx @@ -0,0 +1,24 @@ +import { useAdminTaxRate } from "medusa-react" +import { useParams } from "react-router-dom" +import { RouteFocusModal } from "../../../components/route-modal" +import { EditTaxRateOverridesForm } from "./components/edit-tax-rate-overrides-form" + +export const TaxRateEditOverrides = () => { + const { rate_id } = useParams() + + const { tax_rate, isLoading, isError, error } = useAdminTaxRate(rate_id!, { + expand: ["products", "shipping_options", "product_types"], + }) + + const ready = !isLoading && tax_rate + + if (isError) { + throw error + } + + return ( + + {ready && } + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/edit-tax-rate-form.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/edit-tax-rate-form.tsx new file mode 100644 index 0000000000..70703b029a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/edit-tax-rate-form.tsx @@ -0,0 +1,133 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { TaxRate } from "@medusajs/medusa" +import { Button, Input } from "@medusajs/ui" +import { useAdminUpdateTaxRate } from "medusa-react" +import { useTranslation } from "react-i18next" +import { Form } from "../../../../../components/common/form" +import { PercentageInput } from "../../../../../components/common/percentage-input" +import { + RouteDrawer, + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" + +type EditTaxRateFormProps = { + taxRate: TaxRate +} + +const EditTaxRateSchema = z.object({ + name: z.string().min(1), + code: z.string().min(1), + rate: z.union([z.string(), z.number()]).refine((value) => { + if (value === "") { + return true // we allow empty string and read it as null + } + + const num = Number(value) + + if (num >= 0 && num <= 100) { + return true + } + + return false + }, "Tax rate must be a number between 0 and 100"), +}) + +export const EditTaxRateForm = ({ taxRate }: EditTaxRateFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + name: taxRate.name, + code: taxRate.code || "", + rate: taxRate.rate || "", + }, + resolver: zodResolver(EditTaxRateSchema), + }) + + const { mutateAsync, isLoading } = useAdminUpdateTaxRate(taxRate.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + ...data, + rate: data.rate ? Number(data.rate) : null, + }, + { + onSuccess: () => { + handleSuccess() + }, + } + ) + }) + + return ( + +
    + + { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.rate")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.code")} + + + + + + ) + }} + /> + + +
    + + + + +
    +
    +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/index.ts new file mode 100644 index 0000000000..7181c7e4be --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/components/edit-tax-rate-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-tax-rate-form" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/index.ts b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/index.ts new file mode 100644 index 0000000000..ff668e0b3b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/index.ts @@ -0,0 +1 @@ +export { TaxRateEdit as Component } from "./tax-rate-edit" diff --git a/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/tax-rate-edit.tsx b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/tax-rate-edit.tsx new file mode 100644 index 0000000000..2e826f19ce --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/taxes/tax-rate-edit/tax-rate-edit.tsx @@ -0,0 +1,28 @@ +import { Heading } from "@medusajs/ui" +import { useAdminTaxRate } from "medusa-react" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { RouteDrawer } from "../../../components/route-modal" +import { EditTaxRateForm } from "./components/edit-tax-rate-form" + +export const TaxRateEdit = () => { + const { rate_id } = useParams() + const { t } = useTranslation() + + const { tax_rate, isLoading, isError, error } = useAdminTaxRate(rate_id!) + + const ready = !isLoading && tax_rate + + if (isError) { + throw error + } + + return ( + + + {t("taxes.taxRate.editTaxRate")} + + {ready && } + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/taxes/views/tax-details/index.ts b/packages/admin-next/dashboard/src/routes/taxes/views/tax-details/index.ts deleted file mode 100644 index b4c4ee2790..0000000000 --- a/packages/admin-next/dashboard/src/routes/taxes/views/tax-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TaxDetails as Component } from "./tax-details" diff --git a/packages/admin-next/dashboard/src/routes/taxes/views/tax-details/tax-details.tsx b/packages/admin-next/dashboard/src/routes/taxes/views/tax-details/tax-details.tsx deleted file mode 100644 index dfa2cfb698..0000000000 --- a/packages/admin-next/dashboard/src/routes/taxes/views/tax-details/tax-details.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useAdminRegion } from "medusa-react" -import { useParams } from "react-router-dom" -import { TaxDetailsSection } from "../../components/tax-details-section" -import { TaxRatesSection } from "../../components/tax-rates-section" - -export const TaxDetails = () => { - const { id } = useParams() - - const { region, isLoading, isError, error } = useAdminRegion(id!) - - if (isLoading) { - return
    Loading
    - } - - if (isError || !region) { - const err = error ? JSON.parse(JSON.stringify(error)) : null - return ( -
    - {(err as Error & { status: number })?.status === 404 ? ( -
    Not found
    - ) : ( -
    Something went wrong!
    - )} -
    - ) - } - - return ( -
    - - -
    - ) -} diff --git a/packages/admin-next/dashboard/src/routes/taxes/views/tax-list/tax-list.tsx b/packages/admin-next/dashboard/src/routes/taxes/views/tax-list/tax-list.tsx deleted file mode 100644 index bae54be245..0000000000 --- a/packages/admin-next/dashboard/src/routes/taxes/views/tax-list/tax-list.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useAdminRegions } from "medusa-react" -import { Link } from "react-router-dom" - -export const TaxList = () => { - const { regions, isLoading, isError, error } = useAdminRegions() - - if (isLoading) { - return
    Loading...
    - } - - if (isError || !regions) { - return
    Error
    - } - - return ( -
    - {regions.map((region) => { - return ( -
    - {region.name} -
    - ) - })} -
    - ) -} diff --git a/packages/generated/client-types/src/lib/models/AdminGetTaxRatesParams.ts b/packages/generated/client-types/src/lib/models/AdminGetTaxRatesParams.ts index 682301eef5..236e615c33 100644 --- a/packages/generated/client-types/src/lib/models/AdminGetTaxRatesParams.ts +++ b/packages/generated/client-types/src/lib/models/AdminGetTaxRatesParams.ts @@ -16,6 +16,69 @@ export interface AdminGetTaxRatesParams { * Filter by code. */ code?: string + /** + * Filter by a creation date range. + */ + created_at?: { + /** + * filter by dates less than this date + */ + lt?: string + /** + * filter by dates greater than this date + */ + gt?: string + /** + * filter by dates less than or equal to this date + */ + lte?: string + /** + * filter by dates greater than or equal to this date + */ + gte?: string + } + /** + * Filter by an update date range. + */ + updated_at?: { + /** + * filter by dates less than this date + */ + lt?: string + /** + * filter by dates greater than this date + */ + gt?: string + /** + * filter by dates less than or equal to this date + */ + lte?: string + /** + * filter by dates greater than or equal to this date + */ + gte?: string + } + /** + * Filter by a deletion date range. + */ + deleted_at?: { + /** + * filter by dates less than this date + */ + lt?: string + /** + * filter by dates greater than this date + */ + gt?: string + /** + * filter by dates less than or equal to this date + */ + lte?: string + /** + * filter by dates greater than or equal to this date + */ + gte?: string + } /** * Filter by Rate */ @@ -39,6 +102,14 @@ export interface AdminGetTaxRatesParams { */ gte?: number } + /** + * Term used to search tax rates by name. + */ + q?: string + /** + * A tax rate field to sort-order the retrieved tax rates by. + */ + order?: string /** * The number of tax rates to skip when retrieving the tax rates. */ diff --git a/packages/medusa/src/api/routes/admin/tax-rates/list-tax-rates.ts b/packages/medusa/src/api/routes/admin/tax-rates/list-tax-rates.ts index 7d82867a63..f0e38f0be2 100644 --- a/packages/medusa/src/api/routes/admin/tax-rates/list-tax-rates.ts +++ b/packages/medusa/src/api/routes/admin/tax-rates/list-tax-rates.ts @@ -1,13 +1,23 @@ -import { IsArray, IsNumber, IsOptional, IsString } from "class-validator" -import { getListConfig, pickByConfig } from "./utils/get-query-config" +import { + IsArray, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" import { identity, omit, pickBy } from "lodash" +import { getListConfig, pickByConfig } from "./utils/get-query-config" -import { IsType } from "../../../../utils/validators/is-type" -import { NumericalComparisonOperator } from "../../../../types/common" +import { isDefined } from "@medusajs/utils" +import { Type } from "class-transformer" import { TaxRate } from "../../../.." import { TaxRateService } from "../../../../services" -import { Type } from "class-transformer" +import { + DateComparisonOperator, + NumericalComparisonOperator, +} from "../../../../types/common" import { validator } from "../../../../utils/validator" +import { IsType } from "../../../../utils/validators/is-type" /** * @oas [get] /admin/tax-rates @@ -30,6 +40,72 @@ import { validator } from "../../../../utils/validator" * type: string * - (query) code {string} Filter by code. * - in: query + * name: created_at + * description: Filter by a creation date range. + * schema: + * type: object + * properties: + * lt: + * type: string + * description: filter by dates less than this date + * format: date + * gt: + * type: string + * description: filter by dates greater than this date + * format: date + * lte: + * type: string + * description: filter by dates less than or equal to this date + * format: date + * gte: + * type: string + * description: filter by dates greater than or equal to this date + * format: date + * - in: query + * name: updated_at + * description: Filter by an update date range. + * schema: + * type: object + * properties: + * lt: + * type: string + * description: filter by dates less than this date + * format: date + * gt: + * type: string + * description: filter by dates greater than this date + * format: date + * lte: + * type: string + * description: filter by dates less than or equal to this date + * format: date + * gte: + * type: string + * description: filter by dates greater than or equal to this date + * format: date + * - in: query + * name: deleted_at + * description: Filter by a deletion date range. + * schema: + * type: object + * properties: + * lt: + * type: string + * description: filter by dates less than this date + * format: date + * gt: + * type: string + * description: filter by dates greater than this date + * format: date + * lte: + * type: string + * description: filter by dates less than or equal to this date + * format: date + * gte: + * type: string + * description: filter by dates greater than or equal to this date + * format: date + * - in: query * name: rate * style: form * explode: false @@ -51,6 +127,8 @@ import { validator } from "../../../../utils/validator" * gte: * type: number * description: filter by rates greater than or equal to this number + * - (query) q {string} Term used to search tax rates by name. + * - (query) order {string} A tax rate field to sort-order the retrieved tax rates by. * - (query) offset=0 {integer} The number of tax rates to skip when retrieving the tax rates. * - (query) limit=50 {integer} Limit the number of tax rates returned. * - in: query @@ -151,7 +229,29 @@ export default async (req, res) => { const rateService: TaxRateService = req.scope.resolve("taxRateService") - const listConfig = getListConfig() + const order = value.order + let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined + if (isDefined(order)) { + let orderField = order + if (order.startsWith("-")) { + const [, field] = order.split("-") + orderField = field + orderBy = { [field]: "DESC" } + } else { + orderBy = { [order]: "ASC" } + } + } else { + const defaultOrder: string = "created_at" + orderBy = { [defaultOrder]: "DESC" } + } + + const listConfig = getListConfig( + value.fields as (keyof TaxRate)[], + value.expand, + value.limit, + value.offset, + orderBy + ) const filterableFields = omit(value, [ "limit", @@ -234,4 +334,39 @@ export class AdminGetTaxRatesParams { @IsArray() @IsOptional() fields?: string[] + + @IsOptional() + @IsString() + order?: string + + /** + * Date filters to apply on the tax rates' `update_at` date. + */ + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + updated_at?: DateComparisonOperator + + /** + * Date filters to apply on the customer tax rates' `created_at` date. + */ + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + created_at?: DateComparisonOperator + + /** + * Date filters to apply on the tax rates' `deleted_at` date. + */ + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + deleted_at?: DateComparisonOperator + + /** + * Term used to search tax rates by name. + */ + @IsOptional() + @IsString() + q?: string } diff --git a/packages/medusa/src/services/tax-rate.ts b/packages/medusa/src/services/tax-rate.ts index 3ff82a8e75..8488742c5e 100644 --- a/packages/medusa/src/services/tax-rate.ts +++ b/packages/medusa/src/services/tax-rate.ts @@ -1,5 +1,7 @@ +import { promiseAll } from "@medusajs/utils" import { isDefined, MedusaError } from "medusa-core-utils" -import { EntityManager, In } from "typeorm" +import { EntityManager, ILike, In } from "typeorm" +import { TransactionBaseService } from "../interfaces" import { ProductTaxRate, ProductTypeTaxRate, @@ -18,8 +20,6 @@ import { UpdateTaxRateInput, } from "../types/tax-rate" import { buildQuery, PostgresError } from "../utils" -import { TransactionBaseService } from "../interfaces" -import { promiseAll } from "@medusajs/utils" class TaxRateService extends TransactionBaseService { protected readonly productService_: ProductService @@ -49,7 +49,20 @@ class TaxRateService extends TransactionBaseService { const taxRateRepo = this.activeManager_.withRepository( this.taxRateRepository_ ) + + let q: string | undefined + + if (selector.q) { + q = selector.q + delete selector.q + } + const query = buildQuery(selector, config) + + if (q) { + query.where["name"] = ILike(`%${q}%`) + } + return await taxRateRepo.findWithResolution(query) } @@ -60,7 +73,20 @@ class TaxRateService extends TransactionBaseService { const taxRateRepo = this.activeManager_.withRepository( this.taxRateRepository_ ) + + let q: string | undefined + + if (selector.q) { + q = selector.q + delete selector.q + } + const query = buildQuery(selector, config) + + if (q) { + query.where["name"] = ILike(`%${q}%`) + } + return await taxRateRepo.findAndCountWithResolution(query) } diff --git a/packages/medusa/src/types/tax-rate.ts b/packages/medusa/src/types/tax-rate.ts index 7be9ba4e2a..f51e029762 100644 --- a/packages/medusa/src/types/tax-rate.ts +++ b/packages/medusa/src/types/tax-rate.ts @@ -12,6 +12,7 @@ export type FilterableTaxRateProps = { created_at?: Date | DateComparisonOperator updated_at?: Date | DateComparisonOperator deleted_at?: Date | DateComparisonOperator + q?: string } export type UpdateTaxRateInput = {