PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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.2.7, at resources/js/pages/ViewEnquiry.tsx

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