fix(dashboard): Fixes to campaign and promotions domains (#9022)
This commit is contained in:
1 parent
c27aa46939
commit
e5b90b2d97
29 files changed
+502
-326
No files matched your search
+4
-3
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge } from "@medusajs/ui"
|
||||||
|
|
||||||
type CellProps = {
|
type CellProps = {
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
@@ -9,10 +11,9 @@ type HeaderProps = {
|
|||||||
export const CodeCell = ({ code }: CellProps) => {
|
export const CodeCell = ({ code }: CellProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
||||||
{/* // TODO: border color inversion*/}
|
<Badge size="2xsmall" className="truncate">
|
||||||
<span className="bg-ui-tag-neutral-bg truncate rounded-md border border-neutral-200 p-1 text-xs">
|
|
||||||
{code}
|
{code}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File renamed without changes.
@@ -1,13 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Filter } from "../../../components/table/data-table"
|
|
||||||
|
|
||||||
export const usePromotionTableFilters = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
let filters: Filter[] = [
|
|
||||||
{ label: t("fields.createdAt"), key: "created_at", type: "date" },
|
|
||||||
{ label: t("fields.updatedAt"), key: "updated_at", type: "date" },
|
|
||||||
]
|
|
||||||
|
|
||||||
return filters
|
|
||||||
}
|
|
||||||
File renamed without changes.
@@ -1694,8 +1694,15 @@
|
|||||||
"title": "Edit buy rules"
|
"title": "Edit buy rules"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"addToCampaign": {
|
"campaign": {
|
||||||
"title": "Promotion's Campaign"
|
"header": "Campaign",
|
||||||
|
"edit": {
|
||||||
|
"header": "Edit Campaign",
|
||||||
|
"successToast": "Successfully updated the campaign of the promotion."
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"goToCampaign": "Go to campaign"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"campaign_currency": {
|
"campaign_currency": {
|
||||||
"tooltip": "This is the promotion's currency. Change it from the Details tab."
|
"tooltip": "This is the promotion's currency. Change it from the Details tab."
|
||||||
@@ -1800,6 +1807,14 @@
|
|||||||
"header": "Edit Campaign",
|
"header": "Edit Campaign",
|
||||||
"successToast": "Campaign '{{name}}' was successfully updated."
|
"successToast": "Campaign '{{name}}' was successfully updated."
|
||||||
},
|
},
|
||||||
|
"configuration": {
|
||||||
|
"header": "Configuration",
|
||||||
|
"edit": {
|
||||||
|
"header": "Edit Campaign Configuration",
|
||||||
|
"description": "Edit the configuration of the campaign.",
|
||||||
|
"successToast": "Campaign configuration was successfully updated."
|
||||||
|
}
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"hint": "Create a promotional campaign.",
|
"hint": "Create a promotional campaign.",
|
||||||
"header": "Create Campaign",
|
"header": "Create Campaign",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PromotionDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
export enum PromotionStatus {
|
export enum PromotionStatus {
|
||||||
SCHEDULED = "SCHEDULED",
|
SCHEDULED = "SCHEDULED",
|
||||||
@@ -7,7 +7,7 @@ export enum PromotionStatus {
|
|||||||
DISABLED = "DISABLED",
|
DISABLED = "DISABLED",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPromotionStatus = (promotion: PromotionDTO) => {
|
export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const campaign = promotion.campaign
|
const campaign = promotion.campaign
|
||||||
|
|
||||||
|
|||||||
@@ -355,6 +355,11 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "edit",
|
path: "edit",
|
||||||
lazy: () => import("../../routes/campaigns/campaign-edit"),
|
lazy: () => import("../../routes/campaigns/campaign-edit"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "configuration",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/campaigns/campaign-configuration"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "edit-budget",
|
path: "edit-budget",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
|
|||||||
+2
-2
@@ -15,9 +15,9 @@ import { RouteFocusModal, useRouteModal } from "../../../../components/modals"
|
|||||||
import { DataTable } from "../../../../components/table/data-table"
|
import { DataTable } from "../../../../components/table/data-table"
|
||||||
import { useAddOrRemoveCampaignPromotions } from "../../../../hooks/api/campaigns"
|
import { useAddOrRemoveCampaignPromotions } from "../../../../hooks/api/campaigns"
|
||||||
import { usePromotions } from "../../../../hooks/api/promotions"
|
import { usePromotions } from "../../../../hooks/api/promotions"
|
||||||
import { usePromotionTableColumns } from "../../../../hooks/table/columns-v2/use-promotion-table-columns"
|
import { usePromotionTableColumns } from "../../../../hooks/table/columns/use-promotion-table-columns"
|
||||||
import { usePromotionTableFilters } from "../../../../hooks/table/filters/use-promotion-table-filters"
|
import { usePromotionTableFilters } from "../../../../hooks/table/filters/use-promotion-table-filters"
|
||||||
import { usePromotionTableQuery } from "../../../../hooks/table/query-v2/use-promotion-table-query"
|
import { usePromotionTableQuery } from "../../../../hooks/table/query/use-promotion-table-query"
|
||||||
import { useDataTable } from "../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../hooks/use-data-table"
|
||||||
|
|
||||||
type AddCampaignPromotionsFormProps = {
|
type AddCampaignPromotionsFormProps = {
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/modals"
|
||||||
|
import { useCampaign } from "../../../hooks/api/campaigns"
|
||||||
|
import { CampaignConfigurationForm } from "./components/campaign-configuration-form"
|
||||||
|
|
||||||
|
export const CampaignConfiguration = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { campaign, isLoading, isError, error } = useCampaign(id!)
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<RouteDrawer.Title asChild>
|
||||||
|
<Heading>{t("campaigns.configuration.edit.header")}</Heading>
|
||||||
|
</RouteDrawer.Title>
|
||||||
|
<RouteDrawer.Description className="sr-only">
|
||||||
|
{t("campaigns.configuration.edit.description")}
|
||||||
|
</RouteDrawer.Description>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{!isLoading && campaign && (
|
||||||
|
<CampaignConfigurationForm campaign={campaign} />
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { AdminCampaign } from "@medusajs/types"
|
||||||
|
import { Button, DatePicker, toast } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||||
|
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
|
||||||
|
|
||||||
|
type CampaignConfigurationFormProps = {
|
||||||
|
campaign: AdminCampaign
|
||||||
|
}
|
||||||
|
|
||||||
|
const CampaignConfigurationSchema = z.object({
|
||||||
|
starts_at: z.date().nullable(),
|
||||||
|
ends_at: z.date().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CampaignConfigurationForm = ({
|
||||||
|
campaign,
|
||||||
|
}: CampaignConfigurationFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof CampaignConfigurationSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
starts_at: campaign.starts_at ? new Date(campaign.starts_at) : undefined,
|
||||||
|
ends_at: campaign.ends_at ? new Date(campaign.ends_at) : undefined,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CampaignConfigurationSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useUpdateCampaign(campaign.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
starts_at: data.starts_at || null,
|
||||||
|
ends_at: data.ends_at || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ campaign }) => {
|
||||||
|
toast.success(
|
||||||
|
t("campaigns.configuration.edit.successToast", {
|
||||||
|
name: campaign.name,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="starts_at"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
|
||||||
|
|
||||||
|
<Form.Control>
|
||||||
|
<DatePicker
|
||||||
|
granularity="minute"
|
||||||
|
hourCycle={12}
|
||||||
|
shouldCloseOnSelect={false}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="ends_at"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
|
||||||
|
|
||||||
|
<Form.Control>
|
||||||
|
<DatePicker
|
||||||
|
granularity="minute"
|
||||||
|
shouldCloseOnSelect={false}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
isLoading={isPending}
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./campaign-configuration-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CampaignConfiguration as Component } from "./campaign-configuration"
|
||||||
+7
-7
@@ -78,8 +78,12 @@ export const CreateCampaignForm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit} className="flex flex-col overflow-hidden">
|
||||||
<RouteFocusModal.Header>
|
<RouteFocusModal.Header />
|
||||||
|
<RouteFocusModal.Body className="flex size-full flex-col items-center overflow-auto py-16">
|
||||||
|
<CreateCampaignFormFields form={form} />
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
<RouteFocusModal.Close asChild>
|
<RouteFocusModal.Close asChild>
|
||||||
<Button size="small" variant="secondary">
|
<Button size="small" variant="secondary">
|
||||||
@@ -95,11 +99,7 @@ export const CreateCampaignForm = () => {
|
|||||||
{t("actions.create")}
|
{t("actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Footer>
|
||||||
|
|
||||||
<RouteFocusModal.Body className="flex flex-col items-center py-16">
|
|
||||||
<CreateCampaignFormFields form={form} />
|
|
||||||
</RouteFocusModal.Body>
|
|
||||||
</form>
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
import { useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
|
||||||
import { useCampaign } from "../../../hooks/api/campaigns"
|
import { useCampaign } from "../../../hooks/api/campaigns"
|
||||||
import { CampaignBudget } from "./components/campaign-budget"
|
import { CampaignBudget } from "./components/campaign-budget"
|
||||||
import { CampaignGeneralSection } from "./components/campaign-general-section"
|
import { CampaignGeneralSection } from "./components/campaign-general-section"
|
||||||
@@ -12,6 +11,9 @@ import after from "virtual:medusa/widgets/campaign/details/after"
|
|||||||
import before from "virtual:medusa/widgets/campaign/details/before"
|
import before from "virtual:medusa/widgets/campaign/details/before"
|
||||||
import sideAfter from "virtual:medusa/widgets/campaign/details/side/after"
|
import sideAfter from "virtual:medusa/widgets/campaign/details/side/after"
|
||||||
import sideBefore from "virtual:medusa/widgets/campaign/details/side/before"
|
import sideBefore from "virtual:medusa/widgets/campaign/details/side/before"
|
||||||
|
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||||
|
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||||
|
import { CampaignConfigurationSection } from "./components/campaign-configuration-section"
|
||||||
|
|
||||||
export const CampaignDetail = () => {
|
export const CampaignDetail = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
@@ -26,7 +28,14 @@ export const CampaignDetail = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (isLoading || !campaign) {
|
if (isLoading || !campaign) {
|
||||||
return <div>Loading...</div>
|
return (
|
||||||
|
<TwoColumnPageSkeleton
|
||||||
|
mainSections={2}
|
||||||
|
sidebarSections={3}
|
||||||
|
showJSON
|
||||||
|
showMetadata
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@@ -34,54 +43,27 @@ export const CampaignDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<TwoColumnPage
|
||||||
{before.widgets.map((w, i) => {
|
widgets={{
|
||||||
return (
|
after,
|
||||||
<div key={i}>
|
before,
|
||||||
<w.Component data={campaign} />
|
sideAfter,
|
||||||
</div>
|
sideBefore,
|
||||||
)
|
}}
|
||||||
})}
|
hasOutlet
|
||||||
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
showJSON
|
||||||
<div className="flex w-full flex-col gap-y-3">
|
showMetadata
|
||||||
|
data={campaign}
|
||||||
|
>
|
||||||
|
<TwoColumnPage.Main>
|
||||||
<CampaignGeneralSection campaign={campaign} />
|
<CampaignGeneralSection campaign={campaign} />
|
||||||
<CampaignPromotionSection campaign={campaign} />
|
<CampaignPromotionSection campaign={campaign} />
|
||||||
{after.widgets.map((w, i) => {
|
</TwoColumnPage.Main>
|
||||||
return (
|
<TwoColumnPage.Sidebar>
|
||||||
<div key={i}>
|
<CampaignConfigurationSection campaign={campaign} />
|
||||||
<w.Component data={campaign} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<div className="hidden xl:block">
|
|
||||||
<JsonViewSection data={campaign} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]">
|
|
||||||
{sideBefore.widgets.map((w, i) => {
|
|
||||||
return (
|
|
||||||
<div key={i}>
|
|
||||||
<w.Component data={campaign} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<CampaignSpend campaign={campaign} />
|
<CampaignSpend campaign={campaign} />
|
||||||
<CampaignBudget campaign={campaign} />
|
<CampaignBudget campaign={campaign} />
|
||||||
{sideAfter.widgets.map((w, i) => {
|
</TwoColumnPage.Sidebar>
|
||||||
return (
|
</TwoColumnPage>
|
||||||
<div key={i}>
|
|
||||||
<w.Component data={campaign} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<div className="xl:hidden">
|
|
||||||
<JsonViewSection data={campaign} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DateRangeDisplay } from "../../../../../components/common/date-range-display"
|
||||||
|
|
||||||
|
type CampaignConfigurationSectionProps = {
|
||||||
|
campaign: HttpTypes.AdminCampaign
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CampaignConfigurationSection = ({
|
||||||
|
campaign,
|
||||||
|
}: CampaignConfigurationSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Heading level="h2">{t("campaigns.configuration.header")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
to: "configuration",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DateRangeDisplay
|
||||||
|
startsAt={campaign.starts_at}
|
||||||
|
endsAt={campaign.ends_at}
|
||||||
|
showTime
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./campaign-configuration-section";
|
||||||
-21
@@ -12,7 +12,6 @@ import {
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { formatDate } from "../../../../../components/common/date"
|
|
||||||
import { useDeleteCampaign } from "../../../../../hooks/api/campaigns"
|
import { useDeleteCampaign } from "../../../../../hooks/api/campaigns"
|
||||||
import { currencies } from "../../../../../lib/data/currencies"
|
import { currencies } from "../../../../../lib/data/currencies"
|
||||||
import {
|
import {
|
||||||
@@ -133,26 +132,6 @@ export const CampaignGeneralSection = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
|
||||||
{t("campaigns.fields.start_date")}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{campaign.starts_at ? formatDate(campaign.starts_at) : "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
|
||||||
{t("campaigns.fields.end_date")}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{campaign.ends_at ? formatDate(campaign.ends_at) : "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+2
-2
@@ -10,9 +10,9 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
|
|||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { useAddOrRemoveCampaignPromotions } from "../../../../../hooks/api/campaigns"
|
import { useAddOrRemoveCampaignPromotions } from "../../../../../hooks/api/campaigns"
|
||||||
import { usePromotions } from "../../../../../hooks/api/promotions"
|
import { usePromotions } from "../../../../../hooks/api/promotions"
|
||||||
import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/use-promotion-table-columns"
|
import { usePromotionTableColumns } from "../../../../../hooks/table/columns/use-promotion-table-columns"
|
||||||
import { usePromotionTableFilters } from "../../../../../hooks/table/filters/use-promotion-table-filters"
|
import { usePromotionTableFilters } from "../../../../../hooks/table/filters/use-promotion-table-filters"
|
||||||
import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query"
|
import { usePromotionTableQuery } from "../../../../../hooks/table/query/use-promotion-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
|
||||||
type CampaignPromotionSectionProps = {
|
type CampaignPromotionSectionProps = {
|
||||||
|
|||||||
+2
-5
@@ -74,13 +74,10 @@ export const ProductCreateForm = ({
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return regions.reduce(
|
return regions.reduce((acc, reg) => {
|
||||||
(acc, reg) => {
|
|
||||||
acc[reg.id] = reg.currency_code
|
acc[reg.id] = reg.currency_code
|
||||||
return acc
|
return acc
|
||||||
},
|
}, {} as Record<string, string>)
|
||||||
{} as Record<string, string>
|
|
||||||
)
|
|
||||||
}, [regions])
|
}, [regions])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+24
-9
@@ -1,6 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { AdminCampaign, AdminPromotion } from "@medusajs/types"
|
import { AdminCampaign, AdminPromotion } from "@medusajs/types"
|
||||||
import { Button, RadioGroup, Select, Text } from "@medusajs/ui"
|
import { Button, RadioGroup, Select, Text, toast } from "@medusajs/ui"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useForm, useWatch } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
@@ -44,7 +44,7 @@ export const AddCampaignPromotionFields = ({
|
|||||||
const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId)
|
const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col gap-y-8">
|
<div className="flex flex-col gap-y-8">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="campaign_choice"
|
name="campaign_choice"
|
||||||
@@ -55,7 +55,7 @@ export const AddCampaignPromotionFields = ({
|
|||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="flex gap-y-3"
|
className="grid grid-cols-1 gap-3"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
@@ -154,6 +154,8 @@ export const AddCampaignPromotionForm = ({
|
|||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const { campaign } = promotion
|
const { campaign } = promotion
|
||||||
|
|
||||||
|
const originalId = campaign?.id
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof EditPromotionSchema>>({
|
const form = useForm<zod.infer<typeof EditPromotionSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
campaign_id: campaign?.id,
|
campaign_id: campaign?.id,
|
||||||
@@ -162,11 +164,21 @@ export const AddCampaignPromotionForm = ({
|
|||||||
resolver: zodResolver(EditPromotionSchema),
|
resolver: zodResolver(EditPromotionSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { setValue } = form
|
||||||
|
|
||||||
const { mutateAsync, isPending } = useUpdatePromotion(promotion.id)
|
const { mutateAsync, isPending } = useUpdatePromotion(promotion.id)
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{ campaign_id: data.campaign_id },
|
{ campaign_id: data.campaign_id },
|
||||||
{ onSuccess: () => handleSuccess() }
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("promotions.campaign.edit.successToast"))
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(e.message)
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -177,18 +189,21 @@ export const AddCampaignPromotionForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (watchCampaignChoice === "none") {
|
if (watchCampaignChoice === "none") {
|
||||||
form.setValue("campaign_id", null)
|
setValue("campaign_id", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (watchCampaignChoice === "existing") {
|
if (watchCampaignChoice === "existing") {
|
||||||
form.setValue("campaign_id", campaign?.id)
|
setValue("campaign_id", originalId)
|
||||||
}
|
}
|
||||||
}, [watchCampaignChoice])
|
}, [watchCampaignChoice, setValue, originalId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer.Form form={form}>
|
<RouteDrawer.Form form={form}>
|
||||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
<form
|
||||||
<RouteDrawer.Body>
|
onSubmit={handleSubmit}
|
||||||
|
className="flex size-full flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="size-full overflow-auto">
|
||||||
<AddCampaignPromotionFields
|
<AddCampaignPromotionFields
|
||||||
form={form}
|
form={form}
|
||||||
campaigns={campaigns}
|
campaigns={campaigns}
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ export const PromotionAddCampaign = () => {
|
|||||||
return (
|
return (
|
||||||
<RouteDrawer>
|
<RouteDrawer>
|
||||||
<RouteDrawer.Header>
|
<RouteDrawer.Header>
|
||||||
<Heading>{t("promotions.addToCampaign.title")}</Heading>
|
<Heading>{t("promotions.campaign.edit.header")}</Heading>
|
||||||
</RouteDrawer.Header>
|
</RouteDrawer.Header>
|
||||||
|
|
||||||
{!isPending && !areCampaignsLoading && promotion && campaigns && (
|
{!isPending && !areCampaignsLoading && promotion && campaigns && (
|
||||||
|
|||||||
+62
-42
@@ -326,17 +326,15 @@ export const CreatePromotionForm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form
|
<form className="flex h-full flex-col" onSubmit={handleSubmit}>
|
||||||
className="flex h-full flex-col overflow-scroll"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<ProgressTabs
|
<ProgressTabs
|
||||||
value={tab}
|
value={tab}
|
||||||
onValueChange={(tab) => handleTabChange(tab as Tab)}
|
onValueChange={(tab) => handleTabChange(tab as Tab)}
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<RouteFocusModal.Header>
|
<RouteFocusModal.Header>
|
||||||
<div className="flex w-full items-center justify-between gap-x-4">
|
<div className="flex w-full items-center justify-between gap-x-4">
|
||||||
<div className="-my-2 w-full max-w-[400px] border-l">
|
<div className="-my-2 w-full max-w-[600px] border-l">
|
||||||
<ProgressTabs.List className="grid w-full grid-cols-3">
|
<ProgressTabs.List className="grid w-full grid-cols-3">
|
||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -358,39 +356,16 @@ export const CreatePromotionForm = () => {
|
|||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
</ProgressTabs.List>
|
</ProgressTabs.List>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<RouteFocusModal.Close asChild>
|
|
||||||
<Button variant="secondary" size="small">
|
|
||||||
{t("actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
</RouteFocusModal.Close>
|
|
||||||
|
|
||||||
{tab === Tab.CAMPAIGN ? (
|
|
||||||
<Button
|
|
||||||
key="save-btn"
|
|
||||||
type="submit"
|
|
||||||
size="small"
|
|
||||||
isLoading={false}
|
|
||||||
>
|
|
||||||
{t("actions.save")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
key="continue-btn"
|
|
||||||
type="button"
|
|
||||||
onClick={handleContinue}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t("actions.continue")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
|
|
||||||
<RouteFocusModal.Body className="mx-auto my-20 w-[800px]">
|
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||||
<ProgressTabs.Content value={Tab.TYPE}>
|
<ProgressTabs.Content
|
||||||
|
value={Tab.TYPE}
|
||||||
|
className="size-full overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="flex size-full flex-col items-center">
|
||||||
|
<div className="w-full max-w-[720px] py-16">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="template_id"
|
name="template_id"
|
||||||
@@ -423,12 +398,16 @@ export const CreatePromotionForm = () => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
|
|
||||||
<ProgressTabs.Content
|
<ProgressTabs.Content
|
||||||
value={Tab.PROMOTION}
|
value={Tab.PROMOTION}
|
||||||
className="flex flex-1 flex-col gap-8"
|
className="size-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
|
<div className="flex size-full flex-col items-center">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8 py-16">
|
||||||
<Heading level="h1" className="text-fg-base">
|
<Heading level="h1" className="text-fg-base">
|
||||||
{t(`promotions.sections.details`)}
|
{t(`promotions.sections.details`)}
|
||||||
|
|
||||||
@@ -480,7 +459,9 @@ export const CreatePromotionForm = () => {
|
|||||||
|
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
value={"true"}
|
value={"true"}
|
||||||
label={t("promotions.form.method.automatic.title")}
|
label={t(
|
||||||
|
"promotions.form.method.automatic.title"
|
||||||
|
)}
|
||||||
description={t(
|
description={t(
|
||||||
"promotions.form.method.automatic.description"
|
"promotions.form.method.automatic.description"
|
||||||
)}
|
)}
|
||||||
@@ -533,7 +514,9 @@ export const CreatePromotionForm = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("promotions.fields.type")}</Form.Label>
|
<Form.Label>
|
||||||
|
{t("promotions.fields.type")}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="flex gap-y-3"
|
className="flex gap-y-3"
|
||||||
@@ -542,7 +525,9 @@ export const CreatePromotionForm = () => {
|
|||||||
>
|
>
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
value={"standard"}
|
value={"standard"}
|
||||||
label={t("promotions.form.type.standard.title")}
|
label={t(
|
||||||
|
"promotions.form.type.standard.title"
|
||||||
|
)}
|
||||||
description={t(
|
description={t(
|
||||||
"promotions.form.type.standard.description"
|
"promotions.form.type.standard.description"
|
||||||
)}
|
)}
|
||||||
@@ -651,11 +636,11 @@ export const CreatePromotionForm = () => {
|
|||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
onChange(value ? parseInt(value) : "")
|
onChange(value ? parseInt(value) : "")
|
||||||
}}
|
}}
|
||||||
code={currencyCode}
|
code={currencyCode || "USD"}
|
||||||
symbol={
|
symbol={
|
||||||
currencyCode
|
currencyCode
|
||||||
? getCurrencySymbol(currencyCode)
|
? getCurrencySymbol(currencyCode)
|
||||||
: ""
|
: "$"
|
||||||
}
|
}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={!currencyCode}
|
disabled={!currencyCode}
|
||||||
@@ -811,19 +796,54 @@ export const CreatePromotionForm = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
|
|
||||||
<ProgressTabs.Content
|
<ProgressTabs.Content
|
||||||
value={Tab.CAMPAIGN}
|
value={Tab.CAMPAIGN}
|
||||||
className="flex flex-col items-center"
|
className="size-full overflow-auto"
|
||||||
>
|
>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8 py-16">
|
||||||
<AddCampaignPromotionFields
|
<AddCampaignPromotionFields
|
||||||
form={form}
|
form={form}
|
||||||
campaigns={campaigns || []}
|
campaigns={campaigns || []}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</ProgressTabs>
|
</ProgressTabs>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
|
||||||
|
{tab === Tab.CAMPAIGN ? (
|
||||||
|
<Button
|
||||||
|
key="save-btn"
|
||||||
|
type="submit"
|
||||||
|
size="small"
|
||||||
|
isLoading={false}
|
||||||
|
>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
key="continue-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={handleContinue}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t("actions.continue")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Footer>
|
||||||
</form>
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-1
@@ -2,5 +2,9 @@ import { RouteFocusModal } from "../../../components/modals"
|
|||||||
import { CreatePromotionForm } from "./components/create-promotion-form/create-promotion-form"
|
import { CreatePromotionForm } from "./components/create-promotion-form/create-promotion-form"
|
||||||
|
|
||||||
export const PromotionCreate = () => {
|
export const PromotionCreate = () => {
|
||||||
return <RouteFocusModal>{<CreatePromotionForm />}</RouteFocusModal>
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
<CreatePromotionForm />
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+48
-72
@@ -1,97 +1,73 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { ArrowUpRightOnBox, PencilSquare } from "@medusajs/icons"
|
||||||
import { CampaignDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Container, Heading, Text } from "@medusajs/ui"
|
import { Container, Heading, Text } from "@medusajs/ui"
|
||||||
import { format } from "date-fns"
|
|
||||||
import { Fragment } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DateRangeDisplay } from "../../../../../components/common/date-range-display"
|
||||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
|
||||||
function formatDate(date?: string | Date) {
|
const CampaignDetailSection = ({
|
||||||
if (!date) {
|
campaign,
|
||||||
return "-"
|
}: {
|
||||||
}
|
campaign: HttpTypes.AdminCampaign
|
||||||
|
}) => {
|
||||||
return format(new Date(date), "dd MMM yyyy")
|
|
||||||
}
|
|
||||||
|
|
||||||
const CampaignDetailSection = ({ campaign }: { campaign: CampaignDTO }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className="flex flex-col gap-y-3">
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
<div className="text-ui-fg-muted flex items-center gap-x-1.5">
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<Text size="small" weight="plus" className="text-ui-fg-base">
|
||||||
{t("campaigns.fields.name")}
|
{campaign.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text size="small" weight="plus">
|
||||||
<div className="flex items-center gap-1">
|
·
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
</Text>
|
||||||
{campaign?.name}
|
<Text size="small" weight="plus">
|
||||||
|
{campaign.campaign_identifier}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
<DateRangeDisplay
|
||||||
|
startsAt={campaign.starts_at}
|
||||||
|
endsAt={campaign.ends_at}
|
||||||
|
showTime
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("campaigns.fields.identifier")}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
|
||||||
{campaign?.campaign_identifier}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("campaigns.fields.start_date")}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
|
||||||
{formatDate(campaign?.starts_at)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("campaigns.fields.end_date") || "-"}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
|
||||||
{formatDate(campaign?.ends_at)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => {
|
export const CampaignSection = ({
|
||||||
|
campaign,
|
||||||
|
}: {
|
||||||
|
campaign: HttpTypes.AdminCampaign | null
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
|
||||||
return (
|
const actions = [
|
||||||
<Container className="divide-y p-0">
|
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
|
||||||
<Heading level="h2">{t("promotions.fields.campaign")}</Heading>
|
|
||||||
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
{
|
||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
to: "add-to-campaign",
|
to: "add-to-campaign",
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
|
|
||||||
|
if (campaign) {
|
||||||
|
actions.unshift({
|
||||||
|
label: t("promotions.campaign.actions.goToCampaign"),
|
||||||
|
to: `/campaigns/${campaign.id}`,
|
||||||
|
icon: <ArrowUpRightOnBox />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Heading level="h2">{t("promotions.fields.campaign")}</Heading>
|
||||||
|
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -101,7 +77,7 @@ export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => {
|
|||||||
<CampaignDetailSection campaign={campaign} />
|
<CampaignDetailSection campaign={campaign} />
|
||||||
) : (
|
) : (
|
||||||
<NoRecords
|
<NoRecords
|
||||||
className="h-[180px] p-6 text-center"
|
className="h-[180px] pt-4 text-center"
|
||||||
title="Not part of a campaign"
|
title="Not part of a campaign"
|
||||||
message="Add this promotion to an existing campaign"
|
message="Add this promotion to an existing campaign"
|
||||||
action={{
|
action={{
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
import { AdminGetPromotionRulesRes, PromotionRuleTypes } from "@medusajs/types"
|
import { HttpTypes, PromotionRuleTypes } from "@medusajs/types"
|
||||||
import { Badge, Container, Heading } from "@medusajs/ui"
|
import { Badge, Container, Heading } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import { BadgeListSummary } from "../../../../../components/common/badge-list-su
|
|||||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
|
||||||
type RuleProps = {
|
type RuleProps = {
|
||||||
rule: AdminGetPromotionRulesRes
|
rule: HttpTypes.AdminPromotionRule
|
||||||
}
|
}
|
||||||
|
|
||||||
function RuleBlock({ rule }: RuleProps) {
|
function RuleBlock({ rule }: RuleProps) {
|
||||||
@@ -42,7 +42,7 @@ function RuleBlock({ rule }: RuleProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PromotionConditionsSectionProps = {
|
type PromotionConditionsSectionProps = {
|
||||||
rules: AdminGetPromotionRulesRes
|
rules: HttpTypes.AdminPromotionRule[]
|
||||||
ruleType: PromotionRuleTypes
|
ruleType: PromotionRuleTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-19
@@ -1,5 +1,5 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { PromotionDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Container,
|
Container,
|
||||||
@@ -14,13 +14,37 @@ import { useNavigate } from "react-router-dom"
|
|||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { useDeletePromotion } from "../../../../../hooks/api/promotions"
|
import { useDeletePromotion } from "../../../../../hooks/api/promotions"
|
||||||
|
import { formatCurrency } from "../../../../../lib/format-currency"
|
||||||
|
import { formatPercentage } from "../../../../../lib/percentage-helpers"
|
||||||
import {
|
import {
|
||||||
getPromotionStatus,
|
getPromotionStatus,
|
||||||
PromotionStatus,
|
PromotionStatus,
|
||||||
} from "../../../../../lib/promotions"
|
} from "../../../../../lib/promotions"
|
||||||
|
|
||||||
type PromotionGeneralSectionProps = {
|
type PromotionGeneralSectionProps = {
|
||||||
promotion: PromotionDTO
|
promotion: HttpTypes.AdminPromotion
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayValue(promotion: HttpTypes.AdminPromotion) {
|
||||||
|
const value = promotion.application_method?.value
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promotion.application_method?.type === "fixed") {
|
||||||
|
const currency = promotion.application_method?.currency_code
|
||||||
|
|
||||||
|
if (!currency) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatCurrency(value, currency)
|
||||||
|
} else if (promotion.application_method?.type === "percentage") {
|
||||||
|
return formatPercentage(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PromotionGeneralSection = ({
|
export const PromotionGeneralSection = ({
|
||||||
@@ -64,6 +88,8 @@ export const PromotionGeneralSection = ({
|
|||||||
string
|
string
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const displayValue = getDisplayValue(promotion)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
@@ -113,18 +139,15 @@ export const PromotionGeneralSection = ({
|
|||||||
{t("fields.code")}
|
{t("fields.code")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<Copy content={promotion.code!} asChild>
|
||||||
<Text
|
<Badge
|
||||||
size="small"
|
size="2xsmall"
|
||||||
weight="plus"
|
rounded="full"
|
||||||
leading="compact"
|
className="cursor-pointer text-pretty"
|
||||||
className="text-pretty"
|
|
||||||
>
|
>
|
||||||
{promotion.code}
|
{promotion.code}
|
||||||
</Text>
|
</Badge>
|
||||||
|
</Copy>
|
||||||
<Copy content={promotion.code!} variant="mini" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
@@ -142,17 +165,16 @@ export const PromotionGeneralSection = ({
|
|||||||
{t("promotions.fields.value")}
|
{t("promotions.fields.value")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
<div className="flex items-center gap-x-2">
|
||||||
<Text className="inline pr-3" size="small" leading="compact">
|
<Text className="inline" size="small" leading="compact">
|
||||||
{promotion.application_method?.value}
|
{displayValue || "-"}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{promotion?.application_method?.type === "fixed" && (
|
{promotion?.application_method?.type === "fixed" && (
|
||||||
<Badge size="xsmall">
|
<Badge size="2xsmall" rounded="full">
|
||||||
{promotion?.application_method?.currency_code}
|
{promotion?.application_method?.currency_code?.toUpperCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
|
|||||||
+24
-32
@@ -1,6 +1,7 @@
|
|||||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
import { useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||||
|
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||||
import { usePromotion, usePromotionRules } from "../../../hooks/api/promotions"
|
import { usePromotion, usePromotionRules } from "../../../hooks/api/promotions"
|
||||||
import { CampaignSection } from "./components/campaign-section"
|
import { CampaignSection } from "./components/campaign-section"
|
||||||
import { PromotionConditionsSection } from "./components/promotion-conditions-section"
|
import { PromotionConditionsSection } from "./components/promotion-conditions-section"
|
||||||
@@ -9,6 +10,8 @@ import { promotionLoader } from "./loader"
|
|||||||
|
|
||||||
import after from "virtual:medusa/widgets/promotion/details/after"
|
import after from "virtual:medusa/widgets/promotion/details/after"
|
||||||
import before from "virtual:medusa/widgets/promotion/details/before"
|
import before from "virtual:medusa/widgets/promotion/details/before"
|
||||||
|
import sideAfter from "virtual:medusa/widgets/promotion/details/side/after"
|
||||||
|
import sideBefore from "virtual:medusa/widgets/promotion/details/side/before"
|
||||||
|
|
||||||
export const PromotionDetail = () => {
|
export const PromotionDetail = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
@@ -28,20 +31,24 @@ export const PromotionDetail = () => {
|
|||||||
const { rules: buyRules } = usePromotionRules(id!, "buy-rules", query)
|
const { rules: buyRules } = usePromotionRules(id!, "buy-rules", query)
|
||||||
|
|
||||||
if (isLoading || !promotion) {
|
if (isLoading || !promotion) {
|
||||||
return <div>Loading...</div>
|
return (
|
||||||
|
<TwoColumnPageSkeleton mainSections={3} sidebarSections={1} showJSON />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<TwoColumnPage
|
||||||
{before.widgets.map((w, i) => {
|
data={promotion}
|
||||||
return (
|
widgets={{
|
||||||
<div key={i}>
|
after,
|
||||||
<w.Component data={promotion} />
|
before,
|
||||||
</div>
|
sideAfter,
|
||||||
)
|
sideBefore,
|
||||||
})}
|
}}
|
||||||
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
hasOutlet
|
||||||
<div className="flex w-full flex-col gap-y-3">
|
showJSON
|
||||||
|
>
|
||||||
|
<TwoColumnPage.Main>
|
||||||
<PromotionGeneralSection promotion={promotion} />
|
<PromotionGeneralSection promotion={promotion} />
|
||||||
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
|
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
|
||||||
<PromotionConditionsSection
|
<PromotionConditionsSection
|
||||||
@@ -54,25 +61,10 @@ export const PromotionDetail = () => {
|
|||||||
ruleType={"buy-rules"}
|
ruleType={"buy-rules"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{after.widgets.map((w, i) => {
|
</TwoColumnPage.Main>
|
||||||
return (
|
<TwoColumnPage.Sidebar>
|
||||||
<div key={i}>
|
|
||||||
<w.Component data={promotion} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<div className="hidden xl:block">
|
|
||||||
<JsonViewSection data={promotion} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]">
|
|
||||||
<CampaignSection campaign={promotion.campaign!} />
|
<CampaignSection campaign={promotion.campaign!} />
|
||||||
<div className="xl:hidden">
|
</TwoColumnPage.Sidebar>
|
||||||
<JsonViewSection data={promotion} />
|
</TwoColumnPage>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+8
-39
@@ -1,23 +1,13 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { PromotionDTO } from "@medusajs/types"
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
import {
|
import { Button, CurrencyInput, Input, RadioGroup, Text } from "@medusajs/ui"
|
||||||
Button,
|
|
||||||
clx,
|
|
||||||
CurrencyInput,
|
|
||||||
Input,
|
|
||||||
RadioGroup,
|
|
||||||
Text,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useForm, useWatch } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input"
|
import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import {
|
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||||
RouteDrawer,
|
|
||||||
useRouteModal,
|
|
||||||
} from "../../../../../components/modals"
|
|
||||||
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||||
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
|
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
|
||||||
|
|
||||||
@@ -80,9 +70,12 @@ export const EditPromotionDetailsForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer.Form form={form}>
|
<RouteDrawer.Form form={form}>
|
||||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
<form
|
||||||
<RouteDrawer.Body>
|
onSubmit={handleSubmit}
|
||||||
<div className="flex h-full flex-col gap-y-8">
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="is_automatic"
|
name="is_automatic"
|
||||||
@@ -98,10 +91,6 @@ export const EditPromotionDetailsForm = ({
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"false" === field.value,
|
|
||||||
})}
|
|
||||||
value={"false"}
|
value={"false"}
|
||||||
label={t("promotions.form.method.code.title")}
|
label={t("promotions.form.method.code.title")}
|
||||||
description={t(
|
description={t(
|
||||||
@@ -109,10 +98,6 @@ export const EditPromotionDetailsForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"true" === field.value,
|
|
||||||
})}
|
|
||||||
value={"true"}
|
value={"true"}
|
||||||
label={t("promotions.form.method.automatic.title")}
|
label={t("promotions.form.method.automatic.title")}
|
||||||
description={t(
|
description={t(
|
||||||
@@ -171,10 +156,6 @@ export const EditPromotionDetailsForm = ({
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"fixed" === field.value,
|
|
||||||
})}
|
|
||||||
value={"fixed"}
|
value={"fixed"}
|
||||||
label={t("promotions.form.value_type.fixed.title")}
|
label={t("promotions.form.value_type.fixed.title")}
|
||||||
description={t(
|
description={t(
|
||||||
@@ -183,10 +164,6 @@ export const EditPromotionDetailsForm = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"percentage" === field.value,
|
|
||||||
})}
|
|
||||||
value={"percentage"}
|
value={"percentage"}
|
||||||
label={t(
|
label={t(
|
||||||
"promotions.form.value_type.percentage.title"
|
"promotions.form.value_type.percentage.title"
|
||||||
@@ -263,10 +240,6 @@ export const EditPromotionDetailsForm = ({
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"each" === field.value,
|
|
||||||
})}
|
|
||||||
value={"each"}
|
value={"each"}
|
||||||
label={t("promotions.form.allocation.each.title")}
|
label={t("promotions.form.allocation.each.title")}
|
||||||
description={t(
|
description={t(
|
||||||
@@ -275,10 +248,6 @@ export const EditPromotionDetailsForm = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
className={clx("basis-1/2", {
|
|
||||||
"border-ui-border-interactive border-2":
|
|
||||||
"across" === field.value,
|
|
||||||
})}
|
|
||||||
value={"across"}
|
value={"across"}
|
||||||
label={t("promotions.form.allocation.across.title")}
|
label={t("promotions.form.allocation.across.title")}
|
||||||
description={t(
|
description={t(
|
||||||
|
|||||||
+3
-3
@@ -13,9 +13,9 @@ import {
|
|||||||
useDeletePromotion,
|
useDeletePromotion,
|
||||||
usePromotions,
|
usePromotions,
|
||||||
} from "../../../../../hooks/api/promotions"
|
} from "../../../../../hooks/api/promotions"
|
||||||
import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/use-promotion-table-columns"
|
import { usePromotionTableColumns } from "../../../../../hooks/table/columns/use-promotion-table-columns"
|
||||||
import { usePromotionTableFilters } from "../../../../../hooks/table/filters-v2/use-promotion-table-filters"
|
import { usePromotionTableFilters } from "../../../../../hooks/table/filters/use-promotion-table-filters"
|
||||||
import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query"
|
import { usePromotionTableQuery } from "../../../../../hooks/table/query/use-promotion-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { promotionsLoader } from "../../loader"
|
import { promotionsLoader } from "../../loader"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user