@lodashventure/medusa-campaign 1.3.12 → 1.4.0

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.
Files changed (54) hide show
  1. package/.medusa/server/src/admin/index.js +1238 -16
  2. package/.medusa/server/src/admin/index.mjs +1240 -18
  3. package/.medusa/server/src/api/admin/campaigns/[id]/detail/route.js +67 -0
  4. package/.medusa/server/src/api/admin/campaigns/[id]/image/route.js +80 -0
  5. package/.medusa/server/src/api/admin/campaigns/[id]/thumbnail/route.js +80 -0
  6. package/.medusa/server/src/api/admin/campaigns/sync/route.js +8 -6
  7. package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +22 -4
  8. package/.medusa/server/src/api/middlewares.js +24 -1
  9. package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +1 -1
  10. package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +1 -1
  11. package/.medusa/server/src/api/store/buy-x-get-y/route.js +3 -1
  12. package/.medusa/server/src/api/store/campaigns/[id]/route.js +40 -12
  13. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +53 -0
  14. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +22 -0
  15. package/.medusa/server/src/modules/custom-campaigns/models/campaign-detail.js +26 -0
  16. package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
  17. package/.medusa/server/src/subscribers/order-placed.js +2 -2
  18. package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +16 -4
  19. package/.medusa/server/src/workflows/campaign-detail/update-campaign-detail.js +55 -0
  20. package/.medusa/server/src/workflows/campaign-detail/upload-campaign-images.js +120 -0
  21. package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +13 -14
  22. package/package.json +7 -5
  23. package/src/admin/components/campaign-detail-form.tsx +407 -0
  24. package/src/admin/components/campaign-image-uploader.tsx +313 -0
  25. package/src/admin/components/markdown-editor.tsx +298 -0
  26. package/src/admin/routes/flash-sales/[id]/page.tsx +51 -14
  27. package/src/admin/widgets/campaign-detail-widget.tsx +299 -0
  28. package/src/admin/widgets/campaign-stats-widget.tsx +238 -0
  29. package/src/api/admin/campaigns/[id]/detail/route.ts +77 -0
  30. package/src/api/admin/campaigns/[id]/image/route.ts +87 -0
  31. package/src/api/admin/campaigns/[id]/thumbnail/route.ts +87 -0
  32. package/src/api/admin/campaigns/sync/route.ts +53 -28
  33. package/src/api/admin/flash-sales/[id]/route.ts +50 -19
  34. package/src/api/middlewares.ts +21 -0
  35. package/src/api/store/buy-x-get-y/[id]/route.ts +10 -10
  36. package/src/api/store/buy-x-get-y/products/[productId]/route.ts +11 -12
  37. package/src/api/store/buy-x-get-y/route.ts +12 -5
  38. package/src/api/store/campaigns/[id]/route.ts +54 -24
  39. package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +53 -0
  40. package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +19 -0
  41. package/src/modules/custom-campaigns/models/campaign-detail.ts +25 -0
  42. package/src/modules/custom-campaigns/service.ts +2 -0
  43. package/src/subscribers/order-placed.ts +0 -2
  44. package/src/types/index.d.ts +46 -0
  45. package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +41 -18
  46. package/src/workflows/campaign-detail/update-campaign-detail.ts +85 -0
  47. package/src/workflows/campaign-detail/upload-campaign-images.ts +163 -0
  48. package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +23 -22
  49. package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +0 -103
  50. package/.medusa/server/src/api/admin/force-fix/route.js +0 -176
  51. package/.medusa/server/src/api/admin/test-campaign/route.js +0 -132
  52. package/src/api/admin/campaigns/fix-dates/route.ts +0 -107
  53. package/src/api/admin/force-fix/route.ts +0 -184
  54. package/src/api/admin/test-campaign/route.ts +0 -141
@@ -0,0 +1,299 @@
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";
10
+ import { Sparkles, PhotoSolid, PencilSquare, Eye } from "@medusajs/icons";
11
+ import { useState, useEffect } from "react";
12
+ import { useNavigate } from "react-router-dom";
13
+
14
+ interface CampaignDetail {
15
+ id: string;
16
+ campaign_id: string;
17
+ image_url?: string | null;
18
+ thumbnail_url?: string | null;
19
+ detail_content?: string | null;
20
+ terms_and_conditions?: string | null;
21
+ meta_title?: string | null;
22
+ meta_description?: string | null;
23
+ link_url?: string | null;
24
+ link_text?: string | null;
25
+ display_order: number;
26
+ }
27
+
28
+ interface Campaign {
29
+ id: string;
30
+ name: string;
31
+ description?: string;
32
+ campaign_budget?: {
33
+ type: string;
34
+ };
35
+ }
36
+
37
+ interface CampaignDetailWidgetProps {
38
+ data: any; // Promotion data
39
+ }
40
+
41
+ const CampaignDetailWidget = ({ data: promotion }: CampaignDetailWidgetProps) => {
42
+ const navigate = useNavigate();
43
+ const [campaign, setCampaign] = useState<Campaign | null>(null);
44
+ const [campaignDetail, setCampaignDetail] = useState<CampaignDetail | null>(null);
45
+ const [loading, setLoading] = useState(true);
46
+ const [error, setError] = useState<string | null>(null);
47
+
48
+ useEffect(() => {
49
+ if (promotion?.campaign_id) {
50
+ fetchCampaignAndDetail();
51
+ } else {
52
+ setLoading(false);
53
+ }
54
+ }, [promotion?.campaign_id]);
55
+
56
+ const fetchCampaignAndDetail = async () => {
57
+ if (!promotion?.campaign_id) return;
58
+
59
+ setLoading(true);
60
+ setError(null);
61
+
62
+ try {
63
+ // Fetch campaign info
64
+ const campaignResponse = await fetch(
65
+ `/admin/flash-sales/${promotion.campaign_id}`,
66
+ {
67
+ credentials: "include",
68
+ }
69
+ );
70
+
71
+ if (campaignResponse.ok) {
72
+ const campaignData = await campaignResponse.json();
73
+ setCampaign(campaignData);
74
+
75
+ // Check if campaign detail exists in response
76
+ if (campaignData.campaign_detail) {
77
+ setCampaignDetail(campaignData.campaign_detail);
78
+ }
79
+ }
80
+ } catch (err) {
81
+ console.error("Error fetching campaign:", err);
82
+ setError("Failed to load campaign information");
83
+ } finally {
84
+ setLoading(false);
85
+ }
86
+ };
87
+
88
+ const handleEditCampaign = () => {
89
+ if (campaign?.id) {
90
+ navigate(`/flash-sales/${campaign.id}`);
91
+ }
92
+ };
93
+
94
+ const handleViewCampaign = () => {
95
+ if (campaign?.id) {
96
+ // Navigate to campaign detail tab
97
+ navigate(`/flash-sales/${campaign.id}#details`);
98
+ }
99
+ };
100
+
101
+ // Don't show widget if promotion is not part of a campaign
102
+ if (!promotion?.campaign_id) {
103
+ return null;
104
+ }
105
+
106
+ if (loading) {
107
+ return (
108
+ <Container className="divide-y px-0 pb-0 pt-0">
109
+ <div className="px-6 py-6">
110
+ <div className="flex items-center gap-3 mb-4">
111
+ <Sparkles className="text-ui-fg-subtle" />
112
+ <Heading level="h2">Campaign</Heading>
113
+ </div>
114
+ <Text className="text-ui-fg-subtle">Loading campaign...</Text>
115
+ </div>
116
+ </Container>
117
+ );
118
+ }
119
+
120
+ if (error) {
121
+ return (
122
+ <Container className="divide-y px-0 pb-0 pt-0">
123
+ <div className="px-6 py-6">
124
+ <div className="flex items-center gap-3 mb-4">
125
+ <Sparkles className="text-ui-fg-subtle" />
126
+ <Heading level="h2">Campaign</Heading>
127
+ </div>
128
+ <Alert variant="error">{error}</Alert>
129
+ </div>
130
+ </Container>
131
+ );
132
+ }
133
+
134
+ if (!campaign) {
135
+ return (
136
+ <Container className="divide-y px-0 pb-0 pt-0">
137
+ <div className="px-6 py-6">
138
+ <div className="flex items-center gap-3 mb-4">
139
+ <Sparkles className="text-ui-fg-subtle" />
140
+ <Heading level="h2">Campaign</Heading>
141
+ </div>
142
+ <Text className="text-ui-fg-subtle">
143
+ This promotion is not part of a campaign
144
+ </Text>
145
+ </div>
146
+ </Container>
147
+ );
148
+ }
149
+
150
+ const hasCampaignDetail = !!campaignDetail;
151
+ const hasImages = campaignDetail?.image_url || campaignDetail?.thumbnail_url;
152
+ const hasContent = campaignDetail?.detail_content || campaignDetail?.terms_and_conditions;
153
+ const hasSEO = campaignDetail?.meta_title || campaignDetail?.meta_description;
154
+
155
+ return (
156
+ <Container className="divide-y px-0 pb-0 pt-0">
157
+ <div className="px-6 py-6">
158
+ <div className="flex items-center gap-3 mb-4">
159
+ <Sparkles className="text-ui-fg-subtle" />
160
+ <Heading level="h2">Campaign</Heading>
161
+ </div>
162
+
163
+ {/* Campaign Info */}
164
+ <div className="mb-4 p-4 rounded-lg border bg-ui-bg-subtle">
165
+ <div className="flex items-start justify-between mb-3">
166
+ <div className="flex-1">
167
+ <Text className="font-semibold text-lg mb-1">{campaign.name}</Text>
168
+ {campaign.description && (
169
+ <Text className="text-sm text-ui-fg-subtle line-clamp-2">
170
+ {campaign.description}
171
+ </Text>
172
+ )}
173
+ </div>
174
+ <Badge color="purple" size="small">
175
+ <Sparkles className="mr-1" />
176
+ Campaign
177
+ </Badge>
178
+ </div>
179
+
180
+ {/* Campaign Detail Status */}
181
+ <div className="grid grid-cols-2 gap-2 mt-3 pt-3 border-t">
182
+ <div className="flex items-center gap-2">
183
+ <PhotoSolid className={`h-4 w-4 ${hasImages ? 'text-green-500' : 'text-ui-fg-muted'}`} />
184
+ <Text className="text-xs">
185
+ {hasImages ? 'Images added' : 'No images'}
186
+ </Text>
187
+ </div>
188
+ <div className="flex items-center gap-2">
189
+ <PencilSquare className={`h-4 w-4 ${hasContent ? 'text-green-500' : 'text-ui-fg-muted'}`} />
190
+ <Text className="text-xs">
191
+ {hasContent ? 'Content added' : 'No content'}
192
+ </Text>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ {/* Campaign Detail Preview */}
198
+ {hasCampaignDetail && (
199
+ <div className="space-y-3 mb-4">
200
+ {campaignDetail.thumbnail_url && (
201
+ <div>
202
+ <Text className="text-xs font-medium text-ui-fg-subtle mb-2">
203
+ Thumbnail
204
+ </Text>
205
+ <img
206
+ src={campaignDetail.thumbnail_url}
207
+ alt="Campaign thumbnail"
208
+ className="w-full h-32 object-cover rounded-lg border"
209
+ />
210
+ </div>
211
+ )}
212
+
213
+ {campaignDetail.detail_content && (
214
+ <div>
215
+ <Text className="text-xs font-medium text-ui-fg-subtle mb-1">
216
+ Content Preview
217
+ </Text>
218
+ <Text className="text-sm line-clamp-3 text-ui-fg-muted">
219
+ {campaignDetail.detail_content.substring(0, 150)}...
220
+ </Text>
221
+ </div>
222
+ )}
223
+
224
+ {campaignDetail.link_url && (
225
+ <div className="flex items-center gap-2 text-xs">
226
+ <Badge size="xsmall" color="blue">CTA Link</Badge>
227
+ <Text className="text-ui-fg-subtle truncate">
228
+ {campaignDetail.link_text || campaignDetail.link_url}
229
+ </Text>
230
+ </div>
231
+ )}
232
+
233
+ {hasSEO && (
234
+ <div className="flex items-center gap-2 text-xs">
235
+ <Badge size="xsmall" color="green">SEO</Badge>
236
+ <Text className="text-ui-fg-subtle">
237
+ Meta fields configured
238
+ </Text>
239
+ </div>
240
+ )}
241
+ </div>
242
+ )}
243
+
244
+ {/* No Campaign Detail Message */}
245
+ {!hasCampaignDetail && (
246
+ <Alert variant="info" className="mb-4">
247
+ <Text className="text-sm">
248
+ No campaign details added yet. Add images, content, and SEO to enhance this campaign.
249
+ </Text>
250
+ </Alert>
251
+ )}
252
+
253
+ {/* Actions */}
254
+ <div className="flex gap-2">
255
+ <Button
256
+ variant="secondary"
257
+ size="small"
258
+ onClick={handleEditCampaign}
259
+ >
260
+ <PencilSquare className="mr-1" />
261
+ Edit Campaign
262
+ </Button>
263
+ {hasCampaignDetail && (
264
+ <Button
265
+ variant="secondary"
266
+ size="small"
267
+ onClick={handleViewCampaign}
268
+ >
269
+ <Eye className="mr-1" />
270
+ View Details
271
+ </Button>
272
+ )}
273
+ </div>
274
+
275
+ {/* Quick Stats */}
276
+ {campaignDetail && (
277
+ <div className="mt-4 pt-4 border-t grid grid-cols-2 gap-3 text-xs">
278
+ <div>
279
+ <Text className="text-ui-fg-subtle">Display Order</Text>
280
+ <Text className="font-medium">{campaignDetail.display_order}</Text>
281
+ </div>
282
+ <div>
283
+ <Text className="text-ui-fg-subtle">Status</Text>
284
+ <Badge color={hasCampaignDetail ? "green" : "grey"} size="xsmall">
285
+ {hasCampaignDetail ? "Complete" : "Incomplete"}
286
+ </Badge>
287
+ </div>
288
+ </div>
289
+ )}
290
+ </div>
291
+ </Container>
292
+ );
293
+ };
294
+
295
+ export const config = defineWidgetConfig({
296
+ zone: "promotion.details.side.after",
297
+ });
298
+
299
+ export default CampaignDetailWidget;
@@ -0,0 +1,238 @@
1
+ import { defineWidgetConfig } from "@medusajs/admin-sdk";
2
+ import { Container, Heading, Text, Badge } from "@medusajs/ui";
3
+ import {
4
+ Sparkles,
5
+ PhotoSolid,
6
+ CheckCircleSolid,
7
+ ClockSolid,
8
+ } from "@medusajs/icons";
9
+ import { useState, useEffect } from "react";
10
+
11
+ interface CampaignStats {
12
+ total: number;
13
+ active: number;
14
+ inactive: number;
15
+ with_images: number;
16
+ with_content: number;
17
+ complete: number;
18
+ }
19
+
20
+ const CampaignStatsWidget = () => {
21
+ const [stats, setStats] = useState<CampaignStats>({
22
+ total: 0,
23
+ active: 0,
24
+ inactive: 0,
25
+ with_images: 0,
26
+ with_content: 0,
27
+ complete: 0,
28
+ });
29
+ const [loading, setLoading] = useState(true);
30
+
31
+ useEffect(() => {
32
+ fetchStats();
33
+ }, []);
34
+
35
+ const fetchStats = async () => {
36
+ setLoading(true);
37
+ try {
38
+ const response = await fetch("/admin/flash-sales?limit=100", {
39
+ credentials: "include",
40
+ });
41
+
42
+ if (response.ok) {
43
+ const data = await response.json();
44
+ const campaigns = data.campaigns || [];
45
+
46
+ // Calculate stats
47
+ const now = new Date();
48
+ let active = 0;
49
+ let inactive = 0;
50
+ let withImages = 0;
51
+ let withContent = 0;
52
+ let complete = 0;
53
+
54
+ campaigns.forEach((campaign: any) => {
55
+ // Count active/inactive
56
+ if (campaign.ends_at && new Date(campaign.ends_at) > now) {
57
+ active++;
58
+ } else {
59
+ inactive++;
60
+ }
61
+
62
+ // Count campaigns with details
63
+ if (campaign.campaign_detail) {
64
+ if (
65
+ campaign.campaign_detail.image_url ||
66
+ campaign.campaign_detail.thumbnail_url
67
+ ) {
68
+ withImages++;
69
+ }
70
+ if (campaign.campaign_detail.detail_content) {
71
+ withContent++;
72
+ }
73
+ // Complete = has both images and content
74
+ if (
75
+ (campaign.campaign_detail.image_url ||
76
+ campaign.campaign_detail.thumbnail_url) &&
77
+ campaign.campaign_detail.detail_content
78
+ ) {
79
+ complete++;
80
+ }
81
+ }
82
+ });
83
+
84
+ setStats({
85
+ total: campaigns.length,
86
+ active,
87
+ inactive,
88
+ with_images: withImages,
89
+ with_content: withContent,
90
+ complete,
91
+ });
92
+ }
93
+ } catch (err) {
94
+ console.error("Error fetching campaign stats:", err);
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ };
99
+
100
+ if (loading) {
101
+ return (
102
+ <Container className="px-6 py-6">
103
+ <div className="flex items-center gap-3 mb-4">
104
+ <Sparkles className="text-ui-fg-subtle" />
105
+ <Heading level="h2">Campaign Overview</Heading>
106
+ </div>
107
+ <Text className="text-ui-fg-subtle">Loading stats...</Text>
108
+ </Container>
109
+ );
110
+ }
111
+
112
+ const completionRate =
113
+ stats.total > 0 ? Math.round((stats.complete / stats.total) * 100) : 0;
114
+
115
+ return (
116
+ <Container className="px-6 py-6">
117
+ <div className="flex items-center gap-3 mb-6">
118
+ <Sparkles className="text-ui-fg-subtle" />
119
+ <Heading level="h2">Campaign Overview</Heading>
120
+ </div>
121
+
122
+ {/* Main Stats Grid */}
123
+ <div className="grid grid-cols-3 gap-4 mb-6">
124
+ {/* Total Campaigns */}
125
+ <div className="p-4 rounded-lg border bg-ui-bg-subtle">
126
+ <div className="flex items-center justify-between mb-2">
127
+ <Text className="text-xs text-ui-fg-subtle">Total</Text>
128
+ <Sparkles className="h-4 w-4 text-ui-fg-subtle" />
129
+ </div>
130
+ <Text className="text-2xl font-bold">{stats.total}</Text>
131
+ </div>
132
+
133
+ {/* Active Campaigns */}
134
+ <div className="p-4 rounded-lg border bg-green-50">
135
+ <div className="flex items-center justify-between mb-2">
136
+ <Text className="text-xs text-green-700">Active</Text>
137
+ <CheckCircleSolid className="h-4 w-4 text-green-600" />
138
+ </div>
139
+ <Text className="text-2xl font-bold text-green-700">
140
+ {stats.active}
141
+ </Text>
142
+ </div>
143
+
144
+ {/* Inactive Campaigns */}
145
+ <div className="p-4 rounded-lg border bg-gray-50">
146
+ <div className="flex items-center justify-between mb-2">
147
+ <Text className="text-xs text-gray-600">Inactive</Text>
148
+ <ClockSolid className="h-4 w-4 text-gray-500" />
149
+ </div>
150
+ <Text className="text-2xl font-bold text-gray-600">
151
+ {stats.inactive}
152
+ </Text>
153
+ </div>
154
+ </div>
155
+
156
+ {/* Detail Stats */}
157
+ <div className="space-y-3 mb-6">
158
+ <div className="flex items-center justify-between p-3 rounded-lg bg-ui-bg-subtle">
159
+ <div className="flex items-center gap-2">
160
+ <PhotoSolid className="h-4 w-4 text-blue-600" />
161
+ <Text className="text-sm">With Images</Text>
162
+ </div>
163
+ <div className="flex items-center gap-2">
164
+ <Text className="text-sm font-medium">{stats.with_images}</Text>
165
+ <Badge size="xsmall" color="blue">
166
+ {stats.total > 0
167
+ ? Math.round((stats.with_images / stats.total) * 100)
168
+ : 0}
169
+ %
170
+ </Badge>
171
+ </div>
172
+ </div>
173
+
174
+ <div className="flex items-center justify-between p-3 rounded-lg bg-ui-bg-subtle">
175
+ <div className="flex items-center gap-2">
176
+ <CheckCircleSolid className="h-4 w-4 text-green-600" />
177
+ <Text className="text-sm">With Content</Text>
178
+ </div>
179
+ <div className="flex items-center gap-2">
180
+ <Text className="text-sm font-medium">{stats.with_content}</Text>
181
+ <Badge size="xsmall" color="green">
182
+ {stats.total > 0
183
+ ? Math.round((stats.with_content / stats.total) * 100)
184
+ : 0}
185
+ %
186
+ </Badge>
187
+ </div>
188
+ </div>
189
+
190
+ <div className="flex items-center justify-between p-3 rounded-lg bg-ui-bg-subtle">
191
+ <div className="flex items-center gap-2">
192
+ <Sparkles className="h-4 w-4 text-purple-600" />
193
+ <Text className="text-sm">Complete Details</Text>
194
+ </div>
195
+ <div className="flex items-center gap-2">
196
+ <Text className="text-sm font-medium">{stats.complete}</Text>
197
+ <Badge size="xsmall" color="purple">
198
+ {completionRate}%
199
+ </Badge>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ {/* Completion Progress Bar */}
205
+ <div className="pt-4 border-t">
206
+ <div className="flex items-center justify-between mb-2">
207
+ <Text className="text-xs text-ui-fg-subtle">Campaign Completion</Text>
208
+ <Text className="text-xs font-medium">{completionRate}%</Text>
209
+ </div>
210
+ <div className="h-2 bg-ui-bg-subtle rounded-full overflow-hidden">
211
+ <div
212
+ className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-300"
213
+ style={{ width: `${completionRate}%` }}
214
+ />
215
+ </div>
216
+ <Text className="text-xs text-ui-fg-muted mt-2">
217
+ {stats.complete} of {stats.total} campaigns have complete details
218
+ </Text>
219
+ </div>
220
+
221
+ {/* Quick Tip */}
222
+ {completionRate < 50 && stats.total > 0 && (
223
+ <div className="mt-4 p-3 rounded-lg bg-blue-50 border border-blue-200">
224
+ <Text className="text-xs text-blue-800">
225
+ 💡 Tip: Add images and content to campaigns to improve customer
226
+ engagement!
227
+ </Text>
228
+ </div>
229
+ )}
230
+ </Container>
231
+ );
232
+ };
233
+
234
+ export const config = defineWidgetConfig({
235
+ zone: "campaign.list.before",
236
+ });
237
+
238
+ export default CampaignStatsWidget;
@@ -0,0 +1,77 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ import { ContainerRegistrationKeys } from "@medusajs/framework/utils";
3
+ import z from "zod";
4
+ import { updateCampaignDetailWorkflow } from "../../../../../workflows/campaign-detail/update-campaign-detail";
5
+ import { CUSTOM_CAMPAIGN_MODULE } from "../../../../../modules/custom-campaigns";
6
+ import CustomCampaignModuleService from "../../../../../modules/custom-campaigns/service";
7
+
8
+ export const updateCampaignDetailSchema = z.object({
9
+ detail_content: z.string().optional(),
10
+ terms_and_conditions: z.string().optional(),
11
+ meta_title: z.string().optional(),
12
+ meta_description: z.string().optional(),
13
+ meta_keywords: z.string().optional(),
14
+ link_url: z.string().url().optional().or(z.literal("")),
15
+ link_text: z.string().optional(),
16
+ display_order: z.number().optional(),
17
+ });
18
+
19
+ export type UpdateCampaignDetailInput = z.infer<
20
+ typeof updateCampaignDetailSchema
21
+ >;
22
+
23
+ // GET campaign detail
24
+ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
25
+ const { id: campaign_id } = req.params;
26
+ const customCampaignModuleService =
27
+ req.scope.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
28
+
29
+ try {
30
+ const [campaignDetail] =
31
+ await customCampaignModuleService.listCampaignDetails({
32
+ campaign_id,
33
+ });
34
+
35
+ if (!campaignDetail) {
36
+ return res.status(404).json({
37
+ error: "Campaign detail not found",
38
+ });
39
+ }
40
+
41
+ res.status(200).json({ campaign_detail: campaignDetail });
42
+ } catch (error) {
43
+ console.error("Error fetching campaign detail:", error);
44
+ res.status(500).json({
45
+ error: "Failed to fetch campaign detail",
46
+ details: error instanceof Error ? error.message : String(error),
47
+ });
48
+ }
49
+ };
50
+
51
+ // PUT/POST update or create campaign detail
52
+ export const POST = async (
53
+ req: MedusaRequest<UpdateCampaignDetailInput>,
54
+ res: MedusaResponse,
55
+ ) => {
56
+ const { id: campaign_id } = req.params;
57
+ const body = updateCampaignDetailSchema.parse(req.body);
58
+
59
+ try {
60
+ const { result } = await updateCampaignDetailWorkflow(req.scope).run({
61
+ input: {
62
+ campaign_id,
63
+ ...body,
64
+ },
65
+ });
66
+
67
+ res.status(200).json({ campaign_detail: result });
68
+ } catch (error) {
69
+ console.error("Error updating campaign detail:", error);
70
+ res.status(500).json({
71
+ error: "Failed to update campaign detail",
72
+ details: error instanceof Error ? error.message : String(error),
73
+ });
74
+ }
75
+ };
76
+
77
+ export const PUT = POST;
@@ -0,0 +1,87 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ import { uploadCampaignImagesWorkflow } from "../../../../../workflows/campaign-detail/upload-campaign-images";
3
+ import { deleteFilesWorkflow } from "@medusajs/medusa/core-flows";
4
+ import { CUSTOM_CAMPAIGN_MODULE } from "../../../../../modules/custom-campaigns";
5
+ import CustomCampaignModuleService from "../../../../../modules/custom-campaigns/service";
6
+
7
+ // POST upload campaign image
8
+ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
9
+ const { id: campaign_id } = req.params;
10
+ const file = (req as any).file;
11
+
12
+ if (!file) {
13
+ return res.status(400).json({
14
+ error: "No file provided",
15
+ });
16
+ }
17
+
18
+ try {
19
+ const { result } = await uploadCampaignImagesWorkflow(req.scope).run({
20
+ input: {
21
+ campaign_id,
22
+ files: [
23
+ {
24
+ filename: Buffer.from(file.originalname, "latin1").toString("utf8"),
25
+ mimeType: file.mimetype,
26
+ content: file.buffer.toString("binary"),
27
+ access: "public" as const,
28
+ },
29
+ ],
30
+ image_type: "image",
31
+ },
32
+ });
33
+
34
+ res.status(200).json({ campaign_detail: result });
35
+ } catch (error) {
36
+ console.error("Error uploading campaign image:", error);
37
+ res.status(500).json({
38
+ error: "Failed to upload campaign image",
39
+ details: error instanceof Error ? error.message : String(error),
40
+ });
41
+ }
42
+ };
43
+
44
+ // DELETE campaign image
45
+ export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
46
+ const { id: campaign_id } = req.params;
47
+ const customCampaignModuleService =
48
+ req.scope.resolve<CustomCampaignModuleService>(CUSTOM_CAMPAIGN_MODULE);
49
+
50
+ try {
51
+ const [campaignDetail] =
52
+ await customCampaignModuleService.listCampaignDetails({
53
+ campaign_id,
54
+ });
55
+
56
+ if (!campaignDetail || !campaignDetail.image_file_id) {
57
+ return res.status(404).json({
58
+ error: "Campaign image not found",
59
+ });
60
+ }
61
+
62
+ // Delete file
63
+ await deleteFilesWorkflow(req.scope).run({
64
+ input: {
65
+ ids: [campaignDetail.image_file_id],
66
+ },
67
+ });
68
+
69
+ // Update campaign detail
70
+ const [updatedDetail] =
71
+ await customCampaignModuleService.updateCampaignDetails([
72
+ {
73
+ id: campaignDetail.id,
74
+ image_url: null,
75
+ image_file_id: null,
76
+ },
77
+ ]);
78
+
79
+ res.status(200).json({ campaign_detail: updatedDetail });
80
+ } catch (error) {
81
+ console.error("Error deleting campaign image:", error);
82
+ res.status(500).json({
83
+ error: "Failed to delete campaign image",
84
+ details: error instanceof Error ? error.message : String(error),
85
+ });
86
+ }
87
+ };