PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / AbandonedRecoveryEmailForm.tsx

AbandonedRecoveryEmailForm.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.10, at resources/js/pages/AbandonedRecoveryEmailForm.tsx

360 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Abandoned Recovery Email Campaign Form Page
3 *
4 * Create and edit email recovery campaigns
5 *
6 * @package Yatra
7 * @since 3.0.0
8 */
9
10 import React, { useState, useEffect } from "react";
11 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
12 import {
13 Card,
14 CardContent,
15 CardHeader,
16 CardTitle,
17 CardDescription,
18 } from "../components/ui/card";
19 import { Button } from "../components/ui/button";
20 import { Label } from "../components/ui/label";
21 import { Input } from "../components/ui/input";
22 import { Select } from "../components/ui/select";
23 import { PageHeader } from "../components/common/PageHeader";
24 import { useToast } from "../components/ui/toast";
25 import { __ } from "../lib/i18n";
26 import { apiService } from "../lib/api-client";
27 import { ArrowLeft, Save, Loader2 } from "lucide-react";
28
29 interface EmailCampaignFormData {
30 name: string;
31 subject: string;
32 email_body: string;
33 delay_hours: number;
34 status: string;
35 discount_code: string;
36 discount_amount: number;
37 discount_type: string;
38 }
39
40 const AbandonedRecoveryEmailForm: React.FC = () => {
41 const { showToast } = useToast();
42 const queryClient = useQueryClient();
43 const urlParams = new URLSearchParams(window.location.search);
44 const campaignId = urlParams.get("id");
45 const isEdit = !!campaignId;
46
47 const [formData, setFormData] = useState<EmailCampaignFormData>({
48 name: "",
49 subject: "",
50 email_body: "",
51 delay_hours: 1,
52 status: "active",
53 discount_code: "",
54 discount_amount: 0,
55 discount_type: "percentage",
56 });
57
58 // Fetch existing campaign if editing
59 const { data: campaignData, isLoading } = useQuery({
60 queryKey: ["abandoned-recovery-campaign", campaignId],
61 queryFn: async () => {
62 if (!campaignId) return null;
63 const data = await apiService.getAbandonedBookingCampaign(campaignId!);
64 return data;
65 },
66 enabled: isEdit,
67 });
68
69 // Populate form with existing data
70 useEffect(() => {
71 if (campaignData?.data) {
72 setFormData(campaignData.data);
73 }
74 }, [campaignData]);
75
76 // Create/Update mutation
77 const saveMutation = useMutation({
78 mutationFn: async (data: EmailCampaignFormData) => {
79 return isEdit
80 ? await apiService.updateAbandonedBookingCampaign(campaignId!, data)
81 : await apiService.createAbandonedBookingCampaign(data);
82 },
83 onSuccess: () => {
84 showToast(
85 isEdit
86 ? __("Email campaign updated successfully")
87 : __("Email campaign created successfully"),
88 "success",
89 );
90 queryClient.invalidateQueries({
91 queryKey: ["abandoned-recovery-campaigns"],
92 });
93 // Navigate back to main page
94 window.location.href = window.location.href.split("&action=")[0];
95 },
96 onError: (error: any) => {
97 showToast(error?.message || __("Failed to save email campaign"), "error");
98 },
99 });
100
101 const handleSubmit = (e: React.FormEvent) => {
102 e.preventDefault();
103 saveMutation.mutate(formData);
104 };
105
106 const handleCancel = () => {
107 window.location.href = window.location.href.split("&action=")[0];
108 };
109
110 const handleChange = (field: keyof EmailCampaignFormData, value: any) => {
111 setFormData((prev) => ({ ...prev, [field]: value }));
112 };
113
114 if (isLoading) {
115 return (
116 <div className="flex items-center justify-center h-64">
117 <Loader2 className="w-8 h-8 animate-spin text-blue-600" />
118 </div>
119 );
120 }
121
122 return (
123 <div className="space-y-6">
124 <PageHeader
125 title={isEdit ? __("Edit Email Campaign") : __("Create Email Campaign")}
126 description={__(
127 "Configure automated email campaigns to recover abandoned bookings",
128 )}
129 />
130
131 <form onSubmit={handleSubmit} className="space-y-6">
132 {/* Basic Information */}
133 <Card>
134 <CardHeader>
135 <CardTitle>{__("Campaign Information")}</CardTitle>
136 <CardDescription>{__("Basic campaign details")}</CardDescription>
137 </CardHeader>
138 <CardContent className="space-y-4">
139 <div>
140 <Label htmlFor="name">{__("Campaign Name")} *</Label>
141 <Input
142 id="name"
143 value={formData.name}
144 onChange={(e) => handleChange("name", e.target.value)}
145 placeholder={__("e.g., First Reminder Email")}
146 required
147 />
148 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
149 {__(
150 "Internal name for this campaign (not visible to customers)",
151 )}
152 </p>
153 </div>
154
155 <div>
156 <Label htmlFor="delay_hours">{__("Send After (Hours)")} *</Label>
157 <Input
158 id="delay_hours"
159 type="number"
160 value={formData.delay_hours}
161 onChange={(e) =>
162 handleChange("delay_hours", parseInt(e.target.value))
163 }
164 min="1"
165 max="168"
166 required
167 />
168 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
169 {__(
170 "How many hours after abandonment to send this email (1-168 hours)",
171 )}
172 </p>
173 </div>
174
175 <div>
176 <Label htmlFor="status">{__("Status")} *</Label>
177 <Select
178 id="status"
179 value={formData.status}
180 onChange={(e) => handleChange("status", e.target.value)}
181 required
182 >
183 <option value="active">{__("Active")}</option>
184 <option value="inactive">{__("Inactive")}</option>
185 </Select>
186 </div>
187 </CardContent>
188 </Card>
189
190 {/* Email Content */}
191 <Card>
192 <CardHeader>
193 <CardTitle>{__("Email Content")}</CardTitle>
194 <CardDescription>
195 {__("Customize the email message")}
196 </CardDescription>
197 </CardHeader>
198 <CardContent className="space-y-4">
199 <div>
200 <Label htmlFor="subject">{__("Email Subject")} *</Label>
201 <Input
202 id="subject"
203 value={formData.subject}
204 onChange={(e) => handleChange("subject", e.target.value)}
205 placeholder={__("Complete your booking and save 10%")}
206 required
207 />
208 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
209 {__(
210 "Available variables: {customer_name}, {trip_name}, {booking_amount}",
211 )}
212 </p>
213 </div>
214
215 <div>
216 <Label htmlFor="email_body">{__("Email Body")} *</Label>
217 <textarea
218 id="email_body"
219 value={formData.email_body}
220 onChange={(e) => handleChange("email_body", e.target.value)}
221 placeholder={__(
222 "Hi {customer_name},\n\nWe noticed you started booking {trip_name} but didn't complete it...",
223 )}
224 required
225 rows={10}
226 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-white"
227 />
228 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
229 {__(
230 "Available variables: {customer_name}, {trip_name}, {booking_amount}, {recovery_link}, {discount_code}",
231 )}
232 </p>
233 </div>
234 </CardContent>
235 </Card>
236
237 {/* Incentive */}
238 <Card>
239 <CardHeader>
240 <CardTitle>{__("Discount Incentive (Optional)")}</CardTitle>
241 <CardDescription>
242 {__("Offer a discount to encourage completion")}
243 </CardDescription>
244 </CardHeader>
245 <CardContent className="space-y-4">
246 <div>
247 <Label htmlFor="discount_code">{__("Discount Code")}</Label>
248 <Input
249 id="discount_code"
250 value={formData.discount_code}
251 onChange={(e) => handleChange("discount_code", e.target.value)}
252 placeholder={__("e.g., COMEBACK10")}
253 />
254 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
255 {__("Leave empty if no discount should be offered")}
256 </p>
257 </div>
258
259 <div>
260 <Label htmlFor="discount_type">{__("Discount Type")}</Label>
261 <Select
262 id="discount_type"
263 value={formData.discount_type}
264 onChange={(e) => handleChange("discount_type", e.target.value)}
265 >
266 <option value="percentage">{__("Percentage")}</option>
267 <option value="fixed">{__("Fixed Amount")}</option>
268 </Select>
269 </div>
270
271 <div>
272 <Label htmlFor="discount_amount">{__("Discount Amount")}</Label>
273 <Input
274 id="discount_amount"
275 type="number"
276 step="0.01"
277 value={formData.discount_amount}
278 onChange={(e) =>
279 handleChange("discount_amount", parseFloat(e.target.value))
280 }
281 min="0"
282 placeholder={
283 formData.discount_type === "percentage" ? "10" : "50"
284 }
285 />
286 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
287 {formData.discount_type === "percentage"
288 ? __("Enter percentage (e.g., 10 for 10% off)")
289 : __("Enter fixed amount (e.g., 50 for $50 off)")}
290 </p>
291 </div>
292 </CardContent>
293 </Card>
294
295 {/* Preview */}
296 <Card>
297 <CardHeader>
298 <CardTitle>{__("Email Preview")}</CardTitle>
299 <CardDescription>{__("How the email will look")}</CardDescription>
300 </CardHeader>
301 <CardContent>
302 <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-md border border-gray-200 dark:border-gray-700">
303 <div className="mb-4">
304 <strong className="text-sm text-gray-600 dark:text-gray-400">
305 {__("Subject:")}
306 </strong>
307 <p className="text-gray-900 dark:text-white">
308 {formData.subject || __("(No subject)")}
309 </p>
310 </div>
311 <div>
312 <strong className="text-sm text-gray-600 dark:text-gray-400">
313 {__("Body:")}
314 </strong>
315 <div className="text-gray-900 dark:text-white whitespace-pre-wrap mt-2">
316 {formData.email_body || __("(No content)")}
317 </div>
318 </div>
319 {formData.discount_code && (
320 <div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded">
321 <p className="text-sm text-blue-800 dark:text-blue-400">
322 <strong>{__("Discount:")}</strong> {formData.discount_code}{" "}
323 - {formData.discount_amount}
324 {formData.discount_type === "percentage" ? "%" : ""}{" "}
325 {__("off")}
326 </p>
327 </div>
328 )}
329 </div>
330 </CardContent>
331 </Card>
332
333 {/* Actions */}
334 <div className="flex items-center justify-between">
335 <Button type="button" variant="outline" onClick={handleCancel}>
336 <ArrowLeft className="w-4 h-4 mr-2" />
337 {__("Cancel")}
338 </Button>
339
340 <Button type="submit" disabled={saveMutation.isPending}>
341 {saveMutation.isPending ? (
342 <>
343 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
344 {__("Saving...")}
345 </>
346 ) : (
347 <>
348 <Save className="w-4 h-4 mr-2" />
349 {isEdit ? __("Update Campaign") : __("Create Campaign")}
350 </>
351 )}
352 </Button>
353 </div>
354 </form>
355 </div>
356 );
357 };
358
359 export default AbandonedRecoveryEmailForm;
360