feat(dashboard): Discounts details + edits (#6547)
**What** - Discounts details page - Edit discount details - Edit discount configurations - `ListSummary` component **NOTE** - conditions edit form will be implemented in a separate PR - edit details from is missing metadata component which will be added later --- https://github.com/medusajs/medusa/assets/16856471/c878af4a-48c2-4c45-b824-662784c7a139
This commit is contained in:
@@ -190,7 +190,7 @@
|
||||
"partiallyRefunded": "Partially refunded",
|
||||
"refunded": "Refunded",
|
||||
"canceled": "Canceled",
|
||||
"requresAction": "Requires action"
|
||||
"requiresAction": "Requires action"
|
||||
}
|
||||
},
|
||||
"reservations": {
|
||||
@@ -213,7 +213,7 @@
|
||||
"partiallyReturned": "Partially returned",
|
||||
"returned": "Returned",
|
||||
"canceled": "Canceled",
|
||||
"requresAction": "Requires action"
|
||||
"requiresAction": "Requires action"
|
||||
},
|
||||
"trackingLabel": "Tracking",
|
||||
"shippingFromLabel": "Shipping from",
|
||||
@@ -232,7 +232,51 @@
|
||||
},
|
||||
"discounts": {
|
||||
"domain": "Discounts",
|
||||
"deleteWarning": "You are about to delete the discount {{title}}. This action cannot be undone.",
|
||||
"startDate": "Start date",
|
||||
"validDuration": "Duration of the discount",
|
||||
"redemptionsLimit": "Redemptions limit",
|
||||
"endDate": "End date",
|
||||
"percentageDiscount": "Percentage discount",
|
||||
"freeShipping": "Free shipping",
|
||||
"fixedDiscount": "Fixed discount",
|
||||
"validRegions": "Valid regions",
|
||||
"deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.",
|
||||
"editDiscountDetails": "Edit discount details",
|
||||
"editDiscountConfiguration": "Edit discount configuration",
|
||||
"hasStartDate": "Discount has a start date",
|
||||
"hasEndDate": "Discount has an expiry date",
|
||||
"startDateHint": "Schedule the discount to activate in the future.",
|
||||
"endDateHint": "Schedule the discount to deactivate in the future.",
|
||||
"codeHint": "Discount code applies from when you hit the publish button and forever if left untouched.",
|
||||
"hasUsageLimit": "Limit the number of redemptions?",
|
||||
"usageLimitHint": "Limit applies across all customers, not per customer.",
|
||||
"titleHint": "The code your customers will enter during checkout. This will appear on your customer’s invoice.\nUppercase letters and numbers only.",
|
||||
"hasDurationLimit": "Availability duration",
|
||||
"durationHint": "Set the duration of the discount",
|
||||
"chooseValidRegions": "Choose valid regions",
|
||||
"noConditions": "No conditions are defined for this discount.",
|
||||
"editConditions": "Edit conditions",
|
||||
"conditions": {
|
||||
"including": {
|
||||
"products_one": "Discount applies to <0/> product.",
|
||||
"products_other": "Discount applies to <0/> products.",
|
||||
"customer_groups_one": "Discount applies to <0/> customer group.",
|
||||
"customer_groups_other": "Discount applies to <0/> customer groups.",
|
||||
"product_tags_one": "Discount applies to <0/> tag.",
|
||||
"product_tags_other": "Discount applies to <0/> tags.",
|
||||
"product_collections_one": "Discount applies to <0/> product collection.",
|
||||
"product_collections_other": "Discount applies to <0/> product collections.",
|
||||
"product_types_one": "Discount applies to <0/> product type.",
|
||||
"product_types_other": "Discount applies to <0/> product types."
|
||||
},
|
||||
"excluding": {
|
||||
"products": "Discount applies to <1>all</1> products except <0/>.",
|
||||
"customer_groups": "Discount applies to <1>all</1> customer groups except <0/>.",
|
||||
"product_tags": "Discount applies to <1>all</1> product tags except <0/>.",
|
||||
"product_collections": "Discount applies to <1>all</1> product collections except <0/>.",
|
||||
"product_types": "Discount applies to <1>all</1> product types except <0/>."
|
||||
}
|
||||
},
|
||||
"discountStatus": {
|
||||
"scheduled": "Scheduled",
|
||||
"expired": "Expired",
|
||||
@@ -414,6 +458,7 @@
|
||||
"invalidCredentials": "Wrong email or password"
|
||||
},
|
||||
"fields": {
|
||||
"amount": "Amount",
|
||||
"name": "Name",
|
||||
"lastName": "Last Name",
|
||||
"firstName": "First Name",
|
||||
@@ -426,13 +471,17 @@
|
||||
"newPassword": "New Password",
|
||||
"repeatNewPassword": "Repeat New Password",
|
||||
"categories": "Categories",
|
||||
"configurations": "Configurations",
|
||||
"conditions": "Conditions",
|
||||
"category": "Category",
|
||||
"collection": "Collection",
|
||||
"discountable": "Discountable",
|
||||
"handle": "Handle",
|
||||
"subtitle": "Subtitle",
|
||||
"limit": "Limit",
|
||||
"tags": "Tags",
|
||||
"type": "Type",
|
||||
"percentage": "Percentage",
|
||||
"sales_channels": "Sales Channels",
|
||||
"status": "Status",
|
||||
"code": "Code",
|
||||
@@ -440,6 +489,11 @@
|
||||
"disabled": "Disabled",
|
||||
"dynamic": "Dynamic",
|
||||
"normal": "Normal",
|
||||
"years": "Years",
|
||||
"months": "Months",
|
||||
"days": "Days",
|
||||
"hours": "Hours",
|
||||
"minutes": "Minutes",
|
||||
"totalRedemptions": "Total Redemptions",
|
||||
"countries": "Countries",
|
||||
"paymentProviders": "Payment Providers",
|
||||
@@ -513,6 +567,23 @@
|
||||
"minSubtotal": "Min. Subtotal",
|
||||
"maxSubtotal": "Max. Subtotal",
|
||||
"shippingProfile": "Shipping Profile",
|
||||
"summary": "Summary"
|
||||
"summary": "Summary",
|
||||
"shippingProfile": "Shipping Profile"
|
||||
},
|
||||
"dateTime" : {
|
||||
"years_one": "Year",
|
||||
"years_other": "Years",
|
||||
"months_one": "Month",
|
||||
"months_other": "Months",
|
||||
"weeks_one": "Week",
|
||||
"weeks_other": "Weeks",
|
||||
"days_one": "Day",
|
||||
"days_other": "Days",
|
||||
"hours_one": "Hour",
|
||||
"hours_other": "Hours",
|
||||
"minutes_one": "Minute",
|
||||
"minutes_other": "Minutes",
|
||||
"seconds_one": "Second",
|
||||
"seconds_other": "Seconds"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const NoRecords = ({
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{title ?? t("general.noRecordsTitle")}
|
||||
</Text>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
<Text size="small" className="text-ui-fg-muted">
|
||||
{message ?? t("general.noRecordsMessage")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { ListSummary } from "./list-summary"
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Text, Tooltip, clx } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type ListSummaryProps = {
|
||||
/**
|
||||
* Number of initial items to display
|
||||
* @default 2
|
||||
*/
|
||||
n?: number
|
||||
/**
|
||||
* List of strings to display as abbreviated list
|
||||
*/
|
||||
list: string[]
|
||||
/**
|
||||
* Is hte summary displayed inline.
|
||||
* Determines whether the center text is truncated if there is no space in the container
|
||||
*/
|
||||
inline?: boolean
|
||||
}
|
||||
|
||||
export const ListSummary = ({ list, inline, n = 2 }: ListSummaryProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className={clx("text-ui-fg-subtle gap-x-2", {
|
||||
"inline-flex": inline,
|
||||
flex: !inline,
|
||||
})}
|
||||
>
|
||||
<Text as="span" leading="compact" size="small" className="truncate">
|
||||
{list.slice(0, n).join(", ")}
|
||||
</Text>
|
||||
{list.length > n && (
|
||||
<Tooltip
|
||||
content={
|
||||
<ul>
|
||||
{list.slice(n).map((c) => (
|
||||
<li key={c}>{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="cursor-default whitespace-nowrap"
|
||||
>
|
||||
{t("general.plusCountMore", {
|
||||
count: list.length - n,
|
||||
})}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +1,17 @@
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import { end, parse } from "iso8601-duration"
|
||||
|
||||
import { StatusCell as StatusCell_ } from "../../common/status-cell"
|
||||
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
getDiscountStatus,
|
||||
PromotionStatus,
|
||||
} from "../../../../../lib/discounts.ts"
|
||||
|
||||
type DiscountCellProps = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
enum PromotionStatus {
|
||||
SCHEDULED = "SCHEDULED",
|
||||
EXPIRED = "EXPIRED",
|
||||
ACTIVE = "ACTIVE",
|
||||
DISABLED = "DISABLED",
|
||||
}
|
||||
|
||||
const getDiscountStatus = (discount: Discount) => {
|
||||
if (discount.is_disabled) {
|
||||
return PromotionStatus.DISABLED
|
||||
}
|
||||
|
||||
const date = new Date()
|
||||
if (new Date(discount.starts_at) > date) {
|
||||
return PromotionStatus.SCHEDULED
|
||||
}
|
||||
|
||||
if (
|
||||
(discount.ends_at && new Date(discount.ends_at) < date) ||
|
||||
(discount.valid_duration &&
|
||||
date >
|
||||
end(parse(discount.valid_duration), new Date(discount.starts_at))) ||
|
||||
discount.usage_count === discount.usage_limit
|
||||
) {
|
||||
return PromotionStatus.EXPIRED
|
||||
}
|
||||
|
||||
return PromotionStatus.ACTIVE
|
||||
}
|
||||
|
||||
export const StatusCell = ({ discount }: DiscountCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const FulfillmentStatusCell = ({
|
||||
],
|
||||
returned: [t("orders.fulfillment.status.returned"), "green"],
|
||||
canceled: [t("orders.fulfillment.status.canceled"), "red"],
|
||||
requires_action: [t("orders.fulfillment.status.requresAction"), "orange"],
|
||||
requires_action: [t("orders.fulfillment.status.requiresAction"), "orange"],
|
||||
}[status] as [string, "red" | "orange" | "green"]
|
||||
|
||||
return <StatusCell color={color}>{label}</StatusCell>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => {
|
||||
"orange",
|
||||
],
|
||||
canceled: [t("orders.payment.status.canceled"), "red"],
|
||||
requires_action: [t("orders.payment.status.requresAction"), "orange"],
|
||||
requires_action: [t("orders.payment.status.requiresAction"), "orange"],
|
||||
}[status] as [string, "red" | "orange" | "green"]
|
||||
|
||||
return <StatusCell color={color}>{label}</StatusCell>
|
||||
|
||||
@@ -83,7 +83,7 @@ export const useOrderTableFilters = (): Filter[] => {
|
||||
value: "canceled",
|
||||
},
|
||||
{
|
||||
label: t("orders.payment.status.requresAction"),
|
||||
label: t("orders.payment.status.requiresAction"),
|
||||
value: "requires_action",
|
||||
},
|
||||
],
|
||||
@@ -128,7 +128,7 @@ export const useOrderTableFilters = (): Filter[] => {
|
||||
value: "canceled",
|
||||
},
|
||||
{
|
||||
label: t("orders.fulfillment.status.requresAction"),
|
||||
label: t("orders.fulfillment.status.requiresAction"),
|
||||
value: "requires_action",
|
||||
},
|
||||
],
|
||||
|
||||
16
packages/admin-next/dashboard/src/lib/common.ts
Normal file
16
packages/admin-next/dashboard/src/lib/common.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Pick properties from an object and copy them to a new object
|
||||
* @param obj
|
||||
* @param keys
|
||||
*/
|
||||
export function pick(obj: Record<string, any>, keys: string[]) {
|
||||
const ret: Record<string, any> = {}
|
||||
|
||||
keys.forEach((k) => {
|
||||
if (k in obj) {
|
||||
ret[k] = obj[k]
|
||||
}
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -728,3 +728,7 @@ export const currencies: Record<string, CurrencyInfo> = {
|
||||
decimal_digits: 0,
|
||||
},
|
||||
}
|
||||
|
||||
export function getCurrencySymbol(code: string) {
|
||||
return currencies[code.toUpperCase()].symbol_native
|
||||
}
|
||||
|
||||
32
packages/admin-next/dashboard/src/lib/discounts.ts
Normal file
32
packages/admin-next/dashboard/src/lib/discounts.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import { end, parse } from "iso8601-duration"
|
||||
|
||||
export enum PromotionStatus {
|
||||
SCHEDULED = "SCHEDULED",
|
||||
EXPIRED = "EXPIRED",
|
||||
ACTIVE = "ACTIVE",
|
||||
DISABLED = "DISABLED",
|
||||
}
|
||||
|
||||
export const getDiscountStatus = (discount: Discount) => {
|
||||
if (discount.is_disabled) {
|
||||
return PromotionStatus.DISABLED
|
||||
}
|
||||
|
||||
const date = new Date()
|
||||
if (new Date(discount.starts_at) > date) {
|
||||
return PromotionStatus.SCHEDULED
|
||||
}
|
||||
|
||||
if (
|
||||
(discount.ends_at && new Date(discount.ends_at) < date) ||
|
||||
(discount.valid_duration &&
|
||||
date >
|
||||
end(parse(discount.valid_duration), new Date(discount.starts_at))) ||
|
||||
discount.usage_count === discount.usage_limit
|
||||
) {
|
||||
return PromotionStatus.EXPIRED
|
||||
}
|
||||
|
||||
return PromotionStatus.ACTIVE
|
||||
}
|
||||
@@ -352,9 +352,24 @@ const router = createBrowserRouter([
|
||||
index: true,
|
||||
lazy: () => import("../../routes/discounts/list"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
lazy: () => import("../../routes/discounts/create"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/discounts/details"),
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/discounts/edit-details"),
|
||||
},
|
||||
{
|
||||
path: "configuration",
|
||||
lazy: () =>
|
||||
import("../../routes/discounts/edit-configuration"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Heading, Input, Text } from "@medusajs/ui"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { CreateDiscountFormReturn } from "./create-discount-form.tsx"
|
||||
|
||||
type CreateDiscountPropsProps = {
|
||||
form: CreateDiscountFormReturn
|
||||
}
|
||||
|
||||
export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
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>
|
||||
</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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.percentage")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
i18nKey="discounts.titleHint"
|
||||
t={t}
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as zod from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { UseFormReturn, useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Button } from "@medusajs/ui"
|
||||
import { useAdminCreateDiscount } from "medusa-react"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { CreateDiscountDetails } from "./create-discount-details.tsx"
|
||||
|
||||
const CreateDiscountSchema = zod.object({
|
||||
code: zod.string(),
|
||||
})
|
||||
|
||||
type Schema = zod.infer<typeof CreateDiscountSchema>
|
||||
export type CreateDiscountFormReturn = UseFormReturn<Schema>
|
||||
|
||||
export const CreateDiscountForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
code: "",
|
||||
},
|
||||
resolver: zodResolver(CreateDiscountSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateDiscount()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
code: values.code,
|
||||
},
|
||||
{
|
||||
onSuccess: ({ discount }) => {
|
||||
handleSuccess(`../${discount.id}`)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="flex h-full w-full">
|
||||
<CreateDiscountDetails form={form} />
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-discount-form.tsx"
|
||||
@@ -0,0 +1,10 @@
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateDiscountForm } from "./components/create-discount-form"
|
||||
|
||||
export const DiscountCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateDiscountForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DiscountCreate as Component } from "./discount-create"
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import type { Discount } from "@medusajs/medusa"
|
||||
import { Container, Copy, Heading, Text } from "@medusajs/ui"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||
|
||||
export const DetailsSection = ({ discount }: { discount: Discount }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("general.details")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: "edit",
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{t("fields.code")}
|
||||
</Text>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="text-ui-fg-base text-pretty"
|
||||
>
|
||||
{discount.code}
|
||||
</Text>
|
||||
<Copy content={discount.code} variant="mini" />
|
||||
</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("fields.type")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{discount.rule.type === "percentage"
|
||||
? t("discounts.percentageDiscount")
|
||||
: discount.rule.type === "free_shipping"
|
||||
? t("discounts.freeShipping")
|
||||
: t("discounts.fixedDiscount")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{discount.rule.type === "percentage" && (
|
||||
<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.value")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{discount.rule.value}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{discount.rule.type === "fixed" && (
|
||||
<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.amount")}
|
||||
</Text>
|
||||
<Text size="small" className="text-pretty">
|
||||
<MoneyAmountCell
|
||||
currencyCode={discount.regions[0]?.currency_code}
|
||||
amount={discount.rule.value}
|
||||
/>
|
||||
</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("discounts.validRegions")}
|
||||
</Text>
|
||||
<Text size="small" className="text-pretty">
|
||||
<ListSummary list={discount.regions.map((r) => r.name)} />
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./details-section.tsx"
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { Discount, DiscountCondition } from "@medusajs/medusa"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
|
||||
type ConditionTypeProps = {
|
||||
condition: DiscountCondition
|
||||
}
|
||||
|
||||
const N = 2
|
||||
|
||||
function ConditionType({ condition }: ConditionTypeProps) {
|
||||
const operator = condition.operator === "in" ? "including" : "excluding"
|
||||
const entity = condition.type
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-borders-base flex flex-row justify-around rounded-md p-2">
|
||||
<span className="text-ui-fg-subtle txt-small">
|
||||
<Trans
|
||||
count={
|
||||
condition[entity].length > N
|
||||
? N - condition[entity].length
|
||||
: condition[entity].length
|
||||
}
|
||||
i18nKey={`discounts.conditions.${operator}.${entity}`}
|
||||
components={[
|
||||
<span
|
||||
key="discounts-incl"
|
||||
className="bg-ui-tag-neutral-bg mx-1 rounded-md border p-1"
|
||||
>
|
||||
<ListSummary
|
||||
inline
|
||||
n={N}
|
||||
list={condition[entity].map(
|
||||
(p) => p.title || p.name || p.value
|
||||
)}
|
||||
/>
|
||||
</span>,
|
||||
<span
|
||||
key="discounts-excl"
|
||||
className="bg-ui-tag-neutral-bg mx-1 rounded-md border p-1"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type DiscountConditionsSectionProps = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
export const DiscountConditionsSection = ({
|
||||
discount,
|
||||
}: DiscountConditionsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const conditions = useMemo(
|
||||
() =>
|
||||
discount.rule.conditions.sort((c1, c2) => c1.type.localeCompare(c2.type)),
|
||||
[discount]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className="p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<Heading>{t("fields.conditions")}</Heading>
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `conditions`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-2 px-6 pb-4 pt-2">
|
||||
{!conditions.length && (
|
||||
<NoRecords
|
||||
className="h-[180px]"
|
||||
action={{ label: t("discounts.editConditions"), to: "conditions" }}
|
||||
/>
|
||||
)}
|
||||
{conditions.map((condition) => (
|
||||
<ConditionType key={condition.id} condition={condition} />
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./discount-conditions-section"
|
||||
@@ -0,0 +1,89 @@
|
||||
import { parse } from "iso8601-duration"
|
||||
import { format, formatDuration } from "date-fns"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import { Container, Heading, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
type DiscountConfigurationsSection = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
function formatTime(dateTime?: string) {
|
||||
if (!dateTime) {
|
||||
return
|
||||
}
|
||||
return format(new Date(dateTime), "dd MMM, yyyy, HH:mm:ss")
|
||||
}
|
||||
|
||||
export const DiscountConfigurationSection = ({
|
||||
discount,
|
||||
}: DiscountConfigurationsSection) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const duration = useMemo(() => {
|
||||
if (!discount.valid_duration) {
|
||||
return "-"
|
||||
}
|
||||
return formatDuration(parse(discount.valid_duration))
|
||||
}, [discount.valid_duration])
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<Heading>{t("fields.configurations")}</Heading>
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `configuration`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</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("discounts.startDate")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{formatTime(discount.starts_at as unknown as string)}
|
||||
</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("discounts.endDate")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{formatTime(discount.ends_at as unknown as string)}
|
||||
</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("discounts.redemptionsLimit")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{discount.usage_limit || "-"}
|
||||
</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("discounts.validDuration")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{duration}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./discount-configurations-section.tsx"
|
||||
@@ -0,0 +1,109 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui"
|
||||
import { useAdminDeleteDiscount } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
getDiscountStatus,
|
||||
PromotionStatus,
|
||||
} from "../../../../../lib/discounts"
|
||||
|
||||
type DiscountGeneralSectionProps = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
export const DiscountGeneralSection = ({
|
||||
discount,
|
||||
}: DiscountGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteDiscount(discount.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirm = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("discounts.deleteWarning", {
|
||||
code: discount.code,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: discount.code,
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate("/discounts", { replace: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const [color, text] = {
|
||||
[PromotionStatus.DISABLED]: [
|
||||
"grey",
|
||||
t("discounts.discountStatus.disabled"),
|
||||
],
|
||||
[PromotionStatus.ACTIVE]: ["green", t("discounts.discountStatus.active")],
|
||||
[PromotionStatus.SCHEDULED]: [
|
||||
"orange",
|
||||
t("discounts.discountStatus.scheduled"),
|
||||
],
|
||||
[PromotionStatus.EXPIRED]: ["red", t("discounts.discountStatus.expired")],
|
||||
}[getDiscountStatus(discount)] 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>{discount.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: `/discounts/${discount.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("fields.description")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{discount.rule.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./discount-general-section.tsx"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./redemptions-section.tsx"
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Gift } from "@medusajs/icons"
|
||||
import { Container, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const RedemptionsSection = ({
|
||||
redemptions,
|
||||
}: {
|
||||
redemptions: number
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="flex flex-col gap-y-6 px-6 py-4">
|
||||
<div className="grid grid-cols-[28px_1fr] items-center gap-x-3">
|
||||
<div className="bg-ui-bg-base shadow-borders-base flex size-7 items-center justify-center rounded-md">
|
||||
<div className="bg-ui-bg-component flex size-6 items-center justify-center rounded-[4px]">
|
||||
<Gift className="text-ui-fg-subtle" />
|
||||
</div>
|
||||
</div>
|
||||
<Text
|
||||
className="text-ui-fg-subtle"
|
||||
weight="plus"
|
||||
size="small"
|
||||
leading="compact"
|
||||
>
|
||||
{t("fields.totalRedemptions")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="border-ui-fg-muted border-l-2 pl-2">
|
||||
<Text
|
||||
className="text-ui-fg-base leading-[100%]"
|
||||
weight="plus"
|
||||
size="xlarge"
|
||||
>
|
||||
{redemptions}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const DiscountsDetails = () => {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Heading>Discounts</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useAdminDiscount } from "medusa-react"
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { DiscountGeneralSection } from "./components/discounts-general-section"
|
||||
import { DiscountConfigurationSection } from "./components/discounts-configurations-section"
|
||||
|
||||
import { discountLoader, expand } from "./loader"
|
||||
import { RedemptionsSection } from "./components/redemptions-section"
|
||||
import { DetailsSection } from "./components/details-section"
|
||||
import { DiscountConditionsSection } from "./components/discounts-conditions-section"
|
||||
|
||||
import before from "medusa-admin:widgets/discount/details/before"
|
||||
import after from "medusa-admin:widgets/discount/details/after"
|
||||
|
||||
export const DiscountDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof discountLoader>
|
||||
>
|
||||
|
||||
const { id } = useParams()
|
||||
const { discount, isLoading } = useAdminDiscount(
|
||||
id!,
|
||||
{ expand },
|
||||
{
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading || !discount) {
|
||||
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="grid grid-cols-1 gap-x-4 lg:grid-cols-[1fr,400px]">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DiscountGeneralSection discount={discount} />
|
||||
<DiscountConfigurationSection discount={discount} />
|
||||
<DiscountConditionsSection discount={discount} />
|
||||
<div className="flex flex-col gap-y-2 lg:hidden">
|
||||
<RedemptionsSection redemptions={discount.usage_count} />
|
||||
<DetailsSection discount={discount} />
|
||||
</div>
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<JsonViewSection data={discount} />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-y-2 lg:flex">
|
||||
<RedemptionsSection redemptions={discount.usage_count} />
|
||||
<DetailsSection discount={discount} />
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { DiscountsDetails as Component } from "./details";
|
||||
export { discountLoader as loader } from "./loader"
|
||||
export { DiscountDetail as Component } from "./discount-detail.tsx"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { AdminDiscountsRes } from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { adminDiscountKeys } from "medusa-react"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { medusa, queryClient } from "../../../lib/medusa"
|
||||
|
||||
export const expand =
|
||||
"regions," +
|
||||
"rule.conditions.products," +
|
||||
"rule.conditions.product_types," +
|
||||
"rule.conditions.product_tags," +
|
||||
"rule.conditions.product_collections," +
|
||||
"rule.conditions.customer_groups"
|
||||
|
||||
const discountDetailQuery = (id: string) => ({
|
||||
queryKey: adminDiscountKeys.detail(id),
|
||||
queryFn: async () =>
|
||||
medusa.admin.discounts.retrieve(id, {
|
||||
expand,
|
||||
}),
|
||||
})
|
||||
|
||||
export const discountLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = discountDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminDiscountsRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
import { useMemo } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { parse, Duration } from "iso8601-duration"
|
||||
import { formatISODuration } from "date-fns"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import { Button, Input, Text, Switch, DatePicker } from "@medusajs/ui"
|
||||
import { useAdminUpdateDiscount } from "medusa-react"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { pick } from "../../../../../lib/common"
|
||||
|
||||
type EditDiscountFormProps = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
const EditDiscountSchema = zod.object({
|
||||
start_date_enabled: zod.boolean(),
|
||||
start_date: zod.date(),
|
||||
|
||||
end_date_enabled: zod.boolean(),
|
||||
end_date: zod.date().nullish(),
|
||||
|
||||
enable_usage_limit: zod.boolean(),
|
||||
usage_limit: zod.number().nullish(),
|
||||
|
||||
enable_duration: zod.boolean(),
|
||||
|
||||
years: zod.number().optional(),
|
||||
months: zod.number().optional(),
|
||||
days: zod.number().optional(),
|
||||
hours: zod.number().optional(),
|
||||
minutes: zod.number().optional(),
|
||||
})
|
||||
|
||||
export const EditDiscountConfigurationForm = ({
|
||||
discount,
|
||||
}: EditDiscountFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const duration = useMemo(
|
||||
() =>
|
||||
discount.valid_duration
|
||||
? parse(discount.valid_duration)
|
||||
: ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration),
|
||||
[discount]
|
||||
)
|
||||
|
||||
const form = useForm<zod.infer<typeof EditDiscountSchema>>({
|
||||
defaultValues: {
|
||||
start_date_enabled: !!discount.starts_at,
|
||||
start_date: new Date(discount.starts_at),
|
||||
|
||||
enable_usage_limit: !!discount.usage_limit,
|
||||
usage_limit: discount.usage_limit,
|
||||
|
||||
enable_duration: !!discount.valid_duration,
|
||||
|
||||
end_date_enabled: !!discount.ends_at,
|
||||
end_date: discount.ends_at ? new Date(discount.ends_at) : null,
|
||||
|
||||
years: duration.years,
|
||||
months: duration.months,
|
||||
days: duration.days,
|
||||
hours: duration.hours,
|
||||
minutes: duration.minutes,
|
||||
},
|
||||
resolver: zodResolver(EditDiscountSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateDiscount(discount.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
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,
|
||||
},
|
||||
{
|
||||
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">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
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={() => (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label tooltip="todo">
|
||||
{t("discounts.hasStartDate")}
|
||||
</Form.Label>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("discounts.startDateHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="start_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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</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.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.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="enable_usage_limit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label tooltip="todo">
|
||||
{t("discounts.hasUsageLimit")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={!!field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("discounts.usageLimitHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<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
|
||||
|
||||
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">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="enable_duration"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label tooltip="todo">
|
||||
{t("discounts.hasDurationLimit")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={!!field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("discounts.durationHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
|
||||
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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
<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={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-discount-configuration-form"
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useAdminDiscount } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { EditDiscountConfigurationForm } from "./components/edit-discount-form"
|
||||
|
||||
export const DiscountEditConfiguration = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { discount, isLoading, isError, error } = useAdminDiscount(id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("discounts.editDiscountConfiguration")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && discount && (
|
||||
<EditDiscountConfigurationForm discount={discount} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DiscountEditConfiguration as Component } from "./discount-edit-configuration"
|
||||
@@ -0,0 +1,237 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import {
|
||||
CurrencyInput,
|
||||
Button,
|
||||
Input,
|
||||
Text,
|
||||
Textarea,
|
||||
Select,
|
||||
} from "@medusajs/ui"
|
||||
import { useAdminRegions, useAdminUpdateDiscount } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import {
|
||||
getDbAmount,
|
||||
getPresentationalAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||
import { Combobox } from "../../../../../components/common/combobox"
|
||||
|
||||
type EditDiscountFormProps = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
const EditDiscountSchema = zod.object({
|
||||
code: zod.string().min(1),
|
||||
description: zod.string(),
|
||||
value: zod.number(),
|
||||
regions: zod.array(zod.string()),
|
||||
})
|
||||
|
||||
export const EditDiscountDetailsForm = ({
|
||||
discount,
|
||||
}: EditDiscountFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const { regions } = useAdminRegions()
|
||||
|
||||
const isFixedDiscount = discount.rule.type === "fixed"
|
||||
const isFreeShipping = discount.rule.type === "free_shipping"
|
||||
|
||||
const form = useForm<zod.infer<typeof EditDiscountSchema>>({
|
||||
defaultValues: {
|
||||
code: discount.code,
|
||||
description: discount.rule.description || "",
|
||||
regions: discount.regions.map((r) => r.id),
|
||||
value: isFixedDiscount
|
||||
? getPresentationalAmount(
|
||||
discount.rule.value,
|
||||
discount.regions[0].currency_code
|
||||
)
|
||||
: discount.rule.value,
|
||||
},
|
||||
resolver: zodResolver(EditDiscountSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateDiscount(discount.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
code: data.code,
|
||||
regions: data.regions,
|
||||
rule: {
|
||||
id: discount.rule.id,
|
||||
description: data.description,
|
||||
value: isFixedDiscount
|
||||
? getDbAmount(data.value, discount.regions[0].currency_code)
|
||||
: (data.value as number),
|
||||
},
|
||||
},
|
||||
{
|
||||
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">
|
||||
<div className="flex flex-col gap-y-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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="discounts.titleHint"
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<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) => 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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isFreeShipping && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{isFixedDiscount
|
||||
? t("fields.amount")
|
||||
: t("fields.percentage")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
{isFixedDiscount ? (
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
onValueChange={onChange}
|
||||
code={discount.regions[0].currency_code}
|
||||
symbol={getCurrencySymbol(
|
||||
discount.regions[0].currency_code
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</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={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-discount-details-form"
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useAdminDiscount } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { EditDiscountDetailsForm } from "./components/edit-discount-form"
|
||||
|
||||
export const DiscountEditDetails = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { discount, isLoading, isError, error } = useAdminDiscount(id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("discounts.editDiscountDetails")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && discount && (
|
||||
<EditDiscountDetailsForm discount={discount} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DiscountEditDetails as Component } from "./discount-edit-details"
|
||||
@@ -86,7 +86,7 @@ const DiscountActions = ({ discount }: { discount: Discount }) => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("discounts.deleteWarning", {
|
||||
title: discount.code,
|
||||
code: discount.code,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
|
||||
@@ -83,7 +83,7 @@ export const ProductOrganizationSection = ({
|
||||
{t("fields.categories")}
|
||||
</Text>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{product.categories.length > 0
|
||||
{product.categories?.length > 0
|
||||
? product.categories.map((pcat) => (
|
||||
<Badge
|
||||
key={pcat.id}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AdminDiscountsDeleteRes,
|
||||
AdminDiscountsListRes,
|
||||
AdminDiscountsRes,
|
||||
AdminGetDiscountParams,
|
||||
AdminGetDiscountsDiscountConditionsConditionParams,
|
||||
AdminGetDiscountsParams,
|
||||
AdminPostDiscountsDiscountConditions,
|
||||
@@ -23,12 +24,12 @@ import BaseResource from "../base"
|
||||
/**
|
||||
* This class is used to send requests to [Admin Discount API Routes](https://docs.medusajs.com/api/admin#discounts). All its method
|
||||
* are available in the JS Client under the `medusa.admin.discounts` property.
|
||||
*
|
||||
*
|
||||
* All methods in this class require {@link AdminAuthResource.createSession | user authentication}.
|
||||
*
|
||||
*
|
||||
* Admins can create discounts with conditions and rules, providing them with advanced settings for variety of cases.
|
||||
* The methods in this class can be used to manage discounts, their conditions, resources, and more.
|
||||
*
|
||||
*
|
||||
* Related Guide: [How to manage discounts](https://docs.medusajs.com/modules/discounts/admin/manage-discounts).
|
||||
*/
|
||||
class AdminDiscountsResource extends BaseResource {
|
||||
@@ -38,7 +39,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {string} regionId - The ID of the region to add.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the discount's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -62,7 +63,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminPostDiscountsReq} payload - The discount to create.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the discount's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* import { AllocationType, DiscountRuleType } from "@medusajs/medusa"
|
||||
@@ -97,7 +98,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminPostDiscountsDiscountReq} payload - The attributes to update in the discount.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -124,7 +125,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminPostDiscountsDiscountDynamicCodesReq} payload - The dynamic code to create.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -151,7 +152,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {string} id - The discount's ID.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsDeleteRes>} Resolves to the delete operation details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -175,7 +176,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {string} code - The code of the dynamic code to delete.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -197,9 +198,10 @@ class AdminDiscountsResource extends BaseResource {
|
||||
/**
|
||||
* Retrieve a discount.
|
||||
* @param {string} id - The discount's ID.
|
||||
* @param {AdminGetDiscountParams} query - Configurations to apply on the retrieved product category.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -211,9 +213,16 @@ class AdminDiscountsResource extends BaseResource {
|
||||
*/
|
||||
retrieve(
|
||||
id: string,
|
||||
query?: AdminGetDiscountParams,
|
||||
customHeaders: Record<string, any> = {}
|
||||
): ResponsePromise<AdminDiscountsRes> {
|
||||
const path = `/admin/discounts/${id}`
|
||||
let path = `/admin/discounts/${id}`
|
||||
|
||||
if (query) {
|
||||
const queryString = qs.stringify(query)
|
||||
path = `${path}?${queryString}`
|
||||
}
|
||||
|
||||
return this.client.request("GET", path, undefined, {}, customHeaders)
|
||||
}
|
||||
|
||||
@@ -222,7 +231,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {string} code - The code of the discount.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -245,10 +254,10 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminGetDiscountsParams} query - Filters and pagination configurations to apply on the retrieved discounts.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsListRes>} Resolves to the list of discounts with pagination fields.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To list discounts:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -258,9 +267,9 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* console.log(discounts.id);
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify relations that should be retrieved within the discounts:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -272,9 +281,9 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* console.log(discounts.id);
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* By default, only the first `20` records are retrieved. You can control pagination by specifying the `limit` and `offset` properties:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -309,7 +318,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {string} regionId - The ID of the region to remove.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -329,17 +338,17 @@ class AdminDiscountsResource extends BaseResource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a discount condition. Only one of `products`, `product_types`, `product_collections`, `product_tags`, and `customer_groups` should be provided in the `payload` parameter,
|
||||
* Create a discount condition. Only one of `products`, `product_types`, `product_collections`, `product_tags`, and `customer_groups` should be provided in the `payload` parameter,
|
||||
* based on the type of discount condition. For example, if the discount condition's type is `products`, the `products` field should be provided in the `payload` parameter.
|
||||
* @param {string} discountId - The discount's ID.
|
||||
* @param {AdminPostDiscountsDiscountConditions} payload - The discount condition to create.
|
||||
* @param {AdminPostDiscountsDiscountConditionsParams} query - Configurations to apply on the returned discount.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To create a condition in a discount:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* import { DiscountConditionOperator } from "@medusajs/medusa"
|
||||
@@ -353,9 +362,9 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* console.log(discount.id);
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify relations that should be retrieved as part of the response:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* import { DiscountConditionOperator } from "@medusajs/medusa"
|
||||
@@ -397,10 +406,10 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminPostDiscountsDiscountConditionsConditionParams} query - Configurations to apply on the returned discount.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To update a condition in a discount:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -414,9 +423,9 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* console.log(discount.id);
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify relations that should be retrieved as part of the response:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -456,7 +465,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {string} conditionId - The ID of the discount condition.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsDeleteRes>} Resolves to the deletion operation details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -482,10 +491,10 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminGetDiscountsDiscountConditionsConditionParams} query - Configurations to apply on the retrieved discount condition.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountConditionsRes>} Resolves to the discount condition details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* A simple example that retrieves a discount condition by its ID:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -495,9 +504,9 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* console.log(discount_condition.id);
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify relations that should be retrieved:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -535,10 +544,10 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminPostDiscountsDiscountConditionsConditionBatchParams} query - Configurations to apply on the retrieved discount.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To add resources to a discount condition:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -550,9 +559,9 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* console.log(discount.id);
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify relations to include in the returned discount:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
@@ -591,7 +600,7 @@ class AdminDiscountsResource extends BaseResource {
|
||||
* @param {AdminDeleteDiscountsDiscountConditionsConditionBatchReq} payload - The resources to remove.
|
||||
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
|
||||
* @returns {ResponsePromise<AdminDiscountsRes>} Resolves to the details of the discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
|
||||
@@ -28,19 +28,19 @@ export const adminDiscountKeys = {
|
||||
type DiscountQueryKeys = typeof adminDiscountKeys
|
||||
|
||||
/**
|
||||
* This hook retrieves a list of Discounts. The discounts can be filtered by fields such as `rule` or `is_dynamic`.
|
||||
* This hook retrieves a list of Discounts. The discounts can be filtered by fields such as `rule` or `is_dynamic`.
|
||||
* The discounts can also be paginated.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To list discounts:
|
||||
*
|
||||
*
|
||||
* ```tsx
|
||||
* import React from "react"
|
||||
* import { useAdminDiscounts } from "medusa-react"
|
||||
*
|
||||
*
|
||||
* const Discounts = () => {
|
||||
* const { discounts, isLoading } = useAdminDiscounts()
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isLoading && <span>Loading...</span>}
|
||||
@@ -57,21 +57,21 @@ type DiscountQueryKeys = typeof adminDiscountKeys
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
*
|
||||
*
|
||||
* export default Discounts
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify relations that should be retrieved within the discounts:
|
||||
*
|
||||
*
|
||||
* ```tsx
|
||||
* import React from "react"
|
||||
* import { useAdminDiscounts } from "medusa-react"
|
||||
*
|
||||
*
|
||||
* const Discounts = () => {
|
||||
* const { discounts, isLoading } = useAdminDiscounts({
|
||||
* expand: "rule"
|
||||
* })
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isLoading && <span>Loading...</span>}
|
||||
@@ -88,19 +88,19 @@ type DiscountQueryKeys = typeof adminDiscountKeys
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
*
|
||||
*
|
||||
* export default Discounts
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* By default, only the first `20` records are retrieved. You can control pagination by specifying the `limit` and `offset` properties:
|
||||
*
|
||||
*
|
||||
* ```tsx
|
||||
* import React from "react"
|
||||
* import { useAdminDiscounts } from "medusa-react"
|
||||
*
|
||||
*
|
||||
* const Discounts = () => {
|
||||
* const {
|
||||
* discounts,
|
||||
* const {
|
||||
* discounts,
|
||||
* limit,
|
||||
* offset,
|
||||
* isLoading
|
||||
@@ -109,7 +109,7 @@ type DiscountQueryKeys = typeof adminDiscountKeys
|
||||
* limit: 10,
|
||||
* offset: 0
|
||||
* })
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isLoading && <span>Loading...</span>}
|
||||
@@ -126,10 +126,10 @@ type DiscountQueryKeys = typeof adminDiscountKeys
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
*
|
||||
*
|
||||
* export default Discounts
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @customNamespace Hooks.Admin.Discounts
|
||||
* @category Queries
|
||||
*/
|
||||
@@ -155,20 +155,20 @@ export const useAdminDiscounts = (
|
||||
|
||||
/**
|
||||
* This hook retrieves a discount.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import React from "react"
|
||||
* import { useAdminDiscount } from "medusa-react"
|
||||
*
|
||||
*
|
||||
* type Props = {
|
||||
* discountId: string
|
||||
* }
|
||||
*
|
||||
*
|
||||
* const Discount = ({ discountId }: Props) => {
|
||||
* const { discount, isLoading } = useAdminDiscount(
|
||||
* discountId
|
||||
* )
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isLoading && <span>Loading...</span>}
|
||||
@@ -176,9 +176,9 @@ export const useAdminDiscounts = (
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
*
|
||||
*
|
||||
* export default Discount
|
||||
*
|
||||
*
|
||||
* @customNamespace Hooks.Admin.Discounts
|
||||
* @category Queries
|
||||
*/
|
||||
@@ -209,20 +209,20 @@ export const useAdminDiscount = (
|
||||
/**
|
||||
* This hook adds a batch of resources to a discount condition. The type of resource depends on the type of discount condition. For example, if the discount condition's type is `products`,
|
||||
* the resources being added should be products.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import React from "react"
|
||||
* import { useAdminGetDiscountByCode } from "medusa-react"
|
||||
*
|
||||
*
|
||||
* type Props = {
|
||||
* discountCode: string
|
||||
* }
|
||||
*
|
||||
*
|
||||
* const Discount = ({ discountCode }: Props) => {
|
||||
* const { discount, isLoading } = useAdminGetDiscountByCode(
|
||||
* discountCode
|
||||
* )
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isLoading && <span>Loading...</span>}
|
||||
@@ -230,9 +230,9 @@ export const useAdminDiscount = (
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
*
|
||||
*
|
||||
* export default Discount
|
||||
*
|
||||
*
|
||||
* @customNamespace Hooks.Admin.Discounts
|
||||
* @category Queries
|
||||
*/
|
||||
@@ -258,28 +258,28 @@ export const useAdminGetDiscountByCode = (
|
||||
|
||||
/**
|
||||
* This hook retries a Discount Condition's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import React from "react"
|
||||
* import { useAdminGetDiscountCondition } from "medusa-react"
|
||||
*
|
||||
*
|
||||
* type Props = {
|
||||
* discountId: string
|
||||
* discountConditionId: string
|
||||
* }
|
||||
*
|
||||
*
|
||||
* const DiscountCondition = ({
|
||||
* discountId,
|
||||
* discountConditionId
|
||||
* }: Props) => {
|
||||
* const {
|
||||
* discount_condition,
|
||||
* const {
|
||||
* discount_condition,
|
||||
* isLoading
|
||||
* } = useAdminGetDiscountCondition(
|
||||
* discountId,
|
||||
* discountConditionId
|
||||
* )
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isLoading && <span>Loading...</span>}
|
||||
@@ -289,9 +289,9 @@ export const useAdminGetDiscountByCode = (
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
*
|
||||
*
|
||||
* export default DiscountCondition
|
||||
*
|
||||
*
|
||||
* @customNamespace Hooks.Admin.Discounts
|
||||
* @category Queries
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user