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 = {