feat(dashboard): added details page for promotions + edit sliders (#6882)
* chore: added details page for promotions * chore: add edit rules, edit details and edit campaign pages * chore: change to type button * chore: connection for rules * chore: listing rule labels of multiple modules * chore: add badge summary list * chore: fix campaigns
This commit is contained in:
1 parent
07fb058d96
commit
5e30b8cce6
60 files changed
+2803
-153
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"medusa-react": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(dashboard): added details page for promotions
|
||||||
@@ -194,7 +194,7 @@ medusaIntegrationTestRunner({
|
|||||||
`/admin/pricing/rule-types/${ruleType.id}`,
|
`/admin/pricing/rule-types/${ruleType.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
console.log("response.data -- ", response.data)
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data).toEqual({
|
expect(response.data).toEqual({
|
||||||
id: ruleType.id,
|
id: ruleType.id,
|
||||||
|
|||||||
@@ -140,6 +140,15 @@
|
|||||||
},
|
},
|
||||||
"required": ["domain"]
|
"required": ["domain"]
|
||||||
},
|
},
|
||||||
|
"campaigns": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
"giftCards": {
|
"giftCards": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"items_one": "{{count}} item",
|
"items_one": "{{count}} item",
|
||||||
"items_other": "{{count}} items",
|
"items_other": "{{count}} items",
|
||||||
"countSelected": "{{count}} selected",
|
"countSelected": "{{count}} selected",
|
||||||
|
"plusCount": "+ {{count}}",
|
||||||
"plusCountMore": "+ {{count}} more",
|
"plusCountMore": "+ {{count}} more",
|
||||||
"areYouSure": "Are you sure?",
|
"areYouSure": "Are you sure?",
|
||||||
"noRecordsFound": "No records found",
|
"noRecordsFound": "No records found",
|
||||||
@@ -611,12 +612,116 @@
|
|||||||
"domain": "Promotions",
|
"domain": "Promotions",
|
||||||
"fields": {
|
"fields": {
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
"campaign": "Campaign"
|
"type": "Type",
|
||||||
|
"value_type": "Value Type",
|
||||||
|
"value": "Value",
|
||||||
|
"campaign": "Campaign",
|
||||||
|
"allocation": "Allocation",
|
||||||
|
"conditions": {
|
||||||
|
"rules": {
|
||||||
|
"title": "Who can use this code?",
|
||||||
|
"description": "Is the customer allowed to add the promotion code? Discount code can be used by all customers if left untouched. Choose between attributes, operators, and values to set up the conditions."
|
||||||
|
},
|
||||||
|
"target-rules": {
|
||||||
|
"title": "What needs to be in the cart to unlock the promotion?",
|
||||||
|
"description": "If these conditions match, we enable a promotion action on the target items. Choose between attributes, operators, and values to set up the conditions."
|
||||||
|
},
|
||||||
|
"buy-rules": {
|
||||||
|
"title": "What will the promotion be applied to?",
|
||||||
|
"description": "The promotion will be applied to items that match these conditions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Edit Promotion Details",
|
||||||
|
"rules": {
|
||||||
|
"title": "Edit rules"
|
||||||
|
},
|
||||||
|
"target-rules": {
|
||||||
|
"title": "Edit target rules"
|
||||||
|
},
|
||||||
|
"buy-rules": {
|
||||||
|
"title": "Edit buy rules"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"addToCampaign": {
|
||||||
|
"title": "Add Promotion To Campaign"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"campaign": {
|
||||||
|
"existing": {
|
||||||
|
"title": "Existing Campaign",
|
||||||
|
"description": "Would you like to add promotion to an existing campaign?"
|
||||||
|
},
|
||||||
|
"new": {
|
||||||
|
"title": "New Campaign",
|
||||||
|
"description": "Would you like to create a new campaign with this promotion?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Status"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"code": {
|
||||||
|
"title": "Promotion code",
|
||||||
|
"description": "Customers must enter this at checkout"
|
||||||
|
},
|
||||||
|
"automatic": {
|
||||||
|
"title": "Automatic",
|
||||||
|
"description": "Customers will automatically see this during checkout"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allocation": {
|
||||||
|
"each": {
|
||||||
|
"title": "Each",
|
||||||
|
"description": "Applies value on each product"
|
||||||
|
},
|
||||||
|
"across": {
|
||||||
|
"title": "Across",
|
||||||
|
"description": "Applies value proportionally across products"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"title": "Code",
|
||||||
|
"description": "The code your customers will enter during checkout."
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"title": "Value"
|
||||||
|
},
|
||||||
|
"value_type": {
|
||||||
|
"fixed": {
|
||||||
|
"title": "Fixed amount",
|
||||||
|
"description": "eg. 100"
|
||||||
|
},
|
||||||
|
"percentage": {
|
||||||
|
"title": "Percentage",
|
||||||
|
"description": "eg. 8%"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"deleteWarning": "You are about to delete the promotion {{code}}. This action cannot be undone.",
|
"deleteWarning": "You are about to delete the promotion {{code}}. This action cannot be undone.",
|
||||||
"createPromotionTitle": "Create Promotion",
|
"createPromotionTitle": "Create Promotion",
|
||||||
"type": "Promotion type"
|
"type": "Promotion type"
|
||||||
},
|
},
|
||||||
|
"campaigns": {
|
||||||
|
"domain": "Campaigns",
|
||||||
|
"details": "Campaign details",
|
||||||
|
"fields": {
|
||||||
|
"name": "Name",
|
||||||
|
"identifier": "Identifier",
|
||||||
|
"start_date": "Start date",
|
||||||
|
"end_date": "End date"
|
||||||
|
},
|
||||||
|
"budget": {
|
||||||
|
"details": "Campaign budget",
|
||||||
|
"fields": {
|
||||||
|
"type": "Type",
|
||||||
|
"currency": "Currency",
|
||||||
|
"limit": "Limit",
|
||||||
|
"used": "Used"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"pricing": {
|
"pricing": {
|
||||||
"domain": "Pricing",
|
"domain": "Pricing",
|
||||||
"deletePriceListWarning": "You are about to delete the price list {{name}}. This action cannot be undone.",
|
"deletePriceListWarning": "You are about to delete the price list {{name}}. This action cannot be undone.",
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
import { Badge, Tooltip, clx } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type BadgeListSummaryProps = {
|
||||||
|
/**
|
||||||
|
* Number of initial items to display
|
||||||
|
* @default 2
|
||||||
|
*/
|
||||||
|
n?: number
|
||||||
|
/**
|
||||||
|
* List of strings to display as abbreviated list
|
||||||
|
*/
|
||||||
|
list: string[]
|
||||||
|
/**
|
||||||
|
* Is the summary displayed inline.
|
||||||
|
* Determines whether the center text is truncated if there is no space in the container
|
||||||
|
*/
|
||||||
|
inline?: boolean
|
||||||
|
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadgeListSummary = ({
|
||||||
|
list,
|
||||||
|
className,
|
||||||
|
inline,
|
||||||
|
n = 2,
|
||||||
|
}: BadgeListSummaryProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const title = t("general.plusCount", {
|
||||||
|
count: list.length - n,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"ml-2 text-ui-fg-subtle txt-compact-small gap-x-2 overflow-hidden",
|
||||||
|
{
|
||||||
|
"inline-flex": inline,
|
||||||
|
flex: !inline,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{list.slice(0, n).map((item) => {
|
||||||
|
return (
|
||||||
|
<Badge key={item} size="2xsmall">
|
||||||
|
{item}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{list.length > n && (
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<ul>
|
||||||
|
{list.slice(n).map((c) => (
|
||||||
|
<li key={c}>{c}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Badge size="2xsmall" className="cursor-default whitespace-nowrap">
|
||||||
|
{title}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./badge-list-summary"
|
||||||
+34
-11
@@ -1,4 +1,4 @@
|
|||||||
import { ExclamationCircle, MagnifyingGlass } from "@medusajs/icons"
|
import { ExclamationCircle, MagnifyingGlass, PlusMini } from "@medusajs/icons"
|
||||||
import { Button, Text, clx } from "@medusajs/ui"
|
import { Button, Text, clx } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
@@ -32,21 +32,44 @@ export const NoResults = ({ title, message, className }: NoResultsProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoRecordsProps = {
|
type ActionProps = {
|
||||||
title?: string
|
|
||||||
message?: string
|
|
||||||
action?: {
|
action?: {
|
||||||
to: string
|
to: string
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
className?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NoRecordsProps = {
|
||||||
|
title?: string
|
||||||
|
message?: string
|
||||||
|
className?: string
|
||||||
|
buttonVariant?: string
|
||||||
|
} & ActionProps
|
||||||
|
|
||||||
|
const DefaultButton = ({ action }: ActionProps) =>
|
||||||
|
action && (
|
||||||
|
<Link to={action.to}>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
const TransparentIconLeftButton = ({ action }: ActionProps) =>
|
||||||
|
action && (
|
||||||
|
<Link to={action.to}>
|
||||||
|
<Button variant="transparent" className="text-ui-fg-interactive">
|
||||||
|
<PlusMini /> {action.label}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
export const NoRecords = ({
|
export const NoRecords = ({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
action,
|
action,
|
||||||
className,
|
className,
|
||||||
|
buttonVariant = "default",
|
||||||
}: NoRecordsProps) => {
|
}: NoRecordsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -59,19 +82,19 @@ export const NoRecords = ({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-y-2">
|
<div className="flex flex-col items-center gap-y-2">
|
||||||
<ExclamationCircle />
|
<ExclamationCircle />
|
||||||
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{title ?? t("general.noRecordsTitle")}
|
{title ?? t("general.noRecordsTitle")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="small" className="text-ui-fg-muted">
|
<Text size="small" className="text-ui-fg-muted">
|
||||||
{message ?? t("general.noRecordsMessage")}
|
{message ?? t("general.noRecordsMessage")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{action && (
|
|
||||||
<Link to={action.to}>
|
{buttonVariant === "default" && <DefaultButton action={action} />}
|
||||||
<Button variant="secondary" size="small">
|
{buttonVariant === "transparentIconLeft" && (
|
||||||
{action.label}
|
<TransparentIconLeftButton action={action} />
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
QueryKey,
|
||||||
|
UseMutationOptions,
|
||||||
|
UseQueryOptions,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
|
import { client } from "../../lib/client"
|
||||||
|
import { queryClient } from "../../lib/medusa"
|
||||||
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import { CreateCampaignReq, UpdateCampaignReq } from "../../types/api-payloads"
|
||||||
|
import {
|
||||||
|
CampaignDeleteRes,
|
||||||
|
CampaignListRes,
|
||||||
|
CampaignRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
|
||||||
|
const REGIONS_QUERY_KEY = "campaigns" as const
|
||||||
|
const campaignsQueryKeys = queryKeysFactory(REGIONS_QUERY_KEY)
|
||||||
|
|
||||||
|
export const useCampaign = (
|
||||||
|
id: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<CampaignRes, Error, CampaignRes, QueryKey>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryKey: campaignsQueryKeys.detail(id),
|
||||||
|
queryFn: async () => client.campaigns.retrieve(id),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCampaigns = (
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<CampaignListRes, Error, CampaignListRes, QueryKey>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.campaigns.list(query),
|
||||||
|
queryKey: campaignsQueryKeys.list(query),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateCampaign = (
|
||||||
|
options?: UseMutationOptions<CampaignRes, Error, CreateCampaignReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.campaigns.create(payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() })
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateCampaign = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<CampaignRes, Error, UpdateCampaignReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.campaigns.update(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.detail(id) })
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteCampaign = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<CampaignDeleteRes, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.campaigns.delete(id),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.detail(id) })
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,12 +1,47 @@
|
|||||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
|
||||||
|
|
||||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
QueryKey,
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions,
|
||||||
|
useQuery,
|
||||||
|
UseQueryOptions,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
import { client } from "../../lib/client"
|
import { client } from "../../lib/client"
|
||||||
|
import { queryClient } from "../../lib/medusa"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
import { PromotionListRes, PromotionRes } from "../../types/api-responses"
|
import {
|
||||||
|
BatchAddPromotionRulesReq,
|
||||||
|
BatchRemovePromotionRulesReq,
|
||||||
|
BatchUpdatePromotionRulesReq,
|
||||||
|
CreatePromotionReq,
|
||||||
|
UpdatePromotionReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
|
import {
|
||||||
|
PromotionDeleteRes,
|
||||||
|
PromotionListRes,
|
||||||
|
PromotionRes,
|
||||||
|
PromotionRuleAttributesListRes,
|
||||||
|
PromotionRuleOperatorsListRes,
|
||||||
|
PromotionRulesListRes,
|
||||||
|
PromotionRuleValuesListRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
|
||||||
const PROMOTIONS_QUERY_KEY = "promotions" as const
|
const PROMOTIONS_QUERY_KEY = "promotions" as const
|
||||||
const promotionsQueryKeys = queryKeysFactory(PROMOTIONS_QUERY_KEY)
|
export const promotionsQueryKeys = {
|
||||||
|
...queryKeysFactory(PROMOTIONS_QUERY_KEY),
|
||||||
|
listRules: (id: string, ruleType: string) => [
|
||||||
|
PROMOTIONS_QUERY_KEY,
|
||||||
|
id,
|
||||||
|
ruleType,
|
||||||
|
],
|
||||||
|
listRuleAttributes: (ruleType: string) => [PROMOTIONS_QUERY_KEY, ruleType],
|
||||||
|
listRuleValues: (ruleType: string, ruleValue: string) => [
|
||||||
|
PROMOTIONS_QUERY_KEY,
|
||||||
|
ruleType,
|
||||||
|
ruleValue,
|
||||||
|
],
|
||||||
|
listRuleOperators: () => [PROMOTIONS_QUERY_KEY],
|
||||||
|
}
|
||||||
|
|
||||||
export const usePromotion = (
|
export const usePromotion = (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -24,6 +59,28 @@ export const usePromotion = (
|
|||||||
return { ...data, ...rest }
|
return { ...data, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePromotionRules = (
|
||||||
|
id: string,
|
||||||
|
ruleType: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
PromotionRulesListRes,
|
||||||
|
Error,
|
||||||
|
PromotionRulesListRes,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryKey: promotionsQueryKeys.listRules(id, ruleType),
|
||||||
|
queryFn: async () => client.promotions.listRules(id, ruleType),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
export const usePromotions = (
|
export const usePromotions = (
|
||||||
query?: AdminGetPromotionsParams,
|
query?: AdminGetPromotionsParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
@@ -39,3 +96,185 @@ export const usePromotions = (
|
|||||||
|
|
||||||
return { ...data, ...rest }
|
return { ...data, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePromotionRuleOperators = (
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
PromotionListRes,
|
||||||
|
Error,
|
||||||
|
PromotionRuleOperatorsListRes,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryKey: promotionsQueryKeys.listRuleOperators(),
|
||||||
|
queryFn: async () => client.promotions.listRuleOperators(),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePromotionRuleAttributes = (
|
||||||
|
ruleType: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
PromotionListRes,
|
||||||
|
Error,
|
||||||
|
PromotionRuleAttributesListRes,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryKey: promotionsQueryKeys.listRuleAttributes(ruleType),
|
||||||
|
queryFn: async () => client.promotions.listRuleAttributes(ruleType),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePromotionRuleValues = (
|
||||||
|
ruleType: string,
|
||||||
|
ruleValue: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
PromotionListRes,
|
||||||
|
Error,
|
||||||
|
PromotionRuleValuesListRes,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryKey: promotionsQueryKeys.listRuleValues(ruleType, ruleValue),
|
||||||
|
queryFn: async () => client.promotions.listRuleValues(ruleType, ruleValue),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePromotion = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<PromotionDeleteRes, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.promotions.delete(id),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: promotionsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreatePromotion = (
|
||||||
|
options?: UseMutationOptions<PromotionRes, Error, CreatePromotionReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.promotions.create(payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdatePromotion = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<PromotionRes, Error, UpdatePromotionReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.promotions.update(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: promotionsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePromotionAddRules = (
|
||||||
|
id: string,
|
||||||
|
ruleType: string,
|
||||||
|
options?: UseMutationOptions<PromotionRes, Error, BatchAddPromotionRulesReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => client.promotions.addRules(id, ruleType, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: promotionsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePromotionRemoveRules = (
|
||||||
|
id: string,
|
||||||
|
ruleType: string,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
PromotionRes,
|
||||||
|
Error,
|
||||||
|
BatchRemovePromotionRulesReq
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) =>
|
||||||
|
client.promotions.removeRules(id, ruleType, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: promotionsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePromotionUpdateRules = (
|
||||||
|
id: string,
|
||||||
|
ruleType: string,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
PromotionRes,
|
||||||
|
Error,
|
||||||
|
BatchUpdatePromotionRulesReq
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) =>
|
||||||
|
client.promotions.updateRules(id, ruleType, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: promotionsQueryKeys.listRules(id, ruleType),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: promotionsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
AdminCampaignRes,
|
||||||
|
AdminCampaignsListRes,
|
||||||
|
AdminGetCampaignsCampaignParams,
|
||||||
|
AdminGetCampaignsParams,
|
||||||
|
AdminPostCampaignsCampaignReq,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import { queryKeysFactory, useAdminCustomQuery } from "medusa-react"
|
||||||
|
import { medusa } from "../medusa"
|
||||||
|
|
||||||
|
const QUERY_KEY = "admin_campaigns"
|
||||||
|
export const adminCampaignKeys = queryKeysFactory<
|
||||||
|
typeof QUERY_KEY,
|
||||||
|
AdminGetCampaignsParams
|
||||||
|
>(QUERY_KEY)
|
||||||
|
|
||||||
|
export const adminCampaignQueryFns = {
|
||||||
|
list: (query: AdminGetCampaignsParams) =>
|
||||||
|
medusa.admin.custom.get(`/admin/campaigns`, query),
|
||||||
|
detail: (id: string) => medusa.admin.custom.get(`/admin/campaigns/${id}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useV2Campaigns = (
|
||||||
|
query?: AdminGetCampaignsParams,
|
||||||
|
options?: object
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useAdminCustomQuery<
|
||||||
|
AdminGetCampaignsParams,
|
||||||
|
AdminCampaignsListRes
|
||||||
|
>("/admin/campaigns", adminCampaignKeys.list(query), query, options)
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useV2Campaign = (
|
||||||
|
id: string,
|
||||||
|
query?: AdminGetCampaignsParams,
|
||||||
|
options?: object
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useAdminCustomQuery<
|
||||||
|
AdminGetCampaignsCampaignParams,
|
||||||
|
AdminCampaignRes
|
||||||
|
>(`/admin/campaigns/${id}`, adminCampaignKeys.detail(id), query, options)
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useV2DeleteCampaign = (id: string) => {
|
||||||
|
return useMutation(() => medusa.admin.custom.delete(`/admin/campaigns/${id}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useV2PostCampaign = (id: string) => {
|
||||||
|
return useMutation((args: AdminPostCampaignsCampaignReq) =>
|
||||||
|
medusa.client.request("POST", `/admin/campaigns/${id}`, args)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from "./auth"
|
export * from "./auth"
|
||||||
export * from "./promotion"
|
|
||||||
export * from "./store"
|
export * from "./store"
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import {
|
|
||||||
AdminGetPromotionsParams,
|
|
||||||
AdminPromotionsListRes,
|
|
||||||
} from "@medusajs/medusa"
|
|
||||||
import { Response } from "@medusajs/medusa-js"
|
|
||||||
import {
|
|
||||||
UseQueryOptionsWrapper,
|
|
||||||
queryKeysFactory,
|
|
||||||
useAdminCustomQuery,
|
|
||||||
} from "medusa-react"
|
|
||||||
|
|
||||||
const ADMIN_PROMOTIONS_QUERY_KEY = "admin_promotions"
|
|
||||||
|
|
||||||
export const adminPromotionKeys = queryKeysFactory<
|
|
||||||
typeof ADMIN_PROMOTIONS_QUERY_KEY,
|
|
||||||
AdminGetPromotionsParams
|
|
||||||
>(ADMIN_PROMOTIONS_QUERY_KEY)
|
|
||||||
|
|
||||||
type PromotionQueryKey = typeof adminPromotionKeys
|
|
||||||
|
|
||||||
export const useV2Promotions = (
|
|
||||||
query?: AdminGetPromotionsParams,
|
|
||||||
options?: UseQueryOptionsWrapper<
|
|
||||||
Response<AdminPromotionsListRes>,
|
|
||||||
Error,
|
|
||||||
ReturnType<PromotionQueryKey["list"]>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { data, ...rest } = useAdminCustomQuery<
|
|
||||||
AdminGetPromotionsParams,
|
|
||||||
AdminPromotionsListRes
|
|
||||||
>("/promotions", adminPromotionKeys.list(query), query, options as any)
|
|
||||||
|
|
||||||
return { ...data, ...rest }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { CreateCampaignDTO, UpdateCampaignDTO } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
CampaignDeleteRes,
|
||||||
|
CampaignListRes,
|
||||||
|
CampaignRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
|
async function retrieveCampaign(id: string, query?: Record<string, any>) {
|
||||||
|
return getRequest<CampaignRes>(`/admin/campaigns/${id}`, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listCampaigns(query?: Record<string, any>) {
|
||||||
|
return getRequest<CampaignListRes>(`/admin/campaigns`, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCampaign(payload: CreateCampaignDTO) {
|
||||||
|
return postRequest<CampaignRes>(`/admin/campaigns`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCampaign(id: string, payload: UpdateCampaignDTO) {
|
||||||
|
return postRequest<CampaignRes>(`/admin/campaigns/${id}`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCampaign(id: string) {
|
||||||
|
return deleteRequest<CampaignDeleteRes>(`/admin/campaigns/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const campaigns = {
|
||||||
|
retrieve: retrieveCampaign,
|
||||||
|
list: listCampaigns,
|
||||||
|
create: createCampaign,
|
||||||
|
update: updateCampaign,
|
||||||
|
delete: deleteCampaign,
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { apiKeys } from "./api-keys"
|
import { apiKeys } from "./api-keys"
|
||||||
import { auth } from "./auth"
|
import { auth } from "./auth"
|
||||||
|
import { campaigns } from "./campaigns"
|
||||||
import { categories } from "./categories"
|
import { categories } from "./categories"
|
||||||
import { collections } from "./collections"
|
import { collections } from "./collections"
|
||||||
import { currencies } from "./currencies"
|
import { currencies } from "./currencies"
|
||||||
@@ -19,6 +20,7 @@ import { workflowExecutions } from "./workflow-executions"
|
|||||||
export const client = {
|
export const client = {
|
||||||
auth: auth,
|
auth: auth,
|
||||||
apiKeys: apiKeys,
|
apiKeys: apiKeys,
|
||||||
|
campaigns: campaigns,
|
||||||
categories: categories,
|
categories: categories,
|
||||||
customers: customers,
|
customers: customers,
|
||||||
currencies: currencies,
|
currencies: currencies,
|
||||||
|
|||||||
@@ -1,23 +1,113 @@
|
|||||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||||
|
|
||||||
import { PromotionListRes, PromotionRes } from "../../types/api-responses"
|
import {
|
||||||
import { getRequest } from "./common"
|
BatchAddPromotionRulesReq,
|
||||||
|
BatchRemovePromotionRulesReq,
|
||||||
|
BatchUpdatePromotionRulesReq,
|
||||||
|
CreatePromotionReq,
|
||||||
|
UpdatePromotionReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
|
import {
|
||||||
|
PromotionDeleteRes,
|
||||||
|
PromotionListRes,
|
||||||
|
PromotionRes,
|
||||||
|
PromotionRuleAttributesListRes,
|
||||||
|
PromotionRuleOperatorsListRes,
|
||||||
|
PromotionRuleValuesListRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
const retrievePromotion = async (
|
async function retrievePromotion(id: string, query?: AdminGetPromotionsParams) {
|
||||||
id: string,
|
|
||||||
query?: AdminGetPromotionsParams
|
|
||||||
) => {
|
|
||||||
return getRequest<PromotionRes, AdminGetPromotionsParams>(
|
return getRequest<PromotionRes, AdminGetPromotionsParams>(
|
||||||
`/admin/promotions/${id}`,
|
`/admin/promotions/${id}`,
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const listPromotions = async (query?: AdminGetPromotionsParams) => {
|
async function listPromotions(query?: AdminGetPromotionsParams) {
|
||||||
return getRequest<PromotionListRes>(`/admin/promotions`, query)
|
return getRequest<PromotionListRes>(`/admin/promotions`, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deletePromotion(id: string) {
|
||||||
|
return deleteRequest<PromotionDeleteRes>(`/admin/promotions/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePromotion(id: string, payload: UpdatePromotionReq) {
|
||||||
|
return postRequest<PromotionRes>(`/admin/promotions/${id}`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPromotion(payload: CreatePromotionReq) {
|
||||||
|
return postRequest<PromotionRes>(`/admin/promotions`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addPromotionRules(
|
||||||
|
id: string,
|
||||||
|
ruleType: string,
|
||||||
|
payload: BatchAddPromotionRulesReq
|
||||||
|
) {
|
||||||
|
return postRequest<PromotionRes>(
|
||||||
|
`/admin/promotions/${id}/${ruleType}/batch/add`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePromotionRules(
|
||||||
|
id: string,
|
||||||
|
ruleType: string,
|
||||||
|
payload: BatchUpdatePromotionRulesReq
|
||||||
|
) {
|
||||||
|
return postRequest<PromotionRes>(
|
||||||
|
`/admin/promotions/${id}/${ruleType}/batch/update`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removePromotionRules(
|
||||||
|
id: string,
|
||||||
|
ruleType: string,
|
||||||
|
payload: BatchRemovePromotionRulesReq
|
||||||
|
) {
|
||||||
|
return postRequest<PromotionRes>(
|
||||||
|
`/admin/promotions/${id}/${ruleType}/batch/remove`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPromotionRules(id: string, ruleType: string) {
|
||||||
|
return getRequest<PromotionRuleAttributesListRes>(
|
||||||
|
`/admin/promotions/${id}/${ruleType}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPromotionRuleAttributes(ruleType: string) {
|
||||||
|
return getRequest<PromotionRuleAttributesListRes>(
|
||||||
|
`/admin/promotions/rule-attribute-options/${ruleType}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPromotionRuleValues(ruleType: string, ruleValue: string) {
|
||||||
|
return getRequest<PromotionRuleValuesListRes>(
|
||||||
|
`/admin/promotions/rule-value-options/${ruleType}/${ruleValue}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPromotionRuleOperators() {
|
||||||
|
return getRequest<PromotionRuleOperatorsListRes>(
|
||||||
|
`/admin/promotions/rule-operator-options`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const promotions = {
|
export const promotions = {
|
||||||
retrieve: retrievePromotion,
|
retrieve: retrievePromotion,
|
||||||
list: listPromotions,
|
list: listPromotions,
|
||||||
|
delete: deletePromotion,
|
||||||
|
update: updatePromotion,
|
||||||
|
create: createPromotion,
|
||||||
|
addRules: addPromotionRules,
|
||||||
|
removeRules: removePromotionRules,
|
||||||
|
updateRules: updatePromotionRules,
|
||||||
|
listRules: listPromotionRules,
|
||||||
|
listRuleAttributes: listPromotionRuleAttributes,
|
||||||
|
listRuleOperators: listPromotionRuleOperators,
|
||||||
|
listRuleValues: listPromotionRuleValues,
|
||||||
}
|
}
|
||||||
@@ -150,6 +150,34 @@ export const v2Routes: RouteObject[] = [
|
|||||||
path: "",
|
path: "",
|
||||||
lazy: () => import("../../v2-routes/promotions/promotion-list"),
|
lazy: () => import("../../v2-routes/promotions/promotion-list"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/promotions/promotion-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminPromotionRes) => data.promotion?.code,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/promotions/promotion-edit-details"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "add-to-campaign",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/promotions/promotion-add-campaign"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":ruleType/edit",
|
||||||
|
lazy: () => import("../../v2-routes/promotions/edit-rules"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,15 +4,21 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CreateApiKeyDTO,
|
CreateApiKeyDTO,
|
||||||
|
CreateCampaignDTO,
|
||||||
CreateCustomerDTO,
|
CreateCustomerDTO,
|
||||||
CreateInviteDTO,
|
CreateInviteDTO,
|
||||||
CreateProductCollectionDTO,
|
CreateProductCollectionDTO,
|
||||||
|
CreatePromotionDTO,
|
||||||
|
CreatePromotionRuleDTO,
|
||||||
CreateRegionDTO,
|
CreateRegionDTO,
|
||||||
CreateSalesChannelDTO,
|
CreateSalesChannelDTO,
|
||||||
CreateStockLocationInput,
|
CreateStockLocationInput,
|
||||||
UpdateApiKeyDTO,
|
UpdateApiKeyDTO,
|
||||||
|
UpdateCampaignDTO,
|
||||||
UpdateCustomerDTO,
|
UpdateCustomerDTO,
|
||||||
UpdateProductCollectionDTO,
|
UpdateProductCollectionDTO,
|
||||||
|
UpdatePromotionDTO,
|
||||||
|
UpdatePromotionRuleDTO,
|
||||||
UpdateRegionDTO,
|
UpdateRegionDTO,
|
||||||
UpdateSalesChannelDTO,
|
UpdateSalesChannelDTO,
|
||||||
UpdateStockLocationInput,
|
UpdateStockLocationInput,
|
||||||
@@ -57,3 +63,14 @@ export type UpdateStockLocationReq = UpdateStockLocationInput
|
|||||||
// Product Collections
|
// Product Collections
|
||||||
export type CreateProductCollectionReq = CreateProductCollectionDTO
|
export type CreateProductCollectionReq = CreateProductCollectionDTO
|
||||||
export type UpdateProductCollectionReq = UpdateProductCollectionDTO
|
export type UpdateProductCollectionReq = UpdateProductCollectionDTO
|
||||||
|
|
||||||
|
// Promotion
|
||||||
|
export type CreatePromotionReq = CreatePromotionDTO
|
||||||
|
export type UpdatePromotionReq = UpdatePromotionDTO
|
||||||
|
export type BatchAddPromotionRulesReq = { rules: CreatePromotionRuleDTO[] }
|
||||||
|
export type BatchRemovePromotionRulesReq = { rule_ids: string[] }
|
||||||
|
export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] }
|
||||||
|
|
||||||
|
// Campaign
|
||||||
|
export type CreateCampaignReq = CreateCampaignDTO
|
||||||
|
export type UpdateCampaignReq = UpdateCampaignDTO
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ApiKeyDTO,
|
ApiKeyDTO,
|
||||||
|
CampaignDTO,
|
||||||
CurrencyDTO,
|
CurrencyDTO,
|
||||||
CustomerDTO,
|
CustomerDTO,
|
||||||
InviteDTO,
|
InviteDTO,
|
||||||
@@ -45,6 +46,10 @@ export type CustomerListRes = { customers: CustomerDTO[] } & ListRes
|
|||||||
// Promotions
|
// Promotions
|
||||||
export type PromotionRes = { promotion: PromotionDTO }
|
export type PromotionRes = { promotion: PromotionDTO }
|
||||||
export type PromotionListRes = { promotions: PromotionDTO[] } & ListRes
|
export type PromotionListRes = { promotions: PromotionDTO[] } & ListRes
|
||||||
|
export type PromotionRuleAttributesListRes = { attributes: Record<any, any>[] }
|
||||||
|
export type PromotionRuleOperatorsListRes = { operators: Record<any, any>[] }
|
||||||
|
export type PromotionRuleValuesListRes = { values: Record<any, any>[] }
|
||||||
|
export type PromotionRulesListRes = { rules: Record<any, any>[] }
|
||||||
export type PromotionDeleteRes = DeleteRes
|
export type PromotionDeleteRes = DeleteRes
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
@@ -65,6 +70,11 @@ export type RegionRes = { region: RegionDTO }
|
|||||||
export type RegionListRes = { regions: RegionDTO[] } & ListRes
|
export type RegionListRes = { regions: RegionDTO[] } & ListRes
|
||||||
export type RegionDeleteRes = DeleteRes
|
export type RegionDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Campaigns
|
||||||
|
export type CampaignRes = { campaign: CampaignDTO }
|
||||||
|
export type CampaignListRes = { campaigns: CampaignDTO[] } & ListRes
|
||||||
|
export type CampaignDeleteRes = DeleteRes
|
||||||
|
|
||||||
// API Keys
|
// API Keys
|
||||||
export type ExtendedApiKeyDTO = ApiKeyDTO & {
|
export type ExtendedApiKeyDTO = ApiKeyDTO & {
|
||||||
sales_channels: SalesChannelDTO[] | null
|
sales_channels: SalesChannelDTO[] | null
|
||||||
|
|||||||
+442
@@ -0,0 +1,442 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
|
import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
|
||||||
|
import { Badge, Button, Heading, Input, Select, Text } from "@medusajs/ui"
|
||||||
|
import { Fragment, useState } from "react"
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { Combobox } from "../../../../../components/common/combobox"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import {
|
||||||
|
usePromotionAddRules,
|
||||||
|
usePromotionRemoveRules,
|
||||||
|
usePromotionRuleValues,
|
||||||
|
usePromotionUpdateRules,
|
||||||
|
useUpdatePromotion,
|
||||||
|
} from "../../../../../hooks/api/promotions"
|
||||||
|
import { RuleTypeValues } from "../../edit-rules"
|
||||||
|
import { getDisguisedRules } from "./utils"
|
||||||
|
|
||||||
|
type EditPromotionFormProps = {
|
||||||
|
promotion: PromotionDTO
|
||||||
|
rules: PromotionRuleDTO[]
|
||||||
|
ruleType: RuleTypeValues
|
||||||
|
attributes: any[]
|
||||||
|
operators: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditRules = zod.object({
|
||||||
|
rules: zod.array(
|
||||||
|
zod.object({
|
||||||
|
id: zod.string().optional(),
|
||||||
|
attribute: zod.string().min(1, { message: "Required field" }),
|
||||||
|
operator: zod.string().min(1, { message: "Required field" }),
|
||||||
|
values: zod.union([
|
||||||
|
zod.number().min(1, { message: "Required field" }),
|
||||||
|
zod.string().min(1, { message: "Required field" }),
|
||||||
|
zod.array(zod.string()).min(1, { message: "Required field" }),
|
||||||
|
]),
|
||||||
|
required: zod.boolean().optional(),
|
||||||
|
field_type: zod.string().optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchOptionsForRule = (ruleType: string, attribute: string) => {
|
||||||
|
const { values } = usePromotionRuleValues(ruleType, attribute)
|
||||||
|
|
||||||
|
return values || []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditRulesForm = ({
|
||||||
|
promotion,
|
||||||
|
rules,
|
||||||
|
ruleType,
|
||||||
|
attributes,
|
||||||
|
operators,
|
||||||
|
}: EditPromotionFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
const requiredAttributes = attributes?.filter((ra) => ra.required) || []
|
||||||
|
const requiredAttributeValues = requiredAttributes?.map((ra) => ra.value)
|
||||||
|
const disguisedRules =
|
||||||
|
getDisguisedRules(promotion, requiredAttributes, ruleType) || []
|
||||||
|
const [rulesToRemove, setRulesToRemove] = useState([])
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditRules>>({
|
||||||
|
defaultValues: {
|
||||||
|
rules: [...disguisedRules, ...rules].map((rule) => ({
|
||||||
|
id: rule.id,
|
||||||
|
required: requiredAttributeValues.includes(rule.attribute),
|
||||||
|
field_type: rule.field_type,
|
||||||
|
attribute: rule.attribute!,
|
||||||
|
operator: rule.operator!,
|
||||||
|
values: rule?.values?.map((v: { value: string }) => v.value!),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditRules),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fields, append, remove, update } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "rules",
|
||||||
|
keyName: "rules_id",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync: updatePromotion } = useUpdatePromotion(promotion.id)
|
||||||
|
const { mutateAsync: addPromotionRules } = usePromotionAddRules(
|
||||||
|
promotion.id,
|
||||||
|
ruleType
|
||||||
|
)
|
||||||
|
|
||||||
|
const { mutateAsync: removePromotionRules } = usePromotionRemoveRules(
|
||||||
|
promotion.id,
|
||||||
|
ruleType
|
||||||
|
)
|
||||||
|
|
||||||
|
const { mutateAsync: updatePromotionRules } = usePromotionUpdateRules(
|
||||||
|
promotion.id,
|
||||||
|
ruleType
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const applicationMethodData: Record<any, any> = {}
|
||||||
|
const { rules: allRules = [] } = data
|
||||||
|
|
||||||
|
const disguisedRulesData = allRules.filter((rule) =>
|
||||||
|
disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!)
|
||||||
|
)
|
||||||
|
|
||||||
|
// For all the rules that were disguised, convert them to actual values in the
|
||||||
|
// database, they are currently all under application_method. If more of these are coming
|
||||||
|
// up, abstract this away.
|
||||||
|
for (const rule of disguisedRulesData) {
|
||||||
|
applicationMethodData[rule.id!] = parseInt(rule.values as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This variable will contain the rules that are actual rule objects, without the disguised
|
||||||
|
// objects
|
||||||
|
const rulesData = allRules.filter(
|
||||||
|
(rule) =>
|
||||||
|
!disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!)
|
||||||
|
)
|
||||||
|
|
||||||
|
const rulesToCreate = rulesData.filter((rule) => !("id" in rule))
|
||||||
|
const rulesToUpdate = rulesData.filter(
|
||||||
|
(rule) => typeof rule.id === "string"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Object.keys(applicationMethodData).length) {
|
||||||
|
await updatePromotion({ application_method: applicationMethodData })
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesToCreate.length &&
|
||||||
|
(await addPromotionRules({
|
||||||
|
rules: rulesToCreate.map((rule) => {
|
||||||
|
return {
|
||||||
|
attribute: rule.attribute,
|
||||||
|
operator: rule.operator,
|
||||||
|
values: rule.values,
|
||||||
|
} as any
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
rulesToRemove.length &&
|
||||||
|
(await removePromotionRules({
|
||||||
|
rule_ids: rulesToRemove.map((r) => r.id!),
|
||||||
|
}))
|
||||||
|
|
||||||
|
rulesToUpdate.length &&
|
||||||
|
(await updatePromotionRules({
|
||||||
|
rules: rulesToUpdate.map((rule) => {
|
||||||
|
return {
|
||||||
|
id: rule.id!,
|
||||||
|
attribute: rule.attribute,
|
||||||
|
operator: rule.operator,
|
||||||
|
values: rule.values,
|
||||||
|
} as any
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
handleSuccess()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Heading level="h2" className="mb-2">
|
||||||
|
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text className="text-ui-fg-subtle txt-small mb-10">
|
||||||
|
{t(`promotions.fields.conditions.${ruleType}.description`)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{fields.map((fieldRule, index) => {
|
||||||
|
const identifier = fieldRule.id
|
||||||
|
const { ref: attributeRef, ...attributeFields } = form.register(
|
||||||
|
`rules.${index}.attribute`
|
||||||
|
)
|
||||||
|
const { ref: operatorRef, ...operatorFields } = form.register(
|
||||||
|
`rules.${index}.operator`
|
||||||
|
)
|
||||||
|
const { ref: valuesRef, ...valuesFields } = form.register(
|
||||||
|
`rules.${index}.values`
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<div className="flex flex-row gap-2 bg-ui-bg-subtle py-2 px-2 rounded-xl border border-ui-border-base">
|
||||||
|
<div className="grow">
|
||||||
|
<Form.Field
|
||||||
|
key={`${identifier}.${attributeFields.name}`}
|
||||||
|
{...attributeFields}
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
const existingAttributes =
|
||||||
|
fields?.map((field) => field.attribute) || []
|
||||||
|
const attributeOptions =
|
||||||
|
attributes?.filter((attr) => {
|
||||||
|
if (attr.value === fieldRule.attribute) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !existingAttributes.includes(attr.value)
|
||||||
|
}) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item className="mb-2">
|
||||||
|
{fieldRule.required && (
|
||||||
|
<p className="text text-ui-fg-muted txt-small">
|
||||||
|
Required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Control>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
update(index, { ...fieldRule, values: [] })
|
||||||
|
onChange(e)
|
||||||
|
}}
|
||||||
|
disabled={fieldRule.required}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
ref={attributeRef}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
>
|
||||||
|
<Select.Value placeholder="Select Attribute" />
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{attributeOptions?.map((c, i) => (
|
||||||
|
<Select.Item
|
||||||
|
key={`${identifier}-attribute-option-${i}`}
|
||||||
|
value={c.value}
|
||||||
|
>
|
||||||
|
<span className="text-ui-fg-subtle">
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Form.Field
|
||||||
|
key={`${identifier}.${operatorFields.name}`}
|
||||||
|
{...operatorFields}
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={onChange}
|
||||||
|
disabled={fieldRule.required}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
ref={operatorRef}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
>
|
||||||
|
<Select.Value placeholder="Select Operator" />
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{operators?.map((c, i) => (
|
||||||
|
<Select.Item
|
||||||
|
key={`${identifier}-operator-option-${i}`}
|
||||||
|
value={c.value}
|
||||||
|
>
|
||||||
|
<span className="text-ui-fg-subtle">
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
key={`${identifier}.${valuesFields.name}-${fieldRule.attribute}`}
|
||||||
|
{...valuesFields}
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
if (fieldRule.field_type === "number") {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={onChange}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
ref={valuesRef}
|
||||||
|
min={1}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
} else if (fieldRule.field_type === "text") {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
onChange={onChange}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const attribute = attributes?.find(
|
||||||
|
(attr) => attr.value === fieldRule.attribute
|
||||||
|
)
|
||||||
|
const options = attribute
|
||||||
|
? fetchOptionsForRule(ruleType, attribute.id)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-1/2">
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
placeholder="Select Values"
|
||||||
|
options={options}
|
||||||
|
onChange={onChange}
|
||||||
|
className="bg-ui-bg-base"
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-none self-center px-1">
|
||||||
|
<XMarkMini
|
||||||
|
className={`text-ui-fg-muted cursor-pointer ${
|
||||||
|
fieldRule.required ? "invisible" : "visible"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!fieldRule.required) {
|
||||||
|
if (fieldRule.id) {
|
||||||
|
setRulesToRemove([...rulesToRemove, fieldRule])
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(index)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{index < fields.length - 1 && (
|
||||||
|
<div className="relative px-6 py-3">
|
||||||
|
<div className="absolute top-0 bottom-0 left-[40px] z-[-1] border-ui-border-strong w-px bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-repeat-y"></div>
|
||||||
|
|
||||||
|
<Badge size="2xsmall" className=" text-xs">
|
||||||
|
AND
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
className="inline-block"
|
||||||
|
onClick={() => {
|
||||||
|
append({
|
||||||
|
attribute: "",
|
||||||
|
operator: "",
|
||||||
|
values: [],
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add condition
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="transparent"
|
||||||
|
className="inline-block text-ui-fg-muted hover:text-ui-fg-subtle ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
const indicesToRemove = fields
|
||||||
|
.map((field, index) => (field.required ? null : index))
|
||||||
|
.filter((f) => f !== null)
|
||||||
|
|
||||||
|
setRulesToRemove(fields.filter((f) => !f.required))
|
||||||
|
remove(indicesToRemove)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
|
||||||
|
<Button size="small" type="submit">
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-rules-form"
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
|
import { RuleType } from "../../edit-rules"
|
||||||
|
|
||||||
|
// We are disguising couple of database columns as rules here, namely
|
||||||
|
// apply_to_quantity and buy_rules_min_quantity.
|
||||||
|
// We need to transform the database value into a disugised "rule" shape
|
||||||
|
// for the form
|
||||||
|
export function getDisguisedRules(
|
||||||
|
promotion: PromotionDTO,
|
||||||
|
requiredAttributes: any[],
|
||||||
|
ruleType: string
|
||||||
|
) {
|
||||||
|
if (ruleType === RuleType.RULES && !requiredAttributes?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyToQuantityRule = requiredAttributes.find(
|
||||||
|
(attr) => attr.id === "apply_to_quantity"
|
||||||
|
)
|
||||||
|
|
||||||
|
const buyRulesMinQuantityRule = requiredAttributes.find(
|
||||||
|
(attr) => attr.id === "buy_rules_min_quantity"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ruleType === RuleType.TARGET_RULES) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "apply_to_quantity",
|
||||||
|
attribute: "apply_to_quantity",
|
||||||
|
operator: "eq",
|
||||||
|
required: applyToQuantityRule?.required,
|
||||||
|
field_type: applyToQuantityRule?.field_type,
|
||||||
|
values: [{ value: promotion?.application_method?.apply_to_quantity }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ruleType === RuleType.BUY_RULES) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "buy_rules_min_quantity",
|
||||||
|
attribute: "buy_rules_min_quantity",
|
||||||
|
operator: "eq",
|
||||||
|
required: buyRulesMinQuantityRule?.required,
|
||||||
|
field_type: buyRulesMinQuantityRule?.field_type,
|
||||||
|
values: [
|
||||||
|
{ value: promotion?.application_method?.buy_rules_min_quantity },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { PromotionRuleDTO } from "@medusajs/types"
|
||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import {
|
||||||
|
usePromotion,
|
||||||
|
usePromotionRuleAttributes,
|
||||||
|
usePromotionRuleOperators,
|
||||||
|
} from "../../../hooks/api/promotions"
|
||||||
|
import { EditRulesForm } from "./components/edit-rules-form"
|
||||||
|
|
||||||
|
export enum RuleType {
|
||||||
|
RULES = "rules",
|
||||||
|
BUY_RULES = "buy-rules",
|
||||||
|
TARGET_RULES = "target-rules",
|
||||||
|
}
|
||||||
|
export type RuleTypeValues = "rules" | "buy-rules" | "target-rules"
|
||||||
|
|
||||||
|
export const EditRules = () => {
|
||||||
|
const params = useParams()
|
||||||
|
const allowedParams: string[] = [
|
||||||
|
RuleType.RULES,
|
||||||
|
RuleType.BUY_RULES,
|
||||||
|
RuleType.TARGET_RULES,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!allowedParams.includes(params.ruleType!)) {
|
||||||
|
throw "invalid page"
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const ruleType = params.ruleType as RuleTypeValues
|
||||||
|
const id = params.id as string
|
||||||
|
const rules: PromotionRuleDTO[] = []
|
||||||
|
const { promotion, isLoading, isError, error } = usePromotion(id)
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
isError: isAttributesError,
|
||||||
|
error: attributesError,
|
||||||
|
} = usePromotionRuleAttributes(ruleType!)
|
||||||
|
const {
|
||||||
|
operators,
|
||||||
|
isError: isOperatorsError,
|
||||||
|
error: operatorsError,
|
||||||
|
} = usePromotionRuleOperators()
|
||||||
|
|
||||||
|
if (promotion) {
|
||||||
|
if (ruleType === RuleType.RULES) {
|
||||||
|
rules.push(...(promotion.rules || []))
|
||||||
|
} else if (ruleType === RuleType.TARGET_RULES) {
|
||||||
|
rules.push(...(promotion?.application_method?.target_rules || []))
|
||||||
|
} else if (ruleType === RuleType.BUY_RULES) {
|
||||||
|
rules.push(...(promotion.application_method?.buy_rules || []))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || isAttributesError || isOperatorsError) {
|
||||||
|
throw error || attributesError || operatorsError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t(`promotions.edit.${ruleType}.title`)}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
|
||||||
|
{!isLoading && promotion && attributes && operators && (
|
||||||
|
<EditRulesForm
|
||||||
|
promotion={promotion}
|
||||||
|
rules={rules}
|
||||||
|
ruleType={ruleType}
|
||||||
|
attributes={attributes}
|
||||||
|
operators={operators}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { EditRules as Component } from "./edit-rules"
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { CampaignDTO, PromotionDTO } from "@medusajs/types"
|
||||||
|
import { Button, RadioGroup, Select } from "@medusajs/ui"
|
||||||
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { CampaignDetails } from "./campaign-details"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||||
|
|
||||||
|
type EditPromotionFormProps = {
|
||||||
|
promotion: PromotionDTO
|
||||||
|
campaigns: CampaignDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditPromotionSchema = zod.object({
|
||||||
|
campaign_id: zod.string().optional(),
|
||||||
|
existing: zod.string().toLowerCase(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AddCampaignPromotionForm = ({
|
||||||
|
promotion,
|
||||||
|
campaigns,
|
||||||
|
}: EditPromotionFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
const { campaign } = promotion
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditPromotionSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
campaign_id: campaign?.id,
|
||||||
|
existing: "true",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditPromotionSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchCampaignId = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "campaign_id",
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId)
|
||||||
|
const { mutateAsync, isPending } = useUpdatePromotion(promotion.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{ campaign_id: data.campaign_id },
|
||||||
|
{ onSuccess: () => handleSuccess() }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex h-full flex-col gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="existing"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>Method</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex-col gap-y-3"
|
||||||
|
{...field}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"true"}
|
||||||
|
label={t("promotions.form.campaign.existing.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.campaign.existing.description"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"false"}
|
||||||
|
label={t("promotions.form.campaign.new.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.campaign.new.description"
|
||||||
|
)}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="campaign_id"
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("promotions.form.campaign.existing.title")}
|
||||||
|
</Form.Label>
|
||||||
|
|
||||||
|
<Form.Control>
|
||||||
|
<Select onValueChange={onChange} {...field}>
|
||||||
|
<Select.Trigger ref={ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{campaigns.map((c) => (
|
||||||
|
<Select.Item key={c.id} value={c.id}>
|
||||||
|
{c.name?.toUpperCase()}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CampaignDetails campaign={selectedCampaign} />
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
|
||||||
|
<Button size="small" type="submit" isLoading={isPending}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
import { CampaignDTO } from "@medusajs/types"
|
||||||
|
import { Heading, Text } from "@medusajs/ui"
|
||||||
|
import { Fragment } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type CampaignDetailsProps = {
|
||||||
|
campaign?: CampaignDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CampaignDetails = ({ campaign }: CampaignDetailsProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!campaign) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div>
|
||||||
|
<Heading level="h2" className="mb-4">
|
||||||
|
{t("campaigns.details")}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus font-">
|
||||||
|
{t("campaigns.fields.identifier")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">
|
||||||
|
{campaign.campaign_identifier || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus">{t("fields.description")}</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">{campaign.description || "-"}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus">
|
||||||
|
{t("campaigns.fields.start_date")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">
|
||||||
|
{campaign.starts_at?.toString() || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus">
|
||||||
|
{t("campaigns.fields.end_date")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">
|
||||||
|
{campaign.ends_at?.toString() || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Heading level="h2" className="mb-4">
|
||||||
|
{t("campaigns.budget.details")}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus font-">
|
||||||
|
{t("campaigns.budget.fields.type")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">{campaign.budget?.type || "-"}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus">
|
||||||
|
{t("campaigns.budget.fields.currency")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">{campaign.currency || "-"}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus">
|
||||||
|
{t("campaigns.budget.fields.limit")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">{campaign.budget?.limit || "-"}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||||
|
<Text className="txt-small-plus">
|
||||||
|
{t("campaigns.budget.fields.used")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text className="txt-small">{campaign.budget?.used || "-"}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./add-campaign-promotion-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PromotionAddCampaign as Component } from "./promotion-add-campaign"
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { useCampaigns } from "../../../hooks/api/campaigns"
|
||||||
|
import { usePromotion } from "../../../hooks/api/promotions"
|
||||||
|
import { AddCampaignPromotionForm } from "./components/add-campaign-promotion-form"
|
||||||
|
|
||||||
|
export const PromotionAddCampaign = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { promotion, isPending, isError, error } = usePromotion(id!)
|
||||||
|
const {
|
||||||
|
campaigns,
|
||||||
|
isPending: areCampaignsLoading,
|
||||||
|
isError: isCampaignError,
|
||||||
|
error: campaignError,
|
||||||
|
} = useCampaigns()
|
||||||
|
|
||||||
|
if (isError || isCampaignError) {
|
||||||
|
throw error || campaignError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("promotions.addToCampaign.title")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
|
||||||
|
{!isPending && !areCampaignsLoading && promotion && campaigns && (
|
||||||
|
<AddCampaignPromotionForm promotion={promotion} campaigns={campaigns} />
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { CampaignDTO } from "@medusajs/types"
|
||||||
|
import { Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { format } from "date-fns"
|
||||||
|
import { Fragment } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
|
||||||
|
function formatDate(date?: string | Date) {
|
||||||
|
if (!date) {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
return format(new Date(date), "dd MMM yyyy")
|
||||||
|
}
|
||||||
|
|
||||||
|
const CampaignDetailSection = ({ campaign }: { campaign: CampaignDTO }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("campaigns.fields.name")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
|
{campaign?.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</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("campaigns.fields.identifier")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
|
{campaign?.campaign_identifier}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</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("campaigns.fields.start_date")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
|
{formatDate(campaign?.starts_at)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</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("campaigns.fields.end_date") || "-"}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
|
{formatDate(campaign?.ends_at)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("promotions.fields.campaign")}</Heading>
|
||||||
|
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: "add-to-campaign",
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{campaign ? (
|
||||||
|
<CampaignDetailSection campaign={campaign} />
|
||||||
|
) : (
|
||||||
|
<NoRecords
|
||||||
|
className="h-[180px] p-6 text-center"
|
||||||
|
title="Not part of a campaign"
|
||||||
|
message="Add this promotion to an existing campaign"
|
||||||
|
action={{
|
||||||
|
to: `/promotions/${id}/add-to-campaign`,
|
||||||
|
label: "Add to Campaign",
|
||||||
|
}}
|
||||||
|
buttonVariant="transparentIconLeft"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./campaign-section.tsx"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./promotion-conditions-section"
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { AdminGetPromotionRulesRes, PromotionRuleTypes } from "@medusajs/types"
|
||||||
|
import { Badge, Container, Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { BadgeListSummary } from "../../../../../components/common/badge-list-summary"
|
||||||
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
|
||||||
|
type RuleProps = {
|
||||||
|
rule: AdminGetPromotionRulesRes
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleBlock({ rule }: RuleProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-subtle shadow-borders-base flex justify-around rounded-md p-2 align-center">
|
||||||
|
<div className="flex text-ui-fg-subtle txt-compact-xsmall whitespace-nowrap items-center">
|
||||||
|
<Badge
|
||||||
|
size="2xsmall"
|
||||||
|
key="rule-attribute"
|
||||||
|
className="mx-1 txt-compact-xsmall-plus tag-neutral-text truncate inline-block"
|
||||||
|
>
|
||||||
|
{rule.attribute_label}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<span className="mx-1 txt-compact-2xsmall inline-block">
|
||||||
|
{rule.operator_label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<BadgeListSummary
|
||||||
|
inline
|
||||||
|
className="!txt-compact-small-plus"
|
||||||
|
list={rule.values.map((v) => v.label)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PromotionConditionsSectionProps = {
|
||||||
|
rules: AdminGetPromotionRulesRes
|
||||||
|
ruleType: PromotionRuleTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromotionConditionsSection = ({
|
||||||
|
rules,
|
||||||
|
ruleType,
|
||||||
|
}: PromotionConditionsSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Heading>
|
||||||
|
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `${ruleType}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle flex flex-col gap-2 px-6 pb-4 pt-2">
|
||||||
|
{!rules.length && (
|
||||||
|
<NoRecords
|
||||||
|
className="h-[180px]"
|
||||||
|
title="No records yet."
|
||||||
|
message="Please check back later or add a target condition today"
|
||||||
|
action={{ to: "/promotions", label: "Add condition" }}
|
||||||
|
buttonVariant="transparentIconLeft"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{rules.map((rule) => (
|
||||||
|
<RuleBlock key={`${rule.id}-${rule.attribute}`} rule={rule} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./promotion-general-section.tsx"
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Copy,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Text,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useDeletePromotion } from "../../../../../hooks/api/promotions"
|
||||||
|
import {
|
||||||
|
getPromotionStatus,
|
||||||
|
PromotionStatus,
|
||||||
|
} from "../../../../../lib/promotions"
|
||||||
|
|
||||||
|
type PromotionGeneralSectionProps = {
|
||||||
|
promotion: PromotionDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromotionGeneralSection = ({
|
||||||
|
promotion,
|
||||||
|
}: PromotionGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { mutateAsync } = useDeletePromotion(promotion.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const confirm = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("promotions.deleteWarning", {
|
||||||
|
code: promotion.code,
|
||||||
|
}),
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
verificationText: promotion.code,
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate("/promotions", { replace: true })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [color, text] = {
|
||||||
|
[PromotionStatus.DISABLED]: ["grey", t("statuses.disabled")],
|
||||||
|
[PromotionStatus.ACTIVE]: ["green", t("statuses.active")],
|
||||||
|
[PromotionStatus.SCHEDULED]: ["orange", t("statuses.scheduled")],
|
||||||
|
[PromotionStatus.EXPIRED]: ["red", t("statuses.expired")],
|
||||||
|
}[getPromotionStatus(promotion)] as [
|
||||||
|
"grey" | "orange" | "green" | "red",
|
||||||
|
string,
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Heading>{promotion.code}</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<StatusBadge color={color}>{text}</StatusBadge>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `/promotions/${promotion.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("promotions.fields.method")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
|
{promotion.is_automatic ? "Promotion code" : "Automatic"}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
weight="plus"
|
||||||
|
leading="compact"
|
||||||
|
className="text-pretty"
|
||||||
|
>
|
||||||
|
{promotion.code}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Copy content={promotion.code!} variant="mini" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("promotions.fields.type")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="small" leading="compact" className="text-pretty capitalize">
|
||||||
|
{promotion.type}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("promotions.fields.value")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
|
{promotion.application_method?.value}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("promotions.fields.allocation")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="small" leading="compact" className="text-pretty capitalize">
|
||||||
|
{promotion.application_method?.allocation!}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { promotionLoader as loader } from "./loader"
|
||||||
|
export { PromotionDetail as Component } from "./promotion-detail.tsx"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { AdminPromotionRes } from "@medusajs/medusa"
|
||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
import { promotionsQueryKeys } from "../../../hooks/api/promotions"
|
||||||
|
import { client } from "../../../lib/client"
|
||||||
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const promotionDetailQuery = (id: string) => ({
|
||||||
|
queryKey: promotionsQueryKeys.detail(id),
|
||||||
|
queryFn: () => client.promotions.retrieve(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const promotionLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = promotionDetailQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminPromotionRes>>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { usePromotion, usePromotionRules } from "../../../hooks/api/promotions"
|
||||||
|
import { CampaignSection } from "./components/campaign-section"
|
||||||
|
import { PromotionConditionsSection } from "./components/promotion-conditions-section"
|
||||||
|
import { PromotionGeneralSection } from "./components/promotion-general-section"
|
||||||
|
import { promotionLoader } from "./loader"
|
||||||
|
|
||||||
|
import after from "medusa-admin:widgets/promotion/details/after"
|
||||||
|
import before from "medusa-admin:widgets/promotion/details/before"
|
||||||
|
|
||||||
|
export const PromotionDetail = () => {
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof promotionLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { promotion, isLoading } = usePromotion(id!, { initialData })
|
||||||
|
const { rules } = usePromotionRules(id!, "rules")
|
||||||
|
const { rules: targetRules } = usePromotionRules(id!, "target-rules")
|
||||||
|
const { rules: buyRules } = usePromotionRules(id!, "buy-rules")
|
||||||
|
|
||||||
|
if (isLoading || !promotion) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
{before.widgets.map((w, i) => {
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<w.Component />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
||||||
|
<div className="flex w-full flex-col gap-y-2">
|
||||||
|
<PromotionGeneralSection promotion={promotion} />
|
||||||
|
|
||||||
|
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
|
||||||
|
|
||||||
|
<PromotionConditionsSection
|
||||||
|
rules={targetRules || []}
|
||||||
|
ruleType={"target-rules"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{promotion.type === "buyget" && (
|
||||||
|
<PromotionConditionsSection
|
||||||
|
rules={buyRules || []}
|
||||||
|
ruleType={"buy-rules"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex w-full flex-col gap-y-2 xl:hidden">
|
||||||
|
<CampaignSection campaign={promotion.campaign!} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{after.widgets.map((w, i) => {
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<w.Component />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<JsonViewSection data={promotion as any} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
|
||||||
|
<CampaignSection campaign={promotion.campaign!} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
|
import { Button, CurrencyInput, Input, RadioGroup, Text } from "@medusajs/ui"
|
||||||
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||||
|
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||||
|
|
||||||
|
type EditPromotionFormProps = {
|
||||||
|
promotion: PromotionDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditPromotionSchema = zod.object({
|
||||||
|
is_automatic: zod.string().toLowerCase(),
|
||||||
|
code: zod.string().min(1),
|
||||||
|
value_type: zod.enum(["fixed", "percentage"]),
|
||||||
|
value: zod.string(),
|
||||||
|
allocation: zod.enum(["each", "across"]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditPromotionDetailsForm = ({
|
||||||
|
promotion,
|
||||||
|
}: EditPromotionFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditPromotionSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
is_automatic: promotion.is_automatic!.toString(),
|
||||||
|
code: promotion.code,
|
||||||
|
value: promotion.application_method!.value?.toString(),
|
||||||
|
allocation: promotion.application_method!.allocation,
|
||||||
|
value_type: promotion.application_method!.type,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditPromotionSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchValueType = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "value_type",
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFixedValueType = watchValueType === "fixed"
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useUpdatePromotion(promotion.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
console.log("data ------ ", data)
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
is_automatic: data.is_automatic === "true",
|
||||||
|
code: data.code,
|
||||||
|
application_method: {
|
||||||
|
value: data.value,
|
||||||
|
type: data.value_type as any,
|
||||||
|
allocation: data.allocation as any,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex h-full flex-col gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="is_automatic"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>Method</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex-col gap-y-3"
|
||||||
|
{...field}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"false"}
|
||||||
|
label={t("promotions.form.method.code.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.method.code.description"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"true"}
|
||||||
|
label={t("promotions.form.method.automatic.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.method.automatic.description"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("promotions.form.code.title")}</Form.Label>
|
||||||
|
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
<Trans
|
||||||
|
t={t}
|
||||||
|
i18nKey="promotions.form.code.description"
|
||||||
|
components={[<br key="break" />]}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="value_type"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("promotions.fields.value_type")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex-col gap-y-3"
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"fixed"}
|
||||||
|
label={t("promotions.form.value_type.fixed.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.value_type.fixed.description"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"percentage"}
|
||||||
|
label={t(
|
||||||
|
"promotions.form.value_type.percentage.title"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.value_type.percentage.description"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="value"
|
||||||
|
render={({ field: { onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{isFixedValueType
|
||||||
|
? t("fields.amount")
|
||||||
|
: t("fields.percentage")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
{isFixedValueType ? (
|
||||||
|
<CurrencyInput
|
||||||
|
min={0}
|
||||||
|
onValueChange={onChange}
|
||||||
|
code={"USD"}
|
||||||
|
symbol={getCurrencySymbol("USD")}
|
||||||
|
{...field}
|
||||||
|
value={Number(field.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PercentageInput
|
||||||
|
key="amount"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(
|
||||||
|
e.target.value === "" ? null : e.target.value
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="allocation"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("promotions.fields.allocation")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex-col gap-y-3"
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"each"}
|
||||||
|
label={t("promotions.form.allocation.each.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.allocation.each.description"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={"across"}
|
||||||
|
label={t("promotions.form.allocation.across.title")}
|
||||||
|
description={t(
|
||||||
|
"promotions.form.allocation.across.description"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
|
||||||
|
<Button size="small" type="submit" isLoading={isPending}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-promotion-details-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PromotionEditDetails as Component } from "./promotion-edit-details"
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { usePromotion } from "../../../hooks/api/promotions"
|
||||||
|
import { EditPromotionDetailsForm } from "./components/edit-promotion-form"
|
||||||
|
|
||||||
|
export const PromotionEditDetails = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { promotion, isLoading, isError, error } = usePromotion(id!)
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("promotions.edit.title")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
|
||||||
|
{!isLoading && promotion && (
|
||||||
|
<EditPromotionDetailsForm promotion={promotion} />
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+42
-4
@@ -1,15 +1,18 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { PromotionDTO } from "@medusajs/types"
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
import { Button, Container, Heading } from "@medusajs/ui"
|
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, Outlet, useLoaderData } from "react-router-dom"
|
import { Link, Outlet, useLoaderData, useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
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 { usePromotions } from "../../../../../hooks/api/promotions"
|
import {
|
||||||
|
useDeletePromotion,
|
||||||
|
usePromotions,
|
||||||
|
} from "../../../../../hooks/api/promotions"
|
||||||
import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/use-promotion-table-columns"
|
import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/use-promotion-table-columns"
|
||||||
import { usePromotionTableFilters } from "../../../../../hooks/table/filters-v2/use-promotion-table-filters"
|
import { usePromotionTableFilters } from "../../../../../hooks/table/filters-v2/use-promotion-table-filters"
|
||||||
import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query"
|
import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query"
|
||||||
@@ -79,6 +82,36 @@ export const PromotionListTable = () => {
|
|||||||
|
|
||||||
const PromotionActions = ({ promotion }: { promotion: PromotionDTO }) => {
|
const PromotionActions = ({ promotion }: { promotion: PromotionDTO }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { mutateAsync } = useDeletePromotion(promotion.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("promotions.deleteWarning", { code: promotion.code! }),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
verificationText: promotion.code,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate("/promotions", { replace: true })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
`Promotion with code ${promotion.code} could not be deleted`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
@@ -90,6 +123,11 @@ const PromotionActions = ({ promotion }: { promotion: PromotionDTO }) => {
|
|||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
to: `/promotions/${promotion.id}/edit`,
|
to: `/promotions/${promotion.id}/edit`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { Response } from "@medusajs/medusa-js"
|
import { Response } from "@medusajs/medusa-js"
|
||||||
import { QueryClient } from "@tanstack/react-query"
|
import { QueryClient } from "@tanstack/react-query"
|
||||||
import { adminPromotionKeys } from "../../../lib/api-v2"
|
import { promotionsQueryKeys } from "../../../hooks/api/promotions"
|
||||||
import { medusa, queryClient } from "../../../lib/medusa"
|
import { medusa, queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
@@ -13,7 +13,7 @@ const params = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const promotionsListQuery = () => ({
|
const promotionsListQuery = () => ({
|
||||||
queryKey: adminPromotionKeys.list(params),
|
queryKey: promotionsQueryKeys.list(params),
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
medusa.admin.custom.get<AdminGetPromotionsParams, AdminPromotionsListRes>(
|
medusa.admin.custom.get<AdminGetPromotionsParams, AdminPromotionsListRes>(
|
||||||
"/promotions",
|
"/promotions",
|
||||||
|
|||||||
@@ -5,25 +5,25 @@ import { UseQueryOptionsWrapper } from "../../../types"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook sends a `GET` request to a custom API Route.
|
* This hook sends a `GET` request to a custom API Route.
|
||||||
*
|
*
|
||||||
* @typeParam TQuery - The type of accepted query parameters which defaults to `Record<string, any>`.
|
* @typeParam TQuery - The type of accepted query parameters which defaults to `Record<string, any>`.
|
||||||
* @typeParam TResponse - The type of response which defaults to `any`.
|
* @typeParam TResponse - The type of response which defaults to `any`.
|
||||||
* @typeParamDefinition TQuery - The query parameters based on the type specified for `TQuery`.
|
* @typeParamDefinition TQuery - The query parameters based on the type specified for `TQuery`.
|
||||||
* @typeParamDefinition TResponse - The response based on the type specified for `TResponse`.
|
* @typeParamDefinition TResponse - The response based on the type specified for `TResponse`.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* import React from "react"
|
* import React from "react"
|
||||||
* import { useAdminCustomQuery } from "medusa-react"
|
* import { useAdminCustomQuery } from "medusa-react"
|
||||||
* import Post from "./models/Post"
|
* import Post from "./models/Post"
|
||||||
*
|
*
|
||||||
* type RequestQuery = {
|
* type RequestQuery = {
|
||||||
* title: string
|
* title: string
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* type ResponseData = {
|
* type ResponseData = {
|
||||||
* posts: Post
|
* posts: Post
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* const Custom = () => {
|
* const Custom = () => {
|
||||||
* const { data, isLoading } = useAdminCustomQuery
|
* const { data, isLoading } = useAdminCustomQuery
|
||||||
* <RequestQuery, ResponseData>(
|
* <RequestQuery, ResponseData>(
|
||||||
@@ -33,7 +33,7 @@ import { UseQueryOptionsWrapper } from "../../../types"
|
|||||||
* title: "My post"
|
* title: "My post"
|
||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
*
|
*
|
||||||
* return (
|
* return (
|
||||||
* <div>
|
* <div>
|
||||||
* {isLoading && <span>Loading...</span>}
|
* {isLoading && <span>Loading...</span>}
|
||||||
@@ -50,9 +50,9 @@ import { UseQueryOptionsWrapper } from "../../../types"
|
|||||||
* </div>
|
* </div>
|
||||||
* )
|
* )
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* export default Custom
|
* export default Custom
|
||||||
*
|
*
|
||||||
* @customNamespace Hooks.Admin.Custom
|
* @customNamespace Hooks.Admin.Custom
|
||||||
* @category Mutations
|
* @category Mutations
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./validators"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { CampaignDTO, PaginatedResponse } from "@medusajs/types"
|
||||||
|
|
||||||
|
export type AdminCampaignsListRes = PaginatedResponse<{
|
||||||
|
campaigns: CampaignDTO[]
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type AdminCampaignRes = {
|
||||||
|
campaign: CampaignDTO
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from "./campaigns"
|
||||||
export * from "./promotions"
|
export * from "./promotions"
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { AdminGetPromotionRulesRes } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
RuleOperator,
|
||||||
|
RuleType,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../../types/routing"
|
||||||
|
import {
|
||||||
|
operatorsMap,
|
||||||
|
ruleAttributesMap,
|
||||||
|
ruleQueryConfigurations,
|
||||||
|
validateRuleType,
|
||||||
|
} from "../../utils"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: AuthenticatedMedusaRequest,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const { id, rule_type: ruleType } = req.params
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
|
validateRuleType(ruleType)
|
||||||
|
|
||||||
|
const dasherizedRuleType = ruleType.split("-").join("_")
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "promotion",
|
||||||
|
variables: { id },
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [promotion] = await remoteQuery(queryObject)
|
||||||
|
const ruleAttributes = ruleAttributesMap[ruleType]
|
||||||
|
const promotionRules: any[] = []
|
||||||
|
|
||||||
|
if (dasherizedRuleType === RuleType.RULES) {
|
||||||
|
promotionRules.push(...(promotion?.rules || []))
|
||||||
|
} else if (dasherizedRuleType === RuleType.TARGET_RULES) {
|
||||||
|
promotionRules.push(...(promotion.application_method?.target_rules || []))
|
||||||
|
} else if (dasherizedRuleType === RuleType.BUY_RULES) {
|
||||||
|
promotionRules.push(...(promotion.application_method?.buy_rules || []))
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedRules: AdminGetPromotionRulesRes = []
|
||||||
|
const disguisedRules = ruleAttributes.filter((attr) => !!attr.disguised)
|
||||||
|
|
||||||
|
for (const disguisedRule of disguisedRules) {
|
||||||
|
const value = promotion.application_method?.[disguisedRule.id]
|
||||||
|
const values = [{ label: value, value }]
|
||||||
|
|
||||||
|
transformedRules.push({
|
||||||
|
id: undefined,
|
||||||
|
attribute: disguisedRule.id,
|
||||||
|
attribute_label: disguisedRule.label,
|
||||||
|
operator: RuleOperator.EQ,
|
||||||
|
operator_label: operatorsMap[RuleOperator.EQ].label,
|
||||||
|
values,
|
||||||
|
disguised: true,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const promotionRule of promotionRules) {
|
||||||
|
const currentRuleAttribute = ruleAttributes.find(
|
||||||
|
(attr) => attr.value === promotionRule.attribute
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!currentRuleAttribute) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryConfig = ruleQueryConfigurations[currentRuleAttribute.id]
|
||||||
|
const rows = await remoteQuery(
|
||||||
|
remoteQueryObjectFromString({
|
||||||
|
entryPoint: queryConfig.entryPoint,
|
||||||
|
variables: {
|
||||||
|
filters: {
|
||||||
|
[queryConfig.valueAttr]: promotionRule.values.map((v) => v.value),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [queryConfig.labelAttr, queryConfig.valueAttr],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const valueLabelMap = new Map<string, string>(
|
||||||
|
rows.map((row) => [
|
||||||
|
row[queryConfig.valueAttr],
|
||||||
|
row[queryConfig.labelAttr],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
promotionRule.values = promotionRule.values.map((value) => ({
|
||||||
|
value: value.value,
|
||||||
|
label: valueLabelMap.get(value.value) || value.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
transformedRules.push({
|
||||||
|
...promotionRule,
|
||||||
|
attribute_label: currentRuleAttribute.label,
|
||||||
|
operator_label:
|
||||||
|
operatorsMap[promotionRule.operator]?.label || promotionRule.operator,
|
||||||
|
disguised: false,
|
||||||
|
required: currentRuleAttribute.required || false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
rules: transformedRules,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -45,6 +45,16 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/admin/promotions/:id/:rule_type",
|
||||||
|
middlewares: [
|
||||||
|
transformQuery(
|
||||||
|
AdminGetPromotionsPromotionParams,
|
||||||
|
QueryConfig.retrieveNewTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
matcher: "/admin/promotions/:id",
|
matcher: "/admin/promotions/:id",
|
||||||
|
|||||||
@@ -21,25 +21,35 @@ export const defaultAdminPromotionFields = [
|
|||||||
"deleted_at",
|
"deleted_at",
|
||||||
"campaign.id",
|
"campaign.id",
|
||||||
"campaign.name",
|
"campaign.name",
|
||||||
|
"campaign.campaign_identifier",
|
||||||
|
"campaign.starts_at",
|
||||||
|
"campaign.ends_at",
|
||||||
"application_method.value",
|
"application_method.value",
|
||||||
"application_method.type",
|
"application_method.type",
|
||||||
"application_method.max_quantity",
|
"application_method.max_quantity",
|
||||||
|
"application_method.apply_to_quantity",
|
||||||
|
"application_method.buy_rules_min_quantity",
|
||||||
"application_method.target_type",
|
"application_method.target_type",
|
||||||
"application_method.allocation",
|
"application_method.allocation",
|
||||||
"application_method.created_at",
|
"application_method.created_at",
|
||||||
"application_method.updated_at",
|
"application_method.updated_at",
|
||||||
"application_method.deleted_at",
|
"application_method.deleted_at",
|
||||||
|
"application_method.buy_rules.id",
|
||||||
"application_method.buy_rules.attribute",
|
"application_method.buy_rules.attribute",
|
||||||
"application_method.buy_rules.operator",
|
"application_method.buy_rules.operator",
|
||||||
"application_method.buy_rules.values.value",
|
"application_method.buy_rules.values.value",
|
||||||
|
"application_method.target_rules.id",
|
||||||
"application_method.target_rules.attribute",
|
"application_method.target_rules.attribute",
|
||||||
"application_method.target_rules.operator",
|
"application_method.target_rules.operator",
|
||||||
"application_method.target_rules.values.value",
|
"application_method.target_rules.values.value",
|
||||||
|
"rules.id",
|
||||||
"rules.attribute",
|
"rules.attribute",
|
||||||
"rules.operator",
|
"rules.operator",
|
||||||
"rules.values.value",
|
"rules.values.value",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const defaults = [...defaultAdminPromotionFields]
|
||||||
|
|
||||||
export const retrieveTransformQueryConfig = {
|
export const retrieveTransformQueryConfig = {
|
||||||
defaultFields: defaultAdminPromotionFields,
|
defaultFields: defaultAdminPromotionFields,
|
||||||
defaultRelations: defaultAdminPromotionRelations,
|
defaultRelations: defaultAdminPromotionRelations,
|
||||||
@@ -57,3 +67,9 @@ export const listRuleValueTransformQueryConfig = {
|
|||||||
allowed: [],
|
allowed: [],
|
||||||
isList: true,
|
isList: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace this with the old one
|
||||||
|
export const retrieveNewTransformQueryConfig = {
|
||||||
|
defaults,
|
||||||
|
isList: false,
|
||||||
|
}
|
||||||
@@ -1,32 +1,14 @@
|
|||||||
import { RuleOperator } from "@medusajs/utils"
|
|
||||||
import {
|
import {
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../../types/routing"
|
} from "../../../../types/routing"
|
||||||
|
import { operatorsMap } from "../utils"
|
||||||
const operators = [
|
|
||||||
{
|
|
||||||
id: RuleOperator.IN,
|
|
||||||
value: RuleOperator.IN,
|
|
||||||
label: "In",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: RuleOperator.EQ,
|
|
||||||
value: RuleOperator.EQ,
|
|
||||||
label: "Equals",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: RuleOperator.NE,
|
|
||||||
value: RuleOperator.NE,
|
|
||||||
label: "Not In",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
) => {
|
) => {
|
||||||
res.json({
|
res.json({
|
||||||
operators,
|
operators: Object.values(operatorsMap),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+6
-55
@@ -6,67 +6,18 @@ import {
|
|||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../../../../types/routing"
|
} from "../../../../../../types/routing"
|
||||||
import { validateRuleAttribute, validateRuleType } from "../../../utils"
|
import {
|
||||||
|
ruleQueryConfigurations,
|
||||||
const queryConfigurations = {
|
validateRuleAttribute,
|
||||||
region: {
|
validateRuleType,
|
||||||
entryPoint: "region",
|
} from "../../../utils"
|
||||||
labelAttr: "name",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
currency: {
|
|
||||||
entryPoint: "currency",
|
|
||||||
labelAttr: "name",
|
|
||||||
valueAttr: "code",
|
|
||||||
},
|
|
||||||
customer_group: {
|
|
||||||
entryPoint: "customer_group",
|
|
||||||
labelAttr: "name",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
sales_channel: {
|
|
||||||
entryPoint: "sales_channel",
|
|
||||||
labelAttr: "name",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
country: {
|
|
||||||
entryPoint: "country",
|
|
||||||
labelAttr: "display_name",
|
|
||||||
valueAttr: "iso_2",
|
|
||||||
},
|
|
||||||
product: {
|
|
||||||
entryPoint: "product",
|
|
||||||
labelAttr: "title",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
product_category: {
|
|
||||||
entryPoint: "product_category",
|
|
||||||
labelAttr: "name",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
product_collection: {
|
|
||||||
entryPoint: "product_collection",
|
|
||||||
labelAttr: "title",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
product_type: {
|
|
||||||
entryPoint: "product_type",
|
|
||||||
labelAttr: "value",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
product_tag: {
|
|
||||||
entryPoint: "product_tag",
|
|
||||||
labelAttr: "value",
|
|
||||||
valueAttr: "id",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
) => {
|
) => {
|
||||||
const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params
|
const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params
|
||||||
const queryConfig = queryConfigurations[ruleAttributeId]
|
const queryConfig = ruleQueryConfigurations[ruleAttributeId]
|
||||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
validateRuleType(ruleType)
|
validateRuleType(ruleType)
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ import { PaginatedResponse, PromotionDTO } from "@medusajs/types"
|
|||||||
export type AdminPromotionsListRes = PaginatedResponse<{
|
export type AdminPromotionsListRes = PaginatedResponse<{
|
||||||
promotions: PromotionDTO[]
|
promotions: PromotionDTO[]
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
export type AdminPromotionRes = {
|
||||||
|
promotion: PromotionDTO
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * from "./operators-map"
|
||||||
export * from "./rule-attributes-map"
|
export * from "./rule-attributes-map"
|
||||||
|
export * from "./rule-query-configuration"
|
||||||
export * from "./validate-rule-attribute"
|
export * from "./validate-rule-attribute"
|
||||||
export * from "./validate-rule-type"
|
export * from "./validate-rule-type"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { RuleOperator } from "@medusajs/utils"
|
||||||
|
|
||||||
|
export const operatorsMap = {
|
||||||
|
[RuleOperator.IN]: {
|
||||||
|
id: RuleOperator.IN,
|
||||||
|
value: RuleOperator.IN,
|
||||||
|
label: "In",
|
||||||
|
},
|
||||||
|
[RuleOperator.EQ]: {
|
||||||
|
id: RuleOperator.EQ,
|
||||||
|
value: RuleOperator.EQ,
|
||||||
|
label: "Equals",
|
||||||
|
},
|
||||||
|
[RuleOperator.NE]: {
|
||||||
|
id: RuleOperator.NE,
|
||||||
|
value: RuleOperator.NE,
|
||||||
|
label: "Not In",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
export enum DisguisedRule {
|
||||||
|
APPLY_TO_QUANTITY = "apply_to_quantity",
|
||||||
|
BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const disguisedRulesMap = {
|
||||||
|
[DisguisedRule.APPLY_TO_QUANTITY]: {
|
||||||
|
relation: "application_method",
|
||||||
|
},
|
||||||
|
[DisguisedRule.BUY_RULES_MIN_QUANTITY]: {
|
||||||
|
relation: "application_method",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const ruleAttributes = [
|
const ruleAttributes = [
|
||||||
{
|
{
|
||||||
id: "currency",
|
id: "currency",
|
||||||
@@ -66,20 +80,24 @@ const commonAttributes = [
|
|||||||
|
|
||||||
const buyRuleAttributes = [
|
const buyRuleAttributes = [
|
||||||
{
|
{
|
||||||
id: "buy_rules_min_quantity",
|
id: DisguisedRule.BUY_RULES_MIN_QUANTITY,
|
||||||
value: "buy_rules_min_quantity",
|
value: DisguisedRule.BUY_RULES_MIN_QUANTITY,
|
||||||
label: "Minimum quantity of items",
|
label: "Minimum quantity of items",
|
||||||
|
field_type: "number",
|
||||||
required: true,
|
required: true,
|
||||||
|
disguised: true,
|
||||||
},
|
},
|
||||||
...commonAttributes,
|
...commonAttributes,
|
||||||
]
|
]
|
||||||
|
|
||||||
const targetRuleAttributes = [
|
const targetRuleAttributes = [
|
||||||
{
|
{
|
||||||
id: "apply_to_quantity",
|
id: DisguisedRule.APPLY_TO_QUANTITY,
|
||||||
value: "apply_to_quantity",
|
value: DisguisedRule.APPLY_TO_QUANTITY,
|
||||||
label: "Quantity of items promotion will apply to",
|
label: "Quantity of items promotion will apply to",
|
||||||
|
field_type: "number",
|
||||||
required: true,
|
required: true,
|
||||||
|
disguised: true,
|
||||||
},
|
},
|
||||||
...commonAttributes,
|
...commonAttributes,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export const ruleQueryConfigurations = {
|
||||||
|
region: {
|
||||||
|
entryPoint: "region",
|
||||||
|
labelAttr: "name",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
currency: {
|
||||||
|
entryPoint: "currency",
|
||||||
|
labelAttr: "name",
|
||||||
|
valueAttr: "code",
|
||||||
|
},
|
||||||
|
customer_group: {
|
||||||
|
entryPoint: "customer_group",
|
||||||
|
labelAttr: "name",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
sales_channel: {
|
||||||
|
entryPoint: "sales_channel",
|
||||||
|
labelAttr: "name",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
entryPoint: "country",
|
||||||
|
labelAttr: "display_name",
|
||||||
|
valueAttr: "iso_2",
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
entryPoint: "product",
|
||||||
|
labelAttr: "title",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
product_category: {
|
||||||
|
entryPoint: "product_category",
|
||||||
|
labelAttr: "name",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
product_collection: {
|
||||||
|
entryPoint: "product_collection",
|
||||||
|
labelAttr: "title",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
product_type: {
|
||||||
|
entryPoint: "product_type",
|
||||||
|
labelAttr: "value",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
product_tag: {
|
||||||
|
entryPoint: "product_tag",
|
||||||
|
labelAttr: "value",
|
||||||
|
valueAttr: "id",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { XorConstraint } from "../../../types/validators/xor"
|
|||||||
import { AdminPostCampaignsReq } from "../campaigns/validators"
|
import { AdminPostCampaignsReq } from "../campaigns/validators"
|
||||||
|
|
||||||
export class AdminGetPromotionsPromotionParams extends FindParams {}
|
export class AdminGetPromotionsPromotionParams extends FindParams {}
|
||||||
|
export class AdminGetPromotionRules extends FindParams {}
|
||||||
|
|
||||||
export class AdminGetPromotionsRuleValueParams extends extendedFindParamsMixin({
|
export class AdminGetPromotionsRuleValueParams extends extendedFindParamsMixin({
|
||||||
limit: 100,
|
limit: 100,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type AdminGetPromotionRulesRes = {
|
||||||
|
id?: string
|
||||||
|
attribute: string
|
||||||
|
attribute_label: string
|
||||||
|
operator: string
|
||||||
|
operator_label: string
|
||||||
|
values: { label?: string; value: string }[]
|
||||||
|
disguised?: boolean
|
||||||
|
required?: boolean
|
||||||
|
}[]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./common"
|
export * from "./common"
|
||||||
|
export * from "./http"
|
||||||
export * from "./mutations"
|
export * from "./mutations"
|
||||||
export * from "./service"
|
export * from "./service"
|
||||||
export * from "./workflows"
|
export * from "./workflows"
|
||||||
Reference in new issue
Block a user