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
← All changes | resources/js/pages/CustomerForm.tsx +112 -10 3.0.3 → 3.0.16 View file →
@@ -21,11 +21,14 @@
21 21 } from "../components/ui/card";
22 22 import { ConditionalRender } from "../components/ui/conditional-render";
23 23 import { Skeleton } from "../components/ui/skeleton";
24 24 import { DatePicker } from "../components/ui/date-picker";
25 +import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
25 26
26 27 interface CustomerFormData {
27 28 first_name: string;
29 + /** Create-mode only: also create a WordPress login account for this customer */
30 + create_account?: boolean;
28 31 last_name: string;
29 32 email: string;
30 33 phone: string;
31 34 secondary_phone: string;
@@ -57,8 +60,9 @@
57 60
58 61 const baseAdminUrl = (window as any).yatraAdmin?.adminUrl || "";
59 62 const [formData, setFormData] = useState<CustomerFormData>({
60 63 first_name: "",
64 + create_account: false,
61 65 last_name: "",
62 66 email: "",
63 67 phone: "",
64 68 secondary_phone: "",
@@ -84,8 +88,13 @@
84 88 loyalty_points: 0,
85 89 });
86 90 const [errors, setErrors] = useState<Record<string, string>>({});
87 91 const [isSubmitting, setIsSubmitting] = useState(false);
92 + // When the entered email already has a login account, we ask before linking.
93 + const [linkConfirm, setLinkConfirm] = useState<{ open: boolean; message: string }>({
94 + open: false,
95 + message: "",
96 + });
88 97
89 98 // Get action and id from URL
90 99 const action = useMemo(() => {
91 100 const params = new URLSearchParams(window.location.search);
@@ -99,8 +108,9 @@
99 108
100 109 const isEditMode = action === "edit" && customerId !== null;
101 110
102 111 // Fetch customer data if editing
112 +
103 113 const { data: customerData, isLoading: isLoadingCustomer } = useQuery<
104 114 { data?: any } | CustomerFormData | null
105 115 >({
106 116 queryKey: ["customer", customerId],
@@ -140,13 +150,22 @@
140 150 loyalty_points:
141 151 typeof data.loyalty_points === "number"
142 152 ? data.loyalty_points
143 153 : parseInt(data.loyalty_points || "0", 10) || 0,
154 + // Carried through so the form can tell whether this customer already has
155 + // a login account (controls the "Create a login account" option below).
156 + user_id: (data as any).user_id ?? null,
144 157 } as CustomerFormData;
145 158 },
146 159 enabled: isEditMode,
147 160 });
148 161
162 + // True only when editing a customer that already has a WordPress login. Used
163 + // to HIDE the "Create a login account" option — an existing account is never
164 + // touched from this form; the option is only for adding one where none exists.
165 + const customerHasAccount =
166 + isEditMode && !!(customerData as any)?.user_id;
167 +
149 168 // Load customer data into form when editing
150 169 useEffect(() => {
151 170 if (customerData && isEditMode) {
152 171 setFormData(customerData as CustomerFormData);
@@ -184,21 +203,41 @@
184 203
185 204 if (isEditMode && customerId) {
186 205 return await apiService.updateCustomer(customerId, payload);
187 206 } else {
188 - // For creation, we need a different approach since CustomerController
189 - // doesn't have a create endpoint yet - customers are created during booking
190 - // For now, return error for create
191 - throw new Error(
192 - "Creating customers directly is not supported. Customers are created when bookings are made.",
193 - );
207 + return await apiService.createCustomer(payload);
194 208 }
195 209 },
196 - onSuccess: () => {
210 + onSuccess: (result: any) => {
211 + // The email already has a login account — don't treat this as saved. Ask
212 + // the operator to confirm the link; nothing was written server-side.
213 + if (result?.needs_link_confirmation) {
214 + setIsSubmitting(false);
215 + setLinkConfirm({
216 + open: true,
217 + message:
218 + result.message ||
219 + __(
220 + "This email already has a login account. Link this customer to it?",
221 + "yatra",
222 + ),
223 + });
224 + return;
225 + }
197 226 queryClient.invalidateQueries({ queryKey: ["customers"] });
198 227 queryClient.invalidateQueries({ queryKey: ["customer", customerId] });
199 228 setIsSubmitting(false);
200 - showToast(__("Customer updated successfully", "yatra"), "success");
229 + // The server returns a message tailored to what happened (created, created
230 + // with a login account, or linked to an existing account); prefer it.
231 + const message =
232 + result?.message ||
233 + (isEditMode
234 + ? __("Customer updated successfully", "yatra")
235 + : __("Customer created successfully", "yatra"));
236 + showToast(message, "success");
237 + if (!isEditMode) {
238 + window.location.href = `${baseAdminUrl}?page=yatra&subpage=customers`;
239 + }
201 240 },
202 241 onError: (error: any) => {
203 242 const errorMessage =
204 243 error?.message ||
@@ -310,9 +349,9 @@
310 349 </Button>
311 350 }
312 351 />
313 352
314 - <ConditionalRender capability="yatra_edit_bookings">
353 + <ConditionalRender capability="yatra_edit_customers">
315 354 <form onSubmit={handleSubmit}>
316 355 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
317 356 {/* Main Form Fields */}
318 357 <div className="lg:col-span-2 space-y-3">
@@ -390,9 +429,8 @@
390 429 }
391 430 placeholder={__("[email protected]", "yatra")}
392 431 className={errors.email ? "border-red-500" : ""}
393 432 required
394 - disabled={isEditMode} // Can't change email
395 433 />
396 434 {errors.email && (
397 435 <p className="mt-1 text-sm text-red-500">
398 436 {errors.email}
@@ -397,8 +435,53 @@
397 435 <p className="mt-1 text-sm text-red-500">
398 436 {errors.email}
399 437 </p>
400 438 )}
439 + {isEditMode && !errors.email && (
440 + <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
441 + {__(
442 + "If this customer can sign in, a confirmation link is sent to the new address and their email changes only once it is confirmed there. Customers without an account are updated straight away.",
443 + "yatra",
444 + )}
445 + </p>
446 + )}
447 + {/* Editing a customer that already signs in: no checkbox —
448 + their account is managed elsewhere, never from here. */}
449 + {customerHasAccount && (
450 + <div className="mt-3 flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-400">
451 + {__("This customer has a login account.", "yatra")}
452 + </div>
453 + )}
454 + {/* Shown on Add, and on Edit when the customer has no login
455 + yet — ticking it creates one on save (never removes one). */}
456 + {!customerHasAccount && (
457 + <div className="mt-3 flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
458 + <input
459 + type="checkbox"
460 + id="create_account"
461 + checked={!!formData.create_account}
462 + onChange={(e) =>
463 + setFormData({
464 + ...formData,
465 + create_account: e.target.checked,
466 + })
467 + }
468 + className="w-4 h-4 mt-0.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
469 + />
470 + <label
471 + htmlFor="create_account"
472 + className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer"
473 + >
474 + {__("Create a login account for this customer", "yatra")}
475 + <span className="block text-xs text-gray-500 dark:text-gray-400 mt-0.5">
476 + {__(
477 + "They'll receive an email to set their own password and can then sign in to see their bookings. If this email already has an account, it will be linked instead.",
478 + "yatra",
479 + )}
480 + </span>
481 + </label>
482 + </div>
483 + )}
401 484 </div>
402 485
403 486 {/* Phone Numbers */}
404 487 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
@@ -947,8 +1030,27 @@
947 1030 </div>
948 1031 </div>
949 1032 </form>
950 1033 </ConditionalRender>
1034 +
1035 + {/* Shown when the entered email already has a login account. Confirming
1036 + re-submits with confirm_link_existing so the backend links to it;
1037 + cancelling leaves everything unsaved (nothing was written). */}
1038 + <ConfirmationDialog
1039 + isOpen={linkConfirm.open}
1040 + onClose={() => setLinkConfirm({ open: false, message: "" })}
1041 + onConfirm={() => {
1042 + setLinkConfirm({ open: false, message: "" });
1043 + setIsSubmitting(true);
1044 + saveMutation.mutate({
1045 + ...formData,
1046 + confirm_link_existing: true,
1047 + } as any);
1048 + }}
1049 + title={__("Link to existing account?", "yatra")}
1050 + message={linkConfirm.message}
1051 + confirmText={__("Link account", "yatra")}
1052 + />
951 1053 </div>
952 1054 );
953 1055 };
954 1056