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