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>all products except <0/>.", + "customer_groups": "Discount applies to <1>all customer groups except <0/>.", + "product_tags": "Discount applies to <1>all product tags except <0/>.", + "product_collections": "Discount applies to <1>all product collections except <0/>.", + "product_types": "Discount applies to <1>all 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")} + +
    +
    +
    +
    +
    + { + return ( + + {t("fields.code")} + + + + + ) + }} + /> + { + return ( + + {t("fields.percentage")} + + + + + ) + }} + /> +
    + + ]} + /> + +
    +
    +
    +
    +
    + ) +} 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 ( + +
    + +
    +
    + + ]} + /> + +
    + +
    + ( + +
    + + {t("discounts.hasStartDate")} + +
    + + {t("discounts.startDateHint")} + + +
    + )} + /> + { + return ( + +
    + + { + onChange(v ?? null) + }} + {...field} + /> + +
    + +
    + ) + }} + /> +
    + +
    + { + return ( + +
    + + {t("discounts.hasEndDate")} + + + + +
    + + {t("discounts.endDateHint")} + + +
    + ) + }} + /> + { + return ( + +
    + + { + 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")} + /> + +
    + + +
    + ) + }} + /> +
    + +
    + { + return ( + +
    + + {t("discounts.hasUsageLimit")} + + + + + + +
    + + {t("discounts.usageLimitHint")} + + +
    + ) + }} + /> + + { + return ( + + + { + const value = e.target.value + + if (value === "") { + field.onChange(null) + } else { + field.onChange(Number(value)) + } + }} + /> + + + + ) + }} + /> +
    + +
    + { + return ( + +
    + + {t("discounts.hasDurationLimit")} + + + + + + +
    + + {t("discounts.durationHint")} + + +
    + ) + }} + /> +
    + { + return ( + + {t("fields.years")} + + { + const value = e.target.value + + if (value === "") { + field.onChange(null) + } else { + field.onChange(Number(value)) + } + }} + /> + + + + ) + }} + /> + { + return ( + + {t("fields.months")} + + { + const value = e.target.value + + if (value === "") { + field.onChange(null) + } else { + field.onChange(Number(value)) + } + }} + /> + + + + ) + }} + /> + { + return ( + + {t("fields.days")} + + { + const value = e.target.value + + if (value === "") { + field.onChange(null) + } else { + field.onChange(Number(value)) + } + }} + /> + + + + ) + }} + /> +
    +
    + { + return ( + + {t("fields.hours")} + + { + const value = e.target.value + + if (value === "") { + field.onChange(null) + } else { + field.onChange(Number(value)) + } + }} + /> + + + + ) + }} + /> + { + return ( + + {t("fields.minutes")} + + { + const value = e.target.value + if (value === "") { + field.onChange(null) + } else { + console.log(Number(value)) + field.onChange(Number(value)) + } + }} + /> + + + + ) + }} + /> +
    +
    +
    +
    + +
    + + + + +
    +
    +
    +
    + ) +} 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 ( + +
    + +
    +
    + { + return ( + + {t("fields.code")} + + + + + + ) + }} + /> + + ]} + /> + +
    + + { + return ( + + {t("discounts.chooseValidRegions")} + + {isFixedDiscount ? ( + + ) : ( + ({ + label: r.name, + value: r.id, + }))} + value={value} + onChange={onChange} + {...field} + /> + )} + + + + ) + }} + /> + + {!isFreeShipping && ( + { + return ( + + + {isFixedDiscount + ? t("fields.amount") + : t("fields.percentage")} + + + {isFixedDiscount ? ( + + ) : ( + + )} + + + + ) + }} + /> + )} + + { + return ( + + {t("fields.description")} + +