feat: Add tax regions table (#6983)

This commit is contained in:
Oli Juhl
2024-04-07 15:02:48 +02:00
committed by GitHub
parent 44bcde92c8
commit 31b07aea3d
69 changed files with 381 additions and 2923 deletions

View File

@@ -854,7 +854,7 @@
}
},
"taxes": {
"domain": "Taxes",
"domain": "Tax Regions",
"countries": {
"taxCountriesHint": "Tax settings apply to the listed countries."
},

View File

@@ -0,0 +1,53 @@
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
import { client } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import {
AdminTaxRegionResponse,
AdminTaxRegionListResponse,
} from "@medusajs/types"
const TAX_REGIONS_QUERY_KEY = "tax_regions" as const
const taxRegionsQueryKeys = queryKeysFactory(TAX_REGIONS_QUERY_KEY)
export const useTaxRegion = (
id: string,
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<
AdminTaxRegionResponse,
Error,
AdminTaxRegionResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: taxRegionsQueryKeys.detail(id),
queryFn: async () => client.taxes.retrieveTaxRegion(id, query),
...options,
})
return { ...data, ...rest }
}
export const useTaxRegions = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<
AdminTaxRegionListResponse,
Error,
AdminTaxRegionListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => client.taxes.listTaxRegions(query),
queryKey: taxRegionsQueryKeys.list(query),
...options,
})
return { ...data, ...rest }
}

View File

@@ -0,0 +1,32 @@
import { useQueryParams } from "../../use-query-params"
type UseTaxRegionTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useTaxRegionTableQuery = ({
prefix,
pageSize = 20,
}: UseTaxRegionTableQueryProps) => {
const queryObject = useQueryParams(
["offset", "q", "order", "created_at", "updated_at"],
prefix
)
const { offset, q, order, created_at, updated_at } = queryObject
const searchParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
q,
}
return {
searchParams,
raw: queryObject,
}
}

View File

@@ -6,15 +6,16 @@ import { collections } from "./collections"
import { currencies } from "./currencies"
import { customers } from "./customers"
import { invites } from "./invites"
import { payments } from "./payments"
import { productTypes } from "./product-types"
import { products } from "./products"
import { payments } from "./payments"
import { promotions } from "./promotions"
import { regions } from "./regions"
import { salesChannels } from "./sales-channels"
import { stockLocations } from "./stock-locations"
import { stores } from "./stores"
import { tags } from "./tags"
import { taxes } from "./taxes"
import { users } from "./users"
import { workflowExecutions } from "./workflow-executions"
@@ -33,6 +34,7 @@ export const client = {
tags: tags,
users: users,
regions: regions,
taxes: taxes,
invites: invites,
products: products,
productTypes: productTypes,

View File

@@ -0,0 +1,18 @@
import {
AdminTaxRegionListResponse,
AdminTaxRegionResponse,
} from "@medusajs/types"
import { getRequest } from "./common"
async function retrieveTaxRegion(id: string, query?: Record<string, any>) {
return getRequest<AdminTaxRegionResponse>(`/admin/tax-regions/${id}`, query)
}
async function listTaxRegions(query?: Record<string, any>) {
return getRequest<AdminTaxRegionListResponse>(`/admin/tax-regions`, query)
}
export const taxes = {
retrieveTaxRegion,
listTaxRegions,
}

View File

@@ -5,9 +5,6 @@ import type {
AdminDraftOrdersRes,
AdminGiftCardsRes,
AdminOrdersRes,
AdminRegionsRes,
AdminSalesChannelsRes,
AdminUserRes,
} from "@medusajs/medusa"
import { Outlet, RouteObject } from "react-router-dom"

View File

@@ -512,6 +512,20 @@ export const v2Routes: RouteObject[] = [
},
],
},
{
path: "taxes",
element: <Outlet />,
handle: {
crumb: () => "Taxes",
},
children: [
{
path: "",
lazy: () => import("../../v2-routes/taxes/tax-region-list"),
children: [],
},
],
},
],
},
],

View File

@@ -1 +0,0 @@
export * from "./override-chip"

View File

@@ -1,24 +0,0 @@
import { XMarkMini } from "@medusajs/icons"
import { OverrideOption } from "../../types"
type OverrideChipProps = {
override: OverrideOption
onRemove: (value: string) => void
}
export const OverrideChip = ({ override, onRemove }: OverrideChipProps) => {
return (
<div className="bg-ui-bg-field shadow-borders-base transition-fg hover:bg-ui-bg-field-hover flex h-7 items-center overflow-hidden rounded-md">
<div className="txt-compact-small-plus flex h-full select-none items-center justify-center px-2 py-0.5">
{override.label}
</div>
<button
type="button"
onClick={() => onRemove(override.value)}
className="focus-visible:bg-ui-bg-field-hover transition-fg hover:bg-ui-bg-field-hover flex h-full w-7 items-center justify-center border-l outline-none"
>
<XMarkMini className="text-ui-fg-muted" />
</button>
</div>
)
}

View File

@@ -1 +0,0 @@
export * from "./override-grid"

View File

@@ -1,42 +0,0 @@
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 (
<div className="flex flex-wrap gap-2">
{overrides.map((override) => (
<OverrideChip
key={override.value}
override={override}
onRemove={onRemove}
/>
))}
<Button
variant="transparent"
size="small"
className="text-ui-fg-muted hover:text-ui-fg-subtle"
onClick={onClear}
>
{t("actions.clearAll")}
</Button>
</div>
)
}

View File

@@ -1 +0,0 @@
export * from "./overrides-drawer"

View File

@@ -1,394 +0,0 @@
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 (
<div className="flex items-center justify-end gap-x-2 border-t p-4">
<SplitView.Close type="button" asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</SplitView.Close>
<Button size="small" type="button" onClick={onSave}>
{t("actions.save")}
</Button>
</div>
)
}
const ProductOverrideTable = ({ selected = [], onSave }: OverrideProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
initRowState(selected)
)
const [intermediate, setIntermediate] = useState<OverrideOption[]>(selected)
const { searchParams, raw } = useProductTableQuery({
pageSize: PAGE_SIZE,
prefix: PRODUCT_PREFIX,
})
const { products, count, isLoading, isError, error } = useAdminProducts(
{
...searchParams,
},
{
keepPreviousData: true,
}
)
const updater: OnChangeFn<RowSelectionState> = (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 (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
queryObject={raw}
pagination
search
filters={filters}
isLoading={isLoading}
layout="fill"
orderBy={["title", "created_at", "updated_at"]}
prefix={PRODUCT_PREFIX}
/>
<OverrideFooter onSave={handleSave} />
</div>
)
}
const ProductTypeOverrideTable = ({ onSave, selected = [] }: OverrideProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
initRowState(selected)
)
const [intermediate, setIntermediate] = useState<OverrideOption[]>(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<RowSelectionState> = (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 (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
queryObject={raw}
pagination
search
filters={filters}
isLoading={isLoading}
layout="fill"
orderBy={["value", "created_at", "updated_at"]}
prefix={PRODUCT_PREFIX}
/>
<OverrideFooter onSave={handleSave} />
</div>
)
}
const ShippingOptionOverrideTable = ({
onSave,
selected = [],
regionId,
}: OverrideProps & { regionId: string }) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
initRowState(selected)
)
const [intermediate, setIntermediate] = useState<OverrideOption[]>(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<RowSelectionState> = (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 (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
queryObject={raw}
pagination
search
filters={filters}
isLoading={isLoading}
layout="fill"
orderBy={["title", "created_at", "updated_at"]}
prefix={SHIPPING_OPTION_PREFIX}
/>
<OverrideFooter onSave={handleSave} />
</div>
)
}
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 <ProductOverrideTable {...product} />
case Override.PRODUCT_TYPE:
return <ProductTypeOverrideTable {...productType} />
case Override.SHIPPING_OPTION:
return (
<ShippingOptionOverrideTable {...shippingOption} regionId={regionId} />
)
default:
return null
}
}

View File

@@ -1,5 +0,0 @@
export enum Override {
PRODUCT = "product",
PRODUCT_TYPE = "product_type",
SHIPPING_OPTION = "shipping_option",
}

View File

@@ -1,46 +0,0 @@
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<Product>()
export const useProductOverrideTableColumns = () => {
const base = useProductTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
...base,
],
[base]
)
}

View File

@@ -1,49 +0,0 @@
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<ProductType>()
export const useProductTypeOverrideTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
columnHelper.accessor("value", {
header: t("fields.value"),
cell: ({ getValue }) => getValue(),
}),
],
[t]
)
}

View File

@@ -1,46 +0,0 @@
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<PricedShippingOption>()
export const useShippingOptionOverrideTableColumns = () => {
const base = useShippingOptionTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
...base,
],
[base]
)
}

View File

@@ -1,17 +0,0 @@
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
}

View File

@@ -1,29 +0,0 @@
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,
}
}

View File

@@ -1,10 +0,0 @@
import { Override } from "./constants"
export type OverrideOption = {
value: string
label: string
}
export type OverrideState = {
[K in Override]: boolean
}

View File

@@ -1 +0,0 @@
export * from "./tax-countries-section"

View File

@@ -1,69 +0,0 @@
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 (
<Container className="flex flex-col gap-y-4 px-6 py-4">
<div className="flex items-center justify-between">
<Heading level="h2">{t("fields.countries")}</Heading>
</div>
<div className="grid grid-cols-[28px_1fr] items-center gap-x-3">
<div className="bg-ui-bg-base shadow-borders-base flex size-7 items-center justify-center rounded-md">
<div className="bg-ui-bg-component flex size-6 items-center justify-center rounded-[4px]">
<GlobeEurope className="text-ui-fg-subtle" />
</div>
</div>
{countries.length > 0 ? (
<div className="flex items-center gap-x-1">
<Text size="small" leading="compact">
{firstCountries.map((sc) => sc.display_name).join(", ")}
</Text>
{restCountries.length > 0 && (
<Tooltip
content={
<ul>
{restCountries.map((sc) => (
<li key={sc.id}>{sc.display_name}</li>
))}
</ul>
}
>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
{t("general.plusCountMore", {
count: restCountries.length,
})}
</Text>
</Tooltip>
)}
</div>
) : (
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{t("products.noSalesChannels")}
</Text>
)}
</div>
<div>
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{t("taxes.countries.taxCountriesHint")}
</Text>
</div>
</Container>
)
}

View File

@@ -1 +0,0 @@
export * from "./tax-default-tax-rate-section"

View File

@@ -1,56 +0,0 @@
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 (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("taxes.defaultRate.sectionTitle")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
to: "tax-rates/default/edit",
icon: <PencilSquare />,
},
],
},
]}
/>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("fields.rate")}
</Text>
<Text size="small" leading="compact">
{formatPercentage(defaultTaxRate)}
</Text>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("fields.code")}
</Text>
<Text size="small" leading="compact">
{defaultTaxCode || "-"}
</Text>
</div>
</Container>
)
}

View File

@@ -1 +0,0 @@
export * from "./tax-general-section"

View File

@@ -1,93 +0,0 @@
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 (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading>{region.name}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: "edit",
},
],
},
]}
/>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("taxes.settings.taxProviderLabel")}
</Text>
<Text size="small" leading="compact">
{region.tax_provider_id
? region.tax_provider_id
: t("taxes.settings.systemTaxProviderLabel")}
</Text>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("taxes.settings.calculateTaxesAutomaticallyLabel")}
</Text>
<StatusBadge
color={region.automatic_taxes ? "green" : "grey"}
className="w-fit"
>
{region.automatic_taxes
? t("general.enabled")
: t("general.disabled")}
</StatusBadge>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("taxes.settings.applyTaxesOnGiftCardsLabel")}
</Text>
<StatusBadge
color={region.gift_cards_taxable ? "green" : "grey"}
className="w-fit"
>
{region.gift_cards_taxable
? t("general.enabled")
: t("general.disabled")}
</StatusBadge>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("fields.taxInclusivePricing")}
</Text>
<StatusBadge
color={region.includes_tax ? "green" : "grey"}
className="w-fit"
>
{region.includes_tax ? t("general.enabled") : t("general.disabled")}
</StatusBadge>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("taxes.settings.defaultTaxRateLabel")}
</Text>
<div className="flex items-center gap-x-2">
<Text size="small" leading="compact">
{formatPercentage(region.tax_rate)}
</Text>
{region.tax_code && <Badge size="2xsmall">{region.tax_code}</Badge>}
</div>
</div>
</Container>
)
}

View File

@@ -1 +0,0 @@
export * from "./tax-rates-section"

View File

@@ -1,61 +0,0 @@
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 (
<ActionMenu
groups={[
{
actions: [
{
label: t("taxes.taxRate.editRateAction"),
to: `tax-rates/${taxRate.id}/edit`,
icon: <PencilSquare />,
},
{
label: t("taxes.taxRate.editOverridesAction"),
to: `tax-rates/${taxRate.id}/edit-overrides`,
icon: <ExclamationCircle />,
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
)
}

View File

@@ -1,93 +0,0 @@
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 (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("taxes.taxRate.sectionTitle")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.create"),
to: "tax-rates/create",
icon: <Plus />,
},
],
},
]}
/>
</div>
<DataTable
table={table}
isLoading={isLoading}
count={count}
columns={columns}
pageSize={PAGE_SIZE}
queryObject={raw}
filters={filters}
pagination
search
orderBy={["name", "code", "rate", "created_at", "updated_at"]}
/>
</Container>
)
}

View File

@@ -1,127 +0,0 @@
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<TaxRate>()
export const useTaxRateTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("name", {
header: t("fields.name"),
cell: ({ getValue }) => {
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">{getValue()}</span>
</div>
)
},
}),
columnHelper.accessor("code", {
header: t("fields.code"),
cell: ({ getValue }) => {
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">{getValue()}</span>
</div>
)
},
}),
columnHelper.accessor("rate", {
header: t("fields.rate"),
cell: ({ getValue }) => {
const rate = getValue()
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">{formatPercentage(rate)}</span>
</div>
)
},
}),
columnHelper.accessor("products", {
header: () => {
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">
{t("taxes.taxRate.productOverridesHeader")}
</span>
</div>
)
},
cell: ({ getValue }) => {
const count = getValue()?.length
if (!count) {
return <PlaceholderCell />
}
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">{count}</span>
</div>
)
},
}),
columnHelper.accessor("product_types", {
header: () => {
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">
{t("taxes.taxRate.productTypeOverridesHeader")}
</span>
</div>
)
},
cell: ({ getValue }) => {
const count = getValue()?.length
if (!count) {
return <PlaceholderCell />
}
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">{count}</span>
</div>
)
},
}),
columnHelper.accessor("shipping_options", {
header: () => {
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">
{t("taxes.taxRate.shippingOptionOverridesHeader")}
</span>
</div>
)
},
cell: ({ getValue }) => {
const count = getValue()?.length
if (!count) {
return <PlaceholderCell />
}
return (
<div className="flex items-center overflow-hidden">
<span className="truncate">{count}</span>
</div>
)
},
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => <TaxRateActions taxRate={row.original} />,
}),
],
[t]
)
}

View File

@@ -1,19 +0,0 @@
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
}

View File

@@ -1,29 +0,0 @@
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,
}
}

View File

@@ -1,2 +0,0 @@
export { taxRegionLoader as loader } from "./loader"
export { TaxDetail as Component } from "./tax-detail"

View File

@@ -1,21 +0,0 @@
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<Response<AdminRegionsRes>>(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}

View File

@@ -1,45 +0,0 @@
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<typeof taxRegionLoader>
>
const { region, isLoading, isError, error } = useAdminRegion(id!, {
initialData,
})
if (isLoading || !region) {
return <div>Loading...</div>
}
if (isError) {
throw error
}
return (
<div className="flex flex-col gap-y-2">
<div className="grid grid-cols-1 gap-x-4 xl:grid-cols-[1fr,400px]">
<div className="flex flex-col gap-y-2">
<TaxDetailsSection region={region} />
<TaxRatesSection region={region} />
<div className="flex flex-col gap-y-2 xl:hidden">
<TaxCountriesSection region={region} />
</div>
<JsonViewSection data={region} />
</div>
<div className="hidden flex-col gap-y-2 xl:flex">
<TaxCountriesSection region={region} />
</div>
</div>
<Outlet />
</div>
)
}

View File

@@ -1,257 +0,0 @@
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<z.infer<typeof EditTaxSettingsSchema>>({
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 (
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<RouteDrawer.Body>
<div className="flex flex-col gap-y-8">
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
name="tax_provider_id"
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>
{t("taxes.settings.taxProviderLabel")}
</Form.Label>
<Form.Control>
<Select {...field} onValueChange={onChange}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Item value={SYSTEM_PROVIDER_ID}>
{t("taxes.settings.systemTaxProviderLabel")}
</Select.Item>
{taxProviders.map((tp) => (
<Select.Item key={tp.id} value={tp.id}>
{formatProvider(tp.id)}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="tax_rate"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("taxes.settings.defaultTaxRateLabel")}
</Form.Label>
<Form.Control>
<PercentageInput {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="tax_code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("taxes.settings.defaultTaxCodeLabel")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<Form.Field
control={form.control}
name="automatic_taxes"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div className="grid grid-cols-[1fr_32px] items-start gap-4">
<div className="flex flex-col gap-y-1">
<Form.Label>
{t("taxes.settings.calculateTaxesAutomaticallyLabel")}
</Form.Label>
<Form.Hint className="text-pretty">
{t("taxes.settings.calculateTaxesAutomaticallyHint")}
</Form.Hint>
</div>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="gift_cards_taxable"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div className="grid grid-cols-[1fr_32px] items-start gap-4">
<div className="flex flex-col gap-y-1">
<Form.Label>
{t("taxes.settings.applyTaxesOnGiftCardsLabel")}
</Form.Label>
<Form.Hint className="text-pretty">
{t("taxes.settings.applyTaxesOnGiftCardsHint")}
</Form.Hint>
</div>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="includes_tax"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div className="grid grid-cols-[1fr_32px] items-start gap-4">
<div className="flex flex-col gap-y-1">
<Form.Label>
{t("fields.taxInclusivePricing")}
</Form.Label>
<Form.Hint className="text-pretty">
{t("regions.taxInclusiveHint")}
</Form.Hint>
</div>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center gap-x-2">
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
)
}

View File

@@ -1 +0,0 @@
export * from "./edit-tax-form"

View File

@@ -1 +0,0 @@
export { TaxEdit as Component } from "./tax-edit"

View File

@@ -1,40 +0,0 @@
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 (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("taxes.settings.editTaxSettings")}</Heading>
</RouteDrawer.Header>
{ready && (
<EditTaxSettingsForm region={region} taxProviders={tax_providers} />
)}
</RouteDrawer>
)
}

View File

@@ -1 +0,0 @@
export * from "./tax-list-callout"

View File

@@ -1,24 +0,0 @@
import { Button, Container, Heading, Text } from "@medusajs/ui"
import { Link } from "react-router-dom"
export const TaxListCallout = () => {
return (
<Container className="flex items-center justify-between gap-x-3 px-6 py-4">
<div>
<Heading>Taxes</Heading>
<Text size="small" className="text-ui-fg-subtle text-pretty">
Tax settings are specific to each region. To modify tax settings,
please select a region from the list.
</Text>
</div>
<Button
size="small"
variant="secondary"
asChild
className="shrink-0 whitespace-nowrap"
>
<Link to="/settings/regions">Go to Region settings</Link>
</Button>
</Container>
)
}

View File

@@ -1 +0,0 @@
export * from "../tax-list-table/tax-list-table"

View File

@@ -1,70 +0,0 @@
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 (
<Container className="divide-y p-0">
<div className="px-6 py-4">
<Heading level="h2">{t("regions.domain")}</Heading>
</div>
<DataTable
table={table}
columns={columns}
filters={filters}
isLoading={isLoading}
queryObject={raw}
pageSize={PAGE_SIZE}
count={count}
pagination
search
navigateTo={(row) => `${row.original.id}`}
orderBy={["name", "created_at", "updated_at"]}
/>
</Container>
)
}
const useColumns = () => {
const base = useRegionTableColumns()
return useMemo(() => [...base], [base])
}

View File

@@ -1 +0,0 @@
export { TaxList as Component } from "./tax-list"

View File

@@ -1,11 +0,0 @@
import { TaxListCallout } from "./components/tax-list-callout"
import { TaxListTable } from "./components/tax-list-table"
export const TaxList = () => {
return (
<div className="flex flex-col gap-y-2">
<TaxListCallout />
<TaxListTable />
</div>
)
}

View File

@@ -1,474 +0,0 @@
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<Override | null>(null)
const [state, setState] = useState<OverrideState>({
[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<z.infer<typeof CreateTaxRateSchema>>({
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 (
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-hidden">
<SplitView open={open} onOpenChange={handleOpenChange}>
<SplitView.Content>
<div
className={clx(
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
)}
id="form-section"
>
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("taxes.taxRate.createTaxRate")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.createTaxRateHint")}
</Text>
</div>
<div className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="rate"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.rate")}</Form.Label>
<Form.Control>
<PercentageInput {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.code")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
<div className="bg-ui-border-base h-px w-full" />
<Collapsible.Root
open={state[Override.PRODUCT]}
onOpenChange={handleStateChange(Override.PRODUCT)}
>
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
<div className="flex flex-col">
<Text size="small" weight="plus" leading="compact">
{t("taxes.taxRate.productOverridesLabel")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.productOverridesHint")}
</Text>
</div>
<Collapsible.Trigger asChild>
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
</Collapsible.Trigger>
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-4 pt-4">
<OverrideGrid
overrides={selectedProducts}
onClear={handleClearOverrides(Override.PRODUCT)}
onRemove={handleRemoveOverride(Override.PRODUCT)}
/>
<div className="flex items-center justify-end">
<Button
size="small"
type="button"
variant="secondary"
onClick={() => handleOpenDrawer(Override.PRODUCT)}
>
{t("taxes.taxRate.addProductOverridesAction")}
</Button>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<div className="bg-ui-border-base h-px w-full" />
<Collapsible.Root
open={state[Override.PRODUCT_TYPE]}
onOpenChange={handleStateChange(Override.PRODUCT_TYPE)}
>
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
<div className="flex flex-col">
<Text size="small" weight="plus" leading="compact">
{t("taxes.taxRate.productTypeOverridesLabel")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.productTypeOverridesHint")}
</Text>
</div>
<Collapsible.Trigger asChild>
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
</Collapsible.Trigger>
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-4 pt-4">
<OverrideGrid
overrides={selectedProductTypes}
onClear={handleClearOverrides(Override.PRODUCT_TYPE)}
onRemove={handleRemoveOverride(Override.PRODUCT_TYPE)}
/>
<div className="flex items-center justify-end">
<Button
size="small"
type="button"
variant="secondary"
onClick={() =>
handleOpenDrawer(Override.PRODUCT_TYPE)
}
>
{t("taxes.taxRate.addProductTypeOverridesAction")}
</Button>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<div className="bg-ui-border-base h-px w-full" />
<Collapsible.Root
open={state[Override.SHIPPING_OPTION]}
onOpenChange={handleStateChange(Override.SHIPPING_OPTION)}
>
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
<div className="flex flex-col">
<Text size="small" weight="plus" leading="compact">
{t("taxes.taxRate.shippingOptionOverridesLabel")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.shippingOptionOverridesHint")}
</Text>
</div>
<Collapsible.Trigger asChild>
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
</Collapsible.Trigger>
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-4 pt-4">
<OverrideGrid
overrides={selectedShippingOptions}
onClear={handleClearOverrides(
Override.SHIPPING_OPTION
)}
onRemove={handleRemoveOverride(
Override.SHIPPING_OPTION
)}
/>
<div className="flex items-center justify-end">
<Button
size="small"
type="button"
variant="secondary"
onClick={() =>
handleOpenDrawer(Override.SHIPPING_OPTION)
}
>
{t(
"taxes.taxRate.addShippingOptionOverridesAction"
)}
</Button>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
</div>
</div>
</SplitView.Content>
<SplitView.Drawer>
<OverrideTable
product={{
selected: selectedProducts,
onSave: handleSaveOverrides(Override.PRODUCT),
}}
productType={{
selected: selectedProductTypes,
onSave: handleSaveOverrides(Override.PRODUCT_TYPE),
}}
shippingOption={{
selected: selectedShippingOptions,
onSave: handleSaveOverrides(Override.SHIPPING_OPTION),
}}
regionId={id!}
selected={override}
/>
</SplitView.Drawer>
</SplitView>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}

View File

@@ -1 +0,0 @@
export { TaxRateCreate as Component } from "./tax-rate-create"

View File

@@ -1,10 +0,0 @@
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateTaxRateForm } from "./components/create-tax-rate-form"
export const TaxRateCreate = () => {
return (
<RouteFocusModal>
<CreateTaxRateForm />
</RouteFocusModal>
)
}

View File

@@ -1,414 +0,0 @@
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<Override | null>(null)
const [state, setState] = useState<OverrideState>({
[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<z.infer<typeof EditTaxRateOverridesSchema>>({
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 (
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-hidden">
<SplitView open={open} onOpenChange={handleOpenChange}>
<SplitView.Content>
<div
className={clx(
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
)}
>
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("taxes.taxRate.editOverridesTitle")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.editOverridesHint")}
</Text>
</div>
<Collapsible.Root
open={state[Override.PRODUCT]}
onOpenChange={handleStateChange(Override.PRODUCT)}
>
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
<div className="flex flex-col">
<Text size="small" weight="plus" leading="compact">
{t("taxes.taxRate.productOverridesLabel")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.productOverridesHint")}
</Text>
</div>
<Collapsible.Trigger asChild>
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
</Collapsible.Trigger>
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-4 pt-4">
<OverrideGrid
overrides={selectedProducts}
onClear={handleClearOverrides(Override.PRODUCT)}
onRemove={handleRemoveOverride(Override.PRODUCT)}
/>
<div className="flex items-center justify-end">
<Button
size="small"
type="button"
variant="secondary"
onClick={() => handleOpenDrawer(Override.PRODUCT)}
>
{t("taxes.taxRate.addProductOverridesAction")}
</Button>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<div className="bg-ui-border-base h-px w-full" />
<Collapsible.Root
open={state[Override.PRODUCT_TYPE]}
onOpenChange={handleStateChange(Override.PRODUCT_TYPE)}
>
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
<div className="flex flex-col">
<Text size="small" weight="plus" leading="compact">
{t("taxes.taxRate.productTypeOverridesLabel")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.productTypeOverridesHint")}
</Text>
</div>
<Collapsible.Trigger asChild>
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
</Collapsible.Trigger>
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-4 pt-4">
<OverrideGrid
overrides={selectedProductTypes}
onClear={handleClearOverrides(Override.PRODUCT_TYPE)}
onRemove={handleRemoveOverride(Override.PRODUCT_TYPE)}
/>
<div className="flex items-center justify-end">
<Button
size="small"
type="button"
variant="secondary"
onClick={() =>
handleOpenDrawer(Override.PRODUCT_TYPE)
}
>
{t("taxes.taxRate.addProductTypeOverridesAction")}
</Button>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<div className="bg-ui-border-base h-px w-full" />
<Collapsible.Root
open={state[Override.SHIPPING_OPTION]}
onOpenChange={handleStateChange(Override.SHIPPING_OPTION)}
>
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
<div className="flex flex-col">
<Text size="small" weight="plus" leading="compact">
{t("taxes.taxRate.shippingOptionOverridesLabel")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("taxes.taxRate.shippingOptionOverridesHint")}
</Text>
</div>
<Collapsible.Trigger asChild>
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
</Collapsible.Trigger>
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-4 pt-4">
<OverrideGrid
overrides={selectedShippingOptions}
onClear={handleClearOverrides(
Override.SHIPPING_OPTION
)}
onRemove={handleRemoveOverride(
Override.SHIPPING_OPTION
)}
/>
<div className="flex items-center justify-end">
<Button
size="small"
type="button"
variant="secondary"
onClick={() =>
handleOpenDrawer(Override.SHIPPING_OPTION)
}
>
{t(
"taxes.taxRate.addShippingOptionOverridesAction"
)}
</Button>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
</div>
</div>
</SplitView.Content>
<SplitView.Drawer>
<OverrideTable
product={{
selected: selectedProducts,
onSave: handleSaveOverrides(Override.PRODUCT),
}}
productType={{
selected: selectedProductTypes,
onSave: handleSaveOverrides(Override.PRODUCT_TYPE),
}}
shippingOption={{
selected: selectedShippingOptions,
onSave: handleSaveOverrides(Override.SHIPPING_OPTION),
}}
regionId={taxRate.region_id}
selected={override}
/>
</SplitView.Drawer>
</SplitView>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}

View File

@@ -1 +0,0 @@
export { TaxRateEditOverrides as Component } from "./tax-rate-edit-overrides"

View File

@@ -1,24 +0,0 @@
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 (
<RouteFocusModal>
{ready && <EditTaxRateOverridesForm taxRate={tax_rate} />}
</RouteFocusModal>
)
}

View File

@@ -1,133 +0,0 @@
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<z.infer<typeof EditTaxRateSchema>>({
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 (
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<RouteDrawer.Body className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
name="name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="rate"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.rate")}</Form.Label>
<Form.Control>
<PercentageInput {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.code")}</Form.Label>
<Form.Control>
<Input type="number" {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteFocusModal.Form>
)
}

View File

@@ -1 +0,0 @@
export * from "./edit-tax-rate-form"

View File

@@ -1 +0,0 @@
export { TaxRateEdit as Component } from "./tax-rate-edit"

View File

@@ -1,28 +0,0 @@
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 (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("taxes.taxRate.editTaxRate")}</Heading>
</RouteDrawer.Header>
{ready && <EditTaxRateForm taxRate={tax_rate} />}
</RouteDrawer>
)
}

View File

@@ -0,0 +1 @@
export * from "./tax-region-list-table"

View File

@@ -0,0 +1,125 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { AdminTaxRegionResponse } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useDeleteRegion } from "../../../../../hooks/api/regions"
import { useTaxRegions } from "../../../../../hooks/api/tax-regions"
import { useTaxRegionTableQuery } from "../../../../../hooks/table/query/use-tax-region-table-query copy"
import { t } from "i18next"
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
const PAGE_SIZE = 20
export const TaxRegionListTable = () => {
const { t } = useTranslation()
const { searchParams, raw } = useTaxRegionTableQuery({ pageSize: PAGE_SIZE })
const { tax_regions, count, isLoading, isError, error } = useTaxRegions({
...searchParams,
})
const filters = useRegionTableFilters()
const columns = useColumns()
const { table } = useDataTable({
data: tax_regions ?? [],
columns,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
if (isError) {
throw error
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("taxes.domain")}</Heading>
<Link to="/settings/taxes/create">
<Button size="small" variant="secondary">
{t("actions.create")}
</Button>
</Link>
</div>
<DataTable
table={table}
columns={columns}
count={count}
pageSize={PAGE_SIZE}
isLoading={isLoading}
filters={filters}
orderBy={["name", "created_at", "updated_at"]}
navigateTo={(row) => `${row.original.id}`}
pagination
search
queryObject={raw}
/>
</Container>
)
}
const TaxRegionActions = ({
taxRegion,
}: {
taxRegion: AdminTaxRegionResponse["tax_region"]
}) => {
const { t } = useTranslation()
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
to: `/settings/taxes/${taxRegion.id}/edit`,
icon: <PencilSquare />,
},
],
},
]}
/>
)
}
const columnHelper = createColumnHelper<AdminTaxRegionResponse["tax_region"]>()
const useColumns = () => {
return useMemo(
() => [
columnHelper.accessor("name", {
header: t("fields.code"),
cell: ({ getValue }) => (
<div className="flex size-full items-center">
<span className="truncate">{getValue()}</span>
</div>
),
}),
columnHelper.accessor("created_at", {
header: t("fields.created"),
cell: ({ getValue }) => {
const date = getValue()
return <DateCell date={date} />
},
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => {
return <TaxRegionActions taxRegion={row.original as any} />
},
}),
],
[t]
)
}

View File

@@ -0,0 +1 @@
export { TaxRegionsList as Component } from "./tax-region-list"

View File

@@ -0,0 +1,11 @@
import { Outlet } from "react-router-dom"
import { TaxRegionListTable } from "./components/region-list-table"
export const TaxRegionsList = () => {
return (
<div className="flex flex-col gap-y-2">
<TaxRegionListTable />
<Outlet />
</div>
)
}

View File

@@ -1,10 +1,11 @@
import * as QueryConfig from "./query-config"
import { AdminGetTaxRegionsParams, AdminPostTaxRegionsReq } from "./validators"
import { transformBody, transformQuery } from "../../../api/middlewares"
import { AdminCreateTaxRegion, AdminGetTaxRegionsParams } from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
{
@@ -15,13 +16,19 @@ export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
{
method: "POST",
matcher: "/admin/tax-regions",
middlewares: [transformBody(AdminPostTaxRegionsReq)],
middlewares: [
validateAndTransformBody(AdminCreateTaxRegion),
validateAndTransformQuery(
AdminGetTaxRegionsParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: "GET",
matcher: "/admin/tax-regions",
middlewares: [
transformQuery(
validateAndTransformQuery(
AdminGetTaxRegionsParams,
QueryConfig.listTransformQueryConfig
),

View File

@@ -1,6 +1,4 @@
export const defaultAdminTaxRegionRelations = []
export const allowedAdminTaxRegionRelations = []
export const defaultAdminTaxRegionFields = [
export const defaults = [
"id",
"country_code",
"province_code",
@@ -14,13 +12,11 @@ export const defaultAdminTaxRegionFields = [
]
export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminTaxRegionFields,
defaultRelations: defaultAdminTaxRegionRelations,
allowedRelations: allowedAdminTaxRegionRelations,
defaults,
isList: false,
}
export const listTransformQueryConfig = {
defaultLimit: 20,
...retrieveTransformQueryConfig,
isList: true,
}

View File

@@ -1,14 +1,16 @@
import { createTaxRegionsWorkflow } from "@medusajs/core-flows"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { defaultAdminTaxRegionFields } from "./query-config"
import { AdminPostTaxRegionsReq } from "./validators"
import { AdminCreateTaxRegionType } from "./validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostTaxRegionsReq>,
req: AuthenticatedMedusaRequest<AdminCreateTaxRegionType>,
res: MedusaResponse
) => {
const { result, errors } = await createTaxRegionsWorkflow(req.scope).run({
@@ -30,10 +32,35 @@ export const POST = async (
const query = remoteQueryObjectFromString({
entryPoint: "tax_region",
variables: { id: result[0].id },
fields: defaultAdminTaxRegionFields,
fields: req.remoteQueryConfig.fields,
})
const [taxRegion] = await remoteQuery(query)
res.status(200).json({ tax_region: taxRegion })
}
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const { rows: tax_regions, metadata } = await remoteQuery(
remoteQueryObjectFromString({
entryPoint: "tax_regions",
variables: {
filters: req.filterableFields,
...req.remoteQueryConfig.pagination,
},
fields: req.remoteQueryConfig.fields,
})
)
res.status(200).json({
tax_regions,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}

View File

@@ -1,127 +1,45 @@
import { OperatorMap } from "@medusajs/types"
import { Type } from "class-transformer"
import { z } from "zod"
import {
IsNumber,
IsObject,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { extendedFindParamsMixin, FindParams } from "../../../types/common"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../utils/validators"
export class AdminGetTaxRegionsTaxRegionParams extends FindParams {}
export const AdminGetTaxRegionParams = createSelectParams()
export class AdminGetTaxRegionsParams extends extendedFindParamsMixin({
limit: 50,
export type AdminCreateTaxRegionsParams = z.infer<
typeof AdminGetTaxRegionsParams
>
export const AdminGetTaxRegionsParams = createFindParams({
limit: 20,
offset: 0,
}) {
/**
* Search parameter for regions.
*/
@IsString({ each: true })
@IsOptional()
id?: string | string[]
}).merge(
z.object({
id: z.union([z.string(), z.array(z.string())]).optional(),
country_code: z.union([z.string(), z.array(z.string())]).optional(),
province_code: z.union([z.string(), z.array(z.string())]).optional(),
parent_id: z.union([z.string(), z.array(z.string())]).optional(),
created_by: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
$and: z.lazy(() => AdminGetTaxRegionsParams.array()).optional(),
$or: z.lazy(() => AdminGetTaxRegionsParams.array()).optional(),
})
)
/**
* Filter by country code.
*/
@IsString({ each: true })
@IsOptional()
country_code?: string | string[]
/**
* Filter by province code
*/
@IsString({ each: true })
@IsOptional()
province_code?: string | string[]
/**
* Filter by id of parent Tax Region.
*/
@IsString({ each: true })
@IsOptional()
parent_id?: string | string[]
/**
* Filter by who created the Tax Region.
*/
@IsString({ each: true })
@IsOptional()
created_by?: string | string[]
/**
* Date filters to apply on the Tax Regions' `created_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
created_at?: OperatorMap<string>
/**
* Date filters to apply on the Tax Regions' `updated_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
updated_at?: OperatorMap<string>
/**
* Date filters to apply on the Tax Regions' `deleted_at` date.
*/
@ValidateNested()
@IsOptional()
@Type(() => OperatorMapValidator)
deleted_at?: OperatorMap<string>
// Additional filters from BaseFilterable
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetTaxRegionsParams)
$and?: AdminGetTaxRegionsParams[]
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetTaxRegionsParams)
$or?: AdminGetTaxRegionsParams[]
}
class CreateDefaultTaxRate {
@IsOptional()
@IsNumber()
rate?: number | null
@IsOptional()
@IsString()
code?: string | null
@IsString()
name: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
export class AdminPostTaxRegionsReq {
@IsString()
country_code: string
@IsString()
@IsOptional()
province_code?: string
@IsString()
@IsOptional()
parent_id?: string
@ValidateNested()
@IsOptional()
@Type(() => CreateDefaultTaxRate)
default_tax_rate?: CreateDefaultTaxRate
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
export type AdminCreateTaxRegionType = z.infer<typeof AdminCreateTaxRegion>
export const AdminCreateTaxRegion = z.object({
country_code: z.string(),
province_code: z.string().optional(),
parent_id: z.string().optional(),
default_tax_rate: z
.object({
rate: z.number().optional(),
code: z.string().optional(),
name: z.string(),
metadata: z.record(z.unknown()).optional(),
})
.optional(),
metadata: z.record(z.unknown()).optional(),
})

View File

@@ -3,3 +3,4 @@ export * from "./fulfillment"
export * from "./pricing"
export * from "./sales-channel"
export * from "./stock-locations"
export * from "./tax"

View File

@@ -0,0 +1 @@
export * from "./tax-regions"

View File

@@ -0,0 +1,33 @@
import { PaginatedResponse } from "../../../common"
/**
* @experimental
*/
interface TaxRegionResponse {
id: string
rate: number | null
code: string | null
name: string
metadata: Record<string, unknown> | null
tax_region_id: string
is_combinable: boolean
is_default: boolean
created_at: string | Date
updated_at: string | Date
deleted_at: Date | null
created_by: string | null
}
/**
* @experimental
*/
export interface AdminTaxRegionResponse {
tax_region: TaxRegionResponse
}
/**
* @experimental
*/
export interface AdminTaxRegionListResponse extends PaginatedResponse {
tax_regions: TaxRegionResponse[]
}

View File

@@ -0,0 +1 @@
export * from "./admin"