| @@ -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 | |