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

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