PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / PaymentForm.tsx

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

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