PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / ReviewForm.tsx

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

573 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Review Form Page
3 * Add/Edit Review form with clean, minimal SaaS-style design
4 */
5
6 import React, { useState, useEffect, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2, Star } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { usePermissions } from "../hooks/usePermissions";
11 import { apiClient } from "../lib/api-client";
12 import { Button } from "../components/ui/button";
13 import { Input } from "../components/ui/input";
14 import { Select } from "../components/ui/select";
15 import { PageHeader } from "../components/common/PageHeader";
16 import {
17 Card,
18 CardContent,
19 CardHeader,
20 CardTitle,
21 } from "../components/ui/card";
22 import { ConditionalRender } from "../components/ui/conditional-render";
23
24 interface ReviewFormData {
25 trip_id: string;
26 customer_name: string;
27 customer_email: string;
28 rating: string;
29 title: string;
30 comment: string;
31 status: string;
32 verified: boolean;
33 }
34
35 const ReviewForm: React.FC = () => {
36 const queryClient = useQueryClient();
37 const { can } = usePermissions();
38 const [formData, setFormData] = useState<ReviewFormData>({
39 trip_id: "",
40 customer_name: "",
41 customer_email: "",
42 rating: "5",
43 title: "",
44 comment: "",
45 status: "pending",
46 verified: false,
47 });
48 const [errors, setErrors] = useState<Record<string, string>>({});
49 const [isSubmitting, setIsSubmitting] = useState(false);
50
51 // Get action and id from URL
52 const action = useMemo(() => {
53 const params = new URLSearchParams(window.location.search);
54 return params.get("action") || "create";
55 }, []);
56
57 const reviewId = useMemo(() => {
58 const params = new URLSearchParams(window.location.search);
59 return params.get("id") ? parseInt(params.get("id") || "0") : null;
60 }, []);
61
62 const isEditMode = action === "edit" && reviewId !== null;
63
64 // Fetch trips for dropdown
65 const { data: tripsData } = useQuery({
66 queryKey: ["trips-list"],
67 queryFn: async () => {
68 const response = await apiClient.get("/trips", {
69 params: {
70 per_page: 100,
71 status: "all", // Get all trips including drafts
72 },
73 });
74 // Transform API response
75 const trips = (response?.data || []).map((trip: any) => ({
76 id: trip.id,
77 title: trip.title || "Untitled Trip",
78 }));
79 return { data: trips };
80 },
81 enabled: can("yatra_view_trips"),
82 });
83
84 // Fetch review data if editing
85 const { data: reviewData, isLoading: isLoadingReview } = useQuery({
86 queryKey: ["review", reviewId],
87 queryFn: async () => {
88 if (!reviewId) return null;
89 const response = await apiClient.get(`/reviews/${reviewId}`);
90 // Transform API response - WordPress REST API may wrap response
91 const review = response?.data || response;
92 return {
93 id: review.id,
94 trip_id: review.trip_id,
95 customer_name: review.customer_name || review.author_name || "",
96 customer_email: review.customer_email || review.author_email || "",
97 rating: review.rating,
98 title: review.title || "",
99 comment: review.comment || review.content || "",
100 status: review.status || "pending",
101 verified: review.verified || false,
102 };
103 },
104 enabled: isEditMode && can("yatra_view_reviews"),
105 });
106
107 // Load review data into form when editing
108 useEffect(() => {
109 if (reviewData && isEditMode) {
110 setFormData({
111 trip_id: reviewData.trip_id?.toString() || "",
112 customer_name: reviewData.customer_name || "",
113 customer_email: reviewData.customer_email || "",
114 rating: reviewData.rating?.toString() || "5",
115 title: reviewData.title || "",
116 comment: reviewData.comment || "",
117 status: reviewData.status || "pending",
118 verified: reviewData.verified || false,
119 });
120 }
121 }, [reviewData, isEditMode]);
122
123 const handleFieldChange = (
124 field: keyof ReviewFormData,
125 value: string | boolean,
126 ) => {
127 setFormData((prev) => ({ ...prev, [field]: value }));
128 if (errors[field]) {
129 setErrors((prev) => ({ ...prev, [field]: "" }));
130 }
131 };
132
133 const renderStarRating = (rating: number) => {
134 return (
135 <div className="flex items-center gap-1">
136 {[1, 2, 3, 4, 5].map((star) => (
137 <button
138 key={star}
139 type="button"
140 onClick={() => handleFieldChange("rating", star.toString())}
141 className="focus:outline-none"
142 >
143 <Star
144 className={`w-6 h-6 transition-colors ${
145 star <= rating
146 ? "fill-yellow-400 text-yellow-400"
147 : "fill-gray-300 text-gray-300 dark:fill-gray-600 dark:text-gray-600 hover:fill-yellow-200 hover:text-yellow-200"
148 }`}
149 />
150 </button>
151 ))}
152 <span className="ml-2 text-sm text-gray-600 dark:text-gray-400">
153 {rating} {__("stars", "yatra")}
154 </span>
155 </div>
156 );
157 };
158
159 const validateForm = (): boolean => {
160 const newErrors: Record<string, string> = {};
161
162 if (!formData.trip_id) {
163 newErrors.trip_id = __("Trip is required", "yatra");
164 }
165
166 if (!formData.customer_name.trim()) {
167 newErrors.customer_name = __("Customer name is required", "yatra");
168 }
169
170 if (!formData.customer_email.trim()) {
171 newErrors.customer_email = __("Email is required", "yatra");
172 } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.customer_email)) {
173 newErrors.customer_email = __("Invalid email address", "yatra");
174 }
175
176 if (
177 !formData.rating ||
178 parseInt(formData.rating) < 1 ||
179 parseInt(formData.rating) > 5
180 ) {
181 newErrors.rating = __("Rating must be between 1 and 5", "yatra");
182 }
183
184 if (!formData.title.trim()) {
185 newErrors.title = __("Review title is required", "yatra");
186 }
187
188 if (!formData.comment.trim()) {
189 newErrors.comment = __("Review comment is required", "yatra");
190 }
191
192 setErrors(newErrors);
193 return Object.keys(newErrors).length === 0;
194 };
195
196 // Create/Update mutation
197 const saveMutation = useMutation({
198 mutationFn: async (data: ReviewFormData) => {
199 const payload = {
200 trip_id: parseInt(data.trip_id),
201 customer_name: data.customer_name.trim(),
202 customer_email: data.customer_email.trim(),
203 rating: parseInt(data.rating),
204 title: data.title.trim(),
205 comment: data.comment.trim(),
206 status: data.status,
207 verified: data.verified,
208 };
209
210 if (isEditMode && reviewId) {
211 return await apiClient.put(`/reviews/${reviewId}`, payload);
212 } else {
213 return await apiClient.post("/reviews", payload);
214 }
215 },
216 onSuccess: () => {
217 queryClient.invalidateQueries({ queryKey: ["reviews"] });
218 // Redirect to reviews list
219 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reviews`;
220 },
221 onError: (error: any) => {
222 const errorMessage =
223 error?.message ||
224 __("An error occurred while saving the review", "yatra");
225 setErrors({ submit: errorMessage });
226 setIsSubmitting(false);
227 },
228 });
229
230 const handleSubmit = async (e: React.FormEvent) => {
231 e.preventDefault();
232
233 if (!validateForm()) {
234 return;
235 }
236
237 setIsSubmitting(true);
238 setErrors({});
239 saveMutation.mutate(formData);
240 };
241
242 const handleCancel = () => {
243 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reviews`;
244 };
245
246 if (isEditMode && isLoadingReview) {
247 return (
248 <div className="flex items-center justify-center p-8">
249 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
250 <span className="ml-2 text-gray-600 dark:text-gray-400">
251 {__("Loading review...", "yatra")}
252 </span>
253 </div>
254 );
255 }
256
257 const trips = tripsData?.data || [];
258 const currentRating = parseInt(formData.rating);
259
260 return (
261 <div className="space-y-3">
262 <PageHeader
263 title={
264 isEditMode
265 ? __("Edit Review", "yatra")
266 : __("Add New Review", "yatra")
267 }
268 description={
269 isEditMode
270 ? __("Update review information", "yatra")
271 : __("Create a new customer review", "yatra")
272 }
273 actions={
274 <Button
275 variant="outline"
276 onClick={handleCancel}
277 className="flex items-center gap-2"
278 >
279 <ArrowLeft className="w-4 h-4" />
280 {__("Back", "yatra")}
281 </Button>
282 }
283 />
284
285 <ConditionalRender capability="yatra_edit_reviews">
286 <form onSubmit={handleSubmit}>
287 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
288 {/* Main Form Fields */}
289 <div className="lg:col-span-2 space-y-3">
290 {/* Review Details */}
291 <Card>
292 <CardHeader className="pb-2">
293 <CardTitle className="text-base">
294 {__("Review Details", "yatra")}
295 </CardTitle>
296 </CardHeader>
297 <CardContent className="space-y-3">
298 {/* Trip Selection */}
299 <div>
300 <label
301 htmlFor="trip_id"
302 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
303 >
304 {__("Trip", "yatra")}{" "}
305 <span className="text-red-500">*</span>
306 </label>
307 <Select
308 id="trip_id"
309 value={formData.trip_id}
310 onChange={(e) =>
311 handleFieldChange("trip_id", e.target.value)
312 }
313 className={errors.trip_id ? "border-red-500" : ""}
314 required
315 >
316 <option value="">{__("Select a trip", "yatra")}</option>
317 {trips.map((trip: any) => (
318 <option key={trip.id} value={trip.id}>
319 {trip.title}
320 </option>
321 ))}
322 </Select>
323 {errors.trip_id && (
324 <p className="mt-1 text-sm text-red-500">
325 {errors.trip_id}
326 </p>
327 )}
328 </div>
329
330 {/* Rating */}
331 <div>
332 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
333 {__("Rating", "yatra")}{" "}
334 <span className="text-red-500">*</span>
335 </label>
336 {renderStarRating(currentRating)}
337 {errors.rating && (
338 <p className="mt-1 text-sm text-red-500">
339 {errors.rating}
340 </p>
341 )}
342 </div>
343
344 {/* Title */}
345 <div>
346 <label
347 htmlFor="title"
348 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
349 >
350 {__("Review Title", "yatra")}{" "}
351 <span className="text-red-500">*</span>
352 </label>
353 <Input
354 id="title"
355 type="text"
356 value={formData.title}
357 onChange={(e) =>
358 handleFieldChange("title", e.target.value)
359 }
360 placeholder={__("Enter review title", "yatra")}
361 className={errors.title ? "border-red-500" : ""}
362 required
363 />
364 {errors.title && (
365 <p className="mt-1 text-sm text-red-500">
366 {errors.title}
367 </p>
368 )}
369 </div>
370
371 {/* Comment */}
372 <div>
373 <label
374 htmlFor="comment"
375 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
376 >
377 {__("Review Comment", "yatra")}{" "}
378 <span className="text-red-500">*</span>
379 </label>
380 <textarea
381 id="comment"
382 value={formData.comment}
383 onChange={(e) =>
384 handleFieldChange("comment", e.target.value)
385 }
386 placeholder={__("Enter review comment", "yatra")}
387 rows={6}
388 className={`flex w-full rounded-md border ${errors.comment ? "border-red-500" : "border-gray-300"} bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none`}
389 required
390 />
391 {errors.comment && (
392 <p className="mt-1 text-sm text-red-500">
393 {errors.comment}
394 </p>
395 )}
396 </div>
397 </CardContent>
398 </Card>
399
400 {/* Customer Information */}
401 <Card>
402 <CardHeader className="pb-2">
403 <CardTitle className="text-base">
404 {__("Customer Information", "yatra")}
405 </CardTitle>
406 </CardHeader>
407 <CardContent className="space-y-3">
408 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
409 {/* Customer Name */}
410 <div>
411 <label
412 htmlFor="customer_name"
413 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
414 >
415 {__("Customer Name", "yatra")}{" "}
416 <span className="text-red-500">*</span>
417 </label>
418 <Input
419 id="customer_name"
420 type="text"
421 value={formData.customer_name}
422 onChange={(e) =>
423 handleFieldChange("customer_name", e.target.value)
424 }
425 placeholder={__("Enter customer name", "yatra")}
426 className={errors.customer_name ? "border-red-500" : ""}
427 required
428 />
429 {errors.customer_name && (
430 <p className="mt-1 text-sm text-red-500">
431 {errors.customer_name}
432 </p>
433 )}
434 </div>
435
436 {/* Customer Email */}
437 <div>
438 <label
439 htmlFor="customer_email"
440 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
441 >
442 {__("Customer Email", "yatra")}{" "}
443 <span className="text-red-500">*</span>
444 </label>
445 <Input
446 id="customer_email"
447 type="email"
448 value={formData.customer_email}
449 onChange={(e) =>
450 handleFieldChange("customer_email", e.target.value)
451 }
452 placeholder={__("[email protected]", "yatra")}
453 className={
454 errors.customer_email ? "border-red-500" : ""
455 }
456 required
457 />
458 {errors.customer_email && (
459 <p className="mt-1 text-sm text-red-500">
460 {errors.customer_email}
461 </p>
462 )}
463 </div>
464 </div>
465 </CardContent>
466 </Card>
467 </div>
468
469 {/* Sidebar */}
470 <div className="space-y-3">
471 {/* Status */}
472 <Card>
473 <CardHeader className="pb-2">
474 <CardTitle className="text-base">
475 {__("Status", "yatra")}
476 </CardTitle>
477 </CardHeader>
478 <CardContent className="space-y-3">
479 <div>
480 <label
481 htmlFor="status"
482 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
483 >
484 {__("Review Status", "yatra")}
485 </label>
486 <Select
487 id="status"
488 value={formData.status}
489 onChange={(e) =>
490 handleFieldChange("status", e.target.value)
491 }
492 >
493 <option value="pending">{__("Pending", "yatra")}</option>
494 <option value="approved">
495 {__("Approved", "yatra")}
496 </option>
497 <option value="spam">{__("Spam", "yatra")}</option>
498 <option value="trash">{__("Trash", "yatra")}</option>
499 </Select>
500 </div>
501
502 {/* Verified */}
503 <div className="flex items-center gap-2">
504 <input
505 type="checkbox"
506 id="verified"
507 checked={formData.verified}
508 onChange={(e) =>
509 handleFieldChange("verified", e.target.checked)
510 }
511 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
512 />
513 <label
514 htmlFor="verified"
515 className="text-sm font-medium text-gray-700 dark:text-gray-300"
516 >
517 {__("Verified Purchase", "yatra")}
518 </label>
519 </div>
520 </CardContent>
521 </Card>
522
523 {/* Submit Actions */}
524 <Card>
525 <CardContent className="p-3">
526 <div className="space-y-2">
527 {errors.submit && (
528 <div className="p-2 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-md">
529 {errors.submit}
530 </div>
531 )}
532 <div className="flex gap-2">
533 <Button
534 type="submit"
535 disabled={isSubmitting}
536 className="flex-1 flex items-center justify-center gap-2"
537 >
538 {isSubmitting ? (
539 <>
540 <Loader2 className="w-4 h-4 animate-spin" />
541 {__("Saving...", "yatra")}
542 </>
543 ) : (
544 <>
545 <Save className="w-4 h-4" />
546 {isEditMode
547 ? __("Update Review", "yatra")
548 : __("Create Review", "yatra")}
549 </>
550 )}
551 </Button>
552 <Button
553 type="button"
554 variant="outline"
555 onClick={handleCancel}
556 disabled={isSubmitting}
557 >
558 {__("Cancel", "yatra")}
559 </Button>
560 </div>
561 </div>
562 </CardContent>
563 </Card>
564 </div>
565 </div>
566 </form>
567 </ConditionalRender>
568 </div>
569 );
570 };
571
572 export default ReviewForm;
573