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
yatra / resources / js / pages / CustomerForm.tsx

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

1,058 lines 41.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Customer Form Page
3 * Add/Edit Customer form with dynamic data from API
4 */
5
6 import React, { useState, useEffect, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2 } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { apiService } from "../lib/api-client";
11 import { useToast } from "../components/ui/toast";
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 { Skeleton } from "../components/ui/skeleton";
24 import { DatePicker } from "../components/ui/date-picker";
25 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
26
27 interface CustomerFormData {
28 first_name: string;
29 /** Create-mode only: also create a WordPress login account for this customer */
30 create_account?: boolean;
31 last_name: string;
32 email: string;
33 phone: string;
34 secondary_phone: string;
35 country: string;
36 city: string;
37 state: string;
38 address: string;
39 postal_code: string;
40 nationality: string;
41 date_of_birth: string;
42 gender: string;
43 emergency_name: string;
44 emergency_phone: string;
45 emergency_relationship: string;
46 dietary_requirements: string;
47 medical_conditions: string;
48 special_needs: string;
49 newsletter_optin: boolean;
50 marketing_optin: boolean;
51 status: string;
52 notes: string;
53 loyalty_tier: string;
54 loyalty_points: number;
55 }
56
57 const CustomerForm: React.FC = () => {
58 const queryClient = useQueryClient();
59 const { showToast } = useToast();
60
61 const baseAdminUrl = (window as any).yatraAdmin?.adminUrl || "";
62 const [formData, setFormData] = useState<CustomerFormData>({
63 first_name: "",
64 create_account: false,
65 last_name: "",
66 email: "",
67 phone: "",
68 secondary_phone: "",
69 country: "",
70 city: "",
71 state: "",
72 address: "",
73 postal_code: "",
74 nationality: "",
75 date_of_birth: "",
76 gender: "",
77 emergency_name: "",
78 emergency_phone: "",
79 emergency_relationship: "",
80 dietary_requirements: "",
81 medical_conditions: "",
82 special_needs: "",
83 newsletter_optin: false,
84 marketing_optin: false,
85 status: "active",
86 notes: "",
87 loyalty_tier: "bronze",
88 loyalty_points: 0,
89 });
90 const [errors, setErrors] = useState<Record<string, string>>({});
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 });
97
98 // Get action and id from URL
99 const action = useMemo(() => {
100 const params = new URLSearchParams(window.location.search);
101 return params.get("action") || "create";
102 }, []);
103
104 const customerId = useMemo(() => {
105 const params = new URLSearchParams(window.location.search);
106 return params.get("id") ? parseInt(params.get("id") || "0") : null;
107 }, []);
108
109 const isEditMode = action === "edit" && customerId !== null;
110
111 // Fetch customer data if editing
112
113 const { data: customerData, isLoading: isLoadingCustomer } = useQuery<
114 { data?: any } | CustomerFormData | null
115 >({
116 queryKey: ["customer", customerId],
117 queryFn: async () => {
118 if (!customerId) return null;
119 const response = await apiService.getCustomer(customerId);
120 const data = (response as any)?.data ?? response;
121
122 const emergency = (data as any).emergency_contact || {};
123
124 return {
125 first_name: data.first_name || "",
126 last_name: data.last_name || "",
127 email: data.email || "",
128 phone: data.phone || "",
129 secondary_phone: data.secondary_phone || "",
130 country: data.country || "",
131 city: data.city || "",
132 state: data.state || "",
133 address: data.address || "",
134 postal_code: data.postal_code || "",
135 nationality: data.nationality || "",
136 date_of_birth: data.date_of_birth || "",
137 gender: data.gender || "",
138 emergency_name: (data as any).emergency_name || emergency.name || "",
139 emergency_phone: (data as any).emergency_phone || emergency.phone || "",
140 emergency_relationship:
141 (data as any).emergency_relationship || emergency.relationship || "",
142 dietary_requirements: data.dietary_requirements || "",
143 medical_conditions: data.medical_conditions || "",
144 special_needs: data.special_needs || "",
145 newsletter_optin: Boolean(data.newsletter_optin),
146 marketing_optin: Boolean(data.marketing_optin),
147 status: data.status || "active",
148 notes: data.notes || "",
149 loyalty_tier: data.loyalty_tier || "bronze",
150 loyalty_points:
151 typeof data.loyalty_points === "number"
152 ? data.loyalty_points
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,
157 } as CustomerFormData;
158 },
159 enabled: isEditMode,
160 });
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
168 // Load customer data into form when editing
169 useEffect(() => {
170 if (customerData && isEditMode) {
171 setFormData(customerData as CustomerFormData);
172 }
173 }, [customerData, isEditMode]);
174
175 const handleFieldChange = (field: keyof CustomerFormData, value: any) => {
176 setFormData((prev) => ({ ...prev, [field]: value }));
177 if (errors[field]) {
178 setErrors((prev) => ({ ...prev, [field]: "" }));
179 }
180 };
181
182 const validateForm = (): boolean => {
183 const newErrors: Record<string, string> = {};
184
185 if (!formData.first_name.trim()) {
186 newErrors.first_name = __("First name is required", "yatra");
187 }
188
189 if (!formData.email.trim()) {
190 newErrors.email = __("Email is required", "yatra");
191 } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
192 newErrors.email = __("Invalid email address", "yatra");
193 }
194
195 setErrors(newErrors);
196 return Object.keys(newErrors).length === 0;
197 };
198
199 // Create/Update mutation
200 const saveMutation = useMutation({
201 mutationFn: async (data: CustomerFormData) => {
202 const payload = { ...data };
203
204 if (isEditMode && customerId) {
205 return await apiService.updateCustomer(customerId, payload);
206 } else {
207 return await apiService.createCustomer(payload);
208 }
209 },
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 }
226 queryClient.invalidateQueries({ queryKey: ["customers"] });
227 queryClient.invalidateQueries({ queryKey: ["customer", customerId] });
228 setIsSubmitting(false);
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 }
240 },
241 onError: (error: any) => {
242 const errorMessage =
243 error?.message ||
244 __("An error occurred while saving the customer", "yatra");
245 setErrors({ submit: errorMessage });
246 setIsSubmitting(false);
247 showToast(errorMessage, "error");
248 },
249 });
250
251 const handleSubmit = async (e: React.FormEvent) => {
252 e.preventDefault();
253
254 if (!validateForm()) {
255 return;
256 }
257
258 setIsSubmitting(true);
259 setErrors({});
260 saveMutation.mutate(formData);
261 };
262
263 const handleCancel = () => {
264 window.location.href = `${baseAdminUrl}?page=yatra&subpage=customers`;
265 };
266
267 // Skeleton loader
268 const renderSkeleton = () => (
269 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
270 <div className="lg:col-span-2 space-y-3">
271 <Card>
272 <CardHeader className="pb-2">
273 <Skeleton className="h-5 w-40" />
274 </CardHeader>
275 <CardContent className="space-y-3">
276 <div className="grid grid-cols-2 gap-3">
277 <div className="space-y-2">
278 <Skeleton className="h-4 w-20" />
279 <Skeleton className="h-9 w-full" />
280 </div>
281 <div className="space-y-2">
282 <Skeleton className="h-4 w-20" />
283 <Skeleton className="h-9 w-full" />
284 </div>
285 </div>
286 <div className="space-y-2">
287 <Skeleton className="h-4 w-24" />
288 <Skeleton className="h-9 w-full" />
289 </div>
290 </CardContent>
291 </Card>
292 </div>
293 <div className="space-y-3">
294 <Card>
295 <CardHeader className="pb-2">
296 <Skeleton className="h-5 w-20" />
297 </CardHeader>
298 <CardContent>
299 <Skeleton className="h-9 w-full" />
300 </CardContent>
301 </Card>
302 </div>
303 </div>
304 );
305
306 if (isEditMode && isLoadingCustomer) {
307 return (
308 <div className="space-y-3">
309 <PageHeader
310 title={__("Edit Customer", "yatra")}
311 description={__("Loading...", "yatra")}
312 actions={
313 <Button
314 variant="outline"
315 onClick={handleCancel}
316 className="flex items-center gap-2"
317 >
318 <ArrowLeft className="w-4 h-4" />
319 {__("Back", "yatra")}
320 </Button>
321 }
322 />
323 {renderSkeleton()}
324 </div>
325 );
326 }
327
328 return (
329 <div className="space-y-3">
330 <PageHeader
331 title={
332 isEditMode
333 ? __("Edit Customer", "yatra")
334 : __("Add New Customer", "yatra")
335 }
336 description={
337 isEditMode
338 ? __("Update customer information", "yatra")
339 : __("Create a new customer profile", "yatra")
340 }
341 actions={
342 <Button
343 variant="outline"
344 onClick={handleCancel}
345 className="flex items-center gap-2"
346 >
347 <ArrowLeft className="w-4 h-4" />
348 {__("Back", "yatra")}
349 </Button>
350 }
351 />
352
353 <ConditionalRender capability="yatra_edit_customers">
354 <form onSubmit={handleSubmit}>
355 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
356 {/* Main Form Fields */}
357 <div className="lg:col-span-2 space-y-3">
358 {/* Personal Information */}
359 <Card>
360 <CardHeader className="pb-2">
361 <CardTitle className="text-base">
362 {__("Personal Information", "yatra")}
363 </CardTitle>
364 </CardHeader>
365 <CardContent className="space-y-3">
366 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
367 {/* First Name */}
368 <div>
369 <label
370 htmlFor="first_name"
371 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
372 >
373 {__("First Name", "yatra")}{" "}
374 <span className="text-red-500">*</span>
375 </label>
376 <Input
377 id="first_name"
378 type="text"
379 value={formData.first_name}
380 onChange={(e) =>
381 handleFieldChange("first_name", e.target.value)
382 }
383 placeholder={__("Enter first name", "yatra")}
384 className={errors.first_name ? "border-red-500" : ""}
385 required
386 />
387 {errors.first_name && (
388 <p className="mt-1 text-sm text-red-500">
389 {errors.first_name}
390 </p>
391 )}
392 </div>
393
394 {/* Last Name */}
395 <div>
396 <label
397 htmlFor="last_name"
398 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
399 >
400 {__("Last Name", "yatra")}
401 </label>
402 <Input
403 id="last_name"
404 type="text"
405 value={formData.last_name}
406 onChange={(e) =>
407 handleFieldChange("last_name", e.target.value)
408 }
409 placeholder={__("Enter last name", "yatra")}
410 />
411 </div>
412 </div>
413
414 {/* Email */}
415 <div>
416 <label
417 htmlFor="email"
418 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
419 >
420 {__("Email Address", "yatra")}{" "}
421 <span className="text-red-500">*</span>
422 </label>
423 <Input
424 id="email"
425 type="email"
426 value={formData.email}
427 onChange={(e) =>
428 handleFieldChange("email", e.target.value)
429 }
430 placeholder={__("[email protected]", "yatra")}
431 className={errors.email ? "border-red-500" : ""}
432 required
433 />
434 {errors.email && (
435 <p className="mt-1 text-sm text-red-500">
436 {errors.email}
437 </p>
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 )}
484 </div>
485
486 {/* Phone Numbers */}
487 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
488 <div>
489 <label
490 htmlFor="phone"
491 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
492 >
493 {__("Phone Number", "yatra")}
494 </label>
495 <Input
496 id="phone"
497 type="tel"
498 value={formData.phone}
499 onChange={(e) =>
500 handleFieldChange("phone", e.target.value)
501 }
502 placeholder={__("+1234567890", "yatra")}
503 />
504 </div>
505 <div>
506 <label
507 htmlFor="secondary_phone"
508 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
509 >
510 {__("Secondary Phone", "yatra")}
511 </label>
512 <Input
513 id="secondary_phone"
514 type="tel"
515 value={formData.secondary_phone}
516 onChange={(e) =>
517 handleFieldChange("secondary_phone", e.target.value)
518 }
519 placeholder={__("+1234567890", "yatra")}
520 />
521 </div>
522 </div>
523
524 {/* Date of Birth & Gender */}
525 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
526 <div>
527 <label
528 htmlFor="date_of_birth"
529 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
530 >
531 {__("Date of Birth", "yatra")}
532 </label>
533 <DatePicker
534 value={formData.date_of_birth}
535 onChange={(value: string) =>
536 handleFieldChange("date_of_birth", value)
537 }
538 placeholder={__("Select date of birth", "yatra")}
539 />
540 </div>
541 <div>
542 <label
543 htmlFor="gender"
544 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
545 >
546 {__("Gender", "yatra")}
547 </label>
548 <Select
549 id="gender"
550 value={formData.gender}
551 onChange={(e) =>
552 handleFieldChange("gender", e.target.value)
553 }
554 >
555 <option value="">{__("Select gender", "yatra")}</option>
556 <option value="male">{__("Male", "yatra")}</option>
557 <option value="female">{__("Female", "yatra")}</option>
558 <option value="other">{__("Other", "yatra")}</option>
559 <option value="prefer_not_to_say">
560 {__("Prefer not to say", "yatra")}
561 </option>
562 </Select>
563 </div>
564 </div>
565
566 {/* Nationality */}
567 <div>
568 <label
569 htmlFor="nationality"
570 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
571 >
572 {__("Nationality", "yatra")}
573 </label>
574 <Input
575 id="nationality"
576 type="text"
577 value={formData.nationality}
578 onChange={(e) =>
579 handleFieldChange("nationality", e.target.value)
580 }
581 placeholder={__("Enter nationality", "yatra")}
582 />
583 </div>
584 </CardContent>
585 </Card>
586
587 {/* Address Information */}
588 <Card>
589 <CardHeader className="pb-2">
590 <CardTitle className="text-base">
591 {__("Address Information", "yatra")}
592 </CardTitle>
593 </CardHeader>
594 <CardContent className="space-y-3">
595 {/* Address */}
596 <div>
597 <label
598 htmlFor="address"
599 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
600 >
601 {__("Street Address", "yatra")}
602 </label>
603 <Input
604 id="address"
605 type="text"
606 value={formData.address}
607 onChange={(e) =>
608 handleFieldChange("address", e.target.value)
609 }
610 placeholder={__("Enter street address", "yatra")}
611 />
612 </div>
613
614 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
615 {/* City */}
616 <div>
617 <label
618 htmlFor="city"
619 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
620 >
621 {__("City", "yatra")}
622 </label>
623 <Input
624 id="city"
625 type="text"
626 value={formData.city}
627 onChange={(e) =>
628 handleFieldChange("city", e.target.value)
629 }
630 placeholder={__("Enter city", "yatra")}
631 />
632 </div>
633
634 {/* State */}
635 <div>
636 <label
637 htmlFor="state"
638 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
639 >
640 {__("State/Province", "yatra")}
641 </label>
642 <Input
643 id="state"
644 type="text"
645 value={formData.state}
646 onChange={(e) =>
647 handleFieldChange("state", e.target.value)
648 }
649 placeholder={__("Enter state", "yatra")}
650 />
651 </div>
652 </div>
653
654 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
655 {/* Country */}
656 <div>
657 <label
658 htmlFor="country"
659 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
660 >
661 {__("Country", "yatra")}
662 </label>
663 <Input
664 id="country"
665 type="text"
666 value={formData.country}
667 onChange={(e) =>
668 handleFieldChange("country", e.target.value)
669 }
670 placeholder={__("Enter country", "yatra")}
671 />
672 </div>
673
674 {/* Postal Code */}
675 <div>
676 <label
677 htmlFor="postal_code"
678 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
679 >
680 {__("Postal Code", "yatra")}
681 </label>
682 <Input
683 id="postal_code"
684 type="text"
685 value={formData.postal_code}
686 onChange={(e) =>
687 handleFieldChange("postal_code", e.target.value)
688 }
689 placeholder={__("Enter postal code", "yatra")}
690 />
691 </div>
692 </div>
693 </CardContent>
694 </Card>
695
696 {/* Emergency Contact */}
697 <Card>
698 <CardHeader className="pb-2">
699 <CardTitle className="text-base">
700 {__("Emergency Contact", "yatra")}
701 </CardTitle>
702 </CardHeader>
703 <CardContent className="space-y-3">
704 <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
705 <div>
706 <label
707 htmlFor="emergency_name"
708 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
709 >
710 {__("Contact Name", "yatra")}
711 </label>
712 <Input
713 id="emergency_name"
714 type="text"
715 value={formData.emergency_name}
716 onChange={(e) =>
717 handleFieldChange("emergency_name", e.target.value)
718 }
719 placeholder={__("Full name", "yatra")}
720 />
721 </div>
722 <div>
723 <label
724 htmlFor="emergency_phone"
725 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
726 >
727 {__("Contact Phone", "yatra")}
728 </label>
729 <Input
730 id="emergency_phone"
731 type="tel"
732 value={formData.emergency_phone}
733 onChange={(e) =>
734 handleFieldChange("emergency_phone", e.target.value)
735 }
736 placeholder={__("+1234567890", "yatra")}
737 />
738 </div>
739 <div>
740 <label
741 htmlFor="emergency_relationship"
742 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
743 >
744 {__("Relationship", "yatra")}
745 </label>
746 <Input
747 id="emergency_relationship"
748 type="text"
749 value={formData.emergency_relationship}
750 onChange={(e) =>
751 handleFieldChange(
752 "emergency_relationship",
753 e.target.value,
754 )
755 }
756 placeholder={__("e.g., Spouse, Parent", "yatra")}
757 />
758 </div>
759 </div>
760 </CardContent>
761 </Card>
762
763 {/* Special Requirements */}
764 <Card>
765 <CardHeader className="pb-2">
766 <CardTitle className="text-base">
767 {__("Special Requirements", "yatra")}
768 </CardTitle>
769 </CardHeader>
770 <CardContent className="space-y-3">
771 <div>
772 <label
773 htmlFor="dietary_requirements"
774 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
775 >
776 {__("Dietary Requirements", "yatra")}
777 </label>
778 <Input
779 id="dietary_requirements"
780 type="text"
781 value={formData.dietary_requirements}
782 onChange={(e) =>
783 handleFieldChange(
784 "dietary_requirements",
785 e.target.value,
786 )
787 }
788 placeholder={__("e.g., Vegetarian, Gluten-free", "yatra")}
789 />
790 </div>
791 <div>
792 <label
793 htmlFor="medical_conditions"
794 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
795 >
796 {__("Medical Conditions", "yatra")}
797 </label>
798 <textarea
799 id="medical_conditions"
800 value={formData.medical_conditions}
801 onChange={(e) =>
802 handleFieldChange("medical_conditions", e.target.value)
803 }
804 placeholder={__(
805 "Any relevant medical conditions",
806 "yatra",
807 )}
808 rows={2}
809 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium 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"
810 />
811 </div>
812 <div>
813 <label
814 htmlFor="special_needs"
815 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
816 >
817 {__("Special Needs", "yatra")}
818 </label>
819 <textarea
820 id="special_needs"
821 value={formData.special_needs}
822 onChange={(e) =>
823 handleFieldChange("special_needs", e.target.value)
824 }
825 placeholder={__(
826 "Any special accommodations needed",
827 "yatra",
828 )}
829 rows={2}
830 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium 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"
831 />
832 </div>
833 </CardContent>
834 </Card>
835
836 {/* Notes */}
837 <Card>
838 <CardHeader className="pb-2">
839 <CardTitle className="text-base">
840 {__("Internal Notes", "yatra")}
841 </CardTitle>
842 </CardHeader>
843 <CardContent>
844 <textarea
845 id="notes"
846 value={formData.notes}
847 onChange={(e) => handleFieldChange("notes", e.target.value)}
848 placeholder={__(
849 "Enter any internal notes about this customer (not visible to customer)",
850 "yatra",
851 )}
852 rows={4}
853 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium 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"
854 />
855 </CardContent>
856 </Card>
857 </div>
858
859 {/* Sidebar */}
860 <div className="space-y-3">
861 {/* Status */}
862 <Card>
863 <CardHeader className="pb-2">
864 <CardTitle className="text-base">
865 {__("Status", "yatra")}
866 </CardTitle>
867 </CardHeader>
868 <CardContent className="space-y-3">
869 <div>
870 <label
871 htmlFor="status"
872 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
873 >
874 {__("Customer Status", "yatra")}
875 </label>
876 <Select
877 id="status"
878 value={formData.status}
879 onChange={(e) =>
880 handleFieldChange("status", e.target.value)
881 }
882 >
883 <option value="active">{__("Active", "yatra")}</option>
884 <option value="inactive">
885 {__("Inactive", "yatra")}
886 </option>
887 <option value="blocked">{__("Blocked", "yatra")}</option>
888 </Select>
889 </div>
890 </CardContent>
891 </Card>
892
893 {/* Loyalty */}
894 {isEditMode && (
895 <Card>
896 <CardHeader className="pb-2">
897 <CardTitle className="text-base">
898 {__("Loyalty", "yatra")}
899 </CardTitle>
900 </CardHeader>
901 <CardContent className="space-y-3">
902 <div>
903 <label
904 htmlFor="loyalty_tier"
905 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
906 >
907 {__("Loyalty Tier", "yatra")}
908 </label>
909 <Select
910 id="loyalty_tier"
911 value={formData.loyalty_tier}
912 onChange={(e) =>
913 handleFieldChange("loyalty_tier", e.target.value)
914 }
915 >
916 <option value="bronze">
917 {__("🥉 Bronze", "yatra")}
918 </option>
919 <option value="silver">
920 {__("🥈 Silver", "yatra")}
921 </option>
922 <option value="gold">{__("🥇 Gold", "yatra")}</option>
923 <option value="platinum">
924 {__("💎 Platinum", "yatra")}
925 </option>
926 </Select>
927 </div>
928 <div>
929 <label
930 htmlFor="loyalty_points"
931 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
932 >
933 {__("Loyalty Points", "yatra")}
934 </label>
935 <Input
936 id="loyalty_points"
937 type="number"
938 min={0}
939 value={formData.loyalty_points}
940 onChange={(e) =>
941 handleFieldChange(
942 "loyalty_points",
943 parseInt(e.target.value) || 0,
944 )
945 }
946 />
947 </div>
948 </CardContent>
949 </Card>
950 )}
951
952 {/* Preferences */}
953 <Card>
954 <CardHeader className="pb-2">
955 <CardTitle className="text-base">
956 {__("Preferences", "yatra")}
957 </CardTitle>
958 </CardHeader>
959 <CardContent className="space-y-3">
960 <label className="flex items-center gap-2 cursor-pointer">
961 <input
962 type="checkbox"
963 checked={formData.newsletter_optin}
964 onChange={(e) =>
965 handleFieldChange("newsletter_optin", e.target.checked)
966 }
967 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
968 />
969 <span className="text-sm text-gray-700 dark:text-gray-300">
970 {__("Subscribe to newsletter", "yatra")}
971 </span>
972 </label>
973 <label className="flex items-center gap-2 cursor-pointer">
974 <input
975 type="checkbox"
976 checked={formData.marketing_optin}
977 onChange={(e) =>
978 handleFieldChange("marketing_optin", e.target.checked)
979 }
980 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
981 />
982 <span className="text-sm text-gray-700 dark:text-gray-300">
983 {__("Receive marketing emails", "yatra")}
984 </span>
985 </label>
986 </CardContent>
987 </Card>
988
989 {/* Submit Actions */}
990 <Card>
991 <CardContent className="p-3">
992 <div className="space-y-2">
993 {errors.submit && (
994 <div className="p-2 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-md">
995 {errors.submit}
996 </div>
997 )}
998 <div className="flex gap-2">
999 <Button
1000 type="submit"
1001 disabled={isSubmitting}
1002 className="flex-1 flex items-center justify-center gap-2"
1003 >
1004 {isSubmitting ? (
1005 <>
1006 <Loader2 className="w-4 h-4 animate-spin" />
1007 {__("Saving...", "yatra")}
1008 </>
1009 ) : (
1010 <>
1011 <Save className="w-4 h-4" />
1012 {isEditMode
1013 ? __("Update Customer", "yatra")
1014 : __("Create Customer", "yatra")}
1015 </>
1016 )}
1017 </Button>
1018 <Button
1019 type="button"
1020 variant="outline"
1021 onClick={handleCancel}
1022 disabled={isSubmitting}
1023 >
1024 {__("Cancel", "yatra")}
1025 </Button>
1026 </div>
1027 </div>
1028 </CardContent>
1029 </Card>
1030 </div>
1031 </div>
1032 </form>
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 />
1053 </div>
1054 );
1055 };
1056
1057 export default CustomerForm;
1058