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:
1 parent
92b633d1cb
commit
00e6b21bb5
77 files changed
+3641
-91
No files matched your search
@@ -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 { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { ITaxModuleService } from "@medusajs/types"
|
||||||
|
|
||||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -58,6 +58,8 @@ medusaIntegrationTestRunner({
|
|||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
created_by: null,
|
created_by: null,
|
||||||
|
rules: [],
|
||||||
|
tax_region: expect.any(Object),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -91,6 +93,9 @@ medusaIntegrationTestRunner({
|
|||||||
created_by: "admin_user",
|
created_by: "admin_user",
|
||||||
provider_id: null,
|
provider_id: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
|
children: [],
|
||||||
|
parent: null,
|
||||||
|
tax_rates: expect.any(Array),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -121,6 +126,13 @@ medusaIntegrationTestRunner({
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
created_by: "admin_user",
|
created_by: "admin_user",
|
||||||
is_combinable: false,
|
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",
|
created_by: "admin_user",
|
||||||
metadata: null,
|
metadata: null,
|
||||||
provider_id: null,
|
provider_id: null,
|
||||||
|
children: [],
|
||||||
|
tax_rates: [],
|
||||||
|
parent: expect.objectContaining({
|
||||||
|
id: usRegionId,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -219,6 +236,9 @@ medusaIntegrationTestRunner({
|
|||||||
created_by: "admin_user",
|
created_by: "admin_user",
|
||||||
provider_id: null,
|
provider_id: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
|
children: [],
|
||||||
|
parent: null,
|
||||||
|
tax_rates: expect.any(Array),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -249,6 +269,13 @@ medusaIntegrationTestRunner({
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
created_by: "admin_user",
|
created_by: "admin_user",
|
||||||
is_combinable: false,
|
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),
|
updated_at: expect.any(String),
|
||||||
created_by: "admin_user",
|
created_by: "admin_user",
|
||||||
is_combinable: true,
|
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()
|
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 () => {
|
it("can create a tax region and delete it", async () => {
|
||||||
const regionRes = await api.post(
|
const regionRes = await api.post(
|
||||||
`/admin/tax-regions`,
|
`/admin/tax-regions`,
|
||||||
|
|||||||
@@ -140,6 +140,27 @@
|
|||||||
},
|
},
|
||||||
"required": ["domain"]
|
"required": ["domain"]
|
||||||
},
|
},
|
||||||
|
"taxRegions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
|
"taxRates": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
"campaigns": {
|
"campaigns": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -633,6 +633,49 @@
|
|||||||
"disabled": "Disabled"
|
"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": {
|
"promotions": {
|
||||||
"domain": "Promotions",
|
"domain": "Promotions",
|
||||||
"fields": {
|
"fields": {
|
||||||
@@ -907,6 +950,7 @@
|
|||||||
},
|
},
|
||||||
"taxes": {
|
"taxes": {
|
||||||
"domain": "Tax Regions",
|
"domain": "Tax Regions",
|
||||||
|
"domainDescription": "Manage your tax region",
|
||||||
"countries": {
|
"countries": {
|
||||||
"taxCountriesHint": "Tax settings apply to the listed countries."
|
"taxCountriesHint": "Tax settings apply to the listed countries."
|
||||||
},
|
},
|
||||||
@@ -1123,6 +1167,7 @@
|
|||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"refundAmount": "Refund amount",
|
"refundAmount": "Refund amount",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"default": "Default",
|
||||||
"lastName": "Last Name",
|
"lastName": "Last Name",
|
||||||
"firstName": "First Name",
|
"firstName": "First Name",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
@@ -1247,6 +1292,7 @@
|
|||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
"variant": "Variant",
|
"variant": "Variant",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"parent": "Parent",
|
||||||
"minSubtotal": "Min. Subtotal",
|
"minSubtotal": "Min. Subtotal",
|
||||||
"maxSubtotal": "Max. Subtotal",
|
"maxSubtotal": "Max. Subtotal",
|
||||||
"shippingProfile": "Shipping Profile",
|
"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)
|
||||||
|
}
|
||||||
+2
@@ -17,6 +17,7 @@ export const DataTableQuery = ({
|
|||||||
prefix,
|
prefix,
|
||||||
}: DataTableQueryProps) => {
|
}: DataTableQueryProps) => {
|
||||||
return (
|
return (
|
||||||
|
(search || orderBy || filters || prefix) && (
|
||||||
<div className="flex items-start justify-between gap-x-4 px-6 py-4">
|
<div className="flex items-start justify-between gap-x-4 px-6 py-4">
|
||||||
<div className="w-full max-w-[60%]">
|
<div className="w-full max-w-[60%]">
|
||||||
{filters && filters.length > 0 && (
|
{filters && filters.length > 0 && (
|
||||||
@@ -29,4 +30,5 @@ export const DataTableQuery = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./type-cell"
|
||||||
+33
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 { AdminCreateTaxRegion } from "@medusajs/medusa"
|
||||||
import { client } from "../../lib/client"
|
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
|
||||||
import {
|
import {
|
||||||
AdminTaxRegionResponse,
|
|
||||||
AdminTaxRegionListResponse,
|
AdminTaxRegionListResponse,
|
||||||
|
AdminTaxRegionResponse,
|
||||||
} from "@medusajs/types"
|
} 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 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 = (
|
export const useTaxRegion = (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -51,3 +60,38 @@ export const useTaxRegions = (
|
|||||||
|
|
||||||
return { ...data, ...rest }
|
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 {
|
import {
|
||||||
|
AdminCreateTaxRegion,
|
||||||
|
AdminPostTaxRatesTaxRateReq,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
AdminTaxRateResponse,
|
||||||
AdminTaxRegionListResponse,
|
AdminTaxRegionListResponse,
|
||||||
AdminTaxRegionResponse,
|
AdminTaxRegionResponse,
|
||||||
} from "@medusajs/types"
|
} 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>) {
|
async function retrieveTaxRegion(id: string, query?: Record<string, any>) {
|
||||||
return getRequest<AdminTaxRegionResponse>(`/admin/tax-regions/${id}`, query)
|
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)
|
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 = {
|
export const taxes = {
|
||||||
retrieveTaxRegion,
|
retrieveTaxRegion,
|
||||||
listTaxRegions,
|
listTaxRegions,
|
||||||
|
retrieveTaxRate,
|
||||||
|
listTaxRates,
|
||||||
|
updateTaxRate,
|
||||||
|
createTaxRegion,
|
||||||
|
deleteTaxRegion,
|
||||||
|
createTaxRate,
|
||||||
|
deleteTaxRate,
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
/** This file is auto-generated. Do not modify it manually. */
|
/** This file is auto-generated. Do not modify it manually. */
|
||||||
import type { RegionCountryDTO } from "@medusajs/types"
|
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">[] = [
|
export const countries: Omit<RegionCountryDTO, "id">[] = [
|
||||||
{
|
{
|
||||||
iso_2: "af",
|
iso_2: "af",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
AdminApiKeyResponse,
|
AdminApiKeyResponse,
|
||||||
AdminCustomerGroupResponse,
|
AdminCustomerGroupResponse,
|
||||||
AdminProductCategoryResponse,
|
AdminProductCategoryResponse,
|
||||||
|
AdminTaxRateResponse,
|
||||||
|
AdminTaxRegionResponse,
|
||||||
SalesChannelDTO,
|
SalesChannelDTO,
|
||||||
UserDTO,
|
UserDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
@@ -757,10 +759,59 @@ export const v2Routes: RouteObject[] = [
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
lazy: () => import("../../v2-routes/taxes/tax-region-list"),
|
lazy: () => import("../../v2-routes/taxes/tax-region-list"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/taxes/tax-region-create"),
|
||||||
children: [],
|
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
|
} & ListRes
|
||||||
export type ProductCollectionDeleteRes = DeleteRes
|
export type ProductCollectionDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Taxes
|
||||||
|
export type TaxRegionDeleteRes = DeleteRes
|
||||||
|
export type TaxRateDeleteRes = DeleteRes
|
||||||
|
|
||||||
// Inventory Items
|
// Inventory Items
|
||||||
export type InventoryItemRes = {
|
export type InventoryItemRes = {
|
||||||
inventory_item: InventoryNext.InventoryItemDTO & {
|
inventory_item: InventoryNext.InventoryItemDTO & {
|
||||||
|
|||||||
+3
-2
@@ -1,18 +1,18 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { RegionDTO } from "@medusajs/types"
|
||||||
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { RegionDTO } from "@medusajs/types"
|
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
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 { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns"
|
||||||
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
|
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
|
||||||
import { useRegionTableQuery } from "../../../../../hooks/table/query/use-region-table-query"
|
import { useRegionTableQuery } from "../../../../../hooks/table/query/use-region-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useDeleteRegion, useRegions } from "../../../../../hooks/api/regions"
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ export const RegionListTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
+663
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./conditions-drawer"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-region-create-form"
|
||||||
+250
@@ -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",
|
||||||
|
}
|
||||||
+46
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+60
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+46
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+48
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+49
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+17
@@ -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
|
||||||
|
}
|
||||||
+17
@@ -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
|
||||||
|
}
|
||||||
+17
@@ -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
|
||||||
|
}
|
||||||
+17
@@ -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
|
||||||
|
}
|
||||||
+42
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -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"
|
||||||
+36
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+53
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./condition"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./condition"
|
||||||
|
export * from "./tax-rate-create-form"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-rate-create-form"
|
||||||
+480
@@ -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"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-rate-edit-form"
|
||||||
+488
@@ -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"
|
||||||
+21
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./tax-rate-list"
|
||||||
|
export * from "./tax-region-general-detail"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-rate-list"
|
||||||
+214
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-region-general-detail"
|
||||||
+41
@@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
+30
@@ -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>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
+40
-34
@@ -1,31 +1,34 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { Trash } from "@medusajs/icons"
|
||||||
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
import { AdminTaxRegionResponse } from "@medusajs/types"
|
||||||
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { t } from "i18next"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
import { AdminTaxRegionResponse } from "@medusajs/types"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
|
import {
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
useDeleteTaxRegion,
|
||||||
import { useDeleteRegion } from "../../../../../hooks/api/regions"
|
useTaxRegions,
|
||||||
import { useTaxRegions } from "../../../../../hooks/api/tax-regions"
|
} from "../../../../../hooks/api/tax-regions"
|
||||||
import { useTaxRegionTableQuery } from "../../../../../hooks/table/query/use-tax-region-table-query copy"
|
import { useTaxRegionTableQuery } from "../../../../../hooks/table/query/use-tax-region-table-query copy"
|
||||||
import { t } from "i18next"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
|
import { getCountryByIso2 } from "../../../../../lib/countries"
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
export const TaxRegionListTable = () => {
|
export const TaxRegionListTable = () => {
|
||||||
const { t } = useTranslation()
|
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({
|
const { tax_regions, count, isLoading, isError, error } = useTaxRegions({
|
||||||
...searchParams,
|
...searchParams,
|
||||||
|
parent_id: "null",
|
||||||
})
|
})
|
||||||
|
|
||||||
const filters = useRegionTableFilters()
|
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
@@ -45,11 +48,9 @@ export const TaxRegionListTable = () => {
|
|||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">{t("taxes.domain")}</Heading>
|
<Heading level="h2">{t("taxes.domain")}</Heading>
|
||||||
<Link to="/settings/taxes/create">
|
<Button size="small" variant="secondary" asChild>
|
||||||
<Button size="small" variant="secondary">
|
<Link to="/settings/taxes/create">{t("actions.create")}</Link>
|
||||||
{t("actions.create")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
@@ -57,11 +58,8 @@ export const TaxRegionListTable = () => {
|
|||||||
count={count}
|
count={count}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
filters={filters}
|
|
||||||
orderBy={["name", "created_at", "updated_at"]}
|
|
||||||
navigateTo={(row) => `${row.original.id}`}
|
navigateTo={(row) => `${row.original.id}`}
|
||||||
pagination
|
pagination
|
||||||
search
|
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -74,6 +72,16 @@ const TaxRegionActions = ({
|
|||||||
taxRegion: AdminTaxRegionResponse["tax_region"]
|
taxRegion: AdminTaxRegionResponse["tax_region"]
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { mutateAsync } = useDeleteTaxRegion(taxRegion.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate("/settings/taxes", { replace: true })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
@@ -81,9 +89,9 @@ const TaxRegionActions = ({
|
|||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: t("actions.edit"),
|
icon: <Trash />,
|
||||||
to: `/settings/taxes/${taxRegion.id}/edit`,
|
label: t("actions.delete"),
|
||||||
icon: <PencilSquare />,
|
onClick: handleDelete,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -97,20 +105,18 @@ const columnHelper = createColumnHelper<AdminTaxRegionResponse["tax_region"]>()
|
|||||||
const useColumns = () => {
|
const useColumns = () => {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("country_code", {
|
||||||
header: t("fields.code"),
|
header: t("fields.country"),
|
||||||
cell: ({ getValue }) => (
|
|
||||||
<div className="flex size-full items-center">
|
|
||||||
<span className="truncate">{getValue()}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("created_at", {
|
|
||||||
header: t("fields.created"),
|
|
||||||
cell: ({ getValue }) => {
|
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({
|
columnHelper.display({
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import {
|
|
||||||
AuthenticatedMedusaRequest,
|
|
||||||
MedusaResponse,
|
|
||||||
} from "../../../../types/routing"
|
|
||||||
|
|
||||||
import {
|
|
||||||
remoteQueryObjectFromString,
|
|
||||||
ContainerRegistrationKeys,
|
|
||||||
} from "@medusajs/utils"
|
|
||||||
import {
|
import {
|
||||||
deleteTaxRatesWorkflow,
|
deleteTaxRatesWorkflow,
|
||||||
updateTaxRatesWorkflow,
|
updateTaxRatesWorkflow,
|
||||||
} from "@medusajs/core-flows"
|
} from "@medusajs/core-flows"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../types/routing"
|
||||||
|
import { refetchTaxRate } from "../helpers"
|
||||||
import {
|
import {
|
||||||
AdminGetTaxRateParamsType,
|
AdminGetTaxRateParamsType,
|
||||||
AdminUpdateTaxRateType,
|
AdminUpdateTaxRateType,
|
||||||
} from "../validators"
|
} from "../validators"
|
||||||
import { refetchTaxRate } from "../helpers"
|
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: AuthenticatedMedusaRequest<AdminUpdateTaxRateType>,
|
req: AuthenticatedMedusaRequest<AdminUpdateTaxRateType>,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as QueryConfig from "./query-config"
|
import * as QueryConfig from "./query-config"
|
||||||
|
|
||||||
|
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||||
import {
|
import {
|
||||||
AdminCreateTaxRate,
|
AdminCreateTaxRate,
|
||||||
AdminCreateTaxRateRule,
|
AdminCreateTaxRateRule,
|
||||||
@@ -11,7 +12,6 @@ import {
|
|||||||
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
|
||||||
|
|
||||||
export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
|
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",
|
method: "GET",
|
||||||
matcher: "/admin/tax-rates/:id",
|
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",
|
method: "POST",
|
||||||
matcher: "/admin/tax-rates/:id/rules",
|
matcher: "/admin/tax-rates/:id/rules",
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export const defaultAdminTaxRateRelations = []
|
export const defaults = [
|
||||||
export const allowedAdminTaxRateRelations = []
|
|
||||||
export const defaultAdminTaxRateFields = [
|
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"code",
|
"code",
|
||||||
@@ -13,14 +11,16 @@ export const defaultAdminTaxRateFields = [
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
"deleted_at",
|
"deleted_at",
|
||||||
"metadata",
|
"metadata",
|
||||||
|
"*tax_region",
|
||||||
|
"*rules",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveTransformQueryConfig = {
|
export const retrieveTransformQueryConfig = {
|
||||||
defaults: defaultAdminTaxRateFields,
|
defaults,
|
||||||
isList: false,
|
isList: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listTransformQueryConfig = {
|
export const listTransformQueryConfig = {
|
||||||
defaults: defaultAdminTaxRateFields,
|
defaults,
|
||||||
isList: true,
|
isList: true,
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import { createTaxRatesWorkflow } from "@medusajs/core-flows"
|
import { createTaxRatesWorkflow } from "@medusajs/core-flows"
|
||||||
import {
|
import {
|
||||||
remoteQueryObjectFromString,
|
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../types/routing"
|
} from "../../../types/routing"
|
||||||
|
import { refetchTaxRate } from "./helpers"
|
||||||
import {
|
import {
|
||||||
AdminCreateTaxRateType,
|
AdminCreateTaxRateType,
|
||||||
AdminGetTaxRatesParamsType,
|
AdminGetTaxRatesParamsType,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
import { refetchTaxRate } from "./helpers"
|
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: AuthenticatedMedusaRequest<AdminCreateTaxRateType>,
|
req: AuthenticatedMedusaRequest<AdminCreateTaxRateType>,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { createFindParams, createSelectParams } from "../../utils/validators"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
createFindParams,
|
||||||
|
createOperatorMap,
|
||||||
|
createSelectParams,
|
||||||
|
} from "../../utils/validators"
|
||||||
|
|
||||||
export type AdminGetTaxRateParamsType = z.infer<typeof AdminGetTaxRateParams>
|
export type AdminGetTaxRateParamsType = z.infer<typeof AdminGetTaxRateParams>
|
||||||
export const AdminGetTaxRateParams = createSelectParams()
|
export const AdminGetTaxRateParams = createSelectParams()
|
||||||
@@ -8,7 +12,20 @@ export type AdminGetTaxRatesParamsType = z.infer<typeof AdminGetTaxRatesParams>
|
|||||||
export const AdminGetTaxRatesParams = createFindParams({
|
export const AdminGetTaxRatesParams = createFindParams({
|
||||||
limit: 20,
|
limit: 20,
|
||||||
offset: 0,
|
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 type AdminCreateTaxRateRuleType = z.infer<typeof AdminCreateTaxRateRule>
|
||||||
export const AdminCreateTaxRateRule = z.object({
|
export const AdminCreateTaxRateRule = z.object({
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
import { deleteTaxRegionsWorkflow } from "@medusajs/core-flows"
|
import { deleteTaxRegionsWorkflow } from "@medusajs/core-flows"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../../types/routing"
|
} 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 (
|
export const DELETE = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import * as QueryConfig from "./query-config"
|
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 { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
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",
|
"updated_at",
|
||||||
"deleted_at",
|
"deleted_at",
|
||||||
"metadata",
|
"metadata",
|
||||||
|
"*children",
|
||||||
|
"*children.tax_rates",
|
||||||
|
"*children.tax_rates.rules",
|
||||||
|
"*parent",
|
||||||
|
"*tax_rates",
|
||||||
|
"*tax_rates.rules",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveTransformQueryConfig = {
|
export const retrieveTransformQueryConfig = {
|
||||||
|
|||||||
@@ -19,9 +19,16 @@ export const AdminGetTaxRegionsParams = createFindParams({
|
|||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
country_code: z.union([z.string(), z.array(z.string())]).optional(),
|
q: z.string().optional(),
|
||||||
province_code: z.union([z.string(), z.array(z.string())]).optional(),
|
country_code: z
|
||||||
parent_id: z.union([z.string(), z.array(z.string())]).optional(),
|
.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_by: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
created_at: createOperatorMap().optional(),
|
created_at: createOperatorMap().optional(),
|
||||||
updated_at: createOperatorMap().optional(),
|
updated_at: createOperatorMap().optional(),
|
||||||
@@ -41,6 +48,9 @@ export const AdminCreateTaxRegion = z.object({
|
|||||||
rate: z.number().optional(),
|
rate: z.number().optional(),
|
||||||
code: z.string().optional(),
|
code: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
is_combinable: z
|
||||||
|
.union([z.literal("true"), z.literal("false")])
|
||||||
|
.optional(),
|
||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.unknown()).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
import { MapToConfig } from "@medusajs/utils"
|
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> = {
|
export const LinkableKeys: Record<string, string> = {
|
||||||
tax_rate_id: TaxRate.name,
|
tax_rate_id: TaxRate.name,
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { DAL } from "@medusajs/types"
|
import { DAL } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
DALUtils,
|
|
||||||
createPsqlIndexStatementHelper,
|
createPsqlIndexStatementHelper,
|
||||||
|
DALUtils,
|
||||||
generateEntityId,
|
generateEntityId,
|
||||||
|
Searchable,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
Filter,
|
|
||||||
BeforeCreate,
|
BeforeCreate,
|
||||||
Cascade,
|
Cascade,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
|
Filter,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OnInit,
|
|
||||||
OneToMany,
|
OneToMany,
|
||||||
|
OnInit,
|
||||||
OptionalProps,
|
OptionalProps,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import TaxRegion from "./tax-region"
|
|
||||||
import TaxRateRule from "./tax-rate-rule"
|
import TaxRateRule from "./tax-rate-rule"
|
||||||
|
import TaxRegion from "./tax-region"
|
||||||
|
|
||||||
type OptionalTaxRateProps = DAL.SoftDeletableEntityDateColumns
|
type OptionalTaxRateProps = DAL.SoftDeletableEntityDateColumns
|
||||||
|
|
||||||
@@ -53,9 +54,11 @@ export default class TaxRate {
|
|||||||
@Property({ columnType: "real", nullable: true })
|
@Property({ columnType: "real", nullable: true })
|
||||||
rate: number | null = null
|
rate: number | null = null
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
code: string | null = null
|
code: string | null = null
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ export default class TaxRate {
|
|||||||
is_combinable = false
|
is_combinable = false
|
||||||
|
|
||||||
@ManyToOne(() => TaxRegion, {
|
@ManyToOne(() => TaxRegion, {
|
||||||
type: "text",
|
columnType: "text",
|
||||||
fieldName: "tax_region_id",
|
fieldName: "tax_region_id",
|
||||||
mapToPk: true,
|
mapToPk: true,
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
@@ -79,7 +82,6 @@ export default class TaxRate {
|
|||||||
|
|
||||||
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, {
|
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, {
|
||||||
cascade: ["soft-remove" as Cascade],
|
cascade: ["soft-remove" as Cascade],
|
||||||
persist: false,
|
|
||||||
})
|
})
|
||||||
rules = new Collection<TaxRateRule>(this)
|
rules = new Collection<TaxRateRule>(this)
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import { DAL } from "@medusajs/types"
|
import { DAL } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
DALUtils,
|
DALUtils,
|
||||||
|
Searchable,
|
||||||
createPsqlIndexStatementHelper,
|
createPsqlIndexStatementHelper,
|
||||||
generateEntityId,
|
generateEntityId,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
Filter,
|
|
||||||
BeforeCreate,
|
BeforeCreate,
|
||||||
|
Cascade,
|
||||||
|
Check,
|
||||||
Collection,
|
Collection,
|
||||||
Entity,
|
Entity,
|
||||||
|
Filter,
|
||||||
|
ManyToOne,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
OneToMany,
|
||||||
OptionalProps,
|
OptionalProps,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
OneToMany,
|
|
||||||
ManyToOne,
|
|
||||||
Check,
|
|
||||||
Cascade,
|
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import TaxRate from "./tax-rate"
|
|
||||||
import TaxProvider from "./tax-provider"
|
import TaxProvider from "./tax-provider"
|
||||||
|
import TaxRate from "./tax-rate"
|
||||||
|
|
||||||
type OptionalTaxRegionProps = DAL.SoftDeletableEntityDateColumns
|
type OptionalTaxRegionProps = DAL.SoftDeletableEntityDateColumns
|
||||||
|
|
||||||
@@ -63,9 +64,14 @@ export default class TaxRegion {
|
|||||||
})
|
})
|
||||||
provider_id: string | null = null
|
provider_id: string | null = null
|
||||||
|
|
||||||
|
@ManyToOne(() => TaxProvider, { persist: false })
|
||||||
|
provider: TaxProvider
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
country_code: string
|
country_code: string
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
province_code: string | null = null
|
province_code: string | null = null
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from "./tax-rates"
|
||||||
export * from "./tax-regions"
|
export * from "./tax-regions"
|
||||||
@@ -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 { PaginatedResponse } from "../../../common"
|
||||||
|
import { TaxRateResponse } from "./tax-rates"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
interface TaxRegionResponse {
|
export interface TaxRegionResponse {
|
||||||
id: string
|
id: string
|
||||||
rate: number | null
|
rate: number | null
|
||||||
code: string | null
|
code: string | null
|
||||||
|
country_code: string | null
|
||||||
|
province_code: string | null
|
||||||
name: string
|
name: string
|
||||||
metadata: Record<string, unknown> | null
|
metadata: Record<string, unknown> | null
|
||||||
tax_region_id: string
|
tax_region_id: string
|
||||||
is_combinable: boolean
|
is_combinable: boolean
|
||||||
is_default: boolean
|
is_default: boolean
|
||||||
|
parent_id: string | null
|
||||||
created_at: string | Date
|
created_at: string | Date
|
||||||
updated_at: string | Date
|
updated_at: string | Date
|
||||||
deleted_at: Date | null
|
deleted_at: Date | null
|
||||||
created_by: string | null
|
created_by: string | null
|
||||||
|
|
||||||
|
tax_rates: TaxRateResponse[]
|
||||||
|
|
||||||
|
parent: TaxRegionResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user