United Trip Summary Renderer

Extracts and displays detailed flight info from /api/myTrips/lookup JSON on united.com after booking lookup completes.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         United Trip Summary Renderer
// @namespace    https://github.com/wangdashuai888/United-Trip-Summary
// @version      1.2
// @description  Extracts and displays detailed flight info from /api/myTrips/lookup JSON on united.com after booking lookup completes.
// @author       wangdashuai888
// @include      https://www.united.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const originalXHR = window.XMLHttpRequest;
    class InterceptedXHR extends originalXHR {
        constructor() {
            super();
            let url = '', method = '';
            const origOpen = this.open;
            const origSend = this.send;

            this.open = function (m, u) {
                method = m;
                url = u;
                return origOpen.apply(this, arguments);
            };

            this.send = function (body) {
                this.addEventListener('load', function () {
                    if (url.includes('/api/myTrips/lookup') && method.toUpperCase() === 'POST') {
                        try {
                            const json = JSON.parse(this.responseText);
                            renderFlightSummary(json);
                        } catch (err) {
                            console.error('❌ Failed to parse response JSON:', err);
                        }
                    }
                });
                return origSend.apply(this, arguments);
            };
        }
    }
    window.XMLHttpRequest = InterceptedXHR;

    function renderFlightSummary(data) {
        const detail = data?.Detail;
        if (!detail) return;

        const container = document.createElement('div');
        container.style = 'background: #f0f8ff; border: 2px solid #0071bc; padding: 16px; margin: 20px; font-family: sans-serif; line-height: 1.6; white-space: pre-wrap; max-width: 1000px;';

        const toggleBtn = document.createElement('button');
        toggleBtn.textContent = '🔄 Toggle: Show Raw JSON';
        toggleBtn.style = 'margin-bottom: 10px; padding: 10px 16px; font-size: 15px; font-weight: bold; border: 2px solid #005ea2; background-color: #e0f0ff; color: #003b70; border-radius: 6px; cursor: pointer;';
        container.appendChild(toggleBtn);

        const summaryView = document.createElement('div');
        const rawJsonView = document.createElement('pre');
        rawJsonView.style.display = 'none';
        rawJsonView.style.maxHeight = '500px';
        rawJsonView.style.overflow = 'auto';
        rawJsonView.style.background = '#fff';
        rawJsonView.style.border = '1px solid #ccc';
        rawJsonView.style.padding = '10px';
        rawJsonView.textContent = JSON.stringify(data, null, 2);

        const flights = extractFlights(detail);
        const tickets = extractTickets(detail);
        const passengers = extractPassengers(detail);
        const remarks = extractRemarks(detail);
        const ssrs = extractSSRs(detail, data);

        const lines = [];

        lines.push(`\n✈️ Confirmation #: ${detail.ConfirmationID}\n`);
        lines.push(`📅 Booking Date: ${detail.CreateDate}`);

        lines.push(`\n📍 Flights:`);
        for (const f of flights) {
            lines.push(`- ${f.MarketingAirlineCode}${f.FlightNumber} ${f.OriginAirportCode} → ${f.DestinationAirportCode}`);
            lines.push(`  · Status: ${f.Status}, Action: ${f.CurrentActionCode}`);
            lines.push(`  · Departure: ${f.ScheduledDeparture}`);
            lines.push(`  · Arrival:   ${f.ScheduledArrival}`);
            lines.push(`  · Cabin: ${f.ClassOfService} (${f.Cabin}), Operated by ${f.OperatingAirlineCode}`);
        }

        lines.push(`\n🎟 Tickets:`);
        for (const t of tickets) {
            lines.push(`- Ticket #: ${t.DocumentID}`);
            lines.push(`  · Issue: ${t.IssueDate}, Valid Until: ${t.TicketValidityDate}`);
            for (const c of t.Coupons) {
                lines.push(`    • ${c.Status}: ${c.OperatingAirlineCode} ${c.FlightNumber} (${c.DepartureAirport} → ${c.ArrivalAirport})`);
            }
        }

        lines.push(`\n🧍 Passengers:`);
        for (const p of passengers) {
            lines.push(`- ${p.Name} (Status: ${p.Status})`);
        }

        if (ssrs.length) {
            lines.push(`\n📑 SSRs:`);
            for (const s of ssrs) {
                lines.push(`- ${s.Code || s.Key}: ${s.Description || s.Comments || JSON.stringify(s)}`);
            }
        }

        if (remarks.length) {
            lines.push(`\n🗒 Remarks:`);
            for (const r of remarks) {
                lines.push(`- [${r.DisplaySequence}] ${r.Description}`);
            }
        }

        summaryView.textContent = lines.join('\n');
        container.appendChild(summaryView);
        container.appendChild(rawJsonView);
        document.body.prepend(container);

        toggleBtn.addEventListener('click', () => {
            const showingJson = rawJsonView.style.display === 'block';
            rawJsonView.style.display = showingJson ? 'none' : 'block';
            summaryView.style.display = showingJson ? 'block' : 'none';
            toggleBtn.textContent = showingJson ? '🔄 Toggle: Show Raw JSON' : '🔄 Toggle: Show Summary';
        });
    }

    function extractFlights(detail) {
        const segments = detail.FlightSegments || [];
        return segments.map(seg => {
            const f = seg.FlightSegment || {};
            return {
                OriginAirportCode: f.DepartureAirport?.IATACode || 'N/A',
                DestinationAirportCode: f.ArrivalAirport?.IATACode || 'N/A',
                Distance: f.Distance || 'N/A',
                CurrentActionCode: f.FlightSegmentType || '—',
                Status: seg.Characteristic?.find(c => c.Code === 'uflifo-FlightStatus')?.Value || 'N/A',
                MarketingAirlineCode: f.MarketedFlightSegment?.[0]?.MarketingAirlineCode || 'N/A',
                OperatingAirlineCode: f.OperatingAirlineCode || 'N/A',
                ClassOfService: seg.BookingClass?.Cabin?.Name || 'N/A',
                Cabin: seg.BookingClass?.Code || 'N/A',
                UpgradeStatus: f.UpgradeEligibilityStatus || '—',
                ScheduledDeparture: f.DepartureDateTime,
                ScheduledArrival: f.ArrivalDateTime,
                FlightNumber: f.FlightNumber
            };
        });
    }

    function extractTickets(detail) {
        const travelers = detail.Travelers || [];
        const tickets = [];
        for (const traveler of travelers) {
            for (const tkt of (traveler.Tickets || [])) {
                const coupons = (tkt.FlightCoupons || []).map(cpn => ({
                    Status: cpn.Status?.Code || '—',
                    DepartureAirport: cpn.FlightSegment?.DepartureAirport?.IATACode || 'N/A',
                    ArrivalAirport: cpn.FlightSegment?.ArrivalAirport?.IATACode || 'N/A',
                    FlightNumber: cpn.FlightSegment?.FlightNumber || 'N/A',
                    OperatingAirlineCode: cpn.FlightSegment?.OperatingAirlineCode || 'N/A'
                }));
                tickets.push({
                    DocumentID: tkt.DocumentID,
                    IssueDate: tkt.IssueDate,
                    TicketValidityDate: tkt.TicketValidityDate,
                    Coupons: coupons
                });
            }
        }
        return tickets;
    }

    function extractPassengers(detail) {
        return (detail.Travelers || []).map(p => ({
            Name: `${p.Person?.GivenName || ''} ${p.Person?.Surname || ''}`.trim(),
            Status: p.LoyaltyProgramProfile?.LoyaltyProgramMemberTierLevel || 'N/A'
        }));
    }

    function extractRemarks(detail) {
        return (detail.Remarks || []).map(r => ({
            DisplaySequence: r.DisplaySequence,
            Description: r.Description
        }));
    }

    function extractSSRs(detail, fullData) {
        const services = detail.Services || [];
        if (services.length > 0) {
            return services.map(s => ({
                Code: s.Code,
                Description: s.Description,
                Key: s.Key,
                Comments: s.Comments
            }));
        }
        // Fallback: parse SSRs from Traveler.Characteristics
        const ssrs = [];
        for (const t of (detail.Travelers || [])) {
            for (const c of (t.Characteristics || [])) {
                if ((c.Value || '').toUpperCase() === 'SSR') {
                    ssrs.push({
                        Code: c.Code,
                        Description: c.Description,
                        Comments: ''
                    });
                }
            }
        }
        return ssrs;
    }
})();