PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
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.11, at resources/js/pages/CustomerForm.tsx

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