"use client"; import { useState } from "react"; import type { CategoryId } from "@transportationer/shared"; import { CATEGORIES } from "@transportationer/shared"; type Weights = Record; export type OverlayMode = "isochrone" | "relative"; export interface SubcategoryDetail { subcategory: string; name: string | null; distanceM: number | null; travelTimeS: number | null; } export interface LocationScoreData { lat: number; lng: number; categoryScores: Record; distancesM: Partial>; travelTimesS: Partial>; subcategoryDetails?: Partial>; } const SUBCATEGORY_LABELS: Record = { // service_trade supermarket: "Supermarket", pharmacy: "Pharmacy", convenience: "Convenience store", restaurant: "Restaurant", cafe: "Café", bank: "Bank", atm: "ATM", market: "Market", laundry: "Laundry", post_office: "Post office", // transport train_station: "Train station", metro: "Metro", tram_stop: "Tram stop", bus_stop: "Bus stop", stop: "Transit stop", ferry: "Ferry", bike_share: "Bike share", car_share: "Car share", // work_school school: "School", driving_school: "Driving school", kindergarten: "Kindergarten", university: "University", coworking: "Coworking", office: "Office", // culture_community hospital: "Hospital", clinic: "Clinic", library: "Library", community_center: "Community center", social_services: "Social services", theatre: "Theatre", place_of_worship: "Place of worship", government: "Government", museum: "Museum", // recreation park: "Park", playground: "Playground", sports_facility: "Sports facility", gym: "Gym", green_space: "Green space", swimming_pool: "Swimming pool", }; export function compositeScore( scores: Record, weights: Weights, ): number { const entries = Object.entries(weights) as [CategoryId, number][]; const total = entries.reduce((s, [, w]) => s + w, 0); if (total === 0) return 0; return entries.reduce((s, [cat, w]) => s + (scores[cat] ?? 0) * w, 0) / total; } function grade(score: number): string { if (score >= 0.8) return "A"; if (score >= 0.65) return "B"; if (score >= 0.5) return "C"; if (score >= 0.35) return "D"; return "F"; } function gradeColor(g: string): string { const map: Record = { A: "text-green-600", B: "text-green-500", C: "text-yellow-500", D: "text-orange-500", F: "text-red-600", }; return map[g] ?? "text-gray-600"; } function formatDist(m: number): string { return m >= 1000 ? `${(m / 1000).toFixed(1)} km` : `${Math.round(m)} m`; } function formatTime(s: number): string { const min = Math.round(s / 60); return min < 1 ? "<1 min" : `${min} min`; } export function LocationScorePanel({ data, weights, address, estateValue, estatePercentile, estateScorePercentile, overlayMode, isochroneLoading, showPois, onTogglePois, poiCount, onOverlayModeChange, onClose, }: { data: LocationScoreData; weights: Weights; address?: string; estateValue?: number | null; /** % of zones within 5 km with a lower value (0–100). */ estatePercentile?: number | null; /** % of zones with similar accessibility score that have a lower value (0–100). */ estateScorePercentile?: number | null; overlayMode: OverlayMode; isochroneLoading: boolean; showPois: boolean; onTogglePois: () => void; poiCount: number; onOverlayModeChange: (mode: OverlayMode) => void; onClose: () => void; }) { const [expandedCategory, setExpandedCategory] = useState(null); const composite = compositeScore(data.categoryScores, weights); const g = grade(composite); return (
{/* Header: grade + address + close */}
{g}
{Math.round(composite * 100)} / 100
{estateValue != null && (
{estateValue.toLocaleString("de-DE")} €/m² land value
{(estatePercentile != null || estateScorePercentile != null) && (
{estatePercentile != null && (
Pricier than {estatePercentile}% of zones · 5 km radius
)} {estateScorePercentile != null && (
Pricier than {estateScorePercentile}% · similar accessibility
)}
)}
)} {address && (
{address}
)}
{/* Overlay mode toggle */}
{/* POI pin toggle */} {poiCount > 0 && (
)} {/* Per-category score bars */}
{CATEGORIES.map((cat) => { const score = data.categoryScores[cat.id] ?? 0; const dist = data.distancesM[cat.id]; const time = data.travelTimesS[cat.id]; const barColor = score >= 0.65 ? "#22c55e" : score >= 0.4 ? "#eab308" : "#ef4444"; const subcats = data.subcategoryDetails?.[cat.id]; const isExpanded = expandedCategory === cat.id; const hasDetails = subcats && subcats.length > 0; return (
{isExpanded && hasDetails && (
{subcats.map((d) => (
{SUBCATEGORY_LABELS[d.subcategory] ?? d.subcategory} {d.name && ( {d.name} )} {d.distanceM != null && formatDist(d.distanceM)} {d.distanceM != null && d.travelTimeS != null && " · "} {d.travelTimeS != null && formatTime(d.travelTimeS)}
))}
)}
); })}
); }