feat: Add tax regions table (#6983)
This commit is contained in:
@@ -854,7 +854,7 @@
|
||||
}
|
||||
},
|
||||
"taxes": {
|
||||
"domain": "Taxes",
|
||||
"domain": "Tax Regions",
|
||||
"countries": {
|
||||
"taxCountriesHint": "Tax settings apply to the listed countries."
|
||||
},
|
||||
|
||||
53
packages/admin-next/dashboard/src/hooks/api/tax-regions.tsx
Normal file
53
packages/admin-next/dashboard/src/hooks/api/tax-regions.tsx
Normal 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 }
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
18
packages/admin-next/dashboard/src/lib/client/taxes.ts
Normal file
18
packages/admin-next/dashboard/src/lib/client/taxes.ts
Normal 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,
|
||||
}
|
||||
@@ -5,9 +5,6 @@ import type {
|
||||
AdminDraftOrdersRes,
|
||||
AdminGiftCardsRes,
|
||||
AdminOrdersRes,
|
||||
AdminRegionsRes,
|
||||
AdminSalesChannelsRes,
|
||||
AdminUserRes,
|
||||
} from "@medusajs/medusa"
|
||||
import { Outlet, RouteObject } from "react-router-dom"
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./override-chip"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./override-grid"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./overrides-drawer"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum Override {
|
||||
PRODUCT = "product",
|
||||
PRODUCT_TYPE = "product_type",
|
||||
SHIPPING_OPTION = "shipping_option",
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Override } from "./constants"
|
||||
|
||||
export type OverrideOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type OverrideState = {
|
||||
[K in Override]: boolean
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./tax-countries-section"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./tax-default-tax-rate-section"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./tax-general-section"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./tax-rates-section"
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { taxRegionLoader as loader } from "./loader"
|
||||
export { TaxDetail as Component } from "./tax-detail"
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./edit-tax-form"
|
||||
@@ -1 +0,0 @@
|
||||
export { TaxEdit as Component } from "./tax-edit"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./tax-list-callout"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "../tax-list-table/tax-list-table"
|
||||
@@ -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])
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { TaxList as Component } from "./tax-list"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./create-tax-rate-form"
|
||||
@@ -1 +0,0 @@
|
||||
export { TaxRateCreate as Component } from "./tax-rate-create"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./edit-tax-rate-overrides-form"
|
||||
@@ -1 +0,0 @@
|
||||
export { TaxRateEditOverrides as Component } from "./tax-rate-edit-overrides"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./edit-tax-rate-form"
|
||||
@@ -1 +0,0 @@
|
||||
export { TaxRateEdit as Component } from "./tax-rate-edit"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tax-region-list-table"
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { TaxRegionsList as Component } from "./tax-region-list"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./fulfillment"
|
||||
export * from "./pricing"
|
||||
export * from "./sales-channel"
|
||||
export * from "./stock-locations"
|
||||
export * from "./tax"
|
||||
|
||||
1
packages/types/src/http/tax/admin/index.ts
Normal file
1
packages/types/src/http/tax/admin/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./tax-regions"
|
||||
33
packages/types/src/http/tax/admin/tax-regions.ts
Normal file
33
packages/types/src/http/tax/admin/tax-regions.ts
Normal 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[]
|
||||
}
|
||||
1
packages/types/src/http/tax/index.ts
Normal file
1
packages/types/src/http/tax/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./admin"
|
||||
Reference in New Issue
Block a user