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

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