PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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 / PaymentForm.tsx

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

592 lines 22.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Payment Form Page
3 * Add/Edit Payment form
4 */
5
6 import React, { useState, useEffect, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2, Info } 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 { Select } from "../components/ui/select";
15 import { DatePicker } from "../components/ui/date-picker";
16 import { PageHeader } from "../components/common/PageHeader";
17 import { BookingPicker } from "../components/common/BookingPicker";
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 { HelpText } from "../components/ui/help-text";
26 import { Alert } from "../components/ui/alert";
27
28 interface PaymentFormData {
29 booking_id: string;
30 amount: string;
31 payment_method: string;
32 payment_status: "pending" | "completed" | "failed" | "refunded" | "partial";
33 payment_date: string;
34 transaction_id: string;
35 notes: string;
36 }
37
38 const PaymentForm: React.FC = () => {
39 const queryClient = useQueryClient();
40 const { can } = usePermissions();
41 const [formData, setFormData] = useState<PaymentFormData>({
42 booking_id: "",
43 amount: "",
44 payment_method: "Credit Card",
45 payment_status: "pending",
46 payment_date: new Date().toISOString().split("T")[0],
47 transaction_id: "",
48 notes: "",
49 });
50 const [errors, setErrors] = useState<Record<string, string>>({});
51 const [isSubmitting, setIsSubmitting] = useState(false);
52
53 // Get action and id from URL
54 const action = useMemo(() => {
55 const params = new URLSearchParams(window.location.search);
56 return params.get("action") || "create";
57 }, []);
58
59 const paymentId = useMemo(() => {
60 const params = new URLSearchParams(window.location.search);
61 return params.get("id") ? parseInt(params.get("id") || "0") : null;
62 }, []);
63
64 const isEditMode = action === "edit" && paymentId !== null;
65
66 // Fetch payment data if editing
67 const { data: paymentData, isLoading: isLoadingPayment } = useQuery({
68 queryKey: ["payment", paymentId],
69 queryFn: async () => {
70 if (!paymentId) return null;
71
72 const result = await apiService.getPayment(paymentId);
73
74 if (!result) {
75 throw new Error("Failed to fetch payment");
76 }
77
78 if (!result.success) {
79 throw new Error(result.message || "Payment not found");
80 }
81
82 const data = result.data;
83 const processedAt = data.processed_at;
84 const processedDate =
85 typeof processedAt === "string" && processedAt.includes(" ")
86 ? processedAt.split(" ")[0]
87 : new Date().toISOString().split("T")[0];
88 return {
89 id: data.id,
90 booking_id: data.booking_id,
91 amount: data.amount,
92 payment_method: data.gateway,
93 payment_status: data.status,
94 payment_date: processedDate,
95 transaction_id: data.transaction_id ?? "",
96 notes: data.notes ?? "",
97 };
98 },
99 enabled: isEditMode && can("yatra_view_bookings"),
100 });
101
102 // Load payment data into form when editing
103 useEffect(() => {
104 if (paymentData && isEditMode) {
105 setFormData({
106 booking_id: String(paymentData.booking_id ?? ""),
107 amount: String(paymentData.amount ?? ""),
108 payment_method: String(paymentData.payment_method || "Credit Card"),
109 payment_status: (String(paymentData.payment_status || "pending") ||
110 "pending") as PaymentFormData["payment_status"],
111 payment_date: String(
112 paymentData.payment_date || new Date().toISOString().split("T")[0],
113 ),
114 transaction_id: String(paymentData.transaction_id ?? ""),
115 notes: String(paymentData.notes ?? ""),
116 });
117 }
118 }, [paymentData, isEditMode]);
119
120 const handleFieldChange = (field: keyof PaymentFormData, value: string) => {
121 setFormData((prev) => ({ ...prev, [field]: value }));
122 if (errors[field]) {
123 setErrors((prev) => ({ ...prev, [field]: "" }));
124 }
125 };
126
127 const validateForm = (): boolean => {
128 const newErrors: Record<string, string> = {};
129
130 if (!formData.booking_id.trim()) {
131 newErrors.booking_id = __("Booking is required", "yatra");
132 } else {
133 const bookingId = parseInt(formData.booking_id);
134 if (isNaN(bookingId) || bookingId <= 0) {
135 newErrors.booking_id = __("Valid booking ID is required", "yatra");
136 }
137 }
138
139 if (!formData.amount.trim()) {
140 newErrors.amount = __("Payment amount is required", "yatra");
141 } else {
142 const amount = parseFloat(formData.amount);
143 if (isNaN(amount) || amount <= 0) {
144 newErrors.amount = __(
145 "Payment amount must be a positive number",
146 "yatra",
147 );
148 }
149 }
150
151 if (!formData.payment_method.trim()) {
152 newErrors.payment_method = __("Payment method is required", "yatra");
153 }
154
155 if (!formData.payment_date.trim()) {
156 newErrors.payment_date = __("Payment date is required", "yatra");
157 }
158
159 setErrors(newErrors);
160 return Object.keys(newErrors).length === 0;
161 };
162
163 // Create/Update mutation
164 const saveMutation = useMutation({
165 mutationFn: async (data: PaymentFormData) => {
166 const payload = {
167 booking_id: parseInt(data.booking_id),
168 amount: parseFloat(data.amount),
169 gateway: data.payment_method,
170 status: data.payment_status,
171 processed_at: data.payment_date,
172 transaction_id: data.transaction_id.trim() || null,
173 notes: data.notes.trim() || null,
174 };
175
176 return isEditMode && paymentId
177 ? await apiService.updatePayment(paymentId, payload)
178 : await apiService.createPayment(payload);
179 },
180 onSuccess: () => {
181 queryClient.invalidateQueries({ queryKey: ["payments"] });
182 queryClient.invalidateQueries({ queryKey: ["bookings"] });
183 // Redirect to payments list
184 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=payments`;
185 },
186 onError: (error: any) => {
187 console.error("Error saving payment:", error);
188 setErrors({
189 submit: error.message || __("Failed to save payment", "yatra"),
190 });
191 },
192 });
193
194 const handleSubmit = async (e: React.FormEvent) => {
195 e.preventDefault();
196
197 if (!validateForm()) {
198 return;
199 }
200
201 setIsSubmitting(true);
202 try {
203 await saveMutation.mutateAsync(formData);
204 } catch (error) {
205 // Error handled in mutation
206 } finally {
207 setIsSubmitting(false);
208 }
209 };
210
211 const handleBack = () => {
212 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=payments`;
213 };
214
215 if (isLoadingPayment) {
216 return (
217 <div className="flex items-center justify-center p-8">
218 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
219 </div>
220 );
221 }
222
223 return (
224 <div className="space-y-3">
225 <PageHeader
226 title={
227 isEditMode
228 ? __("Edit Payment", "yatra")
229 : __("Add New Payment", "yatra")
230 }
231 description={
232 isEditMode
233 ? __("Update payment details", "yatra")
234 : __("Record a new payment for a booking", "yatra")
235 }
236 actions={
237 <Button
238 variant="outline"
239 onClick={handleBack}
240 className="flex items-center gap-2"
241 >
242 <ArrowLeft className="w-4 h-4" />
243 {__("Back", "yatra")}
244 </Button>
245 }
246 />
247
248 <ConditionalRender capability="yatra_edit_bookings">
249 <form onSubmit={handleSubmit}>
250 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
251 {/* Main Form Fields */}
252 <div className="lg:col-span-2 space-y-3">
253 {/* Payment Information */}
254 <Card>
255 <CardHeader className="pb-2">
256 <CardTitle className="text-base">
257 {__("Payment Information", "yatra")}
258 </CardTitle>
259 </CardHeader>
260 <CardContent className="space-y-3">
261 {/* Booking */}
262 <div>
263 <label
264 htmlFor="booking_id"
265 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
266 >
267 {__("Booking", "yatra")}{" "}
268 <span className="text-red-500">*</span>
269 </label>
270 <HelpText
271 text={__(
272 "Search by booking code (e.g. YTR-123), customer name, or email. Results load from your existing bookings as you type.",
273 "yatra",
274 )}
275 className="mb-2"
276 />
277 <BookingPicker
278 value={formData.booking_id}
279 onChange={(id) => handleFieldChange("booking_id", id)}
280 error={Boolean(errors.booking_id)}
281 disabled={isEditMode}
282 placeholder={__("Select a booking…", "yatra")}
283 searchPlaceholder={__(
284 "Search by booking code, name, or email…",
285 "yatra",
286 )}
287 />
288 {errors.booking_id && (
289 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
290 <Info className="w-4 h-4" />
291 {errors.booking_id}
292 </p>
293 )}
294 {isEditMode && (
295 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
296 {__(
297 "The booking linked to a payment cannot be changed after creation.",
298 "yatra",
299 )}
300 </p>
301 )}
302 </div>
303
304 {/* Amount and Payment Method */}
305 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
306 {/* Amount */}
307 <div>
308 <label
309 htmlFor="amount"
310 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
311 >
312 {__("Payment Amount", "yatra")}{" "}
313 <span className="text-red-500">*</span>
314 </label>
315 <HelpText
316 text={__("Enter the payment amount received.", "yatra")}
317 className="mb-2"
318 />
319 <div className="relative">
320 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
321 $
322 </span>
323 <Input
324 id="amount"
325 type="number"
326 min="0"
327 step="0.01"
328 value={formData.amount}
329 onChange={(e) =>
330 handleFieldChange("amount", e.target.value)
331 }
332 placeholder={__("e.g., 2500.00", "yatra")}
333 className={`pl-7 ${errors.amount ? "border-red-500" : ""}`}
334 required
335 />
336 </div>
337 {errors.amount && (
338 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
339 <Info className="w-4 h-4" />
340 {errors.amount}
341 </p>
342 )}
343 </div>
344
345 {/* Payment Method */}
346 <div>
347 <label
348 htmlFor="payment_method"
349 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
350 >
351 {__("Payment Method", "yatra")}{" "}
352 <span className="text-red-500">*</span>
353 </label>
354 <HelpText
355 text={__(
356 "Select the payment method used for this transaction.",
357 "yatra",
358 )}
359 className="mb-2"
360 />
361 <Select
362 id="payment_method"
363 value={formData.payment_method}
364 onChange={(e) =>
365 handleFieldChange("payment_method", e.target.value)
366 }
367 className={
368 errors.payment_method ? "border-red-500" : ""
369 }
370 required
371 >
372 <option value="Credit Card">
373 {__("Credit Card", "yatra")}
374 </option>
375 <option value="Debit Card">
376 {__("Debit Card", "yatra")}
377 </option>
378 <option value="PayPal">{__("PayPal", "yatra")}</option>
379 <option value="Bank Transfer">
380 {__("Bank Transfer", "yatra")}
381 </option>
382 <option value="Cash">{__("Cash", "yatra")}</option>
383 <option value="Check">{__("Check", "yatra")}</option>
384 <option value="Other">{__("Other", "yatra")}</option>
385 </Select>
386 {errors.payment_method && (
387 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
388 <Info className="w-4 h-4" />
389 {errors.payment_method}
390 </p>
391 )}
392 </div>
393 </div>
394
395 {/* Payment Date and Transaction ID */}
396 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
397 {/* Payment Date */}
398 <div>
399 <label
400 htmlFor="payment_date"
401 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
402 >
403 {__("Payment Date", "yatra")}{" "}
404 <span className="text-red-500">*</span>
405 </label>
406 <HelpText
407 text={__(
408 "Date when the payment was received.",
409 "yatra",
410 )}
411 className="mb-2"
412 />
413 <DatePicker
414 value={formData.payment_date}
415 onChange={(value) =>
416 handleFieldChange("payment_date", value)
417 }
418 placeholder={__("Select payment date", "yatra")}
419 maxDate={new Date()}
420 error={Boolean(errors.payment_date)}
421 />
422 {errors.payment_date && (
423 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
424 <Info className="w-4 h-4" />
425 {errors.payment_date}
426 </p>
427 )}
428 </div>
429
430 {/* Transaction ID */}
431 <div>
432 <label
433 htmlFor="transaction_id"
434 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
435 >
436 {__("Transaction ID", "yatra")}
437 </label>
438 <HelpText
439 text={__(
440 "Optional transaction ID or reference number from the payment gateway.",
441 "yatra",
442 )}
443 className="mb-2"
444 />
445 <Input
446 id="transaction_id"
447 type="text"
448 value={formData.transaction_id}
449 onChange={(e) =>
450 handleFieldChange("transaction_id", e.target.value)
451 }
452 placeholder={__("e.g., TXN123456789", "yatra")}
453 className={
454 errors.transaction_id ? "border-red-500" : ""
455 }
456 />
457 {errors.transaction_id && (
458 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
459 <Info className="w-4 h-4" />
460 {errors.transaction_id}
461 </p>
462 )}
463 </div>
464 </div>
465
466 {/* Notes */}
467 <div>
468 <label
469 htmlFor="notes"
470 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
471 >
472 {__("Notes", "yatra")}
473 </label>
474 <HelpText
475 text={__(
476 "Optional notes about this payment for internal reference.",
477 "yatra",
478 )}
479 className="mb-2"
480 />
481 <textarea
482 id="notes"
483 value={formData.notes}
484 onChange={(e) =>
485 handleFieldChange("notes", e.target.value)
486 }
487 placeholder={__(
488 "e.g., Full payment received, Partial payment - balance pending",
489 "yatra",
490 )}
491 rows={3}
492 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
493 />
494 </div>
495 </CardContent>
496 </Card>
497 </div>
498
499 {/* Sidebar */}
500 <div className="space-y-3">
501 {/* Payment Status */}
502 <Card>
503 <CardHeader className="pb-2">
504 <CardTitle className="text-base">
505 {__("Payment Status", "yatra")}
506 </CardTitle>
507 </CardHeader>
508 <CardContent className="space-y-3">
509 <Select
510 value={formData.payment_status}
511 onChange={(e) =>
512 handleFieldChange(
513 "payment_status",
514 e.target.value as PaymentFormData["payment_status"],
515 )
516 }
517 >
518 <option value="pending">{__("Pending", "yatra")}</option>
519 <option value="completed">
520 {__("Completed", "yatra")}
521 </option>
522 <option value="partial">{__("Partial", "yatra")}</option>
523 <option value="failed">{__("Failed", "yatra")}</option>
524 <option value="refunded">{__("Refunded", "yatra")}</option>
525 </Select>
526 <p className="text-xs text-gray-500 dark:text-gray-400">
527 {formData.payment_status === "completed" &&
528 __("Payment has been successfully processed.", "yatra")}
529 {formData.payment_status === "pending" &&
530 __("Payment is pending confirmation.", "yatra")}
531 {formData.payment_status === "partial" &&
532 __(
533 "Partial payment recorded. Balance may be pending.",
534 "yatra",
535 )}
536 {formData.payment_status === "failed" &&
537 __(
538 "Payment processing failed. Please check the details.",
539 "yatra",
540 )}
541 {formData.payment_status === "refunded" &&
542 __("Payment has been refunded to the customer.", "yatra")}
543 </p>
544 </CardContent>
545 </Card>
546 </div>
547 </div>
548
549 {/* Submit Button */}
550 {errors.submit && (
551 <Alert variant="error" className="mt-3">
552 {errors.submit}
553 </Alert>
554 )}
555
556 <div className="flex items-center justify-end gap-3 mt-6">
557 <Button
558 type="button"
559 variant="outline"
560 onClick={handleBack}
561 disabled={isSubmitting}
562 >
563 {__("Cancel", "yatra")}
564 </Button>
565 <Button
566 type="submit"
567 disabled={isSubmitting}
568 className="flex items-center gap-2"
569 >
570 {isSubmitting ? (
571 <>
572 <Loader2 className="w-4 h-4 animate-spin" />
573 {__("Saving...", "yatra")}
574 </>
575 ) : (
576 <>
577 <Save className="w-4 h-4" />
578 {isEditMode
579 ? __("Update Payment", "yatra")
580 : __("Record Payment", "yatra")}
581 </>
582 )}
583 </Button>
584 </div>
585 </form>
586 </ConditionalRender>
587 </div>
588 );
589 };
590
591 export default PaymentForm;
592