From fcb03d60ea922f413a2879b70802549098780812 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Frane=20Poli=C4=87?=
<16856471+fPolic@users.noreply.github.com>
Date: Wed, 6 Mar 2024 15:08:15 +0100
Subject: [PATCH] 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
---
.../public/locales/en-US/translation.json | 79 ++-
.../empty-table-content.tsx | 2 +-
.../components/common/list-summary/index.ts | 1 +
.../common/list-summary/list-summary.tsx | 58 +++
.../discount/status-cell/status-cell.tsx | 35 +-
.../fulfillment-status-cell.tsx | 2 +-
.../payment-status-cell.tsx | 2 +-
.../table/filters/use-order-table-filters.tsx | 4 +-
.../admin-next/dashboard/src/lib/common.ts | 16 +
.../dashboard/src/lib/currencies.ts | 4 +
.../admin-next/dashboard/src/lib/discounts.ts | 32 ++
.../router-provider/router-provider.tsx | 15 +
.../create-discount-details.tsx | 73 +++
.../create-discount-form.tsx | 73 +++
.../components/create-discount-form/index.ts | 1 +
.../discounts/create/discount-create.tsx | 10 +
.../src/routes/discounts/create/index.ts | 1 +
.../details-section/details-section.tsx | 97 ++++
.../components/details-section/index.ts | 1 +
.../discount-conditions-section.tsx | 103 ++++
.../discounts-conditions-section/index.ts | 1 +
.../discount-configurations-section.tsx | 89 ++++
.../discounts-configurations-section/index.ts | 1 +
.../discount-general-section.tsx | 109 ++++
.../discounts-general-section/index.ts | 1 +
.../components/redemptions-section/index.ts | 1 +
.../redemptions-section.tsx | 40 ++
.../src/routes/discounts/details/details.tsx | 11 -
.../discounts/details/discount-detail.tsx | 69 +++
.../src/routes/discounts/details/index.ts | 3 +-
.../src/routes/discounts/details/loader.ts | 32 ++
.../edit-discount-configuration-form.tsx | 482 ++++++++++++++++++
.../components/edit-discount-form/index.ts | 1 +
.../discount-edit-configuration.tsx | 29 ++
.../discounts/edit-configuration/index.ts | 1 +
.../edit-discount-details-form.tsx | 237 +++++++++
.../components/edit-discount-form/index.ts | 1 +
.../edit-details/discount-edit-details.tsx | 29 ++
.../routes/discounts/edit-details/index.ts | 1 +
.../discount-list-table.tsx | 2 +-
.../product-organization-section.tsx | 2 +-
.../src/resources/admin/discounts.ts | 85 +--
.../src/hooks/admin/discounts/queries.ts | 78 +--
43 files changed, 1783 insertions(+), 131 deletions(-)
create mode 100644 packages/admin-next/dashboard/src/components/common/list-summary/index.ts
create mode 100644 packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx
create mode 100644 packages/admin-next/dashboard/src/lib/common.ts
create mode 100644 packages/admin-next/dashboard/src/lib/discounts.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-form.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/create/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/details-section.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-section.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-section.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/redemptions-section.tsx
delete mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/details.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/details/loader.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/index.ts
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-details/discount-edit-details.tsx
create mode 100644 packages/admin-next/dashboard/src/routes/discounts/edit-details/index.ts
diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index e50fcc654b..03ce6c3021 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -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>all1> products except <0/>.",
+ "customer_groups": "Discount applies to <1>all1> customer groups except <0/>.",
+ "product_tags": "Discount applies to <1>all1> product tags except <0/>.",
+ "product_collections": "Discount applies to <1>all1> product collections except <0/>.",
+ "product_types": "Discount applies to <1>all1> 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"
}
}
diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx
index 320df80867..76a50895c0 100644
--- a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx
+++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx
@@ -62,7 +62,7 @@ export const NoRecords = ({
{title ?? t("general.noRecordsTitle")}
-
+
{message ?? t("general.noRecordsMessage")}
diff --git a/packages/admin-next/dashboard/src/components/common/list-summary/index.ts b/packages/admin-next/dashboard/src/components/common/list-summary/index.ts
new file mode 100644
index 0000000000..0fbd1961ee
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/common/list-summary/index.ts
@@ -0,0 +1 @@
+export { ListSummary } from "./list-summary"
diff --git a/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx b/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx
new file mode 100644
index 0000000000..57d79598b7
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx
@@ -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 (
+
+
+ {list.slice(0, n).join(", ")}
+
+ {list.length > n && (
+
+ {list.slice(n).map((c) => (
+ {c}
+ ))}
+
+ }
+ >
+
+ {t("general.plusCountMore", {
+ count: list.length - n,
+ })}
+
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/discount/status-cell/status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/discount/status-cell/status-cell.tsx
index 61a0362090..6d841f0ccf 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/discount/status-cell/status-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/discount/status-cell/status-cell.tsx
@@ -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()
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx
index 71add7608c..f3de326ebb 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx
@@ -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 {label}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx
index 01eef86a8d..5b95a9aebf 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx
@@ -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 {label}
diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx
index 0c2b6348bc..1288ec7b8b 100644
--- a/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx
+++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx
@@ -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",
},
],
diff --git a/packages/admin-next/dashboard/src/lib/common.ts b/packages/admin-next/dashboard/src/lib/common.ts
new file mode 100644
index 0000000000..9b1c4f8b5f
--- /dev/null
+++ b/packages/admin-next/dashboard/src/lib/common.ts
@@ -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, keys: string[]) {
+ const ret: Record = {}
+
+ keys.forEach((k) => {
+ if (k in obj) {
+ ret[k] = obj[k]
+ }
+ })
+
+ return ret
+}
diff --git a/packages/admin-next/dashboard/src/lib/currencies.ts b/packages/admin-next/dashboard/src/lib/currencies.ts
index 5380fc1716..2f28d86f1d 100644
--- a/packages/admin-next/dashboard/src/lib/currencies.ts
+++ b/packages/admin-next/dashboard/src/lib/currencies.ts
@@ -728,3 +728,7 @@ export const currencies: Record = {
decimal_digits: 0,
},
}
+
+export function getCurrencySymbol(code: string) {
+ return currencies[code.toUpperCase()].symbol_native
+}
diff --git a/packages/admin-next/dashboard/src/lib/discounts.ts b/packages/admin-next/dashboard/src/lib/discounts.ts
new file mode 100644
index 0000000000..2fa7856f11
--- /dev/null
+++ b/packages/admin-next/dashboard/src/lib/discounts.ts
@@ -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
+}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
index 9bfa8a61ca..c5424203e1 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
@@ -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"),
+ },
+ ],
},
],
},
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx b/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx
new file mode 100644
index 0000000000..a21c05aed1
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx
@@ -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 (
+
+
+
+ {t("discount.createDiscountTitle")}
+
+ {t("discounts.createDiscountHint")}
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-form.tsx
new file mode 100644
index 0000000000..c93622d141
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-form.tsx
@@ -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
+export type CreateDiscountFormReturn = UseFormReturn
+
+export const CreateDiscountForm = () => {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm({
+ 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 (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/index.ts b/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/index.ts
new file mode 100644
index 0000000000..03d0e18796
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/index.ts
@@ -0,0 +1 @@
+export * from "./create-discount-form.tsx"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx b/packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx
new file mode 100644
index 0000000000..4982466851
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx
@@ -0,0 +1,10 @@
+import { RouteFocusModal } from "../../../components/route-modal"
+import { CreateDiscountForm } from "./components/create-discount-form"
+
+export const DiscountCreate = () => {
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/index.ts b/packages/admin-next/dashboard/src/routes/discounts/create/index.ts
new file mode 100644
index 0000000000..bc78639038
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/create/index.ts
@@ -0,0 +1 @@
+export { DiscountCreate as Component } from "./discount-create"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/details-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/details-section.tsx
new file mode 100644
index 0000000000..6963683b50
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/details-section.tsx
@@ -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 (
+
+
+
{t("general.details")}
+
,
+ },
+ ],
+ },
+ ]}
+ />
+
+
+
+ {t("fields.code")}
+
+
+
+ {discount.code}
+
+
+
+
+
+
+
+ {t("fields.type")}
+
+
+ {discount.rule.type === "percentage"
+ ? t("discounts.percentageDiscount")
+ : discount.rule.type === "free_shipping"
+ ? t("discounts.freeShipping")
+ : t("discounts.fixedDiscount")}
+
+
+
+ {discount.rule.type === "percentage" && (
+
+
+ {t("fields.value")}
+
+
+ {discount.rule.value}
+
+
+ )}
+
+ {discount.rule.type === "fixed" && (
+
+
+ {t("fields.amount")}
+
+
+
+
+
+ )}
+
+
+
+ {t("discounts.validRegions")}
+
+
+ r.name)} />
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts
new file mode 100644
index 0000000000..230293127a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts
@@ -0,0 +1 @@
+export * from "./details-section.tsx"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-section.tsx
new file mode 100644
index 0000000000..32fbc233cc
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-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 (
+
+
+ N
+ ? N - condition[entity].length
+ : condition[entity].length
+ }
+ i18nKey={`discounts.conditions.${operator}.${entity}`}
+ components={[
+
+ p.title || p.name || p.value
+ )}
+ />
+ ,
+ ,
+ ]}
+ />
+
+
+ )
+}
+
+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 (
+
+
+
+ {t("fields.conditions")}
+
+
,
+ label: t("actions.edit"),
+ to: `conditions`,
+ },
+ ],
+ },
+ ]}
+ />
+
+
+ {!conditions.length && (
+
+ )}
+ {conditions.map((condition) => (
+
+ ))}
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/index.ts
new file mode 100644
index 0000000000..f56f629d84
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/index.ts
@@ -0,0 +1 @@
+export * from "./discount-conditions-section"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx
new file mode 100644
index 0000000000..86d3777c3d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx
@@ -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 (
+
+
+
+ {t("fields.configurations")}
+
+
,
+ label: t("actions.edit"),
+ to: `configuration`,
+ },
+ ],
+ },
+ ]}
+ />
+
+
+
+ {t("discounts.startDate")}
+
+
+ {formatTime(discount.starts_at as unknown as string)}
+
+
+
+
+ {t("discounts.endDate")}
+
+
+ {formatTime(discount.ends_at as unknown as string)}
+
+
+
+
+ {t("discounts.redemptionsLimit")}
+
+
+ {discount.usage_limit || "-"}
+
+
+
+
+ {t("discounts.validDuration")}
+
+
+ {duration}
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/index.ts
new file mode 100644
index 0000000000..966f5c59cd
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/index.ts
@@ -0,0 +1 @@
+export * from "./discount-configurations-section.tsx"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-section.tsx
new file mode 100644
index 0000000000..f2ce0e6b39
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-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 (
+
+
+
+ {discount.code}
+
+
+
+
{text}
+
,
+ label: t("actions.edit"),
+ to: `/discounts/${discount.id}/edit`,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ icon:
,
+ label: t("actions.delete"),
+ onClick: handleDelete,
+ },
+ ],
+ },
+ ]}
+ />
+
+
+
+
+ {t("fields.description")}
+
+
+ {discount.rule.description}
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/index.ts
new file mode 100644
index 0000000000..3388dbad5b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/index.ts
@@ -0,0 +1 @@
+export * from "./discount-general-section.tsx"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts
new file mode 100644
index 0000000000..a3c0cc3e0d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts
@@ -0,0 +1 @@
+export * from "./redemptions-section.tsx"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/redemptions-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/redemptions-section.tsx
new file mode 100644
index 0000000000..507512c662
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/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 (
+
+
+
+
+ {t("fields.totalRedemptions")}
+
+
+
+
+ {redemptions}
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/details.tsx b/packages/admin-next/dashboard/src/routes/discounts/details/details.tsx
deleted file mode 100644
index e93087080e..0000000000
--- a/packages/admin-next/dashboard/src/routes/discounts/details/details.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Container, Heading } from "@medusajs/ui";
-
-export const DiscountsDetails = () => {
- return (
-
-
- Discounts
-
-
- );
-};
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx b/packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx
new file mode 100644
index 0000000000..f2bedf3542
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx
@@ -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
+ >
+
+ const { id } = useParams()
+ const { discount, isLoading } = useAdminDiscount(
+ id!,
+ { expand },
+ {
+ initialData,
+ }
+ )
+
+ if (isLoading || !discount) {
+ return Loading...
+ }
+
+ return (
+
+ {before.widgets.map((w, i) => {
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+ {after.widgets.map((w, i) => {
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/index.ts b/packages/admin-next/dashboard/src/routes/discounts/details/index.ts
index a189be735d..2debe10554 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/details/index.ts
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/index.ts
@@ -1 +1,2 @@
-export { DiscountsDetails as Component } from "./details";
+export { discountLoader as loader } from "./loader"
+export { DiscountDetail as Component } from "./discount-detail.tsx"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/loader.ts b/packages/admin-next/dashboard/src/routes/discounts/details/loader.ts
new file mode 100644
index 0000000000..bb6198cb4c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/details/loader.ts
@@ -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>(query.queryKey) ??
+ (await queryClient.fetchQuery(query))
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
new file mode 100644
index 0000000000..800a4696ca
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
@@ -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>({
+ 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 (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/index.ts b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/index.ts
new file mode 100644
index 0000000000..416db6fd44
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-discount-configuration-form"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx
new file mode 100644
index 0000000000..7344b0dc37
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx
@@ -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 (
+
+
+ {t("discounts.editDiscountConfiguration")}
+
+ {!isLoading && discount && (
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts
new file mode 100644
index 0000000000..26d5f684cc
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts
@@ -0,0 +1 @@
+export { DiscountEditConfiguration as Component } from "./discount-edit-configuration"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx
new file mode 100644
index 0000000000..181e6d54b4
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx
@@ -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>({
+ 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 (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/index.ts b/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/index.ts
new file mode 100644
index 0000000000..27d5b03f77
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-discount-details-form"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-details/discount-edit-details.tsx b/packages/admin-next/dashboard/src/routes/discounts/edit-details/discount-edit-details.tsx
new file mode 100644
index 0000000000..2854c4cc55
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-details/discount-edit-details.tsx
@@ -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 (
+
+
+ {t("discounts.editDiscountDetails")}
+
+ {!isLoading && discount && (
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-details/index.ts b/packages/admin-next/dashboard/src/routes/discounts/edit-details/index.ts
new file mode 100644
index 0000000000..9e8f266c62
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/edit-details/index.ts
@@ -0,0 +1 @@
+export { DiscountEditDetails as Component } from "./discount-edit-details"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/list/components/discount-list-table/discount-list-table.tsx b/packages/admin-next/dashboard/src/routes/discounts/list/components/discount-list-table/discount-list-table.tsx
index 71cd438564..7fb2251f44 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/list/components/discount-list-table/discount-list-table.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/list/components/discount-list-table/discount-list-table.tsx
@@ -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"),
diff --git a/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx
index c24aa90d83..8fd9768975 100644
--- a/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx
@@ -83,7 +83,7 @@ export const ProductOrganizationSection = ({
{t("fields.categories")}
- {product.categories.length > 0
+ {product.categories?.length > 0
? product.categories.map((pcat) => (
} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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 = {}
): ResponsePromise {
- 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} 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} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise} Resolves to the details of the discount.
- *
+ *
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
diff --git a/packages/medusa-react/src/hooks/admin/discounts/queries.ts b/packages/medusa-react/src/hooks/admin/discounts/queries.ts
index 720a5c4945..4e52269b35 100644
--- a/packages/medusa-react/src/hooks/admin/discounts/queries.ts
+++ b/packages/medusa-react/src/hooks/admin/discounts/queries.ts
@@ -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 (
*
* {isLoading && Loading...}
@@ -57,21 +57,21 @@ type DiscountQueryKeys = typeof adminDiscountKeys
*
* )
* }
- *
+ *
* 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 (
*
* {isLoading && Loading...}
@@ -88,19 +88,19 @@ type DiscountQueryKeys = typeof adminDiscountKeys
*
* )
* }
- *
+ *
* 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 (
*
* {isLoading && Loading...}
@@ -126,10 +126,10 @@ type DiscountQueryKeys = typeof adminDiscountKeys
*
* )
* }
- *
+ *
* 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 (
*
* {isLoading && Loading...}
@@ -176,9 +176,9 @@ export const useAdminDiscounts = (
*
* )
* }
- *
+ *
* 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 (
*
* {isLoading && Loading...}
@@ -230,9 +230,9 @@ export const useAdminDiscount = (
*
* )
* }
- *
+ *
* 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 (
*
* {isLoading && Loading...}
@@ -289,9 +289,9 @@ export const useAdminGetDiscountByCode = (
*
* )
* }
- *
+ *
* export default DiscountCondition
- *
+ *
* @customNamespace Hooks.Admin.Discounts
* @category Queries
*/