PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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 / TravelerCategoryForm.tsx

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

849 lines 31.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Traveler Category Form Page
3 * Add/Edit Traveler Category form with clean, minimal SaaS-style design
4 */
5
6 import React, { useState, useEffect, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2, Edit2, X, AlertCircle } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { usePermissions } from "../hooks/usePermissions";
11 import { useToast } from "../components/ui/toast";
12 import { apiClient } from "../lib/api-client";
13 import { generateSlug } from "../lib/slug";
14 import { Button } from "../components/ui/button";
15 import { Input } from "../components/ui/input";
16 import { Select } from "../components/ui/select";
17 import { PageHeader } from "../components/common/PageHeader";
18 import {
19 Card,
20 CardContent,
21 CardHeader,
22 CardTitle,
23 } from "../components/ui/card";
24 import { ConditionalRender } from "../components/ui/conditional-render";
25 import { IconPicker, IconPickerValue } from "../components/ui/icon-picker";
26 import { Skeleton } from "../components/ui/skeleton";
27 import { HelpText } from "../components/ui/help-text";
28
29 interface TravelerCategoryFormData {
30 label: string;
31 slug: string;
32 description: string;
33 age_min: string;
34 age_max: string;
35 icon: {
36 type: "icon" | "image";
37 value: string;
38 } | null;
39 status: "draft" | "publish" | "trash";
40 pricing_mode: "per_person" | "per_group";
41 min_pax: string;
42 max_pax: string;
43 }
44
45 const TravelerCategoryForm: React.FC = () => {
46 const queryClient = useQueryClient();
47 const { can } = usePermissions();
48 const { showToast } = useToast();
49 const [formData, setFormData] = useState<TravelerCategoryFormData>({
50 label: "",
51 slug: "",
52 description: "",
53 age_min: "",
54 age_max: "",
55 icon: null,
56 status: "publish",
57 pricing_mode: "per_person",
58 min_pax: "",
59 max_pax: "",
60 });
61 const [errors, setErrors] = useState<Record<string, string>>({});
62 const [isSubmitting, setIsSubmitting] = useState(false);
63 const [isSlugEditable, setIsSlugEditable] = useState(false);
64
65 // Get action and id from URL
66 const action = useMemo(() => {
67 const params = new URLSearchParams(window.location.search);
68 return params.get("action") || "create";
69 }, []);
70
71 const categoryId = useMemo(() => {
72 const params = new URLSearchParams(window.location.search);
73 return params.get("id") ? parseInt(params.get("id") || "0") : null;
74 }, []);
75
76 const isEditMode = action === "edit" && categoryId !== null;
77
78 // Fetch category data if editing
79 const { data: categoryData, isLoading: isLoadingCategory } = useQuery({
80 queryKey: ["traveler-category", categoryId],
81 queryFn: async () => {
82 if (!categoryId) return null;
83 try {
84 const response = await apiClient.get(
85 `/traveler-categories/${categoryId}`,
86 );
87 return response;
88 } catch (error: any) {
89 showToast(
90 error?.message || __("Failed to load traveler category", "yatra"),
91 "error",
92 );
93 throw error;
94 }
95 },
96 enabled: isEditMode && can("yatra_view_trips"),
97 });
98
99 // Load category data into form when editing
100 useEffect(() => {
101 if (categoryData && isEditMode) {
102 setFormData({
103 label: categoryData.label || "",
104 slug: categoryData.slug || "",
105 description: categoryData.description || "",
106 age_min: categoryData.age_min?.toString() || "",
107 age_max: categoryData.age_max?.toString() || "",
108 icon: (categoryData.icon as IconPickerValue) || null,
109 status: categoryData.status || "draft",
110 pricing_mode:
111 (categoryData.pricing_mode as "per_person" | "per_group") ||
112 "per_person",
113 min_pax:
114 categoryData.min_pax !== undefined && categoryData.min_pax !== null
115 ? categoryData.min_pax.toString()
116 : "",
117 max_pax:
118 categoryData.max_pax !== undefined && categoryData.max_pax !== null
119 ? categoryData.max_pax.toString()
120 : "",
121 });
122 }
123 }, [categoryData, isEditMode]);
124
125 const handleLabelChange = (value: string) => {
126 // Auto-generate slug from label only on create (and when slug is not manually edited)
127 if (!isEditMode && !isSlugEditable) {
128 const newSlug = generateSlug(value);
129 setFormData((prev) => ({
130 ...prev,
131 label: value,
132 slug: newSlug,
133 }));
134 } else {
135 setFormData((prev) => ({
136 ...prev,
137 label: value,
138 }));
139 }
140 if (errors.label) {
141 setErrors((prev) => ({ ...prev, label: "" }));
142 }
143 };
144
145 const handleSlugChange = (value: string) => {
146 // Only allow manual slug editing if edit mode is enabled
147 if (isSlugEditable) {
148 setFormData((prev) => ({ ...prev, slug: value }));
149 if (errors.slug) {
150 setErrors((prev) => ({ ...prev, slug: "" }));
151 }
152 }
153 };
154
155 const handleToggleSlugEdit = () => {
156 if (isSlugEditable) {
157 // If disabling edit, regenerate slug from label
158 const newSlug = generateSlug(formData.label);
159 setFormData((prev) => ({ ...prev, slug: newSlug }));
160 }
161 setIsSlugEditable(!isSlugEditable);
162 };
163
164 const handleFieldChange = (
165 field: keyof TravelerCategoryFormData,
166 value: string | IconPickerValue | null,
167 ) => {
168 setFormData((prev) => ({ ...prev, [field]: value }));
169 if (errors[field]) {
170 setErrors((prev) => ({ ...prev, [field]: "" }));
171 }
172 };
173
174 const validateForm = (): boolean => {
175 const newErrors: Record<string, string> = {};
176
177 if (!formData.label.trim()) {
178 newErrors.label = __("Label is required", "yatra");
179 }
180
181 if (!formData.slug.trim()) {
182 newErrors.slug = __("Slug is required", "yatra");
183 } else if (!/^[\p{L}\p{N}-]+$/u.test(formData.slug)) {
184 newErrors.slug = __(
185 "Slug can only contain letters, numbers, and hyphens",
186 "yatra",
187 );
188 }
189
190 if (!formData.description.trim()) {
191 newErrors.description = __("Description is required", "yatra");
192 }
193
194 // Validate age range
195 if (formData.age_min && isNaN(parseInt(formData.age_min))) {
196 newErrors.age_min = __("Minimum age must be a valid number", "yatra");
197 }
198 if (formData.age_max && isNaN(parseInt(formData.age_max))) {
199 newErrors.age_max = __("Maximum age must be a valid number", "yatra");
200 }
201 if (
202 formData.age_min &&
203 formData.age_max &&
204 parseInt(formData.age_min) >= parseInt(formData.age_max)
205 ) {
206 newErrors.age_max = __(
207 "Maximum age must be greater than minimum age",
208 "yatra",
209 );
210 }
211
212 // Validate group size when pricing_mode is per_group
213 if (formData.pricing_mode === "per_group") {
214 if (formData.min_pax && isNaN(parseInt(formData.min_pax))) {
215 newErrors.min_pax = __(
216 "Minimum group size must be a valid number",
217 "yatra",
218 );
219 }
220 if (formData.max_pax && isNaN(parseInt(formData.max_pax))) {
221 newErrors.max_pax = __(
222 "Maximum group size must be a valid number",
223 "yatra",
224 );
225 }
226 if (
227 formData.min_pax &&
228 formData.max_pax &&
229 !isNaN(parseInt(formData.min_pax)) &&
230 !isNaN(parseInt(formData.max_pax)) &&
231 parseInt(formData.min_pax) > parseInt(formData.max_pax)
232 ) {
233 newErrors.max_pax = __(
234 "Maximum group size should be greater than or equal to minimum group size",
235 "yatra",
236 );
237 }
238 }
239
240 setErrors(newErrors);
241 return Object.keys(newErrors).length === 0;
242 };
243
244 // Create/Update mutation
245 const saveMutation = useMutation({
246 mutationFn: async (data: TravelerCategoryFormData) => {
247 const payload: any = {
248 label: data.label.trim(),
249 slug: data.slug.trim(),
250 description: data.description.trim(),
251 icon: data.icon,
252 status: data.status,
253 };
254
255 // Handle age_min
256 if (data.age_min && data.age_min.trim() !== "") {
257 payload.age_min = parseInt(data.age_min);
258 } else {
259 payload.age_min = null;
260 }
261
262 // Handle age_max
263 if (data.age_max && data.age_max.trim() !== "") {
264 payload.age_max = parseInt(data.age_max);
265 } else {
266 payload.age_max = null;
267 }
268
269 // Pricing mode and group size
270 payload.pricing_mode = data.pricing_mode || "per_person";
271
272 if (data.pricing_mode === "per_group") {
273 if (data.min_pax && data.min_pax.trim() !== "") {
274 payload.min_pax = parseInt(data.min_pax);
275 } else {
276 payload.min_pax = null;
277 }
278
279 if (data.max_pax && data.max_pax.trim() !== "") {
280 payload.max_pax = parseInt(data.max_pax);
281 } else {
282 payload.max_pax = null;
283 }
284 } else {
285 payload.min_pax = null;
286 payload.max_pax = null;
287 }
288
289 // If slug was manually edited, add flag to preserve it
290 if (isEditMode && isSlugEditable) {
291 payload.preserve_slug = true;
292 }
293
294 if (isEditMode && categoryId) {
295 return await apiClient.put(
296 `/traveler-categories/${categoryId}`,
297 payload,
298 );
299 } else {
300 return await apiClient.post("/traveler-categories", payload);
301 }
302 },
303 onSuccess: (response: any) => {
304 queryClient.invalidateQueries({ queryKey: ["traveler-categories"] });
305 queryClient.invalidateQueries({
306 queryKey: ["traveler-category", categoryId],
307 });
308 showToast(
309 isEditMode
310 ? __("Traveler category updated successfully", "yatra")
311 : __("Traveler category created successfully", "yatra"),
312 "success",
313 );
314 if (!isEditMode) {
315 const newId = response?.id || response?.data?.id;
316 if (newId) {
317 setTimeout(() => {
318 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=traveler-categories&action=edit&id=${newId}`;
319 }, 800);
320 }
321 } else {
322 setIsSubmitting(false);
323 }
324 },
325 onError: (error: any) => {
326 const errorMessage =
327 error?.message ||
328 __("An error occurred while saving the traveler category", "yatra");
329 showToast(errorMessage, "error");
330 setIsSubmitting(false);
331 },
332 });
333
334 const handleSubmit = async (e: React.FormEvent) => {
335 e.preventDefault();
336
337 if (!validateForm()) {
338 showToast(__("Please fix the form errors", "yatra"), "warning");
339 return;
340 }
341
342 setIsSubmitting(true);
343 setErrors({});
344 saveMutation.mutate(formData);
345 };
346
347 const handleCancel = () => {
348 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=traveler-categories`;
349 };
350
351 if (isEditMode && isLoadingCategory) {
352 return (
353 <div className="space-y-3">
354 <PageHeader
355 title={__("Edit Category", "yatra")}
356 description={__("Update traveler category information", "yatra")}
357 actions={<Skeleton className="w-24 h-9 rounded-md" />}
358 />
359
360 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
361 <div className="lg:col-span-2 space-y-3">
362 <Card>
363 <CardHeader className="pb-2">
364 <Skeleton className="h-5 w-48" />
365 </CardHeader>
366 <CardContent className="space-y-4">
367 {[1, 2, 3].map((section) => (
368 <div key={section} className="space-y-2">
369 <Skeleton className="h-4 w-32" />
370 <Skeleton className="h-10 w-full" />
371 </div>
372 ))}
373 <div className="space-y-2">
374 <Skeleton className="h-4 w-32" />
375 <Skeleton className="h-24 w-full" />
376 </div>
377 </CardContent>
378 </Card>
379 </div>
380
381 <div className="space-y-3">
382 {[1, 2, 3].map((card) => (
383 <Card key={card}>
384 <CardHeader className="pb-2">
385 <Skeleton className="h-5 w-36" />
386 </CardHeader>
387 <CardContent>
388 <Skeleton className="h-40 w-full" />
389 </CardContent>
390 </Card>
391 ))}
392 </div>
393 </div>
394 </div>
395 );
396 }
397
398 return (
399 <div className="space-y-3">
400 <PageHeader
401 title={
402 isEditMode
403 ? __("Edit Category", "yatra")
404 : __("Add Category", "yatra")
405 }
406 description={
407 isEditMode
408 ? __("Update traveler category information", "yatra")
409 : __("Create a new traveler category for pricing", "yatra")
410 }
411 actions={
412 <Button
413 variant="outline"
414 onClick={handleCancel}
415 className="flex items-center gap-2"
416 >
417 <ArrowLeft className="w-4 h-4" />
418 {__("Back", "yatra")}
419 </Button>
420 }
421 />
422
423 <ConditionalRender capability="yatra_edit_trips">
424 <form onSubmit={handleSubmit}>
425 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
426 {/* Main Form Fields */}
427 <div className="lg:col-span-2 space-y-3">
428 {/* Basic Information */}
429 <Card>
430 <CardHeader className="pb-2">
431 <CardTitle className="text-base">
432 {__("Category Information", "yatra")}
433 </CardTitle>
434 </CardHeader>
435 <CardContent className="space-y-3">
436 {/* Label */}
437 <div>
438 <label
439 htmlFor="label"
440 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
441 >
442 {__("Category Label", "yatra")}{" "}
443 <span className="text-red-500">*</span>
444 </label>
445 <HelpText
446 text={__(
447 "The name of this traveler category (e.g., Adult, Child, Senior, Student)",
448 "yatra",
449 )}
450 className="mb-2"
451 />
452 <Input
453 id="label"
454 type="text"
455 value={formData.label}
456 onChange={(e) => handleLabelChange(e.target.value)}
457 placeholder={__("e.g., Adult", "yatra")}
458 className={errors.label ? "border-red-500" : ""}
459 required
460 />
461 {errors.label && (
462 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
463 <AlertCircle className="w-4 h-4" />
464 {errors.label}
465 </p>
466 )}
467 </div>
468
469 {/* Slug */}
470 <div>
471 <label
472 htmlFor="slug"
473 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
474 >
475 {__("Slug", "yatra")}{" "}
476 <span className="text-red-500">*</span>
477 </label>
478 <div className="relative">
479 <Input
480 id="slug"
481 type="text"
482 value={formData.slug}
483 onChange={(e) => handleSlugChange(e.target.value)}
484 placeholder={__("category-slug", "yatra")}
485 className={`pr-10 ${errors.slug ? "border-red-500" : ""} ${!isSlugEditable ? "bg-gray-50 dark:bg-gray-800 cursor-not-allowed" : ""}`}
486 disabled={!isSlugEditable}
487 required
488 />
489 <button
490 type="button"
491 onClick={handleToggleSlugEdit}
492 className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors rounded"
493 aria-label={
494 isSlugEditable
495 ? __("Cancel editing slug", "yatra")
496 : __("Edit slug", "yatra")
497 }
498 >
499 {isSlugEditable ? (
500 <X className="w-4 h-4" />
501 ) : (
502 <Edit2 className="w-4 h-4" />
503 )}
504 </button>
505 </div>
506 {errors.slug && (
507 <p className="mt-1 text-sm text-red-500">{errors.slug}</p>
508 )}
509 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
510 {isSlugEditable
511 ? __(
512 "You are manually editing the slug. Click X to cancel and regenerate from label.",
513 "yatra",
514 )
515 : __(
516 "This slug is auto-generated from the label. Click the edit icon to customize.",
517 "yatra",
518 )}
519 </p>
520 </div>
521
522 {/* Description */}
523 <div>
524 <label
525 htmlFor="description"
526 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
527 >
528 {__("Description", "yatra")}{" "}
529 <span className="text-red-500">*</span>
530 </label>
531 <HelpText
532 text={__(
533 "A brief description of this category and who it applies to",
534 "yatra",
535 )}
536 className="mb-2"
537 />
538 <textarea
539 id="description"
540 value={formData.description}
541 onChange={(e) =>
542 handleFieldChange("description", e.target.value)
543 }
544 placeholder={__(
545 "e.g., Standard adult pricing for travelers aged 18 and above",
546 "yatra",
547 )}
548 rows={3}
549 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 ${errors.description ? "border-red-500" : ""}`}
550 required
551 />
552 {errors.description && (
553 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
554 <AlertCircle className="w-4 h-4" />
555 {errors.description}
556 </p>
557 )}
558 </div>
559
560 {/* Age Range */}
561 <div>
562 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
563 {__("Age Range", "yatra")}
564 </label>
565 <HelpText
566 text={__(
567 "Specify the age range for this category (e.g., 0-4 for Infant, 5-17 for Child, 18+ for Adult). Leave empty if no age restriction.",
568 "yatra",
569 )}
570 className="mb-2"
571 />
572 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
573 <div>
574 <label
575 htmlFor="age_min"
576 className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5"
577 >
578 {__("Minimum Age", "yatra")}
579 </label>
580 <Input
581 id="age_min"
582 type="number"
583 min="0"
584 value={formData.age_min}
585 onChange={(e) =>
586 handleFieldChange("age_min", e.target.value)
587 }
588 placeholder={__("e.g., 0, 5, 18", "yatra")}
589 className={errors.age_min ? "border-red-500" : ""}
590 />
591 {errors.age_min && (
592 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
593 <AlertCircle className="w-4 h-4" />
594 {errors.age_min}
595 </p>
596 )}
597 </div>
598 <div>
599 <label
600 htmlFor="age_max"
601 className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5"
602 >
603 {__("Maximum Age", "yatra")}
604 </label>
605 <Input
606 id="age_max"
607 type="number"
608 min="0"
609 value={formData.age_max}
610 onChange={(e) =>
611 handleFieldChange("age_max", e.target.value)
612 }
613 placeholder={__(
614 "e.g., 4, 17, leave empty for 18+",
615 "yatra",
616 )}
617 className={errors.age_max ? "border-red-500" : ""}
618 />
619 {errors.age_max && (
620 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
621 <AlertCircle className="w-4 h-4" />
622 {errors.age_max}
623 </p>
624 )}
625 </div>
626 </div>
627 {formData.age_min &&
628 formData.age_max &&
629 parseInt(formData.age_min) >=
630 parseInt(formData.age_max) && (
631 <p className="mt-1.5 text-sm text-yellow-600 dark:text-yellow-400 flex items-center gap-1">
632 <AlertCircle className="w-4 h-4" />
633 {__(
634 "Minimum age should be less than maximum age",
635 "yatra",
636 )}
637 </p>
638 )}
639 </div>
640
641 {/* Pricing Mode & Group Size */}
642 <div>
643 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
644 {__("Pricing Mode", "yatra")}
645 </label>
646 <HelpText
647 text={__(
648 "Choose how this traveler category is priced. Per person charges per traveler, while per group uses a flat price for a group booking.",
649 "yatra",
650 )}
651 className="mb-2"
652 />
653 <Select
654 id="pricing_mode"
655 value={formData.pricing_mode}
656 onChange={(e) =>
657 handleFieldChange(
658 "pricing_mode",
659 e.target.value as "per_person" | "per_group",
660 )
661 }
662 >
663 <option value="per_person">
664 {__("Per person", "yatra")}
665 </option>
666 <option value="per_group">
667 {__("Per group", "yatra")}
668 </option>
669 </Select>
670
671 {formData.pricing_mode === "per_group" && (
672 <div className="mt-3 space-y-2">
673 <HelpText
674 text={__(
675 "Optional group size limits for this category. These are used to validate bookings when this category is priced per group.",
676 "yatra",
677 )}
678 className="mb-1"
679 />
680 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
681 <div>
682 <label
683 htmlFor="min_pax"
684 className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5"
685 >
686 {__("Minimum Group Size", "yatra")}
687 </label>
688 <Input
689 id="min_pax"
690 type="number"
691 min="1"
692 value={formData.min_pax}
693 onChange={(e) =>
694 handleFieldChange("min_pax", e.target.value)
695 }
696 placeholder={__("e.g., 10", "yatra")}
697 className={errors.min_pax ? "border-red-500" : ""}
698 />
699 {errors.min_pax && (
700 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
701 <AlertCircle className="w-4 h-4" />
702 {errors.min_pax}
703 </p>
704 )}
705 </div>
706 <div>
707 <label
708 htmlFor="max_pax"
709 className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5"
710 >
711 {__("Maximum Group Size", "yatra")}
712 </label>
713 <Input
714 id="max_pax"
715 type="number"
716 min="1"
717 value={formData.max_pax}
718 onChange={(e) =>
719 handleFieldChange("max_pax", e.target.value)
720 }
721 placeholder={__("e.g., 6 (optional)", "yatra")}
722 className={errors.max_pax ? "border-red-500" : ""}
723 />
724 {errors.max_pax && (
725 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
726 <AlertCircle className="w-4 h-4" />
727 {errors.max_pax}
728 </p>
729 )}
730 </div>
731 </div>
732 </div>
733 )}
734 </div>
735 </CardContent>
736 </Card>
737 </div>
738
739 {/* Sidebar */}
740 <div className="space-y-3">
741 {/* Icon/Image Picker */}
742 <Card>
743 <CardHeader className="pb-2">
744 <CardTitle className="text-base">
745 {__("Category Icon or Image", "yatra")}
746 </CardTitle>
747 </CardHeader>
748 <CardContent className="space-y-3">
749 <IconPicker
750 value={formData.icon}
751 onChange={(value) => handleFieldChange("icon", value)}
752 label={undefined}
753 helpText={__(
754 "Select an icon from the library or upload a custom image for this category.",
755 "yatra",
756 )}
757 allowImageUpload={true}
758 allowIconSelection={true}
759 size="md"
760 />
761 </CardContent>
762 </Card>
763
764 {/* Status */}
765 <Card>
766 <CardHeader className="pb-2">
767 <CardTitle className="text-base">
768 {__("Status", "yatra")}
769 </CardTitle>
770 </CardHeader>
771 <CardContent className="space-y-3">
772 <div>
773 <label
774 htmlFor="status"
775 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
776 >
777 {__("Status", "yatra")}
778 </label>
779 <HelpText
780 text={__(
781 "Draft categories will not be available when creating trips",
782 "yatra",
783 )}
784 className="mb-2"
785 />
786 <Select
787 id="status"
788 value={formData.status}
789 onChange={(e) =>
790 handleFieldChange(
791 "status",
792 e.target.value as "draft" | "publish" | "trash",
793 )
794 }
795 >
796 <option value="draft">{__("Draft", "yatra")}</option>
797 <option value="publish">{__("Publish", "yatra")}</option>
798 <option value="trash">{__("Trash", "yatra")}</option>
799 </Select>
800 </div>
801 </CardContent>
802 </Card>
803
804 {/* Submit Actions */}
805 <Card>
806 <CardContent className="p-3">
807 <div className="space-y-2">
808 <div className="flex gap-2">
809 <Button
810 type="submit"
811 disabled={isSubmitting}
812 className="flex-1 flex items-center justify-center gap-2"
813 >
814 {isSubmitting ? (
815 <>
816 <Loader2 className="w-4 h-4 animate-spin" />
817 {__("Saving...", "yatra")}...
818 </>
819 ) : (
820 <>
821 <Save className="w-4 h-4" />
822 {isEditMode
823 ? __("Update Category", "yatra")
824 : __("Create Category", "yatra")}
825 </>
826 )}
827 </Button>
828 <Button
829 type="button"
830 variant="outline"
831 onClick={handleCancel}
832 disabled={isSubmitting}
833 >
834 {__("Cancel", "yatra")}
835 </Button>
836 </div>
837 </div>
838 </CardContent>
839 </Card>
840 </div>
841 </div>
842 </form>
843 </ConditionalRender>
844 </div>
845 );
846 };
847
848 export default TravelerCategoryForm;
849