United Trip Summary Renderer

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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;
    }
})();