"use client"; import { BarChart3, ChevronDown, ChevronUp } from "lucide-react"; import React from "react"; import { useI18n } from "@/hooks/useI18n"; import type { CityDetail, ScanOpportunityRow, } from "@/lib/dashboard-types"; import { formatTemperatureValue, normalizeTemperatureSymbol, } from "@/lib/temperature-utils"; import { buildOpportunityGroups, formatWindowMinutes, getAiMeta, getBucketDisplayLabel, getDebDistanceSummary, getDecisionReasonItems, getDetailForRow, getForecastRangeLabel, getForecastRiskItems, getMetarConflictSummary, getModelSupportSummary, getPaceSignalLabel, getThresholdDecision, getV4CityForecast, } from "./OpportunityTable.utils"; export { getWindowPhaseMeta } from "./OpportunityTable.utils"; export const OpportunityTable = React.memo(function OpportunityTable({ rows, status, stale, staleReason, loading, selectedRowId, onSelectRow, cityDetailsByName, }: { rows: ScanOpportunityRow[]; status?: string | null; stale?: boolean; staleReason?: string | null; loading?: boolean; selectedRowId?: string | null; onSelectRow?: (row: ScanOpportunityRow) => void; cityDetailsByName?: Record; }) { const { locale } = useI18n(); const isEn = locale === "en-US"; const hasRows = rows.length > 0; const scanInProgress = loading || status === "partial" || status === "scanning"; const groups = React.useMemo( () => buildOpportunityGroups(rows, locale, cityDetailsByName), [rows, locale, cityDetailsByName], ); const [expandedRowIds, setExpandedRowIds] = React.useState>( () => new Set(), ); const toggleExpandedRow = React.useCallback((rowId: string) => { setExpandedRowIds((current) => { const next = new Set(current); if (next.has(rowId)) { next.delete(rowId); } else { next.add(rowId); } return next; }); }, []); const ensureExpandedRow = React.useCallback((rowId: string) => { setExpandedRowIds((current) => { if (current.has(rowId)) return current; const next = new Set(current); next.add(rowId); return next; }); }, []); const selectAndOpenRow = React.useCallback( (row: ScanOpportunityRow) => { ensureExpandedRow(row.id); onSelectRow?.(row); }, [ensureExpandedRow, onSelectRow], ); const toggleRowAnalysis = React.useCallback( (row: ScanOpportunityRow) => { toggleExpandedRow(row.id); onSelectRow?.(row); }, [onSelectRow, toggleExpandedRow], ); if (!hasRows) { const title = scanInProgress ? isEn ? "Scanning markets" : "正在扫描市场" : status === "failed" ? isEn ? "Scan failed" : "扫描失败" : isEn ? "No tradable market right now" : "当前暂无可交易市场"; const copy = scanInProgress ? isEn ? "Waiting for the latest market snapshot. Existing data will stay on screen when available." : "正在等待最新市场快照;如果有旧数据,会继续保留在页面上。" : status === "failed" ? staleReason || (isEn ? "No valid market snapshot is available." : "当前没有可用的市场快照。") : isEn ? "The current snapshot does not contain a tradable main signal." : "当前快照里还没有可交易的主信号。"; return (
{title}
{copy}
); } return (
{stale ? (
{isEn ? "Showing delayed snapshot" : "当前显示延迟快照"} {staleReason || (isEn ? "Latest refresh failed, fallback to the last successful scan." : "最新刷新失败,已回退到上次成功扫描结果。")}
) : null}
{groups.map((group) => { const groupSelected = group.rows.some((row) => row.id === selectedRowId); const firstRow = group.rows[0]; const firstTempSymbol = normalizeTemperatureSymbol( firstRow?.target_unit || firstRow?.temp_symbol || group.tempSymbol, ); const firstDetail = firstRow ? getDetailForRow(firstRow, cityDetailsByName) : null; const groupForecast = firstRow ? getV4CityForecast(firstRow, group, firstDetail, locale, firstTempSymbol) : null; const groupForecastLabel = groupForecast?.predicted != null ? formatTemperatureValue(groupForecast.predicted, firstTempSymbol, { digits: 1 }) : "--"; const groupRangeLabel = groupForecast ? getForecastRangeLabel(groupForecast, firstTempSymbol) : group.peakLabel; return (
{group.rows.map((row) => { const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol); const detail = getDetailForRow(row, cityDetailsByName); const debDistanceLabel = isEn ? "DEB distance" : "DEB 距离"; const modelSupportLabel = isEn ? "Model support" : "模型支持"; const metarLabel = "METAR"; const paceLabel = isEn ? "Path vs DEB" : "路径偏差"; const debDistanceText = getDebDistanceSummary(row, locale, tempSymbol); const modelSupportText = getModelSupportSummary(row, locale); const metarConflictText = getMetarConflictSummary(row, detail, locale); const cityForecast = getV4CityForecast( row, group, detail, locale, tempSymbol, ); const paceSignalText = getPaceSignalLabel(cityForecast, locale, tempSymbol); const aiMeta = getAiMeta(row, locale); const thresholdDecision = getThresholdDecision( row, cityForecast, locale, tempSymbol, ); const expanded = expandedRowIds.has(row.id); const shortConclusion = `${thresholdDecision.headline}。${thresholdDecision.summary}`; const keyReasons = getDecisionReasonItems( row, cityForecast, modelSupportText, locale, tempSymbol, ); const riskItems = getForecastRiskItems( row, detail, cityForecast, locale, tempSymbol, ); const bucketLabel = getBucketDisplayLabel(row, locale, tempSymbol); const forecastRangeLabel = getForecastRangeLabel(cityForecast, tempSymbol); return (
selectAndOpenRow(row)} >
{isEn ? "Conclusion" : "最终判断"} {thresholdDecision.headline} {thresholdDecision.summary}
{debDistanceLabel} {debDistanceText} {modelSupportLabel} {modelSupportText} {metarLabel} {metarConflictText} {paceLabel} {paceSignalText}
{thresholdDecision.relation}
{isEn ? "Current read" : "当前判断"} {shortConclusion}
{expanded ? (
{isEn ? "Conclusion" : "结论"}

{thresholdDecision.headline}

{thresholdDecision.summary}
{thresholdDecision.relation}
{isEn ? "Bucket" : "判断对象"} {bucketLabel} {isEn ? "AI forecast" : "AI 预测"} {cityForecast.predicted != null ? formatTemperatureValue(cityForecast.predicted, tempSymbol, { digits: 1 }) : "--"} {isEn ? "Forecast range" : "预测区间"} {forecastRangeLabel} {isEn ? "Confidence" : "信心"} {cityForecast.confidence || thresholdDecision.confidence}
DEB {group.debLabel} {modelSupportLabel} {modelSupportText} METAR {metarConflictText} {paceLabel} {paceSignalText}
{isEn ? "Why" : "为什么"}
    {keyReasons.map((reason) => (
  • {reason}
  • ))}
{isEn ? "What can change it" : "可能改变判断的因素"}
    {riskItems.map((reason) => (
  • {reason}
  • ))}
{cityForecast.airportRead ? (

{cityForecast.airportRead}

) : null} {cityForecast.weatherRead ? (

{cityForecast.weatherRead}

) : null} {cityForecast.paceRead ? (

{cityForecast.paceRead}

) : null} {cityForecast.peakWindow ? (

{cityForecast.peakWindow}

) : null}
) : null}
); })}
); })}
); });