Files
nadlan-mcp/web/src/components/DealsTable.tsx
T
chaim 85df33a537 feat(appraisals): tiered nearby search; center-align numeric columns
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>
2026-04-25 16:49:03 +00:00

146 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}