PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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.2.7, at resources/js/components/trip-form/shared/ItineraryEntryFields.tsx

1,325 lines 47.5 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 : [-8.3405, 115.092]
389 }
390 defaultZoom={13}
391 mapHeight="300px"
392 showMapButton={false}
393 searchLimit={8}
394 __={__}
395 className=""
396 mapClassName="rounded-lg"
397 />
398 </div>
399
400 {/* GPS Coordinates - Manual Override */}
401 <div className="space-y-3">
402 <div className="flex items-center justify-between">
403 <label
404 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2`}
405 >
406 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
407 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
408 2
409 </span>
410 </div>
411 {__("GPS Coordinates", "yatra")}
412 <span className="text-xs text-gray-500 dark:text-gray-400">
413 ({__("Manual override", "yatra")})
414 </span>
415 </label>
416 <div className="flex items-center gap-2">
417 <button
418 type="button"
419 onClick={() => {
420 if (navigator.geolocation) {
421 navigator.geolocation.getCurrentPosition(
422 (position) => {
423 onFieldChange(
424 "location_latitude",
425 position.coords.latitude.toString(),
426 );
427 onFieldChange(
428 "location_longitude",
429 position.coords.longitude.toString(),
430 );
431 // Also update location name with coordinates
432 onFieldChange(
433 "location",
434 `${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`,
435 );
436 },
437 (error) => {
438 let message = __(
439 "Unable to get your location",
440 "yatra",
441 );
442 let showHttpsNotice = false;
443
444 switch (error.code) {
445 case 1: // PERMISSION_DENIED
446 if (error.message.includes("secure origins")) {
447 message = __(
448 "Location access requires HTTPS. This feature will work on your live HTTPS site.",
449 "yatra",
450 );
451 showHttpsNotice = true;
452 } else {
453 message = __(
454 "Location access denied. Please allow location access in your browser.",
455 "yatra",
456 );
457 }
458 break;
459 case 2: // POSITION_UNAVAILABLE
460 message = __(
461 "Location information is unavailable. Please try again.",
462 "yatra",
463 );
464 break;
465 case 3: // TIMEOUT
466 message = __(
467 "Location request timed out. Please try again.",
468 "yatra",
469 );
470 break;
471 }
472
473 if (showHttpsNotice) {
474 alert(
475 message +
476 "\n\n" +
477 __(
478 "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",
479 "yatra",
480 ),
481 );
482 } else {
483 alert(message);
484 }
485 },
486 );
487 } else {
488 alert(
489 __(
490 "Geolocation is not supported by your browser",
491 "yatra",
492 ),
493 );
494 }
495 }}
496 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"
497 >
498 <div className="w-3 h-3">
499 <svg fill="currentColor" viewBox="0 0 20 20">
500 <path
501 fillRule="evenodd"
502 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"
503 clipRule="evenodd"
504 />
505 </svg>
506 </div>
507 {__("Use Current Location", "yatra")}
508 </button>
509 </div>
510 </div>
511
512 <div className="grid grid-cols-2 gap-3">
513 <div className="space-y-2">
514 <label
515 className={`text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1`}
516 >
517 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
518 {__("Latitude", "yatra")}
519 </label>
520 <Input
521 type="text"
522 value={entry.location_latitude || ""}
523 onChange={(e) =>
524 onFieldChange("location_latitude", e.target.value)
525 }
526 placeholder={__("e.g., -8.3405", "yatra")}
527 className={`w-full text-sm`}
528 />
529 </div>
530 <div className="space-y-2">
531 <label
532 className={`text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1`}
533 >
534 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
535 {__("Longitude", "yatra")}
536 </label>
537 <Input
538 type="text"
539 value={entry.location_longitude || ""}
540 onChange={(e) =>
541 onFieldChange("location_longitude", e.target.value)
542 }
543 placeholder={__("e.g., 115.0920", "yatra")}
544 className={`w-full text-sm`}
545 />
546 </div>
547 </div>
548 <p className={`text-xs text-gray-500 dark:text-gray-400`}>
549 {__(
550 "Manual coordinate entry. These will be auto-filled when you select a location from the map above.",
551 "yatra",
552 )}
553 </p>
554 </div>
555
556 <div>
557 <label
558 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
559 >
560 {__("Time Type", "yatra")}
561 </label>
562 <HelpText
563 text={__("How should the time be displayed?", "yatra")}
564 className="mb-2"
565 />
566 <Select
567 value={entry.time_type || "exact"}
568 onChange={(e) => onFieldChange("time_type", e.target.value)}
569 className={textSize}
570 >
571 <option value="exact">{__("Exact Time", "yatra")}</option>
572 <option value="approximate">
573 {__("Approximate Time", "yatra")}
574 </option>
575 <option value="all_day">{__("All Day", "yatra")}</option>
576 <option value="flexible">{__("Flexible", "yatra")}</option>
577 </Select>
578 </div>
579
580 {entry.time_type !== "all_day" && (
581 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
582 <div>
583 <label
584 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
585 >
586 {__("Start Time", "yatra")}
587 </label>
588 <HelpText
589 text={__("When does this activity start?", "yatra")}
590 className="mb-2"
591 />
592 <TimePicker
593 value={entry.start_time || ""}
594 onChange={(value) => {
595 onFieldChange("start_time", value);
596 // Auto-calculate duration if function provided
597 if (calculateDuration && entry.end_time) {
598 const newDuration = calculateDuration(
599 value,
600 entry.end_time,
601 entry.time_type || "exact",
602 );
603 if (newDuration && !entry.duration) {
604 onFieldChange("duration", newDuration);
605 }
606 }
607 }}
608 placeholder="08:00"
609 className="w-full"
610 error={!!errors?.start_time}
611 />
612 {errors.start_time && (
613 <p
614 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
615 >
616 <Info className="w-4 h-4" />
617 {errors.start_time}
618 </p>
619 )}
620 </div>
621
622 <div>
623 <label
624 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
625 >
626 {__("End Time", "yatra")}
627 </label>
628 <HelpText
629 text={__("When does this activity end?", "yatra")}
630 className="mb-2"
631 />
632 <TimePicker
633 value={entry.end_time || ""}
634 onChange={(value) => {
635 onFieldChange("end_time", value);
636 // Auto-calculate duration if function provided
637 if (calculateDuration && entry.start_time) {
638 const newDuration = calculateDuration(
639 entry.start_time,
640 value,
641 entry.time_type || "exact",
642 );
643 if (newDuration && !entry.duration) {
644 onFieldChange("duration", newDuration);
645 }
646 }
647 }}
648 placeholder="17:00"
649 className="w-full"
650 error={!!errors?.end_time}
651 />
652 {errors.end_time && (
653 <p
654 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
655 >
656 <Info className="w-4 h-4" />
657 {errors.end_time}
658 </p>
659 )}
660 </div>
661 </div>
662 )}
663
664 <div>
665 <label
666 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
667 >
668 {__("Duration", "yatra")}
669 </label>
670 <HelpText
671 text={__(
672 'How long does this activity take? (e.g., "3 hours", "1 hour 30 minutes"). Leave empty to auto-calculate from times.',
673 "yatra",
674 )}
675 className="mb-2"
676 />
677 <div className="relative">
678 <Clock className="absolute left-2.5 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
679 <Input
680 type="text"
681 value={entry.duration || ""}
682 onChange={(e) => onFieldChange("duration", e.target.value)}
683 placeholder={autoDuration || __("e.g., 3 hours", "yatra")}
684 className={`pl-9 ${textSize}`}
685 />
686 </div>
687 {autoDuration && entry.time_type === "exact" && !entry.duration && (
688 <p
689 className={`mt-1.5 ${isCompact ? "text-xs" : "text-xs"} text-gray-500 dark:text-gray-400`}
690 >
691 {__("Auto-calculated:", "yatra")} {autoDuration}
692 </p>
693 )}
694 </div>
695 </div>
696
697 {/* Cost & Additional Information */}
698 <div
699 className={`space-y-3 ${!isCompact ? "pt-3 border-t border-gray-200 dark:border-gray-700" : ""}`}
700 >
701 {!isCompact && (
702 <h5
703 className={`${labelSize} font-semibold text-gray-900 dark:text-white`}
704 >
705 {__("Cost and Additional Information", "yatra")}
706 </h5>
707 )}
708
709 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
710 <div>
711 <label
712 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
713 >
714 {__("Cost", "yatra")}
715 </label>
716 <HelpText
717 text={__(
718 "Optional cost for this activity. Leave empty if included in trip price.",
719 "yatra",
720 )}
721 className="mb-2"
722 />
723 <div className="relative">
724 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
725 {currencySymbol}
726 </span>
727 <Input
728 type="number"
729 min="0"
730 step="0.01"
731 value={entry.cost || ""}
732 onChange={(e) => onFieldChange("cost", e.target.value)}
733 placeholder="0.00"
734 className={`pl-8 ${textSize}`}
735 />
736 </div>
737 </div>
738
739 <div className="flex items-end">
740 <label className="flex items-center gap-2 cursor-pointer">
741 <input
742 type="checkbox"
743 checked={entry.cost_per_person !== false}
744 onChange={(e) =>
745 onFieldChange("cost_per_person", e.target.checked)
746 }
747 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
748 />
749 <span className={`${textSize} text-gray-700 dark:text-gray-300`}>
750 {__("Cost per person", "yatra")}
751 </span>
752 </label>
753 </div>
754 </div>
755
756 <div>
757 <label
758 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
759 >
760 {__("Notes / Instructions", "yatra")}
761 </label>
762 <HelpText
763 text={__(
764 "Additional notes or special instructions for this activity.",
765 "yatra",
766 )}
767 className="mb-2"
768 />
769 <textarea
770 value={entry.notes || ""}
771 onChange={(e) => onFieldChange("notes", e.target.value)}
772 placeholder={__(
773 "e.g., Please arrive 15 minutes early, Bring comfortable shoes...",
774 "yatra",
775 )}
776 rows={isCompact ? 2 : 3}
777 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`}
778 />
779 </div>
780
781 {/* Included/Excluded Items */}
782 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
783 <div>
784 <label
785 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-2`}
786 >
787 {__("Included Items", "yatra")}
788 </label>
789 <HelpText
790 text={__("Items or services included in this activity.", "yatra")}
791 className="mb-2"
792 />
793 <div className="space-y-2">
794 <div className="flex gap-2">
795 <Input
796 type="text"
797 value={newIncludedItem}
798 onChange={(e) => onIncludedItemChange?.(e.target.value)}
799 onKeyPress={(e) => {
800 if (e.key === "Enter") {
801 e.preventDefault();
802 onAddIncludedItem?.();
803 }
804 }}
805 placeholder={__("Add included item...", "yatra")}
806 className={`flex-1 ${textSize}`}
807 />
808 <Button
809 type="button"
810 variant="outline"
811 size="icon"
812 onClick={(e) => {
813 e.preventDefault();
814 e.stopPropagation();
815 if (newIncludedItem.trim()) {
816 onAddIncludedItem?.();
817 }
818 }}
819 disabled={!newIncludedItem.trim()}
820 className={`flex-shrink-0 ${isCompact ? "h-9 w-9" : ""}`}
821 >
822 <Plus className="w-4 h-4" />
823 </Button>
824 </div>
825 <div className="flex flex-wrap gap-2">
826 {(entry.included_items || []).map((item, index) => (
827 <Badge
828 key={index}
829 variant="info"
830 className={`flex items-center gap-1.5 ${textSize}`}
831 >
832 {item}
833 <button
834 type="button"
835 onClick={() => onRemoveIncludedItem?.(index)}
836 className="ml-1 hover:text-red-600"
837 >
838 <X className="w-3 h-3" />
839 </button>
840 </Badge>
841 ))}
842 </div>
843 </div>
844 </div>
845
846 <div>
847 <label
848 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-2`}
849 >
850 {__("Excluded Items", "yatra")}
851 </label>
852 <HelpText
853 text={__(
854 "Items or services NOT included in this activity.",
855 "yatra",
856 )}
857 className="mb-2"
858 />
859 <div className="space-y-2">
860 <div className="flex gap-2">
861 <Input
862 type="text"
863 value={newExcludedItem}
864 onChange={(e) => onExcludedItemChange?.(e.target.value)}
865 onKeyPress={(e) => {
866 if (e.key === "Enter") {
867 e.preventDefault();
868 onAddExcludedItem?.();
869 }
870 }}
871 placeholder={__("Add excluded item...", "yatra")}
872 className={`flex-1 ${textSize}`}
873 />
874 <Button
875 type="button"
876 variant="outline"
877 size="icon"
878 onClick={(e) => {
879 e.preventDefault();
880 e.stopPropagation();
881 if (newExcludedItem.trim()) {
882 onAddExcludedItem?.();
883 }
884 }}
885 disabled={!newExcludedItem.trim()}
886 className={`flex-shrink-0 ${isCompact ? "h-9 w-9" : ""}`}
887 >
888 <Plus className="w-4 h-4" />
889 </Button>
890 </div>
891 <div className="flex flex-wrap gap-2">
892 {(entry.excluded_items || []).map((item, index) => (
893 <Badge
894 key={index}
895 variant="default"
896 className={`flex items-center gap-1.5 ${textSize} border border-gray-300 dark:border-gray-600`}
897 >
898 {item}
899 <button
900 type="button"
901 onClick={() => onRemoveExcludedItem?.(index)}
902 className="ml-1 hover:text-red-600"
903 >
904 <X className="w-3 h-3" />
905 </button>
906 </Badge>
907 ))}
908 </div>
909 </div>
910 </div>
911
912 {/* Gallery Section */}
913 <div className="space-y-4">
914 <div>
915 <label
916 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
917 >
918 {__("Photo Gallery", "yatra")}
919 </label>
920 <HelpText
921 text={__("Add photos to showcase this activity", "yatra")}
922 className="mb-2"
923 />
924 <MediaUpload
925 items={localGallery}
926 onChange={(mediaItems) => {
927 setLocalGallery(mediaItems); // Update local state immediately
928 onFieldChange("gallery", mediaItems); // Still call parent for data persistence
929 }}
930 maxItems={6}
931 acceptTypes="images"
932 title={__("Activity Photos", "yatra")}
933 description={__(
934 "Upload photos to showcase this activity",
935 "yatra",
936 )}
937 className="w-full"
938 />
939 </div>
940 </div>
941
942 {/* Video Section */}
943 <div className="space-y-4">
944 <div>
945 <label
946 className={`block ${labelSize} font-medium text-gray-700 dark:text-gray-300 mb-1.5`}
947 >
948 {__("Video URL", "yatra")}
949 </label>
950 <HelpText
951 text={__(
952 "Add a video URL to showcase this activity (YouTube, Vimeo, etc.)",
953 "yatra",
954 )}
955 className="mb-2"
956 />
957 <input
958 type="text"
959 value={localVideoUrl}
960 onChange={(e) => {
961 setLocalVideoUrl(e.target.value); // Update local state immediately
962 onFieldChange("video_url", e.target.value); // Still call parent for data persistence
963 }}
964 placeholder={__("https://youtube.com/watch?v=...", "yatra")}
965 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}`}
966 />
967 {errors.video_url && (
968 <p
969 className={`mt-1.5 ${textSize} text-red-600 dark:text-red-400 flex items-center gap-1`}
970 >
971 <Info className="w-4 h-4" />
972 {errors.video_url}
973 </p>
974 )}
975 </div>
976 </div>
977 </div>
978 </div>
979
980 {/* Item Type Creation Modal */}
981 <Modal
982 isOpen={isItemTypeModalOpen}
983 onClose={() => setIsItemTypeModalOpen(false)}
984 title={__("Add New Item Type", "yatra")}
985 description={__(
986 "Create a new category for organizing itinerary items",
987 "yatra",
988 )}
989 size="xl"
990 >
991 <ItemTypeFormContent
992 onSuccess={(newItemType) => {
993 setIsItemTypeModalOpen(false);
994
995 // Select the newly created item type
996 const itemTypeId = newItemType?.data?.id || newItemType?.id;
997 if (itemTypeId) {
998 onFieldChange("item_type_id", itemTypeId.toString());
999 }
1000
1001 // Reset item selection
1002 onFieldChange("item_id", "");
1003 // Refresh data to update dropdowns
1004 onRefreshData?.();
1005 }}
1006 onCancel={() => setIsItemTypeModalOpen(false)}
1007 />
1008 </Modal>
1009
1010 {/* Item Creation Modal */}
1011 <Modal
1012 isOpen={isItemModalOpen}
1013 onClose={() => setIsItemModalOpen(false)}
1014 title={__("Add New Item", "yatra")}
1015 description={__(
1016 "Create a new item for the selected item type",
1017 "yatra",
1018 )}
1019 size="xl"
1020 >
1021 <ItemFormContent
1022 selectedTypeId={entry.item_type_id}
1023 onSuccess={(newItem) => {
1024 setIsItemModalOpen(false);
1025
1026 // Select the newly created item
1027 const itemId = newItem?.data?.id || newItem?.id;
1028 if (itemId) {
1029 onFieldChange("item_id", itemId.toString());
1030 }
1031
1032 // Refresh data to update dropdowns
1033 onRefreshData?.();
1034 }}
1035 onCancel={() => setIsItemModalOpen(false)}
1036 />
1037 </Modal>
1038 </>
1039 );
1040
1041 return renderField(content);
1042 };
1043
1044 // Minimal Item Type Form - Uses exact same API as main ItemTypeForm
1045 interface ItemTypeFormContentProps {
1046 onSuccess: (itemType: any) => void;
1047 onCancel: () => void;
1048 }
1049
1050 const ItemTypeFormContent: React.FC<ItemTypeFormContentProps> = ({
1051 onSuccess,
1052 onCancel,
1053 }) => {
1054 const [formData, setFormData] = useState({
1055 name: "",
1056 slug: "",
1057 description: "",
1058 icon: null,
1059 color: "blue",
1060 status: "publish",
1061 });
1062 const [errors, setErrors] = useState<Record<string, string>>({});
1063 const [isSubmitting, setIsSubmitting] = useState(false);
1064
1065 // Auto-generate slug from name (same as main form)
1066 const handleNameChange = (value: string) => {
1067 const slug = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
1068 setFormData((prev) => ({
1069 ...prev,
1070 name: value,
1071 slug: slug,
1072 }));
1073 if (errors.name) {
1074 setErrors((prev) => ({ ...prev, name: "" }));
1075 }
1076 };
1077
1078 const handleSubmit = async (e: React.FormEvent) => {
1079 e.preventDefault();
1080 setIsSubmitting(true);
1081
1082 try {
1083 // Use exact same API endpoint and structure as main ItemTypeForm
1084 const response = await fetch("/wp-json/yatra/v1/item-types", {
1085 method: "POST",
1086 headers: {
1087 "Content-Type": "application/json",
1088 "X-WP-Nonce": (window as any).wpApiSettings?.nonce || "",
1089 },
1090 body: JSON.stringify(formData),
1091 });
1092
1093 if (response.ok) {
1094 const newItemType = await response.json();
1095 onSuccess(newItemType);
1096 } else {
1097 const errorData = await response.json();
1098 setErrors(
1099 errorData.errors || { general: "Failed to create item type" },
1100 );
1101 }
1102 } catch (error) {
1103 setErrors({ general: "An error occurred while creating the item type" });
1104 } finally {
1105 setIsSubmitting(false);
1106 }
1107 };
1108
1109 return (
1110 <form onSubmit={handleSubmit} className="space-y-4">
1111 <div>
1112 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1113 {__("Type Name", "yatra")} <span className="text-red-500">*</span>
1114 </label>
1115 <Input
1116 type="text"
1117 value={formData.name}
1118 onChange={(e) => handleNameChange(e.target.value)}
1119 placeholder={__("e.g., Activity, Meal, Accommodation", "yatra")}
1120 className={errors.name ? "border-red-500" : ""}
1121 required
1122 />
1123 {errors.name && (
1124 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
1125 {errors.name}
1126 </p>
1127 )}
1128 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
1129 {__(
1130 "Slug will be auto-generated. You can edit icon, color, and other options later in Item Types page",
1131 "yatra",
1132 )}
1133 </p>
1134 </div>
1135
1136 <div>
1137 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1138 {__("Description", "yatra")}
1139 </label>
1140 <textarea
1141 value={formData.description}
1142 onChange={(e) =>
1143 setFormData({ ...formData, description: e.target.value })
1144 }
1145 placeholder={__("Brief description of this item type...", "yatra")}
1146 rows={3}
1147 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"
1148 />
1149 </div>
1150
1151 <div className="flex justify-end gap-2 pt-4 border-t dark:border-gray-700">
1152 <Button
1153 type="button"
1154 variant="outline"
1155 onClick={onCancel}
1156 disabled={isSubmitting}
1157 >
1158 {__("Cancel", "yatra")}
1159 </Button>
1160 <Button type="submit" disabled={isSubmitting}>
1161 {isSubmitting
1162 ? __("Creating...", "yatra")
1163 : __("Create Item Type", "yatra")}
1164 </Button>
1165 </div>
1166 </form>
1167 );
1168 };
1169
1170 // Minimal Item Form - Uses exact same API as main ItemForm
1171 interface ItemFormContentProps {
1172 selectedTypeId?: string;
1173 onSuccess: (item: any) => void;
1174 onCancel: () => void;
1175 }
1176
1177 const ItemFormContent: React.FC<ItemFormContentProps> = ({
1178 selectedTypeId,
1179 onSuccess,
1180 onCancel,
1181 }) => {
1182 // Check if item type is selected
1183 if (!selectedTypeId) {
1184 return (
1185 <div className="p-6 text-center">
1186 <div className="mb-4">
1187 <Info className="w-12 h-12 text-yellow-500 mx-auto" />
1188 </div>
1189 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
1190 {__("Item Type Required", "yatra")}
1191 </h3>
1192 <p className="text-gray-600 dark:text-gray-400 mb-4">
1193 {__(
1194 "Please select an Item Type first before creating an Item.",
1195 "yatra",
1196 )}
1197 </p>
1198 <Button type="button" variant="outline" onClick={onCancel}>
1199 {__("Close", "yatra")}
1200 </Button>
1201 </div>
1202 );
1203 }
1204
1205 const [formData, setFormData] = useState({
1206 name: "",
1207 slug: "",
1208 description: "",
1209 type_id: "",
1210 status: "publish",
1211 });
1212 const [errors, setErrors] = useState<Record<string, string>>({});
1213 const [isSubmitting, setIsSubmitting] = useState(false);
1214
1215 // Update formData when selectedTypeId changes
1216 useEffect(() => {
1217 setFormData((prev) => ({
1218 ...prev,
1219 type_id: selectedTypeId || "",
1220 }));
1221 }, [selectedTypeId]);
1222
1223 // Auto-generate slug from name (same as main form)
1224 const handleNameChange = (value: string) => {
1225 const slug = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
1226 setFormData((prev) => ({
1227 ...prev,
1228 name: value,
1229 slug: slug,
1230 }));
1231 if (errors.name) {
1232 setErrors((prev) => ({ ...prev, name: "" }));
1233 }
1234 };
1235
1236 const handleSubmit = async (e: React.FormEvent) => {
1237 e.preventDefault();
1238 setIsSubmitting(true);
1239
1240 try {
1241 // Use exact same API endpoint and structure as main ItemForm
1242 const response = await fetch("/wp-json/yatra/v1/items", {
1243 method: "POST",
1244 headers: {
1245 "Content-Type": "application/json",
1246 "X-WP-Nonce": (window as any).wpApiSettings?.nonce || "",
1247 },
1248 body: JSON.stringify(formData),
1249 });
1250
1251 if (response.ok) {
1252 const newItem = await response.json();
1253 onSuccess(newItem);
1254 } else {
1255 const errorData = await response.json();
1256 setErrors(errorData.errors || { general: "Failed to create item" });
1257 }
1258 } catch (error) {
1259 setErrors({ general: "An error occurred while creating the item" });
1260 } finally {
1261 setIsSubmitting(false);
1262 }
1263 };
1264
1265 return (
1266 <form onSubmit={handleSubmit} className="space-y-4">
1267 <div>
1268 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1269 {__("Item Name", "yatra")} <span className="text-red-500">*</span>
1270 </label>
1271 <Input
1272 type="text"
1273 value={formData.name}
1274 onChange={(e) => handleNameChange(e.target.value)}
1275 placeholder={__("e.g., Hiking, Lunch, Hotel", "yatra")}
1276 className={errors.name ? "border-red-500" : ""}
1277 required
1278 />
1279 {errors.name && (
1280 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
1281 {errors.name}
1282 </p>
1283 )}
1284 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
1285 {__(
1286 "Slug will be auto-generated. You can edit additional details later in Items page",
1287 "yatra",
1288 )}
1289 </p>
1290 </div>
1291
1292 <div>
1293 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1294 {__("Description", "yatra")}
1295 </label>
1296 <textarea
1297 value={formData.description}
1298 onChange={(e) =>
1299 setFormData({ ...formData, description: e.target.value })
1300 }
1301 placeholder={__("Brief description of this item...", "yatra")}
1302 rows={3}
1303 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"
1304 />
1305 </div>
1306
1307 <div className="flex justify-end gap-2 pt-4 border-t dark:border-gray-700">
1308 <Button
1309 type="button"
1310 variant="outline"
1311 onClick={onCancel}
1312 disabled={isSubmitting}
1313 >
1314 {__("Cancel", "yatra")}
1315 </Button>
1316 <Button type="submit" disabled={isSubmitting}>
1317 {isSubmitting
1318 ? __("Creating...", "yatra")
1319 : __("Create Item", "yatra")}
1320 </Button>
1321 </div>
1322 </form>
1323 );
1324 };
1325