feat(medusa,dashboard): Tax settings pages and fixes to list tax rates endpoint (#6606)
**What** - Adds pages for managing tax settings and tax rates for regions. - Fixes `list` tax rates endpoint, which was missing pagination, search, order, and filters. **Note** The fix to the tax rate list endpoint is very rough, as I have had to reimplement most of the logic from our transformQuery middleware. This is because this endpoints does not follow normal convention for fields and expand and uses string arrays instead of strings separated by commas. Our middleware does not support this, and changing the endpoint to align with other endpoints on the expand and fields params would be a breaking change. Since this is very temporary until 2.0 I think it's okay for the time being. CLOSES CORE-1654
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/client-types": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): Fixes pagination on list Tax Rate endpoint, and also adds missing query params like order, search and filters.
|
||||||
@@ -347,7 +347,7 @@
|
|||||||
"removeCountriesWarning_one": "You are about to remove {{count}} country from the region. This action cannot be undone.",
|
"removeCountriesWarning_one": "You are about to remove {{count}} country from the region. This action cannot be undone.",
|
||||||
"removeCountriesWarning_other": "You are about to remove {{count}} countries from the region. This action cannot be undone.",
|
"removeCountriesWarning_other": "You are about to remove {{count}} countries from the region. This action cannot be undone.",
|
||||||
"removeCountryWarning": "You are about to remove the country {{name}} from the region. This action cannot be undone.",
|
"removeCountryWarning": "You are about to remove the country {{name}} from the region. This action cannot be undone.",
|
||||||
"taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.",
|
"taxInclusiveHint": "When enabled prices in the region will be tax inclusive.",
|
||||||
"providersHint": " Add which fulfillment and payment providers should be available in this region.",
|
"providersHint": " Add which fulfillment and payment providers should be available in this region.",
|
||||||
"shippingOptions": "Shipping Options",
|
"shippingOptions": "Shipping Options",
|
||||||
"deleteShippingOptionWarning": "You are about to delete the shipping option {{name}}. This action cannot be undone.",
|
"deleteShippingOptionWarning": "You are about to delete the shipping option {{name}}. This action cannot be undone.",
|
||||||
@@ -382,6 +382,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"taxes": {
|
||||||
|
"domain": "Taxes",
|
||||||
|
"countries": {
|
||||||
|
"taxCountriesHint": "Tax settings apply to the listed countries."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"editTaxSettings": "Edit Tax Settings",
|
||||||
|
"taxProviderLabel": "Tax provider",
|
||||||
|
"systemTaxProviderLabel": "System Tax Provider",
|
||||||
|
"calculateTaxesAutomaticallyLabel": "Calculate taxes automatically",
|
||||||
|
"calculateTaxesAutomaticallyHint": "When enabled, tax rates will be calculated automatically and applied to carts. When disabled, taxes must be manually computed at checkout. Manual taxes are recommended for usage with third-party tax providers.",
|
||||||
|
"applyTaxesOnGiftCardsLabel": "Apply taxes on gift cards",
|
||||||
|
"applyTaxesOnGiftCardsHint": "When enabled, taxes will be applied to gift cards at checkout. In some countries, tax regulations require the application of taxes to gift cards upon purchase.",
|
||||||
|
"defaultTaxRateLabel": "Default tax rate",
|
||||||
|
"defaultTaxCodeLabel": "Default tax code"
|
||||||
|
},
|
||||||
|
"defaultRate": {
|
||||||
|
"sectionTitle": "Default Tax Rate"
|
||||||
|
},
|
||||||
|
"taxRate": {
|
||||||
|
"sectionTitle": "Tax Rates",
|
||||||
|
"createTaxRate": "Create Tax Rate",
|
||||||
|
"createTaxRateHint": "Create a new tax rate for the region.",
|
||||||
|
"deleteRateDescription": "You are about to delete the tax rate {{name}}. This action cannot be undone.",
|
||||||
|
"editTaxRate": "Edit Tax Rate",
|
||||||
|
"editRateAction": "Edit rate",
|
||||||
|
"editOverridesAction": "Edit overrides",
|
||||||
|
"editOverridesTitle": "Edit Tax Rate Overrides",
|
||||||
|
"editOverridesHint": "Specify the overrides for the tax rate.",
|
||||||
|
"deleteTaxRateWarning": "You are about to delete the tax rate {{name}}. This action cannot be undone.",
|
||||||
|
"productOverridesLabel": "Product overrides",
|
||||||
|
"productOverridesHint": "Specify the product overrides for the tax rate.",
|
||||||
|
"addProductOverridesAction": "Add product overrides",
|
||||||
|
"productTypeOverridesLabel": "Product type overrides",
|
||||||
|
"productTypeOverridesHint": "Specify the product type overrides for the tax rate.",
|
||||||
|
"addProductTypeOverridesAction": "Add product type overrides",
|
||||||
|
"shippingOptionOverridesLabel": "Shipping option overrides",
|
||||||
|
"shippingOptionOverridesHint": "Specify the shipping option overrides for the tax rate.",
|
||||||
|
"addShippingOptionOverridesAction": "Add shipping option overrides",
|
||||||
|
"productOverridesHeader": "Products",
|
||||||
|
"productTypeOverridesHeader": "Product Types",
|
||||||
|
"shippingOptionOverridesHeader": "Shipping Options"
|
||||||
|
}
|
||||||
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"domain": "Locations",
|
"domain": "Locations",
|
||||||
"createLocation": "Create location",
|
"createLocation": "Create location",
|
||||||
@@ -503,7 +547,7 @@
|
|||||||
"availability": "Availability",
|
"availability": "Availability",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"optional": "Optional",
|
"optional": "Optional",
|
||||||
"taxInclusivePricing": "Tax Inclusive Pricing",
|
"taxInclusivePricing": "Tax inclusive pricing",
|
||||||
"taxRate": "Tax Rate",
|
"taxRate": "Tax Rate",
|
||||||
"taxCode": "Tax Code",
|
"taxCode": "Tax Code",
|
||||||
"currency": "Currency",
|
"currency": "Currency",
|
||||||
@@ -568,9 +612,9 @@
|
|||||||
"maxSubtotal": "Max. Subtotal",
|
"maxSubtotal": "Max. Subtotal",
|
||||||
"shippingProfile": "Shipping Profile",
|
"shippingProfile": "Shipping Profile",
|
||||||
"summary": "Summary",
|
"summary": "Summary",
|
||||||
"shippingProfile": "Shipping Profile"
|
"rate": "Rate"
|
||||||
},
|
},
|
||||||
"dateTime" : {
|
"dateTime": {
|
||||||
"years_one": "Year",
|
"years_one": "Year",
|
||||||
"years_other": "Years",
|
"years_other": "Years",
|
||||||
"months_one": "Month",
|
"months_one": "Month",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./percentage-input"
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import { Input, Text } from "@medusajs/ui"
|
||||||
|
import { ComponentProps, ElementRef, forwardRef } from "react"
|
||||||
|
|
||||||
|
export const PercentageInput = forwardRef<
|
||||||
|
ElementRef<typeof Input>,
|
||||||
|
Omit<ComponentProps<typeof Input>, "type">
|
||||||
|
>(({ min = 0, max = 100, step = 0.0001, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 z-10 flex w-8 items-center justify-center border-r">
|
||||||
|
<Text
|
||||||
|
className="text-ui-fg-muted"
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
weight="plus"
|
||||||
|
>
|
||||||
|
%
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
type="number"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
{...props}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
PercentageInput.displayName = "HandleInput"
|
||||||
+1
-1
@@ -109,7 +109,7 @@ export const DateFilter = ({
|
|||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
collisionPadding={24}
|
collisionPadding={24}
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] w-[300px] overflow-hidden rounded-lg"
|
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout h-full max-h-[var(--radix-popper-available-height)] w-[300px] overflow-auto rounded-lg"
|
||||||
)}
|
)}
|
||||||
onInteractOutside={(e) => {
|
onInteractOutside={(e) => {
|
||||||
if (e.target instanceof HTMLElement) {
|
if (e.target instanceof HTMLElement) {
|
||||||
|
|||||||
+89
-66
@@ -9,6 +9,7 @@ type DataTableSkeletonProps = {
|
|||||||
orderBy: boolean
|
orderBy: boolean
|
||||||
filterable: boolean
|
filterable: boolean
|
||||||
pagination: boolean
|
pagination: boolean
|
||||||
|
layout?: "fit" | "fill"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataTableSkeleton = ({
|
export const DataTableSkeleton = ({
|
||||||
@@ -18,6 +19,7 @@ export const DataTableSkeleton = ({
|
|||||||
searchable,
|
searchable,
|
||||||
orderBy,
|
orderBy,
|
||||||
pagination,
|
pagination,
|
||||||
|
layout = "fit",
|
||||||
}: DataTableSkeletonProps) => {
|
}: DataTableSkeletonProps) => {
|
||||||
const rows = Array.from({ length: rowCount }, (_, i) => i)
|
const rows = Array.from({ length: rowCount }, (_, i) => i)
|
||||||
|
|
||||||
@@ -30,7 +32,11 @@ export const DataTableSkeleton = ({
|
|||||||
const colWidth = 100 / colCount
|
const colWidth = 100 / colCount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
className={clx({
|
||||||
|
"flex h-full flex-col overflow-hidden": layout === "fill",
|
||||||
|
})}
|
||||||
|
>
|
||||||
{hasToolbar && (
|
{hasToolbar && (
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
{filterable && <Skeleton className="h-7 w-full max-w-[160px]" />}
|
{filterable && <Skeleton className="h-7 w-full max-w-[160px]" />}
|
||||||
@@ -42,74 +48,91 @@ export const DataTableSkeleton = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Table>
|
<div
|
||||||
<Table.Header>
|
className={clx("flex w-full flex-col overflow-hidden", {
|
||||||
<Table.Row
|
"flex flex-1 flex-col": layout === "fill",
|
||||||
className={clx({
|
})}
|
||||||
"border-b-0 [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap":
|
>
|
||||||
hasActions,
|
<div
|
||||||
"[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap":
|
className={clx("w-full", {
|
||||||
hasSelect,
|
"min-h-0 flex-grow overflow-hidden": layout === "fill",
|
||||||
|
"overflow-x-auto": layout === "fit",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Table>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row
|
||||||
|
className={clx({
|
||||||
|
"border-b-0 [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap":
|
||||||
|
hasActions,
|
||||||
|
"[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap":
|
||||||
|
hasSelect,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{columns.map((col, i) => {
|
||||||
|
const isSelectHeader = col.id === "select"
|
||||||
|
const isActionsHeader = col.id === "actions"
|
||||||
|
|
||||||
|
const isSpecialHeader = isSelectHeader || isActionsHeader
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.HeaderCell
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
width: !isSpecialHeader ? `${colWidth}%` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isActionsHeader ? null : (
|
||||||
|
<Skeleton
|
||||||
|
className={clx("h-7", {
|
||||||
|
"w-7": isSelectHeader,
|
||||||
|
"w-full": !isSelectHeader,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Table.HeaderCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{rows.map((_, j) => (
|
||||||
|
<Table.Row key={j}>
|
||||||
|
{columns.map((col, k) => {
|
||||||
|
const isSpecialCell =
|
||||||
|
col.id === "select" || col.id === "actions"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Cell key={k}>
|
||||||
|
<Skeleton
|
||||||
|
className={clx("h-7", {
|
||||||
|
"w-7": isSpecialCell,
|
||||||
|
"w-full": !isSpecialCell,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
{pagination && (
|
||||||
|
<div
|
||||||
|
className={clx("flex items-center justify-between p-4", {
|
||||||
|
"border-t": layout === "fill",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{columns.map((col, i) => {
|
<Skeleton className="h-7 w-[138px]" />
|
||||||
const isSelectHeader = col.id === "select"
|
<div className="flex items-center gap-x-2">
|
||||||
const isActionsHeader = col.id === "actions"
|
<Skeleton className="h-7 w-24" />
|
||||||
|
<Skeleton className="h-7 w-11" />
|
||||||
const isSpecialHeader = isSelectHeader || isActionsHeader
|
<Skeleton className="h-7 w-11" />
|
||||||
|
</div>
|
||||||
return (
|
|
||||||
<Table.HeaderCell
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
width: !isSpecialHeader ? `${colWidth}%` : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isActionsHeader ? null : (
|
|
||||||
<Skeleton
|
|
||||||
className={clx("h-7", {
|
|
||||||
"w-7": isSelectHeader,
|
|
||||||
"w-full": !isSelectHeader,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Table.HeaderCell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{rows.map((_, j) => (
|
|
||||||
<Table.Row key={j}>
|
|
||||||
{columns.map((col, k) => {
|
|
||||||
const isSpecialCell =
|
|
||||||
col.id === "select" || col.id === "actions"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.Cell key={k}>
|
|
||||||
<Skeleton
|
|
||||||
className={clx("h-7", {
|
|
||||||
"w-7": isSpecialCell,
|
|
||||||
"w-full": !isSpecialCell,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
{pagination && (
|
|
||||||
<div className="flex items-center justify-between p-4">
|
|
||||||
<Skeleton className="h-7 w-[138px]" />
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<Skeleton className="h-7 w-24" />
|
|
||||||
<Skeleton className="h-7 w-11" />
|
|
||||||
<Skeleton className="h-7 w-11" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const DataTable = <TData,>({
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<DataTableSkeleton
|
<DataTableSkeleton
|
||||||
|
layout={layout}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowCount={pageSize}
|
rowCount={pageSize}
|
||||||
searchable={search}
|
searchable={search}
|
||||||
@@ -52,7 +53,13 @@ export const DataTable = <TData,>({
|
|||||||
const noRecords = !isLoading && count === 0 && noQuery
|
const noRecords = !isLoading && count === 0 && noQuery
|
||||||
|
|
||||||
if (noRecords) {
|
if (noRecords) {
|
||||||
return <NoRecords />
|
return (
|
||||||
|
<NoRecords
|
||||||
|
className={clx({
|
||||||
|
"flex h-full flex-col overflow-hidden": layout === "fill",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { StatusCell } from "../../common/status-cell"
|
||||||
|
|
||||||
|
type AdminOnlyCellProps = {
|
||||||
|
adminOnly: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminOnlyCell = ({ adminOnly }: AdminOnlyCellProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const color = adminOnly ? "blue" : "green"
|
||||||
|
const text = adminOnly ? t("general.admin") : t("general.store")
|
||||||
|
|
||||||
|
return <StatusCell color={color}>{text}</StatusCell>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminOnlyHeader = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{t("fields.availability")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./admin-only-cell"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./is-return-cell"
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type IsReturnCellProps = {
|
||||||
|
isReturn?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IsReturnCell = ({ isReturn }: IsReturnCellProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{isReturn ? t("regions.return") : t("regions.outbound")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IsReturnHeader = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{t("fields.type")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./price-type-cell"
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
|
type PriceTypeCellProps = {
|
||||||
|
priceType?: "flat_rate" | "calculated"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PriceTypeCell = ({ priceType }: PriceTypeCellProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!priceType) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlatRate = priceType === "flat_rate"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{isFlatRate ? t("regions.flatRate") : t("regions.calculated")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PriceTypeHeader = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{t("regions.priceType")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./shipping-option-cell"
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
|
type ShippingOptionCellProps = {
|
||||||
|
name?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShippingOptionCell = ({ name }: ShippingOptionCellProps) => {
|
||||||
|
if (!name) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{name}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShippingOptionHeader = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{t("fields.name")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { MoneyAmountCell } from "../../common/money-amount-cell"
|
||||||
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
|
type ShippingPriceCellProps = {
|
||||||
|
isCalculated: boolean
|
||||||
|
price?: number | null
|
||||||
|
currencyCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShippingPriceCell = ({
|
||||||
|
price,
|
||||||
|
currencyCode,
|
||||||
|
isCalculated,
|
||||||
|
}: ShippingPriceCellProps) => {
|
||||||
|
if (isCalculated || !price) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MoneyAmountCell currencyCode={currencyCode} amount={price} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShippingPriceHeader = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{t("fields.price")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./subtotal-requirement-cell"
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { ShippingOption } from "@medusajs/medusa"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { MoneyAmountCell } from "../../common/money-amount-cell"
|
||||||
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
|
type SubtotalType = "min" | "max"
|
||||||
|
|
||||||
|
type SubtotalRequirementCellProps = {
|
||||||
|
type: SubtotalType
|
||||||
|
shippingOption: ShippingOption
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubtotalRequirementCell = (
|
||||||
|
props: SubtotalRequirementCellProps
|
||||||
|
) => {
|
||||||
|
const requirement = props.shippingOption.requirements?.find(
|
||||||
|
(r) => r.type === `${props.type}_subtotal`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!requirement) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MoneyAmountCell
|
||||||
|
currencyCode={props.shippingOption.region!.currency_code}
|
||||||
|
amount={requirement.amount}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubtotalRequirementHeader = ({ type }: { type: SubtotalType }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const header =
|
||||||
|
type === "min" ? t("fields.minSubtotal") : t("fields.maxSubtotal")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{header}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
import { ShippingOption } from "@medusajs/medusa"
|
||||||
|
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdminOnlyCell,
|
||||||
|
AdminOnlyHeader,
|
||||||
|
} from "../../../components/table/table-cells/shipping-option/admin-only-cell"
|
||||||
|
import {
|
||||||
|
IsReturnCell,
|
||||||
|
IsReturnHeader,
|
||||||
|
} from "../../../components/table/table-cells/shipping-option/is-return-cell"
|
||||||
|
import {
|
||||||
|
PriceTypeCell,
|
||||||
|
PriceTypeHeader,
|
||||||
|
} from "../../../components/table/table-cells/shipping-option/price-type-cell"
|
||||||
|
import {
|
||||||
|
ShippingOptionCell,
|
||||||
|
ShippingOptionHeader,
|
||||||
|
} from "../../../components/table/table-cells/shipping-option/shipping-option-cell"
|
||||||
|
import {
|
||||||
|
ShippingPriceCell,
|
||||||
|
ShippingPriceHeader,
|
||||||
|
} from "../../../components/table/table-cells/shipping-option/shipping-price-cell/shipping-price-cell"
|
||||||
|
import {
|
||||||
|
SubtotalRequirementCell,
|
||||||
|
SubtotalRequirementHeader,
|
||||||
|
} from "../../../components/table/table-cells/shipping-option/subtotal-requirement-cell"
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<PricedShippingOption>()
|
||||||
|
|
||||||
|
export const useShippingOptionTableColumns = () => {
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: () => <ShippingOptionHeader />,
|
||||||
|
cell: ({ getValue }) => <ShippingOptionCell name={getValue()} />,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("is_return", {
|
||||||
|
header: () => <IsReturnHeader />,
|
||||||
|
cell: (cell) => {
|
||||||
|
const value = cell.getValue()
|
||||||
|
|
||||||
|
return <IsReturnCell isReturn={value} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("price_type", {
|
||||||
|
header: () => <PriceTypeHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const type = getValue()
|
||||||
|
|
||||||
|
return <PriceTypeCell priceType={type} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("price_incl_tax", {
|
||||||
|
header: () => <ShippingPriceHeader />,
|
||||||
|
cell: ({ getValue, row }) => {
|
||||||
|
const isCalculated = row.original.price_type === "calculated"
|
||||||
|
const amount = getValue()
|
||||||
|
const currencyCode = row.original.region!.currency_code
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShippingPriceCell
|
||||||
|
isCalculated={isCalculated}
|
||||||
|
currencyCode={currencyCode}
|
||||||
|
price={amount}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "min_amount",
|
||||||
|
header: () => <SubtotalRequirementHeader type="min" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<SubtotalRequirementCell
|
||||||
|
type="min"
|
||||||
|
shippingOption={row.original as unknown as ShippingOption}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "max_amount",
|
||||||
|
header: () => <SubtotalRequirementHeader type="max" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<SubtotalRequirementCell
|
||||||
|
type="max"
|
||||||
|
shippingOption={row.original as unknown as ShippingOption}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("admin_only", {
|
||||||
|
header: () => <AdminOnlyHeader />,
|
||||||
|
cell: (cell) => {
|
||||||
|
const value = cell.getValue() || false
|
||||||
|
|
||||||
|
return <AdminOnlyCell adminOnly={value} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Filter } from "../../../../../components/table/data-table"
|
import { Filter } from "../../../components/table/data-table"
|
||||||
|
|
||||||
export const useShippingOptionTableFilters = () => {
|
export const useShippingOptionTableFilters = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { AdminGetShippingOptionsParams } from "@medusajs/medusa"
|
import { AdminGetShippingOptionsParams } from "@medusajs/medusa"
|
||||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
import { useQueryParams } from "../../use-query-params"
|
||||||
|
|
||||||
type UseShippingOptionTableQueryProps = {
|
type UseShippingOptionTableQueryProps = {
|
||||||
regionId: string
|
regionId: string
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const formatter = new Intl.NumberFormat(undefined, {
|
||||||
|
style: "percent",
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number as a percentage
|
||||||
|
* @param value - The value to format
|
||||||
|
* @param isPercentageValue - Whether the value is already a percentage value (where `0` is 0%, `0.5` is 50%, `0.75` is 75%, etc). Defaults to false
|
||||||
|
* @returns The formatted percentage in the form of a localized string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* formatPercentage(0.5, true) // "50%"
|
||||||
|
* formatPercentage(50) // "50%"
|
||||||
|
*/
|
||||||
|
export const formatPercentage = (
|
||||||
|
value?: number | null,
|
||||||
|
isPercentageValue = false
|
||||||
|
) => {
|
||||||
|
let val = value || 0
|
||||||
|
|
||||||
|
if (!isPercentageValue) {
|
||||||
|
val = val / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.format(val)
|
||||||
|
}
|
||||||
@@ -559,12 +559,34 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: "",
|
||||||
lazy: () => import("../../routes/taxes/views/tax-list"),
|
lazy: () => import("../../routes/taxes/tax-list"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: ":id",
|
||||||
lazy: () => import("../../routes/taxes/views/tax-details"),
|
lazy: () => import("../../routes/taxes/tax-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminRegionsRes) => data.region.name,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../routes/taxes/tax-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "tax-rates/create",
|
||||||
|
lazy: () => import("../../routes/taxes/tax-rate-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "tax-rates/:rate_id/edit",
|
||||||
|
lazy: () => import("../../routes/taxes/tax-rate-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "tax-rates/:rate_id/edit-overrides",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/taxes/tax-rate-edit-overrides"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+5
-2
@@ -62,8 +62,11 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer.Form form={form}>
|
<RouteDrawer.Form form={form}>
|
||||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
<form
|
||||||
<RouteDrawer.Body>
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||||
<div className="flex h-full flex-col gap-y-8">
|
<div className="flex h-full flex-col gap-y-8">
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
|
|||||||
+84
-7
@@ -1,16 +1,21 @@
|
|||||||
import { Region } from "@medusajs/medusa"
|
import { Region } from "@medusajs/medusa"
|
||||||
import { Container, Heading } from "@medusajs/ui"
|
import { Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
import { useAdminShippingOptions } from "medusa-react"
|
import {
|
||||||
|
useAdminDeleteShippingOption,
|
||||||
|
useAdminShippingOptions,
|
||||||
|
} from "medusa-react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { PlusMini } from "@medusajs/icons"
|
import { PencilSquare, PlusMini, Trash } from "@medusajs/icons"
|
||||||
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
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 { useShippingOptionTableColumns } from "../../../../../hooks/table/columns/use-shipping-option-table-columns"
|
||||||
|
import { useShippingOptionTableFilters } from "../../../../../hooks/table/filters/use-shipping-option-table-filters"
|
||||||
|
import { useShippingOptionTableQuery } from "../../../../../hooks/table/query/use-shipping-option-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useShippingOptionColumns } from "./use-shipping-option-table-columns"
|
|
||||||
import { useShippingOptionTableFilters } from "./use-shipping-option-table-filters"
|
|
||||||
import { useShippingOptionTableQuery } from "./use-shipping-option-table-query"
|
|
||||||
|
|
||||||
type RegionShippingOptionSectionProps = {
|
type RegionShippingOptionSectionProps = {
|
||||||
region: Region
|
region: Region
|
||||||
@@ -36,7 +41,7 @@ export const RegionShippingOptionSection = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const filters = useShippingOptionTableFilters()
|
const filters = useShippingOptionTableFilters()
|
||||||
const columns = useShippingOptionColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
data: (shipping_options ?? []) as unknown as PricedShippingOption[],
|
data: (shipping_options ?? []) as unknown as PricedShippingOption[],
|
||||||
@@ -94,3 +99,75 @@ export const RegionShippingOptionSection = ({
|
|||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ShippingOptionActions = ({
|
||||||
|
shippingOption,
|
||||||
|
}: {
|
||||||
|
shippingOption: PricedShippingOption
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminDeleteShippingOption(shippingOption.id!)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("regions.deleteShippingOptionWarning", {
|
||||||
|
name: shippingOption.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `shipping-options/${shippingOption.id}/edit`,
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
icon: <Trash />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<PricedShippingOption>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useShippingOptionTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
...base,
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <ShippingOptionActions shippingOption={row.original} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
-179
@@ -1,179 +0,0 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
|
||||||
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
|
||||||
import { usePrompt } from "@medusajs/ui"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useAdminDeleteShippingOption } from "medusa-react"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
|
||||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
|
||||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<PricedShippingOption>()
|
|
||||||
|
|
||||||
export const useShippingOptionColumns = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: ({ getValue }) => (
|
|
||||||
<div className="flex size-full items-center overflow-hidden">
|
|
||||||
<span className="truncate">{getValue()}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("is_return", {
|
|
||||||
header: t("fields.type"),
|
|
||||||
cell: (cell) => {
|
|
||||||
const value = cell.getValue()
|
|
||||||
|
|
||||||
return value ? t("regions.return") : t("regions.outbound")
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("price_type", {
|
|
||||||
header: t("regions.priceType"),
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
const type = getValue()
|
|
||||||
|
|
||||||
return type === "flat_rate"
|
|
||||||
? t("regions.flatRate")
|
|
||||||
: t("regions.calculated")
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("price_incl_tax", {
|
|
||||||
header: t("fields.price"),
|
|
||||||
cell: ({ getValue, row }) => {
|
|
||||||
const isCalculated = row.original.price_type === "calculated"
|
|
||||||
|
|
||||||
if (isCalculated) {
|
|
||||||
return <PlaceholderCell />
|
|
||||||
}
|
|
||||||
|
|
||||||
const amount = getValue()
|
|
||||||
const currencyCode = row.original.region!.currency_code
|
|
||||||
|
|
||||||
return <MoneyAmountCell currencyCode={currencyCode} amount={amount} />
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
|
||||||
id: "min_amount",
|
|
||||||
header: () => (
|
|
||||||
<div className="flex size-full items-center overflow-hidden">
|
|
||||||
<span className="truncate">{t("fields.minSubtotal")}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const minAmountReq = row.original.requirements?.find(
|
|
||||||
(r) => r.type === "min_subtotal"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!minAmountReq) {
|
|
||||||
return <PlaceholderCell />
|
|
||||||
}
|
|
||||||
|
|
||||||
const amount = minAmountReq.amount
|
|
||||||
const currencyCode = row.original.region!.currency_code
|
|
||||||
|
|
||||||
return <MoneyAmountCell currencyCode={currencyCode} amount={amount} />
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
|
||||||
id: "max_amount",
|
|
||||||
header: () => (
|
|
||||||
<div className="flex size-full items-center overflow-hidden">
|
|
||||||
<span className="truncate">{t("fields.maxSubtotal")}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const maxAmountReq = row.original.requirements?.find(
|
|
||||||
(r) => r.type === "max_subtotal"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!maxAmountReq) {
|
|
||||||
return <PlaceholderCell />
|
|
||||||
}
|
|
||||||
|
|
||||||
const amount = maxAmountReq.amount
|
|
||||||
const currencyCode = row.original.region!.currency_code
|
|
||||||
|
|
||||||
return <MoneyAmountCell currencyCode={currencyCode} amount={amount} />
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("admin_only", {
|
|
||||||
header: t("fields.availability"),
|
|
||||||
cell: (cell) => {
|
|
||||||
const value = cell.getValue()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatusCell color={value ? "blue" : "green"}>
|
|
||||||
{value ? t("general.admin") : t("general.store")}
|
|
||||||
</StatusCell>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return <ShippingOptionActions shippingOption={row.original} />
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShippingOptionActions = ({
|
|
||||||
shippingOption,
|
|
||||||
}: {
|
|
||||||
shippingOption: PricedShippingOption
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const prompt = usePrompt()
|
|
||||||
|
|
||||||
const { mutateAsync } = useAdminDeleteShippingOption(shippingOption.id!)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const res = await prompt({
|
|
||||||
title: t("general.areYouSure"),
|
|
||||||
description: t("regions.deleteShippingOptionWarning", {
|
|
||||||
name: shippingOption.name,
|
|
||||||
}),
|
|
||||||
confirmText: t("actions.delete"),
|
|
||||||
cancelText: t("actions.cancel"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: `shipping-options/${shippingOption.id}/edit`,
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("actions.delete"),
|
|
||||||
onClick: handleDelete,
|
|
||||||
icon: <Trash />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AdminProductsRes } from "@medusajs/medusa"
|
import { AdminRegionsRes } from "@medusajs/medusa"
|
||||||
import { Response } from "@medusajs/medusa-js"
|
import { Response } from "@medusajs/medusa-js"
|
||||||
import { adminRegionKeys } from "medusa-react"
|
import { adminRegionKeys } from "medusa-react"
|
||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
@@ -15,7 +15,7 @@ export const regionLoader = async ({ params }: LoaderFunctionArgs) => {
|
|||||||
const query = regionQuery(id!)
|
const query = regionQuery(id!)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
queryClient.getQueryData<Response<AdminProductsRes>>(query.queryKey) ??
|
queryClient.getQueryData<Response<AdminRegionsRes>>(query.queryKey) ??
|
||||||
(await queryClient.fetchQuery(query))
|
(await queryClient.fetchQuery(query))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { useAdminRegion } from "medusa-react"
|
import { useAdminRegion } from "medusa-react"
|
||||||
import { Outlet, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
import { RegionCountrySection } from "./components/region-country-section"
|
import { RegionCountrySection } from "./components/region-country-section"
|
||||||
import { RegionGeneralSection } from "./components/region-general-section"
|
import { RegionGeneralSection } from "./components/region-general-section"
|
||||||
import { RegionShippingOptionSection } from "./components/region-shipping-option-section"
|
import { RegionShippingOptionSection } from "./components/region-shipping-option-section"
|
||||||
|
import { regionLoader } from "./loader"
|
||||||
|
|
||||||
export const RegionDetail = () => {
|
export const RegionDetail = () => {
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof regionLoader>
|
||||||
|
>
|
||||||
|
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
const { region, isLoading, isError, error } = useAdminRegion(id!, {
|
||||||
|
initialData,
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
||||||
if (isLoading || !region) {
|
if (isLoading || !region) {
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./override-chip"
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
|
import { OverrideOption } from "../../types"
|
||||||
|
|
||||||
|
type OverrideChipProps = {
|
||||||
|
override: OverrideOption
|
||||||
|
onRemove: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OverrideChip = ({ override, onRemove }: OverrideChipProps) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-field shadow-borders-base transition-fg hover:bg-ui-bg-field-hover flex h-7 items-center overflow-hidden rounded-md">
|
||||||
|
<div className="txt-compact-small-plus flex h-full select-none items-center justify-center px-2 py-0.5">
|
||||||
|
{override.label}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRemove(override.value)}
|
||||||
|
className="focus-visible:bg-ui-bg-field-hover transition-fg hover:bg-ui-bg-field-hover flex h-full w-7 items-center justify-center border-l outline-none"
|
||||||
|
>
|
||||||
|
<XMarkMini className="text-ui-fg-muted" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./override-grid"
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
import { Button } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { OverrideOption } from "../../types"
|
||||||
|
import { OverrideChip } from "../override-chip"
|
||||||
|
|
||||||
|
type OverrideGridProps = {
|
||||||
|
overrides: OverrideOption[]
|
||||||
|
onRemove: (value: string) => void
|
||||||
|
onClear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OverrideGrid = ({
|
||||||
|
overrides,
|
||||||
|
onRemove,
|
||||||
|
onClear,
|
||||||
|
}: OverrideGridProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!overrides.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{overrides.map((override) => (
|
||||||
|
<OverrideChip
|
||||||
|
key={override.value}
|
||||||
|
override={override}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="transparent"
|
||||||
|
size="small"
|
||||||
|
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
{t("actions.clearAll")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./overrides-drawer"
|
||||||
+394
@@ -0,0 +1,394 @@
|
|||||||
|
import { Product } from "@medusajs/medusa"
|
||||||
|
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
||||||
|
import { Button } from "@medusajs/ui"
|
||||||
|
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
useAdminProductTypes,
|
||||||
|
useAdminProducts,
|
||||||
|
useAdminShippingOptions,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
||||||
|
import { useShippingOptionTableFilters } from "../../../../../hooks/table/filters/use-shipping-option-table-filters"
|
||||||
|
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||||
|
import { useShippingOptionTableQuery } from "../../../../../hooks/table/query/use-shipping-option-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { Override } from "../../constants"
|
||||||
|
import { useProductOverrideTableColumns } from "../../hooks/columns/use-product-override-table-columns"
|
||||||
|
import { useProductTypeOverrideTableColumns } from "../../hooks/columns/use-product-type-override-table-columns"
|
||||||
|
import { useShippingOptionOverrideTableColumns } from "../../hooks/columns/use-shipping-option-override-table-columns"
|
||||||
|
import { useProductTypeOverrideTableFilters } from "../../hooks/filters/use-product-type-override-table-filters"
|
||||||
|
import { useProductTypeOverrideTableQuery } from "../../hooks/query/use-product-type-override-table-query"
|
||||||
|
import { OverrideOption } from "../../types"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
const PRODUCT_PREFIX = "product"
|
||||||
|
const PRODUCT_TYPE_PREFIX = "product_type"
|
||||||
|
const SHIPPING_OPTION_PREFIX = "shipping_option"
|
||||||
|
|
||||||
|
type OverrideProps = {
|
||||||
|
selected: OverrideOption[]
|
||||||
|
onSave: (options: OverrideOption[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initRowState = (selected: OverrideOption[] = []): RowSelectionState => {
|
||||||
|
return selected.reduce((acc, { value }) => {
|
||||||
|
acc[value] = true
|
||||||
|
return acc
|
||||||
|
}, {} as RowSelectionState)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OverrideFooter = ({ onSave }: { onSave: () => void }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
||||||
|
<SplitView.Close type="button" asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</SplitView.Close>
|
||||||
|
<Button size="small" type="button" onClick={onSave}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductOverrideTable = ({ selected = [], onSave }: OverrideProps) => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||||
|
initRowState(selected)
|
||||||
|
)
|
||||||
|
const [intermediate, setIntermediate] = useState<OverrideOption[]>(selected)
|
||||||
|
|
||||||
|
const { searchParams, raw } = useProductTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PRODUCT_PREFIX,
|
||||||
|
})
|
||||||
|
const { products, count, isLoading, isError, error } = useAdminProducts(
|
||||||
|
{
|
||||||
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||||
|
const newState: RowSelectionState =
|
||||||
|
typeof fn === "function" ? fn(rowSelection) : fn
|
||||||
|
|
||||||
|
const diff = Object.keys(newState).filter(
|
||||||
|
(k) => newState[k] !== rowSelection[k]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (diff.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedProducts = (products?.filter((p) => diff.includes(p.id!)) ??
|
||||||
|
[]) as Product[]
|
||||||
|
|
||||||
|
if (addedProducts.length > 0) {
|
||||||
|
const newOverrides = addedProducts.map((p) => ({
|
||||||
|
label: p.title,
|
||||||
|
value: p.id!,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setIntermediate((prev) => {
|
||||||
|
const filteredPrev = prev.filter((p) =>
|
||||||
|
Object.keys(newState).includes(p.value)
|
||||||
|
)
|
||||||
|
const update = Array.from(new Set([...filteredPrev, ...newOverrides]))
|
||||||
|
return update
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setRowSelection(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(intermediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useProductOverrideTableColumns()
|
||||||
|
const filters = useProductTableFilters()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: (products ?? []) as Product[],
|
||||||
|
columns: columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
enableRowSelection: true,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater,
|
||||||
|
},
|
||||||
|
prefix: PRODUCT_PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col overflow-hidden">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
queryObject={raw}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
filters={filters}
|
||||||
|
isLoading={isLoading}
|
||||||
|
layout="fill"
|
||||||
|
orderBy={["title", "created_at", "updated_at"]}
|
||||||
|
prefix={PRODUCT_PREFIX}
|
||||||
|
/>
|
||||||
|
<OverrideFooter onSave={handleSave} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductTypeOverrideTable = ({ onSave, selected = [] }: OverrideProps) => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||||
|
initRowState(selected)
|
||||||
|
)
|
||||||
|
const [intermediate, setIntermediate] = useState<OverrideOption[]>(selected)
|
||||||
|
|
||||||
|
const { searchParams, raw } = useProductTypeOverrideTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PRODUCT_TYPE_PREFIX,
|
||||||
|
})
|
||||||
|
const { product_types, count, isLoading, isError, error } =
|
||||||
|
useAdminProductTypes(
|
||||||
|
{
|
||||||
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||||
|
const newState: RowSelectionState =
|
||||||
|
typeof fn === "function" ? fn(rowSelection) : fn
|
||||||
|
|
||||||
|
const diff = Object.keys(newState).filter(
|
||||||
|
(k) => newState[k] !== rowSelection[k]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (diff.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedProductTypes =
|
||||||
|
product_types?.filter((p) => diff.includes(p.id!)) ?? []
|
||||||
|
|
||||||
|
if (addedProductTypes.length > 0) {
|
||||||
|
const newOverrides = addedProductTypes.map((p) => ({
|
||||||
|
label: p.value,
|
||||||
|
value: p.id!,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setIntermediate((prev) => {
|
||||||
|
const filteredPrev = prev.filter((p) =>
|
||||||
|
Object.keys(newState).includes(p.value)
|
||||||
|
)
|
||||||
|
const update = Array.from(new Set([...filteredPrev, ...newOverrides]))
|
||||||
|
return update
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setRowSelection(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(intermediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useProductTypeOverrideTableColumns()
|
||||||
|
const filters = useProductTypeOverrideTableFilters()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: product_types ?? [],
|
||||||
|
columns: columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
enableRowSelection: true,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater,
|
||||||
|
},
|
||||||
|
prefix: PRODUCT_TYPE_PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col overflow-hidden">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
queryObject={raw}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
filters={filters}
|
||||||
|
isLoading={isLoading}
|
||||||
|
layout="fill"
|
||||||
|
orderBy={["value", "created_at", "updated_at"]}
|
||||||
|
prefix={PRODUCT_PREFIX}
|
||||||
|
/>
|
||||||
|
<OverrideFooter onSave={handleSave} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShippingOptionOverrideTable = ({
|
||||||
|
onSave,
|
||||||
|
selected = [],
|
||||||
|
regionId,
|
||||||
|
}: OverrideProps & { regionId: string }) => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||||
|
initRowState(selected)
|
||||||
|
)
|
||||||
|
const [intermediate, setIntermediate] = useState<OverrideOption[]>(selected)
|
||||||
|
|
||||||
|
const { searchParams, raw } = useShippingOptionTableQuery({
|
||||||
|
regionId,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: SHIPPING_OPTION_PREFIX,
|
||||||
|
})
|
||||||
|
const { shipping_options, count, isLoading, isError, error } =
|
||||||
|
useAdminShippingOptions(
|
||||||
|
{
|
||||||
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||||
|
const newState: RowSelectionState =
|
||||||
|
typeof fn === "function" ? fn(rowSelection) : fn
|
||||||
|
|
||||||
|
const diff = Object.keys(newState).filter(
|
||||||
|
(k) => newState[k] !== rowSelection[k]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (diff.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedShippingOptions =
|
||||||
|
shipping_options?.filter((p) => diff.includes(p.id!)) ?? []
|
||||||
|
|
||||||
|
if (addedShippingOptions.length > 0) {
|
||||||
|
const newOverrides = addedShippingOptions.map((p) => ({
|
||||||
|
label: p.name,
|
||||||
|
value: p.id!,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setIntermediate((prev) => {
|
||||||
|
const filteredPrev = prev.filter((p) =>
|
||||||
|
Object.keys(newState).includes(p.value)
|
||||||
|
)
|
||||||
|
const update = Array.from(new Set([...filteredPrev, ...newOverrides]))
|
||||||
|
return update
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setRowSelection(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(intermediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useShippingOptionOverrideTableColumns()
|
||||||
|
const filters = useShippingOptionTableFilters()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: (shipping_options ?? []) as unknown as PricedShippingOption[],
|
||||||
|
columns: columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id!,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
enableRowSelection: true,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater,
|
||||||
|
},
|
||||||
|
prefix: SHIPPING_OPTION_PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col overflow-hidden">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
queryObject={raw}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
filters={filters}
|
||||||
|
isLoading={isLoading}
|
||||||
|
layout="fill"
|
||||||
|
orderBy={["title", "created_at", "updated_at"]}
|
||||||
|
prefix={SHIPPING_OPTION_PREFIX}
|
||||||
|
/>
|
||||||
|
<OverrideFooter onSave={handleSave} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverrideTableProps = {
|
||||||
|
product: OverrideProps
|
||||||
|
productType: OverrideProps
|
||||||
|
shippingOption: OverrideProps
|
||||||
|
regionId: string
|
||||||
|
selected: Override | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OverrideTable = ({
|
||||||
|
product,
|
||||||
|
productType,
|
||||||
|
shippingOption,
|
||||||
|
regionId,
|
||||||
|
selected,
|
||||||
|
}: OverrideTableProps) => {
|
||||||
|
switch (selected) {
|
||||||
|
case Override.PRODUCT:
|
||||||
|
return <ProductOverrideTable {...product} />
|
||||||
|
case Override.PRODUCT_TYPE:
|
||||||
|
return <ProductTypeOverrideTable {...productType} />
|
||||||
|
case Override.SHIPPING_OPTION:
|
||||||
|
return (
|
||||||
|
<ShippingOptionOverrideTable {...shippingOption} regionId={regionId} />
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum Override {
|
||||||
|
PRODUCT = "product",
|
||||||
|
PRODUCT_TYPE = "product_type",
|
||||||
|
SHIPPING_OPTION = "shipping_option",
|
||||||
|
}
|
||||||
+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 useProductOverrideTableColumns = () => {
|
||||||
|
const base = useProductTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
+49
@@ -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 useProductTypeOverrideTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("value", {
|
||||||
|
header: t("fields.value"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
||||||
|
import { Checkbox } from "@medusajs/ui"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useShippingOptionTableColumns } from "../../../../../hooks/table/columns/use-shipping-option-table-columns"
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<PricedShippingOption>()
|
||||||
|
|
||||||
|
export const useShippingOptionOverrideTableColumns = () => {
|
||||||
|
const base = useShippingOptionTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Filter } from "../../../../../components/table/data-table"
|
||||||
|
|
||||||
|
export const useProductTypeOverrideTableFilters = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const filters: Filter[] = [
|
||||||
|
{ label: t("fields.createdAt"), key: "created_at" },
|
||||||
|
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||||
|
].map((f) => ({
|
||||||
|
key: f.key,
|
||||||
|
label: f.label,
|
||||||
|
type: "date",
|
||||||
|
}))
|
||||||
|
|
||||||
|
return filters
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { AdminGetProductTypesParams } from "@medusajs/medusa"
|
||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useProductTypeOverrideTableQuery = ({
|
||||||
|
pageSize = 50,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize?: number
|
||||||
|
prefix: string
|
||||||
|
}) => {
|
||||||
|
const raw = useQueryParams(
|
||||||
|
["offset", "q", "order", "created_at", "updated_at"],
|
||||||
|
prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchParams: AdminGetProductTypesParams = {
|
||||||
|
limit: pageSize,
|
||||||
|
offset: raw.offset ? Number(raw.offset) : 0,
|
||||||
|
q: raw.q,
|
||||||
|
created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
|
||||||
|
updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
|
||||||
|
order: raw.order,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Override } from "./constants"
|
||||||
|
|
||||||
|
export type OverrideOption = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OverrideState = {
|
||||||
|
[K in Override]: boolean
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./tax-details-section"
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
import { Region } from "@medusajs/medusa"
|
|
||||||
import { Container, Heading, StatusBadge, Text, Tooltip } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
region: Region
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TaxDetailsSection = ({ region }: Props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="divide-ui-border-base divide-y p-0">
|
|
||||||
<div className="px-8 py-6">
|
|
||||||
<Heading>{region.name}</Heading>
|
|
||||||
<div className="text-ui-fg-subtle flex items-center gap-x-2">
|
|
||||||
<Text leading="compact" size="small">
|
|
||||||
{region.countries
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((c) => c.display_name)
|
|
||||||
.join(", ")}
|
|
||||||
</Text>
|
|
||||||
{region.countries.length > 2 && (
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<ul>
|
|
||||||
{region.countries.slice(2).map((c) => (
|
|
||||||
<li key={c.id}>{c.display_name}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
leading="compact"
|
|
||||||
size="small"
|
|
||||||
weight="plus"
|
|
||||||
className="cursor-default"
|
|
||||||
>
|
|
||||||
{t("general.plusCountMore", {
|
|
||||||
count: region.countries.length - 2,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 px-8 py-6">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
Tax Provider
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{region.tax_provider_id
|
|
||||||
? region.tax_provider_id
|
|
||||||
: "System Tax Provider"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 px-8 py-6">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
Automatic Tax Calculation
|
|
||||||
</Text>
|
|
||||||
<StatusBadge
|
|
||||||
color={region.automatic_taxes ? "green" : "green"}
|
|
||||||
className="w-fit"
|
|
||||||
>
|
|
||||||
{region.automatic_taxes
|
|
||||||
? t("general.enabled")
|
|
||||||
: t("general.disabled")}
|
|
||||||
</StatusBadge>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 px-8 py-6">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
Gift Cards Taxable
|
|
||||||
</Text>
|
|
||||||
<StatusBadge
|
|
||||||
color={region.gift_cards_taxable ? "green" : "grey"}
|
|
||||||
className="w-fit"
|
|
||||||
>
|
|
||||||
{region.gift_cards_taxable
|
|
||||||
? t("general.enabled")
|
|
||||||
: t("general.disabled")}
|
|
||||||
</StatusBadge>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
import { TaxRate } from "@medusajs/medusa"
|
|
||||||
import { Button, Container, Heading } from "@medusajs/ui"
|
|
||||||
import {
|
|
||||||
createColumnHelper,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table"
|
|
||||||
import { useAdminTaxRates } from "medusa-react"
|
|
||||||
import { useMemo, useState } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { useParams } from "react-router-dom"
|
|
||||||
import { DebouncedSearch } from "../../../../components/common/debounced-search"
|
|
||||||
|
|
||||||
export const TaxRatesSection = () => {
|
|
||||||
const { id } = useParams()
|
|
||||||
const [query, setQuery] = useState("")
|
|
||||||
|
|
||||||
const { tax_rates, isLoading } = useAdminTaxRates(
|
|
||||||
{
|
|
||||||
region_id: id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const columns = useColumns()
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: tax_rates ?? [],
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="divide-y p-0">
|
|
||||||
<div className="flex items-center justify-between px-8 pb-4 pt-6">
|
|
||||||
<Heading level="h2">Tax Rates</Heading>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<DebouncedSearch size="small" value={query} onChange={setQuery} />
|
|
||||||
<Button variant="secondary">Add Tax Rate</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<TaxRate>()
|
|
||||||
|
|
||||||
const useColumns = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
return <span>{getValue()}</span>
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-countries-section"
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
import { GlobeEurope } from "@medusajs/icons"
|
||||||
|
import { Region } from "@medusajs/medusa"
|
||||||
|
import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type TaxCountriesSectionProps = {
|
||||||
|
region: Region
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TaxCountriesSection = ({ region }: TaxCountriesSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const countries = region.countries ?? []
|
||||||
|
|
||||||
|
const firstCountries = countries.slice(0, 3)
|
||||||
|
const restCountries = countries.slice(3)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex flex-col gap-y-4 px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Heading level="h2">{t("fields.countries")}</Heading>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-[28px_1fr] items-center gap-x-3">
|
||||||
|
<div className="bg-ui-bg-base shadow-borders-base flex size-7 items-center justify-center rounded-md">
|
||||||
|
<div className="bg-ui-bg-component flex size-6 items-center justify-center rounded-[4px]">
|
||||||
|
<GlobeEurope className="text-ui-fg-subtle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{countries.length > 0 ? (
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{firstCountries.map((sc) => sc.display_name).join(", ")}
|
||||||
|
</Text>
|
||||||
|
{restCountries.length > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<ul>
|
||||||
|
{restCountries.map((sc) => (
|
||||||
|
<li key={sc.id}>{sc.display_name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
{t("general.plusCountMore", {
|
||||||
|
count: restCountries.length,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||||
|
{t("products.noSalesChannels")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
||||||
|
{t("taxes.countries.taxCountriesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-default-tax-rate-section"
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { Region } from "@medusajs/medusa"
|
||||||
|
import { Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { formatPercentage } from "../../../../../lib/percentage-helpers"
|
||||||
|
|
||||||
|
type TaxDefaultRateSectionProps = {
|
||||||
|
region: Region
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TaxDefaultTaxRateSection = ({
|
||||||
|
region,
|
||||||
|
}: TaxDefaultRateSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const defaultTaxCode = region.tax_code
|
||||||
|
const defaultTaxRate = region.tax_rate
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("taxes.defaultRate.sectionTitle")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: "tax-rates/default/edit",
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("fields.rate")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{formatPercentage(defaultTaxRate)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("fields.code")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{defaultTaxCode || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-general-section"
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { Region } from "@medusajs/medusa"
|
||||||
|
import { Badge, Container, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { formatPercentage } from "../../../../../lib/percentage-helpers"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
region: Region
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TaxDetailsSection = ({ region }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{region.name}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
to: "edit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.settings.taxProviderLabel")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{region.tax_provider_id
|
||||||
|
? region.tax_provider_id
|
||||||
|
: t("taxes.settings.systemTaxProviderLabel")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.settings.calculateTaxesAutomaticallyLabel")}
|
||||||
|
</Text>
|
||||||
|
<StatusBadge
|
||||||
|
color={region.automatic_taxes ? "green" : "grey"}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
{region.automatic_taxes
|
||||||
|
? t("general.enabled")
|
||||||
|
: t("general.disabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.settings.applyTaxesOnGiftCardsLabel")}
|
||||||
|
</Text>
|
||||||
|
<StatusBadge
|
||||||
|
color={region.gift_cards_taxable ? "green" : "grey"}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
{region.gift_cards_taxable
|
||||||
|
? t("general.enabled")
|
||||||
|
: t("general.disabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("fields.taxInclusivePricing")}
|
||||||
|
</Text>
|
||||||
|
<StatusBadge
|
||||||
|
color={region.includes_tax ? "green" : "grey"}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
{region.includes_tax ? t("general.enabled") : t("general.disabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.settings.defaultTaxRateLabel")}
|
||||||
|
</Text>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{formatPercentage(region.tax_rate)}
|
||||||
|
</Text>
|
||||||
|
{region.tax_code && <Badge size="2xsmall">{region.tax_code}</Badge>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import type { TaxRate } from "@medusajs/medusa"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { ExclamationCircle, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { usePrompt } from "@medusajs/ui"
|
||||||
|
import { useAdminDeleteTaxRate } from "medusa-react"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
|
||||||
|
export const TaxRateActions = ({ taxRate }: { taxRate: TaxRate }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminDeleteTaxRate(taxRate.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("taxes.taxRate.deleteRateDescription", {
|
||||||
|
name: taxRate.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("taxes.taxRate.editRateAction"),
|
||||||
|
to: `tax-rates/${taxRate.id}/edit`,
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("taxes.taxRate.editOverridesAction"),
|
||||||
|
to: `tax-rates/${taxRate.id}/edit-overrides`,
|
||||||
|
icon: <ExclamationCircle />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
icon: <Trash />,
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
import { Plus } from "@medusajs/icons"
|
||||||
|
import { Region } from "@medusajs/medusa"
|
||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminTaxRates } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useTaxRateTableColumns } from "./use-tax-rate-table-columns"
|
||||||
|
import { useTaxRateTableFilters } from "./use-tax-rate-table-filters"
|
||||||
|
import { useTaxRateTableQuery } from "./use-tax-rate-table-query"
|
||||||
|
|
||||||
|
type TaxRatesSectionProps = {
|
||||||
|
region: Region
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export const TaxRatesSection = ({ region }: TaxRatesSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { searchParams, raw } = useTaxRateTableQuery({ pageSize: PAGE_SIZE })
|
||||||
|
const { tax_rates, count, isLoading, isError, error } = useAdminTaxRates(
|
||||||
|
{
|
||||||
|
region_id: region.id,
|
||||||
|
expand: ["products", "product_types", "shipping_options"],
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"code",
|
||||||
|
"rate",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"products.id",
|
||||||
|
"product_types.id",
|
||||||
|
"shipping_options.id",
|
||||||
|
],
|
||||||
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const filters = useTaxRateTableFilters()
|
||||||
|
const columns = useTaxRateTableColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: tax_rates ?? [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("taxes.taxRate.sectionTitle")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.create"),
|
||||||
|
to: "tax-rates/create",
|
||||||
|
icon: <Plus />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
isLoading={isLoading}
|
||||||
|
count={count}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
queryObject={raw}
|
||||||
|
filters={filters}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
orderBy={["name", "code", "rate", "created_at", "updated_at"]}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
import type { TaxRate } from "@medusajs/medusa"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { formatPercentage } from "../../../../../lib/percentage-helpers"
|
||||||
|
import { TaxRateActions } from "./tax-rate-actions"
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<TaxRate>()
|
||||||
|
|
||||||
|
export const useTaxRateTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{getValue()}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("code", {
|
||||||
|
header: t("fields.code"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{getValue()}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("rate", {
|
||||||
|
header: t("fields.rate"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const rate = getValue()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{formatPercentage(rate)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("products", {
|
||||||
|
header: () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{t("taxes.taxRate.productOverridesHeader")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const count = getValue()?.length
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{count}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("product_types", {
|
||||||
|
header: () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{t("taxes.taxRate.productTypeOverridesHeader")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const count = getValue()?.length
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{count}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("shipping_options", {
|
||||||
|
header: () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{t("taxes.taxRate.shippingOptionOverridesHeader")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const count = getValue()?.length
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="truncate">{count}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <TaxRateActions taxRate={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import type { Filter } from "../../../../../components/table/data-table"
|
||||||
|
|
||||||
|
export const useTaxRateTableFilters = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const dateFilters: Filter[] = [
|
||||||
|
{ label: t("fields.createdAt"), key: "created_at" },
|
||||||
|
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||||
|
].map((f) => ({
|
||||||
|
key: f.key,
|
||||||
|
label: f.label,
|
||||||
|
type: "date",
|
||||||
|
}))
|
||||||
|
|
||||||
|
const filters = [...dateFilters]
|
||||||
|
|
||||||
|
return filters
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { AdminGetTaxRatesParams } from "@medusajs/medusa"
|
||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useTaxRateTableQuery = ({
|
||||||
|
pageSize = 10,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize?: number
|
||||||
|
prefix?: string
|
||||||
|
}) => {
|
||||||
|
const raw = useQueryParams(
|
||||||
|
["offset", "q", "order", "created_at", "updated_at"],
|
||||||
|
prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchParams: AdminGetTaxRatesParams = {
|
||||||
|
limit: pageSize,
|
||||||
|
offset: raw.offset ? parseInt(raw.offset) : 0,
|
||||||
|
q: raw.q,
|
||||||
|
order: raw.order,
|
||||||
|
created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
|
||||||
|
updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { taxRegionLoader as loader } from "./loader"
|
||||||
|
export { TaxDetail as Component } from "./tax-detail"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { AdminRegionsRes } from "@medusajs/medusa"
|
||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
|
import { adminRegionKeys } from "medusa-react"
|
||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { medusa, queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const regionQuery = (id: string) => ({
|
||||||
|
queryKey: adminRegionKeys.detail(id),
|
||||||
|
queryFn: async () => medusa.admin.regions.retrieve(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const taxRegionLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = regionQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminRegionsRes>>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useAdminRegion } from "medusa-react"
|
||||||
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { TaxCountriesSection } from "./components/tax-countries-section"
|
||||||
|
import { TaxDetailsSection } from "./components/tax-general-section"
|
||||||
|
import { TaxRatesSection } from "./components/tax-rates-section"
|
||||||
|
import { taxRegionLoader } from "./loader"
|
||||||
|
|
||||||
|
export const TaxDetail = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof taxRegionLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { region, isLoading, isError, error } = useAdminRegion(id!, {
|
||||||
|
initialData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading || !region) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="grid grid-cols-1 gap-x-4 xl:grid-cols-[1fr,400px]">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<TaxDetailsSection region={region} />
|
||||||
|
<TaxRatesSection region={region} />
|
||||||
|
<div className="flex flex-col gap-y-2 xl:hidden">
|
||||||
|
<TaxCountriesSection region={region} />
|
||||||
|
</div>
|
||||||
|
<JsonViewSection data={region} />
|
||||||
|
</div>
|
||||||
|
<div className="hidden flex-col gap-y-2 xl:flex">
|
||||||
|
<TaxCountriesSection region={region} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Region, TaxProvider } from "@medusajs/medusa"
|
||||||
|
import { Button, Input, Select, Switch } from "@medusajs/ui"
|
||||||
|
import { useAdminUpdateRegion } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
|
||||||
|
type EditTaxRateFormProps = {
|
||||||
|
region: Region
|
||||||
|
taxProviders: TaxProvider[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditTaxSettingsSchema = z.object({
|
||||||
|
tax_code: z.string().optional(),
|
||||||
|
tax_rate: z.union([z.string(), z.number()]).refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (num >= 0 && num <= 100) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}, "Default tax rate must be a number between 0 and 100"),
|
||||||
|
includes_tax: z.boolean(),
|
||||||
|
automatic_taxes: z.boolean(),
|
||||||
|
gift_cards_taxable: z.boolean(),
|
||||||
|
tax_provider_id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const SYSTEM_PROVIDER_ID = "system"
|
||||||
|
|
||||||
|
export const EditTaxSettingsForm = ({
|
||||||
|
region,
|
||||||
|
taxProviders,
|
||||||
|
}: EditTaxRateFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof EditTaxSettingsSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
tax_code: region.tax_code || "",
|
||||||
|
tax_rate: region.tax_rate,
|
||||||
|
includes_tax: region.includes_tax,
|
||||||
|
automatic_taxes: region.automatic_taxes,
|
||||||
|
gift_cards_taxable: region.gift_cards_taxable,
|
||||||
|
tax_provider_id: region.tax_provider_id || SYSTEM_PROVIDER_ID,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditTaxSettingsSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const { tax_provider_id, tax_rate, ...rest } = data
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
tax_provider_id:
|
||||||
|
tax_provider_id === SYSTEM_PROVIDER_ID ? null : tax_provider_id,
|
||||||
|
tax_rate: Number(tax_rate),
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="tax_provider_id"
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("taxes.settings.taxProviderLabel")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select {...field} onValueChange={onChange}>
|
||||||
|
<Select.Trigger ref={ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value={SYSTEM_PROVIDER_ID}>
|
||||||
|
{t("taxes.settings.systemTaxProviderLabel")}
|
||||||
|
</Select.Item>
|
||||||
|
{taxProviders.map((tp) => (
|
||||||
|
<Select.Item key={tp.id} value={tp.id}>
|
||||||
|
{formatProvider(tp.id)}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="tax_rate"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("taxes.settings.defaultTaxRateLabel")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<PercentageInput {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="tax_code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("taxes.settings.defaultTaxCodeLabel")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="automatic_taxes"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-4">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<Form.Label>
|
||||||
|
{t("taxes.settings.calculateTaxesAutomaticallyLabel")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint className="text-pretty">
|
||||||
|
{t("taxes.settings.calculateTaxesAutomaticallyHint")}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="gift_cards_taxable"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-4">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<Form.Label>
|
||||||
|
{t("taxes.settings.applyTaxesOnGiftCardsLabel")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint className="text-pretty">
|
||||||
|
{t("taxes.settings.applyTaxesOnGiftCardsHint")}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="includes_tax"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-4">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<Form.Label>
|
||||||
|
{t("fields.taxInclusivePricing")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint className="text-pretty">
|
||||||
|
{t("regions.taxInclusiveHint")}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-tax-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { TaxEdit as Component } from "./tax-edit"
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminRegion, useAdminStoreTaxProviders } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { EditTaxSettingsForm } from "./components/edit-tax-form"
|
||||||
|
|
||||||
|
export const TaxEdit = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
||||||
|
const {
|
||||||
|
tax_providers,
|
||||||
|
isLoading: isLoadingTaxProvider,
|
||||||
|
isError: isTaxProviderError,
|
||||||
|
error: taxProviderError,
|
||||||
|
} = useAdminStoreTaxProviders()
|
||||||
|
|
||||||
|
const ready = !isLoading && region && !isLoadingTaxProvider && tax_providers
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTaxProviderError) {
|
||||||
|
throw taxProviderError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("taxes.settings.editTaxSettings")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && (
|
||||||
|
<EditTaxSettingsForm region={region} taxProviders={tax_providers} />
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./tax-list-callout"
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
export const TaxListCallout = () => {
|
||||||
|
return (
|
||||||
|
<Container className="flex items-center justify-between gap-x-3 px-6 py-4">
|
||||||
|
<div>
|
||||||
|
<Heading>Taxes</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle text-pretty">
|
||||||
|
Tax settings are specific to each region. To modify tax settings,
|
||||||
|
please select a region from the list.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
asChild
|
||||||
|
className="shrink-0 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Link to="/settings/regions">Go to Region settings</Link>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "../tax-list-table/tax-list-table"
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminRegions } from "medusa-react"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns"
|
||||||
|
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
|
||||||
|
import { useRegionTableQuery } from "../../../../../hooks/table/query/use-region-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const TaxListTable = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { searchParams, raw } = useRegionTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
const { regions, count, isLoading, isError, error } = useAdminRegions(
|
||||||
|
{
|
||||||
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
const filters = useRegionTableFilters()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: regions || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<Heading level="h2">{t("regions.domain")}</Heading>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
filters={filters}
|
||||||
|
isLoading={isLoading}
|
||||||
|
queryObject={raw}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
navigateTo={(row) => `${row.original.id}`}
|
||||||
|
orderBy={["name", "created_at", "updated_at"]}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useRegionTableColumns()
|
||||||
|
|
||||||
|
return useMemo(() => [...base], [base])
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TaxListCallout } from "./components/tax-list-callout"
|
||||||
|
import { TaxListTable } from "./components/tax-list-table"
|
||||||
|
|
||||||
|
export const TaxList = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<TaxListCallout />
|
||||||
|
<TaxListTable />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+474
@@ -0,0 +1,474 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, Heading, Input, Switch, Text, clx } from "@medusajs/ui"
|
||||||
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
|
import { useAdminCreateTaxRate } from "medusa-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams, useSearchParams } from "react-router-dom"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { OverrideGrid } from "../../../common/components/override-grid"
|
||||||
|
import { OverrideTable } from "../../../common/components/overrides-drawer/overrides-drawer"
|
||||||
|
import { Override } from "../../../common/constants"
|
||||||
|
import { OverrideOption, OverrideState } from "../../../common/types"
|
||||||
|
|
||||||
|
const CreateTaxRateSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
code: z.string().min(1),
|
||||||
|
rate: z.union([z.string(), z.number()]).refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (num >= 0 && num <= 100) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}, "Tax rate must be a number between 0 and 100"),
|
||||||
|
products: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
product_types: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
shipping_options: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateTaxRateForm = () => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const [override, setOverride] = useState<Override | null>(null)
|
||||||
|
const [state, setState] = useState<OverrideState>({
|
||||||
|
[Override.PRODUCT]: false,
|
||||||
|
[Override.PRODUCT_TYPE]: false,
|
||||||
|
[Override.SHIPPING_OPTION]: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof CreateTaxRateSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
code: "",
|
||||||
|
rate: "",
|
||||||
|
products: [],
|
||||||
|
product_types: [],
|
||||||
|
shipping_options: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CreateTaxRateSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedProducts = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "products",
|
||||||
|
defaultValue: [],
|
||||||
|
})
|
||||||
|
const selectedProductTypes = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "product_types",
|
||||||
|
defaultValue: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedShippingOptions = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "shipping_options",
|
||||||
|
defaultValue: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminCreateTaxRate()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const { rate, products, product_types, shipping_options, ...rest } = data
|
||||||
|
|
||||||
|
const productsPaylaod = state[Override.PRODUCT]
|
||||||
|
? products.map((p) => p.value)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const productTypesPayload = state[Override.PRODUCT_TYPE]
|
||||||
|
? product_types.map((p) => p.value)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const shippingOptionsPayload = state[Override.SHIPPING_OPTION]
|
||||||
|
? shipping_options.map((p) => p.value)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
region_id: id!,
|
||||||
|
rate: Number(rate),
|
||||||
|
products: productsPaylaod,
|
||||||
|
product_types: productTypesPayload,
|
||||||
|
shipping_options: shippingOptionsPayload,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOpenDrawer = (override: Override) => {
|
||||||
|
setOverride(override)
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveOverrides = (override: Override) => {
|
||||||
|
let field: "products" | "product_types" | "shipping_options"
|
||||||
|
|
||||||
|
switch (override) {
|
||||||
|
case Override.PRODUCT:
|
||||||
|
field = "products"
|
||||||
|
break
|
||||||
|
case Override.PRODUCT_TYPE:
|
||||||
|
field = "product_types"
|
||||||
|
break
|
||||||
|
case Override.SHIPPING_OPTION:
|
||||||
|
field = "shipping_options"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (options: OverrideOption[]) => {
|
||||||
|
form.setValue(field, options, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveOverride = (override: Override) => {
|
||||||
|
let field: "products" | "product_types" | "shipping_options"
|
||||||
|
|
||||||
|
switch (override) {
|
||||||
|
case Override.PRODUCT:
|
||||||
|
field = "products"
|
||||||
|
break
|
||||||
|
case Override.PRODUCT_TYPE:
|
||||||
|
field = "product_types"
|
||||||
|
break
|
||||||
|
case Override.SHIPPING_OPTION:
|
||||||
|
field = "shipping_options"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (value: string) => {
|
||||||
|
const current = form.getValues(field)
|
||||||
|
|
||||||
|
const newValue = current.filter((c: OverrideOption) => c.value !== value)
|
||||||
|
|
||||||
|
form.setValue(field, newValue, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearOverrides = (override: Override) => {
|
||||||
|
let field: "products" | "product_types" | "shipping_options"
|
||||||
|
|
||||||
|
switch (override) {
|
||||||
|
case Override.PRODUCT:
|
||||||
|
field = "products"
|
||||||
|
break
|
||||||
|
case Override.PRODUCT_TYPE:
|
||||||
|
field = "product_types"
|
||||||
|
break
|
||||||
|
case Override.SHIPPING_OPTION:
|
||||||
|
field = "shipping_options"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
form.setValue(field, [], {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStateChange = (override: Override) => {
|
||||||
|
return (enabled: boolean) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[override]: enabled,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
setOverride(null)
|
||||||
|
setSearchParams(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
replace: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-hidden">
|
||||||
|
<SplitView open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<SplitView.Content>
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
|
||||||
|
)}
|
||||||
|
id="form-section"
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
|
<div>
|
||||||
|
<Heading>{t("taxes.taxRate.createTaxRate")}</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.createTaxRateHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="rate"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.rate")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<PercentageInput {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.code")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Collapsible.Root
|
||||||
|
open={state[Override.PRODUCT]}
|
||||||
|
onOpenChange={handleStateChange(Override.PRODUCT)}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.taxRate.productOverridesLabel")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.productOverridesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-4 pt-4">
|
||||||
|
<OverrideGrid
|
||||||
|
overrides={selectedProducts}
|
||||||
|
onClear={handleClearOverrides(Override.PRODUCT)}
|
||||||
|
onRemove={handleRemoveOverride(Override.PRODUCT)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleOpenDrawer(Override.PRODUCT)}
|
||||||
|
>
|
||||||
|
{t("taxes.taxRate.addProductOverridesAction")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Collapsible.Root
|
||||||
|
open={state[Override.PRODUCT_TYPE]}
|
||||||
|
onOpenChange={handleStateChange(Override.PRODUCT_TYPE)}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.taxRate.productTypeOverridesLabel")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.productTypeOverridesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-4 pt-4">
|
||||||
|
<OverrideGrid
|
||||||
|
overrides={selectedProductTypes}
|
||||||
|
onClear={handleClearOverrides(Override.PRODUCT_TYPE)}
|
||||||
|
onRemove={handleRemoveOverride(Override.PRODUCT_TYPE)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenDrawer(Override.PRODUCT_TYPE)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("taxes.taxRate.addProductTypeOverridesAction")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Collapsible.Root
|
||||||
|
open={state[Override.SHIPPING_OPTION]}
|
||||||
|
onOpenChange={handleStateChange(Override.SHIPPING_OPTION)}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.taxRate.shippingOptionOverridesLabel")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.shippingOptionOverridesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-4 pt-4">
|
||||||
|
<OverrideGrid
|
||||||
|
overrides={selectedShippingOptions}
|
||||||
|
onClear={handleClearOverrides(
|
||||||
|
Override.SHIPPING_OPTION
|
||||||
|
)}
|
||||||
|
onRemove={handleRemoveOverride(
|
||||||
|
Override.SHIPPING_OPTION
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenDrawer(Override.SHIPPING_OPTION)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"taxes.taxRate.addShippingOptionOverridesAction"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Content>
|
||||||
|
<SplitView.Drawer>
|
||||||
|
<OverrideTable
|
||||||
|
product={{
|
||||||
|
selected: selectedProducts,
|
||||||
|
onSave: handleSaveOverrides(Override.PRODUCT),
|
||||||
|
}}
|
||||||
|
productType={{
|
||||||
|
selected: selectedProductTypes,
|
||||||
|
onSave: handleSaveOverrides(Override.PRODUCT_TYPE),
|
||||||
|
}}
|
||||||
|
shippingOption={{
|
||||||
|
selected: selectedShippingOptions,
|
||||||
|
onSave: handleSaveOverrides(Override.SHIPPING_OPTION),
|
||||||
|
}}
|
||||||
|
regionId={id!}
|
||||||
|
selected={override}
|
||||||
|
/>
|
||||||
|
</SplitView.Drawer>
|
||||||
|
</SplitView>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-tax-rate-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { TaxRateCreate as Component } from "./tax-rate-create"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { CreateTaxRateForm } from "./components/create-tax-rate-form"
|
||||||
|
|
||||||
|
export const TaxRateCreate = () => {
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
<CreateTaxRateForm />
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+414
@@ -0,0 +1,414 @@
|
|||||||
|
import { TaxRate } from "@medusajs/medusa"
|
||||||
|
import { Button, Heading, Switch, Text, clx } from "@medusajs/ui"
|
||||||
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
|
import { useAdminUpdateTaxRate } from "medusa-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { OverrideGrid } from "../../../common/components/override-grid"
|
||||||
|
import { OverrideTable } from "../../../common/components/overrides-drawer/overrides-drawer"
|
||||||
|
import { Override } from "../../../common/constants"
|
||||||
|
import { OverrideOption, OverrideState } from "../../../common/types"
|
||||||
|
|
||||||
|
type EditTaxRateOverridesFormProps = {
|
||||||
|
taxRate: TaxRate
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditTaxRateOverridesSchema = z.object({
|
||||||
|
products: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
product_types: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
shipping_options: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditTaxRateOverridesForm = ({
|
||||||
|
taxRate,
|
||||||
|
}: EditTaxRateOverridesFormProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const [override, setOverride] = useState<Override | null>(null)
|
||||||
|
const [state, setState] = useState<OverrideState>({
|
||||||
|
[Override.PRODUCT]: taxRate.products?.length > 0,
|
||||||
|
[Override.PRODUCT_TYPE]: taxRate.product_types?.length > 0,
|
||||||
|
[Override.SHIPPING_OPTION]: taxRate.shipping_options?.length > 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof EditTaxRateOverridesSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
products:
|
||||||
|
taxRate.products?.map((p) => ({
|
||||||
|
label: p.title,
|
||||||
|
value: p.id,
|
||||||
|
})) || [],
|
||||||
|
product_types:
|
||||||
|
taxRate.product_types?.map((pt) => ({
|
||||||
|
label: pt.value,
|
||||||
|
value: pt.id,
|
||||||
|
})) || [],
|
||||||
|
shipping_options:
|
||||||
|
taxRate.shipping_options?.map((so) => ({
|
||||||
|
label: so.name,
|
||||||
|
value: so.id,
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditTaxRateOverridesSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedProducts = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "products",
|
||||||
|
})
|
||||||
|
const selectedProductTypes = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "product_types",
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedShippingOptions = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "shipping_options",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdateTaxRate(taxRate.id)
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const { products, product_types, shipping_options } = data
|
||||||
|
|
||||||
|
const productsPaylaod = state[Override.PRODUCT]
|
||||||
|
? products.map((p) => p.value)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const productTypesPayload = state[Override.PRODUCT_TYPE]
|
||||||
|
? product_types.map((p) => p.value)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const shippingOptionsPayload = state[Override.SHIPPING_OPTION]
|
||||||
|
? shipping_options.map((p) => p.value)
|
||||||
|
: []
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
products: productsPaylaod,
|
||||||
|
product_types: productTypesPayload,
|
||||||
|
shipping_options: shippingOptionsPayload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOpenDrawer = (override: Override) => {
|
||||||
|
setOverride(override)
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveOverrides = (override: Override) => {
|
||||||
|
let field: "products" | "product_types" | "shipping_options"
|
||||||
|
|
||||||
|
switch (override) {
|
||||||
|
case Override.PRODUCT:
|
||||||
|
field = "products"
|
||||||
|
break
|
||||||
|
case Override.PRODUCT_TYPE:
|
||||||
|
field = "product_types"
|
||||||
|
break
|
||||||
|
case Override.SHIPPING_OPTION:
|
||||||
|
field = "shipping_options"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (options: OverrideOption[]) => {
|
||||||
|
form.setValue(field, options, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveOverride = (override: Override) => {
|
||||||
|
let field: "products" | "product_types" | "shipping_options"
|
||||||
|
|
||||||
|
switch (override) {
|
||||||
|
case Override.PRODUCT:
|
||||||
|
field = "products"
|
||||||
|
break
|
||||||
|
case Override.PRODUCT_TYPE:
|
||||||
|
field = "product_types"
|
||||||
|
break
|
||||||
|
case Override.SHIPPING_OPTION:
|
||||||
|
field = "shipping_options"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (value: string) => {
|
||||||
|
const current = form.getValues(field)
|
||||||
|
|
||||||
|
const newValue = current.filter((c: OverrideOption) => c.value !== value)
|
||||||
|
|
||||||
|
form.setValue(field, newValue, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearOverrides = (override: Override) => {
|
||||||
|
let field: "products" | "product_types" | "shipping_options"
|
||||||
|
|
||||||
|
switch (override) {
|
||||||
|
case Override.PRODUCT:
|
||||||
|
field = "products"
|
||||||
|
break
|
||||||
|
case Override.PRODUCT_TYPE:
|
||||||
|
field = "product_types"
|
||||||
|
break
|
||||||
|
case Override.SHIPPING_OPTION:
|
||||||
|
field = "shipping_options"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
form.setValue(field, [], {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStateChange = (override: Override) => {
|
||||||
|
return (enabled: boolean) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[override]: enabled,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
setOverride(null)
|
||||||
|
setSearchParams(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
replace: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-hidden">
|
||||||
|
<SplitView open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<SplitView.Content>
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
|
<div>
|
||||||
|
<Heading>{t("taxes.taxRate.editOverridesTitle")}</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.editOverridesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={state[Override.PRODUCT]}
|
||||||
|
onOpenChange={handleStateChange(Override.PRODUCT)}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.taxRate.productOverridesLabel")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.productOverridesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-4 pt-4">
|
||||||
|
<OverrideGrid
|
||||||
|
overrides={selectedProducts}
|
||||||
|
onClear={handleClearOverrides(Override.PRODUCT)}
|
||||||
|
onRemove={handleRemoveOverride(Override.PRODUCT)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleOpenDrawer(Override.PRODUCT)}
|
||||||
|
>
|
||||||
|
{t("taxes.taxRate.addProductOverridesAction")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Collapsible.Root
|
||||||
|
open={state[Override.PRODUCT_TYPE]}
|
||||||
|
onOpenChange={handleStateChange(Override.PRODUCT_TYPE)}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.taxRate.productTypeOverridesLabel")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.productTypeOverridesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-4 pt-4">
|
||||||
|
<OverrideGrid
|
||||||
|
overrides={selectedProductTypes}
|
||||||
|
onClear={handleClearOverrides(Override.PRODUCT_TYPE)}
|
||||||
|
onRemove={handleRemoveOverride(Override.PRODUCT_TYPE)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenDrawer(Override.PRODUCT_TYPE)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("taxes.taxRate.addProductTypeOverridesAction")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Collapsible.Root
|
||||||
|
open={state[Override.SHIPPING_OPTION]}
|
||||||
|
onOpenChange={handleStateChange(Override.SHIPPING_OPTION)}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[1fr_32px] items-start gap-x-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("taxes.taxRate.shippingOptionOverridesLabel")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("taxes.taxRate.shippingOptionOverridesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Switch className="data-[state=open]:bg-ui-bg-interactive" />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="flex flex-col gap-y-4 pt-4">
|
||||||
|
<OverrideGrid
|
||||||
|
overrides={selectedShippingOptions}
|
||||||
|
onClear={handleClearOverrides(
|
||||||
|
Override.SHIPPING_OPTION
|
||||||
|
)}
|
||||||
|
onRemove={handleRemoveOverride(
|
||||||
|
Override.SHIPPING_OPTION
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenDrawer(Override.SHIPPING_OPTION)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"taxes.taxRate.addShippingOptionOverridesAction"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Content>
|
||||||
|
<SplitView.Drawer>
|
||||||
|
<OverrideTable
|
||||||
|
product={{
|
||||||
|
selected: selectedProducts,
|
||||||
|
onSave: handleSaveOverrides(Override.PRODUCT),
|
||||||
|
}}
|
||||||
|
productType={{
|
||||||
|
selected: selectedProductTypes,
|
||||||
|
onSave: handleSaveOverrides(Override.PRODUCT_TYPE),
|
||||||
|
}}
|
||||||
|
shippingOption={{
|
||||||
|
selected: selectedShippingOptions,
|
||||||
|
onSave: handleSaveOverrides(Override.SHIPPING_OPTION),
|
||||||
|
}}
|
||||||
|
regionId={taxRate.region_id}
|
||||||
|
selected={override}
|
||||||
|
/>
|
||||||
|
</SplitView.Drawer>
|
||||||
|
</SplitView>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-tax-rate-overrides-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { TaxRateEditOverrides as Component } from "./tax-rate-edit-overrides"
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import { useAdminTaxRate } from "medusa-react"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { EditTaxRateOverridesForm } from "./components/edit-tax-rate-overrides-form"
|
||||||
|
|
||||||
|
export const TaxRateEditOverrides = () => {
|
||||||
|
const { rate_id } = useParams()
|
||||||
|
|
||||||
|
const { tax_rate, isLoading, isError, error } = useAdminTaxRate(rate_id!, {
|
||||||
|
expand: ["products", "shipping_options", "product_types"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const ready = !isLoading && tax_rate
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
{ready && <EditTaxRateOverridesForm taxRate={tax_rate} />}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { TaxRate } from "@medusajs/medusa"
|
||||||
|
import { Button, Input } from "@medusajs/ui"
|
||||||
|
import { useAdminUpdateTaxRate } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
|
||||||
|
type EditTaxRateFormProps = {
|
||||||
|
taxRate: TaxRate
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditTaxRateSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
code: z.string().min(1),
|
||||||
|
rate: z.union([z.string(), z.number()]).refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return true // we allow empty string and read it as null
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (num >= 0 && num <= 100) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}, "Tax rate must be a number between 0 and 100"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditTaxRateForm = ({ taxRate }: EditTaxRateFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof EditTaxRateSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: taxRate.name,
|
||||||
|
code: taxRate.code || "",
|
||||||
|
rate: taxRate.rate || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditTaxRateSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdateTaxRate(taxRate.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
rate: data.rate ? Number(data.rate) : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
|
<RouteDrawer.Body className="flex flex-col gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="rate"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.rate")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<PercentageInput {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.code")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input type="number" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-tax-rate-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { TaxRateEdit as Component } from "./tax-rate-edit"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminTaxRate } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { EditTaxRateForm } from "./components/edit-tax-rate-form"
|
||||||
|
|
||||||
|
export const TaxRateEdit = () => {
|
||||||
|
const { rate_id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { tax_rate, isLoading, isError, error } = useAdminTaxRate(rate_id!)
|
||||||
|
|
||||||
|
const ready = !isLoading && tax_rate
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("taxes.taxRate.editTaxRate")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && <EditTaxRateForm taxRate={tax_rate} />}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { TaxDetails as Component } from "./tax-details"
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useAdminRegion } from "medusa-react"
|
|
||||||
import { useParams } from "react-router-dom"
|
|
||||||
import { TaxDetailsSection } from "../../components/tax-details-section"
|
|
||||||
import { TaxRatesSection } from "../../components/tax-rates-section"
|
|
||||||
|
|
||||||
export const TaxDetails = () => {
|
|
||||||
const { id } = useParams()
|
|
||||||
|
|
||||||
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div>Loading</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError || !region) {
|
|
||||||
const err = error ? JSON.parse(JSON.stringify(error)) : null
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{(err as Error & { status: number })?.status === 404 ? (
|
|
||||||
<div>Not found</div>
|
|
||||||
) : (
|
|
||||||
<div>Something went wrong!</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<TaxDetailsSection region={region} />
|
|
||||||
<TaxRatesSection />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { useAdminRegions } from "medusa-react"
|
|
||||||
import { Link } from "react-router-dom"
|
|
||||||
|
|
||||||
export const TaxList = () => {
|
|
||||||
const { regions, isLoading, isError, error } = useAdminRegions()
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div>Loading...</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError || !regions) {
|
|
||||||
return <div>Error</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{regions.map((region) => {
|
|
||||||
return (
|
|
||||||
<div key={region.id}>
|
|
||||||
<Link to={`/settings/taxes/${region.id}`}>{region.name}</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,69 @@ export interface AdminGetTaxRatesParams {
|
|||||||
* Filter by code.
|
* Filter by code.
|
||||||
*/
|
*/
|
||||||
code?: string
|
code?: string
|
||||||
|
/**
|
||||||
|
* Filter by a creation date range.
|
||||||
|
*/
|
||||||
|
created_at?: {
|
||||||
|
/**
|
||||||
|
* filter by dates less than this date
|
||||||
|
*/
|
||||||
|
lt?: string
|
||||||
|
/**
|
||||||
|
* filter by dates greater than this date
|
||||||
|
*/
|
||||||
|
gt?: string
|
||||||
|
/**
|
||||||
|
* filter by dates less than or equal to this date
|
||||||
|
*/
|
||||||
|
lte?: string
|
||||||
|
/**
|
||||||
|
* filter by dates greater than or equal to this date
|
||||||
|
*/
|
||||||
|
gte?: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Filter by an update date range.
|
||||||
|
*/
|
||||||
|
updated_at?: {
|
||||||
|
/**
|
||||||
|
* filter by dates less than this date
|
||||||
|
*/
|
||||||
|
lt?: string
|
||||||
|
/**
|
||||||
|
* filter by dates greater than this date
|
||||||
|
*/
|
||||||
|
gt?: string
|
||||||
|
/**
|
||||||
|
* filter by dates less than or equal to this date
|
||||||
|
*/
|
||||||
|
lte?: string
|
||||||
|
/**
|
||||||
|
* filter by dates greater than or equal to this date
|
||||||
|
*/
|
||||||
|
gte?: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Filter by a deletion date range.
|
||||||
|
*/
|
||||||
|
deleted_at?: {
|
||||||
|
/**
|
||||||
|
* filter by dates less than this date
|
||||||
|
*/
|
||||||
|
lt?: string
|
||||||
|
/**
|
||||||
|
* filter by dates greater than this date
|
||||||
|
*/
|
||||||
|
gt?: string
|
||||||
|
/**
|
||||||
|
* filter by dates less than or equal to this date
|
||||||
|
*/
|
||||||
|
lte?: string
|
||||||
|
/**
|
||||||
|
* filter by dates greater than or equal to this date
|
||||||
|
*/
|
||||||
|
gte?: string
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Filter by Rate
|
* Filter by Rate
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +102,14 @@ export interface AdminGetTaxRatesParams {
|
|||||||
*/
|
*/
|
||||||
gte?: number
|
gte?: number
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Term used to search tax rates by name.
|
||||||
|
*/
|
||||||
|
q?: string
|
||||||
|
/**
|
||||||
|
* A tax rate field to sort-order the retrieved tax rates by.
|
||||||
|
*/
|
||||||
|
order?: string
|
||||||
/**
|
/**
|
||||||
* The number of tax rates to skip when retrieving the tax rates.
|
* The number of tax rates to skip when retrieving the tax rates.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { IsArray, IsNumber, IsOptional, IsString } from "class-validator"
|
import {
|
||||||
import { getListConfig, pickByConfig } from "./utils/get-query-config"
|
IsArray,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator"
|
||||||
import { identity, omit, pickBy } from "lodash"
|
import { identity, omit, pickBy } from "lodash"
|
||||||
|
import { getListConfig, pickByConfig } from "./utils/get-query-config"
|
||||||
|
|
||||||
import { IsType } from "../../../../utils/validators/is-type"
|
import { isDefined } from "@medusajs/utils"
|
||||||
import { NumericalComparisonOperator } from "../../../../types/common"
|
import { Type } from "class-transformer"
|
||||||
import { TaxRate } from "../../../.."
|
import { TaxRate } from "../../../.."
|
||||||
import { TaxRateService } from "../../../../services"
|
import { TaxRateService } from "../../../../services"
|
||||||
import { Type } from "class-transformer"
|
import {
|
||||||
|
DateComparisonOperator,
|
||||||
|
NumericalComparisonOperator,
|
||||||
|
} from "../../../../types/common"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
|
import { IsType } from "../../../../utils/validators/is-type"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /admin/tax-rates
|
* @oas [get] /admin/tax-rates
|
||||||
@@ -30,6 +40,72 @@ import { validator } from "../../../../utils/validator"
|
|||||||
* type: string
|
* type: string
|
||||||
* - (query) code {string} Filter by code.
|
* - (query) code {string} Filter by code.
|
||||||
* - in: query
|
* - in: query
|
||||||
|
* name: created_at
|
||||||
|
* description: Filter by a creation date range.
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* lt:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates less than this date
|
||||||
|
* format: date
|
||||||
|
* gt:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates greater than this date
|
||||||
|
* format: date
|
||||||
|
* lte:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates less than or equal to this date
|
||||||
|
* format: date
|
||||||
|
* gte:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates greater than or equal to this date
|
||||||
|
* format: date
|
||||||
|
* - in: query
|
||||||
|
* name: updated_at
|
||||||
|
* description: Filter by an update date range.
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* lt:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates less than this date
|
||||||
|
* format: date
|
||||||
|
* gt:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates greater than this date
|
||||||
|
* format: date
|
||||||
|
* lte:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates less than or equal to this date
|
||||||
|
* format: date
|
||||||
|
* gte:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates greater than or equal to this date
|
||||||
|
* format: date
|
||||||
|
* - in: query
|
||||||
|
* name: deleted_at
|
||||||
|
* description: Filter by a deletion date range.
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* lt:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates less than this date
|
||||||
|
* format: date
|
||||||
|
* gt:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates greater than this date
|
||||||
|
* format: date
|
||||||
|
* lte:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates less than or equal to this date
|
||||||
|
* format: date
|
||||||
|
* gte:
|
||||||
|
* type: string
|
||||||
|
* description: filter by dates greater than or equal to this date
|
||||||
|
* format: date
|
||||||
|
* - in: query
|
||||||
* name: rate
|
* name: rate
|
||||||
* style: form
|
* style: form
|
||||||
* explode: false
|
* explode: false
|
||||||
@@ -51,6 +127,8 @@ import { validator } from "../../../../utils/validator"
|
|||||||
* gte:
|
* gte:
|
||||||
* type: number
|
* type: number
|
||||||
* description: filter by rates greater than or equal to this number
|
* description: filter by rates greater than or equal to this number
|
||||||
|
* - (query) q {string} Term used to search tax rates by name.
|
||||||
|
* - (query) order {string} A tax rate field to sort-order the retrieved tax rates by.
|
||||||
* - (query) offset=0 {integer} The number of tax rates to skip when retrieving the tax rates.
|
* - (query) offset=0 {integer} The number of tax rates to skip when retrieving the tax rates.
|
||||||
* - (query) limit=50 {integer} Limit the number of tax rates returned.
|
* - (query) limit=50 {integer} Limit the number of tax rates returned.
|
||||||
* - in: query
|
* - in: query
|
||||||
@@ -151,7 +229,29 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||||
|
|
||||||
const listConfig = getListConfig()
|
const order = value.order
|
||||||
|
let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined
|
||||||
|
if (isDefined(order)) {
|
||||||
|
let orderField = order
|
||||||
|
if (order.startsWith("-")) {
|
||||||
|
const [, field] = order.split("-")
|
||||||
|
orderField = field
|
||||||
|
orderBy = { [field]: "DESC" }
|
||||||
|
} else {
|
||||||
|
orderBy = { [order]: "ASC" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const defaultOrder: string = "created_at"
|
||||||
|
orderBy = { [defaultOrder]: "DESC" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const listConfig = getListConfig(
|
||||||
|
value.fields as (keyof TaxRate)[],
|
||||||
|
value.expand,
|
||||||
|
value.limit,
|
||||||
|
value.offset,
|
||||||
|
orderBy
|
||||||
|
)
|
||||||
|
|
||||||
const filterableFields = omit(value, [
|
const filterableFields = omit(value, [
|
||||||
"limit",
|
"limit",
|
||||||
@@ -234,4 +334,39 @@ export class AdminGetTaxRatesParams {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
fields?: string[]
|
fields?: string[]
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
order?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date filters to apply on the tax rates' `update_at` date.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
updated_at?: DateComparisonOperator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date filters to apply on the customer tax rates' `created_at` date.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
created_at?: DateComparisonOperator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date filters to apply on the tax rates' `deleted_at` date.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
deleted_at?: DateComparisonOperator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Term used to search tax rates by name.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
q?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { promiseAll } from "@medusajs/utils"
|
||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
import { EntityManager, In } from "typeorm"
|
import { EntityManager, ILike, In } from "typeorm"
|
||||||
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import {
|
import {
|
||||||
ProductTaxRate,
|
ProductTaxRate,
|
||||||
ProductTypeTaxRate,
|
ProductTypeTaxRate,
|
||||||
@@ -18,8 +20,6 @@ import {
|
|||||||
UpdateTaxRateInput,
|
UpdateTaxRateInput,
|
||||||
} from "../types/tax-rate"
|
} from "../types/tax-rate"
|
||||||
import { buildQuery, PostgresError } from "../utils"
|
import { buildQuery, PostgresError } from "../utils"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
|
||||||
import { promiseAll } from "@medusajs/utils"
|
|
||||||
|
|
||||||
class TaxRateService extends TransactionBaseService {
|
class TaxRateService extends TransactionBaseService {
|
||||||
protected readonly productService_: ProductService
|
protected readonly productService_: ProductService
|
||||||
@@ -49,7 +49,20 @@ class TaxRateService extends TransactionBaseService {
|
|||||||
const taxRateRepo = this.activeManager_.withRepository(
|
const taxRateRepo = this.activeManager_.withRepository(
|
||||||
this.taxRateRepository_
|
this.taxRateRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let q: string | undefined
|
||||||
|
|
||||||
|
if (selector.q) {
|
||||||
|
q = selector.q
|
||||||
|
delete selector.q
|
||||||
|
}
|
||||||
|
|
||||||
const query = buildQuery(selector, config)
|
const query = buildQuery(selector, config)
|
||||||
|
|
||||||
|
if (q) {
|
||||||
|
query.where["name"] = ILike(`%${q}%`)
|
||||||
|
}
|
||||||
|
|
||||||
return await taxRateRepo.findWithResolution(query)
|
return await taxRateRepo.findWithResolution(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +73,20 @@ class TaxRateService extends TransactionBaseService {
|
|||||||
const taxRateRepo = this.activeManager_.withRepository(
|
const taxRateRepo = this.activeManager_.withRepository(
|
||||||
this.taxRateRepository_
|
this.taxRateRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let q: string | undefined
|
||||||
|
|
||||||
|
if (selector.q) {
|
||||||
|
q = selector.q
|
||||||
|
delete selector.q
|
||||||
|
}
|
||||||
|
|
||||||
const query = buildQuery(selector, config)
|
const query = buildQuery(selector, config)
|
||||||
|
|
||||||
|
if (q) {
|
||||||
|
query.where["name"] = ILike(`%${q}%`)
|
||||||
|
}
|
||||||
|
|
||||||
return await taxRateRepo.findAndCountWithResolution(query)
|
return await taxRateRepo.findAndCountWithResolution(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type FilterableTaxRateProps = {
|
|||||||
created_at?: Date | DateComparisonOperator
|
created_at?: Date | DateComparisonOperator
|
||||||
updated_at?: Date | DateComparisonOperator
|
updated_at?: Date | DateComparisonOperator
|
||||||
deleted_at?: Date | DateComparisonOperator
|
deleted_at?: Date | DateComparisonOperator
|
||||||
|
q?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateTaxRateInput = {
|
export type UpdateTaxRateInput = {
|
||||||
|
|||||||
Reference in New Issue
Block a user