PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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
← All changes | resources/js/pages/RecurringRuleForm.tsx +339 -106 3.0.2.9 → 3.0.16 View file →
@@ -18,8 +18,9 @@
18 18 MapPin,
19 19 CheckCircle2,
20 20 } from "lucide-react";
21 21 import { __ } from "../lib/i18n";
22 +import { toDateValue, todayYmd } from "../lib/dateFormat";
22 23 import { Button } from "../components/ui/button";
23 24 import { Input } from "../components/ui/input";
24 25 import { Select } from "../components/ui/select";
25 26 import { SearchableSelect } from "../components/ui/searchable-select";
@@ -86,8 +87,26 @@
86 87 sale_price?: number;
87 88 traveler_pricing?: TravelerPricing[];
88 89 }
89 90
91 +/**
92 + * One entry in a rule's exclusion list: either a single day ("2027-03-08", the
93 + * original format) or an inclusive period. Both are stored in the same
94 + * `excluded_dates` column, so rules saved before periods existed load as-is.
95 + */
96 +type ExcludedEntry = string | { start: string; end: string };
97 +
98 +const isPeriod = (e: ExcludedEntry): e is { start: string; end: string } =>
99 + typeof e === "object" && e !== null && "start" in e && "end" in e;
100 +
101 +/** Sort key: a period sorts by its first day, alongside single dates. */
102 +const excludedSortKey = (e: ExcludedEntry): string =>
103 + isPeriod(e) ? e.start : e;
104 +
105 +/** Stable identity for list keys and removal, for both shapes. */
106 +const excludedId = (e: ExcludedEntry): string =>
107 + isPeriod(e) ? `${e.start}..${e.end}` : e;
108 +
90 109 interface RecurringRule {
91 110 id?: number;
92 111 trip_id: number;
93 112 name: string;
@@ -97,9 +116,9 @@
97 116 day_of_week?: number;
98 117 interval_days?: number;
99 118 start_date: string;
100 119 end_date?: string;
101 - excluded_dates: string[];
120 + excluded_dates: ExcludedEntry[];
102 121 months: number[]; // Array of month numbers (1-12) to filter by
103 122 time_slots: TimeSlot[]; // For single-day trips with multiple slots
104 123 pricing_type: "regular" | "traveler_based"; // Allow override of trip's pricing type
105 124 original_price?: number;
@@ -156,9 +175,9 @@
156 175 days_of_week: [0], // Sunday by default
157 176 week_of_month: "first",
158 177 day_of_week: 0,
159 178 interval_days: 7,
160 - start_date: new Date().toISOString().split("T")[0],
179 + start_date: todayYmd(),
161 180 end_date: "",
162 181 excluded_dates: [],
163 182 months: [], // Empty = all months, otherwise specific months (1-12)
164 183 time_slots: [], // For single-day trips with multiple slots
@@ -180,8 +199,11 @@
180 199 status: "active",
181 200 });
182 201
183 202 const [newExcludedDate, setNewExcludedDate] = useState("");
203 + const [newExcludedStart, setNewExcludedStart] = useState("");
204 + const [newExcludedEnd, setNewExcludedEnd] = useState("");
205 + const [excludedError, setExcludedError] = useState("");
184 206 const [previewData, setPreviewData] = useState<{
185 207 total: number;
186 208 dates: any[];
187 209 } | null>(null);
@@ -194,19 +216,33 @@
194 216 const response = await apiClient.get("/trips", {
195 217 params: { per_page: 100, status: "publish" },
196 218 });
197 219 return {
198 - trips: (response?.data || []).map((trip: any) => ({
199 - id: Number(trip.id) || 0,
200 - title: trip.title,
201 - trip_type:
202 - trip.trip_type ||
203 - (trip.duration_days <= 1 ? "single_day" : "multi_day"),
204 - duration_days: trip.duration_days || 1,
205 - starting_location: trip.starting_location,
206 - ending_location: trip.ending_location,
207 - pricing_type: trip.pricing_type || "regular",
208 - })) as Trip[],
220 + trips: (response?.data || []).map((trip: any) => {
221 + // Some endpoints return `pricing_type` as "regular" even when the trip is
222 + // effectively traveler-based (price types configured). Infer the effective
223 + // pricing type from `price_types` when present so the Rules UI reflects
224 + // real trip configuration.
225 + const rawPriceTypes = trip.price_types;
226 + const hasTravelerPricing = Array.isArray(rawPriceTypes)
227 + ? rawPriceTypes.length > 0
228 + : false;
229 + const effectivePricingType = hasTravelerPricing
230 + ? "traveler_based"
231 + : trip.pricing_type || "regular";
232 +
233 + return {
234 + id: Number(trip.id) || 0,
235 + title: trip.title,
236 + trip_type:
237 + trip.trip_type ||
238 + (trip.duration_days <= 1 ? "single_day" : "multi_day"),
239 + duration_days: trip.duration_days || 1,
240 + starting_location: trip.starting_location,
241 + ending_location: trip.ending_location,
242 + pricing_type: effectivePricingType,
243 + };
244 + }) as Trip[],
209 245 };
210 246 },
211 247 });
212 248
@@ -305,8 +341,23 @@
305 341 | "weekly"
306 342 | "monthly"
307 343 | "interval";
308 344
345 + // Prefer the *trip's effective* pricing type over any stale rule.pricing_type.
346 + // Trips can be traveler-based simply by having price_types configured, even if
347 + // trip.pricing_type is still "regular".
348 + const tripRow = tripsData.trips.find(
349 + (t) => t.id === Number(existingRule.trip_id),
350 + );
351 + const effectivePricingType =
352 + tripRow?.pricing_type ||
353 + ((tripForLocations as any)?.price_types &&
354 + Array.isArray((tripForLocations as any).price_types) &&
355 + (tripForLocations as any).price_types.length > 0
356 + ? "traveler_based"
357 + : (tripForLocations as any)?.pricing_type) ||
358 + "regular";
359 +
309 360 setFormData({
310 361 trip_id: existingRule.trip_id || 0,
311 362 name: existingRule.name || "",
312 363 rule_type: ruleType,
@@ -313,10 +364,9 @@
313 364 days_of_week: daysOfWeek.length > 0 ? daysOfWeek : [0],
314 365 week_of_month: existingRule.week_of_month || "first",
315 366 day_of_week: existingRule.day_of_week ?? 0,
316 367 interval_days: existingRule.interval_days || 7,
317 - start_date:
318 - existingRule.start_date || new Date().toISOString().split("T")[0],
368 + start_date: existingRule.start_date || todayYmd(),
319 369 end_date: existingRule.end_date || "",
320 370 excluded_dates: Array.isArray(existingRule.excluded_dates)
321 371 ? existingRule.excluded_dates
322 372 : [],
@@ -323,9 +373,9 @@
323 373 months: Array.isArray(existingRule.months) ? existingRule.months : [],
324 374 time_slots: Array.isArray(existingRule.time_slots)
325 375 ? existingRule.time_slots
326 376 : [],
327 - pricing_type: existingRule.pricing_type || "regular",
377 + pricing_type: effectivePricingType as "regular" | "traveler_based",
328 378 original_price: existingRule.original_price,
329 379 sale_price: existingRule.sale_price,
330 380 traveler_pricing: Array.isArray(existingRule.traveler_pricing)
331 381 ? existingRule.traveler_pricing
@@ -343,21 +393,31 @@
343 393 alert_threshold: existingRule.alert_threshold || 5,
344 394 status: existingRule.status || "active",
345 395 });
346 396 }
347 - }, [existingRule, tripsData]);
397 + }, [existingRule, tripsData, tripForLocations]);
348 398
349 399 // Set pricing type based on selected trip when not editing
350 400 useEffect(() => {
351 401 if (!isEditing && !existingRule) {
402 + const inferred = (() => {
403 + const priceTypes = (tripForLocations as any)?.price_types;
404 + const hasTravelerPricing =
405 + Array.isArray(priceTypes) && priceTypes.length > 0;
406 + return (
407 + hasTravelerPricing
408 + ? "traveler_based"
409 + : selectedTrip?.pricing_type ||
410 + (tripForLocations as Trip)?.pricing_type ||
411 + "regular"
412 + ) as "regular" | "traveler_based";
413 + })();
414 +
352 415 setFormData((prev) => ({
353 416 ...prev,
354 417 ...(selectedTrip || tripForLocations
355 418 ? {
356 - pricing_type: (selectedTrip?.pricing_type ||
357 - (tripForLocations as Trip)?.pricing_type ||
358 - "regular" ||
359 - "regular") as "regular" | "traveler_based",
419 + pricing_type: inferred,
360 420 }
361 421 : {}),
362 422 ...(tripForLocations
363 423 ? {
@@ -389,11 +449,20 @@
389 449
390 450 // Create mutation
391 451 const createMutation = useMutation({
392 452 mutationFn: async (data: RecurringRule) => {
453 + // `days_of_week` is a JSON column on the backend; send the array as-is
454 + // and let the API JSON-encode it. Sending a CSV string (e.g. "0,1,2,3")
455 + // is rejected by MySQL with "Invalid JSON text".
393 456 return await apiClient.post("/recurring-availability", {
394 457 ...data,
395 - days_of_week: data.days_of_week.join(","),
458 + days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [],
459 + // Normalize month-week selector to backend contract.
460 + week_of_month:
461 + data.rule_type === "monthly"
462 + ? ((data.week_of_month || "first") as string).toLowerCase()
463 + : data.week_of_month,
464 + // On create, omit empty arrays to keep payload small.
396 465 time_slots: data.time_slots.length > 0 ? data.time_slots : undefined,
397 466 traveler_pricing:
398 467 data.traveler_pricing && data.traveler_pricing.length > 0
399 468 ? data.traveler_pricing
@@ -421,14 +490,19 @@
421 490 const updateMutation = useMutation({
422 491 mutationFn: async (data: RecurringRule) => {
423 492 return await apiClient.put(`/recurring-availability/${ruleId}`, {
424 493 ...data,
425 - days_of_week: data.days_of_week.join(","),
426 - time_slots: data.time_slots.length > 0 ? data.time_slots : undefined,
427 - traveler_pricing:
428 - data.traveler_pricing && data.traveler_pricing.length > 0
429 - ? data.traveler_pricing
430 - : undefined,
494 + days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [],
495 + week_of_month:
496 + data.rule_type === "monthly"
497 + ? ((data.week_of_month || "first") as string).toLowerCase()
498 + : data.week_of_month,
499 + // IMPORTANT: on update, send empty arrays explicitly to clear persisted
500 + // JSON columns; omitting the key leaves the old value in DB.
501 + time_slots: Array.isArray(data.time_slots) ? data.time_slots : [],
502 + traveler_pricing: Array.isArray(data.traveler_pricing)
503 + ? data.traveler_pricing
504 + : [],
431 505 });
432 506 },
433 507 onSuccess: () => {
434 508 queryClient.invalidateQueries({ queryKey: ["recurring-availability"] });
@@ -451,9 +525,13 @@
451 525 const previewMutation = useMutation({
452 526 mutationFn: async (data: RecurringRule) => {
453 527 return await apiClient.post("/recurring-availability/preview", {
454 528 ...data,
455 - days_of_week: data.days_of_week.join(","),
529 + days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [],
530 + week_of_month:
531 + data.rule_type === "monthly"
532 + ? ((data.week_of_month || "first") as string).toLowerCase()
533 + : data.week_of_month,
456 534 time_slots: data.time_slots.length > 0 ? data.time_slots : undefined,
457 535 traveler_pricing:
458 536 data.traveler_pricing && data.traveler_pricing.length > 0
459 537 ? data.traveler_pricing
@@ -507,24 +585,79 @@
507 585 : [...prev.days_of_week, day].sort((a, b) => a - b),
508 586 }));
509 587 };
510 588
511 - // Add excluded date
589 + const sortExcluded = (list: ExcludedEntry[]): ExcludedEntry[] =>
590 + [...list].sort((a, b) => excludedSortKey(a).localeCompare(excludedSortKey(b)));
591 +
592 + // Add a single excluded day
512 593 const addExcludedDate = () => {
513 - if (newExcludedDate && !formData.excluded_dates.includes(newExcludedDate)) {
594 + if (!newExcludedDate) {
595 + return;
596 + }
597 + const exists = formData.excluded_dates.some(
598 + (e) => excludedId(e) === newExcludedDate,
599 + );
600 + if (exists) {
601 + setExcludedError(__("That date is already excluded.", "yatra"));
602 + return;
603 + }
604 + setExcludedError("");
605 + setFormData((prev) => ({
606 + ...prev,
607 + excluded_dates: sortExcluded([...prev.excluded_dates, newExcludedDate]),
608 + }));
609 + setNewExcludedDate("");
610 + };
611 +
612 + // Add an excluded period — one entry covering every day from start to end,
613 + // so a shutdown doesn't have to be added a day at a time.
614 + const addExcludedPeriod = () => {
615 + if (!newExcludedStart || !newExcludedEnd) {
616 + return;
617 + }
618 + // Accept the two dates in either order rather than rejecting the input.
619 + const start =
620 + newExcludedStart <= newExcludedEnd ? newExcludedStart : newExcludedEnd;
621 + const end =
622 + newExcludedStart <= newExcludedEnd ? newExcludedEnd : newExcludedStart;
623 +
624 + if (start === end) {
625 + // A one-day period is just a single date; keep the list in its simplest form.
626 + if (formData.excluded_dates.some((e) => excludedId(e) === start)) {
627 + setExcludedError(__("That date is already excluded.", "yatra"));
628 + return;
629 + }
630 + setExcludedError("");
514 631 setFormData((prev) => ({
515 632 ...prev,
516 - excluded_dates: [...prev.excluded_dates, newExcludedDate].sort(),
633 + excluded_dates: sortExcluded([...prev.excluded_dates, start]),
517 634 }));
518 - setNewExcludedDate("");
635 + setNewExcludedStart("");
636 + setNewExcludedEnd("");
637 + return;
519 638 }
639 +
640 + const id = `${start}..${end}`;
641 + if (formData.excluded_dates.some((e) => excludedId(e) === id)) {
642 + setExcludedError(__("That period is already excluded.", "yatra"));
643 + return;
644 + }
645 + setExcludedError("");
646 + setFormData((prev) => ({
647 + ...prev,
648 + excluded_dates: sortExcluded([...prev.excluded_dates, { start, end }]),
649 + }));
650 + setNewExcludedStart("");
651 + setNewExcludedEnd("");
520 652 };
521 653
522 - // Remove excluded date
523 - const removeExcludedDate = (date: string) => {
654 + // Remove an excluded day or period
655 + const removeExcluded = (id: string) => {
656 + setExcludedError("");
524 657 setFormData((prev) => ({
525 658 ...prev,
526 - excluded_dates: prev.excluded_dates.filter((d) => d !== date),
659 + excluded_dates: prev.excluded_dates.filter((e) => excludedId(e) !== id),
527 660 }));
528 661 };
529 662
530 663 const isLoading = createMutation.isPending || updateMutation.isPending;
@@ -779,9 +912,9 @@
779 912 setFormData((prev) => ({ ...prev, end_date: value }))
780 913 }
781 914 minDate={
782 915 formData.start_date
783 - ? new Date(formData.start_date)
916 + ? toDateValue(formData.start_date)
784 917 : undefined
785 918 }
786 919 placeholder={__("Select end date (optional)", "yatra")}
787 920 />
@@ -850,9 +983,9 @@
850 983 </p>
851 984 )}
852 985 </div>
853 986
854 - {/* Excluded Dates */}
987 + {/* Excluded Dates & Periods */}
855 988 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
856 989 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
857 990 {__("Excluded Dates", "yatra")}{" "}
858 991 <span className="text-gray-400">
@@ -875,34 +1008,86 @@
875 1008 >
876 1009 <Plus className="w-4 h-4" />
877 1010 </Button>
878 1011 </div>
1012 +
1013 + {/* A whole period in one go — a shutdown shouldn't have to be
1014 + added a day at a time. Named "Periods" so it can't be
1015 + confused with the rule's own Date Range above. */}
1016 + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 mt-4">
1017 + {__("Excluded Periods", "yatra")}{" "}
1018 + <span className="text-gray-400">
1019 + ({__("holiday closures, vacations", "yatra")})
1020 + </span>
1021 + </label>
1022 + <div className="flex flex-col sm:flex-row gap-2 mb-1">
1023 + <div className="flex-1">
1024 + <DatePicker
1025 + value={newExcludedStart}
1026 + onChange={(value: string) => setNewExcludedStart(value)}
1027 + placeholder={__("First day", "yatra")}
1028 + />
1029 + </div>
1030 + <div className="flex-1">
1031 + <DatePicker
1032 + value={newExcludedEnd}
1033 + onChange={(value: string) => setNewExcludedEnd(value)}
1034 + placeholder={__("Last day", "yatra")}
1035 + />
1036 + </div>
1037 + <Button
1038 + type="button"
1039 + variant="outline"
1040 + onClick={addExcludedPeriod}
1041 + disabled={!newExcludedStart || !newExcludedEnd}
1042 + >
1043 + <Plus className="w-4 h-4" />
1044 + </Button>
1045 + </div>
1046 + <p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
1047 + {__(
1048 + "Both days are included. Every date in between is skipped by this rule.",
1049 + "yatra",
1050 + )}
1051 + </p>
1052 +
1053 + {excludedError && (
1054 + <p className="text-xs text-red-600 dark:text-red-400 mb-2">
1055 + {excludedError}
1056 + </p>
1057 + )}
1058 +
879 1059 {formData.excluded_dates.length > 0 && (
880 1060 <div className="flex flex-wrap gap-2">
881 - {formData.excluded_dates.map((date) => (
882 - <Badge
883 - key={date}
884 - variant="outline"
885 - className="flex items-center gap-1"
886 - >
887 - {new Date(date + "T00:00:00").toLocaleDateString(
888 - "en-US",
889 - {
890 - weekday: "short",
891 - month: "short",
892 - day: "numeric",
893 - year: "numeric",
894 - },
895 - )}
896 - <button
897 - type="button"
898 - onClick={() => removeExcludedDate(date)}
899 - className="ml-1 hover:text-red-500"
1061 + {formData.excluded_dates.map((entry) => {
1062 + const id = excludedId(entry);
1063 + const fmt = (d: string) =>
1064 + new Date(d + "T00:00:00").toLocaleDateString("en-US", {
1065 + weekday: "short",
1066 + month: "short",
1067 + day: "numeric",
1068 + year: "numeric",
1069 + });
1070 + return (
1071 + <Badge
1072 + key={id}
1073 + variant="outline"
1074 + className="flex items-center gap-1"
900 1075 >
901 - <X className="w-3 h-3" />
902 - </button>
903 - </Badge>
904 - ))}
1076 + {isPeriod(entry)
1077 + ? `${fmt(entry.start)} → ${fmt(entry.end)}`
1078 + : fmt(entry)}
1079 + <button
1080 + type="button"
1081 + onClick={() => removeExcluded(id)}
1082 + className="ml-1 hover:text-red-500"
1083 + aria-label={__("Remove exclusion", "yatra")}
1084 + >
1085 + <X className="w-3 h-3" />
1086 + </button>
1087 + </Badge>
1088 + );
1089 + })}
905 1090 </div>
906 1091 )}
907 1092 </div>
908 1093 </CardContent>
@@ -1299,17 +1484,22 @@
1299 1484 )}
1300 1485 </div>
1301 1486 <button
1302 1487 type="button"
1303 - onClick={() =>
1488 + onClick={() => {
1489 + const categoryIdToRemove =
1490 + pricing.category_id;
1304 1491 setFormData((prev) => ({
1305 1492 ...prev,
1306 - traveler_pricing:
1307 - prev.traveler_pricing?.filter(
1308 - (_, i) => i !== index,
1309 - ) || [],
1310 - }))
1311 - }
1493 + traveler_pricing: (
1494 + prev.traveler_pricing || []
1495 + ).filter(
1496 + (tp) =>
1497 + tp.category_id !==
1498 + categoryIdToRemove,
1499 + ),
1500 + }));
1501 + }}
1312 1502 className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
1313 1503 title={__(
1314 1504 "Remove Pricing",
1315 1505 "yatra",
@@ -1946,33 +2136,37 @@
1946 2136 </div>
1947 2137 <button
1948 2138 type="button"
1949 2139 onClick={() => {
1950 - const newSlots = [
1951 - ...formData.time_slots,
1952 - ];
1953 - if (
1954 - newSlots[index] &&
1955 - newSlots[index]
1956 - .traveler_pricing
1957 - ) {
1958 - newSlots[
1959 - index
1960 - ].traveler_pricing =
1961 - newSlots[
1962 - index
1963 - ].traveler_pricing!.filter(
1964 - (_, i) =>
1965 - i !== tpIndex,
1966 - );
1967 - setFormData(
1968 - (prev) => ({
1969 - ...prev,
1970 - time_slots:
1971 - newSlots,
1972 - }),
1973 - );
1974 - }
2140 + const categoryIdToRemove =
2141 + tp.category_id;
2142 + setFormData((prev) => {
2143 + const newSlots = [
2144 + ...prev.time_slots,
2145 + ];
2146 + if (
2147 + !newSlots[index]
2148 + ) {
2149 + return prev;
2150 + }
2151 + newSlots[index] = {
2152 + ...newSlots[index],
2153 + traveler_pricing: (
2154 + newSlots[index]
2155 + .traveler_pricing ||
2156 + []
2157 + ).filter(
2158 + (p) =>
2159 + p.category_id !==
2160 + categoryIdToRemove,
2161 + ),
2162 + };
2163 + return {
2164 + ...prev,
2165 + time_slots:
2166 + newSlots,
2167 + };
2168 + });
1975 2169 }}
1976 2170 className="p-0.5 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
1977 2171 >
1978 2172 <X className="w-3 h-3" />
@@ -2260,11 +2454,18 @@
2260 2454 tripForLocations.starting_longitude,
2261 2455 ),
2262 2456 ),
2263 2457 ]
2264 - : [-8.3405, 115.092]
2458 + : [20, 0]
2265 2459 }
2266 - defaultZoom={13}
2460 + defaultZoom={
2461 + formData.from_latitude && formData.from_longitude
2462 + ? 13
2463 + : tripForLocations?.starting_latitude &&
2464 + tripForLocations?.starting_longitude
2465 + ? 13
2466 + : 2
2467 + }
2267 2468 mapHeight="300px"
2268 2469 showMapButton={false}
2269 2470 searchLimit={8}
2270 2471 __={__}
@@ -2345,11 +2546,44 @@
2345 2546 tripForLocations.ending_longitude,
2346 2547 ),
2347 2548 ),
2348 2549 ]
2349 - : [-8.5069, 115.2625]
2550 + : formData.from_latitude &&
2551 + formData.from_longitude
2552 + ? [
2553 + parseFloat(formData.from_latitude),
2554 + parseFloat(formData.from_longitude),
2555 + ]
2556 + : tripForLocations?.starting_latitude &&
2557 + tripForLocations?.starting_longitude
2558 + ? [
2559 + parseFloat(
2560 + String(
2561 + tripForLocations.starting_latitude,
2562 + ),
2563 + ),
2564 + parseFloat(
2565 + String(
2566 + tripForLocations.starting_longitude,
2567 + ),
2568 + ),
2569 + ]
2570 + : [20, 0]
2350 2571 }
2351 - defaultZoom={13}
2572 + defaultZoom={
2573 + formData.to_latitude && formData.to_longitude
2574 + ? 13
2575 + : tripForLocations?.ending_latitude &&
2576 + tripForLocations?.ending_longitude
2577 + ? 13
2578 + : formData.from_latitude &&
2579 + formData.from_longitude
2580 + ? 13
2581 + : tripForLocations?.starting_latitude &&
2582 + tripForLocations?.starting_longitude
2583 + ? 13
2584 + : 2
2585 + }
2352 2586 mapHeight="300px"
2353 2587 showMapButton={false}
2354 2588 searchLimit={8}
2355 2589 __={__}
@@ -2454,17 +2688,16 @@
2454 2688 key={index}
2455 2689 className="text-xs px-2 py-1 bg-gray-50 dark:bg-gray-800 rounded flex justify-between"
2456 2690 >
2457 2691 <span>
2458 - {new Date(date.departure_date).toLocaleDateString(
2459 - "en-US",
2460 - {
2461 - weekday: "short",
2462 - month: "short",
2463 - day: "numeric",
2464 - year: "numeric",
2465 - },
2466 - )}
2692 + {toDateValue(
2693 + date.departure_date,
2694 + ).toLocaleDateString("en-US", {
2695 + weekday: "short",
2696 + month: "short",
2697 + day: "numeric",
2698 + year: "numeric",
2699 + })}
2467 2700 </span>
2468 2701 {date.departure_time && (
2469 2702 <span className="text-gray-500">
2470 2703 {date.departure_time}