880 lines
21 KiB
TypeScript
880 lines
21 KiB
TypeScript
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<string, string | number | null>[];
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
error_message: string | null;
|
|
started_at: string;
|
|
finished_at: string | null;
|
|
};
|
|
|
|
export type CreateReportGenerationRun = {
|
|
template_key: string;
|
|
target_month: string;
|
|
parameters?: Record<string, unknown>;
|
|
};
|
|
|
|
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<MarketOverview> {
|
|
return fetchJson(`/api/v1/market/overview?month=${encodeURIComponent(month)}`);
|
|
}
|
|
|
|
export async function fetchAreaScores(month: string): Promise<AreaScore[]> {
|
|
return fetchJson(`/api/v1/areas/scores?month=${encodeURIComponent(month)}`);
|
|
}
|
|
|
|
export async function fetchAreaDetail(areaId: string, month: string): Promise<AreaDetail> {
|
|
return fetchJson(
|
|
`/api/v1/areas/${encodeURIComponent(areaId)}/detail?month=${encodeURIComponent(month)}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchAreaDiagnostics(
|
|
areaId: string,
|
|
month: string,
|
|
): Promise<AreaDiagnostics> {
|
|
return fetchJson(
|
|
`/api/v1/areas/${encodeURIComponent(areaId)}/diagnostics?month=${encodeURIComponent(month)}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchNeighborhoodScores(month: string): Promise<NeighborhoodScore[]> {
|
|
return fetchJson(`/api/v1/neighborhoods/scores?month=${encodeURIComponent(month)}`);
|
|
}
|
|
|
|
export async function fetchNeighborhoodDetail(
|
|
neighborhoodId: string,
|
|
month: string,
|
|
): Promise<NeighborhoodDetail> {
|
|
return fetchJson(
|
|
`/api/v1/neighborhoods/${encodeURIComponent(neighborhoodId)}/detail?month=${encodeURIComponent(month)}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchSimilarNeighborhoods(
|
|
neighborhoodId: string,
|
|
month: string,
|
|
limit = 8,
|
|
): Promise<SimilarNeighborhoodResponse> {
|
|
return fetchJson(
|
|
`/api/v1/neighborhoods/${encodeURIComponent(neighborhoodId)}/similar?month=${encodeURIComponent(month)}&limit=${limit}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchAreaComparison(
|
|
areaIds: string[],
|
|
month: string,
|
|
): Promise<AreaComparison> {
|
|
return fetchJson(
|
|
`/api/v1/compare/areas?month=${encodeURIComponent(month)}&ids=${encodeURIComponent(areaIds.join(","))}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchNeighborhoodComparison(
|
|
neighborhoodIds: string[],
|
|
month: string,
|
|
): Promise<NeighborhoodComparison> {
|
|
return fetchJson(
|
|
`/api/v1/compare/neighborhoods?month=${encodeURIComponent(month)}&ids=${encodeURIComponent(neighborhoodIds.join(","))}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchWatchlistItems(
|
|
filters: WatchlistFilters = {},
|
|
): Promise<WatchlistItem[]> {
|
|
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<WatchlistItem> {
|
|
return fetchJson("/api/v1/watchlist", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function archiveWatchlistItem(id: number): Promise<WatchlistItem> {
|
|
return fetchJson(`/api/v1/watchlist/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
export async function updateWatchlistItem(
|
|
id: number,
|
|
payload: UpdateWatchlistItem,
|
|
): Promise<WatchlistItem> {
|
|
return fetchJson(`/api/v1/watchlist/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function fetchWatchlistEvents(id: number): Promise<WatchlistEvent[]> {
|
|
return fetchJson(`/api/v1/watchlist/${id}/events`);
|
|
}
|
|
|
|
export async function createWatchlistEvent(
|
|
id: number,
|
|
payload: CreateWatchlistEvent,
|
|
): Promise<WatchlistEvent> {
|
|
return fetchJson(`/api/v1/watchlist/${id}/events`, {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function fetchDataSources(): Promise<DataSource[]> {
|
|
return fetchJson("/api/v1/data-sources");
|
|
}
|
|
|
|
export async function createDataSource(payload: CreateDataSource): Promise<DataSource> {
|
|
return fetchJson("/api/v1/data-sources", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function fetchIngestionRuns(): Promise<IngestionRun[]> {
|
|
return fetchJson("/api/v1/ingestion-runs");
|
|
}
|
|
|
|
export async function createIngestionRun(payload: CreateIngestionRun): Promise<IngestionRun> {
|
|
return fetchJson("/api/v1/ingestion-runs", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function finishIngestionRun(
|
|
id: number,
|
|
payload: FinishIngestionRun,
|
|
): Promise<IngestionRun> {
|
|
return fetchJson(`/api/v1/ingestion-runs/${id}/finish`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function fetchRawArtifacts(): Promise<RawArtifact[]> {
|
|
return fetchJson("/api/v1/raw-artifacts");
|
|
}
|
|
|
|
export async function createRawArtifact(payload: CreateRawArtifact): Promise<RawArtifact> {
|
|
return fetchJson("/api/v1/raw-artifacts", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function executeImport(
|
|
payload: ImportExecutionRequest,
|
|
): Promise<ImportExecutionResponse> {
|
|
return fetchJson("/api/v1/imports/execute", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function createAreaScoreModelRun(
|
|
payload: CreateAreaScoreModelRun,
|
|
): Promise<AreaScoreModelRunResponse> {
|
|
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<ModelRun[]> {
|
|
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<AreaScoreRunDiff> {
|
|
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<AreaScoreLineage> {
|
|
return fetchJson(
|
|
`/api/v1/areas/${encodeURIComponent(areaId)}/score-lineage?month=${encodeURIComponent(month)}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchScenarioRuns(filters: {
|
|
target_month?: string;
|
|
area_id?: string;
|
|
} = {}): Promise<ScenarioRun[]> {
|
|
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<ScenarioRunResponse> {
|
|
return fetchJson("/api/v1/scenario-runs", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function fetchReports(filters: ReportFilters = {}): Promise<Report[]> {
|
|
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<Report> {
|
|
return fetchJson(`/api/v1/reports/${id}`);
|
|
}
|
|
|
|
export async function createReport(payload: CreateReport): Promise<Report> {
|
|
return fetchJson("/api/v1/reports", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function fetchReportTemplates(): Promise<ReportTemplate[]> {
|
|
return fetchJson("/api/v1/report-templates");
|
|
}
|
|
|
|
export async function fetchReportGenerationRuns(filters: {
|
|
target_month?: string;
|
|
template_key?: string;
|
|
status?: string;
|
|
} = {}): Promise<ReportGenerationRun[]> {
|
|
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<ReportGenerationRunResponse> {
|
|
return fetchJson("/api/v1/report-generation-runs", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
|
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<T>;
|
|
}
|