import { zodResolver } from "@hookform/resolvers/zod" import { ApplicationMethodAllocationValues, ApplicationMethodTargetTypeValues, ApplicationMethodTypeValues, PromotionRuleOperatorValues, PromotionStatusValues, PromotionTypeValues, } from "@medusajs/types" import { Alert, Badge, Button, clx, CurrencyInput, Divider, Heading, Input, ProgressStatus, ProgressTabs, RadioGroup, Switch, Text, toast, } from "@medusajs/ui" import { useEffect, useMemo, useState } from "react" import { useForm, useWatch } from "react-hook-form" import { Trans, useTranslation } from "react-i18next" import { z } from "zod" import { Form } from "../../../../../components/common/form" import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input" import { RouteFocusModal, useRouteModal, } from "../../../../../components/modals" import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { useCreatePromotion } from "../../../../../hooks/api/promotions" import { currencies, getCurrencySymbol, } from "../../../../../lib/data/currencies" import { DEFAULT_CAMPAIGN_VALUES } from "../../../../campaigns/common/constants" import { RulesFormField } from "../../../common/edit-rules/components/rules-form-field" import { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form" import { Tab } from "./constants" import { CreatePromotionSchema } from "./form-schema" import { templates } from "./templates" import { useDocumentDirection } from "../../../../../hooks/use-document-direction" const defaultValues = { campaign_id: undefined, template_id: templates[0].id!, campaign_choice: "none" as "none", is_automatic: "false", code: "", type: "standard" as PromotionTypeValues, status: "draft" as PromotionStatusValues, rules: [], is_tax_inclusive: false, limit: undefined, application_method: { allocation: "each" as ApplicationMethodAllocationValues, type: "fixed" as ApplicationMethodTypeValues, target_type: "items" as ApplicationMethodTargetTypeValues, max_quantity: 1, target_rules: [], buy_rules: [], }, campaign: undefined, } type TabState = Record export const CreatePromotionForm = () => { const [tab, setTab] = useState(Tab.TYPE) const [tabState, setTabState] = useState({ [Tab.TYPE]: "in-progress", [Tab.PROMOTION]: "not-started", [Tab.CAMPAIGN]: "not-started", }) const { t } = useTranslation() const { handleSuccess } = useRouteModal() const direction = useDocumentDirection() const form = useForm>({ defaultValues, resolver: zodResolver(CreatePromotionSchema), }) const { setValue, reset, getValues } = form const { mutateAsync: createPromotion } = useCreatePromotion() const handleSubmit = form.handleSubmit( async (data) => { const { campaign_choice: _campaignChoice, is_automatic, is_tax_inclusive, template_id: _templateId, application_method, rules, ...promotionData } = data const { target_rules: targetRulesData = [], buy_rules: buyRulesData = [], ...applicationMethodData } = application_method const disguisedRules = [ ...targetRulesData.filter((r) => !!r.disguised), ...buyRulesData.filter((r) => !!r.disguised), ...rules.filter((r) => !!r.disguised), ] const applicationMethodRuleData: Record = {} for (const rule of disguisedRules) { applicationMethodRuleData[rule.attribute] = rule.field_type === "number" ? parseInt(rule.values as string) : rule.values } const buildRulesData = ( rules: { operator: string attribute: string values: any[] | any disguised?: boolean }[] ) => { return rules .filter((r) => !r.disguised) .map((rule) => ({ operator: rule.operator as PromotionRuleOperatorValues, attribute: rule.attribute, values: rule.values, })) } if (data.campaign) { data.campaign.budget.attribute = data.campaign.budget.attribute || null data.campaign.budget.type = data.campaign.budget.attribute ? "use_by_attribute" : data.campaign.budget.type } createPromotion( { ...promotionData, rules: buildRulesData(rules), application_method: { ...applicationMethodData, ...applicationMethodRuleData, value: parseFloat(applicationMethodData.value as string) as number, target_rules: buildRulesData(targetRulesData), buy_rules: buildRulesData(buyRulesData), }, is_tax_inclusive, is_automatic: is_automatic === "true", }, { onSuccess: ({ promotion }) => { toast.success( t("promotions.toasts.promotionCreateSuccess", { code: promotion.code, }) ) handleSuccess(`/promotions/${promotion.id}`) }, onError: (e) => { toast.error(e.message) }, } ) }, async (error) => { const { campaign: _campaign, ...rest } = error || {} const errorInPromotionTab = !!Object.keys(rest || {}).length if (errorInPromotionTab) { toast.error(t("promotions.errors.promotionTabError")) } } ) const handleTabChange = async (tab: Tab) => { switch (tab) { case Tab.TYPE: setTabState((prev) => ({ ...prev, [Tab.TYPE]: "in-progress", })) setTab(tab) break case Tab.PROMOTION: setTabState((prev) => ({ ...prev, [Tab.TYPE]: "completed", [Tab.PROMOTION]: "in-progress", })) setTab(tab) break case Tab.CAMPAIGN: { const valid = await form.trigger() if (!valid) { // If the promotion tab is not valid, we want to set the tab state to in-progress // and set the tab to the promotion tab setTabState({ [Tab.TYPE]: "completed", [Tab.PROMOTION]: "in-progress", [Tab.CAMPAIGN]: "not-started", }) setTab(Tab.PROMOTION) break } setTabState((prev) => ({ ...prev, [Tab.PROMOTION]: "completed", [Tab.CAMPAIGN]: "in-progress", })) setTab(tab) break } } } const handleContinue = async () => { switch (tab) { case Tab.TYPE: handleTabChange(Tab.PROMOTION) break case Tab.PROMOTION: { const valid = await form.trigger() if (valid) { handleTabChange(Tab.CAMPAIGN) } break } case Tab.CAMPAIGN: break } } const watchTemplateId = useWatch({ control: form.control, name: "template_id", }) const currentTemplate = useMemo(() => { const currentTemplate = templates.find( (template) => template.id === watchTemplateId ) if (!currentTemplate) { return } reset({ ...defaultValues, template_id: watchTemplateId }) for (const [key, value] of Object.entries(currentTemplate.defaults)) { if (typeof value === "object") { for (const [subKey, subValue] of Object.entries(value)) { setValue(`application_method.${subKey}` as any, subValue) } } else { setValue(key as any, value) } } // if switching to `across` allocation, reset call above will set the max quantity to 1 which is forbidden for this allocation type // but since there wasn't change in the `application_method.allocation` field, useEffect below responsible for resetting will not be triggered if (currentTemplate.defaults?.application_method?.allocation === "across") { setValue("application_method.max_quantity", null) } return currentTemplate }, [watchTemplateId, setValue, reset]) const watchValueType = useWatch({ control: form.control, name: "application_method.type", }) const isFixedValueType = watchValueType === "fixed" const watchAllocation = useWatch({ control: form.control, name: "application_method.allocation", }) useEffect(() => { if (watchAllocation === "across") { setValue("application_method.max_quantity", null) } }, [watchAllocation, setValue]) const watchType = useWatch({ control: form.control, name: "type", }) const isTypeStandard = watchType === "standard" const isTypeBuyGet = watchType === "buyget" const targetType = useWatch({ control: form.control, name: "application_method.target_type", }) const isTargetTypeOrder = targetType === "order" const watchCampaignChoice = useWatch({ control: form.control, name: "campaign_choice", }) useEffect(() => { const formData = getValues() if (watchCampaignChoice !== "existing") { setValue("campaign_id", undefined) } if (watchCampaignChoice !== "new") { setValue("campaign", undefined) } if (watchCampaignChoice === "new") { if (!formData.campaign || !formData.campaign?.budget?.type) { setValue("campaign", { ...DEFAULT_CAMPAIGN_VALUES, budget: { ...DEFAULT_CAMPAIGN_VALUES.budget, currency_code: formData.application_method.currency_code, }, }) } } }, [watchCampaignChoice, getValues, setValue]) const watchRules = useWatch({ control: form.control, name: "rules", }) const watchCurrencyRule = watchRules.find( (rule) => rule.attribute === "currency_code" ) if (watchCurrencyRule) { const formData = form.getValues() const currencyCode = formData.application_method.currency_code const ruleValue = watchCurrencyRule.values if (!Array.isArray(ruleValue) && currencyCode !== ruleValue) { form.setValue("application_method.currency_code", ruleValue as string) } } return ( handleTabChange(tab as Tab)} className="flex h-full flex-col overflow-hidden" >
{t("promotions.tabs.template")} {t("promotions.tabs.details")} {t("promotions.tabs.campaign")}
{ return ( {t("promotions.fields.type")} {templates.map((template) => { return ( ) })} ) }} />
{t(`promotions.sections.details`)} {currentTemplate?.title && ( {t(currentTemplate.title as any)} )} {form.formState.errors.root && ( {form.formState.errors.root.message} )} { return ( {t("promotions.form.method.label")} ) }} /> { return ( {t("promotions.form.status.label")} ) }} />
{ return ( {t("promotions.form.code.title")} ]} /> ) }} />
{!currentTemplate?.hiddenFields?.includes( "is_tax_inclusive" ) && ( <>
{ return (
{t("promotions.form.taxInclusive.title")} {t( "promotions.form.taxInclusive.description" )}
) }} />
)} {!currentTemplate?.hiddenFields?.includes("type") && ( { return ( {t("promotions.fields.type")} ) }} /> )} {!currentTemplate?.hiddenFields?.includes( "application_method.type" ) && ( <> { return ( {t("promotions.fields.value_type")} ) }} /> )} {!currentTemplate?.hiddenFields?.includes( "application_method.value" ) && ( <> { const currencyCode = form.getValues().application_method.currency_code const currencyInfo = currencies[currencyCode?.toUpperCase() || "USD"] return ( {t("promotions.form.value.title")} {isFixedValueType ? ( onChange(values?.value) } decimalScale={ currencyInfo?.decimal_digits ?? 2 } decimalsLimit={ currencyInfo?.decimal_digits ?? 2 } symbol={ currencyCode ? getCurrencySymbol(currencyCode) : "$" } value={value} disabled={!currencyCode} /> ) : ( { onChange( e.target.value === "" ? null : parseFloat(e.target.value) ) }} /> )} ]} /> ) }} /> )} {((isTypeStandard && (watchAllocation === "each" || watchAllocation === "once")) || isTypeBuyGet) && ( <> {isTypeBuyGet && ( <> )} { return ( {t("promotions.form.max_quantity.title")} ]} /> ) }} /> )} {isTypeStandard && !currentTemplate?.hiddenFields?.includes( "application_method.allocation" ) && ( { return ( {t("promotions.fields.allocation")} {!currentTemplate?.hiddenFields?.includes( "application_method.allocation.each" ) && ( )} {!currentTemplate?.hiddenFields?.includes( "application_method.allocation.across" ) && ( )} {!currentTemplate?.hiddenFields?.includes( "application_method.allocation.once" ) && ( )} ) }} /> )} {!isTypeStandard && ( <> )} {!isTargetTypeOrder && ( <> )} { return ( {t("promotions.form.limit.title")} { const val = e.target.value onChange(val === "" ? null : parseInt(val, 10)) }} placeholder="100" /> {t("promotions.form.limit.description")} ) }} />
{tab === Tab.CAMPAIGN ? ( ) : ( )}
) }