| @@ -2,8 +2,9 @@ | ||
| 2 | 2 | import { useQuery } from "@tanstack/react-query"; |
| 3 | 3 | import { apiClient } from "../lib/api-client"; |
| 4 | 4 | import { API_ENDPOINTS } from "../lib/api-endpoints"; |
| 5 | 5 | import { __ } from "../lib/i18n"; |
| 6 | +import { toDateValue } from "../lib/dateFormat"; | |
| 6 | 7 | import { |
| 7 | 8 | LayoutDashboard, |
| 8 | 9 | Calendar, |
| 9 | 10 | CreditCard, |
| @@ -33,8 +34,9 @@ | ||
| 33 | 34 | import Payments from "./account/Payments"; |
| 34 | 35 | import Documents from "./account/Documents"; |
| 35 | 36 | import Profile from "./account/Profile"; |
| 36 | 37 | import SavedTrips from "./account/SavedTrips"; |
| 38 | +import { useToast } from "../components/ui/toast"; | |
| 37 | 39 | |
| 38 | 40 | const navigation: Array<{ |
| 39 | 41 | id: Section; |
| 40 | 42 | label: string; |
| @@ -49,8 +51,9 @@ | ||
| 49 | 51 | ]; |
| 50 | 52 | |
| 51 | 53 | const AccountPage: React.FC = () => { |
| 52 | 54 | const accountShell = useMemo(() => getYatraAccountPageGlobals(), []); |
| 55 | + const { showToast } = useToast(); | |
| 53 | 56 | |
| 54 | 57 | const accountNavigation = React.useMemo(() => { |
| 55 | 58 | const wl = |
| 56 | 59 | typeof window !== "undefined" && |
| @@ -142,8 +145,15 @@ | ||
| 142 | 145 | }; |
| 143 | 146 | |
| 144 | 147 | const [section, setSection] = useState<Section>(getSectionFromUrl); |
| 145 | 148 | |
| 149 | + // Cross-section booking selection. Lifted here from Bookings.tsx so | |
| 150 | + // Dashboard's upcoming-trip cards can deep-link straight into the | |
| 151 | + // booking detail screen — click on the dashboard card sets this id | |
| 152 | + // AND switches to the "bookings" section; the Bookings component | |
| 153 | + // honours `initialBookingId` on mount/update to open the detail view. | |
| 154 | + const [pendingBookingId, setPendingBookingId] = useState<number | null>(null); | |
| 155 | + | |
| 146 | 156 | // Update section when URL changes |
| 147 | 157 | React.useEffect(() => { |
| 148 | 158 | const newSection = getSectionFromUrl(); |
| 149 | 159 | setSection(newSection); |
| @@ -148,8 +158,42 @@ | ||
| 148 | 158 | const newSection = getSectionFromUrl(); |
| 149 | 159 | setSection(newSection); |
| 150 | 160 | }, [urlKey]); |
| 151 | 161 | |
| 162 | + // Surface the result of an email-change confirmation. The confirmation link | |
| 163 | + // (from WordPress-style verification email) redirects here with | |
| 164 | + // ?email_change=success|error; show feedback, open the Profile tab, then | |
| 165 | + // strip the param so a refresh doesn't repeat the toast. | |
| 166 | + React.useEffect(() => { | |
| 167 | + if (typeof window === "undefined") { | |
| 168 | + return; | |
| 169 | + } | |
| 170 | + const params = new URLSearchParams(window.location.search); | |
| 171 | + const outcome = params.get("email_change"); | |
| 172 | + if (outcome !== "success" && outcome !== "error") { | |
| 173 | + return; | |
| 174 | + } | |
| 175 | + if (outcome === "success") { | |
| 176 | + showToast(__("Your email address has been updated.", "yatra"), "success"); | |
| 177 | + setSection("profile"); | |
| 178 | + } else { | |
| 179 | + showToast( | |
| 180 | + __( | |
| 181 | + "This email confirmation link is invalid or has expired.", | |
| 182 | + "yatra", | |
| 183 | + ), | |
| 184 | + "error", | |
| 185 | + ); | |
| 186 | + } | |
| 187 | + params.delete("email_change"); | |
| 188 | + const query = params.toString(); | |
| 189 | + const cleanUrl = `${window.location.pathname}${query ? `?${query}` : ""}`; | |
| 190 | + window.history.replaceState({}, "", cleanUrl); | |
| 191 | + (window as any).__lastAccountSearch = window.location.search; | |
| 192 | + // Run once on mount. | |
| 193 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 194 | + }, []); | |
| 195 | + | |
| 152 | 196 | // Update URL and localStorage when section changes |
| 153 | 197 | const handleSectionChange = (newSection: Section) => { |
| 154 | 198 | setSection(newSection); |
| 155 | 199 | |
| @@ -167,8 +211,16 @@ | ||
| 167 | 211 | window.dispatchEvent(new PopStateEvent("popstate")); |
| 168 | 212 | } |
| 169 | 213 | }; |
| 170 | 214 | |
| 215 | + // Open a specific booking from anywhere (currently used by the | |
| 216 | + // Dashboard's "Upcoming Trips" cards). Switches to the bookings tab | |
| 217 | + // and seeds the id so Bookings opens straight into the detail view. | |
| 218 | + const handleBookingOpen = (bookingId: number) => { | |
| 219 | + setPendingBookingId(bookingId); | |
| 220 | + handleSectionChange("bookings"); | |
| 221 | + }; | |
| 222 | + | |
| 171 | 223 | // State moved to individual components |
| 172 | 224 | |
| 173 | 225 | const unwrapArrayResponse = (response: any): any[] => { |
| 174 | 226 | if (!response) return []; |
| @@ -388,9 +440,9 @@ | ||
| 388 | 440 | const outstanding = payments |
| 389 | 441 | .filter((p: Payment) => p.status === "pending") |
| 390 | 442 | .reduce((sum: number, payment: Payment) => sum + payment.amount, 0); |
| 391 | 443 | const upcoming = bookings.filter( |
| 392 | - (b: Booking) => new Date(b.travel_date) > new Date(), | |
| 444 | + (b: Booking) => toDateValue(b.travel_date) > new Date(), | |
| 393 | 445 | ).length; |
| 394 | 446 | |
| 395 | 447 | // Calculate from real data |
| 396 | 448 | const totalSpent = displayProfile?.total_spent ?? 0; |
| @@ -439,8 +491,9 @@ | ||
| 439 | 491 | conciergeEmail={accountShell.companyEmail} |
| 440 | 492 | onSectionChange={(section: string) => |
| 441 | 493 | handleSectionChange(section as Section) |
| 442 | 494 | } |
| 495 | + onBookingOpen={handleBookingOpen} | |
| 443 | 496 | /> |
| 444 | 497 | ); |
| 445 | 498 | case "bookings": |
| 446 | 499 | return ( |
| @@ -450,8 +503,10 @@ | ||
| 450 | 503 | conciergeEmail={accountShell.companyEmail} |
| 451 | 504 | onSectionChange={(section: string) => |
| 452 | 505 | handleSectionChange(section as Section) |
| 453 | 506 | } |
| 507 | + initialBookingId={pendingBookingId} | |
| 508 | + onBookingIdConsumed={() => setPendingBookingId(null)} | |
| 454 | 509 | /> |
| 455 | 510 | ); |
| 456 | 511 | case "payments": |
| 457 | 512 | return ( |