feat(tradein-ui): placement history card #529
4 changed files with 141 additions and 0 deletions
|
|
@ -17,6 +17,7 @@ import { SourcesProgress } from "@/components/trade-in/SourcesProgress";
|
|||
import { HeroSummary } from "@/components/trade-in/HeroSummary";
|
||||
import { IMVBenchmark } from "@/components/trade-in/IMVBenchmark";
|
||||
import { HouseInfoCard } from "@/components/trade-in/HouseInfoCard";
|
||||
import { PlacementHistoryCard } from "@/components/trade-in/PlacementHistoryCard";
|
||||
import { PhotoUpload } from "@/components/trade-in/PhotoUpload";
|
||||
import { ListingsCard } from "@/components/trade-in/ListingsCard";
|
||||
import { DealsCard } from "@/components/trade-in/DealsCard";
|
||||
|
|
@ -169,6 +170,9 @@ export default function TradeInPage() {
|
|||
houses={houses.data}
|
||||
isLoading={houses.isPending}
|
||||
/>
|
||||
{currentEstimateId && (
|
||||
<PlacementHistoryCard estimateId={currentEstimateId} />
|
||||
)}
|
||||
<PhotoUpload estimateId={resultData.estimate.estimate_id} />
|
||||
<ListingsCard estimate={resultData.estimate} />
|
||||
<DealsCard estimate={resultData.estimate} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
"use client";
|
||||
|
||||
import { useEstimatePlacementHistory } from "@/lib/trade-in-api";
|
||||
import type { PlacementHistoryItem } from "@/types/trade-in";
|
||||
|
||||
type Props = { estimateId: string };
|
||||
|
||||
const fmtPrice = (n: number | null): string =>
|
||||
n == null ? "—" : `${(n / 1_000_000).toFixed(2)} М ₽`;
|
||||
|
||||
const fmtDate = (d: string | null): string => {
|
||||
if (!d) return "—";
|
||||
try {
|
||||
return new Date(d).toLocaleDateString("ru-RU");
|
||||
} catch {
|
||||
return d;
|
||||
}
|
||||
};
|
||||
|
||||
function lotLabel(it: PlacementHistoryItem): string {
|
||||
if (it.title) return it.title;
|
||||
const rooms = it.rooms != null ? `${it.rooms}к` : "?к";
|
||||
const area = it.area_m2 != null ? `${it.area_m2} м²` : "? м²";
|
||||
const floor =
|
||||
it.floor != null && it.total_floors != null
|
||||
? `, эт. ${it.floor}/${it.total_floors}`
|
||||
: "";
|
||||
return `${rooms}, ${area}${floor}`;
|
||||
}
|
||||
|
||||
export function PlacementHistoryCard({ estimateId }: Props) {
|
||||
const { data: items, isPending, isError } = useEstimatePlacementHistory(estimateId);
|
||||
|
||||
if (isPending || isError) return null;
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
const visible = items.slice(0, 20);
|
||||
|
||||
return (
|
||||
<article className="card" style={{ marginTop: 16 }}>
|
||||
<header className="card-head">
|
||||
<h3 style={{ margin: 0, fontSize: 15 }}>История продаж в этом доме</h3>
|
||||
<small style={{ color: "var(--muted, #6b7280)" }}>
|
||||
{items.length} лот{items.length === 1 ? "" : "ов"}
|
||||
</small>
|
||||
</header>
|
||||
<div style={{ padding: "8px 16px 16px" }}>
|
||||
<table
|
||||
style={{ width: "100%", fontSize: 13, borderCollapse: "collapse" }}
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
borderBottom: "1px solid var(--border, #e5e7eb)",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<th style={{ padding: "6px 8px" }}>Лот</th>
|
||||
<th style={{ padding: "6px 8px" }}>Цена нач. → посл.</th>
|
||||
<th style={{ padding: "6px 8px" }}>Дата</th>
|
||||
<th style={{ padding: "6px 8px" }}>Экспозиция</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map((it) => (
|
||||
<tr
|
||||
key={it.id}
|
||||
style={{
|
||||
borderBottom: "1px solid var(--border-soft, #f3f4f6)",
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: "6px 8px" }}>{lotLabel(it)}</td>
|
||||
<td style={{ padding: "6px 8px", whiteSpace: "nowrap" }}>
|
||||
{fmtPrice(it.start_price)} →{" "}
|
||||
<strong>{fmtPrice(it.last_price)}</strong>
|
||||
</td>
|
||||
<td style={{ padding: "6px 8px", whiteSpace: "nowrap" }}>
|
||||
{fmtDate(it.last_price_date ?? it.start_price_date)}
|
||||
</td>
|
||||
<td style={{ padding: "6px 8px" }}>
|
||||
{it.exposure_days ?? "—"} дн.
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{items.length > 20 && (
|
||||
<small
|
||||
style={{
|
||||
color: "var(--muted, #6b7280)",
|
||||
display: "block",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
показано 20 из {items.length}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
AggregatedEstimate,
|
||||
HouseInfoForEstimate,
|
||||
IMVBenchmarkResponse,
|
||||
PlacementHistoryItem,
|
||||
TradeInEstimateInput,
|
||||
} from "@/types/trade-in";
|
||||
|
||||
|
|
@ -54,6 +55,22 @@ export function useEstimateHouses(estimate_id: string | null) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/trade-in/estimate/{id}/placement-history
|
||||
* Historical listings from house_placement_history for the target house.
|
||||
*/
|
||||
export function useEstimatePlacementHistory(estimate_id: string | null) {
|
||||
return useQuery<PlacementHistoryItem[]>({
|
||||
queryKey: ["trade-in", "estimate", estimate_id, "placement-history"],
|
||||
queryFn: () =>
|
||||
apiFetch<PlacementHistoryItem[]>(
|
||||
`${BASE}/estimate/${estimate_id}/placement-history`,
|
||||
),
|
||||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||||
staleTime: 10 * 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/trade-in/estimate/{id}/imv-benchmark
|
||||
* Avito IMV benchmark для UI badge (Stage 3 IMV cache lookup).
|
||||
|
|
|
|||
|
|
@ -101,6 +101,25 @@ export interface HouseInfoForEstimate {
|
|||
raw_characteristics: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface PlacementHistoryItem {
|
||||
id: number;
|
||||
source: string;
|
||||
house_id: number;
|
||||
ext_item_id: string;
|
||||
title: string | null;
|
||||
rooms: number | null;
|
||||
area_m2: number | null;
|
||||
floor: number | null;
|
||||
total_floors: number | null;
|
||||
start_price: number | null;
|
||||
start_price_date: string | null;
|
||||
last_price: number | null;
|
||||
last_price_date: string | null;
|
||||
removed_date: string | null;
|
||||
exposure_days: number | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface IMVBenchmarkResponse {
|
||||
available: boolean;
|
||||
cache_key: string | null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue