| @@ -18,8 +18,9 @@ | ||
| 18 | 18 | Send, |
| 19 | 19 | Loader2, |
| 20 | 20 | } from "lucide-react"; |
| 21 | 21 | import { __ } from "../lib/i18n"; |
| 22 | +import { toDateValue } from "../lib/dateFormat"; | |
| 22 | 23 | import { apiService } from "../lib/api-client"; |
| 23 | 24 | import { usePermissions } from "../hooks/usePermissions"; |
| 24 | 25 | import { Button } from "../components/ui/button"; |
| 25 | 26 | import { PageHeader } from "../components/common/PageHeader"; |
| @@ -29,8 +30,9 @@ | ||
| 29 | 30 | CardHeader, |
| 30 | 31 | CardTitle, |
| 31 | 32 | } from "../components/ui/card"; |
| 32 | 33 | import { ConditionalRender } from "../components/ui/conditional-render"; |
| 34 | +import { EnquiryReplyAffordance } from "../components/ai/EnquiryReplyAffordance"; | |
| 33 | 35 | import { Badge } from "../components/ui/badge"; |
| 34 | 36 | import { useNavigate } from "../hooks/useNavigate"; |
| 35 | 37 | import { Skeleton } from "../components/ui/skeleton"; |
| 36 | 38 | import { Modal } from "../components/ui/modal"; |
| @@ -86,9 +88,9 @@ | ||
| 86 | 88 | const response = await apiService.getEnquiry(enquiryId!); |
| 87 | 89 | // Some endpoints return { success, data }, others return the object directly |
| 88 | 90 | return (response as any)?.data ?? response; |
| 89 | 91 | }, |
| 90 | - enabled: !!enquiryId && can("yatra_view_bookings"), | |
| 92 | + enabled: !!enquiryId && can("yatra_view_enquiries"), | |
| 91 | 93 | }); |
| 92 | 94 | |
| 93 | 95 | // Respond mutation |
| 94 | 96 | const respondMutation = useMutation({ |
| @@ -103,9 +105,11 @@ | ||
| 103 | 105 | }, |
| 104 | 106 | }); |
| 105 | 107 | |
| 106 | 108 | const formatDate = (dateString: string) => { |
| 107 | - return new Date(dateString).toLocaleDateString("en-US", { | |
| 109 | + // toDateValue: a date-only travel_date must not roll back a day in a | |
| 110 | + // behind-UTC zone (datetime values like created_at pass through unchanged). | |
| 111 | + return toDateValue(dateString).toLocaleDateString("en-US", { | |
| 108 | 112 | weekday: "long", |
| 109 | 113 | year: "numeric", |
| 110 | 114 | month: "long", |
| 111 | 115 | day: "numeric", |
| @@ -128,8 +132,13 @@ | ||
| 128 | 132 | className: |
| 129 | 133 | "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400", |
| 130 | 134 | label: __("Responded", "yatra"), |
| 131 | 135 | }, |
| 136 | + completed: { | |
| 137 | + className: | |
| 138 | + "bg-teal-100 text-teal-700 dark:bg-teal-900/20 dark:text-teal-400", | |
| 139 | + label: __("Completed", "yatra"), | |
| 140 | + }, | |
| 132 | 141 | converted: { |
| 133 | 142 | className: |
| 134 | 143 | "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400", |
| 135 | 144 | label: __("Converted", "yatra"), |
| @@ -297,11 +306,12 @@ | ||
| 297 | 306 | > |
| 298 | 307 | <ArrowLeft className="w-4 h-4" /> |
| 299 | 308 | {__("Back", "yatra")} |
| 300 | 309 | </Button> |
| 301 | - <ConditionalRender capability="yatra_edit_bookings"> | |
| 310 | + <ConditionalRender capability="yatra_respond_to_enquiries"> | |
| 302 | 311 | <div className="flex items-center gap-2"> |
| 303 | 312 | {enquiry.status !== "responded" && |
| 313 | + enquiry.status !== "completed" && | |
| 304 | 314 | enquiry.status !== "closed" && ( |
| 305 | 315 | <Button |
| 306 | 316 | variant="outline" |
| 307 | 317 | onClick={handleRespond} |
| @@ -323,9 +333,9 @@ | ||
| 323 | 333 | </div> |
| 324 | 334 | } |
| 325 | 335 | /> |
| 326 | 336 | |
| 327 | - <ConditionalRender capability="yatra_view_bookings"> | |
| 337 | + <ConditionalRender capability="yatra_view_enquiries"> | |
| 328 | 338 | <div className="grid grid-cols-1 lg:grid-cols-3 gap-3"> |
| 329 | 339 | {/* Main Content */} |
| 330 | 340 | <div className="lg:col-span-2 space-y-3"> |
| 331 | 341 | {/* Enquiry Information */} |
| @@ -560,14 +570,29 @@ | ||
| 560 | 570 | </p> |
| 561 | 571 | </div> |
| 562 | 572 | </div> |
| 563 | 573 | |
| 564 | - {/* Response Message */} | |
| 574 | + {/* Response Message — label + AI sparkle on the same | |
| 575 | + row so the operator sees the "Generate with AI" | |
| 576 | + option right where they're about to type. Uses the | |
| 577 | + same EnquiryReplyAffordance component the admin | |
| 578 | + EnquiryForm uses, so behavior (Generate / Warmer / | |
| 579 | + Shorter variants, sensitive-data toggle) is identical | |
| 580 | + across the two surfaces. */} | |
| 565 | 581 | <div> |
| 566 | - <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"> | |
| 567 | - {__("Your Response", "yatra")}{" "} | |
| 568 | - <span className="text-red-500">*</span> | |
| 569 | - </label> | |
| 582 | + <div className="mb-1.5 flex items-center justify-between gap-2"> | |
| 583 | + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300"> | |
| 584 | + {__("Your Response", "yatra")}{" "} | |
| 585 | + <span className="text-red-500">*</span> | |
| 586 | + </label> | |
| 587 | + {enquiryId && ( | |
| 588 | + <EnquiryReplyAffordance | |
| 589 | + enquiryId={Number(enquiryId)} | |
| 590 | + value={responseMessage} | |
| 591 | + onAccept={(text) => setResponseMessage(text)} | |
| 592 | + /> | |
| 593 | + )} | |
| 594 | + </div> | |
| 570 | 595 | <textarea |
| 571 | 596 | value={responseMessage} |
| 572 | 597 | onChange={(e) => setResponseMessage(e.target.value)} |
| 573 | 598 | placeholder={__("Type your response here...", "yatra")} |