export type AreaScore = { area_id: string; name: string; district: string; segment: string; month: string; investment_score: number; recommendation: string; liquidity_score: number; momentum_score: number; rent_support_score: number; safety_margin_score: number; credit_support_score: number; supply_risk_score: number; valuation_pressure_score: number; transaction_count: number; transaction_price_psm: number; listing_count: number; listing_price_psm: number; rent_price_psm: number; median_days_on_market: number; annual_rent_yield_pct: number; listing_pressure_ratio: number; discount_pct: number; price_momentum_pct: number; volume_momentum_pct: number; }; export type AreaScoreSummary = { area_id: string; name: string; district: string; investment_score: number; recommendation: string; }; export type MarketOverview = { month: string; area_count: number; average_investment_score: number; average_rent_yield_pct: number; top_area: AreaScoreSummary | null; weakest_area: AreaScoreSummary | null; highest_supply_risk_area: AreaScoreSummary | null; }; export type NeighborhoodScore = { neighborhood_id: string; area_id: string; name: string; area_name: string; district: string; month: string; investment_score: number; recommendation: string; liquidity_score: number; rent_support_score: number; price_safety_score: number; location_score: number; building_age_score: number; transaction_count: number; transaction_price_psm: number; listing_count: number; listing_price_psm: number; rent_price_psm: number; median_days_on_market: number; annual_rent_yield_pct: number; discount_pct: number; available_units: number; }; export type Neighborhood = { neighborhood_id: string; area_id: string; area_name: string; district: string; name: string; built_year: number | null; property_type: string; metro_distance_m: number | null; school_quality: string | null; notes: string; }; export type NeighborhoodMonthlyMetric = { neighborhood_id: string; month: string; transaction_count: number; transaction_price_psm: number; listing_count: number; listing_price_psm: number; rent_price_psm: number; median_days_on_market: number; available_units: number; source: string; ingestion_run_id: number | null; raw_artifact_id: number | null; }; export type WatchlistItem = { watchlist_item_id: number; neighborhood_id: string | null; area_id: string | null; target_price_psm: number | null; status: string; label: string; priority: string; trigger_operator: string; trigger_price_psm: number | null; notes: string; created_at: string; updated_at: string; last_reviewed_at: string | null; }; export type CreateWatchlistItem = { area_id?: string; neighborhood_id?: string; target_price_psm?: number; label?: string; priority?: string; trigger_operator?: string; trigger_price_psm?: number; notes?: string; }; export type UpdateWatchlistItem = { target_price_psm?: number; status?: "active" | "paused" | "archived"; label?: string; priority?: "low" | "medium" | "high"; trigger_operator?: "lte" | "gte"; trigger_price_psm?: number; notes?: string; mark_reviewed?: boolean; }; export type WatchlistEvent = { event_id: number; watchlist_item_id: number; event_type: string; summary: string; created_at: string; }; export type CreateWatchlistEvent = { event_type: string; summary: string; }; export type WatchlistFilters = { status?: string; area_id?: string; neighborhood_id?: string; label?: string; }; export type DataSource = { source_id: number; name: string; source_type: string; url: string | null; cadence: string | null; reliability: string; notes: string; created_at: string; }; export type CreateDataSource = { name: string; source_type: string; url?: string; cadence?: string; reliability?: string; notes?: string; }; export type IngestionRun = { run_id: number; source_id: number | null; source_name: string | null; raw_artifact_id: number | null; status: string; started_at: string; finished_at: string | null; raw_uri: string | null; row_count: number | null; error_message: string | null; }; export type CreateIngestionRun = { source_id?: number; status?: string; raw_uri?: string; raw_artifact_id?: number; row_count?: number; error_message?: string; }; export type FinishIngestionRun = { status: "succeeded" | "failed"; row_count?: number; error_message?: string; }; export type RawArtifact = { artifact_id: number; run_id: number | null; source_id: number | null; source_name: string | null; original_filename: string; raw_uri: string; sha256: string; size_bytes: number; mime_type: string | null; uploaded_by: string; notes: string; created_at: string; }; export type CreateRawArtifact = { run_id?: number; source_id?: number; original_filename: string; raw_uri: string; sha256: string; size_bytes: number; mime_type?: string; uploaded_by?: string; notes?: string; }; export type ImportExecutionRequest = { template_name: string; run_id: number; raw_artifact_id: number; rows: Record[]; allow_reimport?: boolean; }; export type ImportExecutionResponse = { template_name: string; target_table: string; run_id: number; raw_artifact_id: number; row_count: number; raw_row_count: number; duplicate_artifact: boolean; previous_run_ids: number[]; status: string; }; export type ModelRun = { model_run_id: number; model_name: string; model_version: string; target_month: string; status: string; parameters: Record; started_at: string; finished_at: string | null; row_count: number | null; error_message: string | null; }; export type CreateAreaScoreModelRun = { month: string; model_version?: string; }; export type AreaScoreModelRunResponse = { model_run: ModelRun; scores: AreaScore[]; }; export type AreaScoreRunDiffItem = { area_id: string; name: string; district: string; base_score: number | null; candidate_score: number | null; score_delta: number | null; base_recommendation: string | null; candidate_recommendation: string | null; }; export type AreaScoreRunDiff = { base_run: ModelRun; candidate_run: ModelRun; items: AreaScoreRunDiffItem[]; }; export type AreaScoreLineage = { area_id: string; area_name: string; month: string; investment_score: number; model_run_id: number | null; model_version: string; ingestion_run_id: number | null; raw_artifact_id: number | null; raw_uri: string | null; sha256: string | null; source_name: string | null; metric_updated_at: string; score_computed_at: string; }; export type AreaProfile = { area_id: string; name: string; district: string; segment: string; notes: string; }; export type AreaMonthlyMetric = { area_id: string; month: string; transaction_count: number; transaction_price_psm: number; listing_count: number; listing_price_psm: number; median_days_on_market: number; rent_price_psm: number; new_supply_units: number; land_residential_gfa_sqm: number; mortgage_rate_pct: number; policy_signal: number; source: string; ingestion_run_id: number | null; raw_artifact_id: number | null; }; export type DiagnosticMetric = { key: string; label: string; unit: string; current_value: number; percentile: number; historical_min: number; historical_max: number; sample_size: number; anomaly: boolean; anomaly_direction: string | null; severity: string; }; export type AreaDiagnostics = { area_id: string; month: string; metrics: DiagnosticMetric[]; anomaly_count: number; }; export type AreaDetail = { profile: AreaProfile; current_score: AreaScore | null; score_lineage: AreaScoreLineage | null; monthly_metrics: AreaMonthlyMetric[]; diagnostics: AreaDiagnostics | null; neighborhood_scores: NeighborhoodScore[]; }; export type NeighborhoodDetail = { profile: Neighborhood; current_score: NeighborhoodScore | null; monthly_metrics: NeighborhoodMonthlyMetric[]; similar_neighborhoods: SimilarNeighborhood[]; watchlist_items: WatchlistItem[]; }; export type SimilarNeighborhoodCandidate = { neighborhood_id: string; area_id: string; name: string; area_name: string; district: string; built_year: number | null; property_type: string; metro_distance_m: number | null; school_quality: string | null; month: string; investment_score: number; recommendation: string; transaction_price_psm: number; annual_rent_yield_pct: number; liquidity_score: number; location_score: number; building_age_score: number; }; export type SimilarNeighborhood = { neighborhood_id: string; area_id: string; name: string; area_name: string; district: string; built_year: number | null; property_type: string; metro_distance_m: number | null; school_quality: string | null; investment_score: number; recommendation: string; transaction_price_psm: number; annual_rent_yield_pct: number; similarity_score: number; relative_price_pct: number; reasons: string[]; }; export type SimilarNeighborhoodResponse = { target: SimilarNeighborhoodCandidate; month: string; items: SimilarNeighborhood[]; }; export type ScenarioRun = { scenario_run_id: number; name: string; target_month: string; area_id: string | null; interest_rate_delta_bps: number; policy_signal_delta: number; supply_delta_pct: number; rent_delta_pct: number; notes: string; created_at: string; }; export type CreateScenarioRun = { name: string; target_month: string; area_id?: string; interest_rate_delta_bps: number; policy_signal_delta: number; supply_delta_pct: number; rent_delta_pct: number; notes?: string; }; export type ScenarioAreaResult = { area_id: string; name: string; district: string; baseline_score: number; projected_score: number; score_delta: number; baseline_price_psm: number; projected_price_psm: number; price_delta_pct: number; projected_rent_yield_pct: number; projected_liquidity_score: number; projected_supply_risk_score: number; recommendation: string; risk_notes: string[]; }; export type ScenarioVariant = { key: string; name: string; assumptions: string[]; items: ScenarioAreaResult[]; }; export type ScenarioRunResponse = { run: ScenarioRun; variants: ScenarioVariant[]; }; export type Report = { report_id: number; report_type: string; title: string; target_month: string | null; area_id: string | null; area_name: string | null; neighborhood_id: string | null; neighborhood_name: string | null; summary: string; content_markdown: string; linked_metrics: Record; status: string; created_at: string; updated_at: string; }; export type CreateReport = { report_type: "monthly" | "weekly" | "area_special" | "neighborhood_memo"; title: string; target_month?: string; area_id?: string; neighborhood_id?: string; summary?: string; content_markdown: string; linked_metrics?: Record; status?: "draft" | "published" | "archived"; }; export type ReportFilters = { report_type?: string; target_month?: string; area_id?: string; neighborhood_id?: string; status?: string; }; export type ReportTemplate = { template_id: number; template_key: string; name: string; report_type: string; cadence: string; description: string; body_template: string; created_at: string; updated_at: string; }; export type ReportGenerationRun = { generation_run_id: number; template_id: number; template_key: string; template_name: string; target_month: string; status: string; report_id: number | null; report_title: string | null; parameters: Record; error_message: string | null; started_at: string; finished_at: string | null; }; export type CreateReportGenerationRun = { template_key: string; target_month: string; parameters?: Record; }; export type ReportGenerationRunResponse = { generation_run: ReportGenerationRun; report: Report | null; }; export type ComparisonMetricValue = { id: string; value: number | null; }; export type ComparisonMetric = { key: string; label: string; unit: string; direction: "higher" | "lower" | "neutral"; values: ComparisonMetricValue[]; }; export type AreaComparison = { month: string; requested_ids: string[]; missing_ids: string[]; items: AreaScore[]; metrics: ComparisonMetric[]; }; export type NeighborhoodComparisonItem = NeighborhoodScore & { built_year: number | null; property_type: string; metro_distance_m: number | null; school_quality: string | null; }; export type NeighborhoodComparison = { month: string; requested_ids: string[]; missing_ids: string[]; items: NeighborhoodComparisonItem[]; metrics: ComparisonMetric[]; }; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/$/, "") ?? "http://127.0.0.1:8080"; export async function fetchMarketOverview(month: string): Promise { return fetchJson(`/api/v1/market/overview?month=${encodeURIComponent(month)}`); } export async function fetchAreaScores(month: string): Promise { return fetchJson(`/api/v1/areas/scores?month=${encodeURIComponent(month)}`); } export async function fetchAreaDetail(areaId: string, month: string): Promise { return fetchJson( `/api/v1/areas/${encodeURIComponent(areaId)}/detail?month=${encodeURIComponent(month)}`, ); } export async function fetchAreaDiagnostics( areaId: string, month: string, ): Promise { return fetchJson( `/api/v1/areas/${encodeURIComponent(areaId)}/diagnostics?month=${encodeURIComponent(month)}`, ); } export async function fetchNeighborhoodScores(month: string): Promise { return fetchJson(`/api/v1/neighborhoods/scores?month=${encodeURIComponent(month)}`); } export async function fetchNeighborhoodDetail( neighborhoodId: string, month: string, ): Promise { return fetchJson( `/api/v1/neighborhoods/${encodeURIComponent(neighborhoodId)}/detail?month=${encodeURIComponent(month)}`, ); } export async function fetchSimilarNeighborhoods( neighborhoodId: string, month: string, limit = 8, ): Promise { return fetchJson( `/api/v1/neighborhoods/${encodeURIComponent(neighborhoodId)}/similar?month=${encodeURIComponent(month)}&limit=${limit}`, ); } export async function fetchAreaComparison( areaIds: string[], month: string, ): Promise { return fetchJson( `/api/v1/compare/areas?month=${encodeURIComponent(month)}&ids=${encodeURIComponent(areaIds.join(","))}`, ); } export async function fetchNeighborhoodComparison( neighborhoodIds: string[], month: string, ): Promise { return fetchJson( `/api/v1/compare/neighborhoods?month=${encodeURIComponent(month)}&ids=${encodeURIComponent(neighborhoodIds.join(","))}`, ); } export async function fetchWatchlistItems( filters: WatchlistFilters = {}, ): Promise { const params = new URLSearchParams(); for (const [key, value] of Object.entries(filters)) { if (value) { params.set(key, value); } } const query = params.toString(); return fetchJson(`/api/v1/watchlist${query ? `?${query}` : ""}`); } export async function createWatchlistItem( payload: CreateWatchlistItem, ): Promise { return fetchJson("/api/v1/watchlist", { method: "POST", body: JSON.stringify(payload), }); } export async function archiveWatchlistItem(id: number): Promise { return fetchJson(`/api/v1/watchlist/${id}`, { method: "DELETE", }); } export async function updateWatchlistItem( id: number, payload: UpdateWatchlistItem, ): Promise { return fetchJson(`/api/v1/watchlist/${id}`, { method: "PATCH", body: JSON.stringify(payload), }); } export async function fetchWatchlistEvents(id: number): Promise { return fetchJson(`/api/v1/watchlist/${id}/events`); } export async function createWatchlistEvent( id: number, payload: CreateWatchlistEvent, ): Promise { return fetchJson(`/api/v1/watchlist/${id}/events`, { method: "POST", body: JSON.stringify(payload), }); } export async function fetchDataSources(): Promise { return fetchJson("/api/v1/data-sources"); } export async function createDataSource(payload: CreateDataSource): Promise { return fetchJson("/api/v1/data-sources", { method: "POST", body: JSON.stringify(payload), }); } export async function fetchIngestionRuns(): Promise { return fetchJson("/api/v1/ingestion-runs"); } export async function createIngestionRun(payload: CreateIngestionRun): Promise { return fetchJson("/api/v1/ingestion-runs", { method: "POST", body: JSON.stringify(payload), }); } export async function finishIngestionRun( id: number, payload: FinishIngestionRun, ): Promise { return fetchJson(`/api/v1/ingestion-runs/${id}/finish`, { method: "PATCH", body: JSON.stringify(payload), }); } export async function fetchRawArtifacts(): Promise { return fetchJson("/api/v1/raw-artifacts"); } export async function createRawArtifact(payload: CreateRawArtifact): Promise { return fetchJson("/api/v1/raw-artifacts", { method: "POST", body: JSON.stringify(payload), }); } export async function executeImport( payload: ImportExecutionRequest, ): Promise { return fetchJson("/api/v1/imports/execute", { method: "POST", body: JSON.stringify(payload), }); } export async function createAreaScoreModelRun( payload: CreateAreaScoreModelRun, ): Promise { return fetchJson("/api/v1/model-runs/area-scores", { method: "POST", body: JSON.stringify(payload), }); } export async function fetchModelRuns(filters: { model_name?: string; target_month?: string; } = {}): Promise { const params = new URLSearchParams(); for (const [key, value] of Object.entries(filters)) { if (value) { params.set(key, value); } } const query = params.toString(); return fetchJson(`/api/v1/model-runs${query ? `?${query}` : ""}`); } export async function fetchAreaScoreRunDiff( baseRunId: number, candidateRunId: number, ): Promise { return fetchJson( `/api/v1/model-runs/area-scores/diff?base_run_id=${baseRunId}&candidate_run_id=${candidateRunId}`, ); } export async function fetchAreaScoreLineage( areaId: string, month: string, ): Promise { return fetchJson( `/api/v1/areas/${encodeURIComponent(areaId)}/score-lineage?month=${encodeURIComponent(month)}`, ); } export async function fetchScenarioRuns(filters: { target_month?: string; area_id?: string; } = {}): Promise { const params = new URLSearchParams(); for (const [key, value] of Object.entries(filters)) { if (value) { params.set(key, value); } } const query = params.toString(); return fetchJson(`/api/v1/scenario-runs${query ? `?${query}` : ""}`); } export async function createScenarioRun( payload: CreateScenarioRun, ): Promise { return fetchJson("/api/v1/scenario-runs", { method: "POST", body: JSON.stringify(payload), }); } export async function fetchReports(filters: ReportFilters = {}): Promise { const params = new URLSearchParams(); for (const [key, value] of Object.entries(filters)) { if (value) { params.set(key, value); } } const query = params.toString(); return fetchJson(`/api/v1/reports${query ? `?${query}` : ""}`); } export async function fetchReport(id: number): Promise { return fetchJson(`/api/v1/reports/${id}`); } export async function createReport(payload: CreateReport): Promise { return fetchJson("/api/v1/reports", { method: "POST", body: JSON.stringify(payload), }); } export async function fetchReportTemplates(): Promise { return fetchJson("/api/v1/report-templates"); } export async function fetchReportGenerationRuns(filters: { target_month?: string; template_key?: string; status?: string; } = {}): Promise { const params = new URLSearchParams(); for (const [key, value] of Object.entries(filters)) { if (value) { params.set(key, value); } } const query = params.toString(); return fetchJson(`/api/v1/report-generation-runs${query ? `?${query}` : ""}`); } export async function createReportGenerationRun( payload: CreateReportGenerationRun, ): Promise { return fetchJson("/api/v1/report-generation-runs", { method: "POST", body: JSON.stringify(payload), }); } async function fetchJson(path: string, init?: RequestInit): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { ...init, headers: { "Content-Type": "application/json", ...init?.headers, }, }); if (!response.ok) { throw new Error(`API request failed with ${response.status}`); } return response.json() as Promise; }