diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 5a22f6b0..48394c02 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -252,35 +252,103 @@ function KoyfinRowsTable({ ); } -function KoyfinMarketPanel({ - compact, +function CityRegionList({ isEn, - onSelect, rows, - selectedId, - title, + selectedCity, + onSelectCity, }: { - compact?: boolean; isEn: boolean; - onSelect: (row: ScanOpportunityRow) => void; rows: ScanOpportunityRow[]; - selectedId?: string | null; - title: string; + selectedCity: string | null; + onSelectCity: (city: string) => void; }) { + const cities = useMemo(() => { + const seen = new Set(); + const result: { city: string; name: string; contractCount: number; bestEdge: number | null; signal: string }[] = []; + rows.forEach((row) => { + const key = String(row.city || "").toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + const cityRows = rows.filter((r) => String(r.city || "").toLowerCase() === key); + const bestEdge = cityRows.reduce((best, r) => { + const e = r.edge_percent ?? 0; + return Math.abs(e) > Math.abs(best) ? e : best; + }, 0); + result.push({ + city: key, + name: rowName(row), + contractCount: cityRows.length, + bestEdge, + signal: cityRows[0]?.signal_status || "", + }); + }); + return result; + }, [rows]); + return ( - - + +
+ {cities.map(({ city, name, contractCount, bestEdge }) => ( + + ))} +
); } -function CityGroupedTable({ +function CityContractDetail({ + isEn, + rows, + selectedCity, + selectedId, + onSelect, +}: { + isEn: boolean; + rows: ScanOpportunityRow[]; + selectedCity: string | null; + selectedId?: string | null; + onSelect: (row: ScanOpportunityRow) => void; +}) { + const cityRows = useMemo( + () => (selectedCity ? rows.filter((r) => String(r.city || "").toLowerCase() === selectedCity) : []), + [rows, selectedCity], + ); + + return ( + + {!selectedCity ? ( +
+ {isEn ? "Select a city above" : "在上方选择城市"} +
+ ) : !cityRows.length ? ( +
+ {isEn ? "No contracts for this city" : "该城市暂无合约"} +
+ ) : ( + + )} +
+ ); +} + +function _unused_CityGroupedTable({ compact, isEn, onSelect, @@ -445,6 +513,8 @@ function PolyWeatherTerminal({ searchQuery: string; setSearchQuery: (val: string) => void; searchInputRef: React.RefObject; + selectedCity: string | null; + setSelectedCity: (city: string | null) => void; }) { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -766,12 +836,18 @@ function PolyWeatherTerminal({ {/* Desktop layout */}
- +
@@ -909,6 +985,7 @@ function ScanTerminalScreen() { }, [rows, searchQuery]); const [selectedId, setSelectedId] = useState(null); + const [selectedCity, setSelectedCity] = useState(null); const selectedRow = useMemo( () => filteredRows.find((row) => row.id === selectedId) || filteredRows[0] || null, [filteredRows, selectedId], @@ -951,6 +1028,8 @@ function ScanTerminalScreen() { searchQuery={searchQuery} setSearchQuery={setSearchQuery} searchInputRef={searchInputRef} + selectedCity={selectedCity} + setSelectedCity={setSelectedCity} /> ); }