PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | resources/js/pages/EnquiryForm.tsx +43 -7 3.0.3 → 3.0.17 View file →
@@ -23,8 +23,10 @@
23 23 } from "../components/ui/card";
24 24 import { ConditionalRender } from "../components/ui/conditional-render";
25 25 import { useNavigate } from "../hooks/useNavigate";
26 26 import { Skeleton } from "../components/ui/skeleton";
27 +import { EnquiryReplyAffordance } from "../components/ai/EnquiryReplyAffordance";
28 +import { Sparkles } from "lucide-react";
27 29
28 30 interface Enquiry {
29 31 id: number;
30 32 name: string;
@@ -72,8 +74,13 @@
72 74 travel_date: "",
73 75 status: "new",
74 76 response_notes: "",
75 77 });
78 + // Tracks whether the AI is currently drafting a reply so the banner
79 + // above the textarea can show / hide. Lives in EnquiryForm (not in
80 + // EnquiryReplyAffordance) so the textarea + banner can render side by
81 + // side without the affordance owning the layout.
82 + const [aiDrafting, setAiDrafting] = useState(false);
76 83
77 84 // Get enquiry id from URL (null for create, number for edit)
78 85 const enquiryId = useMemo(() => {
79 86 const params = new URLSearchParams(window.location.search);
@@ -92,9 +99,9 @@
92 99 const response = await apiService.getEnquiry(enquiryId!);
93 100 // Some endpoints return { success, data }, others return the object directly
94 101 return (response as any)?.data ?? response;
95 102 },
96 - enabled: !!enquiryId && can("yatra_edit_bookings"),
103 + enabled: !!enquiryId && can("yatra_respond_to_enquiries"),
97 104 });
98 105
99 106 // Fetch trips for dropdown
100 107 const { data: tripsData } = useQuery({
@@ -101,9 +108,9 @@
101 108 queryKey: ["trips-select"],
102 109 queryFn: async () => {
103 110 return await apiService.getTrips({ per_page: 100 });
104 111 },
105 - enabled: can("yatra_edit_bookings"),
112 + enabled: can("yatra_respond_to_enquiries"),
106 113 });
107 114
108 115 // Normalize trips response to array
109 116 const trips: Trip[] = Array.isArray((tripsData as any)?.data)
@@ -247,9 +254,9 @@
247 254 </div>
248 255 }
249 256 />
250 257
251 - <ConditionalRender capability="yatra_edit_bookings">
258 + <ConditionalRender capability="yatra_respond_to_enquiries">
252 259 <form onSubmit={handleSubmit}>
253 260 {saveMutation.isError && (
254 261 <div className="mb-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-600 dark:text-red-400">
255 262 {saveMutation.error?.message ||
@@ -366,13 +373,39 @@
366 373 {/* Response Notes */}
367 374 {isEdit && (
368 375 <Card>
369 376 <CardHeader className="pb-2">
370 - <CardTitle className="text-base">
371 - {__("Response Notes", "yatra")}
372 - </CardTitle>
377 + <div className="flex items-center justify-between">
378 + <CardTitle className="text-base">
379 + {__("Response Notes", "yatra")}
380 + </CardTitle>
381 + {enquiryId && (
382 + <EnquiryReplyAffordance
383 + enquiryId={Number(enquiryId)}
384 + value={formData.response_notes || ""}
385 + onAccept={(text) =>
386 + handleChange("response_notes", text)
387 + }
388 + autoDraftOnMount={
389 + !(formData.response_notes || "").trim() &&
390 + (formData.status === "new" ||
391 + !formData.responded_at)
392 + }
393 + onDraftingChange={setAiDrafting}
394 + />
395 + )}
396 + </div>
373 397 </CardHeader>
374 398 <CardContent>
399 + {aiDrafting && (
400 + <div className="mb-2 flex items-center gap-2 rounded-md border border-purple-200 bg-purple-50/60 px-2 py-1 text-xs text-purple-700 dark:border-purple-500/40 dark:bg-purple-900/20 dark:text-purple-200">
401 + <Sparkles className="h-3.5 w-3.5 animate-pulse" />
402 + {__(
403 + "AI is drafting a reply based on this enquiry, the trip details, and the customer's history…",
404 + "yatra",
405 + )}
406 + </div>
407 + )}
375 408 <textarea
376 409 value={formData.response_notes || ""}
377 410 onChange={(e) =>
378 411 handleChange("response_notes", e.target.value)
@@ -381,9 +414,9 @@
381 414 "Internal notes about responses to this enquiry...",
382 415 "yatra",
383 416 )}
384 417 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
385 - rows={4}
418 + rows={6}
386 419 />
387 420 </CardContent>
388 421 </Card>
389 422 )}
@@ -406,8 +439,11 @@
406 439 <option value="new">{__("New", "yatra")}</option>
407 440 <option value="pending">{__("Pending", "yatra")}</option>
408 441 <option value="responded">
409 442 {__("Responded", "yatra")}
443 + </option>
444 + <option value="completed">
445 + {__("Completed", "yatra")}
410 446 </option>
411 447 <option value="converted">
412 448 {__("Converted", "yatra")}
413 449 </option>