chore(tradein/v2): #2899 — убрать шум prettier из предыдущего коммита (у tradein-frontend нет prettier-конвенции), оставить только правки
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m17s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m17s
This commit is contained in:
parent
03af7386e9
commit
b6ab582211
3 changed files with 353 additions and 377 deletions
|
|
@ -64,7 +64,13 @@ function DeltaPill({
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontFamily: font.mono, fontSize: 12, fontWeight: 600 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: font.mono,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{delta}
|
{delta}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 9.5, letterSpacing: 0.2 }}>{deltaLabel}</span>
|
<span style={{ fontSize: 9.5, letterSpacing: 0.2 }}>{deltaLabel}</span>
|
||||||
|
|
@ -290,11 +296,7 @@ export default function ResultPanel({
|
||||||
// even the AA "large text" floor (3:1). accentDeep is the same
|
// even the AA "large text" floor (3:1). accentDeep is the same
|
||||||
// brand hue, darkened (existing token, not a one-off hex) — it
|
// brand hue, darkened (existing token, not a one-off hex) — it
|
||||||
// clears ~4.2:1 here, so the label reads reliably too.
|
// clears ~4.2:1 here, so the label reads reliably too.
|
||||||
color: isHeadline
|
color: isHeadline ? accentDeep : isRegistry ? muted2 : body2,
|
||||||
? accentDeep
|
|
||||||
: isRegistry
|
|
||||||
? muted2
|
|
||||||
: body2,
|
|
||||||
fontWeight: isHeadline ? 600 : undefined,
|
fontWeight: isHeadline ? 600 : undefined,
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
}}
|
}}
|
||||||
|
|
@ -516,10 +518,7 @@ export default function ResultPanel({
|
||||||
перцентиль среди 16 аналогов»); при когорте < 15 delta нет. */}
|
перцентиль среди 16 аналогов»); при когорте < 15 delta нет. */}
|
||||||
{card.delta ? (
|
{card.delta ? (
|
||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
<DeltaPill
|
<DeltaPill delta={card.delta} deltaLabel={card.deltaLabel} />
|
||||||
delta={card.delta}
|
|
||||||
deltaLabel={card.deltaLabel}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|
@ -538,10 +537,7 @@ export default function ResultPanel({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{card.delta ? (
|
{card.delta ? (
|
||||||
<DeltaPill
|
<DeltaPill delta={card.delta} deltaLabel={card.deltaLabel} />
|
||||||
delta={card.delta}
|
|
||||||
deltaLabel={card.deltaLabel}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<span />
|
<span />
|
||||||
)}
|
)}
|
||||||
|
|
@ -610,8 +606,8 @@ export default function ResultPanel({
|
||||||
marginTop: -6,
|
marginTop: -6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{SHORT_ESTIMATE_DISCLAIMER} Диапазоны показывают разброс цен на рынке, а
|
{SHORT_ESTIMATE_DISCLAIMER} Диапазоны показывают разброс цен на рынке,
|
||||||
не погрешность оценки.
|
а не погрешность оценки.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ranges + radar */}
|
{/* ranges + radar */}
|
||||||
|
|
|
||||||
|
|
@ -605,16 +605,10 @@ interface DealTier {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Свежайшая дата среди лотов + её точность (лоты не всегда отсортированы). */
|
/** Свежайшая дата среди лотов + её точность (лоты не всегда отсортированы). */
|
||||||
function newestLot(
|
function newestLot(lots: AnalogLot[]): [string | null, "day" | "quarter" | null] {
|
||||||
lots: AnalogLot[],
|
|
||||||
): [string | null, "day" | "quarter" | null] {
|
|
||||||
let best: AnalogLot | null = null;
|
let best: AnalogLot | null = null;
|
||||||
for (const l of lots) {
|
for (const l of lots) {
|
||||||
if (
|
if (l.listing_date && (!best?.listing_date || l.listing_date > best.listing_date)) best = l;
|
||||||
l.listing_date &&
|
|
||||||
(!best?.listing_date || l.listing_date > best.listing_date)
|
|
||||||
)
|
|
||||||
best = l;
|
|
||||||
}
|
}
|
||||||
return [best?.listing_date ?? null, best?.date_precision ?? null];
|
return [best?.listing_date ?? null, best?.date_precision ?? null];
|
||||||
}
|
}
|
||||||
|
|
@ -642,9 +636,7 @@ function resolveDealTier(
|
||||||
// Fix #1/#8: guard the deal ₽/м² histogram against a high outlier the
|
// Fix #1/#8: guard the deal ₽/м² histogram against a high outlier the
|
||||||
// same way the ads card does — otherwise a single mis-scraped lot bins
|
// same way the ads card does — otherwise a single mis-scraped lot bins
|
||||||
// over [min,max] and crushes the real deals into the left buckets.
|
// over [min,max] and crushes the real deals into the left buckets.
|
||||||
bars: bins8(
|
bars: bins8(guardPriceOutliers(sd.deals.map((d) => d.price_per_m2)).clean),
|
||||||
guardPriceOutliers(sd.deals.map((d) => d.price_per_m2)).clean,
|
|
||||||
),
|
|
||||||
// sd.deals — top-10 из ORDER BY deal_date DESC по всем sd.count сделкам,
|
// sd.deals — top-10 из ORDER BY deal_date DESC по всем sd.count сделкам,
|
||||||
// так что максимум по ним = максимум по всей выборке, не по показанным.
|
// так что максимум по ним = максимум по всей выборке, не по показанным.
|
||||||
asOf: dealsAsOfLabel(...newestLot(sd.deals)),
|
asOf: dealsAsOfLabel(...newestLot(sd.deals)),
|
||||||
|
|
@ -833,13 +825,11 @@ export function mapReport(e: AggregatedEstimate): Report {
|
||||||
* - "out_of_coverage" → "вне ЕКБ" (the index only covers Yekaterinburg)
|
* - "out_of_coverage" → "вне ЕКБ" (the index only covers Yekaterinburg)
|
||||||
* - "insufficient_data" → "мало данных" (too few comparable listings)
|
* - "insufficient_data" → "мало данных" (too few comparable listings)
|
||||||
*/
|
*/
|
||||||
function locationIndexBadge(li: LocationIndexResponse | null | undefined): {
|
function locationIndexBadge(
|
||||||
label: string;
|
li: LocationIndexResponse | null | undefined,
|
||||||
ok: boolean;
|
): { label: string; ok: boolean } {
|
||||||
} {
|
|
||||||
if (li == null) return { label: "нет данных", ok: false };
|
if (li == null) return { label: "нет данных", ok: false };
|
||||||
if (li.status === "ok")
|
if (li.status === "ok") return { label: fmtPct(li.location_index_pct), ok: true };
|
||||||
return { label: fmtPct(li.location_index_pct), ok: true };
|
|
||||||
if (li.status === "out_of_coverage") return { label: "вне ЕКБ", ok: false };
|
if (li.status === "out_of_coverage") return { label: "вне ЕКБ", ok: false };
|
||||||
return { label: "мало данных", ok: false }; // status === "insufficient_data"
|
return { label: "мало данных", ok: false }; // status === "insufficient_data"
|
||||||
}
|
}
|
||||||
|
|
@ -1086,7 +1076,8 @@ export function marketPositionPill(
|
||||||
if (pct == null || !Number.isFinite(pct)) return undefined;
|
if (pct == null || !Number.isFinite(pct)) return undefined;
|
||||||
const delta = pct <= 33 ? "Низ рынка" : pct >= 67 ? "Верх рынка" : "В рынке";
|
const delta = pct <= 33 ? "Низ рынка" : pct >= 67 ? "Верх рынка" : "В рынке";
|
||||||
const n = e.n_analogs;
|
const n = e.n_analogs;
|
||||||
const deltaLabel = `${pct}-й перцентиль среди ${n} ${pluralRu(n, ["аналога", "аналогов", "аналогов"])}`;
|
const forms: [string, string, string] = ["аналога", "аналогов", "аналогов"];
|
||||||
|
const deltaLabel = `${pct}-й перцентиль среди ${n} ${pluralRu(n, forms)}`;
|
||||||
return { delta, deltaLabel };
|
return { delta, deltaLabel };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1135,9 +1126,7 @@ export function mapResultPanel(
|
||||||
// 872k выброс this card already reports as "исключён", squashing the real
|
// 872k выброс this card already reports as "исключён", squashing the real
|
||||||
// analogs into the left bins while the number says the outlier is dropped.
|
// analogs into the left bins while the number says the outlier is dropped.
|
||||||
// dealsOnlyPrice → e.analogs is empty → bins8([]) → [] (no bars drawn).
|
// dealsOnlyPrice → e.analogs is empty → bins8([]) → [] (no bars drawn).
|
||||||
bars: bins8(
|
bars: bins8(guardPriceOutliers(e.analogs.map((a) => a.price_per_m2)).clean),
|
||||||
guardPriceOutliers(e.analogs.map((a) => a.price_per_m2)).clean,
|
|
||||||
),
|
|
||||||
// #2899: «Верх рынка · 69-й перцентиль среди 16 аналогов» — та же спокойная
|
// #2899: «Верх рынка · 69-й перцентиль среди 16 аналогов» — та же спокойная
|
||||||
// плашка, что у карточки 2; undefined при когорте < 15 (бэкенд отдаёт null).
|
// плашка, что у карточки 2; undefined при когорте < 15 (бэкенд отдаёт null).
|
||||||
...(marketPositionPill(e) ?? {}),
|
...(marketPositionPill(e) ?? {}),
|
||||||
|
|
@ -1153,9 +1142,7 @@ export function mapResultPanel(
|
||||||
e.expected_sold_range_high_rub,
|
e.expected_sold_range_high_rub,
|
||||||
)
|
)
|
||||||
: "",
|
: "",
|
||||||
ppm: hasExpected
|
ppm: hasExpected ? `${fmtPpm(e.expected_sold_per_m2)} · с учётом торга` : "",
|
||||||
? `${fmtPpm(e.expected_sold_per_m2)} · с учётом торга`
|
|
||||||
: "",
|
|
||||||
delta:
|
delta:
|
||||||
hasExpected && e.asking_to_sold_ratio != null
|
hasExpected && e.asking_to_sold_ratio != null
|
||||||
? fmtPct((e.asking_to_sold_ratio - 1) * 100)
|
? fmtPct((e.asking_to_sold_ratio - 1) * 100)
|
||||||
|
|
@ -2182,10 +2169,7 @@ export function mapSources(
|
||||||
// sample still shown in the table right below this KPI tile) — a bare
|
// sample still shown in the table right below this KPI tile) — a bare
|
||||||
// "0" here would directly contradict visible rows. Fall back to the
|
// "0" here would directly contradict visible rows. Fall back to the
|
||||||
// actual displayed population (same fix as ListingsCard's count-strip).
|
// actual displayed population (same fix as ListingsCard's count-strip).
|
||||||
count:
|
count: e != null ? String(e.n_analogs > 0 ? e.n_analogs : e.analogs.length) : "—",
|
||||||
e != null
|
|
||||||
? String(e.n_analogs > 0 ? e.n_analogs : e.analogs.length)
|
|
||||||
: "—",
|
|
||||||
median: e != null ? fmtMln(e.median_price_rub) : "—",
|
median: e != null ? fmtMln(e.median_price_rub) : "—",
|
||||||
ppm:
|
ppm:
|
||||||
e != null && Number.isFinite(e.median_price_per_m2)
|
e != null && Number.isFinite(e.median_price_per_m2)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ export interface QuotaStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HouseType =
|
export type HouseType =
|
||||||
"panel" | "brick" | "monolith" | "monolith_brick" | "other";
|
| "panel"
|
||||||
|
| "brick"
|
||||||
|
| "monolith"
|
||||||
|
| "monolith_brick"
|
||||||
|
| "other";
|
||||||
|
|
||||||
export type RepairState = "needs_repair" | "standard" | "good" | "excellent";
|
export type RepairState = "needs_repair" | "standard" | "good" | "excellent";
|
||||||
|
|
||||||
|
|
@ -29,19 +33,11 @@ export const REPAIR_STATES: readonly RepairState[] = [
|
||||||
"good",
|
"good",
|
||||||
"excellent",
|
"excellent",
|
||||||
];
|
];
|
||||||
export function asHouseType(
|
export function asHouseType(v: string | null | undefined): HouseType | undefined {
|
||||||
v: string | null | undefined,
|
return v && (HOUSE_TYPES as readonly string[]).includes(v) ? (v as HouseType) : undefined;
|
||||||
): HouseType | undefined {
|
|
||||||
return v && (HOUSE_TYPES as readonly string[]).includes(v)
|
|
||||||
? (v as HouseType)
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
export function asRepairState(
|
export function asRepairState(v: string | null | undefined): RepairState | undefined {
|
||||||
v: string | null | undefined,
|
return v && (REPAIR_STATES as readonly string[]).includes(v) ? (v as RepairState) : undefined;
|
||||||
): RepairState | undefined {
|
|
||||||
return v && (REPAIR_STATES as readonly string[]).includes(v)
|
|
||||||
? (v as RepairState)
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfidenceLevel = "low" | "medium" | "high";
|
export type ConfidenceLevel = "low" | "medium" | "high";
|
||||||
|
|
@ -137,7 +133,7 @@ export interface CianValuationSummary {
|
||||||
rent_price_rub: number | null;
|
rent_price_rub: number | null;
|
||||||
chart: Array<{ date: string; price: number }>;
|
chart: Array<{ date: string; price: number }>;
|
||||||
chart_change_pct: number | null;
|
chart_change_pct: number | null;
|
||||||
chart_change_direction: "increase" | "decrease" | "neutral" | null;
|
chart_change_direction: 'increase' | 'decrease' | 'neutral' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Базис коэффициента asking→sold (S3): по группе комнат либо городской fallback.
|
// Базис коэффициента asking→sold (S3): по группе комнат либо городской fallback.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue