feat(dashboard) admin 3.0 create discounts form (#6590)
This commit is contained in:
@@ -237,12 +237,15 @@
|
||||
"discounts": {
|
||||
"domain": "Discounts",
|
||||
"startDate": "Start date",
|
||||
"createDiscountTitle": "Create discount",
|
||||
"validDuration": "Duration of the discount",
|
||||
"redemptionsLimit": "Redemptions limit",
|
||||
"endDate": "End date",
|
||||
"type": "Discount type",
|
||||
"percentageDiscount": "Percentage discount",
|
||||
"freeShipping": "Free shipping",
|
||||
"fixedDiscount": "Fixed discount",
|
||||
"fixedAmount": "Fixed amount",
|
||||
"validRegions": "Valid regions",
|
||||
"deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.",
|
||||
"editDiscountDetails": "Edit discount details",
|
||||
@@ -260,6 +263,13 @@
|
||||
"chooseValidRegions": "Choose valid regions",
|
||||
"noConditions": "No conditions are defined for this discount.",
|
||||
"editConditions": "Edit conditions",
|
||||
"conditionsHint": "Create conditions to apply on the discount",
|
||||
"isTemplateDiscount": "Is this a template discount?",
|
||||
"percentageDescription" : "Discount applied in %",
|
||||
"fixedDescription" : "Amount discount",
|
||||
"shippingDescription" : "Override delivery amount",
|
||||
"selectRegionFirst": "Select region first",
|
||||
"templateHint": "Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same.",
|
||||
"conditions": {
|
||||
"including": {
|
||||
"products_one": "Discount applies to <0/> product.",
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import { Heading, Input, Text } from "@medusajs/ui"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import {
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Heading,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
Textarea,
|
||||
RadioGroup,
|
||||
CurrencyInput,
|
||||
} from "@medusajs/ui"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { useAdminRegions } from "medusa-react"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { CreateDiscountFormReturn } from "./create-discount-form.tsx"
|
||||
import { CreateDiscountFormReturn } from "./create-discount-form"
|
||||
import { Combobox } from "../../../../../components/common/combobox"
|
||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||
import { DiscountRuleType } from "./types"
|
||||
|
||||
type CreateDiscountPropsProps = {
|
||||
form: CreateDiscountFormReturn
|
||||
@@ -11,56 +27,638 @@ type CreateDiscountPropsProps = {
|
||||
export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { regions } = useAdminRegions()
|
||||
|
||||
const watchType = form.watch("type")
|
||||
const watchRegion = form.watch("regions")
|
||||
|
||||
const isFixedDiscount = watchType === DiscountRuleType.FIXED
|
||||
const isFreeShipping = watchType === DiscountRuleType.FREE_SHIPPING
|
||||
|
||||
const activeRegion = useMemo(() => {
|
||||
if (!watchRegion || !regions?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return regions.find((r) => r.id === watchRegion[0])
|
||||
}, [regions, watchRegion])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("regions", [])
|
||||
form.setValue("value", undefined)
|
||||
}, [watchType])
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col items-center overflow-auto p-16">
|
||||
<div className="flex w-full max-w-[736px] flex-col justify-center px-2 pb-2">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<Heading>{t("discount.createDiscountTitle")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("discounts.createDiscountHint")}
|
||||
</Text>
|
||||
<Heading>{t("discounts.createDiscountTitle")}</Heading>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-8 divide-y [&>div]:pt-8">
|
||||
<div id="general" className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.code")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/* DETAILS */}
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("discounts.type")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex justify-between gap-4"
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
className="flex-1"
|
||||
value={DiscountRuleType.PERCENTAGE}
|
||||
label={t("fields.percentage")}
|
||||
description={t("discounts.percentageDescription")}
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
className="flex-1"
|
||||
value={DiscountRuleType.FIXED}
|
||||
label={t("discounts.fixedAmount")}
|
||||
description={t("discounts.fixedDescription")}
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
className="flex-1"
|
||||
value={DiscountRuleType.FREE_SHIPPING}
|
||||
label={t("discounts.freeShipping")}
|
||||
description={t("discounts.shippingDescription")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="regions"
|
||||
render={({ field: { onChange, value, ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("discounts.chooseValidRegions")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
{isFixedDiscount ? (
|
||||
<Select
|
||||
value={value[0]}
|
||||
onValueChange={(v) => {
|
||||
if (v) {
|
||||
onChange([v])
|
||||
}
|
||||
}}
|
||||
{...field}
|
||||
>
|
||||
<Select.Trigger ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{(regions || []).map((r) => (
|
||||
<Select.Item key={r.id} value={r.id}>
|
||||
{r.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
) : (
|
||||
<Combobox
|
||||
options={(regions || []).map((r) => ({
|
||||
label: r.name,
|
||||
value: r.id,
|
||||
}))}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.code")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{!isFreeShipping && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => {
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.percentage")}</Form.Label>
|
||||
<Form.Label
|
||||
tooltip={
|
||||
isFixedDiscount &&
|
||||
!activeRegion &&
|
||||
t("discounts.selectRegionFirst")
|
||||
}
|
||||
>
|
||||
{isFixedDiscount
|
||||
? t("fields.amount")
|
||||
: t("fields.percentage")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
{isFixedDiscount ? (
|
||||
activeRegion ? (
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
onValueChange={onChange}
|
||||
code={activeRegion.currency_code}
|
||||
symbol={getCurrencySymbol(
|
||||
activeRegion.currency_code
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
) : (
|
||||
<Input key="placeholder" disabled />
|
||||
)
|
||||
) : (
|
||||
<Input
|
||||
key="amount"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "") {
|
||||
onChange(null)
|
||||
} else {
|
||||
onChange(parseFloat(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||
<Trans
|
||||
i18nKey="discounts.titleHint"
|
||||
t={t}
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.description")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="is_dynamic"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex gap-2">
|
||||
<Form.Control>
|
||||
<Checkbox
|
||||
checked={value}
|
||||
onCheckedChange={(s) => onChange(s === true)}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.Label
|
||||
className="cursor-pointer"
|
||||
tooltip={t("discounts.templateHint")}
|
||||
>
|
||||
{t("discounts.isTemplateDiscount")}
|
||||
</Form.Label>
|
||||
</div>
|
||||
{/*<Form.ErrorMessage />*/}
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CONFIGURATIONS */}
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<Text
|
||||
size="large"
|
||||
leading="compact"
|
||||
className="text-ui-fg-base"
|
||||
weight="plus"
|
||||
>
|
||||
{t("fields.configurations")}
|
||||
</Text>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
i18nKey="discounts.titleHint"
|
||||
t={t}
|
||||
i18nKey="discounts.codeHint"
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="start_date_enabled"
|
||||
render={({ field: { value, onChange, ...field } }) => (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label tooltip="todo">
|
||||
{t("discounts.hasStartDate")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={!!value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("discounts.startDateHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
{form.watch("start_date_enabled") && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="start_date"
|
||||
render={({
|
||||
field: { value, onChange, ref: _ref, ...field },
|
||||
}) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Form.Control>
|
||||
<DatePicker
|
||||
showTimePicker
|
||||
value={value ?? undefined}
|
||||
onChange={(v) => {
|
||||
onChange(v ?? null)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="end_date_enabled"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label tooltip="todo">
|
||||
{t("discounts.hasEndDate")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={!!value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("discounts.endDateHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{form.watch("end_date_enabled") && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="end_date"
|
||||
render={({
|
||||
field: { value, onChange, ref: _ref, ...field },
|
||||
}) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Form.Control>
|
||||
<DatePicker
|
||||
showTimePicker
|
||||
value={value ?? undefined}
|
||||
onChange={(v) => {
|
||||
onChange(v ?? null)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="usage_limit_enabled"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label tooltip="todo">
|
||||
{t("discounts.hasUsageLimit")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={!!field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("discounts.usageLimitHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{form.watch("usage_limit_enabled") && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="usage_limit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="valid_duration_enabled"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label tooltip="todo">
|
||||
{t("discounts.hasDurationLimit")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={!!field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("discounts.durationHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{form.watch("valid_duration_enabled") && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="years"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.years")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="months"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.months")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="days"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.days")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="hours"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.hours")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="minutes"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.minutes")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CONDITIONS */}
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<Text
|
||||
size="large"
|
||||
leading="compact"
|
||||
className="text-ui-fg-base"
|
||||
weight="plus"
|
||||
>
|
||||
{t("fields.conditions")}
|
||||
</Text>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="discounts.conditionsHint"
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
@@ -2,19 +2,104 @@ import * as zod from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { UseFormReturn, useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { formatISODuration } from "date-fns"
|
||||
|
||||
import { Button } from "@medusajs/ui"
|
||||
import { useAdminCreateDiscount } from "medusa-react"
|
||||
import { useAdminCreateDiscount, useAdminRegions } from "medusa-react"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { CreateDiscountDetails } from "./create-discount-details.tsx"
|
||||
import { CreateDiscountDetails } from "./create-discount-details"
|
||||
import { DiscountRuleType } from "./types"
|
||||
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
const CreateDiscountSchema = zod.object({
|
||||
code: zod.string(),
|
||||
})
|
||||
/**
|
||||
* There is some duplication here but we cannot achieve
|
||||
* expected behaviour from the form with only union + discriminateUnion
|
||||
*/
|
||||
const CreateDiscountSchema = zod.discriminatedUnion("type", [
|
||||
zod.object({
|
||||
code: zod.string(),
|
||||
regions: zod.array(zod.string()).min(1),
|
||||
is_dynamic: zod.boolean(),
|
||||
start_date: zod.date().optional(),
|
||||
end_date: zod.date().optional(),
|
||||
usage_limit: zod.number().optional(),
|
||||
description: zod.string().optional(),
|
||||
|
||||
start_date_enabled: zod.boolean().optional(),
|
||||
end_date_enabled: zod.boolean().optional(),
|
||||
usage_limit_enabled: zod.boolean().optional(),
|
||||
valid_duration_enabled: zod.boolean().optional(),
|
||||
|
||||
years: zod.number().optional(),
|
||||
months: zod.number().optional(),
|
||||
days: zod.number().optional(),
|
||||
hours: zod.number().optional(),
|
||||
minutes: zod.number().optional(),
|
||||
value: zod.number().min(0).max(100),
|
||||
type: zod.literal(DiscountRuleType.PERCENTAGE),
|
||||
}),
|
||||
zod.object({
|
||||
code: zod.string(),
|
||||
regions: zod.array(zod.string()).min(1),
|
||||
is_dynamic: zod.boolean(),
|
||||
start_date: zod.date().optional(),
|
||||
end_date: zod.date().optional(),
|
||||
usage_limit: zod.number().optional(),
|
||||
description: zod.string().optional(),
|
||||
|
||||
start_date_enabled: zod.boolean().optional(),
|
||||
end_date_enabled: zod.boolean().optional(),
|
||||
usage_limit_enabled: zod.boolean().optional(),
|
||||
valid_duration_enabled: zod.boolean().optional(),
|
||||
|
||||
years: zod.number().optional(),
|
||||
months: zod.number().optional(),
|
||||
days: zod.number().optional(),
|
||||
hours: zod.number().optional(),
|
||||
minutes: zod.number().optional(),
|
||||
|
||||
value: zod.union([zod.string(), zod.number()]).refine((value) => {
|
||||
if (value === "") {
|
||||
return false
|
||||
}
|
||||
|
||||
const num = Number(value)
|
||||
|
||||
if (isNaN(num)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return num >= 0
|
||||
}, "Amount must be a positive number"),
|
||||
type: zod.literal(DiscountRuleType.FIXED),
|
||||
}),
|
||||
zod.object({
|
||||
code: zod.string(),
|
||||
regions: zod.array(zod.string()).min(1),
|
||||
is_dynamic: zod.boolean(),
|
||||
start_date: zod.date().optional(),
|
||||
end_date: zod.date().optional(),
|
||||
usage_limit: zod.number().optional(),
|
||||
description: zod.string().optional(),
|
||||
|
||||
start_date_enabled: zod.boolean().optional(),
|
||||
end_date_enabled: zod.boolean().optional(),
|
||||
usage_limit_enabled: zod.boolean().optional(),
|
||||
valid_duration_enabled: zod.boolean().optional(),
|
||||
|
||||
years: zod.number().optional(),
|
||||
months: zod.number().optional(),
|
||||
days: zod.number().optional(),
|
||||
hours: zod.number().optional(),
|
||||
minutes: zod.number().optional(),
|
||||
value: zod.undefined(),
|
||||
type: zod.literal(DiscountRuleType.FREE_SHIPPING),
|
||||
}),
|
||||
])
|
||||
|
||||
type Schema = zod.infer<typeof CreateDiscountSchema>
|
||||
export type CreateDiscountFormReturn = UseFormReturn<Schema>
|
||||
@@ -25,17 +110,65 @@ export const CreateDiscountForm = () => {
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
code: "",
|
||||
regions: [],
|
||||
is_dynamic: false,
|
||||
type: DiscountRuleType.PERCENTAGE,
|
||||
},
|
||||
resolver: zodResolver(CreateDiscountSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateDiscount()
|
||||
const { regions } = useAdminRegions()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values: Schema) => {
|
||||
const getValue = () => {
|
||||
if (values.type === DiscountRuleType.FREE_SHIPPING) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (values.type === DiscountRuleType.PERCENTAGE) {
|
||||
return values.value
|
||||
}
|
||||
|
||||
const amount =
|
||||
typeof values.value === "string" ? Number(values.value) : values.value
|
||||
|
||||
const region = regions!.find((r) => r.id === values.regions[0])
|
||||
|
||||
return getDbAmount(amount, region!.currency_code)
|
||||
}
|
||||
|
||||
const duration = {
|
||||
years: values.years,
|
||||
months: values.months,
|
||||
days: values.days,
|
||||
hours: values.hours,
|
||||
minutes: values.minutes,
|
||||
}
|
||||
|
||||
const isDurationEmpty = Object.values(duration).every((v) => !v)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
code: values.code,
|
||||
regions: values.regions,
|
||||
is_dynamic: values.is_dynamic,
|
||||
starts_at: values.start_date_enabled ? values.start_date : undefined,
|
||||
ends_at: values.end_date_enabled ? values.end_date : undefined,
|
||||
is_disabled: false,
|
||||
usage_limit: values.usage_limit_enabled
|
||||
? values.usage_limit
|
||||
: undefined,
|
||||
valid_duration:
|
||||
values.valid_duration_enabled && !isDurationEmpty
|
||||
? formatISODuration(duration)
|
||||
: undefined,
|
||||
rule: {
|
||||
value: getValue(),
|
||||
type: values.type,
|
||||
description: values.description,
|
||||
allocation: "total" as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: ({ discount }) => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum DiscountRuleType {
|
||||
FIXED = "fixed",
|
||||
PERCENTAGE = "percentage",
|
||||
FREE_SHIPPING = "free_shipping",
|
||||
}
|
||||
@@ -79,16 +79,18 @@ export const EditDiscountConfigurationForm = ({
|
||||
const { mutateAsync, isLoading } = useAdminUpdateDiscount(discount.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const duration = pick(data, ["years", "months", "days", "hours", "minutes"])
|
||||
const isDurationEmpty = Object.values(duration).every((v) => !v)
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
starts_at: data.start_date,
|
||||
ends_at: data.end_date_enabled ? data.end_date : null,
|
||||
usage_limit: data.enable_usage_limit ? data.usage_limit : null,
|
||||
valid_duration: data.enable_duration
|
||||
? formatISODuration(
|
||||
pick(data, ["years", "months", "days", "hours", "minutes"])
|
||||
)
|
||||
: null,
|
||||
valid_duration:
|
||||
data.enable_duration && !isDurationEmpty
|
||||
? formatISODuration(duration)
|
||||
: null,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -189,40 +191,34 @@ export const EditDiscountConfigurationForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="end_date"
|
||||
render={({
|
||||
field: { value, onChange, ref: _ref, ...field },
|
||||
}) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Control>
|
||||
<DatePicker
|
||||
showTimePicker
|
||||
value={value ?? undefined}
|
||||
onChange={(v) => {
|
||||
onChange(v ?? null)
|
||||
}}
|
||||
{...field}
|
||||
/**
|
||||
* TODO: FIX bug in the picker when a placeholder is provided it resets selected value to undefined
|
||||
*/
|
||||
// placeholder="DD/MM/YYYY HH:MM"
|
||||
/*
|
||||
* Disable input here. If set on Field it wont properly set the value.
|
||||
*/
|
||||
disabled={!form.watch("end_date_enabled")}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
{form.watch("end_date_enabled") && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="end_date"
|
||||
render={({
|
||||
field: { value, onChange, ref: _ref, ...field },
|
||||
}) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Control>
|
||||
<DatePicker
|
||||
showTimePicker
|
||||
value={value ?? undefined}
|
||||
onChange={(v) => {
|
||||
onChange(v ?? null)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
@@ -254,34 +250,35 @@ export const EditDiscountConfigurationForm = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="usage_limit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_usage_limit")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
{form.watch("enable_usage_limit") && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="usage_limit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
@@ -312,155 +309,158 @@ export const EditDiscountConfigurationForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="years"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.years")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
{form.watch("enable_duration") && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="years"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.years")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="months"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.months")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="months"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.months")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="days"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.days")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="days"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.days")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="hours"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.hours")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{form.watch("enable_duration") && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="hours"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.hours")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="minutes"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.minutes")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
console.log(Number(value))
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="minutes"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="flex-1">
|
||||
<Form.Label>{t("fields.minutes")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!form.watch("enable_duration")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
} else {
|
||||
console.log(Number(value))
|
||||
field.onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
Reference in New Issue
Block a user