feat: Add tax regions table (#6983)
This commit is contained in:
@@ -854,7 +854,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"taxes": {
|
"taxes": {
|
||||||
"domain": "Taxes",
|
"domain": "Tax Regions",
|
||||||
"countries": {
|
"countries": {
|
||||||
"taxCountriesHint": "Tax settings apply to the listed countries."
|
"taxCountriesHint": "Tax settings apply to the listed countries."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
+32
@@ -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 { currencies } from "./currencies"
|
||||||
import { customers } from "./customers"
|
import { customers } from "./customers"
|
||||||
import { invites } from "./invites"
|
import { invites } from "./invites"
|
||||||
|
import { payments } from "./payments"
|
||||||
import { productTypes } from "./product-types"
|
import { productTypes } from "./product-types"
|
||||||
import { products } from "./products"
|
import { products } from "./products"
|
||||||
import { payments } from "./payments"
|
|
||||||
import { promotions } from "./promotions"
|
import { promotions } from "./promotions"
|
||||||
import { regions } from "./regions"
|
import { regions } from "./regions"
|
||||||
import { salesChannels } from "./sales-channels"
|
import { salesChannels } from "./sales-channels"
|
||||||
import { stockLocations } from "./stock-locations"
|
import { stockLocations } from "./stock-locations"
|
||||||
import { stores } from "./stores"
|
import { stores } from "./stores"
|
||||||
import { tags } from "./tags"
|
import { tags } from "./tags"
|
||||||
|
import { taxes } from "./taxes"
|
||||||
import { users } from "./users"
|
import { users } from "./users"
|
||||||
import { workflowExecutions } from "./workflow-executions"
|
import { workflowExecutions } from "./workflow-executions"
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export const client = {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
users: users,
|
users: users,
|
||||||
regions: regions,
|
regions: regions,
|
||||||
|
taxes: taxes,
|
||||||
invites: invites,
|
invites: invites,
|
||||||
products: products,
|
products: products,
|
||||||
productTypes: productTypes,
|
productTypes: productTypes,
|
||||||
|
|||||||
@@ -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,
|
AdminDraftOrdersRes,
|
||||||
AdminGiftCardsRes,
|
AdminGiftCardsRes,
|
||||||
AdminOrdersRes,
|
AdminOrdersRes,
|
||||||
AdminRegionsRes,
|
|
||||||
AdminSalesChannelsRes,
|
|
||||||
AdminUserRes,
|
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { Outlet, RouteObject } from "react-router-dom"
|
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
@@ -1 +0,0 @@
|
|||||||
export * from "./override-chip"
|
|
||||||
-24
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./override-grid"
|
|
||||||
-42
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./overrides-drawer"
|
|
||||||
-394
@@ -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",
|
|
||||||
}
|
|
||||||
-46
@@ -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]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-49
@@ -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]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-46
@@ -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]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-17
@@ -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
|
|
||||||
}
|
|
||||||
-29
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./tax-countries-section"
|
|
||||||
-69
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./tax-default-tax-rate-section"
|
|
||||||
-56
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./tax-general-section"
|
|
||||||
-93
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./tax-rates-section"
|
|
||||||
-61
@@ -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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-93
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-127
@@ -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]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-19
@@ -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
|
|
||||||
}
|
|
||||||
-29
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-257
@@ -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
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./tax-list-callout"
|
|
||||||
-24
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "../tax-list-table/tax-list-table"
|
|
||||||
-70
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-474
@@ -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
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-414
@@ -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
@@ -1 +0,0 @@
|
|||||||
export * from "./edit-tax-rate-overrides-form"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { TaxRateEditOverrides as Component } from "./tax-rate-edit-overrides"
|
|
||||||
-24
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-133
@@ -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
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-region-list-table"
|
||||||
+125
@@ -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 * as QueryConfig from "./query-config"
|
||||||
|
|
||||||
import { AdminGetTaxRegionsParams, AdminPostTaxRegionsReq } from "./validators"
|
import { AdminCreateTaxRegion, AdminGetTaxRegionsParams } from "./validators"
|
||||||
import { transformBody, transformQuery } from "../../../api/middlewares"
|
|
||||||
|
|
||||||
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||||
|
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||||
|
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||||
|
|
||||||
export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
|
export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
{
|
{
|
||||||
@@ -15,13 +16,19 @@ export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
matcher: "/admin/tax-regions",
|
matcher: "/admin/tax-regions",
|
||||||
middlewares: [transformBody(AdminPostTaxRegionsReq)],
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminCreateTaxRegion),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetTaxRegionsParams,
|
||||||
|
QueryConfig.listTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
matcher: "/admin/tax-regions",
|
matcher: "/admin/tax-regions",
|
||||||
middlewares: [
|
middlewares: [
|
||||||
transformQuery(
|
validateAndTransformQuery(
|
||||||
AdminGetTaxRegionsParams,
|
AdminGetTaxRegionsParams,
|
||||||
QueryConfig.listTransformQueryConfig
|
QueryConfig.listTransformQueryConfig
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export const defaultAdminTaxRegionRelations = []
|
export const defaults = [
|
||||||
export const allowedAdminTaxRegionRelations = []
|
|
||||||
export const defaultAdminTaxRegionFields = [
|
|
||||||
"id",
|
"id",
|
||||||
"country_code",
|
"country_code",
|
||||||
"province_code",
|
"province_code",
|
||||||
@@ -14,13 +12,11 @@ export const defaultAdminTaxRegionFields = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveTransformQueryConfig = {
|
export const retrieveTransformQueryConfig = {
|
||||||
defaultFields: defaultAdminTaxRegionFields,
|
defaults,
|
||||||
defaultRelations: defaultAdminTaxRegionRelations,
|
|
||||||
allowedRelations: allowedAdminTaxRegionRelations,
|
|
||||||
isList: false,
|
isList: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listTransformQueryConfig = {
|
export const listTransformQueryConfig = {
|
||||||
defaultLimit: 20,
|
...retrieveTransformQueryConfig,
|
||||||
isList: true,
|
isList: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { createTaxRegionsWorkflow } from "@medusajs/core-flows"
|
import { createTaxRegionsWorkflow } from "@medusajs/core-flows"
|
||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../types/routing"
|
} from "../../../types/routing"
|
||||||
import { defaultAdminTaxRegionFields } from "./query-config"
|
import { AdminCreateTaxRegionType } from "./validators"
|
||||||
import { AdminPostTaxRegionsReq } from "./validators"
|
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: AuthenticatedMedusaRequest<AdminPostTaxRegionsReq>,
|
req: AuthenticatedMedusaRequest<AdminCreateTaxRegionType>,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
) => {
|
) => {
|
||||||
const { result, errors } = await createTaxRegionsWorkflow(req.scope).run({
|
const { result, errors } = await createTaxRegionsWorkflow(req.scope).run({
|
||||||
@@ -30,10 +32,35 @@ export const POST = async (
|
|||||||
const query = remoteQueryObjectFromString({
|
const query = remoteQueryObjectFromString({
|
||||||
entryPoint: "tax_region",
|
entryPoint: "tax_region",
|
||||||
variables: { id: result[0].id },
|
variables: { id: result[0].id },
|
||||||
fields: defaultAdminTaxRegionFields,
|
fields: req.remoteQueryConfig.fields,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [taxRegion] = await remoteQuery(query)
|
const [taxRegion] = await remoteQuery(query)
|
||||||
|
|
||||||
res.status(200).json({ tax_region: taxRegion })
|
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 { z } from "zod"
|
||||||
import { Type } from "class-transformer"
|
|
||||||
import {
|
import {
|
||||||
IsNumber,
|
createFindParams,
|
||||||
IsObject,
|
createOperatorMap,
|
||||||
IsOptional,
|
createSelectParams,
|
||||||
IsString,
|
} from "../../utils/validators"
|
||||||
ValidateNested,
|
|
||||||
} from "class-validator"
|
|
||||||
import { extendedFindParamsMixin, FindParams } from "../../../types/common"
|
|
||||||
import { OperatorMapValidator } from "../../../types/validators/operator-map"
|
|
||||||
|
|
||||||
export class AdminGetTaxRegionsTaxRegionParams extends FindParams {}
|
export const AdminGetTaxRegionParams = createSelectParams()
|
||||||
|
|
||||||
export class AdminGetTaxRegionsParams extends extendedFindParamsMixin({
|
export type AdminCreateTaxRegionsParams = z.infer<
|
||||||
limit: 50,
|
typeof AdminGetTaxRegionsParams
|
||||||
|
>
|
||||||
|
export const AdminGetTaxRegionsParams = createFindParams({
|
||||||
|
limit: 20,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}) {
|
}).merge(
|
||||||
/**
|
z.object({
|
||||||
* Search parameter for regions.
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
*/
|
country_code: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
@IsString({ each: true })
|
province_code: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
@IsOptional()
|
parent_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
id?: string | string[]
|
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(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
export type AdminCreateTaxRegionType = z.infer<typeof AdminCreateTaxRegion>
|
||||||
* Filter by country code.
|
export const AdminCreateTaxRegion = z.object({
|
||||||
*/
|
country_code: z.string(),
|
||||||
@IsString({ each: true })
|
province_code: z.string().optional(),
|
||||||
@IsOptional()
|
parent_id: z.string().optional(),
|
||||||
country_code?: string | string[]
|
default_tax_rate: z
|
||||||
|
.object({
|
||||||
/**
|
rate: z.number().optional(),
|
||||||
* Filter by province code
|
code: z.string().optional(),
|
||||||
*/
|
name: z.string(),
|
||||||
@IsString({ each: true })
|
metadata: z.record(z.unknown()).optional(),
|
||||||
@IsOptional()
|
})
|
||||||
province_code?: string | string[]
|
.optional(),
|
||||||
|
metadata: z.record(z.unknown()).optional(),
|
||||||
/**
|
})
|
||||||
* 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>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from "./fulfillment"
|
|||||||
export * from "./pricing"
|
export * from "./pricing"
|
||||||
export * from "./sales-channel"
|
export * from "./sales-channel"
|
||||||
export * from "./stock-locations"
|
export * from "./stock-locations"
|
||||||
|
export * from "./tax"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-regions"
|
||||||
@@ -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[]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./admin"
|
||||||
Reference in New Issue
Block a user