@lodashventure/medusa-campaign 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +939 -504
- package/.medusa/server/src/admin/index.mjs +941 -506
- package/.medusa/server/src/api/admin/buy-x-get-y/[id]/route.js +2 -6
- package/.medusa/server/src/api/admin/coupons/[id]/route.js +76 -0
- package/.medusa/server/src/api/admin/coupons/route.js +88 -0
- package/.medusa/server/src/api/middlewares.js +32 -1
- package/.medusa/server/src/api/store/campaigns/route.js +78 -7
- package/.medusa/server/src/api/store/coupons/public/route.js +110 -0
- package/.medusa/server/src/api/store/customers/me/coupons/route.js +148 -0
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +2 -2
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +2 -2
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251101000000.js +16 -0
- package/.medusa/server/src/modules/custom-campaigns/types/campaign-type.enum.js +2 -1
- package/.medusa/server/src/workflows/custom-campaign/createCouponCampaignWorkflow.js +105 -0
- package/.medusa/server/src/workflows/custom-campaign/updateCouponCampaignWorkflow.js +59 -0
- package/.medusa/server/src/workflows/index.js +6 -2
- package/package.json +15 -30
- package/src/admin/components/BuyXGetYForm.tsx +24 -13
- package/src/admin/components/CouponForm.tsx +352 -0
- package/src/admin/components/CouponPage.tsx +104 -0
- package/src/admin/components/ProductSelector.tsx +22 -11
- package/src/admin/hooks/useCouponById.ts +36 -0
- package/src/admin/hooks/useCoupons.ts +46 -0
- package/src/admin/hooks/useFlashSaleById.ts +36 -10
- package/src/admin/hooks/useFlashSales.ts +36 -10
- package/src/admin/routes/coupons/[id]/page.tsx +147 -0
- package/src/admin/routes/coupons/create/page.tsx +49 -0
- package/src/admin/routes/coupons/page.tsx +15 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +2 -11
- package/src/admin/routes/flash-sales/create/page.tsx +0 -6
- package/src/admin/widgets/campaign-detail-widget.tsx +33 -26
- package/src/api/admin/buy-x-get-y/[id]/route.ts +11 -15
- package/src/api/admin/coupons/[id]/route.ts +98 -0
- package/src/api/admin/coupons/route.ts +109 -0
- package/src/api/middlewares.ts +34 -0
- package/src/api/store/campaigns/route.ts +107 -24
- package/src/api/store/coupons/public/route.ts +165 -0
- package/src/api/store/customers/me/coupons/route.ts +244 -0
- package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +1 -1
- package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +1 -1
- package/src/modules/custom-campaigns/migrations/Migration20251101000000.ts +21 -0
- package/src/modules/custom-campaigns/types/campaign-type.enum.ts +1 -0
- package/src/workflows/custom-campaign/createCouponCampaignWorkflow.ts +176 -0
- package/src/workflows/custom-campaign/updateCouponCampaignWorkflow.ts +105 -0
- package/src/workflows/index.ts +3 -1
- package/src/admin/widgets/campaign-stats-widget.tsx +0 -238
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { CampaignDTO } from "@medusajs/framework/types";
|
|
2
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
1
|
import axios from "axios";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
type CampaignDTO = any;
|
|
4
5
|
|
|
5
6
|
export const useFlashSales = (pagination: {
|
|
6
7
|
limit: number;
|
|
7
8
|
offset: number;
|
|
8
9
|
}) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const [data, setData] = useState<{
|
|
11
|
+
campaigns: CampaignDTO[];
|
|
12
|
+
count: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
offset: number;
|
|
15
|
+
} | null>(null);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
17
|
+
const [error, setError] = useState<Error | null>(null);
|
|
18
|
+
|
|
19
|
+
const fetchFlashSales = async () => {
|
|
20
|
+
setIsLoading(true);
|
|
21
|
+
setError(null);
|
|
22
|
+
try {
|
|
23
|
+
const response = await axios.get<{
|
|
13
24
|
campaigns: CampaignDTO[];
|
|
14
25
|
count: number;
|
|
15
26
|
limit: number;
|
|
@@ -17,9 +28,24 @@ export const useFlashSales = (pagination: {
|
|
|
17
28
|
}>("/admin/flash-sales", {
|
|
18
29
|
params: pagination,
|
|
19
30
|
});
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
31
|
+
setData(response.data);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(
|
|
34
|
+
err instanceof Error ? err : new Error("Failed to fetch flash sales"),
|
|
35
|
+
);
|
|
36
|
+
} finally {
|
|
37
|
+
setIsLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetchFlashSales();
|
|
43
|
+
}, [pagination.limit, pagination.offset]);
|
|
23
44
|
|
|
24
|
-
return
|
|
45
|
+
return {
|
|
46
|
+
data,
|
|
47
|
+
isLoading,
|
|
48
|
+
error,
|
|
49
|
+
refetch: fetchFlashSales,
|
|
50
|
+
};
|
|
25
51
|
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
2
|
+
import { Tag } from "@medusajs/icons";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Container,
|
|
6
|
+
Tabs,
|
|
7
|
+
toast,
|
|
8
|
+
} from "@medusajs/ui";
|
|
9
|
+
import axios from "axios";
|
|
10
|
+
import dayjs from "dayjs";
|
|
11
|
+
import { FC, useState } from "react";
|
|
12
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
13
|
+
import {
|
|
14
|
+
CouponForm,
|
|
15
|
+
CouponFormData,
|
|
16
|
+
} from "../../../components/CouponForm";
|
|
17
|
+
import { useCouponById } from "../../../hooks/useCouponById";
|
|
18
|
+
import { CampaignDetailForm } from "../../../components/campaign-detail-form";
|
|
19
|
+
|
|
20
|
+
const CouponDetail: FC = () => {
|
|
21
|
+
const { id } = useParams<{ id: string }>();
|
|
22
|
+
const navigate = useNavigate();
|
|
23
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
24
|
+
|
|
25
|
+
const { data, isLoading, refetch } = useCouponById(id ?? "");
|
|
26
|
+
|
|
27
|
+
if (isLoading) {
|
|
28
|
+
return (
|
|
29
|
+
<Container className="flex items-center justify-center h-screen">
|
|
30
|
+
<div className="animate-spin h-10 w-10 border-4 border-primary rounded-full border-t-transparent"></div>
|
|
31
|
+
</Container>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!data?.campaign) {
|
|
36
|
+
return (
|
|
37
|
+
<Container>
|
|
38
|
+
<p>Coupon not found</p>
|
|
39
|
+
</Container>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const campaign = data.campaign;
|
|
44
|
+
const promotion = campaign.promotions?.[0];
|
|
45
|
+
|
|
46
|
+
const initialValues: Partial<CouponFormData> = {
|
|
47
|
+
name: campaign.name ?? "",
|
|
48
|
+
description: campaign.description ?? "",
|
|
49
|
+
type: "coupon",
|
|
50
|
+
code: promotion?.code ?? "",
|
|
51
|
+
discount_type: (promotion?.application_method?.type as CouponFormData["discount_type"]) ?? "percentage",
|
|
52
|
+
discount_value: Number(promotion?.application_method?.value ?? 0),
|
|
53
|
+
currency_code: promotion?.application_method?.currency_code ?? undefined,
|
|
54
|
+
starts_at: campaign.starts_at
|
|
55
|
+
? dayjs(campaign.starts_at).format("YYYY-MM-DDTHH:mm")
|
|
56
|
+
: undefined,
|
|
57
|
+
ends_at: campaign.ends_at
|
|
58
|
+
? dayjs(campaign.ends_at).format("YYYY-MM-DDTHH:mm")
|
|
59
|
+
: undefined,
|
|
60
|
+
allocation: promotion?.application_method?.allocation ?? "total",
|
|
61
|
+
target_type: promotion?.application_method?.target_type ?? "order",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleSubmit = async (formData: CouponFormData) => {
|
|
65
|
+
if (!id) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await axios.put(`/admin/coupons/${id}`, {
|
|
71
|
+
...formData,
|
|
72
|
+
starts_at: new Date(formData.starts_at).toUTCString(),
|
|
73
|
+
ends_at: new Date(formData.ends_at).toUTCString(),
|
|
74
|
+
currency_code:
|
|
75
|
+
formData.discount_type === "fixed"
|
|
76
|
+
? formData.currency_code
|
|
77
|
+
: undefined,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
toast.success("Coupon updated successfully");
|
|
81
|
+
setIsEditing(false);
|
|
82
|
+
refetch();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
let message = "Failed to update coupon";
|
|
85
|
+
if (axios.isAxiosError(error)) {
|
|
86
|
+
message = error.response?.data?.message ?? message;
|
|
87
|
+
} else if (error instanceof Error) {
|
|
88
|
+
message = error.message;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
toast.error("Failed to update coupon", {
|
|
92
|
+
description: message,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Container>
|
|
99
|
+
<div className="mb-6 flex items-center justify-between">
|
|
100
|
+
<div>
|
|
101
|
+
<h1 className="text-2xl font-semibold mb-1">{campaign.name}</h1>
|
|
102
|
+
<p className="text-ui-fg-subtle">
|
|
103
|
+
Manage coupon configuration and content
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="flex gap-2">
|
|
107
|
+
<Button variant="secondary" onClick={() => navigate("/coupons")}>
|
|
108
|
+
Back to Coupons
|
|
109
|
+
</Button>
|
|
110
|
+
<Button onClick={() => setIsEditing((prev) => !prev)}>
|
|
111
|
+
{isEditing ? "View Mode" : "Edit Mode"}
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<Tabs defaultValue="settings">
|
|
117
|
+
<Tabs.List>
|
|
118
|
+
<Tabs.Trigger value="settings">Coupon Settings</Tabs.Trigger>
|
|
119
|
+
<Tabs.Trigger value="details">Coupon Details</Tabs.Trigger>
|
|
120
|
+
</Tabs.List>
|
|
121
|
+
|
|
122
|
+
<Tabs.Content value="settings" className="mt-6">
|
|
123
|
+
<CouponForm
|
|
124
|
+
initialData={initialValues}
|
|
125
|
+
onSubmit={handleSubmit}
|
|
126
|
+
onCancel={() => navigate("/coupons")}
|
|
127
|
+
disabled={!isEditing}
|
|
128
|
+
/>
|
|
129
|
+
</Tabs.Content>
|
|
130
|
+
|
|
131
|
+
<Tabs.Content value="details" className="mt-6">
|
|
132
|
+
<CampaignDetailForm
|
|
133
|
+
campaignId={campaign.id}
|
|
134
|
+
campaignName={campaign.name ?? ""}
|
|
135
|
+
/>
|
|
136
|
+
</Tabs.Content>
|
|
137
|
+
</Tabs>
|
|
138
|
+
</Container>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const config = defineRouteConfig({
|
|
143
|
+
label: "Coupon Detail",
|
|
144
|
+
icon: Tag,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export default CouponDetail;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { toast } from "@medusajs/ui";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { FC } from "react";
|
|
4
|
+
import { useNavigate } from "react-router-dom";
|
|
5
|
+
import {
|
|
6
|
+
CouponForm,
|
|
7
|
+
CouponFormData,
|
|
8
|
+
} from "../../../components/CouponForm";
|
|
9
|
+
|
|
10
|
+
const CouponCreate: FC = () => {
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
|
|
13
|
+
const handleSubmit = async (formData: CouponFormData) => {
|
|
14
|
+
try {
|
|
15
|
+
await axios.post("/admin/coupons", {
|
|
16
|
+
...formData,
|
|
17
|
+
starts_at: new Date(formData.starts_at).toUTCString(),
|
|
18
|
+
ends_at: new Date(formData.ends_at).toUTCString(),
|
|
19
|
+
currency_code:
|
|
20
|
+
formData.discount_type === "fixed"
|
|
21
|
+
? formData.currency_code
|
|
22
|
+
: undefined,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
toast.success("Coupon created successfully");
|
|
26
|
+
navigate("/coupons");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
let message = "Failed to create coupon";
|
|
29
|
+
if (axios.isAxiosError(error)) {
|
|
30
|
+
message = error.response?.data?.message ?? message;
|
|
31
|
+
} else if (error instanceof Error) {
|
|
32
|
+
message = error.message;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toast.error("Failed to create coupon", {
|
|
36
|
+
description: message,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<CouponForm
|
|
43
|
+
onSubmit={handleSubmit}
|
|
44
|
+
onCancel={() => navigate("/coupons")}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default CouponCreate;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
2
|
+
import { Tag } from "@medusajs/icons";
|
|
3
|
+
import { FC } from "react";
|
|
4
|
+
import { CouponPage } from "../../components/CouponPage";
|
|
5
|
+
|
|
6
|
+
const Coupons: FC = () => {
|
|
7
|
+
return <CouponPage />;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const config = defineRouteConfig({
|
|
11
|
+
label: "Coupons",
|
|
12
|
+
icon: Tag,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default Coupons;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
2
2
|
import { Sparkles } from "@medusajs/icons";
|
|
3
3
|
import { Button, Container, toast, Tabs } from "@medusajs/ui";
|
|
4
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
5
4
|
import axios from "axios";
|
|
6
5
|
import { FC, useState } from "react";
|
|
7
6
|
import { useNavigate, useParams } from "react-router-dom";
|
|
@@ -18,8 +17,7 @@ import { CampaignDetailForm } from "../../../components/campaign-detail-form";
|
|
|
18
17
|
const FlashSaleDetail: FC = () => {
|
|
19
18
|
const { id } = useParams<{ id: string }>();
|
|
20
19
|
const navigate = useNavigate();
|
|
21
|
-
const
|
|
22
|
-
const { data, isLoading } = useFlashSaleById(id || "");
|
|
20
|
+
const { data, isLoading, refetch } = useFlashSaleById(id || "");
|
|
23
21
|
|
|
24
22
|
const [isEditing, setIsEditing] = useState(false);
|
|
25
23
|
|
|
@@ -32,14 +30,7 @@ const FlashSaleDetail: FC = () => {
|
|
|
32
30
|
});
|
|
33
31
|
|
|
34
32
|
toast.success("Flash sale updated successfully");
|
|
35
|
-
|
|
36
|
-
exact: false,
|
|
37
|
-
predicate(query) {
|
|
38
|
-
return ["flash-sales", "flash-sale"].includes(
|
|
39
|
-
query.queryKey[0] as string,
|
|
40
|
-
);
|
|
41
|
-
},
|
|
42
|
-
});
|
|
33
|
+
refetch();
|
|
43
34
|
navigate("/flash-sales");
|
|
44
35
|
} catch (error) {
|
|
45
36
|
let message: string;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { toast } from "@medusajs/ui";
|
|
2
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
3
2
|
import axios from "axios";
|
|
4
3
|
import { FC } from "react";
|
|
5
4
|
import { useNavigate } from "react-router-dom";
|
|
@@ -7,7 +6,6 @@ import { CampaignData, FlashSaleForm } from "../../../components/FlashSaleForm";
|
|
|
7
6
|
|
|
8
7
|
const FlashSaleCreate: FC = () => {
|
|
9
8
|
const navigate = useNavigate();
|
|
10
|
-
const queryClient = useQueryClient();
|
|
11
9
|
|
|
12
10
|
async function handleSubmit(campaignData: CampaignData) {
|
|
13
11
|
try {
|
|
@@ -17,10 +15,6 @@ const FlashSaleCreate: FC = () => {
|
|
|
17
15
|
ends_at: new Date(campaignData.ends_at).toUTCString(),
|
|
18
16
|
});
|
|
19
17
|
toast.success("Flash sale created successfully");
|
|
20
|
-
queryClient.invalidateQueries({
|
|
21
|
-
exact: false,
|
|
22
|
-
queryKey: ["flash-sales"],
|
|
23
|
-
});
|
|
24
18
|
navigate("/flash-sales");
|
|
25
19
|
} catch (error) {
|
|
26
20
|
let message: string;
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { defineWidgetConfig } from "@medusajs/admin-sdk";
|
|
2
|
-
import {
|
|
3
|
-
Container,
|
|
4
|
-
Heading,
|
|
5
|
-
Text,
|
|
6
|
-
Badge,
|
|
7
|
-
Button,
|
|
8
|
-
Alert,
|
|
9
|
-
} from "@medusajs/ui";
|
|
2
|
+
import { Container, Heading, Text, Badge, Button, Alert } from "@medusajs/ui";
|
|
10
3
|
import { Sparkles, PhotoSolid, PencilSquare, Eye } from "@medusajs/icons";
|
|
11
4
|
import { useState, useEffect } from "react";
|
|
12
5
|
import { useNavigate } from "react-router-dom";
|
|
@@ -38,10 +31,14 @@ interface CampaignDetailWidgetProps {
|
|
|
38
31
|
data: any; // Promotion data
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
const CampaignDetailWidget = ({
|
|
34
|
+
const CampaignDetailWidget = ({
|
|
35
|
+
data: promotion,
|
|
36
|
+
}: CampaignDetailWidgetProps) => {
|
|
42
37
|
const navigate = useNavigate();
|
|
43
38
|
const [campaign, setCampaign] = useState<Campaign | null>(null);
|
|
44
|
-
const [campaignDetail, setCampaignDetail] = useState<CampaignDetail | null>(
|
|
39
|
+
const [campaignDetail, setCampaignDetail] = useState<CampaignDetail | null>(
|
|
40
|
+
null,
|
|
41
|
+
);
|
|
45
42
|
const [loading, setLoading] = useState(true);
|
|
46
43
|
const [error, setError] = useState<string | null>(null);
|
|
47
44
|
|
|
@@ -65,7 +62,7 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
65
62
|
`/admin/flash-sales/${promotion.campaign_id}`,
|
|
66
63
|
{
|
|
67
64
|
credentials: "include",
|
|
68
|
-
}
|
|
65
|
+
},
|
|
69
66
|
);
|
|
70
67
|
|
|
71
68
|
if (campaignResponse.ok) {
|
|
@@ -149,7 +146,8 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
149
146
|
|
|
150
147
|
const hasCampaignDetail = !!campaignDetail;
|
|
151
148
|
const hasImages = campaignDetail?.image_url || campaignDetail?.thumbnail_url;
|
|
152
|
-
const hasContent =
|
|
149
|
+
const hasContent =
|
|
150
|
+
campaignDetail?.detail_content || campaignDetail?.terms_and_conditions;
|
|
153
151
|
const hasSEO = campaignDetail?.meta_title || campaignDetail?.meta_description;
|
|
154
152
|
|
|
155
153
|
return (
|
|
@@ -164,7 +162,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
164
162
|
<div className="mb-4 p-4 rounded-lg border bg-ui-bg-subtle">
|
|
165
163
|
<div className="flex items-start justify-between mb-3">
|
|
166
164
|
<div className="flex-1">
|
|
167
|
-
<Text className="font-semibold text-lg mb-1">
|
|
165
|
+
<Text className="font-semibold text-lg mb-1">
|
|
166
|
+
{campaign.name}
|
|
167
|
+
</Text>
|
|
168
168
|
{campaign.description && (
|
|
169
169
|
<Text className="text-sm text-ui-fg-subtle line-clamp-2">
|
|
170
170
|
{campaign.description}
|
|
@@ -180,15 +180,19 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
180
180
|
{/* Campaign Detail Status */}
|
|
181
181
|
<div className="grid grid-cols-2 gap-2 mt-3 pt-3 border-t">
|
|
182
182
|
<div className="flex items-center gap-2">
|
|
183
|
-
<PhotoSolid
|
|
183
|
+
<PhotoSolid
|
|
184
|
+
className={`h-4 w-4 ${hasImages ? "text-green-500" : "text-ui-fg-muted"}`}
|
|
185
|
+
/>
|
|
184
186
|
<Text className="text-xs">
|
|
185
|
-
{hasImages ?
|
|
187
|
+
{hasImages ? "Images added" : "No images"}
|
|
186
188
|
</Text>
|
|
187
189
|
</div>
|
|
188
190
|
<div className="flex items-center gap-2">
|
|
189
|
-
<PencilSquare
|
|
191
|
+
<PencilSquare
|
|
192
|
+
className={`h-4 w-4 ${hasContent ? "text-green-500" : "text-ui-fg-muted"}`}
|
|
193
|
+
/>
|
|
190
194
|
<Text className="text-xs">
|
|
191
|
-
{hasContent ?
|
|
195
|
+
{hasContent ? "Content added" : "No content"}
|
|
192
196
|
</Text>
|
|
193
197
|
</div>
|
|
194
198
|
</div>
|
|
@@ -223,7 +227,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
223
227
|
|
|
224
228
|
{campaignDetail.link_url && (
|
|
225
229
|
<div className="flex items-center gap-2 text-xs">
|
|
226
|
-
<Badge size="xsmall" color="blue">
|
|
230
|
+
<Badge size="xsmall" color="blue">
|
|
231
|
+
CTA Link
|
|
232
|
+
</Badge>
|
|
227
233
|
<Text className="text-ui-fg-subtle truncate">
|
|
228
234
|
{campaignDetail.link_text || campaignDetail.link_url}
|
|
229
235
|
</Text>
|
|
@@ -232,7 +238,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
232
238
|
|
|
233
239
|
{hasSEO && (
|
|
234
240
|
<div className="flex items-center gap-2 text-xs">
|
|
235
|
-
<Badge size="xsmall" color="green">
|
|
241
|
+
<Badge size="xsmall" color="green">
|
|
242
|
+
SEO
|
|
243
|
+
</Badge>
|
|
236
244
|
<Text className="text-ui-fg-subtle">
|
|
237
245
|
Meta fields configured
|
|
238
246
|
</Text>
|
|
@@ -245,18 +253,15 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
245
253
|
{!hasCampaignDetail && (
|
|
246
254
|
<Alert variant="info" className="mb-4">
|
|
247
255
|
<Text className="text-sm">
|
|
248
|
-
No campaign details added yet. Add images, content, and SEO to
|
|
256
|
+
No campaign details added yet. Add images, content, and SEO to
|
|
257
|
+
enhance this campaign.
|
|
249
258
|
</Text>
|
|
250
259
|
</Alert>
|
|
251
260
|
)}
|
|
252
261
|
|
|
253
262
|
{/* Actions */}
|
|
254
263
|
<div className="flex gap-2">
|
|
255
|
-
<Button
|
|
256
|
-
variant="secondary"
|
|
257
|
-
size="small"
|
|
258
|
-
onClick={handleEditCampaign}
|
|
259
|
-
>
|
|
264
|
+
<Button variant="secondary" size="small" onClick={handleEditCampaign}>
|
|
260
265
|
<PencilSquare className="mr-1" />
|
|
261
266
|
Edit Campaign
|
|
262
267
|
</Button>
|
|
@@ -277,7 +282,9 @@ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) =>
|
|
|
277
282
|
<div className="mt-4 pt-4 border-t grid grid-cols-2 gap-3 text-xs">
|
|
278
283
|
<div>
|
|
279
284
|
<Text className="text-ui-fg-subtle">Display Order</Text>
|
|
280
|
-
<Text className="font-medium">
|
|
285
|
+
<Text className="font-medium">
|
|
286
|
+
{campaignDetail.display_order}
|
|
287
|
+
</Text>
|
|
281
288
|
</div>
|
|
282
289
|
<div>
|
|
283
290
|
<Text className="text-ui-fg-subtle">Status</Text>
|
|
@@ -19,7 +19,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
19
19
|
if (!id) {
|
|
20
20
|
throw new MedusaError(
|
|
21
21
|
MedusaError.Types.INVALID_DATA,
|
|
22
|
-
"Campaign ID is required"
|
|
22
|
+
"Campaign ID is required",
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -34,11 +34,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
34
34
|
data: [customCampaignTypes],
|
|
35
35
|
} = await query.graph({
|
|
36
36
|
entity: "custom_campaign_type",
|
|
37
|
-
fields: [
|
|
38
|
-
"id",
|
|
39
|
-
"campaign.*",
|
|
40
|
-
"campaign.promotions.*",
|
|
41
|
-
],
|
|
37
|
+
fields: ["id", "campaign.*", "campaign.promotions.*"],
|
|
42
38
|
filters: {
|
|
43
39
|
type: CampaignTypeEnum.BuyXGetY,
|
|
44
40
|
campaign_id: id,
|
|
@@ -50,7 +46,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
50
46
|
if (!campaign) {
|
|
51
47
|
throw new MedusaError(
|
|
52
48
|
MedusaError.Types.NOT_FOUND,
|
|
53
|
-
`Buy X Get Y campaign with ID ${id} not found
|
|
49
|
+
`Buy X Get Y campaign with ID ${id} not found`,
|
|
54
50
|
);
|
|
55
51
|
}
|
|
56
52
|
|
|
@@ -64,10 +60,10 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
64
60
|
const rules: BuyXGetYCampaign["rules"] = [];
|
|
65
61
|
for (const config of buyXGetYConfigs) {
|
|
66
62
|
const triggerProduct = await productService.retrieveProduct(
|
|
67
|
-
config.trigger_product_id
|
|
63
|
+
config.trigger_product_id,
|
|
68
64
|
);
|
|
69
65
|
const rewardProduct = await productService.retrieveProduct(
|
|
70
|
-
config.reward_product_id
|
|
66
|
+
config.reward_product_id,
|
|
71
67
|
);
|
|
72
68
|
|
|
73
69
|
rules.push({
|
|
@@ -100,7 +96,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
100
96
|
console.error("Error fetching Buy X Get Y campaign:", error);
|
|
101
97
|
throw new MedusaError(
|
|
102
98
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
103
|
-
"An error occurred while fetching the Buy X Get Y campaign"
|
|
99
|
+
"An error occurred while fetching the Buy X Get Y campaign",
|
|
104
100
|
);
|
|
105
101
|
}
|
|
106
102
|
};
|
|
@@ -110,7 +106,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
|
110
106
|
*/
|
|
111
107
|
export const PUT = async (
|
|
112
108
|
req: MedusaRequest<BuyXGetYCampaign>,
|
|
113
|
-
res: MedusaResponse
|
|
109
|
+
res: MedusaResponse,
|
|
114
110
|
) => {
|
|
115
111
|
const { id } = req.params;
|
|
116
112
|
const body = req.body;
|
|
@@ -118,7 +114,7 @@ export const PUT = async (
|
|
|
118
114
|
if (!id) {
|
|
119
115
|
throw new MedusaError(
|
|
120
116
|
MedusaError.Types.INVALID_DATA,
|
|
121
|
-
"Campaign ID is required"
|
|
117
|
+
"Campaign ID is required",
|
|
122
118
|
);
|
|
123
119
|
}
|
|
124
120
|
|
|
@@ -127,7 +123,7 @@ export const PUT = async (
|
|
|
127
123
|
if (new Date(body.ends_at) < new Date(body.starts_at)) {
|
|
128
124
|
throw new MedusaError(
|
|
129
125
|
MedusaError.Types.INVALID_DATA,
|
|
130
|
-
"End date must be after start date"
|
|
126
|
+
"End date must be after start date",
|
|
131
127
|
);
|
|
132
128
|
}
|
|
133
129
|
|
|
@@ -136,7 +132,7 @@ export const PUT = async (
|
|
|
136
132
|
if (rule.rewardType !== "free" && !rule.rewardValue) {
|
|
137
133
|
throw new MedusaError(
|
|
138
134
|
MedusaError.Types.INVALID_DATA,
|
|
139
|
-
"Reward value is required for percentage and fixed reward types"
|
|
135
|
+
"Reward value is required for percentage and fixed reward types",
|
|
140
136
|
);
|
|
141
137
|
}
|
|
142
138
|
}
|
|
@@ -158,7 +154,7 @@ export const PUT = async (
|
|
|
158
154
|
console.error("Error updating Buy X Get Y campaign:", error);
|
|
159
155
|
throw new MedusaError(
|
|
160
156
|
MedusaError.Types.UNEXPECTED_STATE,
|
|
161
|
-
"An error occurred while updating the Buy X Get Y campaign"
|
|
157
|
+
"An error occurred while updating the Buy X Get Y campaign",
|
|
162
158
|
);
|
|
163
159
|
}
|
|
164
160
|
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { container, MedusaRequest, MedusaResponse } from "@medusajs/framework";
|
|
2
|
+
import { MedusaError, Modules } from "@medusajs/framework/utils";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { createCouponCampaignSchema } from "../route";
|
|
5
|
+
import { updateCouponCampaignWorkflow } from "../../../../workflows/custom-campaign/updateCouponCampaignWorkflow";
|
|
6
|
+
import { CUSTOM_CAMPAIGN_MODULE } from "../../../../modules/custom-campaigns";
|
|
7
|
+
import CustomCampaignModuleService from "../../../../modules/custom-campaigns/service";
|
|
8
|
+
import { CampaignTypeEnum } from "../../../../modules/custom-campaigns/types/campaign-type.enum";
|
|
9
|
+
|
|
10
|
+
const updateCouponSchema = createCouponCampaignSchema.extend({
|
|
11
|
+
type: z.literal(CampaignTypeEnum.Coupon),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
15
|
+
const { id } = req.params;
|
|
16
|
+
|
|
17
|
+
const promotionService = container.resolve(Modules.PROMOTION);
|
|
18
|
+
const customCampaignModuleService =
|
|
19
|
+
container.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const campaign = await promotionService.retrieveCampaign(id, {
|
|
23
|
+
relations: ["promotions", "promotions.application_method"],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!campaign) {
|
|
27
|
+
throw new MedusaError(
|
|
28
|
+
MedusaError.Types.NOT_FOUND,
|
|
29
|
+
"Coupon campaign not found",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [campaignType] =
|
|
34
|
+
await customCampaignModuleService.listCustomCampaignTypes({
|
|
35
|
+
campaign_id: campaign.id,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (campaignType?.type !== CampaignTypeEnum.Coupon) {
|
|
39
|
+
throw new MedusaError(
|
|
40
|
+
MedusaError.Types.INVALID_DATA,
|
|
41
|
+
"Campaign is not a coupon",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [detail] = await customCampaignModuleService.listCampaignDetails({
|
|
46
|
+
campaign_id: campaign.id,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
res.status(200).json({
|
|
50
|
+
campaign,
|
|
51
|
+
campaign_detail: detail ?? null,
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof MedusaError) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new MedusaError(
|
|
59
|
+
MedusaError.Types.INVALID_DATA,
|
|
60
|
+
(error as Error)?.message ?? "Failed to retrieve coupon",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const PUT = async (
|
|
66
|
+
req: MedusaRequest<z.infer<typeof updateCouponSchema>>,
|
|
67
|
+
res: MedusaResponse,
|
|
68
|
+
) => {
|
|
69
|
+
const { id } = req.params;
|
|
70
|
+
const body = req.body;
|
|
71
|
+
|
|
72
|
+
if (body.type !== CampaignTypeEnum.Coupon) {
|
|
73
|
+
throw new MedusaError(
|
|
74
|
+
MedusaError.Types.INVALID_DATA,
|
|
75
|
+
"Campaign type must be coupon",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await updateCouponCampaignWorkflow.run({
|
|
80
|
+
input: {
|
|
81
|
+
id,
|
|
82
|
+
name: body.name,
|
|
83
|
+
description: body.description,
|
|
84
|
+
code: body.code,
|
|
85
|
+
starts_at: new Date(body.starts_at),
|
|
86
|
+
ends_at: new Date(body.ends_at),
|
|
87
|
+
discount_type: body.discount_type,
|
|
88
|
+
discount_value: body.discount_value,
|
|
89
|
+
currency_code: body.currency_code,
|
|
90
|
+
allocation: body.allocation,
|
|
91
|
+
target_type: body.target_type,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
res.status(200).json({
|
|
96
|
+
message: "Coupon campaign updated",
|
|
97
|
+
});
|
|
98
|
+
};
|