PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / CustomerForm.tsx

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

956 lines 36.4 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 disabled={isEditMode} // Can't change email
395 />
396 {errors.email && (
397 <p className="mt-1 text-sm text-red-500">
398 {errors.email}
399 </p>
400 )}
401 </div>
402
403 {/* Phone Numbers */}
404 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
405 <div>
406 <label
407 htmlFor="phone"
408 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
409 >
410 {__("Phone Number", "yatra")}
411 </label>
412 <Input
413 id="phone"
414 type="tel"
415 value={formData.phone}
416 onChange={(e) =>
417 handleFieldChange("phone", e.target.value)
418 }
419 placeholder={__("+1234567890", "yatra")}
420 />
421 </div>
422 <div>
423 <label
424 htmlFor="secondary_phone"
425 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
426 >
427 {__("Secondary Phone", "yatra")}
428 </label>
429 <Input
430 id="secondary_phone"
431 type="tel"
432 value={formData.secondary_phone}
433 onChange={(e) =>
434 handleFieldChange("secondary_phone", e.target.value)
435 }
436 placeholder={__("+1234567890", "yatra")}
437 />
438 </div>
439 </div>
440
441 {/* Date of Birth & Gender */}
442 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
443 <div>
444 <label
445 htmlFor="date_of_birth"
446 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
447 >
448 {__("Date of Birth", "yatra")}
449 </label>
450 <DatePicker
451 value={formData.date_of_birth}
452 onChange={(value: string) =>
453 handleFieldChange("date_of_birth", value)
454 }
455 placeholder={__("Select date of birth", "yatra")}
456 />
457 </div>
458 <div>
459 <label
460 htmlFor="gender"
461 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
462 >
463 {__("Gender", "yatra")}
464 </label>
465 <Select
466 id="gender"
467 value={formData.gender}
468 onChange={(e) =>
469 handleFieldChange("gender", e.target.value)
470 }
471 >
472 <option value="">{__("Select gender", "yatra")}</option>
473 <option value="male">{__("Male", "yatra")}</option>
474 <option value="female">{__("Female", "yatra")}</option>
475 <option value="other">{__("Other", "yatra")}</option>
476 <option value="prefer_not_to_say">
477 {__("Prefer not to say", "yatra")}
478 </option>
479 </Select>
480 </div>
481 </div>
482
483 {/* Nationality */}
484 <div>
485 <label
486 htmlFor="nationality"
487 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
488 >
489 {__("Nationality", "yatra")}
490 </label>
491 <Input
492 id="nationality"
493 type="text"
494 value={formData.nationality}
495 onChange={(e) =>
496 handleFieldChange("nationality", e.target.value)
497 }
498 placeholder={__("Enter nationality", "yatra")}
499 />
500 </div>
501 </CardContent>
502 </Card>
503
504 {/* Address Information */}
505 <Card>
506 <CardHeader className="pb-2">
507 <CardTitle className="text-base">
508 {__("Address Information", "yatra")}
509 </CardTitle>
510 </CardHeader>
511 <CardContent className="space-y-3">
512 {/* Address */}
513 <div>
514 <label
515 htmlFor="address"
516 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
517 >
518 {__("Street Address", "yatra")}
519 </label>
520 <Input
521 id="address"
522 type="text"
523 value={formData.address}
524 onChange={(e) =>
525 handleFieldChange("address", e.target.value)
526 }
527 placeholder={__("Enter street address", "yatra")}
528 />
529 </div>
530
531 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
532 {/* City */}
533 <div>
534 <label
535 htmlFor="city"
536 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
537 >
538 {__("City", "yatra")}
539 </label>
540 <Input
541 id="city"
542 type="text"
543 value={formData.city}
544 onChange={(e) =>
545 handleFieldChange("city", e.target.value)
546 }
547 placeholder={__("Enter city", "yatra")}
548 />
549 </div>
550
551 {/* State */}
552 <div>
553 <label
554 htmlFor="state"
555 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
556 >
557 {__("State/Province", "yatra")}
558 </label>
559 <Input
560 id="state"
561 type="text"
562 value={formData.state}
563 onChange={(e) =>
564 handleFieldChange("state", e.target.value)
565 }
566 placeholder={__("Enter state", "yatra")}
567 />
568 </div>
569 </div>
570
571 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
572 {/* Country */}
573 <div>
574 <label
575 htmlFor="country"
576 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
577 >
578 {__("Country", "yatra")}
579 </label>
580 <Input
581 id="country"
582 type="text"
583 value={formData.country}
584 onChange={(e) =>
585 handleFieldChange("country", e.target.value)
586 }
587 placeholder={__("Enter country", "yatra")}
588 />
589 </div>
590
591 {/* Postal Code */}
592 <div>
593 <label
594 htmlFor="postal_code"
595 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
596 >
597 {__("Postal Code", "yatra")}
598 </label>
599 <Input
600 id="postal_code"
601 type="text"
602 value={formData.postal_code}
603 onChange={(e) =>
604 handleFieldChange("postal_code", e.target.value)
605 }
606 placeholder={__("Enter postal code", "yatra")}
607 />
608 </div>
609 </div>
610 </CardContent>
611 </Card>
612
613 {/* Emergency Contact */}
614 <Card>
615 <CardHeader className="pb-2">
616 <CardTitle className="text-base">
617 {__("Emergency Contact", "yatra")}
618 </CardTitle>
619 </CardHeader>
620 <CardContent className="space-y-3">
621 <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
622 <div>
623 <label
624 htmlFor="emergency_name"
625 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
626 >
627 {__("Contact Name", "yatra")}
628 </label>
629 <Input
630 id="emergency_name"
631 type="text"
632 value={formData.emergency_name}
633 onChange={(e) =>
634 handleFieldChange("emergency_name", e.target.value)
635 }
636 placeholder={__("Full name", "yatra")}
637 />
638 </div>
639 <div>
640 <label
641 htmlFor="emergency_phone"
642 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
643 >
644 {__("Contact Phone", "yatra")}
645 </label>
646 <Input
647 id="emergency_phone"
648 type="tel"
649 value={formData.emergency_phone}
650 onChange={(e) =>
651 handleFieldChange("emergency_phone", e.target.value)
652 }
653 placeholder={__("+1234567890", "yatra")}
654 />
655 </div>
656 <div>
657 <label
658 htmlFor="emergency_relationship"
659 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
660 >
661 {__("Relationship", "yatra")}
662 </label>
663 <Input
664 id="emergency_relationship"
665 type="text"
666 value={formData.emergency_relationship}
667 onChange={(e) =>
668 handleFieldChange(
669 "emergency_relationship",
670 e.target.value,
671 )
672 }
673 placeholder={__("e.g., Spouse, Parent", "yatra")}
674 />
675 </div>
676 </div>
677 </CardContent>
678 </Card>
679
680 {/* Special Requirements */}
681 <Card>
682 <CardHeader className="pb-2">
683 <CardTitle className="text-base">
684 {__("Special Requirements", "yatra")}
685 </CardTitle>
686 </CardHeader>
687 <CardContent className="space-y-3">
688 <div>
689 <label
690 htmlFor="dietary_requirements"
691 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
692 >
693 {__("Dietary Requirements", "yatra")}
694 </label>
695 <Input
696 id="dietary_requirements"
697 type="text"
698 value={formData.dietary_requirements}
699 onChange={(e) =>
700 handleFieldChange(
701 "dietary_requirements",
702 e.target.value,
703 )
704 }
705 placeholder={__("e.g., Vegetarian, Gluten-free", "yatra")}
706 />
707 </div>
708 <div>
709 <label
710 htmlFor="medical_conditions"
711 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
712 >
713 {__("Medical Conditions", "yatra")}
714 </label>
715 <textarea
716 id="medical_conditions"
717 value={formData.medical_conditions}
718 onChange={(e) =>
719 handleFieldChange("medical_conditions", e.target.value)
720 }
721 placeholder={__(
722 "Any relevant medical conditions",
723 "yatra",
724 )}
725 rows={2}
726 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"
727 />
728 </div>
729 <div>
730 <label
731 htmlFor="special_needs"
732 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
733 >
734 {__("Special Needs", "yatra")}
735 </label>
736 <textarea
737 id="special_needs"
738 value={formData.special_needs}
739 onChange={(e) =>
740 handleFieldChange("special_needs", e.target.value)
741 }
742 placeholder={__(
743 "Any special accommodations needed",
744 "yatra",
745 )}
746 rows={2}
747 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"
748 />
749 </div>
750 </CardContent>
751 </Card>
752
753 {/* Notes */}
754 <Card>
755 <CardHeader className="pb-2">
756 <CardTitle className="text-base">
757 {__("Internal Notes", "yatra")}
758 </CardTitle>
759 </CardHeader>
760 <CardContent>
761 <textarea
762 id="notes"
763 value={formData.notes}
764 onChange={(e) => handleFieldChange("notes", e.target.value)}
765 placeholder={__(
766 "Enter any internal notes about this customer (not visible to customer)",
767 "yatra",
768 )}
769 rows={4}
770 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"
771 />
772 </CardContent>
773 </Card>
774 </div>
775
776 {/* Sidebar */}
777 <div className="space-y-3">
778 {/* Status */}
779 <Card>
780 <CardHeader className="pb-2">
781 <CardTitle className="text-base">
782 {__("Status", "yatra")}
783 </CardTitle>
784 </CardHeader>
785 <CardContent className="space-y-3">
786 <div>
787 <label
788 htmlFor="status"
789 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
790 >
791 {__("Customer Status", "yatra")}
792 </label>
793 <Select
794 id="status"
795 value={formData.status}
796 onChange={(e) =>
797 handleFieldChange("status", e.target.value)
798 }
799 >
800 <option value="active">{__("Active", "yatra")}</option>
801 <option value="inactive">
802 {__("Inactive", "yatra")}
803 </option>
804 <option value="blocked">{__("Blocked", "yatra")}</option>
805 </Select>
806 </div>
807 </CardContent>
808 </Card>
809
810 {/* Loyalty */}
811 {isEditMode && (
812 <Card>
813 <CardHeader className="pb-2">
814 <CardTitle className="text-base">
815 {__("Loyalty", "yatra")}
816 </CardTitle>
817 </CardHeader>
818 <CardContent className="space-y-3">
819 <div>
820 <label
821 htmlFor="loyalty_tier"
822 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
823 >
824 {__("Loyalty Tier", "yatra")}
825 </label>
826 <Select
827 id="loyalty_tier"
828 value={formData.loyalty_tier}
829 onChange={(e) =>
830 handleFieldChange("loyalty_tier", e.target.value)
831 }
832 >
833 <option value="bronze">
834 {__("🥉 Bronze", "yatra")}
835 </option>
836 <option value="silver">
837 {__("🥈 Silver", "yatra")}
838 </option>
839 <option value="gold">{__("🥇 Gold", "yatra")}</option>
840 <option value="platinum">
841 {__("💎 Platinum", "yatra")}
842 </option>
843 </Select>
844 </div>
845 <div>
846 <label
847 htmlFor="loyalty_points"
848 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
849 >
850 {__("Loyalty Points", "yatra")}
851 </label>
852 <Input
853 id="loyalty_points"
854 type="number"
855 min={0}
856 value={formData.loyalty_points}
857 onChange={(e) =>
858 handleFieldChange(
859 "loyalty_points",
860 parseInt(e.target.value) || 0,
861 )
862 }
863 />
864 </div>
865 </CardContent>
866 </Card>
867 )}
868
869 {/* Preferences */}
870 <Card>
871 <CardHeader className="pb-2">
872 <CardTitle className="text-base">
873 {__("Preferences", "yatra")}
874 </CardTitle>
875 </CardHeader>
876 <CardContent className="space-y-3">
877 <label className="flex items-center gap-2 cursor-pointer">
878 <input
879 type="checkbox"
880 checked={formData.newsletter_optin}
881 onChange={(e) =>
882 handleFieldChange("newsletter_optin", e.target.checked)
883 }
884 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
885 />
886 <span className="text-sm text-gray-700 dark:text-gray-300">
887 {__("Subscribe to newsletter", "yatra")}
888 </span>
889 </label>
890 <label className="flex items-center gap-2 cursor-pointer">
891 <input
892 type="checkbox"
893 checked={formData.marketing_optin}
894 onChange={(e) =>
895 handleFieldChange("marketing_optin", e.target.checked)
896 }
897 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
898 />
899 <span className="text-sm text-gray-700 dark:text-gray-300">
900 {__("Receive marketing emails", "yatra")}
901 </span>
902 </label>
903 </CardContent>
904 </Card>
905
906 {/* Submit Actions */}
907 <Card>
908 <CardContent className="p-3">
909 <div className="space-y-2">
910 {errors.submit && (
911 <div className="p-2 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-md">
912 {errors.submit}
913 </div>
914 )}
915 <div className="flex gap-2">
916 <Button
917 type="submit"
918 disabled={isSubmitting}
919 className="flex-1 flex items-center justify-center gap-2"
920 >
921 {isSubmitting ? (
922 <>
923 <Loader2 className="w-4 h-4 animate-spin" />
924 {__("Saving...", "yatra")}
925 </>
926 ) : (
927 <>
928 <Save className="w-4 h-4" />
929 {isEditMode
930 ? __("Update Customer", "yatra")
931 : __("Create Customer", "yatra")}
932 </>
933 )}
934 </Button>
935 <Button
936 type="button"
937 variant="outline"
938 onClick={handleCancel}
939 disabled={isSubmitting}
940 >
941 {__("Cancel", "yatra")}
942 </Button>
943 </div>
944 </div>
945 </CardContent>
946 </Card>
947 </div>
948 </div>
949 </form>
950 </ConditionalRender>
951 </div>
952 );
953 };
954
955 export default CustomerForm;
956