85df33a537
UI:
- Center-align all numeric table columns (deals + appraisals) for better
visual balance — text-start was crowding numbers against the right edge.
Backend (search_appraisals):
- Add include_nearby + nearby_block_radius flags. When set, fetches three
buckets in one request:
1. exact match (block + plot, original behavior)
2. same block, other plots
3. nearby blocks (block ± radius, default ±2)
- Composite-key dedupe across buckets (gov.il has no stable decision id).
Frontend:
- AppraisalsTable now renders the three buckets as stacked sections with
Hebrew headers ("החלטות תואמות" / "באותו גוש" / "בגושים סמוכים").
- Tab counter sums all three buckets.
Auto-enables include_nearby whenever a block is in scope (address mode
via gush_helka lookup, or block_plot mode directly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
6.1 KiB
TypeScript
146 lines
6.1 KiB
TypeScript
import { Deal } from "../api/types";
|
||
import { formatCurrencyILS, formatDate, formatNumber } from "../lib/utils";
|
||
|
||
interface Props {
|
||
deals: Deal[];
|
||
resolvedAddress?: string;
|
||
searchedBlock?: string;
|
||
searchedPlot?: string;
|
||
}
|
||
|
||
// Extract street name from a Govmap address string like "הילדסהיימר 14, ירושלים".
|
||
// Takes all leading tokens that contain no digits. Used as fallback only —
|
||
// for parcel searches Govmap echoes back "גוש N חלקה M" with no street info.
|
||
function streetFromAddress(address: string | undefined): string | null {
|
||
if (!address) return null;
|
||
const tokens = address.split(/[\s,]+/);
|
||
const parts: string[] = [];
|
||
for (const t of tokens) {
|
||
if (/\d/.test(t)) break;
|
||
if (t) parts.push(t);
|
||
}
|
||
return parts.length ? parts.join(" ") : null;
|
||
}
|
||
|
||
// Find the street name of the searched parcel by picking the deal with the
|
||
// closest parcelNum within the same gush. Cadastral parcel numbers are usually
|
||
// assigned along street frontage, so the nearest plot is almost always the
|
||
// same street.
|
||
function streetByClosestParcel(
|
||
deals: Deal[],
|
||
block: string | undefined,
|
||
plot: string | undefined
|
||
): string | null {
|
||
if (!plot) return null;
|
||
const target = parseInt(plot, 10);
|
||
if (!Number.isFinite(target)) return null;
|
||
const blockNum = block ? parseInt(block, 10) : null;
|
||
let best: { dist: number; street: string } | null = null;
|
||
for (const d of deals) {
|
||
if (!d.streetNameHeb || d.parcelNum == null) continue;
|
||
if (blockNum !== null && d.gushNum !== blockNum) continue;
|
||
const dist = Math.abs((d.parcelNum as number) - target);
|
||
if (!best || dist < best.dist) {
|
||
best = { dist, street: d.streetNameHeb };
|
||
}
|
||
}
|
||
return best?.street ?? null;
|
||
}
|
||
|
||
function proximityLabel(deal: Deal, inferredStreet: string | null): string {
|
||
if (!deal.deal_source) return "—";
|
||
if (deal.deal_source === "same_building") return "אותו בניין";
|
||
if (deal.deal_source === "neighborhood") return "שכונה";
|
||
// "street" source — check if actually same street
|
||
if (inferredStreet && deal.streetNameHeb === inferredStreet) return "אותו רחוב";
|
||
return "סביבה קרובה";
|
||
}
|
||
|
||
function proximityClass(deal: Deal, inferredStreet: string | null): string {
|
||
if (deal.deal_source === "same_building") return "bg-emerald-100 text-emerald-800";
|
||
if (deal.deal_source === "street" && inferredStreet && deal.streetNameHeb === inferredStreet)
|
||
return "bg-blue-100 text-blue-800";
|
||
if (deal.deal_source === "street") return "bg-sky-50 text-sky-700";
|
||
return "bg-slate-100 text-slate-700";
|
||
}
|
||
|
||
export function DealsTable({ deals, resolvedAddress, searchedBlock, searchedPlot }: Props) {
|
||
if (!deals.length) {
|
||
return (
|
||
<div className="text-center py-12 text-muted-foreground">
|
||
לא נמצאו עסקאות באזור הזה
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Prefer closest-parcel inference (works for both address and block_plot
|
||
// searches); fall back to parsing the resolved autocomplete text.
|
||
const inferredStreet =
|
||
streetByClosestParcel(deals, searchedBlock, searchedPlot) ??
|
||
streetFromAddress(resolvedAddress);
|
||
|
||
return (
|
||
<div className="overflow-x-auto rounded-md border">
|
||
<table dir="rtl" className="w-full text-sm">
|
||
<thead className="bg-muted text-muted-foreground">
|
||
<tr>
|
||
<th className="p-3 text-start font-medium">תאריך</th>
|
||
<th className="p-3 text-start font-medium">כתובת</th>
|
||
<th className="p-3 text-start font-medium">סוג</th>
|
||
<th className="p-3 text-center font-medium">חדרים</th>
|
||
<th className="p-3 text-center font-medium">קומה</th>
|
||
<th className="p-3 text-center font-medium">שטח (מ"ר)</th>
|
||
<th className="p-3 text-center font-medium">מחיר</th>
|
||
<th className="p-3 text-center font-medium">מחיר/מ"ר</th>
|
||
<th className="p-3 text-center font-medium">גוש</th>
|
||
<th className="p-3 text-center font-medium">חלקה</th>
|
||
<th className="p-3 text-center font-medium">תת-חלקה</th>
|
||
<th className="p-3 text-start font-medium">קרבה</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{deals.map((d) => {
|
||
const street = d.streetNameHeb || "";
|
||
const houseNum = d.houseNum ?? "";
|
||
const settlement = d.settlementNameHeb || "";
|
||
const address = [street, houseNum, settlement].filter(Boolean).join(" ");
|
||
return (
|
||
<tr key={d.id} className="border-t hover:bg-accent/40">
|
||
<td className="p-3 whitespace-nowrap">{formatDate(d.deal_date)}</td>
|
||
<td className="p-3">{address || "—"}</td>
|
||
<td className="p-3">{d.property_type_description || "—"}</td>
|
||
<td className="p-3 text-center tabular-nums">{d.rooms ?? "—"}</td>
|
||
<td className="p-3 text-center tabular-nums">{d.floor ?? "—"}</td>
|
||
<td className="p-3 text-center tabular-nums">{formatNumber(d.asset_area)}</td>
|
||
<td className="p-3 text-center tabular-nums">
|
||
{formatCurrencyILS(d.deal_amount)}
|
||
</td>
|
||
<td className="p-3 text-center tabular-nums">
|
||
{formatCurrencyILS(d.price_per_sqm)}
|
||
</td>
|
||
<td className="p-3 text-center tabular-nums">{d.gushNum ?? "—"}</td>
|
||
<td className="p-3 text-center tabular-nums">{d.parcelNum ?? "—"}</td>
|
||
<td className="p-3 text-center tabular-nums">{d.subParcelNum ?? "—"}</td>
|
||
<td className="p-3">
|
||
{d.deal_source ? (
|
||
<span
|
||
className={
|
||
"inline-block rounded-full px-2 py-0.5 text-xs " +
|
||
proximityClass(d, inferredStreet)
|
||
}
|
||
>
|
||
{proximityLabel(d, inferredStreet)}
|
||
</span>
|
||
) : (
|
||
"—"
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|