| @@ -39,8 +39,9 @@ | ||
| 39 | 39 | Plus, |
| 40 | 40 | X, |
| 41 | 41 | } from "lucide-react"; |
| 42 | 42 | import { __ } from "../lib/i18n"; |
| 43 | +import { formatDateForInput, toDateValue } from "../lib/dateFormat"; | |
| 43 | 44 | import { usePermissions } from "../hooks/usePermissions"; |
| 44 | 45 | import { TodaysBriefCard } from "../components/ai/TodaysBriefCard"; |
| 45 | 46 | import { StatCard } from "../components/common/StatCard"; |
| 46 | 47 | import { |
| @@ -162,11 +163,10 @@ | ||
| 162 | 163 | // below the hook calls; every useQuery now reads `enabled: isWpAdmin` |
| 163 | 164 | // so non-admin renders don't fire the admin-only endpoints, and the |
| 164 | 165 | // RoleDashboard branch returns immediately after all hooks are |
| 165 | 166 | // registered. |
| 166 | - const isWpAdmin = !!( | |
| 167 | - window.yatraAdmin as { isWpAdmin?: boolean } | undefined | |
| 168 | - )?.isWpAdmin; | |
| 167 | + const isWpAdmin = !!(window.yatraAdmin as { isWpAdmin?: boolean } | undefined) | |
| 168 | + ?.isWpAdmin; | |
| 169 | 169 | |
| 170 | 170 | const { can } = usePermissions(); |
| 171 | 171 | |
| 172 | 172 | const [range, setRange] = useState<DashboardRange>("all_time"); |
| @@ -283,9 +283,11 @@ | ||
| 283 | 283 | const amounts: Record<string, number> = {}; |
| 284 | 284 | |
| 285 | 285 | items.forEach((b) => { |
| 286 | 286 | const dateStr = b.created_at || b.travel_date; |
| 287 | - const date = dateStr ? new Date(dateStr) : null; | |
| 287 | + // toDateValue: a date-only travel_date fallback (e.g. "2026-08-01") must | |
| 288 | + // bucket in its LOCAL month, not roll back to July in a behind-UTC zone. | |
| 289 | + const date = dateStr ? toDateValue(dateStr) : null; | |
| 288 | 290 | if (!date || Number.isNaN(date.getTime())) return; |
| 289 | 291 | const ym = `${date.getFullYear()}-${pad(date.getMonth() + 1)}`; |
| 290 | 292 | counts[ym] = (counts[ym] || 0) + 1; |
| 291 | 293 | const amount = Number(b.total_amount ?? 0) || 0; |
| @@ -392,17 +394,25 @@ | ||
| 392 | 394 | const { data: departures } = useQuery({ |
| 393 | 395 | queryKey: ["dashboard-upcoming-departures"], |
| 394 | 396 | queryFn: async () => { |
| 395 | 397 | const today = new Date(); |
| 396 | - const todayStr = today.toISOString().split("T")[0]; | |
| 398 | + const todayStr = formatDateForInput(today); | |
| 399 | + // Fetch all FUTURE departures, not only status === "upcoming". A departure | |
| 400 | + // that a booking has filled flips to status "full" (booked >= capacity), | |
| 401 | + // and a single group-of-x booking fills a small/private departure in one | |
| 402 | + // go — so filtering to "upcoming" here silently dropped fully-booked | |
| 403 | + // upcoming tours from the dashboard while partially-booked ones stayed. We | |
| 404 | + // keep upcoming + full (a full trip is still an upcoming trip, just sold | |
| 405 | + // out) and exclude only cancelled/trash/past. | |
| 397 | 406 | const response = await apiClient.get("/departures", { |
| 398 | 407 | params: { |
| 399 | - status: "upcoming", | |
| 400 | 408 | date_from: todayStr, |
| 401 | 409 | include_past: false, |
| 402 | 410 | }, |
| 403 | 411 | }); |
| 404 | - const items = response?.data || []; | |
| 412 | + const items = (response?.data || []).filter( | |
| 413 | + (d: any) => d?.status === "upcoming" || d?.status === "full", | |
| 414 | + ); | |
| 405 | 415 | return items.map((d: any) => { |
| 406 | 416 | const tripTitle = d?.trip?.title || d?.trip_title || d?.title || ""; |
| 407 | 417 | const destination = |
| 408 | 418 | (d?.trip?.destinations && d.trip.destinations[0]?.name) || |
| @@ -407,9 +417,16 @@ | ||
| 407 | 417 | const destination = |
| 408 | 418 | (d?.trip?.destinations && d.trip.destinations[0]?.name) || |
| 409 | 419 | d?.destination || |
| 410 | 420 | undefined; |
| 421 | + // `/departures` returns max_capacity / available_capacity. Neither was | |
| 422 | + // listed here, so every card fell back to 0 capacity — which made | |
| 423 | + // available go negative (0 - bookings) and pinned occupancy at 0%, even | |
| 424 | + // though the trip page computed it correctly from the same rows. The | |
| 425 | + // API's own names come first; the rest remain as fallbacks for any other | |
| 426 | + // shape that reaches this mapper. | |
| 411 | 427 | const totalSpots = |
| 428 | + d?.max_capacity ?? | |
| 412 | 429 | d?.total_spots ?? |
| 413 | 430 | d?.capacity ?? |
| 414 | 431 | d?.total_seats ?? |
| 415 | 432 | d?.max_travelers ?? |
| @@ -414,12 +431,13 @@ | ||
| 414 | 431 | d?.total_seats ?? |
| 415 | 432 | d?.max_travelers ?? |
| 416 | 433 | 0; |
| 417 | 434 | const availableSpots = |
| 435 | + d?.available_capacity ?? | |
| 418 | 436 | d?.available_spots ?? |
| 419 | 437 | d?.available_seats ?? |
| 420 | 438 | d?.remaining_slots ?? |
| 421 | - totalSpots - (d?.bookings_count || 0); | |
| 439 | + totalSpots - (d?.booked_count ?? d?.bookings_count ?? 0); | |
| 422 | 440 | return { |
| 423 | 441 | id: d.id, |
| 424 | 442 | trip_id: d.trip_id || d?.trip?.id, |
| 425 | 443 | trip_title: tripTitle, |