PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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.16, at resources/js/pages/PaymentForm.tsx

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