PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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.3, at resources/js/pages/AvailabilityForm.tsx

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