PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / AvailabilityForm.tsx

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

1,645 lines 66.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 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 : [-8.3405, 115.092]
881 }
882 defaultZoom={13}
883 mapHeight="300px"
884 showMapButton={false}
885 searchLimit={8}
886 __={__}
887 className=""
888 mapClassName="rounded-lg"
889 showManualCoordinateFields
890 />
891 <HelpText
892 text={__(
893 "Overrides trip starting location for this date only. Coordinates power maps and distance features.",
894 "yatra",
895 )}
896 />
897 </div>
898 </div>
899
900 <div className="space-y-4">
901 <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">
902 <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">
903 <CheckCircle2 className="w-5 h-5 text-white" />
904 </div>
905 <div className="flex-1">
906 <h4 className="text-base font-semibold text-green-900 dark:text-green-100">
907 {__("Ending Point", "yatra")}
908 </h4>
909 <p className="text-xs text-green-700 dark:text-green-300">
910 {__("Where the journey concludes", "yatra")}
911 </p>
912 </div>
913 {formData.to_latitude && formData.to_longitude && (
914 <div
915 className="w-2 h-2 bg-green-500 rounded-full"
916 title={__("Coordinates set", "yatra")}
917 />
918 )}
919 </div>
920 <div className="space-y-2">
921 <label className="text-sm font-medium text-gray-700 dark:text-gray-300">
922 {__("To location (destination)", "yatra")}
923 </label>
924 <LocationPicker
925 value={{
926 name: formData.to_location,
927 latitude: formData.to_latitude,
928 longitude: formData.to_longitude,
929 }}
930 onChange={(loc) => {
931 setFormData((prev) => ({
932 ...prev,
933 to_location: loc.name,
934 to_latitude: loc.latitude,
935 to_longitude: loc.longitude,
936 }));
937 }}
938 label=""
939 placeholder={__("Search for ending location...", "yatra")}
940 helpText=""
941 required={false}
942 defaultMapCenter={
943 formData.to_latitude && formData.to_longitude
944 ? [
945 parseFloat(formData.to_latitude),
946 parseFloat(formData.to_longitude),
947 ]
948 : tripData?.ending_latitude &&
949 tripData?.ending_longitude
950 ? [
951 parseFloat(String(tripData.ending_latitude)),
952 parseFloat(String(tripData.ending_longitude)),
953 ]
954 : [-8.5069, 115.2625]
955 }
956 defaultZoom={13}
957 mapHeight="300px"
958 showMapButton={false}
959 searchLimit={8}
960 __={__}
961 className=""
962 mapClassName="rounded-lg"
963 showManualCoordinateFields
964 />
965 <HelpText
966 text={__(
967 "Overrides trip ending location for this date only.",
968 "yatra",
969 )}
970 />
971 </div>
972 </div>
973 </div>
974 </CardContent>
975 </Card>
976
977 <Card>
978 <CardHeader>
979 <CardTitle>{__("Pricing & Availability", "yatra")}</CardTitle>
980 <CardDescription>
981 {__(
982 "Set pricing for traveler categories and seat availability for this date",
983 "yatra",
984 )}
985 </CardDescription>
986 </CardHeader>
987 <CardContent className="space-y-4">
988 {/* Pricing Override Info */}
989 <div className="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mb-4">
990 <div className="flex items-start gap-3">
991 <div className="p-2 rounded-full bg-amber-100 dark:bg-amber-900/30">
992 <svg
993 className="w-5 h-5 text-amber-600 dark:text-amber-400"
994 fill="none"
995 stroke="currentColor"
996 viewBox="0 0 24 24"
997 >
998 <path
999 strokeLinecap="round"
1000 strokeLinejoin="round"
1001 strokeWidth={2}
1002 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1003 />
1004 </svg>
1005 </div>
1006 <div className="flex-1">
1007 <p className="text-sm font-medium text-gray-900 dark:text-white mb-1">
1008 {__("Pricing Override (Optional)", "yatra")}
1009 </p>
1010 <p className="text-xs text-gray-600 dark:text-gray-400">
1011 {__(
1012 "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.",
1013 "yatra",
1014 )}
1015 </p>
1016 </div>
1017 </div>
1018 </div>
1019
1020 {/* Pricing Type Info - Inherited from Trip */}
1021 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
1022 <div className="flex items-center gap-3">
1023 <div
1024 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"}`}
1025 >
1026 {formData.pricing_type === "traveler_based" ? (
1027 <svg
1028 className="w-5 h-5 text-green-600 dark:text-green-400"
1029 fill="none"
1030 stroke="currentColor"
1031 viewBox="0 0 24 24"
1032 >
1033 <path
1034 strokeLinecap="round"
1035 strokeLinejoin="round"
1036 strokeWidth={2}
1037 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"
1038 />
1039 </svg>
1040 ) : (
1041 <svg
1042 className="w-5 h-5 text-blue-600 dark:text-blue-400"
1043 fill="none"
1044 stroke="currentColor"
1045 viewBox="0 0 24 24"
1046 >
1047 <path
1048 strokeLinecap="round"
1049 strokeLinejoin="round"
1050 strokeWidth={2}
1051 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"
1052 />
1053 </svg>
1054 )}
1055 </div>
1056 <div>
1057 <p className="text-sm font-medium text-gray-900 dark:text-white">
1058 {formData.pricing_type === "traveler_based"
1059 ? __("Traveler-Based Pricing", "yatra")
1060 : __("Regular Pricing", "yatra")}
1061 </p>
1062 <p className="text-xs text-gray-600 dark:text-gray-400">
1063 {formData.pricing_type === "traveler_based"
1064 ? __(
1065 "This trip uses traveler category pricing. Set prices for each category below.",
1066 "yatra",
1067 )
1068 : __(
1069 "This trip uses regular pricing. Set a single price for all travelers below.",
1070 "yatra",
1071 )}
1072 </p>
1073 </div>
1074 </div>
1075 </div>
1076
1077 {/* Regular Pricing */}
1078 {formData.pricing_type === "regular" && (
1079 <div className="space-y-4">
1080 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1081 <div>
1082 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1083 {__("Original Price", "yatra")}{" "}
1084 <span className="text-gray-400 text-xs">
1085 ({__("Optional", "yatra")})
1086 </span>
1087 </label>
1088 <div className="relative">
1089 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1090 {getCurrencySymbol(tripData?.currency || "USD")}
1091 </span>
1092 <Input
1093 type="number"
1094 step="0.01"
1095 min="0"
1096 value={formData.original_price}
1097 onChange={(e) =>
1098 handleFieldChange("original_price", e.target.value)
1099 }
1100 placeholder="0.00"
1101 className={`pl-7 ${errors.original_price ? "border-red-500" : ""}`}
1102 />
1103 </div>
1104 {errors.original_price && (
1105 <p className="mt-1 text-xs text-red-600">
1106 {errors.original_price}
1107 </p>
1108 )}
1109 {!formData.original_price && tripData && (
1110 <p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
1111 {__("Using trip default", "yatra")}:{" "}
1112 {getCurrencySymbol(tripData?.currency || "USD")}
1113 {tripData.original_price || "0.00"}
1114 </p>
1115 )}
1116 </div>
1117
1118 <div>
1119 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1120 {__("Discounted Price", "yatra")} (
1121 {__("Optional", "yatra")})
1122 </label>
1123 <div className="relative">
1124 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1125 {getCurrencySymbol(tripData?.currency || "USD")}
1126 </span>
1127 <Input
1128 type="number"
1129 step="0.01"
1130 min="0"
1131 value={formData.discounted_price}
1132 onChange={(e) =>
1133 handleFieldChange("discounted_price", e.target.value)
1134 }
1135 placeholder="0.00"
1136 className={`pl-7 ${errors.discounted_price ? "border-red-500" : ""}`}
1137 />
1138 </div>
1139 {errors.discounted_price && (
1140 <p className="mt-1 text-xs text-red-600">
1141 {errors.discounted_price}
1142 </p>
1143 )}
1144 </div>
1145 </div>
1146 </div>
1147 )}
1148
1149 {/* Traveler Category Pricing */}
1150 {formData.pricing_type === "traveler_based" && (
1151 <div className="space-y-4">
1152 <div>
1153 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1154 {__("Traveler Category Pricing", "yatra")}{" "}
1155 <span className="text-gray-400 text-xs">
1156 ({__("Optional", "yatra")})
1157 </span>
1158 </label>
1159 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
1160 {__(
1161 "Add pricing to override trip default pricing for specific traveler categories. Leave empty to use trip default pricing.",
1162 "yatra",
1163 )}
1164 </p>
1165 </div>
1166
1167 {isLoadingCategories ? (
1168 <div className="flex items-center justify-center py-8">
1169 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
1170 </div>
1171 ) : activeCategories.length === 0 ? (
1172 <div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center">
1173 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
1174 {__("No active traveler categories found.", "yatra")}
1175 </p>
1176 <Button
1177 type="button"
1178 variant="outline"
1179 onClick={() =>
1180 (window.location.href =
1181 "?subpage=traveler-categories&action=create")
1182 }
1183 className="flex items-center gap-2 mx-auto"
1184 >
1185 <Plus className="w-4 h-4" />
1186 {__("Create Category", "yatra")}
1187 </Button>
1188 </div>
1189 ) : (
1190 <div className="space-y-4">
1191 {/* Add Pricing Button */}
1192 <div className="relative">
1193 <Button
1194 type="button"
1195 variant="outline"
1196 onClick={() =>
1197 setShowCategorySelector(!showCategorySelector)
1198 }
1199 className="flex items-center gap-2"
1200 disabled={
1201 activeCategories.filter(
1202 (cat) =>
1203 !formData.price_types.some(
1204 (pt) => pt.category_id === cat.id,
1205 ),
1206 ).length === 0
1207 }
1208 >
1209 <Plus className="w-4 h-4" />
1210 {__("Add Pricing", "yatra")}
1211 </Button>
1212
1213 {/* Category Selection Dropdown */}
1214 {showCategorySelector && (
1215 <>
1216 <div
1217 className="fixed inset-0 z-10"
1218 onClick={() => setShowCategorySelector(false)}
1219 />
1220 <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">
1221 <div className="p-2">
1222 <div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1">
1223 {__(
1224 "Select a category to add pricing",
1225 "yatra",
1226 )}
1227 </div>
1228 {activeCategories.filter(
1229 (cat) =>
1230 !formData.price_types.some(
1231 (pt) => pt.category_id === cat.id,
1232 ),
1233 ).length === 0 ? (
1234 <div className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
1235 {__(
1236 "All categories have pricing added",
1237 "yatra",
1238 )}
1239 </div>
1240 ) : (
1241 <div className="space-y-1">
1242 {activeCategories
1243 .filter(
1244 (cat) =>
1245 !formData.price_types.some(
1246 (pt) => pt.category_id === cat.id,
1247 ),
1248 )
1249 .map((category: TravelerCategory) => {
1250 const ageRange =
1251 category.age_min !== undefined ||
1252 category.age_max !== undefined
1253 ? category.age_min !== undefined &&
1254 category.age_max !== undefined
1255 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
1256 : category.age_min !== undefined
1257 ? `${category.age_min}+ ${__("years", "yatra")}`
1258 : category.age_max !== undefined
1259 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
1260 : ""
1261 : null;
1262
1263 return (
1264 <button
1265 key={category.id}
1266 type="button"
1267 onClick={() => {
1268 handlePriceTypeAdd(category.id);
1269 setShowCategorySelector(false);
1270 }}
1271 className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
1272 >
1273 <div className="font-medium text-sm text-gray-900 dark:text-white">
1274 {category.label}
1275 {ageRange && (
1276 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1277 ({ageRange})
1278 </span>
1279 )}
1280 </div>
1281 {category.description && (
1282 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
1283 {category.description}
1284 </div>
1285 )}
1286 </button>
1287 );
1288 })}
1289 </div>
1290 )}
1291 </div>
1292 </div>
1293 </>
1294 )}
1295 </div>
1296
1297 {/* Added Pricing List */}
1298 {formData.price_types.length > 0 && (
1299 <div className="space-y-3">
1300 {formData.price_types.map((priceType, index) => {
1301 const category = activeCategories.find(
1302 (cat) => cat.id === priceType.category_id,
1303 );
1304 if (!category) return null;
1305
1306 return (
1307 <div
1308 key={priceType.category_id}
1309 className="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg"
1310 >
1311 <div className="flex items-start justify-between mb-3">
1312 <div className="flex-1">
1313 <div className="flex items-center gap-2 mb-1">
1314 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
1315 {category.label}
1316 {(category.age_min !== undefined ||
1317 category.age_max !== undefined) && (
1318 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1319 (
1320 {category.age_min !== undefined &&
1321 category.age_max !== undefined
1322 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
1323 : category.age_min !== undefined
1324 ? `${category.age_min}+ ${__("years", "yatra")}`
1325 : category.age_max !== undefined
1326 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
1327 : ""}
1328 )
1329 </span>
1330 )}
1331 </h4>
1332 </div>
1333 <p className="text-xs text-gray-600 dark:text-gray-400">
1334 {category.description}
1335 </p>
1336 </div>
1337 <button
1338 type="button"
1339 onClick={() =>
1340 handlePriceTypeRemove(category.id)
1341 }
1342 className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
1343 title={__("Remove Pricing", "yatra")}
1344 >
1345 <X className="w-4 h-4" />
1346 </button>
1347 </div>
1348 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1349 <div>
1350 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1351 {__("Original Price", "yatra")}{" "}
1352 <span className="text-gray-400 text-xs">
1353 ({__("Optional", "yatra")})
1354 </span>
1355 </label>
1356 <div className="relative">
1357 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1358 {getCurrencySymbol(
1359 tripData?.currency || "USD",
1360 )}
1361 </span>
1362 <Input
1363 type="number"
1364 step="0.01"
1365 min="0"
1366 value={priceType.original_price}
1367 onChange={(e) =>
1368 handlePriceTypeChange(
1369 category.id,
1370 "original_price",
1371 e.target.value,
1372 )
1373 }
1374 placeholder="0.00"
1375 className={`pl-7 ${errors[`price_type_${index}_original`] ? "border-red-500" : ""}`}
1376 />
1377 </div>
1378 {errors[`price_type_${index}_original`] && (
1379 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
1380 {errors[`price_type_${index}_original`]}
1381 </p>
1382 )}
1383 </div>
1384 <div>
1385 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1386 {__("Discounted Price", "yatra")} (
1387 {__("Optional", "yatra")})
1388 </label>
1389 <div className="relative">
1390 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1391 {getCurrencySymbol(
1392 tripData?.currency || "USD",
1393 )}
1394 </span>
1395 <Input
1396 type="number"
1397 step="0.01"
1398 min="0"
1399 value={priceType.discounted_price}
1400 onChange={(e) =>
1401 handlePriceTypeChange(
1402 category.id,
1403 "discounted_price",
1404 e.target.value,
1405 )
1406 }
1407 placeholder="0.00"
1408 className={`pl-7 ${errors[`price_type_${index}_discounted`] ? "border-red-500" : ""}`}
1409 />
1410 </div>
1411 {errors[`price_type_${index}_discounted`] && (
1412 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
1413 {errors[`price_type_${index}_discounted`]}
1414 </p>
1415 )}
1416 </div>
1417 </div>
1418 </div>
1419 );
1420 })}
1421 </div>
1422 )}
1423 {errors.price_types && (
1424 <p className="text-xs text-red-600 dark:text-red-400">
1425 {errors.price_types}
1426 </p>
1427 )}
1428 </div>
1429 )}
1430 </div>
1431 )}
1432
1433 {/* Inventory Management */}
1434 <div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
1435 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
1436 {__("Inventory Management", "yatra")}
1437 </h3>
1438 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1439 <div>
1440 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1441 {__("Total Capacity", "yatra")}{" "}
1442 <span className="text-red-500">*</span>
1443 </label>
1444 <Input
1445 type="number"
1446 min="1"
1447 value={formData.total_seats}
1448 onChange={(e) =>
1449 handleFieldChange("total_seats", e.target.value)
1450 }
1451 placeholder="20"
1452 className={errors.total_seats ? "border-red-500" : ""}
1453 />
1454 {errors.total_seats && (
1455 <p className="mt-1 text-xs text-red-600">
1456 {errors.total_seats}
1457 </p>
1458 )}
1459 <HelpText
1460 text={__(
1461 "Maximum number of seats available for this date",
1462 "yatra",
1463 )}
1464 className="mt-1"
1465 />
1466 </div>
1467 {isEditMode && (
1468 <div>
1469 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1470 {__("Booked Seats", "yatra")}
1471 </label>
1472 <Input
1473 type="number"
1474 min="0"
1475 value={formData.booked_seats}
1476 readOnly
1477 className="bg-gray-50 dark:bg-gray-800 cursor-not-allowed"
1478 />
1479 <HelpText
1480 text={__("Currently booked seats (read-only)", "yatra")}
1481 className="mt-1"
1482 />
1483 </div>
1484 )}
1485 <div>
1486 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1487 {__("Alert Threshold", "yatra")}
1488 </label>
1489 <Input
1490 type="number"
1491 min="1"
1492 value={formData.alert_threshold}
1493 onChange={(e) =>
1494 handleFieldChange("alert_threshold", e.target.value)
1495 }
1496 placeholder="5"
1497 />
1498 <HelpText
1499 text={__(
1500 "Alert when available seats drop below this number",
1501 "yatra",
1502 )}
1503 className="mt-1"
1504 />
1505 </div>
1506 </div>
1507 </div>
1508
1509 {/* Block Date */}
1510 <div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
1511 <div className="flex items-center justify-between mb-4">
1512 <h3 className="text-sm font-semibold text-gray-900 dark:text-white">
1513 {__("Block Date", "yatra")}
1514 </h3>
1515 <label className="flex items-center gap-2 cursor-pointer">
1516 <input
1517 type="checkbox"
1518 checked={formData.is_blocked}
1519 onChange={(e) => {
1520 handleFieldChange("is_blocked", e.target.checked);
1521 if (e.target.checked) {
1522 handleFieldChange("status", "blocked");
1523 } else {
1524 handleFieldChange("status", "available");
1525 }
1526 }}
1527 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1528 />
1529 <span className="text-sm text-gray-700 dark:text-gray-300">
1530 {__("Block this date from bookings", "yatra")}
1531 </span>
1532 </label>
1533 </div>
1534 {formData.is_blocked && (
1535 <div>
1536 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1537 {__("Block Reason", "yatra")}
1538 </label>
1539 <Input
1540 type="text"
1541 value={formData.block_reason}
1542 onChange={(e) =>
1543 handleFieldChange("block_reason", e.target.value)
1544 }
1545 placeholder={__(
1546 "e.g., Maintenance, Holiday, Special Event",
1547 "yatra",
1548 )}
1549 />
1550 <HelpText
1551 text={__(
1552 "Reason for blocking this date (optional but recommended)",
1553 "yatra",
1554 )}
1555 className="mt-1"
1556 />
1557 </div>
1558 )}
1559 </div>
1560
1561 {/* Status */}
1562 <div className="mt-4">
1563 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1564 {__("Status", "yatra")}
1565 </label>
1566 <Select
1567 value={formData.status}
1568 onChange={(e) => {
1569 handleFieldChange("status", e.target.value);
1570 if (e.target.value === "blocked") {
1571 handleFieldChange("is_blocked", true);
1572 } else if (
1573 formData.is_blocked &&
1574 e.target.value !== "blocked"
1575 ) {
1576 handleFieldChange("is_blocked", false);
1577 }
1578 }}
1579 disabled={formData.is_blocked}
1580 >
1581 <option value="available">{__("Available", "yatra")}</option>
1582 <option value="limited">{__("Limited", "yatra")}</option>
1583 <option value="sold_out">{__("Sold Out", "yatra")}</option>
1584 <option value="closed">{__("Closed", "yatra")}</option>
1585 <option value="blocked">{__("Blocked", "yatra")}</option>
1586 </Select>
1587 {formData.is_blocked && (
1588 <HelpText
1589 text={__(
1590 'Status is automatically set to "Blocked" when date is blocked',
1591 "yatra",
1592 )}
1593 className="mt-1"
1594 />
1595 )}
1596 </div>
1597 </CardContent>
1598 </Card>
1599
1600 {errors.submit && (
1601 <Alert variant="error">
1602 <AlertCircle className="w-4 h-4" />
1603 <p>{errors.submit}</p>
1604 </Alert>
1605 )}
1606
1607 <div className="flex items-center justify-end gap-4">
1608 <Button
1609 type="button"
1610 variant="outline"
1611 onClick={() => {
1612 if (tripId) {
1613 navigate({
1614 subpage: "trips",
1615 tab: "availability",
1616 trip_id: tripId.toString(),
1617 });
1618 } else {
1619 navigate({ subpage: "trips", tab: "availability" });
1620 }
1621 }}
1622 >
1623 {__("Cancel", "yatra")}
1624 </Button>
1625 <Button type="submit" disabled={saveMutation.isPending}>
1626 {saveMutation.isPending ? (
1627 <>
1628 <div className="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
1629 {__("Saving...", "yatra")}
1630 </>
1631 ) : (
1632 <>
1633 <Save className="w-4 h-4 mr-2" />
1634 {__("Save Availability", "yatra")}
1635 </>
1636 )}
1637 </Button>
1638 </div>
1639 </form>
1640 </div>
1641 );
1642 };
1643
1644 export default AvailabilityForm;
1645