| @@ -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> |