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 +103 -8 3.0.13 → 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 ||
@@ -404,8 +443,45 @@
404 443 "yatra",
405 444 )}
406 445 </p>
407 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 + )}
408 484 </div>
409 485
410 486 {/* Phone Numbers */}
411 487 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
@@ -954,8 +1030,27 @@
954 1030 </div>
955 1031 </div>
956 1032 </form>
957 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 + />
958 1053 </div>
959 1054 );
960 1055 };
961 1056