feat(promotion,dashboard,types,utils,medusa): Add statuses to promotions (#10950)
what: - adds a status column to promotion table - introduce active promotion query - scope revert, register and compute actions to active promotions - admin to create and update promotion with statuses RESOLVES CMRC-845 RESOLVES CMRC-846 RESOLVES CMRC-847 RESOLVES CMRC-848 RESOLVES CMRC-849 RESOLVES CMRC-850
This commit is contained in:
@@ -202,7 +202,7 @@ const useDynamicSearchResults = (
|
||||
{
|
||||
q: debouncedSearch,
|
||||
limit,
|
||||
fields: "id,code",
|
||||
fields: "id,code,status",
|
||||
},
|
||||
{
|
||||
enabled: isAreaEnabled(currentArea, "promotion"),
|
||||
|
||||
+2
-17
@@ -1,28 +1,13 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
getPromotionStatus,
|
||||
PromotionStatus,
|
||||
} from "../../../../../lib/promotions"
|
||||
import { getPromotionStatus } from "../../../../../lib/promotions"
|
||||
import { StatusCell as StatusCell_ } from "../../common/status-cell"
|
||||
|
||||
type PromotionCellProps = {
|
||||
promotion: HttpTypes.AdminPromotion
|
||||
}
|
||||
type StatusColors = "grey" | "orange" | "green" | "red"
|
||||
type StatusMap = Record<string, [StatusColors, string]>
|
||||
|
||||
export const StatusCell = ({ promotion }: PromotionCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const statusMap: StatusMap = {
|
||||
[PromotionStatus.DISABLED]: ["grey", t("statuses.disabled")],
|
||||
[PromotionStatus.ACTIVE]: ["green", t("statuses.active")],
|
||||
[PromotionStatus.SCHEDULED]: ["orange", t("statuses.scheduled")],
|
||||
[PromotionStatus.EXPIRED]: ["red", t("statuses.expired")],
|
||||
}
|
||||
|
||||
const [color, text] = statusMap[getPromotionStatus(promotion)]
|
||||
const [color, text] = getPromotionStatus(promotion)
|
||||
|
||||
return <StatusCell_ color={color}>{text}</StatusCell_>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
import { HttpTypes, LinkMethodRequest } from "@medusajs/types"
|
||||
import {
|
||||
QueryKey,
|
||||
@@ -10,7 +11,6 @@ import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { promotionsQueryKeys } from "./promotions"
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
|
||||
const REGIONS_QUERY_KEY = "campaigns" as const
|
||||
export const campaignsQueryKeys = queryKeysFactory(REGIONS_QUERY_KEY)
|
||||
@@ -88,6 +88,8 @@ export const useUpdateCampaign = (
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.details() })
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.details() })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1933,7 +1933,19 @@
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"title": "Status"
|
||||
"label": "Status",
|
||||
"draft": {
|
||||
"title": "Draft",
|
||||
"description": "Customers will not be able to use the code yet"
|
||||
},
|
||||
"active": {
|
||||
"title": "Active",
|
||||
"description": "Customers will be able to use the code"
|
||||
},
|
||||
"inactive": {
|
||||
"title": "Inactive",
|
||||
"description": "Customers will no longer be able to use the code"
|
||||
}
|
||||
},
|
||||
"method": {
|
||||
"label": "Method",
|
||||
@@ -2676,6 +2688,8 @@
|
||||
"scheduled": "Scheduled",
|
||||
"expired": "Expired",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"draft": "Draft",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { i18n } from "../components/utilities/i18n"
|
||||
|
||||
export enum PromotionStatus {
|
||||
SCHEDULED = "SCHEDULED",
|
||||
EXPIRED = "EXPIRED",
|
||||
ACTIVE = "ACTIVE",
|
||||
DISABLED = "DISABLED",
|
||||
INACTIVE = "INACTIVE",
|
||||
DRAFT = "DRAFT",
|
||||
}
|
||||
|
||||
export type StatusColors = "grey" | "orange" | "green" | "red" | "grey"
|
||||
export type StatusMap = Record<string, [StatusColors, string]>
|
||||
export const promotionStatusMap: StatusMap = {
|
||||
[PromotionStatus.ACTIVE]: ["green", i18n.t("statuses.active")],
|
||||
[PromotionStatus.INACTIVE]: ["red", i18n.t("statuses.inactive")],
|
||||
[PromotionStatus.DRAFT]: ["grey", i18n.t("statuses.draft")],
|
||||
[PromotionStatus.SCHEDULED]: [
|
||||
"orange",
|
||||
`${i18n.t("promotions.fields.campaign")} ${i18n.t("statuses.scheduled").toLowerCase()}`,
|
||||
],
|
||||
[PromotionStatus.EXPIRED]: [
|
||||
"red",
|
||||
`${i18n.t("promotions.fields.campaign")} ${i18n.t("statuses.expired").toLowerCase()}`,
|
||||
],
|
||||
}
|
||||
|
||||
export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
|
||||
@@ -12,11 +30,11 @@ export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
|
||||
const campaign = promotion.campaign
|
||||
|
||||
if (!campaign) {
|
||||
return PromotionStatus.ACTIVE
|
||||
return promotionStatusMap[promotion.status!.toUpperCase()]
|
||||
}
|
||||
|
||||
if (new Date(campaign.starts_at!) > date) {
|
||||
return PromotionStatus.SCHEDULED
|
||||
if (campaign.starts_at && new Date(campaign.starts_at!) > date) {
|
||||
return promotionStatusMap[PromotionStatus.SCHEDULED]
|
||||
}
|
||||
|
||||
const campaignBudget = campaign.budget
|
||||
@@ -24,8 +42,8 @@ export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
|
||||
campaignBudget && campaignBudget.used! > campaignBudget.limit!
|
||||
|
||||
if ((campaign.ends_at && new Date(campaign.ends_at) < date) || overBudget) {
|
||||
return PromotionStatus.EXPIRED
|
||||
return promotionStatusMap[PromotionStatus.EXPIRED]
|
||||
}
|
||||
|
||||
return PromotionStatus.ACTIVE
|
||||
return promotionStatusMap[promotion.status!.toUpperCase()]
|
||||
}
|
||||
|
||||
+51
-8
@@ -1,4 +1,12 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
ApplicationMethodAllocationValues,
|
||||
ApplicationMethodTargetTypeValues,
|
||||
ApplicationMethodTypeValues,
|
||||
PromotionRuleOperatorValues,
|
||||
PromotionStatusValues,
|
||||
PromotionTypeValues,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
@@ -17,14 +25,6 @@ import { useEffect, useMemo, useState } from "react"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
ApplicationMethodAllocationValues,
|
||||
ApplicationMethodTargetTypeValues,
|
||||
ApplicationMethodTypeValues,
|
||||
PromotionRuleOperatorValues,
|
||||
PromotionTypeValues,
|
||||
} from "@medusajs/types"
|
||||
import { Divider } from "../../../../../components/common/divider"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||
@@ -50,6 +50,7 @@ const defaultValues = {
|
||||
is_automatic: "false",
|
||||
code: "",
|
||||
type: "standard" as PromotionTypeValues,
|
||||
status: "draft" as PromotionStatusValues,
|
||||
rules: [],
|
||||
application_method: {
|
||||
allocation: "each" as ApplicationMethodAllocationValues,
|
||||
@@ -508,6 +509,48 @@ export const CreatePromotionForm = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.form.status.label")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex gap-y-3"
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"draft"}
|
||||
label={t("promotions.form.status.draft.title")}
|
||||
description={t(
|
||||
"promotions.form.status.draft.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"active"}
|
||||
label={t("promotions.form.status.active.title")}
|
||||
description={t(
|
||||
"promotions.form.status.active.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ export const CreatePromotionSchema = z
|
||||
is_automatic: z.string().toLowerCase(),
|
||||
code: z.string().min(1),
|
||||
type: z.enum(["buyget", "standard"]),
|
||||
status: z.enum(["draft", "active", "inactive"]),
|
||||
rules: RuleSchema,
|
||||
application_method: z.object({
|
||||
allocation: z.enum(["each", "across"]),
|
||||
|
||||
+2
-14
@@ -16,10 +16,7 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { useDeletePromotion } from "../../../../../hooks/api/promotions"
|
||||
import { formatCurrency } from "../../../../../lib/format-currency"
|
||||
import { formatPercentage } from "../../../../../lib/percentage-helpers"
|
||||
import {
|
||||
getPromotionStatus,
|
||||
PromotionStatus,
|
||||
} from "../../../../../lib/promotions"
|
||||
import { getPromotionStatus } from "../../../../../lib/promotions"
|
||||
|
||||
type PromotionGeneralSectionProps = {
|
||||
promotion: HttpTypes.AdminPromotion
|
||||
@@ -78,16 +75,7 @@ export const PromotionGeneralSection = ({
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
const [color, text] = getPromotionStatus(promotion)
|
||||
const displayValue = getDisplayValue(promotion)
|
||||
|
||||
return (
|
||||
|
||||
+50
-2
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { PromotionDTO } from "@medusajs/types"
|
||||
import { AdminPromotion } 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"
|
||||
@@ -13,12 +13,13 @@ import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
|
||||
|
||||
type EditPromotionFormProps = {
|
||||
promotion: PromotionDTO
|
||||
promotion: AdminPromotion
|
||||
}
|
||||
|
||||
const EditPromotionSchema = zod.object({
|
||||
is_automatic: zod.string().toLowerCase(),
|
||||
code: zod.string().min(1),
|
||||
status: zod.enum(["active", "inactive", "draft"]),
|
||||
value_type: zod.enum(["fixed", "percentage"]),
|
||||
value: zod.number(),
|
||||
allocation: zod.enum(["each", "across"]),
|
||||
@@ -34,6 +35,7 @@ export const EditPromotionDetailsForm = ({
|
||||
defaultValues: {
|
||||
is_automatic: promotion.is_automatic!.toString(),
|
||||
code: promotion.code,
|
||||
status: promotion.status,
|
||||
value: promotion.application_method!.value,
|
||||
allocation: promotion.application_method!.allocation,
|
||||
value_type: promotion.application_method!.type,
|
||||
@@ -55,6 +57,7 @@ export const EditPromotionDetailsForm = ({
|
||||
{
|
||||
is_automatic: data.is_automatic === "true",
|
||||
code: data.code,
|
||||
status: data.status,
|
||||
application_method: {
|
||||
value: data.value,
|
||||
type: data.value_type as any,
|
||||
@@ -77,6 +80,51 @@ export const EditPromotionDetailsForm = ({
|
||||
>
|
||||
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("promotions.form.status.label")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col gap-y-3"
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"draft"}
|
||||
label={t("promotions.form.status.draft.title")}
|
||||
description={t(
|
||||
"promotions.form.status.draft.description"
|
||||
)}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"active"}
|
||||
label={t("promotions.form.status.active.title")}
|
||||
description={t(
|
||||
"promotions.form.status.active.description"
|
||||
)}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"inactive"}
|
||||
label={t("promotions.form.status.inactive.title")}
|
||||
description={t(
|
||||
"promotions.form.status.inactive.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="is_automatic"
|
||||
|
||||
Reference in New Issue
Block a user