PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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 / AvailabilityForm.tsx

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

1,690 lines 68.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Availability Form
3 * Add or edit availability dates for a trip
4 */
5
6 import React, { useState, useEffect } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { useNavigate } from "../hooks/useNavigate";
9 import {
10 ArrowLeft,
11 Save,
12 Plus,
13 X,
14 Loader2,
15 MapPin,
16 CheckCircle2,
17 } from "lucide-react";
18 import { __ } from "../lib/i18n";
19 import { Button } from "../components/ui/button";
20 import { Input } from "../components/ui/input";
21 import { Select } from "../components/ui/select";
22 import { PageHeader } from "../components/common/PageHeader";
23 import {
24 Card,
25 CardContent,
26 CardHeader,
27 CardTitle,
28 CardDescription,
29 } from "../components/ui/card";
30 import { HelpText } from "../components/ui/help-text";
31 import { Alert } from "../components/ui/alert";
32 import { AlertCircle } from "lucide-react";
33 import { DatePicker } from "../components/ui/date-picker";
34 import { TimePicker } from "../components/ui/time-picker";
35 import { apiClient } from "../lib/api-client";
36 import { useToast } from "../components/ui/toast";
37 import { AvailabilityFormSkeleton } from "../components/availability/AvailabilityFormSkeleton";
38 import { LocationPicker } from "../components/trip-form/LocationPicker";
39
40 function coordFromApi(v: unknown): string {
41 if (v == null || v === "") return "";
42 return String(v);
43 }
44
45 interface PriceType {
46 category_id: number;
47 original_price: string;
48 discounted_price: string;
49 }
50
51 interface TravelerCategory {
52 id: number;
53 label: string;
54 description: string;
55 status: "active" | "inactive" | "publish" | "draft";
56 age_min?: number;
57 age_max?: number;
58 }
59
60 interface AvailabilityFormData {
61 departure_date: string;
62 departure_time: string;
63 arrival_date: string;
64 arrival_time: string;
65 total_seats: string; // Total capacity
66 booked_seats: string; // Currently booked (read-only in edit mode)
67 seats_remaining: string; // Legacy field
68 pricing_type: "regular" | "traveler_based";
69 original_price: string;
70 discounted_price: string;
71 price_types: PriceType[];
72 status: "available" | "sold_out" | "limited" | "closed" | "blocked";
73 is_blocked: boolean; // Blocked for maintenance/holidays
74 block_reason: string; // Reason for blocking
75 alert_threshold: string; // Alert when seats drop below this
76 from_location: string;
77 to_location: string;
78 from_latitude: string;
79 from_longitude: string;
80 to_latitude: string;
81 to_longitude: string;
82 }
83
84 const AvailabilityForm: React.FC = () => {
85 const { navigate } = useNavigate();
86 const queryClient = useQueryClient();
87 const { showToast } = useToast();
88
89 // Get trip_id and id from URL
90 const urlParams = new URLSearchParams(window.location.search);
91 const tripIdFromUrl = urlParams.get("trip_id")
92 ? parseInt(urlParams.get("trip_id")!)
93 : null;
94 const availabilityId = urlParams.get("id") || null;
95 const isEditMode = !!availabilityId;
96
97 // In edit mode, trip_id can come from availability data, so we'll use state
98 const [tripId, setTripId] = useState<number | null>(tripIdFromUrl);
99
100 const [formData, setFormData] = useState<AvailabilityFormData>({
101 departure_date: "",
102 departure_time: "",
103 arrival_date: "",
104 arrival_time: "",
105 total_seats: "",
106 booked_seats: "0",
107 seats_remaining: "",
108 pricing_type: "regular",
109 original_price: "",
110 discounted_price: "",
111 price_types: [],
112 status: "available",
113 is_blocked: false,
114 block_reason: "",
115 alert_threshold: "5",
116 from_location: "",
117 to_location: "",
118 from_latitude: "",
119 from_longitude: "",
120 to_latitude: "",
121 to_longitude: "",
122 });
123
124 const [errors, setErrors] = useState<
125 Partial<Record<keyof AvailabilityFormData, string>> & {
126 submit?: string;
127 [key: string]: string | undefined;
128 }
129 >({});
130 const [showCategorySelector, setShowCategorySelector] = useState(false);
131
132 // Fetch trip details
133 const { data: tripData } = useQuery({
134 queryKey: ["trip", tripId],
135 queryFn: async () => {
136 if (!tripId) return null;
137 const response = await apiClient.get(`/trips/${tripId}`);
138 const data = response?.data || response;
139
140 return data;
141 },
142 enabled: !!tripId,
143 });
144
145 // Fetch traveler categories
146 const { data: categoriesData, isLoading: isLoadingCategories } = useQuery({
147 queryKey: ["traveler-categories"],
148 queryFn: async () => {
149 const response = await apiClient.get("/traveler-categories");
150 const categories = response?.data || response || [];
151 return {
152 categories: Array.isArray(categories) ? categories : [],
153 };
154 },
155 });
156
157 // Filter for active categories - check both 'active' and 'publish' status
158 const activeCategories =
159 categoriesData?.categories?.filter(
160 (cat: TravelerCategory) =>
161 cat.status === "active" || cat.status === "publish",
162 ) || [];
163
164 // Fetch existing availability data if editing
165 const { data: availabilityData, isLoading: isLoadingAvailability } = useQuery(
166 {
167 queryKey: ["availability", availabilityId],
168 queryFn: async () => {
169 if (!availabilityId) return null;
170 const response = await apiClient.get(`/availability/${availabilityId}`);
171 return response?.data || response;
172 },
173 enabled: isEditMode && !!availabilityId,
174 },
175 );
176
177 // Set trip_id from availability data if not in URL (for edit mode)
178 useEffect(() => {
179 if (availabilityData && availabilityData.trip_id && !tripId) {
180 setTripId(availabilityData.trip_id);
181 }
182 }, [availabilityData, tripId]);
183
184 // Load form data when trip or availability data is available
185 useEffect(() => {
186 if (tripData && !isEditMode) {
187 const rawPriceTypes = tripData.price_types;
188 const hasTravelerPricing = Array.isArray(rawPriceTypes)
189 ? rawPriceTypes.length > 0
190 : false;
191 const effectivePricingType = hasTravelerPricing
192 ? "traveler_based"
193 : tripData.pricing_type || "regular";
194
195 setFormData((prev) => ({
196 ...prev,
197 from_location: tripData.starting_location || "",
198 to_location: tripData.ending_location || "",
199 from_latitude: coordFromApi(tripData.starting_latitude),
200 from_longitude: coordFromApi(tripData.starting_longitude),
201 to_latitude: coordFromApi(tripData.ending_latitude),
202 to_longitude: coordFromApi(tripData.ending_longitude),
203 // Default pricing type based on trip's pricing type
204 pricing_type: effectivePricingType as "regular" | "traveler_based",
205 }));
206 }
207 }, [tripData, isEditMode]);
208
209 useEffect(() => {
210 if (availabilityData) {
211 const totalSeats =
212 availabilityData.total_seats || availabilityData.seats_total || 0;
213 const availableSeats =
214 availabilityData.available_seats ||
215 availabilityData.seats_available ||
216 0;
217 const bookedSeats = totalSeats - availableSeats;
218
219 // ALWAYS use trip's pricing type, not the old availability pricing type
220 // This ensures availability dates match the current trip pricing configuration
221 const rawTripPriceTypes = tripData?.price_types;
222 const tripHasTravelerPricing = Array.isArray(rawTripPriceTypes)
223 ? rawTripPriceTypes.length > 0
224 : false;
225 const pricingType = tripHasTravelerPricing
226 ? "traveler_based"
227 : tripData?.pricing_type || availabilityData.pricing_type || "regular";
228
229 setFormData({
230 departure_date: availabilityData.departure_date || "",
231 departure_time: availabilityData.departure_time || "",
232 arrival_date:
233 availabilityData.arrival_date ||
234 availabilityData.departure_date ||
235 "",
236 arrival_time: availabilityData.arrival_time || "",
237 total_seats: totalSeats.toString(),
238 booked_seats: bookedSeats.toString(),
239 seats_remaining:
240 availableSeats > 10 ? "10+" : availableSeats.toString(),
241 pricing_type: pricingType as "regular" | "traveler_based",
242 original_price: availabilityData.original_price?.toString() || "",
243 discounted_price: availabilityData.discounted_price?.toString() || "",
244 price_types: availabilityData.price_types || [],
245 status: (availabilityData.status ||
246 (availabilityData.is_blocked ? "blocked" : "available")) as
247 | "available"
248 | "sold_out"
249 | "limited"
250 | "closed"
251 | "blocked",
252 is_blocked:
253 availabilityData.is_blocked ||
254 availabilityData.status === "blocked" ||
255 false,
256 block_reason: availabilityData.block_reason || "",
257 alert_threshold: availabilityData.alert_threshold?.toString() || "5",
258 from_location:
259 availabilityData.from_location || tripData?.starting_location || "",
260 to_location:
261 availabilityData.to_location || tripData?.ending_location || "",
262 from_latitude:
263 coordFromApi(availabilityData.from_latitude) ||
264 coordFromApi(tripData?.starting_latitude),
265 from_longitude:
266 coordFromApi(availabilityData.from_longitude) ||
267 coordFromApi(tripData?.starting_longitude),
268 to_latitude:
269 coordFromApi(availabilityData.to_latitude) ||
270 coordFromApi(tripData?.ending_latitude),
271 to_longitude:
272 coordFromApi(availabilityData.to_longitude) ||
273 coordFromApi(tripData?.ending_longitude),
274 });
275 }
276 }, [availabilityData, tripData]);
277
278 const handleFieldChange = (
279 field: keyof AvailabilityFormData,
280 value: string | boolean,
281 ) => {
282 setFormData((prev) => ({
283 ...prev,
284 [field]: value,
285 }));
286
287 // Clear error for this field
288 if (errors[field]) {
289 setErrors((prev) => {
290 const newErrors = { ...prev };
291 delete newErrors[field];
292 return newErrors;
293 });
294 }
295 };
296
297 const handlePriceTypeAdd = (categoryId: number) => {
298 // Check if category already exists
299 if (formData.price_types.some((pt) => pt.category_id === categoryId)) {
300 return;
301 }
302
303 // Find the category to get its label
304 const category = activeCategories.find((cat) => cat.id === categoryId);
305
306 setFormData((prev) => ({
307 ...prev,
308 price_types: [
309 ...prev.price_types,
310 {
311 category_id: categoryId,
312 category_label: category?.label || "",
313 original_price: "",
314 discounted_price: "",
315 },
316 ],
317 }));
318 };
319
320 const handlePriceTypeRemove = (categoryId: number) => {
321 setFormData((prev) => ({
322 ...prev,
323 price_types: prev.price_types.filter(
324 (pt) => pt.category_id !== categoryId,
325 ),
326 }));
327 };
328
329 const handlePriceTypeChange = (
330 categoryId: number,
331 field: "original_price" | "discounted_price",
332 value: string,
333 ) => {
334 setFormData((prev) => ({
335 ...prev,
336 price_types: prev.price_types.map((pt) =>
337 pt.category_id === categoryId ? { ...pt, [field]: value } : pt,
338 ),
339 }));
340 };
341
342 const validateForm = (): boolean => {
343 const newErrors: Partial<Record<keyof AvailabilityFormData, string>> & {
344 [key: string]: string;
345 } = {};
346
347 if (!formData.departure_date) {
348 newErrors.departure_date = __("Departure date is required", "yatra");
349 }
350
351 if (!formData.arrival_date) {
352 newErrors.arrival_date = __("Arrival date is required", "yatra");
353 }
354
355 // Validate dates and times based on trip type
356 const isSingleDay = tripData?.trip_type === "single_day";
357
358 if (isSingleDay) {
359 // For single day trips, both dates should be the same
360 if (
361 formData.departure_date &&
362 formData.arrival_date &&
363 formData.departure_date !== formData.arrival_date
364 ) {
365 newErrors.arrival_date = __(
366 "For single day trips, departure and arrival must be on the same date",
367 "yatra",
368 );
369 }
370 // Validate times
371 if (!formData.departure_time) {
372 newErrors.departure_time = __(
373 "Departure time is required for single day trips",
374 "yatra",
375 );
376 }
377 if (!formData.arrival_time) {
378 newErrors.arrival_time = __(
379 "Arrival time is required for single day trips",
380 "yatra",
381 );
382 }
383 // Validate that arrival time is after departure time
384 if (
385 formData.departure_time &&
386 formData.arrival_time &&
387 formData.departure_date === formData.arrival_date
388 ) {
389 const [depHour, depMin] = formData.departure_time
390 .split(":")
391 .map(Number);
392 const [arrHour, arrMin] = formData.arrival_time.split(":").map(Number);
393 const depMinutes = depHour * 60 + depMin;
394 const arrMinutes = arrHour * 60 + arrMin;
395 if (arrMinutes <= depMinutes) {
396 newErrors.arrival_time = __(
397 "Arrival time must be after departure time",
398 "yatra",
399 );
400 }
401 }
402 } else {
403 // For multi-day trips, validate dates
404 if (formData.departure_date && formData.arrival_date) {
405 const departure = new Date(formData.departure_date);
406 const arrival = new Date(formData.arrival_date);
407 if (arrival <= departure) {
408 newErrors.arrival_date = __(
409 "Arrival date must be after departure date",
410 "yatra",
411 );
412 }
413 }
414 }
415
416 // Validate inventory
417 if (!formData.total_seats || parseInt(formData.total_seats) <= 0) {
418 newErrors.total_seats = __(
419 "Total capacity is required and must be greater than 0",
420 "yatra",
421 );
422 }
423
424 if (isEditMode && formData.booked_seats) {
425 const total = parseInt(formData.total_seats) || 0;
426 const booked = parseInt(formData.booked_seats) || 0;
427 if (booked > total) {
428 newErrors.booked_seats = __(
429 "Booked seats cannot exceed total capacity",
430 "yatra",
431 );
432 }
433 }
434
435 // Validate pricing based on pricing type (OPTIONAL - only validate if provided)
436 if (formData.pricing_type === "regular") {
437 // Only validate if user has entered a price (optional override)
438 if (formData.original_price && parseFloat(formData.original_price) <= 0) {
439 newErrors.original_price = __(
440 "Original price must be greater than 0",
441 "yatra",
442 );
443 }
444 if (
445 formData.discounted_price &&
446 formData.original_price &&
447 parseFloat(formData.discounted_price) >=
448 parseFloat(formData.original_price)
449 ) {
450 newErrors.discounted_price = __(
451 "Discounted price must be less than original price",
452 "yatra",
453 );
454 }
455 } else {
456 // Validate price types only if provided (optional override)
457 if (formData.price_types.length > 0) {
458 formData.price_types.forEach((priceType, index) => {
459 if (
460 priceType.original_price &&
461 (isNaN(parseFloat(priceType.original_price)) ||
462 parseFloat(priceType.original_price) < 0)
463 ) {
464 newErrors[`price_type_${index}_original`] = __(
465 "Original price must be a valid number",
466 "yatra",
467 );
468 }
469 if (
470 priceType.discounted_price &&
471 (isNaN(parseFloat(priceType.discounted_price)) ||
472 parseFloat(priceType.discounted_price) < 0)
473 ) {
474 newErrors[`price_type_${index}_discounted`] = __(
475 "Discounted price must be a valid number",
476 "yatra",
477 );
478 }
479 if (
480 priceType.discounted_price &&
481 priceType.original_price &&
482 parseFloat(priceType.discounted_price) >=
483 parseFloat(priceType.original_price)
484 ) {
485 newErrors[`price_type_${index}_discounted`] = __(
486 "Discounted price must be less than original price",
487 "yatra",
488 );
489 }
490 });
491 }
492 }
493
494 setErrors(newErrors);
495 return Object.keys(newErrors).length === 0;
496 };
497
498 const saveMutation = useMutation({
499 mutationFn: async (data: AvailabilityFormData) => {
500 const totalSeats = data.total_seats ? parseInt(data.total_seats) : 0;
501 const bookedSeats =
502 isEditMode && data.booked_seats ? parseInt(data.booked_seats) : 0;
503 const availableSeats = totalSeats - bookedSeats;
504
505 const payload = {
506 trip_id: tripId,
507 departure_date: data.departure_date,
508 departure_time:
509 tripData?.trip_type === "single_day"
510 ? data.departure_time || null
511 : null,
512 arrival_date: data.arrival_date || null,
513 arrival_time:
514 tripData?.trip_type === "single_day"
515 ? data.arrival_time || null
516 : null,
517 seats_total: totalSeats,
518 seats_available: availableSeats,
519 seats_reserved: 0,
520 seats_waitlist: 0,
521 pricing_type: data.pricing_type,
522 original_price:
523 data.pricing_type === "regular" && data.original_price
524 ? parseFloat(data.original_price)
525 : null,
526 discounted_price:
527 data.pricing_type === "regular" && data.discounted_price
528 ? parseFloat(data.discounted_price)
529 : null,
530 price_types:
531 data.pricing_type === "traveler_based" ? data.price_types : null,
532 status: data.is_blocked ? "blocked" : data.status,
533 from_location: data.from_location || null,
534 to_location: data.to_location || null,
535 from_latitude: data.from_latitude?.trim() || null,
536 from_longitude: data.from_longitude?.trim() || null,
537 to_latitude: data.to_latitude?.trim() || null,
538 to_longitude: data.to_longitude?.trim() || null,
539 special_notes: null,
540 cutoff_hours: 24,
541 };
542
543 if (isEditMode && availabilityId) {
544 const response = await apiClient.put(
545 `/availability/${availabilityId}`,
546 payload,
547 );
548 return response?.data || response;
549 } else {
550 const response = await apiClient.post("/availability", payload);
551 return response?.data || response;
552 }
553 },
554 onSuccess: () => {
555 queryClient.invalidateQueries({ queryKey: ["availability"] });
556 queryClient.invalidateQueries({ queryKey: ["trips"] });
557 // Navigate immediately, toast will be shown on the availability page if needed
558 navigate({
559 subpage: "trips",
560 tab: "availability",
561 trip_id: tripId?.toString(),
562 });
563 // Show toast after a brief delay to ensure navigation happens first
564 setTimeout(() => {
565 showToast(
566 isEditMode
567 ? __("Availability date updated successfully", "yatra")
568 : __("Availability date created successfully", "yatra"),
569 "success",
570 3000, // Shorter duration
571 );
572 }, 100);
573 },
574 onError: (error: any) => {
575 const errorMessage =
576 error?.message ||
577 error?.data?.message ||
578 __("An error occurred while saving", "yatra");
579 showToast(errorMessage, "error");
580 setErrors({
581 submit: errorMessage,
582 });
583 },
584 });
585
586 const handleSubmit = (e: React.FormEvent) => {
587 e.preventDefault();
588 if (!validateForm()) {
589 return;
590 }
591 saveMutation.mutate(formData);
592 };
593
594 const getCurrencySymbol = (currency: string) => {
595 const symbols: Record<string, string> = {
596 USD: "$",
597 EUR: "€",
598 GBP: "£",
599 INR: "₹",
600 };
601 return symbols[currency] || currency;
602 };
603
604 // Show loading if we're in edit mode and waiting for availability data to get trip_id
605 if (isEditMode && !tripId && isLoadingAvailability) {
606 return <AvailabilityFormSkeleton />;
607 }
608
609 if (!tripId) {
610 return (
611 <Alert variant="error">
612 <AlertCircle className="w-4 h-4" />
613 <div>
614 <p className="font-medium">{__("Trip ID Required", "yatra")}</p>
615 <p className="text-sm">
616 {__("Please select a trip first.", "yatra")}
617 </p>
618 </div>
619 </Alert>
620 );
621 }
622
623 // Show skeleton loader while loading data
624 if (isLoadingAvailability || (isEditMode && !availabilityData)) {
625 return <AvailabilityFormSkeleton />;
626 }
627
628 return (
629 <div className="space-y-6">
630 <PageHeader
631 title={
632 isEditMode
633 ? __("Edit Availability Date", "yatra")
634 : __("Add Availability Date", "yatra")
635 }
636 description={
637 tripData
638 ? `${isEditMode ? __("Edit", "yatra") : __("Add")} availability date for ${tripData.title} (Trip ID: ${tripId})`
639 : __("Add a new availability date for this trip", "yatra")
640 }
641 actions={
642 tripId ? (
643 <Button
644 variant="outline"
645 onClick={() =>
646 navigate({
647 subpage: "trips",
648 tab: "availability",
649 trip_id: tripId.toString(),
650 })
651 }
652 >
653 <ArrowLeft className="w-4 h-4 mr-2" />
654 {__("Back", "yatra")}
655 </Button>
656 ) : null
657 }
658 />
659
660 <form onSubmit={handleSubmit} className="space-y-6">
661 <Card>
662 <CardHeader>
663 <CardTitle>{__("Date Information", "yatra")}</CardTitle>
664 <CardDescription>
665 {__(
666 "Set the departure and arrival dates for this availability",
667 "yatra",
668 )}
669 </CardDescription>
670 </CardHeader>
671 <CardContent className="space-y-4">
672 {tripData?.trip_type === "single_day" ? (
673 // Single Day Trip - Show date and time pickers
674 <>
675 <div>
676 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
677 {__("Date", "yatra")}{" "}
678 <span className="text-red-500">*</span>
679 </label>
680 <DatePicker
681 value={formData.departure_date}
682 onChange={(value: string) => {
683 handleFieldChange("departure_date", value);
684 handleFieldChange("arrival_date", value); // Same date for single day
685 }}
686 placeholder={__("Select date", "yatra")}
687 error={!!errors.departure_date}
688 />
689 {errors.departure_date && (
690 <p className="mt-1 text-xs text-red-600">
691 {errors.departure_date}
692 </p>
693 )}
694 </div>
695 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
696 <div>
697 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
698 {__("Departure Time", "yatra")}{" "}
699 <span className="text-red-500">*</span>
700 </label>
701 <TimePicker
702 value={formData.departure_time}
703 onChange={(value: string) =>
704 handleFieldChange("departure_time", value)
705 }
706 placeholder={__("Select departure time", "yatra")}
707 error={!!errors.departure_time}
708 />
709 {errors.departure_time && (
710 <p className="mt-1 text-xs text-red-600">
711 {errors.departure_time}
712 </p>
713 )}
714 </div>
715 <div>
716 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
717 {__("Arrival Time", "yatra")}{" "}
718 <span className="text-red-500">*</span>
719 </label>
720 <TimePicker
721 value={formData.arrival_time}
722 onChange={(value: string) =>
723 handleFieldChange("arrival_time", value)
724 }
725 placeholder={__("Select arrival time", "yatra")}
726 error={!!errors.arrival_time}
727 />
728 {errors.arrival_time && (
729 <p className="mt-1 text-xs text-red-600">
730 {errors.arrival_time}
731 </p>
732 )}
733 </div>
734 </div>
735 </>
736 ) : (
737 // Multi-Day Trip - Show date pickers only
738 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
739 <div>
740 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
741 {__("Departure Date", "yatra")}{" "}
742 <span className="text-red-500">*</span>
743 </label>
744 <DatePicker
745 value={formData.departure_date}
746 onChange={(value: string) =>
747 handleFieldChange("departure_date", value)
748 }
749 placeholder={__("Select departure date", "yatra")}
750 error={!!errors.departure_date}
751 />
752 {errors.departure_date && (
753 <p className="mt-1 text-xs text-red-600">
754 {errors.departure_date}
755 </p>
756 )}
757 </div>
758
759 <div>
760 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
761 {__("Arrival Date", "yatra")}{" "}
762 <span className="text-red-500">*</span>
763 </label>
764 <DatePicker
765 value={formData.arrival_date}
766 onChange={(value: string) =>
767 handleFieldChange("arrival_date", value)
768 }
769 minDate={
770 formData.departure_date
771 ? new Date(formData.departure_date)
772 : undefined
773 }
774 placeholder={__("Select arrival date", "yatra")}
775 error={!!errors.arrival_date}
776 />
777 {errors.arrival_date && (
778 <p className="mt-1 text-xs text-red-600">
779 {errors.arrival_date}
780 </p>
781 )}
782 {tripData?.duration_days &&
783 formData.departure_date &&
784 formData.arrival_date && (
785 <div className="mt-1.5">
786 {(() => {
787 const departure = new Date(formData.departure_date);
788 const arrival = new Date(formData.arrival_date);
789 const selectedDays = Math.ceil(
790 (arrival.getTime() - departure.getTime()) /
791 (1000 * 60 * 60 * 24),
792 );
793 const expectedDays = tripData.duration_days;
794 const diff = selectedDays - expectedDays;
795
796 if (diff === 0) {
797 return (
798 <p className="text-xs text-green-600 dark:text-green-400">
799 {__("Duration matches trip duration", "yatra")}{" "}
800 ({selectedDays} {__("days", "yatra")})
801 </p>
802 );
803 } else if (diff < 0) {
804 return (
805 <p className="text-xs text-amber-600 dark:text-amber-400">
806 {__("Shorter than trip duration", "yatra")}:{" "}
807 {selectedDays} {__("days", "yatra")} (
808 {Math.abs(diff)} {__("days shorter", "yatra")}{" "}
809 than expected {expectedDays}{" "}
810 {__("days", "yatra")})
811 </p>
812 );
813 } else {
814 return (
815 <p className="text-xs text-amber-600 dark:text-amber-400">
816 {__("Longer than trip duration", "yatra")}:{" "}
817 {selectedDays} {__("days", "yatra")} ({diff}{" "}
818 {__("days longer", "yatra")} than expected{" "}
819 {expectedDays} {__("days", "yatra")})
820 </p>
821 );
822 }
823 })()}
824 </div>
825 )}
826 {tripData?.duration_days && !formData.arrival_date && (
827 <HelpText
828 text={`${__("Trip duration", "yatra")}: ${tripData.duration_days} ${tripData.duration_days === 1 ? __("day", "yatra") : __("days", "yatra")}${tripData.duration_nights ? ` (${tripData.duration_nights} ${tripData.duration_nights === 1 ? __("night", "yatra") : __("nights", "yatra")})` : ""}`}
829 className="mt-1"
830 />
831 )}
832 </div>
833 </div>
834 )}
835
836 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 pt-2">
837 <div className="space-y-4">
838 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl border border-blue-200 dark:border-blue-800">
839 <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
840 <MapPin className="w-5 h-5 text-white" />
841 </div>
842 <div className="flex-1">
843 <h4 className="text-base font-semibold text-blue-900 dark:text-blue-100">
844 {__("Starting Point", "yatra")}
845 </h4>
846 <p className="text-xs text-blue-700 dark:text-blue-300">
847 {__("Where the journey begins", "yatra")}
848 </p>
849 </div>
850 {formData.from_latitude && formData.from_longitude && (
851 <div
852 className="w-2 h-2 bg-blue-500 rounded-full"
853 title={__("Coordinates set", "yatra")}
854 />
855 )}
856 </div>
857 <div className="space-y-2">
858 <label className="text-sm font-medium text-gray-700 dark:text-gray-300">
859 {__("From location (departure)", "yatra")}
860 </label>
861 <LocationPicker
862 value={{
863 name: formData.from_location,
864 latitude: formData.from_latitude,
865 longitude: formData.from_longitude,
866 }}
867 onChange={(loc) => {
868 setFormData((prev) => ({
869 ...prev,
870 from_location: loc.name,
871 from_latitude: loc.latitude,
872 from_longitude: loc.longitude,
873 }));
874 }}
875 label=""
876 placeholder={__("Search for starting location...", "yatra")}
877 helpText=""
878 required={false}
879 defaultMapCenter={
880 formData.from_latitude && formData.from_longitude
881 ? [
882 parseFloat(formData.from_latitude),
883 parseFloat(formData.from_longitude),
884 ]
885 : tripData?.starting_latitude &&
886 tripData?.starting_longitude
887 ? [
888 parseFloat(String(tripData.starting_latitude)),
889 parseFloat(String(tripData.starting_longitude)),
890 ]
891 : [20, 0]
892 }
893 defaultZoom={
894 formData.from_latitude && formData.from_longitude
895 ? 13
896 : tripData?.starting_latitude &&
897 tripData?.starting_longitude
898 ? 13
899 : 2
900 }
901 mapHeight="300px"
902 showMapButton={false}
903 searchLimit={8}
904 __={__}
905 className=""
906 mapClassName="rounded-lg"
907 showManualCoordinateFields
908 />
909 <HelpText
910 text={__(
911 "Overrides trip starting location for this date only. Coordinates power maps and distance features.",
912 "yatra",
913 )}
914 />
915 </div>
916 </div>
917
918 <div className="space-y-4">
919 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-xl border border-green-200 dark:border-green-800">
920 <div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg">
921 <CheckCircle2 className="w-5 h-5 text-white" />
922 </div>
923 <div className="flex-1">
924 <h4 className="text-base font-semibold text-green-900 dark:text-green-100">
925 {__("Ending Point", "yatra")}
926 </h4>
927 <p className="text-xs text-green-700 dark:text-green-300">
928 {__("Where the journey concludes", "yatra")}
929 </p>
930 </div>
931 {formData.to_latitude && formData.to_longitude && (
932 <div
933 className="w-2 h-2 bg-green-500 rounded-full"
934 title={__("Coordinates set", "yatra")}
935 />
936 )}
937 </div>
938 <div className="space-y-2">
939 <label className="text-sm font-medium text-gray-700 dark:text-gray-300">
940 {__("To location (destination)", "yatra")}
941 </label>
942 <LocationPicker
943 value={{
944 name: formData.to_location,
945 latitude: formData.to_latitude,
946 longitude: formData.to_longitude,
947 }}
948 onChange={(loc) => {
949 setFormData((prev) => ({
950 ...prev,
951 to_location: loc.name,
952 to_latitude: loc.latitude,
953 to_longitude: loc.longitude,
954 }));
955 }}
956 label=""
957 placeholder={__("Search for ending location...", "yatra")}
958 helpText=""
959 required={false}
960 defaultMapCenter={
961 formData.to_latitude && formData.to_longitude
962 ? [
963 parseFloat(formData.to_latitude),
964 parseFloat(formData.to_longitude),
965 ]
966 : tripData?.ending_latitude &&
967 tripData?.ending_longitude
968 ? [
969 parseFloat(String(tripData.ending_latitude)),
970 parseFloat(String(tripData.ending_longitude)),
971 ]
972 : formData.from_latitude && formData.from_longitude
973 ? [
974 parseFloat(formData.from_latitude),
975 parseFloat(formData.from_longitude),
976 ]
977 : tripData?.starting_latitude &&
978 tripData?.starting_longitude
979 ? [
980 parseFloat(
981 String(tripData.starting_latitude),
982 ),
983 parseFloat(
984 String(tripData.starting_longitude),
985 ),
986 ]
987 : [20, 0]
988 }
989 defaultZoom={
990 formData.to_latitude && formData.to_longitude
991 ? 13
992 : tripData?.ending_latitude &&
993 tripData?.ending_longitude
994 ? 13
995 : formData.from_latitude && formData.from_longitude
996 ? 13
997 : tripData?.starting_latitude &&
998 tripData?.starting_longitude
999 ? 13
1000 : 2
1001 }
1002 mapHeight="300px"
1003 showMapButton={false}
1004 searchLimit={8}
1005 __={__}
1006 className=""
1007 mapClassName="rounded-lg"
1008 showManualCoordinateFields
1009 />
1010 <HelpText
1011 text={__(
1012 "Overrides trip ending location for this date only.",
1013 "yatra",
1014 )}
1015 />
1016 </div>
1017 </div>
1018 </div>
1019 </CardContent>
1020 </Card>
1021
1022 <Card>
1023 <CardHeader>
1024 <CardTitle>{__("Pricing & Availability", "yatra")}</CardTitle>
1025 <CardDescription>
1026 {__(
1027 "Set pricing for traveler categories and seat availability for this date",
1028 "yatra",
1029 )}
1030 </CardDescription>
1031 </CardHeader>
1032 <CardContent className="space-y-4">
1033 {/* Pricing Override Info */}
1034 <div className="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mb-4">
1035 <div className="flex items-start gap-3">
1036 <div className="p-2 rounded-full bg-amber-100 dark:bg-amber-900/30">
1037 <svg
1038 className="w-5 h-5 text-amber-600 dark:text-amber-400"
1039 fill="none"
1040 stroke="currentColor"
1041 viewBox="0 0 24 24"
1042 >
1043 <path
1044 strokeLinecap="round"
1045 strokeLinejoin="round"
1046 strokeWidth={2}
1047 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1048 />
1049 </svg>
1050 </div>
1051 <div className="flex-1">
1052 <p className="text-sm font-medium text-gray-900 dark:text-white mb-1">
1053 {__("Pricing Override (Optional)", "yatra")}
1054 </p>
1055 <p className="text-xs text-gray-600 dark:text-gray-400">
1056 {__(
1057 "Leave pricing fields empty to use the trip's default pricing. Fill them in only if you want to override the default pricing for this specific date.",
1058 "yatra",
1059 )}
1060 </p>
1061 </div>
1062 </div>
1063 </div>
1064
1065 {/* Pricing Type Info - Inherited from Trip */}
1066 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
1067 <div className="flex items-center gap-3">
1068 <div
1069 className={`p-2 rounded-full ${formData.pricing_type === "traveler_based" ? "bg-green-100 dark:bg-green-900/30" : "bg-blue-100 dark:bg-blue-900/30"}`}
1070 >
1071 {formData.pricing_type === "traveler_based" ? (
1072 <svg
1073 className="w-5 h-5 text-green-600 dark:text-green-400"
1074 fill="none"
1075 stroke="currentColor"
1076 viewBox="0 0 24 24"
1077 >
1078 <path
1079 strokeLinecap="round"
1080 strokeLinejoin="round"
1081 strokeWidth={2}
1082 d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
1083 />
1084 </svg>
1085 ) : (
1086 <svg
1087 className="w-5 h-5 text-blue-600 dark:text-blue-400"
1088 fill="none"
1089 stroke="currentColor"
1090 viewBox="0 0 24 24"
1091 >
1092 <path
1093 strokeLinecap="round"
1094 strokeLinejoin="round"
1095 strokeWidth={2}
1096 d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1097 />
1098 </svg>
1099 )}
1100 </div>
1101 <div>
1102 <p className="text-sm font-medium text-gray-900 dark:text-white">
1103 {formData.pricing_type === "traveler_based"
1104 ? __("Traveler-Based Pricing", "yatra")
1105 : __("Regular Pricing", "yatra")}
1106 </p>
1107 <p className="text-xs text-gray-600 dark:text-gray-400">
1108 {formData.pricing_type === "traveler_based"
1109 ? __(
1110 "This trip uses traveler category pricing. Set prices for each category below.",
1111 "yatra",
1112 )
1113 : __(
1114 "This trip uses regular pricing. Set a single price for all travelers below.",
1115 "yatra",
1116 )}
1117 </p>
1118 </div>
1119 </div>
1120 </div>
1121
1122 {/* Regular Pricing */}
1123 {formData.pricing_type === "regular" && (
1124 <div className="space-y-4">
1125 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1126 <div>
1127 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1128 {__("Original Price", "yatra")}{" "}
1129 <span className="text-gray-400 text-xs">
1130 ({__("Optional", "yatra")})
1131 </span>
1132 </label>
1133 <div className="relative">
1134 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1135 {getCurrencySymbol(tripData?.currency || "USD")}
1136 </span>
1137 <Input
1138 type="number"
1139 step="0.01"
1140 min="0"
1141 value={formData.original_price}
1142 onChange={(e) =>
1143 handleFieldChange("original_price", e.target.value)
1144 }
1145 placeholder="0.00"
1146 className={`pl-7 ${errors.original_price ? "border-red-500" : ""}`}
1147 />
1148 </div>
1149 {errors.original_price && (
1150 <p className="mt-1 text-xs text-red-600">
1151 {errors.original_price}
1152 </p>
1153 )}
1154 {!formData.original_price && tripData && (
1155 <p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
1156 {__("Using trip default", "yatra")}:{" "}
1157 {getCurrencySymbol(tripData?.currency || "USD")}
1158 {tripData.original_price || "0.00"}
1159 </p>
1160 )}
1161 </div>
1162
1163 <div>
1164 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1165 {__("Discounted Price", "yatra")} (
1166 {__("Optional", "yatra")})
1167 </label>
1168 <div className="relative">
1169 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1170 {getCurrencySymbol(tripData?.currency || "USD")}
1171 </span>
1172 <Input
1173 type="number"
1174 step="0.01"
1175 min="0"
1176 value={formData.discounted_price}
1177 onChange={(e) =>
1178 handleFieldChange("discounted_price", e.target.value)
1179 }
1180 placeholder="0.00"
1181 className={`pl-7 ${errors.discounted_price ? "border-red-500" : ""}`}
1182 />
1183 </div>
1184 {errors.discounted_price && (
1185 <p className="mt-1 text-xs text-red-600">
1186 {errors.discounted_price}
1187 </p>
1188 )}
1189 </div>
1190 </div>
1191 </div>
1192 )}
1193
1194 {/* Traveler Category Pricing */}
1195 {formData.pricing_type === "traveler_based" && (
1196 <div className="space-y-4">
1197 <div>
1198 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1199 {__("Traveler Category Pricing", "yatra")}{" "}
1200 <span className="text-gray-400 text-xs">
1201 ({__("Optional", "yatra")})
1202 </span>
1203 </label>
1204 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
1205 {__(
1206 "Add pricing to override trip default pricing for specific traveler categories. Leave empty to use trip default pricing.",
1207 "yatra",
1208 )}
1209 </p>
1210 </div>
1211
1212 {isLoadingCategories ? (
1213 <div className="flex items-center justify-center py-8">
1214 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
1215 </div>
1216 ) : activeCategories.length === 0 ? (
1217 <div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center">
1218 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
1219 {__("No active traveler categories found.", "yatra")}
1220 </p>
1221 <Button
1222 type="button"
1223 variant="outline"
1224 onClick={() =>
1225 (window.location.href =
1226 "?subpage=traveler-categories&action=create")
1227 }
1228 className="flex items-center gap-2 mx-auto"
1229 >
1230 <Plus className="w-4 h-4" />
1231 {__("Create Category", "yatra")}
1232 </Button>
1233 </div>
1234 ) : (
1235 <div className="space-y-4">
1236 {/* Add Pricing Button */}
1237 <div className="relative">
1238 <Button
1239 type="button"
1240 variant="outline"
1241 onClick={() =>
1242 setShowCategorySelector(!showCategorySelector)
1243 }
1244 className="flex items-center gap-2"
1245 disabled={
1246 activeCategories.filter(
1247 (cat) =>
1248 !formData.price_types.some(
1249 (pt) => pt.category_id === cat.id,
1250 ),
1251 ).length === 0
1252 }
1253 >
1254 <Plus className="w-4 h-4" />
1255 {__("Add Pricing", "yatra")}
1256 </Button>
1257
1258 {/* Category Selection Dropdown */}
1259 {showCategorySelector && (
1260 <>
1261 <div
1262 className="fixed inset-0 z-10"
1263 onClick={() => setShowCategorySelector(false)}
1264 />
1265 <div className="absolute top-full left-0 mt-2 w-full max-w-md bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-20 max-h-96 overflow-y-auto">
1266 <div className="p-2">
1267 <div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1">
1268 {__(
1269 "Select a category to add pricing",
1270 "yatra",
1271 )}
1272 </div>
1273 {activeCategories.filter(
1274 (cat) =>
1275 !formData.price_types.some(
1276 (pt) => pt.category_id === cat.id,
1277 ),
1278 ).length === 0 ? (
1279 <div className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
1280 {__(
1281 "All categories have pricing added",
1282 "yatra",
1283 )}
1284 </div>
1285 ) : (
1286 <div className="space-y-1">
1287 {activeCategories
1288 .filter(
1289 (cat) =>
1290 !formData.price_types.some(
1291 (pt) => pt.category_id === cat.id,
1292 ),
1293 )
1294 .map((category: TravelerCategory) => {
1295 const ageRange =
1296 category.age_min !== undefined ||
1297 category.age_max !== undefined
1298 ? category.age_min !== undefined &&
1299 category.age_max !== undefined
1300 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
1301 : category.age_min !== undefined
1302 ? `${category.age_min}+ ${__("years", "yatra")}`
1303 : category.age_max !== undefined
1304 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
1305 : ""
1306 : null;
1307
1308 return (
1309 <button
1310 key={category.id}
1311 type="button"
1312 onClick={() => {
1313 handlePriceTypeAdd(category.id);
1314 setShowCategorySelector(false);
1315 }}
1316 className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
1317 >
1318 <div className="font-medium text-sm text-gray-900 dark:text-white">
1319 {category.label}
1320 {ageRange && (
1321 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1322 ({ageRange})
1323 </span>
1324 )}
1325 </div>
1326 {category.description && (
1327 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
1328 {category.description}
1329 </div>
1330 )}
1331 </button>
1332 );
1333 })}
1334 </div>
1335 )}
1336 </div>
1337 </div>
1338 </>
1339 )}
1340 </div>
1341
1342 {/* Added Pricing List */}
1343 {formData.price_types.length > 0 && (
1344 <div className="space-y-3">
1345 {formData.price_types.map((priceType, index) => {
1346 const category = activeCategories.find(
1347 (cat) => cat.id === priceType.category_id,
1348 );
1349 if (!category) return null;
1350
1351 return (
1352 <div
1353 key={priceType.category_id}
1354 className="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg"
1355 >
1356 <div className="flex items-start justify-between mb-3">
1357 <div className="flex-1">
1358 <div className="flex items-center gap-2 mb-1">
1359 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
1360 {category.label}
1361 {(category.age_min !== undefined ||
1362 category.age_max !== undefined) && (
1363 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1364 (
1365 {category.age_min !== undefined &&
1366 category.age_max !== undefined
1367 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
1368 : category.age_min !== undefined
1369 ? `${category.age_min}+ ${__("years", "yatra")}`
1370 : category.age_max !== undefined
1371 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
1372 : ""}
1373 )
1374 </span>
1375 )}
1376 </h4>
1377 </div>
1378 <p className="text-xs text-gray-600 dark:text-gray-400">
1379 {category.description}
1380 </p>
1381 </div>
1382 <button
1383 type="button"
1384 onClick={() =>
1385 handlePriceTypeRemove(category.id)
1386 }
1387 className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
1388 title={__("Remove Pricing", "yatra")}
1389 >
1390 <X className="w-4 h-4" />
1391 </button>
1392 </div>
1393 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1394 <div>
1395 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1396 {__("Original Price", "yatra")}{" "}
1397 <span className="text-gray-400 text-xs">
1398 ({__("Optional", "yatra")})
1399 </span>
1400 </label>
1401 <div className="relative">
1402 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1403 {getCurrencySymbol(
1404 tripData?.currency || "USD",
1405 )}
1406 </span>
1407 <Input
1408 type="number"
1409 step="0.01"
1410 min="0"
1411 value={priceType.original_price}
1412 onChange={(e) =>
1413 handlePriceTypeChange(
1414 category.id,
1415 "original_price",
1416 e.target.value,
1417 )
1418 }
1419 placeholder="0.00"
1420 className={`pl-7 ${errors[`price_type_${index}_original`] ? "border-red-500" : ""}`}
1421 />
1422 </div>
1423 {errors[`price_type_${index}_original`] && (
1424 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
1425 {errors[`price_type_${index}_original`]}
1426 </p>
1427 )}
1428 </div>
1429 <div>
1430 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1431 {__("Discounted Price", "yatra")} (
1432 {__("Optional", "yatra")})
1433 </label>
1434 <div className="relative">
1435 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1436 {getCurrencySymbol(
1437 tripData?.currency || "USD",
1438 )}
1439 </span>
1440 <Input
1441 type="number"
1442 step="0.01"
1443 min="0"
1444 value={priceType.discounted_price}
1445 onChange={(e) =>
1446 handlePriceTypeChange(
1447 category.id,
1448 "discounted_price",
1449 e.target.value,
1450 )
1451 }
1452 placeholder="0.00"
1453 className={`pl-7 ${errors[`price_type_${index}_discounted`] ? "border-red-500" : ""}`}
1454 />
1455 </div>
1456 {errors[`price_type_${index}_discounted`] && (
1457 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
1458 {errors[`price_type_${index}_discounted`]}
1459 </p>
1460 )}
1461 </div>
1462 </div>
1463 </div>
1464 );
1465 })}
1466 </div>
1467 )}
1468 {errors.price_types && (
1469 <p className="text-xs text-red-600 dark:text-red-400">
1470 {errors.price_types}
1471 </p>
1472 )}
1473 </div>
1474 )}
1475 </div>
1476 )}
1477
1478 {/* Inventory Management */}
1479 <div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
1480 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
1481 {__("Inventory Management", "yatra")}
1482 </h3>
1483 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1484 <div>
1485 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1486 {__("Total Capacity", "yatra")}{" "}
1487 <span className="text-red-500">*</span>
1488 </label>
1489 <Input
1490 type="number"
1491 min="1"
1492 value={formData.total_seats}
1493 onChange={(e) =>
1494 handleFieldChange("total_seats", e.target.value)
1495 }
1496 placeholder="20"
1497 className={errors.total_seats ? "border-red-500" : ""}
1498 />
1499 {errors.total_seats && (
1500 <p className="mt-1 text-xs text-red-600">
1501 {errors.total_seats}
1502 </p>
1503 )}
1504 <HelpText
1505 text={__(
1506 "Maximum number of seats available for this date",
1507 "yatra",
1508 )}
1509 className="mt-1"
1510 />
1511 </div>
1512 {isEditMode && (
1513 <div>
1514 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1515 {__("Booked Seats", "yatra")}
1516 </label>
1517 <Input
1518 type="number"
1519 min="0"
1520 value={formData.booked_seats}
1521 readOnly
1522 className="bg-gray-50 dark:bg-gray-800 cursor-not-allowed"
1523 />
1524 <HelpText
1525 text={__("Currently booked seats (read-only)", "yatra")}
1526 className="mt-1"
1527 />
1528 </div>
1529 )}
1530 <div>
1531 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1532 {__("Alert Threshold", "yatra")}
1533 </label>
1534 <Input
1535 type="number"
1536 min="1"
1537 value={formData.alert_threshold}
1538 onChange={(e) =>
1539 handleFieldChange("alert_threshold", e.target.value)
1540 }
1541 placeholder="5"
1542 />
1543 <HelpText
1544 text={__(
1545 "Alert when available seats drop below this number",
1546 "yatra",
1547 )}
1548 className="mt-1"
1549 />
1550 </div>
1551 </div>
1552 </div>
1553
1554 {/* Block Date */}
1555 <div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
1556 <div className="flex items-center justify-between mb-4">
1557 <h3 className="text-sm font-semibold text-gray-900 dark:text-white">
1558 {__("Block Date", "yatra")}
1559 </h3>
1560 <label className="flex items-center gap-2 cursor-pointer">
1561 <input
1562 type="checkbox"
1563 checked={formData.is_blocked}
1564 onChange={(e) => {
1565 handleFieldChange("is_blocked", e.target.checked);
1566 if (e.target.checked) {
1567 handleFieldChange("status", "blocked");
1568 } else {
1569 handleFieldChange("status", "available");
1570 }
1571 }}
1572 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1573 />
1574 <span className="text-sm text-gray-700 dark:text-gray-300">
1575 {__("Block this date from bookings", "yatra")}
1576 </span>
1577 </label>
1578 </div>
1579 {formData.is_blocked && (
1580 <div>
1581 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1582 {__("Block Reason", "yatra")}
1583 </label>
1584 <Input
1585 type="text"
1586 value={formData.block_reason}
1587 onChange={(e) =>
1588 handleFieldChange("block_reason", e.target.value)
1589 }
1590 placeholder={__(
1591 "e.g., Maintenance, Holiday, Special Event",
1592 "yatra",
1593 )}
1594 />
1595 <HelpText
1596 text={__(
1597 "Reason for blocking this date (optional but recommended)",
1598 "yatra",
1599 )}
1600 className="mt-1"
1601 />
1602 </div>
1603 )}
1604 </div>
1605
1606 {/* Status */}
1607 <div className="mt-4">
1608 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1609 {__("Status", "yatra")}
1610 </label>
1611 <Select
1612 value={formData.status}
1613 onChange={(e) => {
1614 handleFieldChange("status", e.target.value);
1615 if (e.target.value === "blocked") {
1616 handleFieldChange("is_blocked", true);
1617 } else if (
1618 formData.is_blocked &&
1619 e.target.value !== "blocked"
1620 ) {
1621 handleFieldChange("is_blocked", false);
1622 }
1623 }}
1624 disabled={formData.is_blocked}
1625 >
1626 <option value="available">{__("Available", "yatra")}</option>
1627 <option value="limited">{__("Limited", "yatra")}</option>
1628 <option value="sold_out">{__("Sold Out", "yatra")}</option>
1629 <option value="closed">{__("Closed", "yatra")}</option>
1630 <option value="blocked">{__("Blocked", "yatra")}</option>
1631 </Select>
1632 {formData.is_blocked && (
1633 <HelpText
1634 text={__(
1635 'Status is automatically set to "Blocked" when date is blocked',
1636 "yatra",
1637 )}
1638 className="mt-1"
1639 />
1640 )}
1641 </div>
1642 </CardContent>
1643 </Card>
1644
1645 {errors.submit && (
1646 <Alert variant="error">
1647 <AlertCircle className="w-4 h-4" />
1648 <p>{errors.submit}</p>
1649 </Alert>
1650 )}
1651
1652 <div className="flex items-center justify-end gap-4">
1653 <Button
1654 type="button"
1655 variant="outline"
1656 onClick={() => {
1657 if (tripId) {
1658 navigate({
1659 subpage: "trips",
1660 tab: "availability",
1661 trip_id: tripId.toString(),
1662 });
1663 } else {
1664 navigate({ subpage: "trips", tab: "availability" });
1665 }
1666 }}
1667 >
1668 {__("Cancel", "yatra")}
1669 </Button>
1670 <Button type="submit" disabled={saveMutation.isPending}>
1671 {saveMutation.isPending ? (
1672 <>
1673 <div className="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
1674 {__("Saving...", "yatra")}
1675 </>
1676 ) : (
1677 <>
1678 <Save className="w-4 h-4 mr-2" />
1679 {__("Save Availability", "yatra")}
1680 </>
1681 )}
1682 </Button>
1683 </div>
1684 </form>
1685 </div>
1686 );
1687 };
1688
1689 export default AvailabilityForm;
1690