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 63dffaa0e3..055025a412 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -309,7 +309,7 @@ "discounts": { "domain": "Discounts", "startDate": "Start date", - "createDiscountTitle": "Create discount", + "createDiscountTitle": "Create Discount", "validDuration": "Duration of the discount", "redemptionsLimit": "Redemptions limit", "endDate": "End date", @@ -320,8 +320,8 @@ "fixedAmount": "Fixed amount", "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", + "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.", @@ -329,38 +329,54 @@ "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.", + "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", "conditionsHint": "Create conditions to apply on the discount", "isTemplateDiscount": "Is this a template discount?", "percentageDescription": "Discount applied in %", "fixedDescription": "Amount discount", "shippingDescription": "Override delivery amount", - "selectRegionFirst": "Select region first", + "selectRegionFirst": "Select a region first", "templateHint": "Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same.", "conditions": { + "editHeader": "Edit Discount Conditions", + "editHint": "Specify conditions for when the discount can be applied to a cart.", + "manageTypesAction": "Manage condition types", "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." + "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/>." + "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/>" + }, + "edit": { + "appliesTo": "Discount applies to", + "except": { + "products_one": "product, except", + "products_other": "products, except", + "product_tags_one": "product tag, except", + "product_tags_other": "product tags, except", + "product_types_one": "product type, except", + "product_types_other": "product types, except", + "product_collections_one": "product collection, except", + "product_collections_other": "product collections, except", + "customer_groups_one": "customer group, except", + "customer_groups_other": "customer groups, except" + } } }, "discountStatus": { @@ -692,8 +708,14 @@ "limit": "Limit", "tags": "Tags", "type": "Type", + "none": "none", + "all": "all", "percentage": "Percentage", "sales_channels": "Sales Channels", + "customer_groups": "Customer Groups", + "product_tags": "Product Tags", + "product_types": "Product Types", + "product_collections": "Product Collections", "status": "Status", "code": "Code", "value": "Value", 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 index 57d79598b7..ab18f60b1f 100644 --- 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 @@ -1,4 +1,4 @@ -import { Text, Tooltip, clx } from "@medusajs/ui" +import { Tooltip, clx } from "@medusajs/ui" import { useTranslation } from "react-i18next" type ListSummaryProps = { @@ -12,46 +12,54 @@ type ListSummaryProps = { */ list: string[] /** - * Is hte summary displayed inline. + * Is the summary displayed inline. * Determines whether the center text is truncated if there is no space in the container */ inline?: boolean + + className?: string } -export const ListSummary = ({ list, inline, n = 2 }: ListSummaryProps) => { +export const ListSummary = ({ + list, + className, + inline, + n = 2, +}: ListSummaryProps) => { const { t } = useTranslation() + + const title = t("general.plusCountMore", { + count: list.length - n, + }) + return (
- - {list.slice(0, n).join(", ")} - +
+ {list.slice(0, n).join(", ")} +
{list.length > n && ( - - {list.slice(n).map((c) => ( -
  • {c}
  • - ))} - - } - > - + + {list.slice(n).map((c) => ( +
  • {c}
  • + ))} + + } > - {t("general.plusCountMore", { - count: list.length - n, - })} -
    -
    + {title} + +
    )} ) diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx new file mode 100644 index 0000000000..c36a04782b --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx @@ -0,0 +1,41 @@ +import { Tooltip } from "@medusajs/ui" +import format from "date-fns/format" +import { useTranslation } from "react-i18next" + +type DateCellProps = { + date: Date +} + +export const CreatedAtCell = ({ date }: DateCellProps) => { + const value = new Date(date) + value.setMinutes(value.getMinutes() - value.getTimezoneOffset()) + + const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12 + const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM" + + return ( +
    + {`${format( + value, + timestampFormat + )}`} + } + > + {format(value, "dd MMM yyyy")} + +
    + ) +} + +export const CreatedAtHeader = () => { + const { t } = useTranslation() + + return ( +
    + {t("fields.createdAt")} +
    + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/index.ts new file mode 100644 index 0000000000..2a3ce946f5 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/index.ts @@ -0,0 +1 @@ +export * from "./created-at-cell" diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-group-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-group-table-columns.tsx new file mode 100644 index 0000000000..779d65252e --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-group-table-columns.tsx @@ -0,0 +1,34 @@ +import { CustomerGroup } from "@medusajs/medusa" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { Text } from "@medusajs/ui" + +import { NameHeader } from "../../../components/table/table-cells/common/name-cell" +import { + CreatedAtHeader, + CreatedAtCell, +} from "../../../components/table/table-cells/common/created-at-cell" + +const columnHelper = createColumnHelper() + +export const useCustomerGroupTableColumns = () => { + return useMemo( + () => [ + columnHelper.display({ + id: "name", + header: () => , + cell: ({ + row: { + original: { name }, + }, + }) => {name}, + }), + + columnHelper.accessor("created_at", { + header: () => , + cell: ({ getValue }) => , + }), + ], + [] + ) +} diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-customer-group-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-customer-group-table-query.tsx new file mode 100644 index 0000000000..46fd056783 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/query/use-customer-group-table-query.tsx @@ -0,0 +1,34 @@ +import { AdminGetCustomerGroupsParams } from "@medusajs/medusa" + +import { useQueryParams } from "../../use-query-params" + +type UseCustomerGroupTableQueryProps = { + prefix?: string + pageSize?: number +} + +export const useCustomerGroupTableQuery = ({ + prefix, + pageSize = 20, +}: UseCustomerGroupTableQueryProps) => { + const queryObject = useQueryParams( + ["offset", "q", "has_account", "order", "created_at", "updated_at"], + prefix + ) + + const { offset, created_at, updated_at, q, order } = queryObject + + const searchParams: AdminGetCustomerGroupsParams = { + limit: pageSize, + offset: offset ? Number(offset) : 0, + order, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + q, + } + + return { + searchParams, + raw: queryObject, + } +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx index 1d5ec8b053..442cd2a6a2 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx @@ -3,6 +3,7 @@ import type { AdminCustomerGroupsRes, AdminCustomersRes, AdminDraftOrdersRes, + AdminDiscountsRes, AdminGiftCardsRes, AdminOrdersRes, AdminProductsRes, @@ -430,25 +431,36 @@ export const v1Routes: RouteObject[] = [ }, children: [ { - index: true, - lazy: () => import("../../routes/discounts/list"), + path: "", + lazy: () => import("../../routes/discounts/discount-list"), }, { path: "create", - lazy: () => import("../../routes/discounts/create"), + lazy: () => import("../../routes/discounts/discount-create"), }, { path: ":id", - lazy: () => import("../../routes/discounts/details"), + lazy: () => import("../../routes/discounts/discount-detail"), + handle: { + crumb: (data: AdminDiscountsRes) => data.discount.code, + }, children: [ { path: "edit", - lazy: () => import("../../routes/discounts/edit-details"), + lazy: () => + import("../../routes/discounts/discount-edit-details"), }, { path: "configuration", lazy: () => - import("../../routes/discounts/edit-configuration"), + import( + "../../routes/discounts/discount-edit-configuration" + ), + }, + { + path: "conditions", + lazy: () => + import("../../routes/discounts/discount-edit-conditions"), }, ], }, diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/conditions-drawer.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/conditions-drawer.tsx new file mode 100644 index 0000000000..c2c1e6bd07 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/conditions-drawer.tsx @@ -0,0 +1,664 @@ +import { + CustomerGroup, + Product, + ProductCollection, + ProductTag, + ProductType, +} from "@medusajs/medusa" +import { Button } from "@medusajs/ui" +import { OnChangeFn, RowSelectionState } from "@tanstack/react-table" +import { + useAdminCollections, + useAdminCustomerGroups, + useAdminProductTags, + useAdminProductTypes, + useAdminProducts, +} from "medusa-react" +import { useState } from "react" +import { useTranslation } from "react-i18next" + +import { SplitView } from "../../../../../components/layout/split-view" +import { DataTable } from "../../../../../components/table/data-table" + +import { useDataTable } from "../../../../../hooks/use-data-table" + +import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters" +import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query" +import { useProductConditionsTableColumns } from "../../hooks/columns/use-product-conditions-table-columns" + +import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query" +import { useCustomerGroupConditionsTableColumns } from "../../hooks/columns/use-customer-group-conditions-table-columns" +import { useCustomerGroupConditionsTableFilters } from "../../hooks/filters/use-customer-group-conditions-table-filters" + +import { useProductTypeConditionsTableColumns } from "../../hooks/columns/use-product-type-conditions-table-columns" +import { useProductTypeConditionsTableFilters } from "../../hooks/filters/use-product-type-conditions-table-filters" +import { useProductTypeConditionsTableQuery } from "../../hooks/query/use-product-type-conditions-table-query" + +import { useProductCollectionConditionsTableColumns } from "../../hooks/columns/use-product-collection-conditions-table-columns" +import { useProductCollectionConditionsTableFilters } from "../../hooks/filters/use-product-collection-conditions-table-filters" +import { useProductCollectionConditionsTableQuery } from "../../hooks/query/use-product-collection-conditions-table-query" + +import { useProductTagConditionsTableColumns } from "../../hooks/columns/use-product-tag-conditions-table-columns" +import { useProductTagConditionsTableFilters } from "../../hooks/filters/use-product-tag-conditions-table-filters" +import { useProductTagConditionsTableQuery } from "../../hooks/query/use-product-tag-conditions-table-query" + +import { ConditionEntities } from "../../constants" +import { ConditionsOption } from "../../types" + +const PAGE_SIZE = 50 + +const PRODUCT_PREFIX = "product" +const PRODUCT_TYPE_PREFIX = "product_type" +const PRODUCT_COLLECTION_PREFIX = "product_collection" +const CUSTOMER_GROUP_PREFIX = "customer_group" +const PRODUCT_TAG_PREFIX = "customer_group" + +type ConditionsProps = { + selected: ConditionsOption[] + onSave: (options: ConditionsOption[]) => void +} + +const initRowState = (selected: ConditionsOption[] = []): RowSelectionState => { + return selected.reduce((acc, { value }) => { + acc[value] = true + return acc + }, {} as RowSelectionState) +} + +const ConditionsFooter = ({ onSave }: { onSave: () => void }) => { + const { t } = useTranslation() + + return ( +
    + + + + +
    + ) +} + +const ProductConditionsTable = ({ selected = [], onSave }: ConditionsProps) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useProductTableQuery({ + pageSize: PAGE_SIZE, + prefix: PRODUCT_PREFIX, + }) + const { products, count, isLoading, isError, error } = useAdminProducts( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const added = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (added.length) { + const addedProducts = (products?.filter((p) => added.includes(p.id!)) ?? + []) as Product[] + + if (addedProducts.length > 0) { + const newConditions = addedProducts.map((p) => ({ + label: p.title, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => p.value in newState) + return Array.from(new Set([...filteredPrev, ...newConditions])) + }) + } + + setRowSelection(newState) + } + + const removed = Object.keys(rowSelection).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (removed.length) { + setIntermediate((prev) => { + return prev.filter((p) => !removed.includes(p.value)) + }) + + setRowSelection(newState) + } + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useProductConditionsTableColumns() + const filters = useProductTableFilters() + + const { table } = useDataTable({ + data: (products ?? []) as Product[], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: PRODUCT_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
    + + +
    + ) +} + +const CustomerGroupConditionsTable = ({ + selected = [], + onSave, +}: ConditionsProps) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useCustomerGroupTableQuery({ + pageSize: PAGE_SIZE, + prefix: PRODUCT_PREFIX, + }) + const { customer_groups, count, isLoading, isError, error } = + useAdminCustomerGroups( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const added = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (added.length) { + const addedGroups = (customer_groups?.filter((p) => + added.includes(p.id!) + ) ?? []) as CustomerGroup[] + + if (addedGroups.length > 0) { + const newConditions = addedGroups.map((p) => ({ + label: p.name, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => p.value in newState) + return Array.from(new Set([...filteredPrev, ...newConditions])) + }) + } + + setRowSelection(newState) + } + + const removed = Object.keys(rowSelection).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (removed.length) { + setIntermediate((prev) => { + return prev.filter((p) => !removed.includes(p.value)) + }) + + setRowSelection(newState) + } + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useCustomerGroupConditionsTableColumns() + const filters = useCustomerGroupConditionsTableFilters() + + const { table } = useDataTable({ + data: (customer_groups ?? []) as CustomerGroup[], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: CUSTOMER_GROUP_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
    + + +
    + ) +} + +const ProductTypeConditionsTable = ({ + onSave, + selected = [], +}: ConditionsProps) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useProductTypeConditionsTableQuery({ + pageSize: PAGE_SIZE, + prefix: PRODUCT_TYPE_PREFIX, + }) + const { product_types, count, isLoading, isError, error } = + useAdminProductTypes( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const added = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (added.length) { + const addedTypes = (product_types?.filter((p) => added.includes(p.id!)) ?? + []) as ProductType[] + + if (addedTypes.length > 0) { + const newConditions = addedTypes.map((p) => ({ + label: p.value, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => p.value in newState) + return Array.from(new Set([...filteredPrev, ...newConditions])) + }) + } + + setRowSelection(newState) + } + const removed = Object.keys(rowSelection).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (removed.length) { + setIntermediate((prev) => { + return prev.filter((p) => !removed.includes(p.value)) + }) + + setRowSelection(newState) + } + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useProductTypeConditionsTableColumns() + const filters = useProductTypeConditionsTableFilters() + + const { table } = useDataTable({ + data: product_types ?? [], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: PRODUCT_TYPE_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
    + + +
    + ) +} + +const ProductCollectionConditionsTable = ({ + onSave, + selected = [], +}: ConditionsProps) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useProductCollectionConditionsTableQuery({ + pageSize: PAGE_SIZE, + prefix: PRODUCT_COLLECTION_PREFIX, + }) + const { collections, count, isLoading, isError, error } = useAdminCollections( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const added = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (added.length) { + const addedCollections = (collections?.filter((p) => + added.includes(p.id!) + ) ?? []) as ProductCollection[] + + if (addedCollections.length > 0) { + const newConditions = addedCollections.map((p) => ({ + label: p.title, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => p.value in newState) + return Array.from(new Set([...filteredPrev, ...newConditions])) + }) + } + + setRowSelection(newState) + } + + const removed = Object.keys(rowSelection).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (removed.length) { + setIntermediate((prev) => { + return prev.filter((p) => !removed.includes(p.value)) + }) + + setRowSelection(newState) + } + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useProductCollectionConditionsTableColumns() + const filters = useProductCollectionConditionsTableFilters() + + const { table } = useDataTable({ + data: collections ?? [], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: PRODUCT_COLLECTION_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
    + + +
    + ) +} + +const ProductTagConditionsTable = ({ + onSave, + selected = [], +}: ConditionsProps) => { + const [rowSelection, setRowSelection] = useState( + initRowState(selected) + ) + const [intermediate, setIntermediate] = useState(selected) + + const { searchParams, raw } = useProductTagConditionsTableQuery({ + pageSize: PAGE_SIZE, + prefix: PRODUCT_TAG_PREFIX, + }) + const { product_tags, count, isLoading, isError, error } = + useAdminProductTags( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const updater: OnChangeFn = (fn) => { + const newState: RowSelectionState = + typeof fn === "function" ? fn(rowSelection) : fn + + const added = Object.keys(newState).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (added.length) { + const addedTags = (product_tags?.filter((p) => added.includes(p.id!)) ?? + []) as ProductTag[] + + if (addedTags.length > 0) { + const newConditions = addedTags.map((p) => ({ + label: p.value, + value: p.id!, + })) + + setIntermediate((prev) => { + const filteredPrev = prev.filter((p) => p.value in newState) + return Array.from(new Set([...filteredPrev, ...newConditions])) + }) + } + + setRowSelection(newState) + } + + const removed = Object.keys(rowSelection).filter( + (k) => newState[k] !== rowSelection[k] + ) + + if (removed.length) { + setIntermediate((prev) => { + return prev.filter((p) => !removed.includes(p.value)) + }) + + setRowSelection(newState) + } + } + + const handleSave = () => { + onSave(intermediate) + } + + const columns = useProductTagConditionsTableColumns() + const filters = useProductTagConditionsTableFilters() + + const { table } = useDataTable({ + data: product_tags ?? [], + columns: columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + enableRowSelection: true, + rowSelection: { + state: rowSelection, + updater, + }, + prefix: PRODUCT_TAG_PREFIX, + }) + + if (isError) { + throw error + } + + return ( +
    + + +
    + ) +} + +type ConditionsTableProps = { + product: ConditionsProps + productType: ConditionsProps + productTag: ConditionsProps + productCollection: ConditionsProps + customerGroup: ConditionsProps + selected: ConditionEntities | null +} + +export const ConditionsDrawer = ({ + product, + productType, + customerGroup, + productCollection, + productTag, + selected, +}: ConditionsTableProps) => { + switch (selected) { + case ConditionEntities.PRODUCT: + return + case ConditionEntities.PRODUCT_TYPE: + return + case ConditionEntities.PRODUCT_COLLECTION: + return + case ConditionEntities.PRODUCT_TAG: + return + case ConditionEntities.CUSTOMER_GROUP: + return + default: + return null + } +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/index.ts b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/index.ts new file mode 100644 index 0000000000..b65a27e54d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/index.ts @@ -0,0 +1 @@ +export * from "./conditions-drawer" diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/constants.ts b/packages/admin-next/dashboard/src/routes/discounts/common/constants.ts new file mode 100644 index 0000000000..186bbe49e7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/constants.ts @@ -0,0 +1,7 @@ +export enum ConditionEntities { + PRODUCT = "products", + PRODUCT_TYPE = "product_types", + PRODUCT_COLLECTION = "product_collections", + PRODUCT_TAG = "product_tags", + CUSTOMER_GROUP = "customer_groups", +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-customer-group-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-customer-group-conditions-table-columns.tsx new file mode 100644 index 0000000000..5ed653dad6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-customer-group-conditions-table-columns.tsx @@ -0,0 +1,46 @@ +import { CustomerGroup } from "@medusajs/medusa" +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns" + +const columnHelper = createColumnHelper() + +export const useCustomerGroupConditionsTableColumns = () => { + const base = useCustomerGroupTableColumns() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + ...base, + ], + [base] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-collection-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-collection-conditions-table-columns.tsx new file mode 100644 index 0000000000..049e8998e1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-collection-conditions-table-columns.tsx @@ -0,0 +1,60 @@ +import { useTranslation } from "react-i18next" +import { useMemo } from "react" +import { createColumnHelper } from "@tanstack/react-table" +import { ProductCollection } from "@medusajs/medusa" +import { Checkbox } from "@medusajs/ui" + +const columnHelper = createColumnHelper() + +export const useProductCollectionConditionsTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.accessor("title", { + header: t("fields.title"), + }), + columnHelper.accessor("handle", { + header: t("fields.handle"), + cell: ({ getValue }) => `/${getValue()}`, + }), + columnHelper.accessor("products", { + header: t("fields.products"), + cell: ({ getValue }) => { + const count = getValue()?.length + + return {count || "-"} + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-conditions-table-columns.tsx new file mode 100644 index 0000000000..9fc6850729 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-conditions-table-columns.tsx @@ -0,0 +1,46 @@ +import { Product } from "@medusajs/medusa" +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns" + +const columnHelper = createColumnHelper() + +export const useProductConditionsTableColumns = () => { + const base = useProductTableColumns() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + ...base, + ], + [base] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-tag-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-tag-conditions-table-columns.tsx new file mode 100644 index 0000000000..540c8d1354 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-tag-conditions-table-columns.tsx @@ -0,0 +1,48 @@ +import { useTranslation } from "react-i18next" +import { useMemo } from "react" +import { createColumnHelper } from "@tanstack/react-table" +import { ProductTag } from "@medusajs/medusa" +import { Checkbox } from "@medusajs/ui" + +const columnHelper = createColumnHelper() + +export const useProductTagConditionsTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.accessor("value", { + header: t("fields.value"), + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-type-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-type-conditions-table-columns.tsx new file mode 100644 index 0000000000..9066f5b02f --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-type-conditions-table-columns.tsx @@ -0,0 +1,49 @@ +import { ProductType } from "@medusajs/medusa" +import { Checkbox } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +const columnHelper = createColumnHelper() + +export const useProductTypeConditionsTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.accessor("value", { + header: t("fields.value"), + cell: ({ getValue }) => getValue(), + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-customer-group-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-customer-group-conditions-table-filters.tsx new file mode 100644 index 0000000000..50859c8e24 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-customer-group-conditions-table-filters.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useCustomerGroupConditionsTableFilters = () => { + const { t } = useTranslation() + + const filters: Filter[] = [ + { label: t("fields.createdAt"), key: "created_at" }, + { label: t("fields.updatedAt"), key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-collection-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-collection-conditions-table-filters.tsx new file mode 100644 index 0000000000..45de4cc975 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-collection-conditions-table-filters.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useProductCollectionConditionsTableFilters = () => { + const { t } = useTranslation() + + const filters: Filter[] = [ + { label: t("fields.createdAt"), key: "created_at" }, + { label: t("fields.updatedAt"), key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-tag-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-tag-conditions-table-filters.tsx new file mode 100644 index 0000000000..b79dad59a0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-tag-conditions-table-filters.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useProductTagConditionsTableFilters = () => { + const { t } = useTranslation() + + const filters: Filter[] = [ + { label: t("fields.createdAt"), key: "created_at" }, + { label: t("fields.updatedAt"), key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-type-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-type-conditions-table-filters.tsx new file mode 100644 index 0000000000..7c9a74018c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-type-conditions-table-filters.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useProductTypeConditionsTableFilters = () => { + const { t } = useTranslation() + + const filters: Filter[] = [ + { label: t("fields.createdAt"), key: "created_at" }, + { label: t("fields.updatedAt"), key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-collection-conditions-table-query.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-collection-conditions-table-query.tsx new file mode 100644 index 0000000000..159ab0a8bd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-collection-conditions-table-query.tsx @@ -0,0 +1,42 @@ +import { AdminGetCollectionsParams } from "@medusajs/medusa" + +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useProductCollectionConditionsTableQuery = ({ + pageSize = 50, + prefix, +}: { + pageSize?: number + prefix: string +}) => { + const raw = useQueryParams( + [ + "offset", + "q", + "order", + "title", + "handle", + "discount_condition_id", + "created_at", + "updated_at", + ], + prefix + ) + + const searchParams: AdminGetCollectionsParams = { + limit: pageSize, + offset: raw.offset ? Number(raw.offset) : 0, + q: raw.q, + title: raw.title, + handle: raw.handle, + discount_condition_id: raw.discount_condition_id, + created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined, + updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined, + order: raw.order, + } + + return { + searchParams, + raw, + } +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-tag-conditions-table-query.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-tag-conditions-table-query.tsx new file mode 100644 index 0000000000..717cab17f6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-tag-conditions-table-query.tsx @@ -0,0 +1,39 @@ +import { AdminGetProductTagsParams } from "@medusajs/medusa" + +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useProductTagConditionsTableQuery = ({ + pageSize = 50, + prefix, +}: { + pageSize?: number + prefix: string +}) => { + const raw = useQueryParams( + [ + "offset", + "q", + "order", + "value", + "discount_condition_id", + "created_at", + "updated_at", + ], + prefix + ) + + const searchParams: AdminGetProductTagsParams = { + limit: pageSize, + offset: raw.offset ? Number(raw.offset) : 0, + q: raw.q, + discount_condition_id: raw.discount_condition_id, + created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined, + updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined, + order: raw.order, + } + + return { + searchParams, + raw, + } +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-type-conditions-table-query.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-type-conditions-table-query.tsx new file mode 100644 index 0000000000..737d00d143 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-type-conditions-table-query.tsx @@ -0,0 +1,29 @@ +import { AdminGetProductTypesParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useProductTypeConditionsTableQuery = ({ + pageSize = 50, + prefix, +}: { + pageSize?: number + prefix: string +}) => { + const raw = useQueryParams( + ["offset", "q", "order", "created_at", "updated_at"], + prefix + ) + + const searchParams: AdminGetProductTypesParams = { + limit: pageSize, + offset: raw.offset ? Number(raw.offset) : 0, + q: raw.q, + created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined, + updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined, + order: raw.order, + } + + return { + searchParams, + raw, + } +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/types.ts b/packages/admin-next/dashboard/src/routes/discounts/common/types.ts new file mode 100644 index 0000000000..411329ebef --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/common/types.ts @@ -0,0 +1,10 @@ +import { ConditionEntities } from "./constants" + +export type ConditionsOption = { + value: string + label: string +} + +export type ConditionsState = { + [K in ConditionEntities]: boolean +} 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/discount-create/components/create-discount-form/create-discount-details.tsx similarity index 97% rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/create-discount-details.tsx index 937922e6aa..fd064a7184 100644 --- 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/discount-create/components/create-discount-form/create-discount-details.tsx @@ -1,23 +1,25 @@ -import { useEffect, useMemo } from "react" import { Checkbox, + CurrencyInput, DatePicker, Heading, Input, + RadioGroup, Select, Switch, Text, Textarea, - RadioGroup, - CurrencyInput, } from "@medusajs/ui" -import { Trans, useTranslation } from "react-i18next" import { useAdminRegions } from "medusa-react" +import { useEffect, useMemo } from "react" +import { Trans, useTranslation } from "react-i18next" -import { Form } from "../../../../../components/common/form" -import { CreateDiscountFormReturn } from "./create-discount-form" +import { useWatch } from "react-hook-form" import { Combobox } from "../../../../../components/common/combobox" +import { Form } from "../../../../../components/common/form" +import { PercentageInput } from "../../../../../components/common/percentage-input" import { getCurrencySymbol } from "../../../../../lib/currencies" +import { CreateDiscountFormReturn } from "./create-discount-form" import { DiscountRuleType } from "./types" type CreateDiscountPropsProps = { @@ -29,8 +31,14 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => { const { regions } = useAdminRegions() - const watchType = form.watch("type") - const watchRegion = form.watch("regions") + const watchType = useWatch({ + control: form.control, + name: "type", + }) + const watchRegion = useWatch({ + name: "regions", + control: form.control, + }) const isFixedDiscount = watchType === DiscountRuleType.FIXED const isFreeShipping = watchType === DiscountRuleType.FREE_SHIPPING @@ -43,10 +51,12 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => { return regions.find((r) => r.id === watchRegion[0]) }, [regions, watchRegion]) + const { setValue } = form + useEffect(() => { - form.setValue("regions", []) - form.setValue("value", undefined) - }, [watchType]) + setValue("regions", []) + setValue("value", undefined) + }, [watchType, setValue]) return (
    @@ -197,9 +207,8 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => { ) ) : ( - { {t("discounts.isTemplateDiscount")}
    - {/**/} + {/* */} ) }} 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/discount-create/components/create-discount-form/create-discount-form.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-form.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/create-discount-form.tsx 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/discount-create/components/create-discount-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/types.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/types.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/types.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/types.ts diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-create/discount-create.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/discount-create.tsx diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-create/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/create/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/index.ts 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/discount-detail/components/details-section/details-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/details-section.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/details-section/details-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/details-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/details-section/index.ts 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/discount-detail/components/discount-conditions-section/discount-conditions-section.tsx similarity index 61% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-section.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-conditions-section/discount-conditions-section.tsx index 32fbc233cc..cac593ef7e 100644 --- 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/discount-detail/components/discount-conditions-section/discount-conditions-section.tsx @@ -1,27 +1,64 @@ -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 type { + CustomerGroup, + Discount, + DiscountCondition, + Product, + ProductCollection, + ProductTag, + ProductType, +} from "@medusajs/medusa" +import { Badge, Container, Heading } from "@medusajs/ui" import { useMemo } from "react" +import { Trans, useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" -import { ListSummary } from "../../../../../components/common/list-summary" import { NoRecords } from "../../../../../components/common/empty-table-content" +import { ListSummary } from "../../../../../components/common/list-summary" type ConditionTypeProps = { condition: DiscountCondition } -const N = 2 +const N = 1 + +const hasTitle = (p: unknown): p is Product | ProductCollection => { + return (p as { title: string }).title !== undefined +} + +const hasName = (p: unknown): p is CustomerGroup => { + return (p as { name: string }).name !== undefined +} + +const hasValue = (p: unknown): p is ProductTag | ProductType => { + return (p as { value: string }).value !== undefined +} function ConditionType({ condition }: ConditionTypeProps) { + const { t } = useTranslation() + const operator = condition.operator === "in" ? "including" : "excluding" const entity = condition.type + const list = condition[entity].map((p) => { + if (hasTitle(p)) { + return p.title + } + + if (hasName(p)) { + return p.name + } + + if (hasValue(p)) { + return p.value + } + }) as string[] + return ( -
    - +
    + N ? N - condition[entity].length @@ -29,22 +66,19 @@ function ConditionType({ condition }: ConditionTypeProps) { } i18nKey={`discounts.conditions.${operator}.${entity}`} components={[ - p.title || p.name || p.value - )} + list={list} + className="!txt-compact-xsmall-plus" /> - , - , + , + , ]} /> @@ -88,12 +122,7 @@ export const DiscountConditionsSection = ({ />
    - {!conditions.length && ( - - )} + {!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/discount-detail/components/discount-conditions-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-conditions-section/index.ts 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/discount-detail/components/discount-configurations-section/discount-configurations-section.tsx similarity index 93% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-configurations-section/discount-configurations-section.tsx index 86d3777c3d..cd9b262ca1 100644 --- 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/discount-detail/components/discount-configurations-section/discount-configurations-section.tsx @@ -1,6 +1,6 @@ -import { parse } from "iso8601-duration" -import { format, formatDuration } from "date-fns" import { PencilSquare } from "@medusajs/icons" +import { format, formatDuration } from "date-fns" +import { parse } from "iso8601-duration" import { useMemo } from "react" import { Discount } from "@medusajs/medusa" @@ -13,9 +13,9 @@ type DiscountConfigurationsSection = { discount: Discount } -function formatTime(dateTime?: string) { +function formatTime(dateTime?: string | Date | null) { if (!dateTime) { - return + return "-" } return format(new Date(dateTime), "dd MMM, yyyy, HH:mm:ss") } @@ -57,7 +57,7 @@ export const DiscountConfigurationSection = ({ {t("discounts.startDate")} - {formatTime(discount.starts_at as unknown as string)} + {formatTime(discount.starts_at)}
    @@ -65,7 +65,7 @@ export const DiscountConfigurationSection = ({ {t("discounts.endDate")} - {formatTime(discount.ends_at as unknown as string)} + {formatTime(discount.ends_at)}
    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/discount-detail/components/discount-configurations-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-configurations-section/index.ts 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/discount-detail/components/discount-general-section/discount-general-section.tsx similarity index 98% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-section.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-general-section/discount-general-section.tsx index f2ce0e6b39..bf6b3981c8 100644 --- 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/discount-detail/components/discount-general-section/discount-general-section.tsx @@ -60,7 +60,7 @@ export const DiscountGeneralSection = ({ [PromotionStatus.EXPIRED]: ["red", t("discounts.discountStatus.expired")], }[getDiscountStatus(discount)] as [ "grey" | "orange" | "green" | "red", - string + string, ] return ( @@ -101,7 +101,7 @@ export const DiscountGeneralSection = ({ {t("fields.description")} - {discount.rule.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/discount-detail/components/discount-general-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-general-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-redemptions-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-redemptions-section/index.ts 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/discount-detail/components/discount-redemptions-section/redemptions-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/redemptions-section.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-redemptions-section/redemptions-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/discount-detail.tsx similarity index 90% rename from packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/discount-detail.tsx index f2bedf3542..ececf11ff4 100644 --- a/packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx +++ b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/discount-detail.tsx @@ -2,16 +2,16 @@ 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 { DiscountConfigurationSection } from "./components/discount-configurations-section" +import { DiscountGeneralSection } from "./components/discount-general-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 { DiscountConditionsSection } from "./components/discount-conditions-section" +import { RedemptionsSection } from "./components/discount-redemptions-section" +import { discountLoader, expand } from "./loader" -import before from "medusa-admin:widgets/discount/details/before" import after from "medusa-admin:widgets/discount/details/after" +import before from "medusa-admin:widgets/discount/details/before" export const DiscountDetail = () => { const initialData = useLoaderData() as Awaited< diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/index.ts similarity index 55% rename from packages/admin-next/dashboard/src/routes/discounts/details/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/index.ts index 2debe10554..aeedb8f7fc 100644 --- a/packages/admin-next/dashboard/src/routes/discounts/details/index.ts +++ b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/index.ts @@ -1,2 +1,2 @@ -export { discountLoader as loader } from "./loader" export { DiscountDetail as Component } from "./discount-detail.tsx" +export { discountLoader as loader } from "./loader.ts" diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/loader.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/loader.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/details/loader.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/loader.ts diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/edit-discount-conditions-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/edit-discount-conditions-form.tsx new file mode 100644 index 0000000000..17aa2e4e5d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/edit-discount-conditions-form.tsx @@ -0,0 +1,583 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { useSearchParams } from "react-router-dom" + +import { + Discount, + DiscountCondition, + DiscountConditionType, +} from "@medusajs/medusa" +import { Button, DropdownMenu, Heading, Text } from "@medusajs/ui" +import { adminDiscountKeys } from "medusa-react" +import { useForm, useWatch } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +import { medusa, queryClient } from "../../../../../lib/medusa" + +import { ListSummary } from "../../../../../components/common/list-summary" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" + +import { SplitView } from "../../../../../components/layout/split-view" +import { ConditionsDrawer } from "../../../common/components/conditions-drawer" +import { ConditionEntities } from "../../../common/constants" +import { ConditionsOption } from "../../../common/types" + +enum DiscountConditionOperator { + IN = "in", + NOT_IN = "not_in", +} + +type ConditionProps = { + labels: string[] + isInOperator: boolean + type: ConditionEntities + onClick: (op: DiscountConditionOperator) => void +} + +type EditDiscountFormProps = { + discount: Discount +} + +const N = 1 + +const getDefault = (conditionType: ConditionEntities, labelProp: string) => { + return (conditions: DiscountCondition[]) => { + const condition = conditions.find( + (c) => c.type === (conditionType as unknown as DiscountConditionType) + ) + + if (!condition || !condition[conditionType]) { + return [] + } + + return condition[conditionType].map((p) => ({ + label: p[labelProp as keyof typeof p] as string, + value: p.id, + })) + } +} + +const getOperator = (conditionType: ConditionEntities) => { + return (conditions: DiscountCondition[]) => { + return conditions.find((c) => c.type === (conditionType as any))?.operator + } +} + +const SelectedConditionTypesSchema = zod.object({ + [ConditionEntities.PRODUCT]: zod.boolean().optional(), + [ConditionEntities.PRODUCT_TAG]: zod.boolean().optional(), + [ConditionEntities.PRODUCT_TYPE]: zod.boolean().optional(), + [ConditionEntities.PRODUCT_COLLECTION]: zod.boolean().optional(), + [ConditionEntities.CUSTOMER_GROUP]: zod.boolean().optional(), +}) + +type SelectedContionTypeKey = + (typeof ConditionEntities)[keyof typeof ConditionEntities] + +const EditDiscountConditionsSchema = zod.object({ + products: zod.array( + zod.object({ + label: zod.string(), + value: zod.string(), + }) + ), + product_types: zod.array( + zod.object({ + label: zod.string(), + value: zod.string(), + }) + ), + customer_groups: zod.array( + zod.object({ + label: zod.string(), + value: zod.string(), + }) + ), + product_collections: zod.array( + zod.object({ + label: zod.string(), + value: zod.string(), + }) + ), + product_tags: zod.array( + zod.object({ + label: zod.string(), + value: zod.string(), + }) + ), + products_operator: zod.nativeEnum(DiscountConditionOperator).optional(), + product_tags_operator: zod.nativeEnum(DiscountConditionOperator).optional(), + product_collections_operator: zod + .nativeEnum(DiscountConditionOperator) + .optional(), + product_types_operator: zod.nativeEnum(DiscountConditionOperator).optional(), + customer_groups_operator: zod + .nativeEnum(DiscountConditionOperator) + .optional(), + selected_condition_types: SelectedConditionTypesSchema, +}) + +export const EditDiscountConditionsForm = ({ + discount, +}: EditDiscountFormProps) => { + const [isLoading, setIsLoading] = useState(false) + const [open, setOpen] = useState(false) + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + + const [conditionType, setConditionType] = useState( + null + ) + + const [, setSearchParams] = useSearchParams() + + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const conditions = discount.rule.conditions?.sort((c1, c2) => + c1.type.localeCompare(c2.type) + ) // TODO: memo + + const form = useForm>({ + defaultValues: { + products: getDefault( + ConditionEntities.PRODUCT, + "title" + )(discount.rule.conditions), + products_operator: getOperator(ConditionEntities.PRODUCT)( + discount.rule.conditions + ), + product_types: getDefault( + ConditionEntities.PRODUCT_TYPE, + "value" + )(discount.rule.conditions), + product_types_operator: getOperator(ConditionEntities.PRODUCT_TYPE)( + discount.rule.conditions + ), + product_collections: getDefault( + ConditionEntities.PRODUCT_COLLECTION, + "title" + )(discount.rule.conditions), + product_collections_operator: getOperator( + ConditionEntities.PRODUCT_COLLECTION + )(discount.rule.conditions), + product_tags: getDefault( + ConditionEntities.PRODUCT_TAG, + "value" + )(discount.rule.conditions), + product_tags_operator: getOperator(ConditionEntities.PRODUCT_TAG)( + discount.rule.conditions + ), + customer_groups: getDefault( + ConditionEntities.CUSTOMER_GROUP, + "name" + )(discount.rule.conditions), + customer_groups_operator: getOperator(ConditionEntities.CUSTOMER_GROUP)( + discount.rule.conditions + ), + selected_condition_types: { + [ConditionEntities.CUSTOMER_GROUP]: !!conditions.find( + (c) => c.type === (ConditionEntities.CUSTOMER_GROUP as any) + ), + [ConditionEntities.PRODUCT_TYPE]: !!conditions.find( + (c) => c.type === (ConditionEntities.PRODUCT_TYPE as any) + ), + [ConditionEntities.PRODUCT_TAG]: !!conditions.find( + (c) => c.type === (ConditionEntities.PRODUCT_TAG as any) + ), + [ConditionEntities.PRODUCT_COLLECTION]: !!conditions.find( + (c) => c.type === (ConditionEntities.PRODUCT_COLLECTION as any) + ), + [ConditionEntities.PRODUCT]: !!conditions.find( + (c) => c.type === (ConditionEntities.PRODUCT as any) + ), + }, + }, + resolver: zodResolver(EditDiscountConditionsSchema), + }) + + const toggleSelectedConditionTypes = (type: ConditionEntities) => { + const state = { ...form.getValues().selected_condition_types } + if (state[type]) { + delete state[type] + } else { + state[type] = true + } + + form.setValue("selected_condition_types", state, { + shouldDirty: true, + shouldTouch: true, + }) + } + + const clearAllSelectedConditions = () => { + form.setValue( + "selected_condition_types", + { + [ConditionEntities.PRODUCT]: false, + [ConditionEntities.PRODUCT_TAG]: false, + [ConditionEntities.PRODUCT_TYPE]: false, + [ConditionEntities.PRODUCT_COLLECTION]: false, + [ConditionEntities.CUSTOMER_GROUP]: false, + }, + { + shouldDirty: true, + shouldTouch: true, + } + ) + } + + const selectedConditionTypes = useWatch({ + name: "selected_condition_types", + control: form.control, + }) + + const selectedTypes = Object.keys(selectedConditionTypes) + .filter( + (k) => selectedConditionTypes[k as keyof typeof selectedConditionTypes] + ) + .sort() as ConditionEntities[] + + const selectedProducts = useWatch({ + control: form.control, + name: "products", + }) + + const selectedProductTypes = useWatch({ + control: form.control, + name: "product_types", + }) + + const selectedCustomerGroups = useWatch({ + control: form.control, + name: "customer_groups", + }) + + const selectedProductCollections = useWatch({ + control: form.control, + name: "product_collections", + }) + + const selectedProductTags = useWatch({ + control: form.control, + name: "product_tags", + }) + + const handleSubmit = form.handleSubmit(async (data) => { + setIsLoading(true) + + const handleCondition = async (selectedType: string) => { + if ( + !selectedConditionTypes[ + selectedType as keyof typeof selectedConditionTypes + ] + ) { + return + } + + const currentCondition = conditions.find((c) => c.type === selectedType) + + if (currentCondition) { + // delete condition first (in case operator changed since we cannot update that trough API) + await medusa.admin.discounts.deleteCondition( + discount.id, + currentCondition.id + ) + } + + const payload = data[selectedType as SelectedContionTypeKey] + + if (payload?.length) { + await medusa.admin.discounts.createCondition(discount.id, { + operator: data[ + `${selectedType}_operator` as keyof typeof data + ] as DiscountConditionOperator, + [selectedType]: payload.map((d) => d.value), + }) + } + } + + const deleteUnselectedConditions = async () => { + const unselectedConditions = conditions.filter( + (c) => !selectedConditionTypes[c.type] + ) + const deletePromises = unselectedConditions.map((c) => + medusa.admin.discounts.deleteCondition(discount.id, c.id) + ) + await Promise.all(deletePromises) + } + + for (const selectedType of Object.keys(selectedConditionTypes)) { + await handleCondition(selectedType) + } + + await deleteUnselectedConditions() + + await queryClient.invalidateQueries(adminDiscountKeys.detail(discount.id)) + + setIsLoading(false) + handleSuccess() + }) + + const handleOpenDrawer = ( + type: ConditionEntities, + operator: DiscountConditionOperator + ) => { + form.setValue(`${type}_operator`, operator) + setConditionType(type) + setOpen(true) + } + + const handleSaveConditions = (type: ConditionEntities) => { + return (options: ConditionsOption[]) => { + form.setValue(type, options, { + shouldDirty: true, + shouldTouch: true, + }) + + setOpen(false) + } + } + + const handleOpenChange = (open: boolean) => { + if (!open) { + setConditionType(null) + setSearchParams( + {}, + { + replace: true, + } + ) + } + + setOpen(open) + } + + return ( + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + {t("discounts.conditions.editHeader")} + + {t("discounts.conditions.editHint")} + +
    + {selectedTypes.length > 0 && ( +
    + {selectedTypes.map((t) => { + if (t in selectedConditionTypes) { + const field = form.getValues(t) + const operator = form.getValues(`${t}_operator`) + + return field ? ( + f.label)} + isInOperator={operator === "in"} + onClick={(op) => handleOpenDrawer(t, op)} + /> + ) : ( + handleOpenDrawer(t, op)} + /> + ) + } + })} +
    + )} +
    + { + v && setIsDropdownOpen(v) + }} + > + + + + setIsDropdownOpen(false)} + > + {Object.values(ConditionEntities).map((type) => ( + + toggleSelectedConditionTypes(type) + } + > + + {t(`fields.${type}`)} + + + ))} + + + {selectedTypes.length > 0 && ( + + )} +
    +
    +
    +
    + + + +
    +
    +
    +
    + ) +} + +function Condition({ labels, isInOperator, type, onClick }: ConditionProps) { + const { t } = useTranslation() + + const isInButtonDisabled = !!labels.length && !isInOperator + const isExButtonDisabled = !!labels.length && isInOperator + + return ( +
    +
    + + {t("discounts.conditions.edit.appliesTo")} + +
    + +
    + + + {t(`discounts.conditions.edit.except.${type}`, { + count: labels.length > 1 ? labels.length - 1 : labels.length, + })} + + +
    + +
    +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/index.ts new file mode 100644 index 0000000000..d8b9de67e0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-discount-conditions-form" diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/discount-edit-conditions.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/discount-edit-conditions.tsx new file mode 100644 index 0000000000..2bf1156357 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/discount-edit-conditions.tsx @@ -0,0 +1,26 @@ +import { useAdminDiscount } from "medusa-react" +import { useParams } from "react-router-dom" + +import { RouteFocusModal } from "../../../components/route-modal/index.ts" +import { expand } from "../discount-detail/loader.ts" +import { EditDiscountConditionsForm } from "./components/edit-discount-form/index.ts" + +export const DiscountEditConditions = () => { + const { id } = useParams() + + const { discount, isLoading, isError, error } = useAdminDiscount(id!, { + expand, + }) + + if (isError) { + throw error + } + + return ( + + {!isLoading && discount && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/index.ts new file mode 100644 index 0000000000..922672dcff --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/index.ts @@ -0,0 +1 @@ +export { DiscountEditConditions as Component } from "./discount-edit-conditions" 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/discount-edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx similarity index 88% rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx index 1c7808b819..b5da6c8938 100644 --- 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/discount-edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx @@ -1,13 +1,13 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { formatISODuration } from "date-fns" +import { Duration, parse } from "iso8601-duration" 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 { Button, DatePicker, Input, Switch, Text } from "@medusajs/ui" import { useAdminUpdateDiscount } from "medusa-react" import { Form } from "../../../../../components/common/form" @@ -29,15 +29,15 @@ const EditDiscountSchema = zod.object({ end_date: zod.date().nullish(), enable_usage_limit: zod.boolean(), - usage_limit: zod.number().nullish(), + usage_limit: zod.union([zod.string(), zod.number()]).optional(), enable_duration: zod.boolean(), - years: zod.number().optional(), - months: zod.number().optional(), - days: zod.number().optional(), - hours: zod.number().optional(), - minutes: zod.number().optional(), + years: zod.union([zod.string(), zod.number()]).optional(), + months: zod.union([zod.string(), zod.number()]).optional(), + days: zod.union([zod.string(), zod.number()]).optional(), + hours: zod.union([zod.string(), zod.number()]).optional(), + minutes: zod.union([zod.string(), zod.number()]).optional(), }) export const EditDiscountConfigurationForm = ({ @@ -46,13 +46,11 @@ export const EditDiscountConfigurationForm = ({ 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 duration = useMemo(() => { + return discount.valid_duration + ? parse(discount.valid_duration) + : ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration) + }, [discount]) const form = useForm>({ defaultValues: { @@ -60,37 +58,66 @@ export const EditDiscountConfigurationForm = ({ start_date: new Date(discount.starts_at), enable_usage_limit: !!discount.usage_limit, - 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, + 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 formatDuration = (raw: Record) => { + const isEmpty = Object.values(raw).every((v) => !v) + + if (isEmpty) { + return null + } + + const durationObject = Object.entries(raw).reduce((acc, [key, value]) => { + if (value) { + return { + ...acc, + [key]: Number(value), + } + } + return acc + }, {} as Duration) + + return formatISODuration(durationObject) + } + const handleSubmit = form.handleSubmit(async (data) => { - const duration = pick(data, ["years", "months", "days", "hours", "minutes"]) - const isDurationEmpty = Object.values(duration).every((v) => !v) + const rawDuration = pick(data, [ + "years", + "months", + "days", + "hours", + "minutes", + ]) as Record + + const duration = formatDuration(rawDuration) + + const usageLimit = + data.enable_usage_limit && !isNaN(Number(data.usage_limit)) + ? Number(data.usage_limit) + : null 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 && !isDurationEmpty - ? formatISODuration(duration) - : null, + usage_limit: usageLimit, + valid_duration: data.enable_duration && duration ? duration : null, }, { onSuccess: () => { @@ -254,7 +281,7 @@ export const EditDiscountConfigurationForm = ({ { + render={({ field: { value, onChange, ...field } }) => { return ( @@ -262,13 +289,14 @@ export const EditDiscountConfigurationForm = ({ {...field} type="number" min={0} + value={value ?? ""} onChange={(e) => { const value = e.target.value if (value === "") { - field.onChange(null) + onChange(null) } else { - field.onChange(Number(value)) + 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/discount-edit-configuration/components/edit-discount-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/components/edit-discount-form/index.ts diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/discount-edit-configuration.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/discount-edit-configuration.tsx diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/index.ts 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/discount-edit-details/components/edit-discount-form/edit-discount-details-form.tsx similarity index 98% rename from packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-details/components/edit-discount-form/edit-discount-details-form.tsx index 181e6d54b4..c0752053d2 100644 --- 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/discount-edit-details/components/edit-discount-form/edit-discount-details-form.tsx @@ -1,29 +1,30 @@ import { zodResolver } from "@hookform/resolvers/zod" import { Discount } from "@medusajs/medusa" import { - CurrencyInput, Button, + CurrencyInput, Input, + Select, 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 { Combobox } from "../../../../../components/common/combobox" import { Form } from "../../../../../components/common/form" +import { PercentageInput } from "../../../../../components/common/percentage-input" import { RouteDrawer, useRouteModal, } from "../../../../../components/route-modal" +import { getCurrencySymbol } from "../../../../../lib/currencies" import { getDbAmount, getPresentationalAmount, } from "../../../../../lib/money-amount-helpers" -import { getCurrencySymbol } from "../../../../../lib/currencies" -import { Combobox } from "../../../../../components/common/combobox" type EditDiscountFormProps = { discount: Discount @@ -186,9 +187,8 @@ export const EditDiscountDetailsForm = ({ {...field} /> ) : ( -