feat(medusa,dashboard,tax): added tax rates and regions UI (#7026)
whats missing: - make rules required for overrides - conditions for other rules - populating condition reference ids with labels on update Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
6
.changeset/wicked-days-buy.md
Normal file
6
.changeset/wicked-days-buy.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
feat(medusa,types): added tax flows end to end
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ITaxModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { ITaxModuleService } from "@medusajs/types"
|
||||
|
||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
@@ -58,6 +58,8 @@ medusaIntegrationTestRunner({
|
||||
updated_at: expect.any(String),
|
||||
deleted_at: null,
|
||||
created_by: null,
|
||||
rules: [],
|
||||
tax_region: expect.any(Object),
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -91,6 +93,9 @@ medusaIntegrationTestRunner({
|
||||
created_by: "admin_user",
|
||||
provider_id: null,
|
||||
metadata: null,
|
||||
children: [],
|
||||
parent: null,
|
||||
tax_rates: expect.any(Array),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -121,6 +126,13 @@ medusaIntegrationTestRunner({
|
||||
deleted_at: null,
|
||||
created_by: "admin_user",
|
||||
is_combinable: false,
|
||||
tax_region: expect.any(Object),
|
||||
rules: [
|
||||
expect.objectContaining({
|
||||
reference: "product",
|
||||
reference_id: "prod_1234",
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -147,6 +159,11 @@ medusaIntegrationTestRunner({
|
||||
created_by: "admin_user",
|
||||
metadata: null,
|
||||
provider_id: null,
|
||||
children: [],
|
||||
tax_rates: [],
|
||||
parent: expect.objectContaining({
|
||||
id: usRegionId,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -219,6 +236,9 @@ medusaIntegrationTestRunner({
|
||||
created_by: "admin_user",
|
||||
provider_id: null,
|
||||
metadata: null,
|
||||
children: [],
|
||||
parent: null,
|
||||
tax_rates: expect.any(Array),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -249,6 +269,13 @@ medusaIntegrationTestRunner({
|
||||
deleted_at: null,
|
||||
created_by: "admin_user",
|
||||
is_combinable: false,
|
||||
tax_region: expect.any(Object),
|
||||
rules: [
|
||||
expect.objectContaining({
|
||||
reference: "product",
|
||||
reference_id: "prod_1234",
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -279,6 +306,13 @@ medusaIntegrationTestRunner({
|
||||
updated_at: expect.any(String),
|
||||
created_by: "admin_user",
|
||||
is_combinable: true,
|
||||
tax_region: expect.any(Object),
|
||||
rules: [
|
||||
expect.objectContaining({
|
||||
reference: "product",
|
||||
reference_id: "prod_1234",
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -331,6 +365,43 @@ medusaIntegrationTestRunner({
|
||||
expect(rates[0].deleted_at).not.toBeNull()
|
||||
})
|
||||
|
||||
it("can retrieve a tax region", async () => {
|
||||
const region = await service.createTaxRegions({
|
||||
country_code: "us",
|
||||
})
|
||||
|
||||
const rate = await service.create({
|
||||
tax_region_id: region.id,
|
||||
code: "test",
|
||||
rate: 2.5,
|
||||
name: "Test Rate",
|
||||
})
|
||||
|
||||
const response = await api.get(
|
||||
`/admin/tax-regions/${region.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toEqual({
|
||||
tax_region: {
|
||||
id: region.id,
|
||||
country_code: "us",
|
||||
province_code: null,
|
||||
parent_id: null,
|
||||
provider_id: null,
|
||||
created_by: null,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
tax_rates: expect.any(Array),
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
children: [],
|
||||
parent: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("can create a tax region and delete it", async () => {
|
||||
const regionRes = await api.post(
|
||||
`/admin/tax-regions`,
|
||||
|
||||
@@ -140,6 +140,27 @@
|
||||
},
|
||||
"required": ["domain"]
|
||||
},
|
||||
"taxRegions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
},
|
||||
"taxRates": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
},
|
||||
"campaigns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -633,6 +633,49 @@
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
},
|
||||
"taxRates": {
|
||||
"domain": "Tax Rates",
|
||||
"fields": {
|
||||
"isCombinable": "Is combinable?",
|
||||
"appliesTo": "Tax Rate applies to",
|
||||
"customer_groups": "Customer Group",
|
||||
"product_collections": "Product Collection",
|
||||
"product_tags": "Product Tag",
|
||||
"product_types": "Product Type",
|
||||
"products": "Product"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Tax Rate",
|
||||
"description": "Edits tax rate for a tax region"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Tax Rate Override",
|
||||
"description": "Creates tax rate overrides for a tax region"
|
||||
}
|
||||
},
|
||||
"taxRegions": {
|
||||
"domain": "Tax Regions",
|
||||
"description": "Manage your region's tax structure",
|
||||
"create": {
|
||||
"title": "Create Tax Region",
|
||||
"description": "Creates a tax region with default tax rate"
|
||||
},
|
||||
"create-child": {
|
||||
"title": "Create Default Rate for Province",
|
||||
"description": "Creates a tax region for a province with default tax rate"
|
||||
},
|
||||
"removeWarning": "You are about to remove {{tax_region_name}}. This action cannot be undone.",
|
||||
"fields": {
|
||||
"rate": {
|
||||
"name": "Rate",
|
||||
"hint": "Tax rate to apply for a region or province"
|
||||
},
|
||||
"is_combinable": {
|
||||
"name": "Is combinable",
|
||||
"hint": "If this tax rate can be combined with the default rate from province or parent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"promotions": {
|
||||
"domain": "Promotions",
|
||||
"fields": {
|
||||
@@ -907,6 +950,7 @@
|
||||
},
|
||||
"taxes": {
|
||||
"domain": "Tax Regions",
|
||||
"domainDescription": "Manage your tax region",
|
||||
"countries": {
|
||||
"taxCountriesHint": "Tax settings apply to the listed countries."
|
||||
},
|
||||
@@ -1123,6 +1167,7 @@
|
||||
"amount": "Amount",
|
||||
"refundAmount": "Refund amount",
|
||||
"name": "Name",
|
||||
"default": "Default",
|
||||
"lastName": "Last Name",
|
||||
"firstName": "First Name",
|
||||
"title": "Title",
|
||||
@@ -1247,6 +1292,7 @@
|
||||
"quantity": "Quantity",
|
||||
"variant": "Variant",
|
||||
"id": "ID",
|
||||
"parent": "Parent",
|
||||
"minSubtotal": "Min. Subtotal",
|
||||
"maxSubtotal": "Max. Subtotal",
|
||||
"shippingProfile": "Shipping Profile",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import format from "date-fns/format"
|
||||
|
||||
export function formatDate(date: string | Date) {
|
||||
const value = new Date(date)
|
||||
value.setMinutes(value.getMinutes() - value.getTimezoneOffset())
|
||||
|
||||
const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12
|
||||
const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM"
|
||||
|
||||
return format(value, timestampFormat)
|
||||
}
|
||||
@@ -17,16 +17,18 @@ export const DataTableQuery = ({
|
||||
prefix,
|
||||
}: DataTableQueryProps) => {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-x-4 px-6 py-4">
|
||||
<div className="w-full max-w-[60%]">
|
||||
{filters && filters.length > 0 && (
|
||||
<DataTableFilter filters={filters} prefix={prefix} />
|
||||
)}
|
||||
(search || orderBy || filters || prefix) && (
|
||||
<div className="flex items-start justify-between gap-x-4 px-6 py-4">
|
||||
<div className="w-full max-w-[60%]">
|
||||
{filters && filters.length > 0 && (
|
||||
<DataTableFilter filters={filters} prefix={prefix} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-x-2">
|
||||
{search && <DataTableSearch prefix={prefix} />}
|
||||
{orderBy && <DataTableOrderBy keys={orderBy} prefix={prefix} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-x-2">
|
||||
{search && <DataTableSearch prefix={prefix} />}
|
||||
{orderBy && <DataTableOrderBy keys={orderBy} prefix={prefix} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./type-cell"
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Badge } from "@medusajs/ui"
|
||||
|
||||
type CellProps = {
|
||||
is_combinable: boolean
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
text: string
|
||||
}
|
||||
|
||||
export const TypeCell = ({ is_combinable }: CellProps) => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
||||
<span className="truncate">
|
||||
{is_combinable ? (
|
||||
<Badge size="2xsmall" color="green">
|
||||
Combinable
|
||||
</Badge>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TypeHeader = ({ text }: HeaderProps) => {
|
||||
return (
|
||||
<div className=" flex h-full w-full items-center">
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
119
packages/admin-next/dashboard/src/hooks/api/tax-rates.tsx
Normal file
119
packages/admin-next/dashboard/src/hooks/api/tax-rates.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
AdminPostTaxRatesReq,
|
||||
AdminPostTaxRatesTaxRateReq,
|
||||
} from "@medusajs/medusa"
|
||||
import { AdminTaxRateListResponse, AdminTaxRateResponse } from "@medusajs/types"
|
||||
import {
|
||||
QueryKey,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { TaxRateDeleteRes } from "../../types/api-responses"
|
||||
|
||||
const TAX_RATES_QUERY_KEY = "tax_rates" as const
|
||||
export const taxRatesQueryKeys = queryKeysFactory(TAX_RATES_QUERY_KEY)
|
||||
|
||||
export const useTaxRate = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
AdminTaxRateResponse,
|
||||
Error,
|
||||
AdminTaxRateResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: taxRatesQueryKeys.detail(id),
|
||||
queryFn: async () => client.taxes.retrieveTaxRate(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useTaxRates = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
AdminTaxRateListResponse,
|
||||
Error,
|
||||
AdminTaxRateListResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.taxes.listTaxRates(query),
|
||||
queryKey: taxRatesQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useUpdateTaxRate = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
AdminTaxRateResponse,
|
||||
Error,
|
||||
AdminPostTaxRatesTaxRateReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.taxes.updateTaxRate(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: taxRatesQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: taxRatesQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateTaxRate = (
|
||||
options?: UseMutationOptions<
|
||||
AdminTaxRateResponse,
|
||||
Error,
|
||||
AdminPostTaxRatesReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.taxes.createTaxRate(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: taxRatesQueryKeys.lists() })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteTaxRate = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<TaxRateDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.taxes.deleteTaxRate(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: taxRatesQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: taxRatesQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { AdminCreateTaxRegion } from "@medusajs/medusa"
|
||||
import {
|
||||
AdminTaxRegionResponse,
|
||||
AdminTaxRegionListResponse,
|
||||
AdminTaxRegionResponse,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { TaxRegionDeleteRes } from "../../types/api-responses"
|
||||
|
||||
const TAX_REGIONS_QUERY_KEY = "tax_regions" as const
|
||||
const taxRegionsQueryKeys = queryKeysFactory(TAX_REGIONS_QUERY_KEY)
|
||||
export const taxRegionsQueryKeys = queryKeysFactory(TAX_REGIONS_QUERY_KEY)
|
||||
|
||||
export const useTaxRegion = (
|
||||
id: string,
|
||||
@@ -51,3 +60,38 @@ export const useTaxRegions = (
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateTaxRegion = (
|
||||
options?: UseMutationOptions<
|
||||
AdminTaxRegionResponse,
|
||||
Error,
|
||||
AdminCreateTaxRegion
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.taxes.createTaxRegion(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.all })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteTaxRegion = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<TaxRegionDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.taxes.deleteTaxRegion(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: taxRegionsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { TaxRateResponse } from "@medusajs/types"
|
||||
import {
|
||||
TextCell,
|
||||
TextHeader,
|
||||
} from "../../../components/table/table-cells/common/text-cell"
|
||||
|
||||
import {
|
||||
TypeCell,
|
||||
TypeHeader,
|
||||
} from "../../../components/table/table-cells/taxes/type-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<TaxRateResponse>()
|
||||
|
||||
export const useTaxRateTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "name",
|
||||
header: () => <TextHeader text={t("fields.name")} />,
|
||||
cell: ({ row }) => <TextCell text={row.original.name} />,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "province",
|
||||
header: () => <TextHeader text={t("fields.province")} />,
|
||||
cell: ({ row }) => (
|
||||
<TextCell text={row.original.tax_region.province_code} />
|
||||
),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "rate",
|
||||
header: () => <TextHeader text={t("fields.rate")} />,
|
||||
cell: ({ row }) => <TextCell text={`${row.original.rate} %`} />,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "is_combinable",
|
||||
header: () => <TypeHeader text={t("fields.type")} />,
|
||||
cell: ({ row }) => (
|
||||
<TypeCell is_combinable={row.original.is_combinable} />
|
||||
),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "code",
|
||||
header: () => <TextHeader text={t("fields.code")} />,
|
||||
cell: ({ row }) => <TextCell text={row.original.code || "-"} />,
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { 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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { AdminGetTaxRatesParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../use-query-params"
|
||||
|
||||
type UseTaxRateTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useTaxRateTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseTaxRateTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, q, order, created_at, updated_at } = queryObject
|
||||
|
||||
const searchParams: AdminGetTaxRatesParams = {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import {
|
||||
AdminCreateTaxRegion,
|
||||
AdminPostTaxRatesTaxRateReq,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
AdminTaxRateResponse,
|
||||
AdminTaxRegionListResponse,
|
||||
AdminTaxRegionResponse,
|
||||
} from "@medusajs/types"
|
||||
import { getRequest } from "./common"
|
||||
import { TaxRateDeleteRes, TaxRegionDeleteRes } from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function retrieveTaxRegion(id: string, query?: Record<string, any>) {
|
||||
return getRequest<AdminTaxRegionResponse>(`/admin/tax-regions/${id}`, query)
|
||||
@@ -12,7 +18,42 @@ async function listTaxRegions(query?: Record<string, any>) {
|
||||
return getRequest<AdminTaxRegionListResponse>(`/admin/tax-regions`, query)
|
||||
}
|
||||
|
||||
async function createTaxRegion(payload: AdminCreateTaxRegion) {
|
||||
return postRequest<AdminTaxRegionResponse>(`/admin/tax-regions`, payload)
|
||||
}
|
||||
|
||||
async function deleteTaxRegion(id: string) {
|
||||
return deleteRequest<TaxRegionDeleteRes>(`/admin/tax-regions/${id}`)
|
||||
}
|
||||
|
||||
async function retrieveTaxRate(id: string, query?: Record<string, any>) {
|
||||
return getRequest<AdminTaxRegionResponse>(`/admin/tax-rates/${id}`, query)
|
||||
}
|
||||
|
||||
async function listTaxRates(query?: Record<string, any>) {
|
||||
return getRequest<AdminTaxRegionListResponse>(`/admin/tax-rates`, query)
|
||||
}
|
||||
|
||||
async function updateTaxRate(id: string, payload: AdminPostTaxRatesTaxRateReq) {
|
||||
return postRequest<AdminTaxRateResponse>(`/admin/tax-rates/${id}`, payload)
|
||||
}
|
||||
|
||||
async function createTaxRate(payload: AdminCreateTaxRate) {
|
||||
return postRequest<AdminTaxRateResponse>(`/admin/tax-rates`, payload)
|
||||
}
|
||||
|
||||
async function deleteTaxRate(id: string) {
|
||||
return deleteRequest<TaxRateDeleteRes>(`/admin/tax-rates/${id}`)
|
||||
}
|
||||
|
||||
export const taxes = {
|
||||
retrieveTaxRegion,
|
||||
listTaxRegions,
|
||||
retrieveTaxRate,
|
||||
listTaxRates,
|
||||
updateTaxRate,
|
||||
createTaxRegion,
|
||||
deleteTaxRegion,
|
||||
createTaxRate,
|
||||
deleteTaxRate,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
/** This file is auto-generated. Do not modify it manually. */
|
||||
import type { RegionCountryDTO } from "@medusajs/types"
|
||||
|
||||
export function getCountryByIso2(
|
||||
iso2: string | null
|
||||
): Omit<RegionCountryDTO, "id"> | undefined {
|
||||
if (!iso2) {
|
||||
return
|
||||
}
|
||||
|
||||
return countries.find((c) => c.iso_2 === iso2)
|
||||
}
|
||||
|
||||
export const countries: Omit<RegionCountryDTO, "id">[] = [
|
||||
{
|
||||
iso_2: "af",
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
AdminApiKeyResponse,
|
||||
AdminCustomerGroupResponse,
|
||||
AdminProductCategoryResponse,
|
||||
AdminTaxRateResponse,
|
||||
AdminTaxRegionResponse,
|
||||
SalesChannelDTO,
|
||||
UserDTO,
|
||||
} from "@medusajs/types"
|
||||
@@ -757,7 +759,56 @@ export const v2Routes: RouteObject[] = [
|
||||
{
|
||||
path: "",
|
||||
lazy: () => import("../../v2-routes/taxes/tax-region-list"),
|
||||
children: [],
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/taxes/tax-region-create"),
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../v2-routes/taxes/tax-region-detail"),
|
||||
handle: {
|
||||
crumb: (data: AdminTaxRegionResponse) => {
|
||||
return data.tax_region.country_code
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "create-default",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/taxes/tax-province-create"),
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
path: "create-override",
|
||||
lazy: () => import("../../v2-routes/taxes/tax-rate-create"),
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
path: "tax-rates",
|
||||
children: [
|
||||
{
|
||||
path: ":taxRateId",
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/taxes/tax-rate-edit"),
|
||||
handle: {
|
||||
crumb: (data: AdminTaxRateResponse) => {
|
||||
return data.tax_rate.code
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -142,6 +142,10 @@ export type ProductCollectionListRes = {
|
||||
} & ListRes
|
||||
export type ProductCollectionDeleteRes = DeleteRes
|
||||
|
||||
// Taxes
|
||||
export type TaxRegionDeleteRes = DeleteRes
|
||||
export type TaxRateDeleteRes = DeleteRes
|
||||
|
||||
// Inventory Items
|
||||
export type InventoryItemRes = {
|
||||
inventory_item: InventoryNext.InventoryItemDTO & {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { RegionDTO } from "@medusajs/types"
|
||||
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 { RegionDTO } from "@medusajs/types"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useDeleteRegion, useRegions } from "../../../../../hooks/api/regions"
|
||||
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"
|
||||
import { useDeleteRegion, useRegions } from "../../../../../hooks/api/regions"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
@@ -56,6 +56,7 @@ export const RegionListTable = () => {
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
import {
|
||||
CustomerGroup,
|
||||
Product,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
} from "@medusajs/medusa"
|
||||
import { Button } from "@medusajs/ui"
|
||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||
import { useAdminProductTags } 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 { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
|
||||
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
||||
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||
import { useProductConditionsTableColumns } from "../../hooks/columns/use-product-conditions-table-columns"
|
||||
|
||||
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
||||
import { useCustomerGroupConditionsTableColumns } from "../../hooks/columns/use-customer-group-conditions-table-columns"
|
||||
import { useCustomerGroupConditionsTableFilters } from "../../hooks/filters/use-customer-group-conditions-table-filters"
|
||||
|
||||
import { useProductTypeConditionsTableColumns } from "../../hooks/columns/use-product-type-conditions-table-columns"
|
||||
import { useProductTypeConditionsTableFilters } from "../../hooks/filters/use-product-type-conditions-table-filters"
|
||||
import { useProductTypeConditionsTableQuery } from "../../hooks/query/use-product-type-conditions-table-query"
|
||||
|
||||
import { useProductCollectionConditionsTableColumns } from "../../hooks/columns/use-product-collection-conditions-table-columns"
|
||||
import { useProductCollectionConditionsTableFilters } from "../../hooks/filters/use-product-collection-conditions-table-filters"
|
||||
import { useProductCollectionConditionsTableQuery } from "../../hooks/query/use-product-collection-conditions-table-query"
|
||||
|
||||
import { useProductTagConditionsTableColumns } from "../../hooks/columns/use-product-tag-conditions-table-columns"
|
||||
import { useProductTagConditionsTableFilters } from "../../hooks/filters/use-product-tag-conditions-table-filters"
|
||||
import { useProductTagConditionsTableQuery } from "../../hooks/query/use-product-tag-conditions-table-query"
|
||||
|
||||
import { ProductCollectionDTO } from "@medusajs/types"
|
||||
import { useCollections } from "../../../../../hooks/api/collections"
|
||||
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
|
||||
import { useProductTypes } from "../../../../../hooks/api/product-types"
|
||||
import { useProducts } from "../../../../../hooks/api/products"
|
||||
import { ConditionEntities } from "../../constants"
|
||||
import { ConditionsOption } from "../../types"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
const PRODUCT_PREFIX = "product"
|
||||
const PRODUCT_TYPE_PREFIX = "product_type"
|
||||
const PRODUCT_COLLECTION_PREFIX = "product_collection"
|
||||
const CUSTOMER_GROUP_PREFIX = "customer_group"
|
||||
const PRODUCT_TAG_PREFIX = "customer_group"
|
||||
|
||||
type ConditionsProps = {
|
||||
selected: ConditionsOption[]
|
||||
onSave: (options: ConditionsOption[]) => void
|
||||
}
|
||||
|
||||
const initRowState = (selected: ConditionsOption[] = []): RowSelectionState => {
|
||||
return selected.reduce((acc, { value }) => {
|
||||
acc[value] = true
|
||||
return acc
|
||||
}, {} as RowSelectionState)
|
||||
}
|
||||
|
||||
const ConditionsFooter = ({ 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 ProductConditionsTable = ({ selected = [], onSave }: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_PREFIX,
|
||||
})
|
||||
const { products, count, isLoading, isError, error } = useProducts(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedProducts = (products?.filter((p) => added.includes(p.id!)) ??
|
||||
[]) as Product[]
|
||||
|
||||
if (addedProducts.length > 0) {
|
||||
const newConditions = addedProducts.map((p) => ({
|
||||
label: p.title,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductConditionsTableColumns()
|
||||
const filters = useProductTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: products ?? [],
|
||||
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}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomerGroupConditionsTable = ({
|
||||
selected = [],
|
||||
onSave,
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useCustomerGroupTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_PREFIX,
|
||||
})
|
||||
const { customer_groups, count, isLoading, isError, error } =
|
||||
useCustomerGroups(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedGroups =
|
||||
customer_groups?.filter((p) => added.includes(p.id!)) ?? []
|
||||
|
||||
if (addedGroups.length > 0) {
|
||||
const newConditions = addedGroups.map((p) => ({
|
||||
label: p.name,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useCustomerGroupConditionsTableColumns()
|
||||
const filters = useCustomerGroupConditionsTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: (customer_groups ?? []) as CustomerGroup[],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: CUSTOMER_GROUP_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={["name", "created_at", "updated_at"]}
|
||||
prefix={CUSTOMER_GROUP_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductTypeConditionsTable = ({
|
||||
onSave,
|
||||
selected = [],
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductTypeConditionsTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_TYPE_PREFIX,
|
||||
})
|
||||
const { product_types, count, isLoading, isError, error } = useProductTypes(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedTypes = (product_types?.filter((p) => added.includes(p.id!)) ??
|
||||
[]) as ProductType[]
|
||||
|
||||
if (addedTypes.length > 0) {
|
||||
const newConditions = addedTypes.map((p) => ({
|
||||
label: p.value,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductTypeConditionsTableColumns()
|
||||
const filters = useProductTypeConditionsTableFilters()
|
||||
|
||||
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_TYPE_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductCollectionConditionsTable = ({
|
||||
onSave,
|
||||
selected = [],
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductCollectionConditionsTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_COLLECTION_PREFIX,
|
||||
})
|
||||
const { collections, count, isPending, isError, error } = useCollections(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedCollections = (collections?.filter((p) =>
|
||||
added.includes(p.id!)
|
||||
) ?? []) as ProductCollectionDTO[]
|
||||
|
||||
if (addedCollections.length > 0) {
|
||||
const newConditions = addedCollections.map((p) => ({
|
||||
label: p.title,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductCollectionConditionsTableColumns()
|
||||
const filters = useProductCollectionConditionsTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: collections ?? [],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: PRODUCT_COLLECTION_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={isPending}
|
||||
layout="fill"
|
||||
orderBy={["title", "handle", "created_at", "updated_at"]}
|
||||
prefix={PRODUCT_COLLECTION_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductTagConditionsTable = ({
|
||||
onSave,
|
||||
selected = [],
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductTagConditionsTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_TAG_PREFIX,
|
||||
})
|
||||
|
||||
// TODO: replace this with useProductTags when its available
|
||||
const { product_tags, count, isLoading, isError, error } =
|
||||
useAdminProductTags(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedTags = (product_tags?.filter((p) => added.includes(p.id!)) ??
|
||||
[]) as ProductTag[]
|
||||
|
||||
if (addedTags.length > 0) {
|
||||
const newConditions = addedTags.map((p) => ({
|
||||
label: p.value,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductTagConditionsTableColumns()
|
||||
const filters = useProductTagConditionsTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: product_tags ?? [],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: PRODUCT_TAG_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", "handle", "created_at", "updated_at"]}
|
||||
prefix={PRODUCT_TAG_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ConditionsTableProps = {
|
||||
product: ConditionsProps
|
||||
productType: ConditionsProps
|
||||
productTag: ConditionsProps
|
||||
productCollection: ConditionsProps
|
||||
customerGroup: ConditionsProps
|
||||
selected: ConditionEntities | null
|
||||
}
|
||||
|
||||
export const ConditionsDrawer = ({
|
||||
product,
|
||||
productType,
|
||||
customerGroup,
|
||||
productCollection,
|
||||
productTag,
|
||||
selected,
|
||||
}: ConditionsTableProps) => {
|
||||
switch (selected) {
|
||||
case ConditionEntities.PRODUCT:
|
||||
return <ProductConditionsTable {...product} />
|
||||
case ConditionEntities.PRODUCT_TYPE:
|
||||
return <ProductTypeConditionsTable {...productType} />
|
||||
case ConditionEntities.PRODUCT_COLLECTION:
|
||||
return <ProductCollectionConditionsTable {...productCollection} />
|
||||
case ConditionEntities.PRODUCT_TAG:
|
||||
return <ProductTagConditionsTable {...productTag} />
|
||||
case ConditionEntities.CUSTOMER_GROUP:
|
||||
return <CustomerGroupConditionsTable {...customerGroup} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./conditions-drawer"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tax-region-create-form"
|
||||
@@ -0,0 +1,250 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Heading, Input, Select, Switch, Text } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { TaxRegionResponse } from "@medusajs/types"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useCreateTaxRegion } from "../../../../../hooks/api/tax-regions"
|
||||
import { countries } from "../../../../../lib/countries"
|
||||
|
||||
export const TaxRegionCreateForm = ({
|
||||
taxRegion,
|
||||
formSchema,
|
||||
}: {
|
||||
taxRegion?: TaxRegionResponse
|
||||
formSchema: zod.ZodObject<{
|
||||
province_code: any
|
||||
country_code: any
|
||||
parent_id: any
|
||||
name: any
|
||||
code: any
|
||||
rate: any
|
||||
is_combinable: any
|
||||
}>
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof formSchema>>({
|
||||
defaultValues: {
|
||||
country_code: taxRegion?.country_code || undefined,
|
||||
parent_id: taxRegion?.id || undefined,
|
||||
},
|
||||
resolver: zodResolver(formSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useCreateTaxRegion()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
parent_id: taxRegion?.id,
|
||||
province_code: data.province_code,
|
||||
country_code: data.country_code,
|
||||
default_tax_rate: {
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
rate: data.rate,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
taxRegion?.id
|
||||
? handleSuccess(`/settings/taxes/${taxRegion.id}`)
|
||||
: handleSuccess(`/settings/taxes`)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<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"
|
||||
variant="primary"
|
||||
size="small"
|
||||
isLoading={isPending}
|
||||
>
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
|
||||
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-hidden p-16">
|
||||
<div className="flex flex-col gap-y-8 w-full max-w-[720px]">
|
||||
<div>
|
||||
<Heading className="text-left">
|
||||
{taxRegion
|
||||
? t("taxRegions.create-child.title")
|
||||
: t("taxRegions.create.title")}
|
||||
</Heading>
|
||||
|
||||
<Text className="text-ui-fg-subtle txt-small">
|
||||
{taxRegion
|
||||
? t("taxRegions.create-child.description")
|
||||
: t("taxRegions.create.description")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{!taxRegion && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="country_code"
|
||||
render={({ field: { ref, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.country")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Select {...field} onValueChange={onChange}>
|
||||
<Select.Trigger ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content>
|
||||
{countries?.map((country) => (
|
||||
<Select.Item
|
||||
key={country.iso_2}
|
||||
value={country.iso_2}
|
||||
>
|
||||
{country.display_name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{taxRegion && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="province_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.province")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>Tax Rate Name</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</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}
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("taxRegions.fields.rate.hint")}
|
||||
</Form.Hint>
|
||||
</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.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{!taxRegion?.parent_id && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="is_combinable"
|
||||
render={({ field: { ref, onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("taxRates.fields.isCombinable")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("taxRegions.fields.is_combinable.hint")}
|
||||
</Form.Hint>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum ConditionEntities {
|
||||
PRODUCT = "products",
|
||||
PRODUCT_TYPE = "product_types",
|
||||
PRODUCT_COLLECTION = "product_collections",
|
||||
PRODUCT_TAG = "product_tags",
|
||||
CUSTOMER_GROUP = "customer_groups",
|
||||
}
|
||||
|
||||
export enum Operators {
|
||||
IN = "in",
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { CustomerGroup } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
||||
|
||||
const columnHelper = createColumnHelper<CustomerGroup>()
|
||||
|
||||
export const useCustomerGroupConditionsTableColumns = () => {
|
||||
const base = useCustomerGroupTableColumns()
|
||||
|
||||
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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ProductCollectionDTO } from "@medusajs/types"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductCollectionDTO>()
|
||||
|
||||
export const useProductCollectionConditionsTableColumns = () => {
|
||||
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("title", {
|
||||
header: t("fields.title"),
|
||||
}),
|
||||
columnHelper.accessor("handle", {
|
||||
header: t("fields.handle"),
|
||||
cell: ({ getValue }) => `/${getValue()}`,
|
||||
}),
|
||||
columnHelper.accessor("products", {
|
||||
header: t("fields.products"),
|
||||
cell: ({ getValue }) => {
|
||||
const count = getValue()?.length
|
||||
|
||||
return <span>{count || "-"}</span>
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
export const useProductConditionsTableColumns = () => {
|
||||
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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMemo } from "react"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { ProductTag } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductTag>()
|
||||
|
||||
export const useProductTagConditionsTableColumns = () => {
|
||||
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"),
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ProductType } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductType>()
|
||||
|
||||
export const useProductTypeConditionsTableColumns = () => {
|
||||
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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useCustomerGroupConditionsTableFilters = () => {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useProductCollectionConditionsTableFilters = () => {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useProductTagConditionsTableFilters = () => {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useProductTypeConditionsTableFilters = () => {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AdminGetCollectionsParams } from "@medusajs/medusa"
|
||||
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useProductCollectionConditionsTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
[
|
||||
"offset",
|
||||
"q",
|
||||
"order",
|
||||
"title",
|
||||
"handle",
|
||||
"discount_condition_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const searchParams: AdminGetCollectionsParams = {
|
||||
limit: pageSize,
|
||||
offset: raw.offset ? Number(raw.offset) : 0,
|
||||
q: raw.q,
|
||||
title: raw.title,
|
||||
handle: raw.handle,
|
||||
discount_condition_id: raw.discount_condition_id,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { AdminGetProductTagsParams } from "@medusajs/medusa"
|
||||
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useProductTagConditionsTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
[
|
||||
"offset",
|
||||
"q",
|
||||
"order",
|
||||
"value",
|
||||
"discount_condition_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const searchParams: AdminGetProductTagsParams = {
|
||||
limit: pageSize,
|
||||
offset: raw.offset ? Number(raw.offset) : 0,
|
||||
q: raw.q,
|
||||
discount_condition_id: raw.discount_condition_id,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AdminGetProductTypesParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useProductTypeConditionsTableQuery = ({
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ConditionEntities } from "./constants"
|
||||
|
||||
export type ConditionsOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type ConditionsState = {
|
||||
[K in ConditionEntities]: boolean
|
||||
}
|
||||
|
||||
export type ConditionEntitiesValues = `${ConditionEntities}`
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./tax-province-create"
|
||||
|
||||
export { TaxProvinceCreate as Component } from "./tax-province-create"
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
import * as zod from "zod"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { useTaxRegion } from "../../../hooks/api/tax-regions"
|
||||
import { TaxRegionCreateForm } from "../common/components/tax-region-create-form"
|
||||
|
||||
const CreateTaxProvinceForm = zod.object({
|
||||
province_code: zod.string(),
|
||||
country_code: zod.string(),
|
||||
parent_id: zod.string(),
|
||||
name: zod.string(),
|
||||
code: zod.string().optional(),
|
||||
rate: zod.number(),
|
||||
is_combinable: zod.boolean().default(false),
|
||||
})
|
||||
|
||||
export const TaxProvinceCreate = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { tax_region: taxRegion } = useTaxRegion(
|
||||
id!,
|
||||
{},
|
||||
{
|
||||
enabled: !!id,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<TaxRegionCreateForm
|
||||
taxRegion={taxRegion}
|
||||
formSchema={CreateTaxProvinceForm}
|
||||
/>
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Button, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||
import { ConditionEntitiesValues } from "../../../common/types"
|
||||
|
||||
const N = 2
|
||||
|
||||
type ConditionProps = {
|
||||
labels: string[]
|
||||
type: ConditionEntitiesValues
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function Condition({ labels, type, onClick }: ConditionProps) {
|
||||
const { t } = useTranslation()
|
||||
const isInButtonDisabled = !!labels.length
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="bg-ui-bg-field shadow-borders-base inline-flex items-center divide-x overflow-hidden rounded-md">
|
||||
<Text
|
||||
as="span"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-muted shrink-0 px-2 py-1"
|
||||
>
|
||||
{t("taxRates.fields.appliesTo")} {t(`taxRates.fields.${type}`)}
|
||||
</Text>
|
||||
|
||||
<div className="text-ui-fg-subtle max-w-[240px] shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="transparent"
|
||||
size="small"
|
||||
disabled={isInButtonDisabled}
|
||||
onClick={() => onClick()}
|
||||
className="txt-compact-small-plus disabled:text-ui-fg-subtle rounded-none"
|
||||
>
|
||||
{labels.length && (
|
||||
<ListSummary
|
||||
inline
|
||||
n={N}
|
||||
className="!txt-compact-small-plus max-w-[200px]"
|
||||
list={labels}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./condition"
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./condition"
|
||||
export * from "./tax-rate-create-form"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tax-rate-create-form"
|
||||
@@ -0,0 +1,480 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
Button,
|
||||
clx,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
} from "@medusajs/ui"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { TaxRegionResponse } from "@medusajs/types"
|
||||
import { useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
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 { useCreateTaxRate } from "../../../../../hooks/api/tax-rates"
|
||||
import { useTaxRegions } from "../../../../../hooks/api/tax-regions"
|
||||
import { ConditionsDrawer } from "../../../common/components/conditions-drawer"
|
||||
import { ConditionEntities } from "../../../common/constants"
|
||||
import {
|
||||
ConditionEntitiesValues,
|
||||
ConditionsOption,
|
||||
} from "../../../common/types"
|
||||
import { Condition } from "../condition"
|
||||
|
||||
const SelectedConditionTypesSchema = zod.object({
|
||||
[ConditionEntities.PRODUCT]: zod.boolean(),
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: zod.boolean(),
|
||||
[ConditionEntities.PRODUCT_TAG]: zod.boolean(),
|
||||
[ConditionEntities.PRODUCT_TYPE]: zod.boolean(),
|
||||
[ConditionEntities.CUSTOMER_GROUP]: zod.boolean(),
|
||||
})
|
||||
|
||||
const ConditionSchema = zod.array(
|
||||
zod.object({
|
||||
label: zod.string(),
|
||||
value: zod.string(),
|
||||
})
|
||||
)
|
||||
|
||||
const CreateTaxRateSchema = zod.object({
|
||||
tax_region_id: zod.string(),
|
||||
name: zod.string(),
|
||||
code: zod.string(),
|
||||
rate: zod.number(),
|
||||
is_combinable: zod.boolean().default(false),
|
||||
selected_condition_types: SelectedConditionTypesSchema,
|
||||
products: ConditionSchema,
|
||||
product_types: ConditionSchema,
|
||||
product_collections: ConditionSchema,
|
||||
product_tags: ConditionSchema,
|
||||
customer_groups: ConditionSchema,
|
||||
})
|
||||
|
||||
export const TaxRateCreateForm = ({
|
||||
taxRegion,
|
||||
}: {
|
||||
taxRegion: TaxRegionResponse
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateTaxRateSchema>>({
|
||||
defaultValues: {
|
||||
selected_condition_types: {
|
||||
[ConditionEntities.PRODUCT]: true,
|
||||
[ConditionEntities.PRODUCT_TYPE]: false,
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: false,
|
||||
[ConditionEntities.PRODUCT_TAG]: false,
|
||||
[ConditionEntities.CUSTOMER_GROUP]: false,
|
||||
},
|
||||
products: [],
|
||||
product_types: [],
|
||||
product_collections: [],
|
||||
product_tags: [],
|
||||
customer_groups: [],
|
||||
},
|
||||
resolver: zodResolver(CreateTaxRateSchema),
|
||||
})
|
||||
|
||||
const { tax_regions: taxRegions } = useTaxRegions({
|
||||
parent_id: taxRegion.id,
|
||||
province_code: { $ne: "null" },
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useCreateTaxRate()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
rate: data.rate,
|
||||
is_combinable: data.is_combinable,
|
||||
tax_region_id: data.tax_region_id,
|
||||
rules:
|
||||
data.products?.map((product) => ({
|
||||
reference: "product",
|
||||
reference_id: product.value,
|
||||
})) || [],
|
||||
},
|
||||
{
|
||||
onSuccess: () => handleSuccess(`/settings/taxes/${taxRegion.id}`),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
const [conditionType, setConditionType] = useState<ConditionEntities | null>(
|
||||
null
|
||||
)
|
||||
const selectedConditionTypes = useWatch({
|
||||
name: "selected_condition_types",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const selectedProducts = useWatch({
|
||||
control: form.control,
|
||||
name: "products",
|
||||
})
|
||||
|
||||
const selectedProductCollections = useWatch({
|
||||
control: form.control,
|
||||
name: "product_collections",
|
||||
})
|
||||
|
||||
const selectedProductTypes = useWatch({
|
||||
control: form.control,
|
||||
name: "product_types",
|
||||
})
|
||||
|
||||
const selectedProductTags = useWatch({
|
||||
control: form.control,
|
||||
name: "product_tags",
|
||||
})
|
||||
|
||||
const selectedCustomerGroups = useWatch({
|
||||
control: form.control,
|
||||
name: "customer_groups",
|
||||
})
|
||||
|
||||
const handleSaveConditions = (type: ConditionEntitiesValues) => {
|
||||
return (options: ConditionsOption[]) => {
|
||||
form.setValue(type, options, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTypes = Object.keys(selectedConditionTypes || {})
|
||||
.filter(
|
||||
(k) => selectedConditionTypes[k as keyof typeof selectedConditionTypes]
|
||||
)
|
||||
.sort() as ConditionEntities[]
|
||||
|
||||
const toggleSelectedConditionTypes = (type: ConditionEntities) => {
|
||||
const state = { ...form.getValues().selected_condition_types }
|
||||
|
||||
if (state[type]) {
|
||||
delete state[type]
|
||||
} else {
|
||||
state[type] = true
|
||||
}
|
||||
|
||||
form.setValue("selected_condition_types", state, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
}
|
||||
|
||||
const clearAllSelectedConditions = () => {
|
||||
form.setValue(
|
||||
"selected_condition_types",
|
||||
{
|
||||
[ConditionEntities.PRODUCT]: false,
|
||||
[ConditionEntities.PRODUCT_TYPE]: false,
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: false,
|
||||
[ConditionEntities.PRODUCT_TAG]: false,
|
||||
[ConditionEntities.CUSTOMER_GROUP]: false,
|
||||
},
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const [, setSearchParams] = useSearchParams()
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setConditionType(null)
|
||||
setSearchParams(
|
||||
{},
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<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={isPending}>
|
||||
{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 flex-col overflow-auto py-16", {
|
||||
"items-center": !open,
|
||||
})}
|
||||
>
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading className="text-left">
|
||||
{t("taxRates.create.title")}
|
||||
</Heading>
|
||||
|
||||
<Text className="text-ui-fg-subtle txt-small">
|
||||
{t("taxRates.create.description")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="tax_region_id"
|
||||
render={({ field: { ref, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.province")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Select {...field} onValueChange={onChange}>
|
||||
<Select.Trigger ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content>
|
||||
{taxRegions?.map((taxRegion) => (
|
||||
<Select.Item
|
||||
key={taxRegion.id}
|
||||
value={taxRegion.id}
|
||||
>
|
||||
{taxRegion.province_code}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<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.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="rate"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.rate")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<PercentageInput
|
||||
{...field}
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
</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.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{taxRegion.parent_id && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="is_combinable"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("taxRates.fields.isCombinable")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-y-8">
|
||||
{selectedTypes.length > 0 && (
|
||||
<div className="flex flex-col items-start gap-y-4">
|
||||
{selectedTypes.map((selectedType) => {
|
||||
if (selectedType in (selectedConditionTypes || {})) {
|
||||
const field = form.getValues(selectedType) || []
|
||||
|
||||
return (
|
||||
<Condition
|
||||
key={selectedType}
|
||||
type={selectedType}
|
||||
labels={field.map((f) => f.label)}
|
||||
onClick={() => {
|
||||
setConditionType(selectedType)
|
||||
setOpen(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<DropdownMenu
|
||||
open={isDropdownOpen}
|
||||
onOpenChange={(v) => {
|
||||
v && setIsDropdownOpen(v)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("discounts.conditions.manageTypesAction")}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
onInteractOutside={(e) => {
|
||||
setIsDropdownOpen(true)
|
||||
}}
|
||||
>
|
||||
{Object.values(ConditionEntities).map((type) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={type}
|
||||
checked={selectedConditionTypes[type]}
|
||||
onCheckedChange={() =>
|
||||
toggleSelectedConditionTypes(type)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
size="small"
|
||||
weight={
|
||||
selectedConditionTypes[type]
|
||||
? "plus"
|
||||
: "regular"
|
||||
}
|
||||
>
|
||||
{t(`fields.${type}`)}
|
||||
</Text>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
|
||||
{selectedTypes.length > 0 && (
|
||||
<Button
|
||||
variant="transparent"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={clearAllSelectedConditions}
|
||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||
>
|
||||
{t("actions.clearAll")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SplitView.Content>
|
||||
|
||||
<SplitView.Drawer>
|
||||
<ConditionsDrawer
|
||||
product={{
|
||||
selected: selectedProducts,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT),
|
||||
}}
|
||||
productCollection={{
|
||||
selected: selectedProductCollections,
|
||||
onSave: handleSaveConditions(
|
||||
ConditionEntities.PRODUCT_COLLECTION
|
||||
),
|
||||
}}
|
||||
productType={{
|
||||
selected: selectedProductTypes,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT_TYPE),
|
||||
}}
|
||||
productTag={{
|
||||
selected: selectedProductTags,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT_TAG),
|
||||
}}
|
||||
customerGroup={{
|
||||
selected: selectedCustomerGroups,
|
||||
onSave: handleSaveConditions(
|
||||
ConditionEntities.CUSTOMER_GROUP
|
||||
),
|
||||
}}
|
||||
selected={conditionType}
|
||||
/>
|
||||
</SplitView.Drawer>
|
||||
</SplitView>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./tax-rate-create"
|
||||
|
||||
export { TaxRateCreate as Component } from "./tax-rate-create"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { useTaxRegion } from "../../../hooks/api/tax-regions"
|
||||
import { TaxRateCreateForm } from "./components"
|
||||
|
||||
export const TaxRateCreate = () => {
|
||||
const params = useParams()
|
||||
const { tax_region: taxRegion, isError, error } = useTaxRegion(params.id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
taxRegion && (
|
||||
<RouteFocusModal>
|
||||
<TaxRateCreateForm taxRegion={taxRegion} />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tax-rate-edit-form"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tax-rate-edit-form"
|
||||
@@ -0,0 +1,488 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
Button,
|
||||
clx,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
Input,
|
||||
Switch,
|
||||
Text,
|
||||
} from "@medusajs/ui"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { TaxRateResponse, TaxRegionResponse } from "@medusajs/types"
|
||||
import { useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
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 { useUpdateTaxRate } from "../../../../../hooks/api/tax-rates"
|
||||
import { ConditionsDrawer } from "../../../common/components/conditions-drawer"
|
||||
import { ConditionEntities, Operators } from "../../../common/constants"
|
||||
import { ConditionsOption } from "../../../common/types"
|
||||
import { Condition } from "../../../tax-rate-create/components"
|
||||
|
||||
const SelectedConditionTypesSchema = zod.object({
|
||||
[ConditionEntities.PRODUCT]: zod.boolean(),
|
||||
[ConditionEntities.PRODUCT_TYPE]: zod.boolean(),
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: zod.boolean(),
|
||||
[ConditionEntities.PRODUCT_TAG]: zod.boolean(),
|
||||
[ConditionEntities.CUSTOMER_GROUP]: zod.boolean(),
|
||||
})
|
||||
|
||||
const ResourceSchema = zod.array(
|
||||
zod.object({
|
||||
label: zod.string(),
|
||||
value: zod.string(),
|
||||
})
|
||||
)
|
||||
|
||||
const UpdateTaxRateSchema = zod.object({
|
||||
name: zod.string().optional(),
|
||||
code: zod.string().optional(),
|
||||
rate: zod.number().optional(),
|
||||
is_combinable: zod.boolean().optional(),
|
||||
selected_condition_types: SelectedConditionTypesSchema,
|
||||
products: ResourceSchema,
|
||||
product_types: ResourceSchema,
|
||||
product_collections: ResourceSchema,
|
||||
product_tags: ResourceSchema,
|
||||
customer_groups: ResourceSchema,
|
||||
})
|
||||
|
||||
export const TaxRateEditForm = ({
|
||||
taxRegion,
|
||||
taxRate,
|
||||
}: {
|
||||
taxRegion: TaxRegionResponse
|
||||
taxRate: TaxRateResponse
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const productRules = taxRate.rules?.filter((r) => r.reference == "product")
|
||||
const productTypeRules = taxRate.rules?.filter(
|
||||
(r) => r.reference == "product_type"
|
||||
)
|
||||
const productCollectionRules = taxRate.rules?.filter(
|
||||
(r) => r.reference == "product_collection"
|
||||
)
|
||||
const productTagRules = taxRate.rules?.filter(
|
||||
(r) => r.reference == "product_tag"
|
||||
)
|
||||
const customerGroupRules = taxRate.rules?.filter(
|
||||
(r) => r.reference == "customer_group"
|
||||
)
|
||||
|
||||
const form = useForm<zod.infer<typeof UpdateTaxRateSchema>>({
|
||||
defaultValues: {
|
||||
name: taxRate.name,
|
||||
code: taxRate.code || undefined,
|
||||
rate: taxRate.rate || undefined,
|
||||
is_combinable: taxRate.is_combinable,
|
||||
selected_condition_types: {
|
||||
[ConditionEntities.PRODUCT]: !!productRules?.length,
|
||||
[ConditionEntities.PRODUCT_TYPE]: !!productTypeRules?.length,
|
||||
[ConditionEntities.PRODUCT_COLLECTION]:
|
||||
!!productCollectionRules?.length,
|
||||
[ConditionEntities.PRODUCT_TAG]: !!productTagRules?.length,
|
||||
[ConditionEntities.CUSTOMER_GROUP]: !!customerGroupRules?.length,
|
||||
},
|
||||
products: productRules.map((r) => ({
|
||||
label: r.reference,
|
||||
value: r.reference_id,
|
||||
})),
|
||||
product_types: productTypeRules.map((r) => ({
|
||||
label: r.reference,
|
||||
value: r.reference_id,
|
||||
})),
|
||||
product_collections: productCollectionRules.map((r) => ({
|
||||
label: r.reference,
|
||||
value: r.reference_id,
|
||||
})),
|
||||
product_tags: productTagRules.map((r) => ({
|
||||
label: r.reference,
|
||||
value: r.reference_id,
|
||||
})),
|
||||
customer_groups: customerGroupRules.map((r) => ({
|
||||
label: r.reference,
|
||||
value: r.reference_id,
|
||||
})),
|
||||
},
|
||||
resolver: zodResolver(UpdateTaxRateSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateTaxRate(taxRate.id)
|
||||
|
||||
const buildRules = (key: string, data: { value: string }[]) =>
|
||||
data?.map((product) => ({
|
||||
reference: key,
|
||||
reference_id: product.value,
|
||||
})) || []
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const rules = [
|
||||
...buildRules("product", data.products),
|
||||
...buildRules("product_type", data.product_types),
|
||||
...buildRules("product_collection", data.product_collections),
|
||||
...buildRules("product_tag", data.product_tags),
|
||||
...buildRules("customer_group", data.customer_groups),
|
||||
]
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
name: data.name,
|
||||
code: data.code || undefined,
|
||||
rate: data.rate,
|
||||
is_combinable: data.is_combinable,
|
||||
rules,
|
||||
},
|
||||
{
|
||||
onSuccess: () => handleSuccess(`/settings/taxes/${taxRegion.id}`),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
const [conditionType, setConditionType] = useState<ConditionEntities | null>(
|
||||
null
|
||||
)
|
||||
const selectedConditionTypes = useWatch({
|
||||
name: "selected_condition_types",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const selectedProducts = useWatch({
|
||||
control: form.control,
|
||||
name: "products",
|
||||
})
|
||||
|
||||
const selectedProductCollections = useWatch({
|
||||
control: form.control,
|
||||
name: "product_collections",
|
||||
})
|
||||
|
||||
const selectedProductTypes = useWatch({
|
||||
control: form.control,
|
||||
name: "product_types",
|
||||
})
|
||||
|
||||
const selectedProductTags = useWatch({
|
||||
control: form.control,
|
||||
name: "product_tags",
|
||||
})
|
||||
|
||||
const selectedCustomerGroups = useWatch({
|
||||
control: form.control,
|
||||
name: "customer_groups",
|
||||
})
|
||||
|
||||
const handleSaveConditions = (type: ConditionEntities) => {
|
||||
return (options: ConditionsOption[]) => {
|
||||
form.setValue(type, options, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTypes = Object.keys(selectedConditionTypes || {})
|
||||
.filter(
|
||||
(k) => selectedConditionTypes[k as keyof typeof selectedConditionTypes]
|
||||
)
|
||||
.sort() as ConditionEntities[]
|
||||
|
||||
const handleOpenDrawer = (type: ConditionEntities, operator: Operators) => {
|
||||
setConditionType(type)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const toggleSelectedConditionTypes = (type: ConditionEntities) => {
|
||||
const state = { ...form.getValues().selected_condition_types }
|
||||
if (state[type]) {
|
||||
delete state[type]
|
||||
} else {
|
||||
state[type] = true
|
||||
}
|
||||
|
||||
form.setValue("selected_condition_types", state, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
}
|
||||
|
||||
const clearAllSelectedConditions = () => {
|
||||
form.setValue(
|
||||
"selected_condition_types",
|
||||
{
|
||||
[ConditionEntities.PRODUCT]: false,
|
||||
[ConditionEntities.PRODUCT_TYPE]: false,
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: false,
|
||||
[ConditionEntities.PRODUCT_TAG]: false,
|
||||
[ConditionEntities.CUSTOMER_GROUP]: false,
|
||||
},
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const [, setSearchParams] = useSearchParams()
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setConditionType(null)
|
||||
setSearchParams(
|
||||
{},
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<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={isPending}>
|
||||
{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 flex-col overflow-auto", {
|
||||
"items-center": !open,
|
||||
})}
|
||||
>
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 py-16">
|
||||
<div>
|
||||
<Heading className="text-left">
|
||||
{t("taxRates.edit.title")}
|
||||
</Heading>
|
||||
|
||||
<Text className="text-ui-fg-subtle txt-small">
|
||||
{t("taxRates.edit.description")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<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.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="rate"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.rate")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<PercentageInput
|
||||
{...field}
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
</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.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{taxRate.tax_region?.parent_id && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="is_combinable"
|
||||
render={({
|
||||
field: { ref, onChange, value, ...field },
|
||||
}) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("taxRates.fields.isCombinable")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!taxRate.is_default && (
|
||||
<div className="flex flex-col gap-y-8">
|
||||
{selectedTypes.length > 0 && (
|
||||
<div className="flex flex-col items-start gap-y-4">
|
||||
{selectedTypes.map((selectedType) => {
|
||||
if (
|
||||
selectedType in (selectedConditionTypes || {})
|
||||
) {
|
||||
const field = form.getValues(selectedType) || []
|
||||
|
||||
return (
|
||||
<Condition
|
||||
key={selectedType}
|
||||
type={selectedType}
|
||||
labels={field.map((f) => f.value)}
|
||||
onClick={() => {
|
||||
setConditionType(selectedType)
|
||||
setOpen(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<DropdownMenu
|
||||
open={isDropdownOpen}
|
||||
onOpenChange={(v) => {
|
||||
v && setIsDropdownOpen(v)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("discounts.conditions.manageTypesAction")}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
onInteractOutside={() => setIsDropdownOpen(false)}
|
||||
>
|
||||
{Object.values(ConditionEntities).map((type) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={type}
|
||||
checked={selectedConditionTypes[type]}
|
||||
onCheckedChange={() =>
|
||||
toggleSelectedConditionTypes(type)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
size="small"
|
||||
weight={
|
||||
selectedConditionTypes[type]
|
||||
? "plus"
|
||||
: "regular"
|
||||
}
|
||||
>
|
||||
{t(`fields.${type}`)}
|
||||
</Text>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
|
||||
{selectedTypes.length > 0 && (
|
||||
<Button
|
||||
variant="transparent"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={clearAllSelectedConditions}
|
||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||
>
|
||||
{t("actions.clearAll")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SplitView.Content>
|
||||
|
||||
<SplitView.Drawer>
|
||||
<ConditionsDrawer
|
||||
product={{
|
||||
selected: selectedProducts,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT),
|
||||
}}
|
||||
productCollection={{
|
||||
selected: selectedProductCollections,
|
||||
onSave: handleSaveConditions(
|
||||
ConditionEntities.PRODUCT_COLLECTION
|
||||
),
|
||||
}}
|
||||
productType={{
|
||||
selected: selectedProductTypes,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT_TYPE),
|
||||
}}
|
||||
productTag={{
|
||||
selected: selectedProductTags,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT_TAG),
|
||||
}}
|
||||
customerGroup={{
|
||||
selected: selectedCustomerGroups,
|
||||
onSave: handleSaveConditions(
|
||||
ConditionEntities.CUSTOMER_GROUP
|
||||
),
|
||||
}}
|
||||
selected={conditionType}
|
||||
/>
|
||||
</SplitView.Drawer>
|
||||
</SplitView>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./tax-rate-edit"
|
||||
|
||||
export { taxRateLoader as loader } from "./loader"
|
||||
export { TaxRateEdit as Component } from "./tax-rate-edit"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { AdminTaxRateResponse } from "@medusajs/types"
|
||||
import { taxRatesQueryKeys } from "../../../hooks/api/tax-rates"
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
|
||||
const taxRateDetailQuery = (id: string) => ({
|
||||
queryKey: taxRatesQueryKeys.detail(id),
|
||||
queryFn: async () => client.taxes.retrieveTaxRate(id),
|
||||
})
|
||||
|
||||
export const taxRateLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.taxRateId
|
||||
const query = taxRateDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<AdminTaxRateResponse>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { useTaxRate } from "../../../hooks/api/tax-rates"
|
||||
import { useTaxRegion } from "../../../hooks/api/tax-regions"
|
||||
import { TaxRateEditForm } from "./components"
|
||||
|
||||
export const TaxRateEdit = () => {
|
||||
const params = useParams()
|
||||
|
||||
const { tax_region: taxRegion } = useTaxRegion(params.id!)
|
||||
const {
|
||||
tax_rate: taxRate,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useTaxRate(params.taxRateId!)
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
taxRegion &&
|
||||
taxRate && (
|
||||
<RouteFocusModal>
|
||||
<TaxRateEditForm taxRegion={taxRegion} taxRate={taxRate} />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./tax-region-create"
|
||||
|
||||
export { TaxRegionCreate as Component } from "./tax-region-create"
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as zod from "zod"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { TaxRegionCreateForm } from "../common/components/tax-region-create-form"
|
||||
|
||||
const CreateTaxRegionForm = zod.object({
|
||||
province_code: zod.string().optional(),
|
||||
country_code: zod.string(),
|
||||
parent_id: zod.string().optional(),
|
||||
name: zod.string(),
|
||||
code: zod.string().optional(),
|
||||
rate: zod.number(),
|
||||
is_combinable: zod.boolean().default(false),
|
||||
})
|
||||
|
||||
export const TaxRegionCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<TaxRegionCreateForm formSchema={CreateTaxRegionForm} />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./tax-rate-list"
|
||||
export * from "./tax-region-general-detail"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tax-rate-list"
|
||||
@@ -0,0 +1,214 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { TaxRateResponse, TaxRegionResponse } from "@medusajs/types"
|
||||
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import {
|
||||
useDeleteTaxRate,
|
||||
useTaxRates,
|
||||
} from "../../../../../hooks/api/tax-rates"
|
||||
import { useDeleteTaxRegion } from "../../../../../hooks/api/tax-regions"
|
||||
import { useTaxRateTableColumns } from "../../../../../hooks/table/columns/use-tax-rates-table-columns"
|
||||
import { useTaxRateTableFilters } from "../../../../../hooks/table/filters/use-tax-rate-table-filters"
|
||||
import { useTaxRateTableQuery } from "../../../../../hooks/table/query/use-tax-rate-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
type TaxRateListProps = {
|
||||
taxRegion: TaxRegionResponse
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
export const TaxRateList = ({
|
||||
taxRegion,
|
||||
isDefault = false,
|
||||
}: TaxRateListProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { searchParams, raw } = useTaxRateTableQuery({ pageSize: PAGE_SIZE })
|
||||
const childrenIds = taxRegion.children?.map((c) => c.id) || []
|
||||
const {
|
||||
tax_rates: taxRates,
|
||||
count,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useTaxRates(
|
||||
{
|
||||
...searchParams,
|
||||
tax_region_id: [taxRegion.id, ...childrenIds],
|
||||
is_default: isDefault,
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
const filters = useTaxRateTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: taxRates ?? [],
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
getRowId: (row) => row.id,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater: setRowSelection,
|
||||
},
|
||||
meta: {
|
||||
taxRegionId: taxRegion.id,
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useDeleteTaxRegion(taxRegion.id)
|
||||
|
||||
const prompt = usePrompt()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleRemove = async () => {
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("taxRegions.removeWarning", {
|
||||
tax_region_name: taxRegion.name,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
setRowSelection({})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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">
|
||||
{isDefault ? `Default ${t("taxRates.domain")}` : `Tax Rate Overrides`}
|
||||
</Heading>
|
||||
|
||||
<Link
|
||||
to={
|
||||
isDefault
|
||||
? `/settings/taxes/${taxRegion.id}/create-default`
|
||||
: `/settings/taxes/${taxRegion.id}/create-override`
|
||||
}
|
||||
>
|
||||
<Button size="small" variant="secondary">
|
||||
Create
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
commands={[
|
||||
{
|
||||
action: handleRemove,
|
||||
label: t("actions.remove"),
|
||||
shortcut: "r",
|
||||
},
|
||||
]}
|
||||
count={count}
|
||||
pagination
|
||||
search
|
||||
filters={filters}
|
||||
navigateTo={(row) =>
|
||||
`/settings/taxes/${taxRegion.id}/tax-rates/${row.id}/edit`
|
||||
}
|
||||
isLoading={isLoading}
|
||||
orderBy={["is_default", "rate", "created_at", "updated_at"]}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<TaxRateResponse>()
|
||||
|
||||
const useColumns = () => {
|
||||
const base = useTaxRateTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
...base,
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
const { taxRegionId } = table.options.meta as {
|
||||
taxRegionId: string
|
||||
}
|
||||
|
||||
return (
|
||||
<TaxRateListActions
|
||||
taxRateId={row.original.id}
|
||||
taxRegionId={taxRegionId}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[base]
|
||||
)
|
||||
}
|
||||
|
||||
const TaxRateListActions = ({
|
||||
taxRateId,
|
||||
taxRegionId,
|
||||
}: {
|
||||
taxRateId: string
|
||||
taxRegionId: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { mutateAsync } = useDeleteTaxRate(taxRateId)
|
||||
|
||||
const onRemove = async () => await mutateAsync()
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/taxes/${taxRegionId}/tax-rates/${taxRateId}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.remove"),
|
||||
onClick: onRemove,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tax-region-general-detail"
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TaxRegionResponse } from "@medusajs/types"
|
||||
import { Container, Heading, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { formatDate } from "../../../../../components/common/date"
|
||||
import { getCountryByIso2 } from "../../../../../lib/countries"
|
||||
|
||||
type TaxRegionGeneralDetailProps = {
|
||||
taxRegion: TaxRegionResponse
|
||||
}
|
||||
|
||||
export const TaxRegionGeneralDetail = ({
|
||||
taxRegion,
|
||||
}: TaxRegionGeneralDetailProps) => {
|
||||
const { t } = useTranslation()
|
||||
const countryCode = taxRegion.parent?.country_code || taxRegion.country_code
|
||||
const displayName = getCountryByIso2(countryCode)?.display_name || countryCode
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<Heading>{displayName}</Heading>
|
||||
|
||||
<Text className="text-ui-fg-subtle" size="small">
|
||||
{t("taxRegions.description")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.created")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" leading="compact">
|
||||
{formatDate(taxRegion.created_at)}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./tax-region-detail"
|
||||
|
||||
export { taxRegionLoader as loader } from "./loader"
|
||||
export { TaxRegionDetail as Component } from "./tax-region-detail"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { AdminTaxRegionResponse } from "@medusajs/types"
|
||||
import { taxRegionsQueryKeys } from "../../../hooks/api/tax-regions"
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
|
||||
const taxRegionDetailQuery = (id: string) => ({
|
||||
queryKey: taxRegionsQueryKeys.detail(id),
|
||||
queryFn: async () => client.taxes.retrieveTaxRegion(id),
|
||||
})
|
||||
|
||||
export const taxRegionLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = taxRegionDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<AdminTaxRegionResponse>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Outlet, useParams } from "react-router-dom"
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { useTaxRegion } from "../../../hooks/api/tax-regions"
|
||||
import { TaxRateList } from "./components/tax-rate-list"
|
||||
import { TaxRegionGeneralDetail } from "./components/tax-region-general-detail"
|
||||
|
||||
export const TaxRegionDetail = () => {
|
||||
const { id } = useParams()
|
||||
const { tax_region: taxRegion, isLoading, isError, error } = useTaxRegion(id!)
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
taxRegion && (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<TaxRegionGeneralDetail taxRegion={taxRegion} />
|
||||
<TaxRateList taxRegion={taxRegion} isDefault={true} />
|
||||
<TaxRateList taxRegion={taxRegion} isDefault={false} />
|
||||
<JsonViewSection data={taxRegion} root="tax_region" />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,31 +1,34 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { Trash } from "@medusajs/icons"
|
||||
import { AdminTaxRegionResponse } from "@medusajs/types"
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { t } from "i18next"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { AdminTaxRegionResponse } from "@medusajs/types"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
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 {
|
||||
useDeleteTaxRegion,
|
||||
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"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { getCountryByIso2 } from "../../../../../lib/countries"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const TaxRegionListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { searchParams, raw } = useTaxRegionTableQuery({ pageSize: PAGE_SIZE })
|
||||
const { searchParams, raw } = useTaxRegionTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { tax_regions, count, isLoading, isError, error } = useTaxRegions({
|
||||
...searchParams,
|
||||
parent_id: "null",
|
||||
})
|
||||
|
||||
const filters = useRegionTableFilters()
|
||||
const columns = useColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
@@ -45,11 +48,9 @@ export const TaxRegionListTable = () => {
|
||||
<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>
|
||||
<Button size="small" variant="secondary" asChild>
|
||||
<Link to="/settings/taxes/create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
@@ -57,11 +58,8 @@ export const TaxRegionListTable = () => {
|
||||
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>
|
||||
@@ -74,6 +72,16 @@ const TaxRegionActions = ({
|
||||
taxRegion: AdminTaxRegionResponse["tax_region"]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { mutateAsync } = useDeleteTaxRegion(taxRegion.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate("/settings/taxes", { replace: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
@@ -81,9 +89,9 @@ const TaxRegionActions = ({
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/taxes/${taxRegion.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -97,20 +105,18 @@ 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"),
|
||||
columnHelper.accessor("country_code", {
|
||||
header: t("fields.country"),
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue()
|
||||
const countryCode = getValue()
|
||||
const displayName =
|
||||
getCountryByIso2(countryCode)?.display_name || countryCode
|
||||
|
||||
return <DateCell date={date} />
|
||||
return (
|
||||
<div className="flex size-full items-center">
|
||||
<span className="truncate">{displayName}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
|
||||
import {
|
||||
remoteQueryObjectFromString,
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
deleteTaxRatesWorkflow,
|
||||
updateTaxRatesWorkflow,
|
||||
} from "@medusajs/core-flows"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
import { refetchTaxRate } from "../helpers"
|
||||
import {
|
||||
AdminGetTaxRateParamsType,
|
||||
AdminUpdateTaxRateType,
|
||||
} from "../validators"
|
||||
import { refetchTaxRate } from "../helpers"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminUpdateTaxRateType>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as QueryConfig from "./query-config"
|
||||
|
||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||
import {
|
||||
AdminCreateTaxRate,
|
||||
AdminCreateTaxRateRule,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||
|
||||
export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
@@ -41,16 +41,6 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
matcher: "/admin/tax-rates",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetTaxRatesParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
matcher: "/admin/tax-rates/:id",
|
||||
@@ -61,6 +51,16 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
matcher: "/admin/tax-rates",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetTaxRatesParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
matcher: "/admin/tax-rates/:id/rules",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export const defaultAdminTaxRateRelations = []
|
||||
export const allowedAdminTaxRateRelations = []
|
||||
export const defaultAdminTaxRateFields = [
|
||||
export const defaults = [
|
||||
"id",
|
||||
"name",
|
||||
"code",
|
||||
@@ -13,14 +11,16 @@ export const defaultAdminTaxRateFields = [
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"*tax_region",
|
||||
"*rules",
|
||||
]
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
defaults: defaultAdminTaxRateFields,
|
||||
defaults,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listTransformQueryConfig = {
|
||||
defaults: defaultAdminTaxRateFields,
|
||||
defaults,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createTaxRatesWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
remoteQueryObjectFromString,
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../types/routing"
|
||||
import { refetchTaxRate } from "./helpers"
|
||||
import {
|
||||
AdminCreateTaxRateType,
|
||||
AdminGetTaxRatesParamsType,
|
||||
} from "./validators"
|
||||
import { refetchTaxRate } from "./helpers"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminCreateTaxRateType>,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createFindParams, createSelectParams } from "../../utils/validators"
|
||||
import { z } from "zod"
|
||||
import {
|
||||
createFindParams,
|
||||
createOperatorMap,
|
||||
createSelectParams,
|
||||
} from "../../utils/validators"
|
||||
|
||||
export type AdminGetTaxRateParamsType = z.infer<typeof AdminGetTaxRateParams>
|
||||
export const AdminGetTaxRateParams = createSelectParams()
|
||||
@@ -8,7 +12,20 @@ export type AdminGetTaxRatesParamsType = z.infer<typeof AdminGetTaxRatesParams>
|
||||
export const AdminGetTaxRatesParams = createFindParams({
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
})
|
||||
}).merge(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
tax_region_id: z
|
||||
.union([z.string(), z.array(z.string()), createOperatorMap()])
|
||||
.optional(),
|
||||
is_default: z.union([z.literal("true"), z.literal("false")]).optional(),
|
||||
created_at: createOperatorMap().optional(),
|
||||
updated_at: createOperatorMap().optional(),
|
||||
deleted_at: createOperatorMap().optional(),
|
||||
$and: z.lazy(() => AdminGetTaxRatesParams.array()).optional(),
|
||||
$or: z.lazy(() => AdminGetTaxRatesParams.array()).optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export type AdminCreateTaxRateRuleType = z.infer<typeof AdminCreateTaxRateRule>
|
||||
export const AdminCreateTaxRateRule = z.object({
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import { deleteTaxRegionsWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const filters = { id: req.params.id }
|
||||
const [taxRegion] = await remoteQuery(
|
||||
remoteQueryObjectFromString({
|
||||
entryPoint: "tax_region",
|
||||
variables: { filters },
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
})
|
||||
)
|
||||
|
||||
res.status(200).json({ tax_region: taxRegion })
|
||||
}
|
||||
|
||||
export const DELETE = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import * as QueryConfig from "./query-config"
|
||||
|
||||
import { AdminCreateTaxRegion, AdminGetTaxRegionsParams } from "./validators"
|
||||
import {
|
||||
AdminCreateTaxRegion,
|
||||
AdminGetTaxRegionParams,
|
||||
AdminGetTaxRegionsParams,
|
||||
} from "./validators"
|
||||
|
||||
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||
@@ -34,4 +38,14 @@ export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
matcher: "/admin/tax-regions/:id",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetTaxRegionParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -9,6 +9,12 @@ export const defaults = [
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"*children",
|
||||
"*children.tax_rates",
|
||||
"*children.tax_rates.rules",
|
||||
"*parent",
|
||||
"*tax_rates",
|
||||
"*tax_rates.rules",
|
||||
]
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
|
||||
@@ -19,9 +19,16 @@ export const AdminGetTaxRegionsParams = createFindParams({
|
||||
}).merge(
|
||||
z.object({
|
||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
country_code: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
province_code: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
parent_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
q: z.string().optional(),
|
||||
country_code: z
|
||||
.union([z.string(), z.array(z.string()), createOperatorMap()])
|
||||
.optional(),
|
||||
province_code: z
|
||||
.union([z.string(), z.array(z.string()), createOperatorMap()])
|
||||
.optional(),
|
||||
parent_id: z
|
||||
.union([z.string(), z.array(z.string()), createOperatorMap()])
|
||||
.optional(),
|
||||
created_by: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
created_at: createOperatorMap().optional(),
|
||||
updated_at: createOperatorMap().optional(),
|
||||
@@ -41,6 +48,9 @@ export const AdminCreateTaxRegion = z.object({
|
||||
rate: z.number().optional(),
|
||||
code: z.string().optional(),
|
||||
name: z.string(),
|
||||
is_combinable: z
|
||||
.union([z.literal("true"), z.literal("false")])
|
||||
.optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
import { TaxRate, TaxRegion, TaxRateRule, TaxProvider } from "@models"
|
||||
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
|
||||
|
||||
export const LinkableKeys: Record<string, string> = {
|
||||
tax_rate_id: TaxRate.name,
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
Filter,
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import TaxRegion from "./tax-region"
|
||||
import TaxRateRule from "./tax-rate-rule"
|
||||
import TaxRegion from "./tax-region"
|
||||
|
||||
type OptionalTaxRateProps = DAL.SoftDeletableEntityDateColumns
|
||||
|
||||
@@ -53,9 +54,11 @@ export default class TaxRate {
|
||||
@Property({ columnType: "real", nullable: true })
|
||||
rate: number | null = null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
code: string | null = null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
@@ -66,7 +69,7 @@ export default class TaxRate {
|
||||
is_combinable = false
|
||||
|
||||
@ManyToOne(() => TaxRegion, {
|
||||
type: "text",
|
||||
columnType: "text",
|
||||
fieldName: "tax_region_id",
|
||||
mapToPk: true,
|
||||
onDelete: "cascade",
|
||||
@@ -79,7 +82,6 @@ export default class TaxRate {
|
||||
|
||||
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, {
|
||||
cascade: ["soft-remove" as Cascade],
|
||||
persist: false,
|
||||
})
|
||||
rules = new Collection<TaxRateRule>(this)
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
Filter,
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Check,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
Check,
|
||||
Cascade,
|
||||
} from "@mikro-orm/core"
|
||||
import TaxRate from "./tax-rate"
|
||||
import TaxProvider from "./tax-provider"
|
||||
import TaxRate from "./tax-rate"
|
||||
|
||||
type OptionalTaxRegionProps = DAL.SoftDeletableEntityDateColumns
|
||||
|
||||
@@ -63,9 +64,14 @@ export default class TaxRegion {
|
||||
})
|
||||
provider_id: string | null = null
|
||||
|
||||
@ManyToOne(() => TaxProvider, { persist: false })
|
||||
provider: TaxProvider
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
country_code: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
province_code: string | null = null
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./tax-rates"
|
||||
export * from "./tax-regions"
|
||||
|
||||
39
packages/types/src/http/tax/admin/tax-rates.ts
Normal file
39
packages/types/src/http/tax/admin/tax-rates.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { PaginatedResponse } from "../../../common"
|
||||
import { TaxRegionResponse } from "./tax-regions"
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface TaxRateResponse {
|
||||
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
|
||||
tax_region: TaxRegionResponse
|
||||
rules: {
|
||||
reference: string
|
||||
reference_id: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface AdminTaxRateResponse {
|
||||
tax_rate: TaxRateResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface AdminTaxRateListResponse extends PaginatedResponse {
|
||||
tax_rates: TaxRateResponse[]
|
||||
}
|
||||
@@ -1,21 +1,29 @@
|
||||
import { PaginatedResponse } from "../../../common"
|
||||
import { TaxRateResponse } from "./tax-rates"
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
interface TaxRegionResponse {
|
||||
export interface TaxRegionResponse {
|
||||
id: string
|
||||
rate: number | null
|
||||
code: string | null
|
||||
country_code: string | null
|
||||
province_code: string | null
|
||||
name: string
|
||||
metadata: Record<string, unknown> | null
|
||||
tax_region_id: string
|
||||
is_combinable: boolean
|
||||
is_default: boolean
|
||||
parent_id: string | null
|
||||
created_at: string | Date
|
||||
updated_at: string | Date
|
||||
deleted_at: Date | null
|
||||
created_by: string | null
|
||||
|
||||
tax_rates: TaxRateResponse[]
|
||||
|
||||
parent: TaxRegionResponse
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user