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

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