PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
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 / EnquiryForm.tsx

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

505 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Enquiry Form Page
3 * Create or edit enquiry in a clean, minimal SaaS-style design
4 */
5
6 import React, { useMemo, useEffect, useState } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2 } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { apiService } from "../lib/api-client";
11 import { usePermissions } from "../hooks/usePermissions";
12 import { Button } from "../components/ui/button";
13 import { Input } from "../components/ui/input";
14 import { DatePicker } from "../components/ui/date-picker";
15 import { Select } from "../components/ui/select";
16 import { useToast } from "../components/ui/toast";
17 import { PageHeader } from "../components/common/PageHeader";
18 import {
19 Card,
20 CardContent,
21 CardHeader,
22 CardTitle,
23 } from "../components/ui/card";
24 import { ConditionalRender } from "../components/ui/conditional-render";
25 import { useNavigate } from "../hooks/useNavigate";
26 import { Skeleton } from "../components/ui/skeleton";
27
28 interface Enquiry {
29 id: number;
30 name: string;
31 email: string;
32 phone?: string;
33 trip_id?: number;
34 trip_title?: string;
35 message: string;
36 travelers_count?: number;
37 travel_date?: string;
38 status:
39 | "new"
40 | "pending"
41 | "responded"
42 | "closed"
43 | "converted"
44 | "read"
45 | "archived"
46 | "spam"
47 | "trash";
48 response_notes?: string;
49 created_at: string;
50 responded_at?: string;
51 }
52
53 interface Trip {
54 id: number;
55 title: string;
56 }
57
58 const EnquiryForm: React.FC = () => {
59 const { can } = usePermissions();
60 const { navigate } = useNavigate();
61 const queryClient = useQueryClient();
62 const { showToast } = useToast();
63
64 // Form state
65 const [formData, setFormData] = useState<Partial<Enquiry>>({
66 name: "",
67 email: "",
68 phone: "",
69 trip_id: undefined,
70 message: "",
71 travelers_count: 1,
72 travel_date: "",
73 status: "new",
74 response_notes: "",
75 });
76
77 // Get enquiry id from URL (null for create, number for edit)
78 const enquiryId = useMemo(() => {
79 const params = new URLSearchParams(window.location.search);
80 const id = params.get("id");
81 return id ? parseInt(id) : null;
82 }, []);
83
84 const isEdit = !!enquiryId;
85
86 // Fetch enquiry data for editing
87 const { data: enquiry, isLoading: isLoadingEnquiry } =
88 useQuery<Enquiry | null>({
89 queryKey: ["enquiry", enquiryId],
90 queryFn: async () => {
91 if (!enquiryId) return null;
92 const response = await apiService.getEnquiry(enquiryId!);
93 // Some endpoints return { success, data }, others return the object directly
94 return (response as any)?.data ?? response;
95 },
96 enabled: !!enquiryId && can("yatra_edit_bookings"),
97 });
98
99 // Fetch trips for dropdown
100 const { data: tripsData } = useQuery({
101 queryKey: ["trips-select"],
102 queryFn: async () => {
103 return await apiService.getTrips({ per_page: 100 });
104 },
105 enabled: can("yatra_edit_bookings"),
106 });
107
108 // Normalize trips response to array
109 const trips: Trip[] = Array.isArray((tripsData as any)?.data)
110 ? (tripsData as any).data
111 : Array.isArray(tripsData)
112 ? (tripsData as Trip[])
113 : [];
114
115 // Update form when enquiry data loads
116 useEffect(() => {
117 if (enquiry) {
118 setFormData({
119 name: enquiry.name || "",
120 email: enquiry.email || "",
121 phone: enquiry.phone || "",
122 trip_id: enquiry.trip_id,
123 message: enquiry.message || "",
124 travelers_count: enquiry.travelers_count || 1,
125 travel_date: enquiry.travel_date || "",
126 status: enquiry.status || "new",
127 response_notes: enquiry.response_notes || "",
128 });
129 }
130 }, [enquiry]);
131
132 // Save mutation
133 const saveMutation = useMutation({
134 mutationFn: async (data: Partial<Enquiry>) => {
135 return isEdit
136 ? await apiService.updateEnquiry(enquiryId!, data)
137 : await apiService.createEnquiry(data);
138 },
139 onSuccess: () => {
140 queryClient.invalidateQueries({ queryKey: ["enquiries"] });
141 queryClient.invalidateQueries({ queryKey: ["enquiry", enquiryId] });
142 showToast(__("Enquiry saved successfully.", "yatra"), "success");
143 navigate({ subpage: "enquiries" });
144 },
145 });
146
147 const handleBack = () => {
148 navigate({ subpage: "enquiries" });
149 };
150
151 const handleSubmit = (e: React.FormEvent) => {
152 e.preventDefault();
153 saveMutation.mutate(formData);
154 };
155
156 const handleChange = (field: keyof Enquiry, value: any) => {
157 setFormData((prev) => ({ ...prev, [field]: value }));
158 };
159
160 if (isEdit && isLoadingEnquiry) {
161 return (
162 <div className="space-y-3">
163 <PageHeader
164 title={__("Edit Enquiry", "yatra")}
165 actions={
166 <div className="flex items-center gap-2">
167 <Skeleton className="h-10 w-24" />
168 <Skeleton className="h-10 w-32" />
169 </div>
170 }
171 />
172 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
173 <div className="lg:col-span-2 space-y-3">
174 <Card>
175 <CardHeader className="pb-2">
176 <Skeleton className="h-5 w-40" />
177 </CardHeader>
178 <CardContent className="space-y-4">
179 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
180 <div>
181 <Skeleton className="h-3 w-16 mb-2" />
182 <Skeleton className="h-11 w-full" />
183 </div>
184 <div>
185 <Skeleton className="h-3 w-16 mb-2" />
186 <Skeleton className="h-11 w-full" />
187 </div>
188 </div>
189 <div>
190 <Skeleton className="h-3 w-20 mb-2" />
191 <Skeleton className="h-32 w-full" />
192 </div>
193 </CardContent>
194 </Card>
195 </div>
196 <div className="space-y-3">
197 <Card>
198 <CardHeader className="pb-2">
199 <Skeleton className="h-5 w-24" />
200 </CardHeader>
201 <CardContent className="space-y-4">
202 <Skeleton className="h-11 w-full" />
203 <Skeleton className="h-11 w-full" />
204 </CardContent>
205 </Card>
206 </div>
207 </div>
208 </div>
209 );
210 }
211
212 return (
213 <div className="space-y-3">
214 <PageHeader
215 title={
216 isEdit ? __("Edit Enquiry", "yatra") : __("Create Enquiry", "yatra")
217 }
218 description={
219 isEdit
220 ? __("Update enquiry details", "yatra")
221 : __("Add a new enquiry", "yatra")
222 }
223 actions={
224 <div className="flex items-center gap-2">
225 <Button
226 variant="outline"
227 onClick={handleBack}
228 className="flex items-center gap-2"
229 >
230 <ArrowLeft className="w-4 h-4" />
231 {__("Back", "yatra")}
232 </Button>
233 <Button
234 onClick={handleSubmit}
235 disabled={saveMutation.isPending}
236 className="flex items-center gap-2"
237 >
238 {saveMutation.isPending ? (
239 <Loader2 className="w-4 h-4 animate-spin" />
240 ) : (
241 <Save className="w-4 h-4" />
242 )}
243 {saveMutation.isPending
244 ? __("Saving...", "yatra")
245 : __("Save", "yatra")}
246 </Button>
247 </div>
248 }
249 />
250
251 <ConditionalRender capability="yatra_edit_bookings">
252 <form onSubmit={handleSubmit}>
253 {saveMutation.isError && (
254 <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 {saveMutation.error?.message ||
256 __("Failed to save enquiry. Please try again.", "yatra")}
257 </div>
258 )}
259
260 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
261 {/* Main Content */}
262 <div className="lg:col-span-2 space-y-3">
263 {/* Customer Information */}
264 <Card>
265 <CardHeader className="pb-2">
266 <CardTitle className="text-base">
267 {__("Customer Information", "yatra")}
268 </CardTitle>
269 </CardHeader>
270 <CardContent className="space-y-4">
271 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
272 <div>
273 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
274 {__("Name", "yatra")}{" "}
275 <span className="text-red-500">*</span>
276 </label>
277 <Input
278 value={formData.name || ""}
279 onChange={(e) => handleChange("name", e.target.value)}
280 placeholder={__("Customer name", "yatra")}
281 required
282 />
283 </div>
284 <div>
285 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
286 {__("Email", "yatra")}{" "}
287 <span className="text-red-500">*</span>
288 </label>
289 <Input
290 type="email"
291 value={formData.email || ""}
292 onChange={(e) => handleChange("email", e.target.value)}
293 placeholder={__("[email protected]", "yatra")}
294 required
295 />
296 </div>
297 </div>
298 <div>
299 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
300 {__("Phone", "yatra")}
301 </label>
302 <Input
303 type="tel"
304 value={formData.phone || ""}
305 onChange={(e) => handleChange("phone", e.target.value)}
306 placeholder={__("Phone number", "yatra")}
307 />
308 </div>
309 </CardContent>
310 </Card>
311
312 {/* Enquiry Details */}
313 <Card>
314 <CardHeader className="pb-2">
315 <CardTitle className="text-base">
316 {__("Enquiry Details", "yatra")}
317 </CardTitle>
318 </CardHeader>
319 <CardContent className="space-y-4">
320 <div>
321 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
322 {__("Message", "yatra")}{" "}
323 <span className="text-red-500">*</span>
324 </label>
325 <textarea
326 value={formData.message || ""}
327 onChange={(e) => handleChange("message", e.target.value)}
328 placeholder={__("Enquiry message...", "yatra")}
329 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"
330 rows={5}
331 required
332 />
333 </div>
334 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
335 <div>
336 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
337 {__("Number of Travelers", "yatra")}
338 </label>
339 <Input
340 type="number"
341 min="1"
342 value={formData.travelers_count || ""}
343 onChange={(e) =>
344 handleChange(
345 "travelers_count",
346 parseInt(e.target.value, 10) || 1,
347 )
348 }
349 placeholder="1"
350 />
351 </div>
352 <div>
353 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
354 {__("Preferred Travel Date", "yatra")}
355 </label>
356 <DatePicker
357 value={formData.travel_date || ""}
358 onChange={(val) => handleChange("travel_date", val)}
359 placeholder={__("Select date", "yatra")}
360 />
361 </div>
362 </div>
363 </CardContent>
364 </Card>
365
366 {/* Response Notes */}
367 {isEdit && (
368 <Card>
369 <CardHeader className="pb-2">
370 <CardTitle className="text-base">
371 {__("Response Notes", "yatra")}
372 </CardTitle>
373 </CardHeader>
374 <CardContent>
375 <textarea
376 value={formData.response_notes || ""}
377 onChange={(e) =>
378 handleChange("response_notes", e.target.value)
379 }
380 placeholder={__(
381 "Internal notes about responses to this enquiry...",
382 "yatra",
383 )}
384 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}
386 />
387 </CardContent>
388 </Card>
389 )}
390 </div>
391
392 {/* Sidebar */}
393 <div className="space-y-3">
394 {/* Status */}
395 <Card>
396 <CardHeader className="pb-2">
397 <CardTitle className="text-base">
398 {__("Status", "yatra")}
399 </CardTitle>
400 </CardHeader>
401 <CardContent>
402 <Select
403 value={formData.status || "new"}
404 onChange={(e) => handleChange("status", e.target.value)}
405 >
406 <option value="new">{__("New", "yatra")}</option>
407 <option value="pending">{__("Pending", "yatra")}</option>
408 <option value="responded">
409 {__("Responded", "yatra")}
410 </option>
411 <option value="converted">
412 {__("Converted", "yatra")}
413 </option>
414 <option value="closed">{__("Closed", "yatra")}</option>
415 <option value="spam">{__("Spam", "yatra")}</option>
416 <option value="trash">{__("Trash", "yatra")}</option>
417 </Select>
418 </CardContent>
419 </Card>
420
421 {/* Related Trip */}
422 <Card>
423 <CardHeader className="pb-2">
424 <CardTitle className="text-base">
425 {__("Related Trip", "yatra")}
426 </CardTitle>
427 </CardHeader>
428 <CardContent>
429 <Select
430 value={formData.trip_id?.toString() || ""}
431 onChange={(e) =>
432 handleChange(
433 "trip_id",
434 e.target.value ? parseInt(e.target.value) : undefined,
435 )
436 }
437 >
438 <option value="">{__("No trip selected", "yatra")}</option>
439 {trips.map((trip) => (
440 <option key={trip.id} value={trip.id.toString()}>
441 {trip.title}
442 </option>
443 ))}
444 </Select>
445 </CardContent>
446 </Card>
447
448 {/* Created Info */}
449 {isEdit && enquiry && (
450 <Card>
451 <CardHeader className="pb-2">
452 <CardTitle className="text-base">
453 {__("Information", "yatra")}
454 </CardTitle>
455 </CardHeader>
456 <CardContent className="space-y-3 text-sm">
457 <div>
458 <span className="text-gray-500 dark:text-gray-400">
459 {__("Created:", "yatra")}
460 </span>
461 <p className="text-gray-900 dark:text-white mt-0.5">
462 {new Date(enquiry.created_at).toLocaleDateString(
463 "en-US",
464 {
465 year: "numeric",
466 month: "long",
467 day: "numeric",
468 hour: "2-digit",
469 minute: "2-digit",
470 },
471 )}
472 </p>
473 </div>
474 {enquiry.responded_at && (
475 <div>
476 <span className="text-gray-500 dark:text-gray-400">
477 {__("Responded:", "yatra")}
478 </span>
479 <p className="text-gray-900 dark:text-white mt-0.5">
480 {new Date(enquiry.responded_at).toLocaleDateString(
481 "en-US",
482 {
483 year: "numeric",
484 month: "long",
485 day: "numeric",
486 hour: "2-digit",
487 minute: "2-digit",
488 },
489 )}
490 </p>
491 </div>
492 )}
493 </CardContent>
494 </Card>
495 )}
496 </div>
497 </div>
498 </form>
499 </ConditionalRender>
500 </div>
501 );
502 };
503
504 export default EnquiryForm;
505