PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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 / ViewEnquiry.tsx

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

610 lines 21.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * View Enquiry Page
3 * Display enquiry details in a clean, minimal SaaS-style design
4 */
5
6 import React, { useMemo, useState } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 ArrowLeft,
10 Mail,
11 Phone,
12 Calendar,
13 MessageSquare,
14 Edit,
15 ExternalLink,
16 MapPin,
17 Users,
18 Send,
19 Loader2,
20 } from "lucide-react";
21 import { __ } from "../lib/i18n";
22 import { apiService } from "../lib/api-client";
23 import { usePermissions } from "../hooks/usePermissions";
24 import { Button } from "../components/ui/button";
25 import { PageHeader } from "../components/common/PageHeader";
26 import {
27 Card,
28 CardContent,
29 CardHeader,
30 CardTitle,
31 } from "../components/ui/card";
32 import { ConditionalRender } from "../components/ui/conditional-render";
33 import { EnquiryReplyAffordance } from "../components/ai/EnquiryReplyAffordance";
34 import { Badge } from "../components/ui/badge";
35 import { useNavigate } from "../hooks/useNavigate";
36 import { Skeleton } from "../components/ui/skeleton";
37 import { Modal } from "../components/ui/modal";
38 import { useToast } from "../components/ui/toast";
39
40 interface Enquiry {
41 id: number;
42 name: string;
43 email: string;
44 phone?: string;
45 trip_id?: number;
46 trip_title?: string;
47 trip_slug?: string;
48 message: string;
49 travelers_count?: number;
50 travel_date?: string;
51 status:
52 | "new"
53 | "pending"
54 | "responded"
55 | "closed"
56 | "converted"
57 | ""
58 | string;
59 created_at: string;
60 responded_at?: string;
61 response?: string;
62 }
63
64 const ViewEnquiry: React.FC = () => {
65 const { can } = usePermissions();
66 const { navigate } = useNavigate();
67 const queryClient = useQueryClient();
68 const { showToast } = useToast();
69 const [respondDialogOpen, setRespondDialogOpen] = useState(false);
70 const [responseMessage, setResponseMessage] = useState("");
71
72 // Get enquiry id from URL
73 const enquiryId = useMemo(() => {
74 const params = new URLSearchParams(window.location.search);
75 return params.get("id") ? parseInt(params.get("id") || "0") : null;
76 }, []);
77
78 // Fetch enquiry data from API
79 const {
80 data: enquiry,
81 isLoading,
82 error,
83 } = useQuery<Enquiry | null>({
84 queryKey: ["enquiry", enquiryId],
85 queryFn: async () => {
86 if (!enquiryId) return null;
87 const response = await apiService.getEnquiry(enquiryId!);
88 // Some endpoints return { success, data }, others return the object directly
89 return (response as any)?.data ?? response;
90 },
91 enabled: !!enquiryId && can("yatra_view_enquiries"),
92 });
93
94 // Respond mutation
95 const respondMutation = useMutation({
96 mutationFn: async ({ id, message }: { id: number; message: string }) => {
97 return await apiService.respondToEnquiry(id, { response: message });
98 },
99 onSuccess: () => {
100 queryClient.invalidateQueries({ queryKey: ["enquiry", enquiryId] });
101 setRespondDialogOpen(false);
102 setResponseMessage("");
103 showToast(__("Response sent successfully.", "yatra"), "success");
104 },
105 });
106
107 const formatDate = (dateString: string) => {
108 return new Date(dateString).toLocaleDateString("en-US", {
109 weekday: "long",
110 year: "numeric",
111 month: "long",
112 day: "numeric",
113 });
114 };
115
116 const getStatusBadge = (status: string) => {
117 const statusMap: Record<string, { className: string; label: string }> = {
118 new: {
119 className:
120 "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
121 label: __("New", "yatra"),
122 },
123 pending: {
124 className:
125 "bg-orange-100 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400",
126 label: __("Pending", "yatra"),
127 },
128 responded: {
129 className:
130 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
131 label: __("Responded", "yatra"),
132 },
133 converted: {
134 className:
135 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
136 label: __("Converted", "yatra"),
137 },
138 closed: {
139 className:
140 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
141 label: __("Closed", "yatra"),
142 },
143 };
144
145 const statusInfo = statusMap[status] || {
146 className:
147 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
148 label: status,
149 };
150
151 return (
152 <Badge className={`text-sm ${statusInfo.className}`}>
153 {statusInfo.label}
154 </Badge>
155 );
156 };
157
158 const handleBack = () => {
159 navigate({ subpage: "enquiries" });
160 };
161
162 const handleEdit = () => {
163 if (enquiryId) {
164 navigate({ subpage: "enquiries", action: "edit", id: enquiryId });
165 }
166 };
167
168 const handleViewTrip = () => {
169 if (enquiry?.trip_id) {
170 navigate({
171 subpage: "trips",
172 tab: "all",
173 action: "edit",
174 id: enquiry.trip_id,
175 });
176 }
177 };
178
179 const handleRespond = () => {
180 setResponseMessage("");
181 setRespondDialogOpen(true);
182 };
183
184 const sendResponse = () => {
185 if (enquiryId && responseMessage.trim()) {
186 respondMutation.mutate({
187 id: enquiryId,
188 message: responseMessage,
189 });
190 }
191 };
192
193 if (isLoading) {
194 return (
195 <div className="space-y-3">
196 <PageHeader
197 title={__("Enquiry Details", "yatra")}
198 actions={
199 <div className="flex items-center gap-2">
200 <Skeleton className="h-10 w-24" />
201 <Skeleton className="h-10 w-32" />
202 </div>
203 }
204 />
205 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
206 <div className="lg:col-span-2 space-y-3">
207 <Card>
208 <CardHeader className="pb-2">
209 <div className="flex items-center justify-between">
210 <Skeleton className="h-5 w-40" />
211 <Skeleton className="h-6 w-20 rounded-full" />
212 </div>
213 </CardHeader>
214 <CardContent className="space-y-4">
215 <div>
216 <Skeleton className="h-3 w-16 mb-2" />
217 <Skeleton className="h-24 w-full rounded-lg" />
218 </div>
219 <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
220 <div>
221 <Skeleton className="h-3 w-32 mb-2" />
222 <Skeleton className="h-5 w-16" />
223 </div>
224 <div>
225 <Skeleton className="h-3 w-36 mb-2" />
226 <Skeleton className="h-5 w-40" />
227 </div>
228 </div>
229 </CardContent>
230 </Card>
231 </div>
232 <div className="space-y-3">
233 <Card>
234 <CardHeader className="pb-2">
235 <Skeleton className="h-5 w-24" />
236 </CardHeader>
237 <CardContent className="space-y-3">
238 <Skeleton className="h-5 w-32" />
239 <Skeleton className="h-4 w-48" />
240 <Skeleton className="h-4 w-36" />
241 </CardContent>
242 </Card>
243 <Card>
244 <CardHeader className="pb-2">
245 <Skeleton className="h-5 w-20" />
246 </CardHeader>
247 <CardContent className="space-y-3">
248 <div>
249 <Skeleton className="h-3 w-20 mb-2" />
250 <Skeleton className="h-5 w-40" />
251 </div>
252 </CardContent>
253 </Card>
254 </div>
255 </div>
256 </div>
257 );
258 }
259 if (error || !enquiry) {
260 return (
261 <div className="space-y-3">
262 <PageHeader
263 title={__("Enquiry Not Found", "yatra")}
264 actions={
265 <Button
266 variant="outline"
267 onClick={handleBack}
268 className="flex items-center gap-2"
269 >
270 <ArrowLeft className="w-4 h-4" />
271 {__("Back to Enquiries", "yatra")}
272 </Button>
273 }
274 />
275 <Card>
276 <CardContent className="p-8 text-center text-red-500">
277 {__(
278 "Enquiry not found or you do not have permission to view it.",
279 "yatra",
280 )}
281 </CardContent>
282 </Card>
283 </div>
284 );
285 }
286
287 return (
288 <div className="space-y-3">
289 <PageHeader
290 title={__("Enquiry Details", "yatra")}
291 description={__("View complete enquiry information", "yatra")}
292 actions={
293 <div className="flex items-center gap-2">
294 <Button
295 variant="outline"
296 onClick={handleBack}
297 className="flex items-center gap-2"
298 >
299 <ArrowLeft className="w-4 h-4" />
300 {__("Back", "yatra")}
301 </Button>
302 <ConditionalRender capability="yatra_respond_to_enquiries">
303 <div className="flex items-center gap-2">
304 {enquiry.status !== "responded" &&
305 enquiry.status !== "closed" && (
306 <Button
307 variant="outline"
308 onClick={handleRespond}
309 className="flex items-center gap-2 text-blue-600 border-blue-300 hover:bg-blue-50 dark:text-blue-400 dark:border-blue-700 dark:hover:bg-blue-900/20"
310 >
311 <Send className="w-4 h-4" />
312 {__("Send Response", "yatra")}
313 </Button>
314 )}
315 <Button
316 onClick={handleEdit}
317 className="flex items-center gap-2"
318 >
319 <Edit className="w-4 h-4" />
320 {__("Edit Enquiry", "yatra")}
321 </Button>
322 </div>
323 </ConditionalRender>
324 </div>
325 }
326 />
327
328 <ConditionalRender capability="yatra_view_enquiries">
329 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
330 {/* Main Content */}
331 <div className="lg:col-span-2 space-y-3">
332 {/* Enquiry Information */}
333 <Card>
334 <CardHeader className="pb-2">
335 <div className="flex items-center justify-between">
336 <CardTitle className="text-base">
337 {__("Enquiry Information", "yatra")}
338 </CardTitle>
339 {getStatusBadge(enquiry.status)}
340 </div>
341 </CardHeader>
342 <CardContent className="space-y-4">
343 <div>
344 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2 block">
345 {__("Message", "yatra")}
346 </label>
347 <div className="flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
348 <MessageSquare className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
349 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap flex-1">
350 {enquiry.message}
351 </p>
352 </div>
353 </div>
354
355 <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
356 {enquiry.travelers_count && (
357 <div>
358 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
359 {__("Number of Travelers", "yatra")}
360 </label>
361 <div className="mt-1 flex items-center gap-2">
362 <Users className="w-4 h-4 text-gray-400" />
363 <p className="text-sm font-medium text-gray-900 dark:text-white">
364 {enquiry.travelers_count}
365 </p>
366 </div>
367 </div>
368 )}
369 {enquiry.travel_date && (
370 <div>
371 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
372 {__("Preferred Travel Date", "yatra")}
373 </label>
374 <div className="mt-1 flex items-center gap-2">
375 <Calendar className="w-4 h-4 text-gray-400" />
376 <p className="text-sm font-medium text-gray-900 dark:text-white">
377 {formatDate(enquiry.travel_date)}
378 </p>
379 </div>
380 </div>
381 )}
382 </div>
383
384 {enquiry.response && (
385 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
386 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2 block">
387 {__("Response", "yatra")}
388 </label>
389 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
390 {enquiry.response}
391 </p>
392 </div>
393 )}
394 </CardContent>
395 </Card>
396
397 {/* Trip Information */}
398 {enquiry.trip_title && (
399 <Card>
400 <CardHeader className="pb-2">
401 <div className="flex items-center justify-between">
402 <CardTitle className="text-base">
403 {__("Related Trip", "yatra")}
404 </CardTitle>
405 {enquiry.trip_id && (
406 <Button
407 variant="ghost"
408 size="sm"
409 onClick={handleViewTrip}
410 className="flex items-center gap-1.5 text-sm"
411 >
412 {__("View Trip", "yatra")}
413 <ExternalLink className="w-3.5 h-3.5" />
414 </Button>
415 )}
416 </div>
417 </CardHeader>
418 <CardContent className="space-y-4">
419 <div>
420 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
421 {__("Trip", "yatra")}
422 </label>
423 <div className="mt-1 flex items-center gap-2">
424 <MapPin className="w-4 h-4 text-gray-400" />
425 <p className="text-sm font-medium text-gray-900 dark:text-white">
426 {enquiry.trip_title}
427 </p>
428 </div>
429 </div>
430 </CardContent>
431 </Card>
432 )}
433 </div>
434
435 {/* Sidebar */}
436 <div className="space-y-3">
437 {/* Customer Information */}
438 <Card>
439 <CardHeader className="pb-2">
440 <CardTitle className="text-base">
441 {__("Customer", "yatra")}
442 </CardTitle>
443 </CardHeader>
444 <CardContent className="space-y-3">
445 <div>
446 <p className="text-sm font-medium text-gray-900 dark:text-white">
447 {enquiry.name}
448 </p>
449 </div>
450 <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
451 <Mail className="w-4 h-4" />
452 <span>{enquiry.email}</span>
453 </div>
454 {enquiry.phone && (
455 <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
456 <Phone className="w-4 h-4" />
457 <span>{enquiry.phone}</span>
458 </div>
459 )}
460 </CardContent>
461 </Card>
462
463 {/* Enquiry Timeline */}
464 <Card>
465 <CardHeader className="pb-2">
466 <CardTitle className="text-base">
467 {__("Timeline", "yatra")}
468 </CardTitle>
469 </CardHeader>
470 <CardContent className="space-y-3">
471 <div>
472 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
473 {__("Submitted", "yatra")}
474 </label>
475 <p className="mt-1 text-sm text-gray-900 dark:text-white">
476 {formatDate(enquiry.created_at)}
477 </p>
478 </div>
479 {enquiry.responded_at && (
480 <div>
481 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
482 {__("Responded", "yatra")}
483 </label>
484 <p className="mt-1 text-sm text-gray-900 dark:text-white">
485 {formatDate(enquiry.responded_at)}
486 </p>
487 </div>
488 )}
489 </CardContent>
490 </Card>
491 </div>
492 </div>
493 </ConditionalRender>
494
495 {/* Respond Dialog via shared Modal */}
496 <Modal
497 isOpen={respondDialogOpen && !!enquiry}
498 onClose={() => {
499 if (!respondMutation.isPending) setRespondDialogOpen(false);
500 }}
501 title={__("Respond to Enquiry", "yatra")}
502 description={
503 enquiry ? (
504 <span>
505 {__("Send a response to", "yatra")}{" "}
506 <strong>{enquiry.name}</strong>
507 </span>
508 ) : null
509 }
510 size="md"
511 footer={
512 <div className="flex gap-2 justify-end">
513 <Button
514 variant="outline"
515 onClick={() => setRespondDialogOpen(false)}
516 disabled={respondMutation.isPending}
517 >
518 {__("Cancel", "yatra")}
519 </Button>
520 <Button
521 onClick={sendResponse}
522 disabled={respondMutation.isPending || !responseMessage.trim()}
523 >
524 {respondMutation.isPending ? (
525 <>
526 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
527 {__("Sending...", "yatra")}
528 </>
529 ) : (
530 <>
531 <Send className="w-4 h-4 mr-2" />
532 {__("Send Response", "yatra")}
533 </>
534 )}
535 </Button>
536 </div>
537 }
538 >
539 {enquiry && (
540 <div className="space-y-4">
541 {/* Customer Info */}
542 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 space-y-2">
543 <div className="flex items-center gap-2 text-sm">
544 <Mail className="w-4 h-4 text-gray-400" />
545 <span className="text-gray-600 dark:text-gray-400">
546 {enquiry.email}
547 </span>
548 </div>
549 {enquiry.trip_title && (
550 <div className="flex items-center gap-2 text-sm">
551 <MapPin className="w-4 h-4 text-gray-400" />
552 <span className="text-gray-600 dark:text-gray-400">
553 {enquiry.trip_title}
554 </span>
555 </div>
556 )}
557 <div className="text-sm text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700 pt-2 mt-2">
558 <strong>{__("Original Message:", "yatra")}</strong>
559 <p className="mt-1 text-gray-600 dark:text-gray-400">
560 {enquiry.message}
561 </p>
562 </div>
563 </div>
564
565 {/* Response Message — label + AI sparkle on the same
566 row so the operator sees the "Generate with AI"
567 option right where they're about to type. Uses the
568 same EnquiryReplyAffordance component the admin
569 EnquiryForm uses, so behavior (Generate / Warmer /
570 Shorter variants, sensitive-data toggle) is identical
571 across the two surfaces. */}
572 <div>
573 <div className="mb-1.5 flex items-center justify-between gap-2">
574 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
575 {__("Your Response", "yatra")}{" "}
576 <span className="text-red-500">*</span>
577 </label>
578 {enquiryId && (
579 <EnquiryReplyAffordance
580 enquiryId={Number(enquiryId)}
581 value={responseMessage}
582 onAccept={(text) => setResponseMessage(text)}
583 />
584 )}
585 </div>
586 <textarea
587 value={responseMessage}
588 onChange={(e) => setResponseMessage(e.target.value)}
589 placeholder={__("Type your response here...", "yatra")}
590 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"
591 rows={5}
592 disabled={respondMutation.isPending}
593 />
594 </div>
595
596 {respondMutation.isError && (
597 <div className="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">
598 {respondMutation.error?.message ||
599 __("Failed to send response. Please try again.", "yatra")}
600 </div>
601 )}
602 </div>
603 )}
604 </Modal>
605 </div>
606 );
607 };
608
609 export default ViewEnquiry;
610