// ─── Email Preview Tab (v3 — Save as Alert + Alert Management) ─── const SAVE_MSG_CLEAR_DELAY_MS = 3000; const SERP_MONTHLY_LIMIT = 3000; function EmailPreview() { const { useState, useEffect, useCallback } = React; // Re-render on theme change const [, _themeRender] = useState(0); useEffect(() => { const h = () => _themeRender(n => n + 1); window.addEventListener("theme-change", h); return () => window.removeEventListener("theme-change", h); }, []); const t = window.activeTheme || window.theme || {}; // ── State ── const [alerts, setAlerts] = useState([]); const [fullAlerts, setFullAlerts] = useState([]); // full alert objects for management const [alertLimit, setAlertLimit] = useState(null); // tier limit (null = unlimited) const [userRole, setUserRole] = useState(""); const [selectedAlert, setSelectedAlert] = useState(""); const [customMode, setCustomMode] = useState(false); // Search-style fields const [origin, setOrigin] = useState("MEM"); const [destination, setDestination] = useState(""); const [cabin, setCabin] = useState("J"); const [maxMiles, setMaxMiles] = useState(""); const [tripType, setTripType] = useState("round_trip"); const [returnOrigin, setReturnOrigin] = useState(""); const [returnDestination, setReturnDestination] = useState(""); // Date range const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); // Trip duration ranges const [minDays, setMinDays] = useState(""); const [maxDays, setMaxDays] = useState(""); // Open Jaw scope const [openJaw, setOpenJaw] = useState("same"); // Max Stops const [maxStops, setMaxStops] = useState(1); // Filters const [selectedSources, setSelectedSources] = useState([]); const [selectedAirlines, setSelectedAirlines] = useState(""); const [selectedBanks, setSelectedBanks] = useState([]); // Preview const [previewHtml, setPreviewHtml] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); // SerpApi usage const [serpUsage, setSerpUsage] = useState(null); // Save-as-alert state const [alertName, setAlertName] = useState(""); const [alertEmail, setAlertEmail] = useState(""); const [alertDuration, setAlertDuration] = useState("none"); const [saving, setSaving] = useState(false); const [saveMsg, setSaveMsg] = useState(""); const [editingAlertId, setEditingAlertId] = useState(null); // null = new, string = editing existing // FlightAPI.io cash price enrichment const [enrichCash, setEnrichCash] = useState(false); const [flightApiStatus, setFlightApiStatus] = useState(null); // ── Load alerts helper ── const loadAlerts = useCallback(() => { (window.authFetch || fetch)("/api/alerts/my-list") .then(r => { if (!r.ok) throw new Error("Not authorized"); return r.json(); }) .then(d => { setAlerts(d.alerts || []); }) .catch(e => console.warn("Load alert list:", e)); (window.authFetch || fetch)("/api/alerts/my") .then(r => { if (!r.ok) throw new Error("Not authorized"); return r.json(); }) .then(d => { setFullAlerts(d.alerts || []); if (d.limit !== undefined) setAlertLimit(d.limit); if (d.role) setUserRole(d.role); }) .catch(e => console.warn("Load alerts:", e)); }, []); // ── Load saved alerts on mount ── useEffect(() => { loadAlerts(); (window.authFetch || fetch)("/api/admin/serpapi-usage") .then(r => { if (!r.ok) throw new Error("Not authorized"); return r.json(); }) .then(setSerpUsage) .catch(e => console.warn("Load SerpAPI usage:", e)); // Pre-fill email from logged in user try { const token = window.authToken || localStorage.getItem("auth_token") || ""; if (token) { const payload = JSON.parse(atob(token.split(".")[1])); if (payload.email) setAlertEmail(payload.email); } } catch (e) {} (window.authFetch || fetch)("/api/admin/flightapi-status") .then(r => { if (!r.ok) throw new Error("Not authorized"); return r.json(); }) .then(setFlightApiStatus) .catch(e => console.warn("Load FlightAPI status:", e)); }, []); // ── When a saved alert is chosen, populate custom fields ── const populateFromAlert = (alertName) => { const a = alerts.find(x => x.name === alertName); if (!a) return; setOrigin(a.origin || ""); setDestination(a.destination || ""); setCabin(a.cabin || "J"); setMaxMiles(a.max_miles || ""); setTripType(a.trip_type || "round_trip"); setMinDays(a.min_days != null ? String(a.min_days) : ""); setMaxDays(a.max_days != null ? String(a.max_days) : ""); setOpenJaw(a.open_jaw || "same"); setMaxStops(a.max_stops != null ? a.max_stops : 1); setDateFrom(a.date_start || ""); setDateTo(a.date_end || ""); }; // ── Load alert into form for editing ── const loadAlertForEdit = (alert) => { setCustomMode(true); setEditingAlertId(alert.id); setAlertName(alert.name || ""); setAlertEmail(alert.email || ""); setOrigin(alert.origin || ""); setDestination(alert.destination || ""); setCabin(alert.cabin || "J"); setMaxMiles(alert.max_miles ? String(alert.max_miles) : ""); setTripType(alert.trip_type || "round_trip"); setMinDays(alert.min_days != null ? String(alert.min_days) : ""); setMaxDays(alert.max_days != null ? String(alert.max_days) : ""); setOpenJaw(alert.open_jaw || "same"); setMaxStops(alert.max_stops != null ? alert.max_stops : 1); setDateFrom(alert.date_start || ""); setDateTo(alert.date_end || ""); setSelectedSources(alert.programs ? alert.programs.split(",").map(s => s.trim()).filter(Boolean) : []); setSaveMsg(""); // scroll to top window.scrollTo({ top: 0, behavior: "smooth" }); }; // ── Clear form for new alert ── const clearForm = () => { setEditingAlertId(null); setAlertName(""); setOrigin("MEM"); setDestination(""); setCabin("J"); setMaxMiles(""); setTripType("round_trip"); setReturnOrigin(""); setReturnDestination(""); setDateFrom(""); setDateTo(""); setMinDays(""); setMaxDays(""); setOpenJaw("same"); setMaxStops(1); setSelectedSources([]); setSelectedAirlines(""); setSelectedBanks([]); setAlertDuration("none"); setSaveMsg(""); }; // ── Build query string & load preview ── const loadPreview = async (alertName) => { setLoading(true); setError(""); setPreviewHtml(""); try { const params = new URLSearchParams(); if (alertName) { params.set("alert_name", alertName); } else { if (origin) params.set("origin", origin); if (destination) params.set("destination", destination); if (cabin) params.set("cabin", cabin); if (maxMiles) params.set("max_miles", maxMiles); if (tripType) params.set("trip_type", tripType); if (returnOrigin) params.set("return_origin", returnOrigin); if (returnDestination) params.set("return_destination", returnDestination); if (minDays) params.set("min_days", minDays); if (maxDays) params.set("max_days", maxDays); if (dateFrom) params.set("date_from", dateFrom); if (dateTo) params.set("date_to", dateTo); if (selectedSources.length) params.set("sources", selectedSources.join(",")); if (selectedAirlines) params.set("airlines", selectedAirlines); if (openJaw && openJaw !== "same") params.set("open_jaw", openJaw); if (maxStops != null) params.set("max_stops", String(maxStops)); if (origin) params.set("home_airport", origin.split(",")[0].trim()); } // Always send banks and cash enrichment (applies to both saved alert and custom) if (selectedBanks.length) params.set("banks", selectedBanks.join(",")); if (enrichCash) params.set("enrich_cash", "true"); const resp = await (window.authFetch || fetch)(`/api/admin/email-preview?${params}`); if (resp.ok) { const html = await resp.text(); setPreviewHtml(html); } else { const text = await resp.text(); try { const err = JSON.parse(text); setError(err.error || err.detail || "Preview failed"); } catch { setError(`Server error (${resp.status}): ${text.slice(0, 200)}`); } } } catch (e) { setError(e.message); } setLoading(false); }; // ── Save current form as alert ── const saveAlert = async () => { if (!alertName.trim()) { setSaveMsg("Please enter an alert name"); return; } if (!origin || !destination) { setSaveMsg("Origin and destination are required"); return; } setSaving(true); setSaveMsg(""); try { const body = { name: alertName.trim(), origin: origin, destination: destination, cabin: cabin, max_miles: maxMiles ? parseInt(maxMiles) : 999999, trip_type: tripType, min_days: minDays ? parseInt(minDays) : 7, max_days: maxDays ? parseInt(maxDays) : 21, open_jaw: openJaw, max_stops: maxStops, date_start: dateFrom || null, date_end: dateTo || null, email: alertEmail.trim(), programs: selectedSources.length ? selectedSources.join(",") : "", duration: alertDuration, active: true, }; const isEdit = !!editingAlertId; const url = isEdit ? `/api/alerts/${editingAlertId}` : "/api/alerts"; const method = isEdit ? "PUT" : "POST"; const resp = await (window.authFetch || fetch)(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await resp.json(); if (resp.ok) { setSaveMsg(isEdit ? "Alert updated!" : "Alert saved!"); setEditingAlertId(null); loadAlerts(); // Auto-generate a name suggestion for next time setTimeout(() => setSaveMsg(""), SAVE_MSG_CLEAR_DELAY_MS); } else { setSaveMsg(data.error || "Save failed"); } } catch (e) { setSaveMsg("Error: " + e.message); } setSaving(false); }; // ── Delete alert ── const deleteAlert = async (id) => { if (!confirm("Delete this alert?")) return; try { await (window.authFetch || fetch)(`/api/alerts/${id}`, { method: "DELETE" }); loadAlerts(); } catch (e) { console.error("Delete failed:", e); } }; // ── Toggle alert active/inactive ── const toggleAlert = async (id) => { try { await (window.authFetch || fetch)(`/api/alerts/${id}/toggle`, { method: "POST" }); loadAlerts(); } catch (e) { console.error("Toggle failed:", e); } }; // ── Claim orphaned alerts (admin: assign NULL user_id alerts to me) ── const claimOrphans = async () => { try { const resp = await (window.authFetch || fetch)("/api/alerts/claim-orphans", { method: "POST" }); const data = await resp.json(); if (data.count > 0) { setSaveMsg(`Claimed ${data.count} orphaned alert(s)!`); loadAlerts(); } } catch (e) { console.error("Claim failed:", e); } }; // ── Programs / Sources reference ── const PROGRAMS_LIST = [ { key: "aeroplan", name: "Air Canada Aeroplan" }, { key: "flyingblue", name: "Air France/KLM Flying Blue" }, { key: "alaska", name: "Alaska Atmos Rewards" }, { key: "american", name: "American AAdvantage" }, { key: "delta", name: "Delta SkyMiles" }, { key: "emirates", name: "Emirates Skywards" }, { key: "etihad", name: "Etihad Guest" }, { key: "united", name: "United MileagePlus" }, { key: "virgin", name: "Virgin Atlantic" }, { key: "jetblue", name: "JetBlue TrueBlue" }, { key: "southwest", name: "Southwest Rapid Rewards" }, { key: "avianca", name: "Avianca LifeMiles" }, { key: "turkish", name: "Turkish Miles&Smiles" }, { key: "qantas", name: "Qantas Frequent Flyer" }, { key: "cathay", name: "Cathay Pacific Asia Miles" }, { key: "singapore", name: "Singapore KrisFlyer" }, { key: "ana", name: "ANA Mileage Club" }, { key: "jal", name: "JAL Mileage Bank" }, { key: "british", name: "British Airways Avios" }, { key: "iberia", name: "Iberia Avios" }, { key: "quantas", name: "Qantas" }, { key: "velocity", name: "Virgin Australia Velocity" }, { key: "smiles", name: "GOL Smiles" }, { key: "aeromexico", name: "Aeromexico Rewards" }, { key: "copaairlines", name: "Copa ConnectMiles" }, ]; // Bank / transfer partner groups const BANK_PARTNERS = { "Chase Ultimate Rewards": { key: "chase", programs: ["united", "aeroplan", "flyingblue", "british", "singapore", "southwest", "jetblue", "iberia", "virgin", "emirates"] }, "Amex Membership Rewards": { key: "amex", programs: ["delta", "aeroplan", "flyingblue", "british", "singapore", "ana", "cathay", "jetblue", "iberia", "virgin", "emirates", "avianca", "etihad"] }, "Citi ThankYou": { key: "citi", programs: ["turkish", "singapore", "cathay", "flyingblue", "virgin", "jetblue", "avianca", "etihad", "qantas"] }, "Capital One Miles": { key: "capital_one", programs: ["flyingblue", "british", "turkish", "singapore", "cathay", "avianca", "emirates", "etihad", "qantas", "aeromexico"] }, "Bilt Rewards": { key: "bilt", programs: ["united", "aeroplan", "flyingblue", "british", "turkish", "cathay", "american", "alaska", "virgin", "emirates", "iberia"] }, "Wells Fargo": { key: "wells_fargo", programs: ["flyingblue", "british", "avianca"] }, "Rove": { key: "rove", programs: ["aeroplan", "flyingblue", "british", "turkish", "avianca", "etihad"] }, }; const toggleSource = (key) => { setSelectedSources(prev => prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] ); }; const toggleBank = (bankKey) => { setSelectedBanks(prev => prev.includes(bankKey) ? prev.filter(b => b !== bankKey) : [...prev, bankKey] ); }; const fillProgramsFromBank = (bankName) => { const bank = BANK_PARTNERS[bankName]; if (!bank) return; setSelectedSources(bank.programs); }; const addProgramsFromBank = (bankName) => { const bank = BANK_PARTNERS[bankName]; if (!bank) return; setSelectedSources(prev => [...new Set([...prev, ...bank.programs])]); }; const programsReachableViaSelectedBanks = React.useMemo(() => { const reachable = new Set(); for (const [, bank] of Object.entries(BANK_PARTNERS)) { if (selectedBanks.includes(bank.key)) { bank.programs.forEach(p => reachable.add(p)); } } return reachable; }, [selectedBanks]); // Cabin labels const cabinLabels = { F: "First", J: "Business", W: "Premium Econ", Y: "Economy" }; // ── Inline styles (dynamic theme) ── const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: 12, padding: 16, marginBottom: 12, overflow: "visible" }; const labelStyle = { fontSize: 11, color: t.textDim, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 4, display: "block" }; const inputStyle = { background: t.card, border: `1px solid ${t.borderLight}`, borderRadius: 8, padding: "8px 12px", color: t.text, fontSize: 14, outline: "none", width: "100%", boxSizing: "border-box", fontFamily: "inherit" }; const selectStyle = { ...inputStyle, appearance: "auto" }; const btnPrimary = { background: t.accent, color: "#fff", border: "none", borderRadius: 8, padding: "10px 20px", fontSize: 14, fontWeight: 600, cursor: "pointer", transition: "background 0.15s", fontFamily: "inherit" }; const btnPrimaryDisabled = { ...btnPrimary, background: t.border, color: t.textDim, cursor: "not-allowed" }; const btnToggleActive = { background: t.accent, color: "#fff", border: "none", borderRadius: 8, padding: "6px 12px", fontSize: 13, fontWeight: 600, cursor: "pointer", transition: "background 0.15s", fontFamily: "inherit" }; const btnToggleInactive = { background: t.card, color: t.textMuted, border: "none", borderRadius: 8, padding: "6px 12px", fontSize: 13, fontWeight: 600, cursor: "pointer", transition: "background 0.15s", fontFamily: "inherit" }; const btnSmall = { padding: "5px 12px", fontSize: 12, fontWeight: 500, border: `1px solid ${t.borderLight}`, borderRadius: 6, cursor: "pointer", fontFamily: "inherit", transition: "all 0.15s" }; const chipBase = { display: "inline-block", padding: "4px 10px", borderRadius: 6, fontSize: 12, fontWeight: 500, cursor: "pointer", transition: "all 0.15s", border: `1px solid ${t.borderLight}`, marginRight: 6, marginBottom: 6, userSelect: "none" }; const chipActive = { ...chipBase, background: t.accent, color: "#fff", borderColor: t.accent }; const chipInactive = { ...chipBase, background: t.card, color: t.textMuted, borderColor: t.borderLight }; const sectionTitle = { fontSize: 13, fontWeight: 600, color: t.textMuted, marginBottom: 8, marginTop: 12 }; const fieldGroup = { flex: 1, minWidth: 100 }; const row = { display: "flex", gap: 12, alignItems: "flex-end", flexWrap: "wrap", overflow: "visible", position: "relative", zIndex: 10 }; // Theme for AutocompleteInput const acTheme = { border: t.borderLight, accent: t.accent, text: t.text, textDim: t.textDim, bg: t.surface, hover: t.hover, card: t.card, cardBg: t.cardBg }; return (
{/* ── Header with mode toggle ── */}

Email Alerts

{!customMode ? ( /* ── Saved alerts mode ── */
) : ( /* ── Custom search mode — full search controls ── */
{/* Editing indicator */} {editingAlertId && (
Editing: {alertName}
)} {/* Alert Name & Email row */}
Alert Settings
setAlertName(e.target.value)} placeholder="e.g. NYC to Tokyo Business" style={inputStyle} />
setAlertEmail(e.target.value)} placeholder="your@email.com" style={inputStyle} />
{/* Row 1: Origin / Destination / Cabin */}
Route
setMaxMiles(e.target.value)} placeholder="any" style={inputStyle} />
{/* Row 2: Trip type / Return route */}
Trip Type
{(tripType === "round_trip" || tripType === "open_jaw") && ( <>
)}
{/* Row 3: Date Range & Trip Duration */}
Dates & Duration
setDateFrom(e.target.value)} style={inputStyle} />
setDateTo(e.target.value)} style={inputStyle} />
setMinDays(e.target.value)} placeholder="8" style={inputStyle} />
setMaxDays(e.target.value)} placeholder="12" style={inputStyle} />
{/* Max Stops */}
Max Stops
{[{ id: 0, label: "Nonstop" }, { id: 1, label: "1 Stop" }, { id: 2, label: "2 Stops" }].map(s => ( ))}
{/* Row 4: Transfer Programs */}
Transfer Programs — filter results to specific programs (empty = all)
Quick fill: {Object.entries(BANK_PARTNERS).map(([bankName, bank]) => ( ))}
{PROGRAMS_LIST.map(p => { const isSelected = selectedSources.includes(p.key); const isReachable = programsReachableViaSelectedBanks.size > 0 && programsReachableViaSelectedBanks.has(p.key); const dimmed = selectedBanks.length > 0 && !isReachable && !isSelected; return ( toggleSource(p.key)} style={{ ...chipBase, ...(isSelected ? { background: t.accent, color: "#fff", borderColor: t.accent } : {}), ...(dimmed ? { opacity: 0.4 } : {}), ...(isReachable && !isSelected ? { borderColor: "rgba(16,185,129,0.4)" } : {}), }} >{p.name} ); })}
{/* Specific Airlines filter */}
Specific Airlines (operating carrier)
setSelectedAirlines(e.target.value.toUpperCase())} placeholder="e.g. NH,JL,CX (IATA airline codes, comma-separated)" style={inputStyle} />
{/* Action buttons: Preview + Save */}
{saveMsg && ( {saveMsg} )}
)} {error && (
{error}
)}
{/* ── Preview Options (shared between saved alerts and custom mode) ── */}
Preview Options
{/* Bank Selection */}
{Object.entries(BANK_PARTNERS).map(([bankName, bank]) => { const isActive = selectedBanks.includes(bank.key); return ( ); })}
{/* Cash Price Toggle */} {flightApiStatus && flightApiStatus.configured && (
)}
{/* ── My Alerts — management section ── */}

My Alerts ({fullAlerts.length}{alertLimit != null ? `/${alertLimit}` : ""})

{(userRole === "master_admin" || userRole === "admin") && fullAlerts.some(a => !a.user_id) && ( )} {alertLimit != null && fullAlerts.length >= alertLimit ? ( Limit reached — upgrade for more ) : ( )}
Active alerts run automatically after each daily ingestion and send results to your email.
{fullAlerts.length === 0 ? (
{"🔔"}
No alerts configured yet
Use Custom Search above to create your first alert
) : (
{fullAlerts.map(a => (
{/* Toggle */} {/* Info */}
{a.name}
{(() => { const fmtCodes = (codes) => { if (!codes) return "—"; const arr = codes.split(",").map(c => c.trim()).filter(Boolean); if (arr.length <= 4) return arr.join(","); return `${arr.slice(0, 3).join(",")}… (${arr.length} airports)`; }; return `${fmtCodes(a.origin)} \u2192 ${fmtCodes(a.destination)} \u00B7 ${cabinLabels[a.cabin] || a.cabin} \u00B7 ${a.min_days}-${a.max_days} days`; })()} {a.max_stops != null && a.max_stops !== 1 ? ` \u00B7 ${a.max_stops === 0 ? "Nonstop" : a.max_stops + " stops"}` : ""} {a.open_jaw && a.open_jaw !== "same" ? ` \u00B7 open jaw (${a.open_jaw})` : ""} {a.email ? ` \u00B7 ${a.email}` : ""} {a.date_start ? ` \u00B7 ${a.date_start}` : ""} {a.date_end ? ` to ${a.date_end}` : ""} {a.expires_at ? ` \u00B7 expires ${new Date(a.expires_at).toLocaleDateString()}` : ""}
{/* Actions */}
))}
)}
{/* ── API Status Cards (admin only) ── */} {(userRole === "master_admin" || userRole === "admin") && (serpUsage || flightApiStatus) && (
{serpUsage && ( <> SerpApi: {serpUsage.used_today || 0} / {serpUsage.daily_limit || 100} today Monthly: {serpUsage.used_month || 0} / {serpUsage.monthly_limit || SERP_MONTHLY_LIMIT} )} {flightApiStatus && ( <> FlightAPI.io: {flightApiStatus.configured ? "Active" : "Not Configured"} {flightApiStatus.configured && flightApiStatus.cache_entries > 0 && ( Cached: {flightApiStatus.cache_entries} routes )} )}
)} {/* ── Email Preview iframe — FULL WIDTH ── */} {previewHtml && (
Email Preview