PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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 / components / trip-form / shared / ItineraryEntryFields.tsx

ItineraryEntryFields.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.8, at resources/js/components/trip-form/shared/ItineraryEntryFields.tsx

1,343 lines 48.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Shared Itinerary Entry Fields Component
3 * Reusable form fields for itinerary entries - used by both ItineraryForm and ItinerarySection
4 */
5
6 import React, { useState, useEffect } from "react";
7 import { Clock, X, Plus, Info } from "lucide-react";
8 import { __ } from "../../../lib/i18n";
9 import { ItineraryEntry, MediaItem } from "../types";
10 import { Input } from "../../ui/input";
11 import { Select } from "../../ui/select";
12 import { Button } from "../../ui/button";
13 import { Badge } from "../../ui/badge";
14 import { HelpText } from "../../ui/help-text";
15 import { Card, CardContent } from "../../ui/card";
16 import { TimePicker } from "../../ui/time-picker";
17 import { MediaUpload } from "../../ui/media-upload";
18 import { Modal } from "../../ui/modal";
19 import { getCurrencySymbol } from "../../../data/currencies";
20 import { LocationPicker } from "../LocationPicker";
21
22 interface ItineraryEntryFieldsProps {
23 entry: Partial<ItineraryEntry>;
24 errors?: Record<string, string>;
25 itemTypes: Array<{ id: number; name: string; icon?: string }>;
26 items: Array<{ id: number; name: string; type_id: number }>;
27 newIncludedItem?: string;
28 newExcludedItem?: string;
29 onFieldChange: (field: keyof ItineraryEntry, value: any) => void;
30 onIncludedItemChange?: (value: string) => void;
31 onExcludedItemChange?: (value: string) => void;
32 onAddIncludedItem?: () => void;
33 onAddExcludedItem?: () => void;
34 onRemoveIncludedItem?: (index: number) => void;
35 onRemoveExcludedItem?: (index: number) => void;
36 calculateDuration?: (
37 startTime: string,
38 endTime: string,
39 timeType: string,
40 ) => string;
41 size?: "default" | "compact";
42 showCardWrapper?: boolean;
43 onRefreshData?: () => void; // Callback to refresh item types and items after creation
44 }
45
46 export const ItineraryEntryFields: React.FC<ItineraryEntryFieldsProps> = ({
47 entry,
48 errors = {},
49 itemTypes,
50 items,
51 newIncludedItem = "",
52 newExcludedItem = "",
53 onFieldChange,
54 onIncludedItemChange,
55 onExcludedItemChange,
56 onAddIncludedItem,
57 onAddExcludedItem,
58 onRemoveIncludedItem,
59 onRemoveExcludedItem,
60 calculateDuration,
61 size = "default",
62 showCardWrapper = false,
63 onRefreshData,
64 }) => {
65 // Local state for gallery (workaround for parent state issue)
66 const [localGallery, setLocalGallery] = React.useState<MediaItem[]>([]);
67
68 // Local state for video_url (workaround for parent state issue)
69 const [localVideoUrl, setLocalVideoUrl] = React.useState<string>("");
70
71 // Sync local state with entry.gallery changes (including initial load)
72 React.useEffect(() => {
73 if (entry.gallery && Array.isArray(entry.gallery)) {
74 setLocalGallery(entry.gallery);
75 } else {
76 setLocalGallery([]);
77 }
78 }, [entry.gallery]);
79
80 // Sync local state with entry.video_url changes (including initial load)
81 React.useEffect(() => {
82 if (entry.video_url !== undefined && entry.video_url !== null) {
83 setLocalVideoUrl(entry.video_url);
84 } else {
85 setLocalVideoUrl("");
86 }
87 }, [entry.video_url]);
88
89 // Modal states
90 const [isItemTypeModalOpen, setIsItemTypeModalOpen] = useState(false);
91 const [isItemModalOpen, setIsItemModalOpen] = useState(false);
92
93 const isCompact = size === "compact";
94 const textSize = isCompact ? "text-xs" : "text-sm";
95 const labelSize = isCompact ? "text-xs" : "text-sm";
96 const autoDuration = calculateDuration
97 ? calculateDuration(
98 entry.start_time || "",
99 entry.end_time || "",
100 entry.time_type || "exact",
101 )
102 : "";
103
104 // Global currency symbol (same logic as other admin pages)
105 const globalCurrency = (window as any).yatraAdmin?.currency || "USD";
106 const currencySymbol = getCurrencySymbol(globalCurrency) || "";
107
108 const renderField = (content: React.ReactNode) => {
109 if (showCardWrapper) {
110 return (
111 <Card>
112 <CardContent className="space-y-3">{content}</CardContent>
113 </Card>
114 );
115 }
116 return <div className="space-y-3">{content}</div>;
117 };
118
119 const content = (
120 <>
121 {/* Basic Information */}
122 <div className="space-y-3">
123 {!isCompact && (
124 <h5
125 className={`${labelSize} font-semibold text-gray-900 dark:text-white`}
126 >
127 {__("Basic Information", "yatra")}
128 </h5>
129 )}
130
131 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
132 <div>
133 <label
134 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
135 >
136 {__("Item Type", "yatra")} <span className="text-red-500">*</span>
137 </label>
138 <HelpText
139 text={__(
140 "Select the type of item (Activity, Meal, Accommodation, etc.).",
141 "yatra",
142 )}
143 className="mb-2"
144 />
145 <div className="flex gap-2">
146 <Select
147 value={
148 entry.item_type_id && Number(entry.item_type_id) !== 0
149 ? entry.item_type_id
150 : ""
151 }
152 onChange={(e) => {
153 onFieldChange("item_type_id", e.target.value);
154 // Reset item_id when type changes
155 onFieldChange("item_id", "");
156 }}
157 className={`${errors.item_type_id ? "border-red-500" : ""} ${textSize} text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 flex-1`}
158 style={{
159 color: "rgb(17, 24, 39)",
160 backgroundColor: "rgb(255, 255, 255)",
161 }}
162 required
163 >
164 <option value="">{__("Select a type...", "yatra")}</option>
165 {itemTypes.map((type: any) => (
166 <option key={type.id} value={String(type.id)}>
167 {type.name}
168 </option>
169 ))}
170 </Select>
171 <Button
172 type="button"
173 variant="outline"
174 size="sm"
175 onClick={() => setIsItemTypeModalOpen(true)}
176 className="flex items-center gap-1 whitespace-nowrap"
177 title={__("Add new item type", "yatra")}
178 >
179 <Plus className="w-4 h-4" />
180 {isCompact ? "" : __("Add Type", "yatra")}
181 </Button>
182 </div>
183 {errors.item_type_id && (
184 <p
185 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
186 >
187 <Info className="w-4 h-4" />
188 {errors.item_type_id}
189 </p>
190 )}
191 </div>
192
193 <div>
194 <label
195 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
196 >
197 {__("Item", "yatra")} <span className="text-red-500">*</span>
198 </label>
199 <HelpText
200 text={__(
201 "Select the specific item (Hiking, Lunch, Bus, etc.).",
202 "yatra",
203 )}
204 className="mb-2"
205 />
206 <div className="flex gap-2">
207 <Select
208 value={
209 entry.item_id && Number(entry.item_id) !== 0
210 ? entry.item_id
211 : ""
212 }
213 onChange={(e) => onFieldChange("item_id", e.target.value)}
214 disabled={
215 !entry.item_type_id ||
216 items.filter((item: any) => {
217 const itemTypeId = entry.item_type_id
218 ? String(entry.item_type_id)
219 : "";
220 const itemTypeIdNum = item.type_id || item.item_type_id;
221 return String(itemTypeIdNum) === itemTypeId;
222 }).length === 0
223 }
224 className={`${errors.item_id ? "border-red-500" : ""} ${textSize} text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 flex-1`}
225 style={{
226 color: "rgb(17, 24, 39)",
227 backgroundColor: "rgb(255, 255, 255)",
228 }}
229 required
230 >
231 <option value="">
232 {!entry.item_type_id
233 ? __("Select type first...", "yatra")
234 : items.filter((item: any) => {
235 // Filter items by the entry's item_type_id
236 const itemTypeId = entry.item_type_id
237 ? String(entry.item_type_id)
238 : "";
239 const itemTypeIdNum =
240 item.type_id || item.item_type_id;
241 return String(itemTypeIdNum) === itemTypeId;
242 }).length === 0
243 ? __("No items available", "yatra")
244 : __("Select an item...", "yatra")}
245 </option>
246 {items
247 .filter((item: any) => {
248 // Filter items by the entry's item_type_id
249 if (!entry.item_type_id) return false;
250 const itemTypeId = String(entry.item_type_id);
251 const itemTypeIdNum = item.type_id || item.item_type_id;
252 return String(itemTypeIdNum) === itemTypeId;
253 })
254 .map((item: any) => (
255 <option key={item.id} value={String(item.id)}>
256 {item.name}
257 </option>
258 ))}
259 </Select>
260 <Button
261 type="button"
262 variant="outline"
263 size="sm"
264 onClick={() => setIsItemModalOpen(true)}
265 disabled={!entry.item_type_id}
266 className="flex items-center gap-1 whitespace-nowrap"
267 title={__("Add new item", "yatra")}
268 >
269 <Plus className="w-4 h-4" />
270 {isCompact ? "" : __("Add Item", "yatra")}
271 </Button>
272 </div>
273 {errors.item_id && (
274 <p
275 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
276 >
277 <Info className="w-4 h-4" />
278 {errors.item_id}
279 </p>
280 )}
281 </div>
282 </div>
283
284 <div>
285 <label
286 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
287 >
288 {__("Itinerary Activity Title", "yatra")}{" "}
289 <span className="text-red-500">*</span>
290 </label>
291 <HelpText
292 text={__(
293 'A descriptive title for this itinerary activity. Examples: "Arrival in Kathmandu", "Trek to Base Camp".',
294 "yatra",
295 )}
296 className="mb-2"
297 />
298 <Input
299 type="text"
300 value={entry.title || ""}
301 onChange={(e) => onFieldChange("title", e.target.value)}
302 placeholder={__("e.g., Arrival in Kathmandu", "yatra")}
303 className={`${errors.title ? "border-red-500" : ""} ${textSize}`}
304 required
305 />
306 {errors.title && (
307 <p
308 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
309 >
310 <Info className="w-4 h-4" />
311 {errors.title}
312 </p>
313 )}
314 </div>
315
316 <div>
317 <label
318 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
319 >
320 {__("Description", "yatra")}
321 </label>
322 <HelpText
323 text={__(
324 "Detailed description of what happens during this itinerary entry.",
325 "yatra",
326 )}
327 className="mb-2"
328 />
329 <textarea
330 value={entry.description || ""}
331 onChange={(e) => onFieldChange("description", e.target.value)}
332 placeholder={__(
333 "Describe what happens during this entry...",
334 "yatra",
335 )}
336 rows={isCompact ? 2 : 4}
337 className={`flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 ${textSize} ring-offset-white 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`}
338 />
339 </div>
340 </div>
341
342 {/* Location & Time */}
343 <div
344 className={`space-y-3 ${!isCompact ? "pt-3 border-t border-gray-200 dark:border-gray-700" : ""}`}
345 >
346 {!isCompact && (
347 <h5
348 className={`${labelSize} font-semibold text-gray-900 dark:text-white`}
349 >
350 {__("Location & Time", "yatra")}
351 </h5>
352 )}
353
354 <div>
355 <label
356 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
357 >
358 {__("Activity Location", "yatra")}
359 </label>
360 <HelpText
361 text={__(
362 "Where does this activity take place? You can search for a location or enter coordinates manually.",
363 "yatra",
364 )}
365 className="mb-2"
366 />
367 <LocationPicker
368 value={{
369 name: entry.location || "",
370 latitude: entry.location_latitude || "",
371 longitude: entry.location_longitude || "",
372 }}
373 onChange={(locationData) => {
374 onFieldChange("location", locationData.name);
375 onFieldChange("location_latitude", locationData.latitude);
376 onFieldChange("location_longitude", locationData.longitude);
377 }}
378 label=""
379 placeholder={__("Search for location...", "yatra")}
380 helpText=""
381 required={false}
382 defaultMapCenter={
383 entry.location_latitude && entry.location_longitude
384 ? [
385 parseFloat(entry.location_latitude),
386 parseFloat(entry.location_longitude),
387 ]
388 : [20, 0]
389 }
390 defaultZoom={
391 entry.location_latitude && entry.location_longitude ? 13 : 2
392 }
393 mapHeight="300px"
394 showMapButton={false}
395 searchLimit={8}
396 __={__}
397 className=""
398 mapClassName="rounded-lg"
399 />
400 </div>
401
402 {/* GPS Coordinates - Manual Override */}
403 <div className="space-y-3">
404 <div className="flex items-center justify-between">
405 <label
406 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2`}
407 >
408 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
409 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
410 2
411 </span>
412 </div>
413 {__("GPS Coordinates", "yatra")}
414 <span className="text-xs text-gray-500 dark:text-gray-400">
415 ({__("Manual override", "yatra")})
416 </span>
417 </label>
418 <div className="flex items-center gap-2">
419 <button
420 type="button"
421 onClick={() => {
422 if (navigator.geolocation) {
423 navigator.geolocation.getCurrentPosition(
424 (position) => {
425 onFieldChange(
426 "location_latitude",
427 position.coords.latitude.toString(),
428 );
429 onFieldChange(
430 "location_longitude",
431 position.coords.longitude.toString(),
432 );
433 // Also update location name with coordinates
434 onFieldChange(
435 "location",
436 `${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`,
437 );
438 },
439 (error) => {
440 let message = __(
441 "Unable to get your location",
442 "yatra",
443 );
444 let showHttpsNotice = false;
445
446 switch (error.code) {
447 case 1: // PERMISSION_DENIED
448 if (error.message.includes("secure origins")) {
449 message = __(
450 "Location access requires HTTPS. This feature will work on your live HTTPS site.",
451 "yatra",
452 );
453 showHttpsNotice = true;
454 } else {
455 message = __(
456 "Location access denied. Please allow location access in your browser.",
457 "yatra",
458 );
459 }
460 break;
461 case 2: // POSITION_UNAVAILABLE
462 message = __(
463 "Location information is unavailable. Please try again.",
464 "yatra",
465 );
466 break;
467 case 3: // TIMEOUT
468 message = __(
469 "Location request timed out. Please try again.",
470 "yatra",
471 );
472 break;
473 }
474
475 if (showHttpsNotice) {
476 alert(
477 message +
478 "\n\n" +
479 __(
480 "For local development, you can:\n1. Use a browser extension that allows geolocation on HTTP\n2. Set up a local HTTPS certificate\n3. Test on your live HTTPS site",
481 "yatra",
482 ),
483 );
484 } else {
485 alert(message);
486 }
487 },
488 );
489 } else {
490 alert(
491 __(
492 "Geolocation is not supported by your browser",
493 "yatra",
494 ),
495 );
496 }
497 }}
498 className="text-xs bg-green-100 text-green-700 px-3 py-1.5 rounded-lg hover:bg-green-200 transition-colors flex items-center gap-1"
499 >
500 <div className="w-3 h-3">
501 <svg fill="currentColor" viewBox="0 0 20 20">
502 <path
503 fillRule="evenodd"
504 d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
505 clipRule="evenodd"
506 />
507 </svg>
508 </div>
509 {__("Use Current Location", "yatra")}
510 </button>
511 </div>
512 </div>
513
514 <div className="grid grid-cols-2 gap-3">
515 <div className="space-y-2">
516 <label
517 className={`text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1`}
518 >
519 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
520 {__("Latitude", "yatra")}
521 </label>
522 <Input
523 type="text"
524 value={entry.location_latitude || ""}
525 onChange={(e) =>
526 onFieldChange("location_latitude", e.target.value)
527 }
528 placeholder={__("e.g., -8.3405", "yatra")}
529 className={`w-full text-sm`}
530 />
531 </div>
532 <div className="space-y-2">
533 <label
534 className={`text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1`}
535 >
536 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
537 {__("Longitude", "yatra")}
538 </label>
539 <Input
540 type="text"
541 value={entry.location_longitude || ""}
542 onChange={(e) =>
543 onFieldChange("location_longitude", e.target.value)
544 }
545 placeholder={__("e.g., 115.0920", "yatra")}
546 className={`w-full text-sm`}
547 />
548 </div>
549 </div>
550 <p className={`text-xs text-gray-500 dark:text-gray-400`}>
551 {__(
552 "Manual coordinate entry. These will be auto-filled when you select a location from the map above.",
553 "yatra",
554 )}
555 </p>
556 </div>
557
558 <div>
559 <label
560 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
561 >
562 {__("Time Type", "yatra")}
563 </label>
564 <HelpText
565 text={__("How should the time be displayed?", "yatra")}
566 className="mb-2"
567 />
568 <Select
569 value={entry.time_type || "exact"}
570 onChange={(e) => onFieldChange("time_type", e.target.value)}
571 className={textSize}
572 >
573 <option value="exact">{__("Exact Time", "yatra")}</option>
574 <option value="duration">{__("Duration Only", "yatra")}</option>
575 <option value="flexible">{__("Flexible", "yatra")}</option>
576 </Select>
577 </div>
578
579 {entry.time_type === "exact" && (
580 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
581 <div>
582 <label
583 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
584 >
585 {__("Start Time", "yatra")}
586 </label>
587 <HelpText
588 text={__("When does this activity start?", "yatra")}
589 className="mb-2"
590 />
591 <TimePicker
592 value={entry.start_time || ""}
593 onChange={(value) => {
594 onFieldChange("start_time", value);
595 // Auto-calculate duration if function provided
596 if (calculateDuration && entry.end_time) {
597 const newDuration = calculateDuration(
598 value,
599 entry.end_time,
600 entry.time_type || "exact",
601 );
602 if (newDuration && !entry.duration) {
603 onFieldChange("duration", newDuration);
604 }
605 }
606 }}
607 placeholder="08:00"
608 className="w-full"
609 error={!!errors?.start_time}
610 />
611 {errors.start_time && (
612 <p
613 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
614 >
615 <Info className="w-4 h-4" />
616 {errors.start_time}
617 </p>
618 )}
619 </div>
620
621 <div>
622 <label
623 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
624 >
625 {__("End Time", "yatra")}
626 </label>
627 <HelpText
628 text={__("When does this activity end?", "yatra")}
629 className="mb-2"
630 />
631 <TimePicker
632 value={entry.end_time || ""}
633 onChange={(value) => {
634 onFieldChange("end_time", value);
635 // Auto-calculate duration if function provided
636 if (calculateDuration && entry.start_time) {
637 const newDuration = calculateDuration(
638 entry.start_time,
639 value,
640 entry.time_type || "exact",
641 );
642 if (newDuration && !entry.duration) {
643 onFieldChange("duration", newDuration);
644 }
645 }
646 }}
647 placeholder="17:00"
648 className="w-full"
649 error={!!errors?.end_time}
650 />
651 {errors.end_time && (
652 <p
653 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
654 >
655 <Info className="w-4 h-4" />
656 {errors.end_time}
657 </p>
658 )}
659 </div>
660 </div>
661 )}
662
663 <div>
664 <label
665 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
666 >
667 {__("Duration", "yatra")}
668 </label>
669 <HelpText
670 text={__(
671 'How long does this activity take? (e.g., "3 hours", "1 hour 30 minutes"). Leave empty to auto-calculate from times.',
672 "yatra",
673 )}
674 className="mb-2"
675 />
676 <div className="relative">
677 <Clock className="absolute left-2.5 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
678 <Input
679 type="text"
680 value={entry.duration || ""}
681 onChange={(e) => onFieldChange("duration", e.target.value)}
682 placeholder={autoDuration || __("e.g., 3 hours", "yatra")}
683 className={`pl-9 ${textSize}`}
684 />
685 </div>
686 {autoDuration && entry.time_type === "exact" && !entry.duration && (
687 <p
688 className={`mt-1.5 ${isCompact ? "text-xs" : "text-xs"} text-gray-500 dark:text-gray-400`}
689 >
690 {__("Auto-calculated:", "yatra")} {autoDuration}
691 </p>
692 )}
693 </div>
694 </div>
695
696 {/* Cost & Additional Information */}
697 <div
698 className={`space-y-3 ${!isCompact ? "pt-3 border-t border-gray-200 dark:border-gray-700" : ""}`}
699 >
700 {!isCompact && (
701 <h5
702 className={`${labelSize} font-semibold text-gray-900 dark:text-white`}
703 >
704 {__("Cost and Additional Information", "yatra")}
705 </h5>
706 )}
707
708 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
709 <div>
710 <label
711 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
712 >
713 {__("Cost", "yatra")}
714 </label>
715 <HelpText
716 text={__(
717 "Optional cost for this activity. Leave empty if included in trip price.",
718 "yatra",
719 )}
720 className="mb-2"
721 />
722 <div className="relative">
723 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
724 {currencySymbol}
725 </span>
726 <Input
727 type="number"
728 min="0"
729 step="0.01"
730 value={entry.cost || ""}
731 onChange={(e) => onFieldChange("cost", e.target.value)}
732 placeholder="0.00"
733 className={`pl-8 ${textSize}`}
734 />
735 </div>
736 </div>
737
738 <div className="flex items-end">
739 <label className="flex items-center gap-2 cursor-pointer">
740 <input
741 type="checkbox"
742 checked={entry.cost_per_person !== false}
743 onChange={(e) =>
744 onFieldChange("cost_per_person", e.target.checked)
745 }
746 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
747 />
748 <span className={`${textSize} text-gray-700 dark:text-gray-300`}>
749 {__("Cost per person", "yatra")}
750 </span>
751 </label>
752 </div>
753 </div>
754
755 <div>
756 <label
757 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
758 >
759 {__("Notes / Instructions", "yatra")}
760 </label>
761 <HelpText
762 text={__(
763 "Additional notes or special instructions for this activity.",
764 "yatra",
765 )}
766 className="mb-2"
767 />
768 <textarea
769 value={entry.notes || ""}
770 onChange={(e) => onFieldChange("notes", e.target.value)}
771 placeholder={__(
772 "e.g., Please arrive 15 minutes early, Bring comfortable shoes...",
773 "yatra",
774 )}
775 rows={isCompact ? 2 : 3}
776 className={`flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 ${textSize} ring-offset-white 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`}
777 />
778 </div>
779
780 {/* Included/Excluded Items */}
781 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
782 <div>
783 <label
784 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-2`}
785 >
786 {__("Included Items", "yatra")}
787 </label>
788 <HelpText
789 text={__("Items or services included in this activity.", "yatra")}
790 className="mb-2"
791 />
792 <div className="space-y-2">
793 <div className="flex gap-2">
794 <Input
795 type="text"
796 value={newIncludedItem}
797 onChange={(e) => onIncludedItemChange?.(e.target.value)}
798 onKeyPress={(e) => {
799 if (e.key === "Enter") {
800 e.preventDefault();
801 onAddIncludedItem?.();
802 }
803 }}
804 placeholder={__("Add included item...", "yatra")}
805 className={`flex-1 ${textSize}`}
806 />
807 <Button
808 type="button"
809 variant="outline"
810 size="icon"
811 onClick={(e) => {
812 e.preventDefault();
813 e.stopPropagation();
814 if (newIncludedItem.trim()) {
815 onAddIncludedItem?.();
816 }
817 }}
818 disabled={!newIncludedItem.trim()}
819 className={`flex-shrink-0 ${isCompact ? "h-9 w-9" : ""}`}
820 >
821 <Plus className="w-4 h-4" />
822 </Button>
823 </div>
824 <div className="flex flex-wrap gap-2">
825 {(entry.included_items || []).map((item, index) => (
826 <Badge
827 key={index}
828 variant="info"
829 className={`flex items-center gap-1.5 ${textSize}`}
830 >
831 {item}
832 <button
833 type="button"
834 onClick={() => onRemoveIncludedItem?.(index)}
835 className="ml-1 hover:text-red-600"
836 >
837 <X className="w-3 h-3" />
838 </button>
839 </Badge>
840 ))}
841 </div>
842 </div>
843 </div>
844
845 <div>
846 <label
847 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-2`}
848 >
849 {__("Excluded Items", "yatra")}
850 </label>
851 <HelpText
852 text={__(
853 "Items or services NOT included in this activity.",
854 "yatra",
855 )}
856 className="mb-2"
857 />
858 <div className="space-y-2">
859 <div className="flex gap-2">
860 <Input
861 type="text"
862 value={newExcludedItem}
863 onChange={(e) => onExcludedItemChange?.(e.target.value)}
864 onKeyPress={(e) => {
865 if (e.key === "Enter") {
866 e.preventDefault();
867 onAddExcludedItem?.();
868 }
869 }}
870 placeholder={__("Add excluded item...", "yatra")}
871 className={`flex-1 ${textSize}`}
872 />
873 <Button
874 type="button"
875 variant="outline"
876 size="icon"
877 onClick={(e) => {
878 e.preventDefault();
879 e.stopPropagation();
880 if (newExcludedItem.trim()) {
881 onAddExcludedItem?.();
882 }
883 }}
884 disabled={!newExcludedItem.trim()}
885 className={`flex-shrink-0 ${isCompact ? "h-9 w-9" : ""}`}
886 >
887 <Plus className="w-4 h-4" />
888 </Button>
889 </div>
890 <div className="flex flex-wrap gap-2">
891 {(entry.excluded_items || []).map((item, index) => (
892 <Badge
893 key={index}
894 variant="default"
895 className={`flex items-center gap-1.5 ${textSize} border border-gray-300 dark:border-gray-600`}
896 >
897 {item}
898 <button
899 type="button"
900 onClick={() => onRemoveExcludedItem?.(index)}
901 className="ml-1 hover:text-red-600"
902 >
903 <X className="w-3 h-3" />
904 </button>
905 </Badge>
906 ))}
907 </div>
908 </div>
909 </div>
910
911 {/* Gallery Section */}
912 <div className="space-y-4">
913 <div>
914 <label
915 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
916 >
917 {__("Photo Gallery", "yatra")}
918 </label>
919 <HelpText
920 text={__("Add photos to showcase this activity", "yatra")}
921 className="mb-2"
922 />
923 <MediaUpload
924 items={localGallery}
925 onChange={(mediaItems) => {
926 setLocalGallery(mediaItems); // Update local state immediately
927 onFieldChange("gallery", mediaItems); // Still call parent for data persistence
928 }}
929 maxItems={6}
930 acceptTypes="images"
931 title={__("Activity Photos", "yatra")}
932 description={__(
933 "Upload photos to showcase this activity",
934 "yatra",
935 )}
936 className="w-full"
937 />
938 </div>
939 </div>
940
941 {/* Video Section */}
942 <div className="space-y-4">
943 <div>
944 <label
945 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
946 >
947 {__("Video URL", "yatra")}
948 </label>
949 <HelpText
950 text={__(
951 "Add a video URL to showcase this activity (YouTube, Vimeo, etc.)",
952 "yatra",
953 )}
954 className="mb-2"
955 />
956 <input
957 type="text"
958 value={localVideoUrl}
959 onChange={(e) => {
960 setLocalVideoUrl(e.target.value); // Update local state immediately
961 onFieldChange("video_url", e.target.value); // Still call parent for data persistence
962 }}
963 placeholder={__("https://youtube.com/watch?v=...", "yatra")}
964 className={`flex h-11 w-full rounded-md border-2 border-gray-300 bg-white px-4 py-2.5 text-base font-normal text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 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:text-gray-100 dark:ring-offset-gray-900 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 transition-colors ${textSize}`}
965 />
966 {errors.video_url && (
967 <p
968 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
969 >
970 <Info className="w-4 h-4" />
971 {errors.video_url}
972 </p>
973 )}
974 </div>
975 </div>
976 </div>
977 </div>
978
979 {/* Item Type Creation Modal */}
980 <Modal
981 isOpen={isItemTypeModalOpen}
982 onClose={() => setIsItemTypeModalOpen(false)}
983 title={__("Add New Item Type", "yatra")}
984 description={__(
985 "Create a new category for organizing itinerary items",
986 "yatra",
987 )}
988 size="xl"
989 >
990 <ItemTypeFormContent
991 onSuccess={(newItemType) => {
992 setIsItemTypeModalOpen(false);
993
994 // Select the newly created item type
995 const itemTypeId = newItemType?.data?.id || newItemType?.id;
996 if (itemTypeId) {
997 onFieldChange("item_type_id", itemTypeId.toString());
998 }
999
1000 // Reset item selection
1001 onFieldChange("item_id", "");
1002 // Refresh data to update dropdowns
1003 onRefreshData?.();
1004 }}
1005 onCancel={() => setIsItemTypeModalOpen(false)}
1006 />
1007 </Modal>
1008
1009 {/* Item Creation Modal */}
1010 <Modal
1011 isOpen={isItemModalOpen}
1012 onClose={() => setIsItemModalOpen(false)}
1013 title={__("Add New Item", "yatra")}
1014 description={__(
1015 "Create a new item for the selected item type",
1016 "yatra",
1017 )}
1018 size="xl"
1019 >
1020 <ItemFormContent
1021 selectedTypeId={entry.item_type_id}
1022 onSuccess={(newItem) => {
1023 setIsItemModalOpen(false);
1024
1025 // Select the newly created item
1026 const itemId = newItem?.data?.id || newItem?.id;
1027 if (itemId) {
1028 onFieldChange("item_id", itemId.toString());
1029 }
1030
1031 // Refresh data to update dropdowns
1032 onRefreshData?.();
1033 }}
1034 onCancel={() => setIsItemModalOpen(false)}
1035 />
1036 </Modal>
1037 </>
1038 );
1039
1040 return renderField(content);
1041 };
1042
1043 // Minimal Item Type Form - Uses exact same API as main ItemTypeForm
1044 interface ItemTypeFormContentProps {
1045 onSuccess: (itemType: any) => void;
1046 onCancel: () => void;
1047 }
1048
1049 const ItemTypeFormContent: React.FC<ItemTypeFormContentProps> = ({
1050 onSuccess,
1051 onCancel,
1052 }) => {
1053 const [formData, setFormData] = useState({
1054 name: "",
1055 slug: "",
1056 description: "",
1057 icon: null,
1058 color: "blue",
1059 status: "publish",
1060 });
1061 const [errors, setErrors] = useState<Record<string, string>>({});
1062 const [isSubmitting, setIsSubmitting] = useState(false);
1063
1064 // Auto-generate slug from name (same as main form)
1065 const handleNameChange = (value: string) => {
1066 // Unicode-aware so Cyrillic / CJK / Devanagari names produce real slugs
1067 // instead of empty strings. \p{L}=letter, \p{N}=digit with the `u` flag.
1068 const slug = value
1069 .toLowerCase()
1070 .replace(/[^\p{L}\p{N}-]+/gu, "-")
1071 .replace(/-+/g, "-")
1072 .replace(/^-+|-+$/g, "");
1073 setFormData((prev) => ({
1074 ...prev,
1075 name: value,
1076 slug: slug,
1077 }));
1078 if (errors.name) {
1079 setErrors((prev) => ({ ...prev, name: "" }));
1080 }
1081 };
1082
1083 const handleSubmit = async (e: React.FormEvent) => {
1084 e.preventDefault();
1085 setIsSubmitting(true);
1086
1087 try {
1088 // Use exact same API endpoint and structure as main ItemTypeForm
1089 const response = await fetch("/wp-json/yatra/v1/item-types", {
1090 method: "POST",
1091 headers: {
1092 "Content-Type": "application/json",
1093 "X-WP-Nonce": (window as any).wpApiSettings?.nonce || "",
1094 },
1095 body: JSON.stringify(formData),
1096 });
1097
1098 if (response.ok) {
1099 const newItemType = await response.json();
1100 onSuccess(newItemType);
1101 } else {
1102 const errorData = await response.json();
1103 setErrors(
1104 errorData.errors || { general: "Failed to create item type" },
1105 );
1106 }
1107 } catch (error) {
1108 setErrors({ general: "An error occurred while creating the item type" });
1109 } finally {
1110 setIsSubmitting(false);
1111 }
1112 };
1113
1114 return (
1115 <form onSubmit={handleSubmit} className="space-y-4">
1116 <div>
1117 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1118 {__("Type Name", "yatra")} <span className="text-red-500">*</span>
1119 </label>
1120 <Input
1121 type="text"
1122 value={formData.name}
1123 onChange={(e) => handleNameChange(e.target.value)}
1124 placeholder={__("e.g., Activity, Meal, Accommodation", "yatra")}
1125 className={errors.name ? "border-red-500" : ""}
1126 required
1127 />
1128 {errors.name && (
1129 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
1130 {errors.name}
1131 </p>
1132 )}
1133 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
1134 {__(
1135 "Slug will be auto-generated. You can edit icon, color, and other options later in Item Types page",
1136 "yatra",
1137 )}
1138 </p>
1139 </div>
1140
1141 <div>
1142 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1143 {__("Description", "yatra")}
1144 </label>
1145 <textarea
1146 value={formData.description}
1147 onChange={(e) =>
1148 setFormData({ ...formData, description: e.target.value })
1149 }
1150 placeholder={__("Brief description of this item type...", "yatra")}
1151 rows={3}
1152 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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:text-white resize-none"
1153 />
1154 </div>
1155
1156 <div className="flex justify-end gap-2 pt-4 border-t dark:border-gray-700">
1157 <Button
1158 type="button"
1159 variant="outline"
1160 onClick={onCancel}
1161 disabled={isSubmitting}
1162 >
1163 {__("Cancel", "yatra")}
1164 </Button>
1165 <Button type="submit" disabled={isSubmitting}>
1166 {isSubmitting
1167 ? __("Creating...", "yatra")
1168 : __("Create Item Type", "yatra")}
1169 </Button>
1170 </div>
1171 </form>
1172 );
1173 };
1174
1175 // Minimal Item Form - Uses exact same API as main ItemForm
1176 interface ItemFormContentProps {
1177 selectedTypeId?: string;
1178 onSuccess: (item: any) => void;
1179 onCancel: () => void;
1180 }
1181
1182 const ItemFormContent: React.FC<ItemFormContentProps> = ({
1183 selectedTypeId,
1184 onSuccess,
1185 onCancel,
1186 }) => {
1187 // Hooks must run unconditionally on every render. The
1188 // "Item Type Required" gate that used to sit above these was
1189 // moved below so React's hook-order invariant holds even when
1190 // the gate flips between renders (e.g. parent selects a type
1191 // after the modal has mounted with no selection).
1192 const [formData, setFormData] = useState({
1193 name: "",
1194 slug: "",
1195 description: "",
1196 type_id: "",
1197 status: "publish",
1198 });
1199 const [errors, setErrors] = useState<Record<string, string>>({});
1200 const [isSubmitting, setIsSubmitting] = useState(false);
1201
1202 // Update formData when selectedTypeId changes
1203 useEffect(() => {
1204 setFormData((prev) => ({
1205 ...prev,
1206 type_id: selectedTypeId || "",
1207 }));
1208 }, [selectedTypeId]);
1209
1210 // Check if item type is selected (moved here from before the hook
1211 // calls above — see rules-of-hooks. The "Item Type Required" empty
1212 // state still renders the same UI).
1213 if (!selectedTypeId) {
1214 return (
1215 <div className="p-6 text-center">
1216 <div className="mb-4">
1217 <Info className="w-12 h-12 text-yellow-500 mx-auto" />
1218 </div>
1219 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
1220 {__("Item Type Required", "yatra")}
1221 </h3>
1222 <p className="text-gray-600 dark:text-gray-400 mb-4">
1223 {__(
1224 "Please select an Item Type first before creating an Item.",
1225 "yatra",
1226 )}
1227 </p>
1228 <Button type="button" variant="outline" onClick={onCancel}>
1229 {__("Close", "yatra")}
1230 </Button>
1231 </div>
1232 );
1233 }
1234
1235 // Auto-generate slug from name (same as main form)
1236 const handleNameChange = (value: string) => {
1237 // Unicode-aware so Cyrillic / CJK / Devanagari names produce real slugs
1238 // instead of empty strings. \p{L}=letter, \p{N}=digit with the `u` flag.
1239 const slug = value
1240 .toLowerCase()
1241 .replace(/[^\p{L}\p{N}-]+/gu, "-")
1242 .replace(/-+/g, "-")
1243 .replace(/^-+|-+$/g, "");
1244 setFormData((prev) => ({
1245 ...prev,
1246 name: value,
1247 slug: slug,
1248 }));
1249 if (errors.name) {
1250 setErrors((prev) => ({ ...prev, name: "" }));
1251 }
1252 };
1253
1254 const handleSubmit = async (e: React.FormEvent) => {
1255 e.preventDefault();
1256 setIsSubmitting(true);
1257
1258 try {
1259 // Use exact same API endpoint and structure as main ItemForm
1260 const response = await fetch("/wp-json/yatra/v1/items", {
1261 method: "POST",
1262 headers: {
1263 "Content-Type": "application/json",
1264 "X-WP-Nonce": (window as any).wpApiSettings?.nonce || "",
1265 },
1266 body: JSON.stringify(formData),
1267 });
1268
1269 if (response.ok) {
1270 const newItem = await response.json();
1271 onSuccess(newItem);
1272 } else {
1273 const errorData = await response.json();
1274 setErrors(errorData.errors || { general: "Failed to create item" });
1275 }
1276 } catch (error) {
1277 setErrors({ general: "An error occurred while creating the item" });
1278 } finally {
1279 setIsSubmitting(false);
1280 }
1281 };
1282
1283 return (
1284 <form onSubmit={handleSubmit} className="space-y-4">
1285 <div>
1286 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1287 {__("Item Name", "yatra")} <span className="text-red-500">*</span>
1288 </label>
1289 <Input
1290 type="text"
1291 value={formData.name}
1292 onChange={(e) => handleNameChange(e.target.value)}
1293 placeholder={__("e.g., Hiking, Lunch, Hotel", "yatra")}
1294 className={errors.name ? "border-red-500" : ""}
1295 required
1296 />
1297 {errors.name && (
1298 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
1299 {errors.name}
1300 </p>
1301 )}
1302 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
1303 {__(
1304 "Slug will be auto-generated. You can edit additional details later in Items page",
1305 "yatra",
1306 )}
1307 </p>
1308 </div>
1309
1310 <div>
1311 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1312 {__("Description", "yatra")}
1313 </label>
1314 <textarea
1315 value={formData.description}
1316 onChange={(e) =>
1317 setFormData({ ...formData, description: e.target.value })
1318 }
1319 placeholder={__("Brief description of this item...", "yatra")}
1320 rows={3}
1321 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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:text-white resize-none"
1322 />
1323 </div>
1324
1325 <div className="flex justify-end gap-2 pt-4 border-t dark:border-gray-700">
1326 <Button
1327 type="button"
1328 variant="outline"
1329 onClick={onCancel}
1330 disabled={isSubmitting}
1331 >
1332 {__("Cancel", "yatra")}
1333 </Button>
1334 <Button type="submit" disabled={isSubmitting}>
1335 {isSubmitting
1336 ? __("Creating...", "yatra")
1337 : __("Create Item", "yatra")}
1338 </Button>
1339 </div>
1340 </form>
1341 );
1342 };
1343